update
This commit is contained in:
@@ -5,6 +5,17 @@ use crate::domain::{
|
||||
use crate::infrastructure::sqlite::layout_repository;
|
||||
use rusqlite::Connection;
|
||||
|
||||
/// Mapeia um erro rusqlite para `ErroLayout`, distinguindo conflito de nome
|
||||
/// de erros genéricos de banco.
|
||||
fn mapear_erro_banco(e: rusqlite::Error, nome: &str) -> ErroLayout {
|
||||
let msg = e.to_string();
|
||||
if msg.contains("UNIQUE constraint failed") {
|
||||
ErroLayout::NomeConflitante(nome.to_string())
|
||||
} else {
|
||||
ErroLayout::ErroBanco(msg)
|
||||
}
|
||||
}
|
||||
|
||||
/// Salva um layout no banco de dados.
|
||||
/// Se o layout já tem um id, atualiza. Caso contrário, insere.
|
||||
/// Retorna `ErroLayout::NomeConflitante` se já existir um layout com o mesmo nome.
|
||||
@@ -14,19 +25,41 @@ pub fn salvar_layout(conn: &Connection, layout: &Layout) -> Result<i64, ErroLayo
|
||||
return Err(ErroLayout::CampoObrigatorioAusente("nome".to_string()));
|
||||
}
|
||||
|
||||
// Validar campos XLSX obrigatórios
|
||||
if let Layout::Xlsx { config, .. } = layout {
|
||||
if config.pos_numero.trim().is_empty() {
|
||||
return Err(ErroLayout::CampoObrigatorioAusente(
|
||||
"pos_numero".to_string(),
|
||||
));
|
||||
}
|
||||
if config.pos_serie.trim().is_empty() {
|
||||
return Err(ErroLayout::CampoObrigatorioAusente("pos_serie".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(id) = layout.id() {
|
||||
// Verificar conflito de nome com outro layout (excluindo o próprio)
|
||||
layout_repository::existe_nome_excluindo_id(conn, layout.nome(), id)
|
||||
.map_err(|e| ErroLayout::ErroBanco(e.to_string()))
|
||||
.and_then(|conflito| {
|
||||
if conflito {
|
||||
Err(ErroLayout::NomeConflitante(layout.nome().to_string()))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})?;
|
||||
|
||||
layout_repository::atualizar(conn, layout)
|
||||
.map_err(|e| ErroLayout::JsonMalformado(e.to_string()))?;
|
||||
.map_err(|e| mapear_erro_banco(e, layout.nome()))?;
|
||||
Ok(id)
|
||||
} else {
|
||||
let nome = layout.nome().to_string();
|
||||
let existe = layout_repository::existe_nome(conn, &nome)
|
||||
.map_err(|e| ErroLayout::JsonMalformado(e.to_string()))?;
|
||||
.map_err(|e| ErroLayout::ErroBanco(e.to_string()))?;
|
||||
if existe {
|
||||
return Err(ErroLayout::NomeConflitante(nome));
|
||||
}
|
||||
layout_repository::salvar(conn, layout)
|
||||
.map_err(|e| ErroLayout::JsonMalformado(e.to_string()))
|
||||
layout_repository::salvar(conn, layout).map_err(|e| mapear_erro_banco(e, layout.nome()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,13 +112,13 @@ pub fn importar_layout_json(
|
||||
// Verificar conflito de nome
|
||||
let nome_atual = layout.nome().to_string();
|
||||
let existe = layout_repository::existe_nome(conn, &nome_atual)
|
||||
.map_err(|e| ErroLayout::JsonMalformado(e.to_string()))?;
|
||||
.map_err(|e| ErroLayout::ErroBanco(e.to_string()))?;
|
||||
|
||||
if existe {
|
||||
if sobrescrever_se_existir {
|
||||
// Buscar o id existente para sobrescrever
|
||||
let layouts_existentes = layout_repository::listar(conn)
|
||||
.map_err(|e| ErroLayout::JsonMalformado(e.to_string()))?;
|
||||
.map_err(|e| ErroLayout::ErroBanco(e.to_string()))?;
|
||||
|
||||
let id_existente = layouts_existentes
|
||||
.iter()
|
||||
@@ -98,7 +131,7 @@ pub fn importar_layout_json(
|
||||
Layout::Xlsx { id: i, .. } => *i = Some(id),
|
||||
}
|
||||
layout_repository::atualizar(conn, &layout)
|
||||
.map_err(|e| ErroLayout::JsonMalformado(e.to_string()))?;
|
||||
.map_err(|e| mapear_erro_banco(e, &nome_atual))?;
|
||||
return Ok(id);
|
||||
}
|
||||
} else {
|
||||
@@ -107,5 +140,5 @@ pub fn importar_layout_json(
|
||||
}
|
||||
|
||||
// Inserir novo
|
||||
layout_repository::salvar(conn, &layout).map_err(|e| ErroLayout::JsonMalformado(e.to_string()))
|
||||
layout_repository::salvar(conn, &layout).map_err(|e| mapear_erro_banco(e, &nome_atual))
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ pub enum ErroLayout {
|
||||
JsonMalformado(String),
|
||||
#[error("Conflito de nome: layout '{0}' já existe")]
|
||||
NomeConflitante(String),
|
||||
#[error("Erro no banco de dados: {0}")]
|
||||
ErroBanco(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error, Clone)]
|
||||
|
||||
@@ -62,6 +62,8 @@ Delega para `abrir_banco_no_caminho()` com o caminho padrão. Retorna
|
||||
`config.db.bak` e segue para a criação de um banco novo.
|
||||
4. Cria um banco vazio e retorna com flag `true` (banco foi recriado).
|
||||
|
||||
> **Nota:** `SELECT 1` não acessa páginas de dados do SQLite e não detecta corrupção real. Um banco com tabelas ou índices corrompidos passaria nessa verificação. `PRAGMA quick_check` seria mais confiável (ver `docs/FIX_SALVAR_LAYOUT.md`, Problema 8).
|
||||
|
||||
---
|
||||
|
||||
## migrations.rs
|
||||
@@ -88,7 +90,11 @@ Fluxo:
|
||||
- `versao_atual < 1` → `migration_v1`
|
||||
- `versao_atual < 2` → `migration_v2`
|
||||
- `versao_atual < 3` → `migration_v3`
|
||||
4. Persiste a nova versão (`INSERT` se era `0`, `UPDATE` caso contrário).
|
||||
4. Persiste a nova versão na tabela `schema_version`:
|
||||
- `versao_atual == 0` → `INSERT INTO schema_version (versao) VALUES (3)`
|
||||
- `versao_atual > 0 && versao_atual < 3` → `UPDATE schema_version SET versao = 3`
|
||||
|
||||
> **Atenção:** A atualização de `schema_version` ocorre **fora** das migrations individuais, após todas terem sido executadas. Isso significa que uma falha parcial entre migrations pode causar re-execução problemática na próxima inicialização (ver `docs/FIX_SALVAR_LAYOUT.md`, Problemas 1 e 2).
|
||||
|
||||
### Histórico de migrations
|
||||
|
||||
@@ -149,8 +155,10 @@ Insere um novo layout e retorna o `rowid` gerado.
|
||||
Atualiza um layout existente pelo `id` embutido na variante. Retorna erro se
|
||||
`id` for `None`.
|
||||
|
||||
- `Layout::Csv` → atualiza apenas as colunas CSV.
|
||||
- `Layout::Xlsx` → atualiza apenas as colunas XLSX.
|
||||
- `Layout::Csv` → atualiza apenas as colunas CSV; colunas XLSX ficam inalteradas no banco.
|
||||
- `Layout::Xlsx` → atualiza apenas as colunas XLSX; colunas CSV ficam inalteradas no banco.
|
||||
|
||||
> **Atenção:** O UPDATE não zera os campos do tipo oposto. Se um layout mudar de tipo (ex: via importação JSON com mesmo nome), os campos do tipo anterior persistem no banco (ver `docs/FIX_SALVAR_LAYOUT.md`, Problema 9).
|
||||
|
||||
#### `listar(conn) -> Result<Vec<Layout>>`
|
||||
|
||||
@@ -161,6 +169,8 @@ Seleciona todos os layouts ordenados por `nome ASC`. Para cada linha:
|
||||
|
||||
Campos opcionais (`Option<usize>`) são lidos como `Option<i64>` e convertidos.
|
||||
|
||||
> **Atenção:** O mapeamento usa índices posicionais (`row.get(0)`, `row.get(1)`, etc.) e não nomes de colunas. Qualquer reordenação no `SELECT` quebra silenciosamente o resultado (ver `docs/FIX_SALVAR_LAYOUT.md`, Problema 6).
|
||||
|
||||
#### `excluir(conn, id) -> Result<()>`
|
||||
|
||||
Remove o registro com o `id` informado via `DELETE`.
|
||||
@@ -187,7 +197,7 @@ aplicar_migrations(conn)
|
||||
├─ cria schema_version se necessário
|
||||
├─ lê versão atual
|
||||
├─ executa migrations pendentes (v1 → v2 → v3)
|
||||
└─ grava versão final
|
||||
└─ grava versão final (INSERT se versao=0, UPDATE se versao<3)
|
||||
```
|
||||
|
||||
Após esse fluxo, a conexão está pronta para uso pelo `layout_repository`.
|
||||
|
||||
@@ -28,8 +28,8 @@ pub fn abrir_banco_no_caminho(caminho: &Path) -> Result<(Connection, bool), Stri
|
||||
if caminho.exists() {
|
||||
match Connection::open(caminho) {
|
||||
Ok(conn) => {
|
||||
// Testar se o banco funciona com uma query simples
|
||||
match conn.execute_batch("SELECT 1;") {
|
||||
// Testar integridade estrutural do banco
|
||||
match conn.execute_batch("PRAGMA quick_check;") {
|
||||
Ok(_) => return Ok((conn, false)),
|
||||
Err(_e) => {
|
||||
// Banco corrompido
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
use crate::domain::entities::layout::{Layout, LayoutCsv, LayoutXlsx};
|
||||
use rusqlite::{params, Connection, Result};
|
||||
|
||||
/// Converte o char delimitador para string legível no banco.
|
||||
fn delim_para_str(c: char) -> String {
|
||||
match c {
|
||||
'\t' => "tab".to_string(),
|
||||
c => c.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converte a string armazenada no banco de volta para char delimitador.
|
||||
fn str_para_delim(s: &str) -> char {
|
||||
match s {
|
||||
"tab" => '\t',
|
||||
s => s.chars().next().unwrap_or(';'),
|
||||
}
|
||||
}
|
||||
|
||||
/// Salva um layout no banco. Retorna o id gerado.
|
||||
pub fn salvar(conn: &Connection, layout: &Layout) -> Result<i64> {
|
||||
match layout {
|
||||
@@ -13,7 +29,7 @@ pub fn salvar(conn: &Connection, layout: &Layout) -> Result<i64> {
|
||||
VALUES (?1, 'csv', ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
|
||||
params![
|
||||
nome,
|
||||
config.delimitador.to_string(),
|
||||
delim_para_str(config.delimitador),
|
||||
config.encoding,
|
||||
config.linha_cabecalho as i64,
|
||||
config.indice_numero as i64,
|
||||
@@ -47,6 +63,7 @@ pub fn salvar(conn: &Connection, layout: &Layout) -> Result<i64> {
|
||||
}
|
||||
|
||||
/// Atualiza um layout existente no banco.
|
||||
/// Zera explicitamente os campos do tipo oposto para evitar dados órfãos.
|
||||
pub fn atualizar(conn: &Connection, layout: &Layout) -> Result<()> {
|
||||
let id = layout
|
||||
.id()
|
||||
@@ -58,11 +75,13 @@ pub fn atualizar(conn: &Connection, layout: &Layout) -> Result<()> {
|
||||
"UPDATE layouts SET
|
||||
nome = ?1, delimitador = ?2, encoding = ?3,
|
||||
linha_cabecalho = ?4, indice_numero = ?5, indice_serie = ?6,
|
||||
indice_valor = ?7, indice_data = ?8, indice_documento_tipo = ?9
|
||||
indice_valor = ?7, indice_data = ?8, indice_documento_tipo = ?9,
|
||||
aba = NULL, pos_numero = NULL, pos_serie = NULL,
|
||||
pos_valor = NULL, pos_data = NULL, pos_documento_tipo = NULL
|
||||
WHERE id = ?10",
|
||||
params![
|
||||
nome,
|
||||
config.delimitador.to_string(),
|
||||
delim_para_str(config.delimitador),
|
||||
config.encoding,
|
||||
config.linha_cabecalho as i64,
|
||||
config.indice_numero as i64,
|
||||
@@ -78,7 +97,10 @@ pub fn atualizar(conn: &Connection, layout: &Layout) -> Result<()> {
|
||||
conn.execute(
|
||||
"UPDATE layouts SET
|
||||
nome = ?1, aba = ?2, pos_numero = ?3, pos_serie = ?4,
|
||||
pos_valor = ?5, pos_data = ?6, pos_documento_tipo = ?7
|
||||
pos_valor = ?5, pos_data = ?6, pos_documento_tipo = ?7,
|
||||
delimitador = NULL, encoding = NULL, linha_cabecalho = NULL,
|
||||
indice_numero = NULL, indice_serie = NULL, indice_valor = NULL,
|
||||
indice_data = NULL, indice_documento_tipo = NULL
|
||||
WHERE id = ?8",
|
||||
params![
|
||||
nome,
|
||||
@@ -109,25 +131,31 @@ pub fn listar(conn: &Connection) -> Result<Vec<Layout>> {
|
||||
|
||||
let layouts: Result<Vec<Layout>> = stmt
|
||||
.query_map([], |row| {
|
||||
let id: i64 = row.get(0)?;
|
||||
let nome: String = row.get(1)?;
|
||||
let tipo: String = row.get(2)?;
|
||||
let id: i64 = row.get("id")?;
|
||||
let nome: String = row.get("nome")?;
|
||||
let tipo: String = row.get("tipo")?;
|
||||
|
||||
if tipo == "csv" {
|
||||
let delim_str: String = row.get(3)?;
|
||||
let delimitador = delim_str.chars().next().unwrap_or(';');
|
||||
let delim_str: String = row.get("delimitador")?;
|
||||
let delimitador = str_para_delim(&delim_str);
|
||||
Ok(Layout::Csv {
|
||||
id: Some(id),
|
||||
nome,
|
||||
config: LayoutCsv {
|
||||
delimitador,
|
||||
encoding: row.get(4)?,
|
||||
linha_cabecalho: row.get::<_, i64>(5)? as usize,
|
||||
indice_numero: row.get::<_, i64>(6)? as usize,
|
||||
indice_serie: row.get::<_, i64>(7)? as usize,
|
||||
indice_valor: row.get::<_, Option<i64>>(8)?.map(|v| v as usize),
|
||||
indice_data: row.get::<_, Option<i64>>(9)?.map(|v| v as usize),
|
||||
indice_documento_tipo: row.get::<_, Option<i64>>(15)?.map(|v| v as usize),
|
||||
encoding: row.get("encoding")?,
|
||||
linha_cabecalho: row.get::<_, i64>("linha_cabecalho")? as usize,
|
||||
indice_numero: row.get::<_, i64>("indice_numero")? as usize,
|
||||
indice_serie: row.get::<_, i64>("indice_serie")? as usize,
|
||||
indice_valor: row
|
||||
.get::<_, Option<i64>>("indice_valor")?
|
||||
.map(|v| v as usize),
|
||||
indice_data: row
|
||||
.get::<_, Option<i64>>("indice_data")?
|
||||
.map(|v| v as usize),
|
||||
indice_documento_tipo: row
|
||||
.get::<_, Option<i64>>("indice_documento_tipo")?
|
||||
.map(|v| v as usize),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
@@ -135,12 +163,12 @@ pub fn listar(conn: &Connection) -> Result<Vec<Layout>> {
|
||||
id: Some(id),
|
||||
nome,
|
||||
config: LayoutXlsx {
|
||||
aba: row.get(10)?,
|
||||
pos_numero: row.get(11)?,
|
||||
pos_serie: row.get(12)?,
|
||||
pos_valor: row.get(13)?,
|
||||
pos_data: row.get(14)?,
|
||||
pos_documento_tipo: row.get(16)?,
|
||||
aba: row.get("aba")?,
|
||||
pos_numero: row.get("pos_numero")?,
|
||||
pos_serie: row.get("pos_serie")?,
|
||||
pos_valor: row.get("pos_valor")?,
|
||||
pos_data: row.get("pos_data")?,
|
||||
pos_documento_tipo: row.get("pos_documento_tipo")?,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -165,3 +193,14 @@ pub fn existe_nome(conn: &Connection, nome: &str) -> Result<bool> {
|
||||
)?;
|
||||
Ok(count > 0)
|
||||
}
|
||||
|
||||
/// Verifica se existe um layout com o nome fornecido, excluindo o registro com o id dado.
|
||||
/// Usado para validar conflito de nome ao renomear um layout existente.
|
||||
pub fn existe_nome_excluindo_id(conn: &Connection, nome: &str, id: i64) -> Result<bool> {
|
||||
let count: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM layouts WHERE nome = ?1 AND id != ?2",
|
||||
params![nome, id],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
Ok(count > 0)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
use rusqlite::{Connection, Result};
|
||||
|
||||
/// Versão atual do schema do banco de dados.
|
||||
const VERSAO_SCHEMA_ATUAL: i64 = 3;
|
||||
|
||||
/// Aplica todas as migrations necessárias para atualizar o banco
|
||||
/// para a versão mais recente.
|
||||
pub fn aplicar_migrations(conn: &Connection) -> Result<()> {
|
||||
@@ -29,26 +26,17 @@ pub fn aplicar_migrations(conn: &Connection) -> Result<()> {
|
||||
migration_v3(conn)?;
|
||||
}
|
||||
|
||||
if versao_atual == 0 {
|
||||
conn.execute(
|
||||
"INSERT INTO schema_version (versao) VALUES (?1);",
|
||||
[VERSAO_SCHEMA_ATUAL],
|
||||
)?;
|
||||
} else if versao_atual < VERSAO_SCHEMA_ATUAL {
|
||||
conn.execute(
|
||||
"UPDATE schema_version SET versao = ?1;",
|
||||
[VERSAO_SCHEMA_ATUAL],
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Migration v3: adicionar colunas de tipo de documento nos layouts.
|
||||
fn migration_v3(conn: &Connection) -> Result<()> {
|
||||
conn.execute_batch(
|
||||
"ALTER TABLE layouts ADD COLUMN indice_documento_tipo INTEGER;
|
||||
ALTER TABLE layouts ADD COLUMN pos_documento_tipo TEXT;",
|
||||
"BEGIN;
|
||||
ALTER TABLE layouts ADD COLUMN indice_documento_tipo INTEGER;
|
||||
ALTER TABLE layouts ADD COLUMN pos_documento_tipo TEXT;
|
||||
UPDATE schema_version SET versao = 3;
|
||||
COMMIT;",
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -59,12 +47,15 @@ fn migration_v3(conn: &Connection) -> Result<()> {
|
||||
/// em bancos que já possuem nomes repetidos.
|
||||
fn migration_v2(conn: &Connection) -> Result<()> {
|
||||
conn.execute_batch(
|
||||
"UPDATE layouts
|
||||
SET nome = nome || ' (' || id || ')'
|
||||
WHERE id NOT IN (
|
||||
SELECT MIN(id) FROM layouts GROUP BY nome
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_layouts_nome ON layouts (nome);",
|
||||
"BEGIN;
|
||||
UPDATE layouts
|
||||
SET nome = nome || ' (' || id || ')'
|
||||
WHERE id NOT IN (
|
||||
SELECT MIN(id) FROM layouts GROUP BY nome
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_layouts_nome ON layouts (nome);
|
||||
UPDATE schema_version SET versao = 2;
|
||||
COMMIT;",
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -72,27 +63,30 @@ fn migration_v2(conn: &Connection) -> Result<()> {
|
||||
/// Migration v1: criar tabela de layouts.
|
||||
fn migration_v1(conn: &Connection) -> Result<()> {
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS layouts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
nome TEXT NOT NULL,
|
||||
tipo TEXT NOT NULL CHECK(tipo IN ('csv', 'xlsx')),
|
||||
"BEGIN;
|
||||
CREATE TABLE IF NOT EXISTS layouts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
nome TEXT NOT NULL,
|
||||
tipo TEXT NOT NULL CHECK(tipo IN ('csv', 'xlsx')),
|
||||
|
||||
-- Campos CSV
|
||||
delimitador TEXT,
|
||||
encoding TEXT,
|
||||
linha_cabecalho INTEGER,
|
||||
indice_numero INTEGER,
|
||||
indice_serie INTEGER,
|
||||
indice_valor INTEGER,
|
||||
indice_data INTEGER,
|
||||
-- Campos CSV
|
||||
delimitador TEXT,
|
||||
encoding TEXT,
|
||||
linha_cabecalho INTEGER,
|
||||
indice_numero INTEGER,
|
||||
indice_serie INTEGER,
|
||||
indice_valor INTEGER,
|
||||
indice_data INTEGER,
|
||||
|
||||
-- Campos XLSX
|
||||
aba TEXT,
|
||||
pos_numero TEXT,
|
||||
pos_serie TEXT,
|
||||
pos_valor TEXT,
|
||||
pos_data TEXT
|
||||
);",
|
||||
-- Campos XLSX
|
||||
aba TEXT,
|
||||
pos_numero TEXT,
|
||||
pos_serie TEXT,
|
||||
pos_valor TEXT,
|
||||
pos_data TEXT
|
||||
);
|
||||
INSERT INTO schema_version (versao) VALUES (1);
|
||||
COMMIT;",
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user