# FIX — Salvar Layout no Banco de Dados **Data:** 04/03/2026 **Versão do PRD:** 1.7 **Status:** Pendente de Implementação --- ## Sumário dos Problemas | # | Severidade | Arquivo(s) | Descrição | |---|-----------|------------|-----------| | 1 | **Crítico** | `migrations.rs` | `migration_v3` sem transação — falha parcial deixa banco permanentemente quebrado | | 2 | **Crítico** | `migrations.rs` | `schema_version` atualizada fora da transação das migrations | | 3 | **Crítico** | `usecases/layouts.rs` | Erros SQL mapeados como `ErroLayout::JsonMalformado` — mensagem completamente enganosa | | 4 | Moderado | `layout_repository.rs` | `delimitador` TAB salvo como byte de controle invisível no banco | | 5 | Moderado | `usecases/layouts.rs` | `pos_numero` / `pos_serie` XLSX podem ser strings vazias — sem validação no fluxo de save da UI | | 6 | Moderado | `layout_repository.rs` | Índices posicionais hardcoded em `row.get(N)` — quebra silenciosa se colunas do SELECT forem reordenadas | | 7 | Menor | `usecases/layouts.rs` + `layout_repository.rs` | Renomear layout para nome já existente (UPDATE) gera `JsonMalformado` em vez de `NomeConflitante` | | 8 | Menor | `connection.rs` | `SELECT 1` não detecta corrupção real de páginas SQLite | | 9 | Menor | `layout_repository.rs` | UPDATE não zera campos do tipo oposto ao tipo atual do layout | --- ## Problema 1 (Crítico) — `migration_v3` sem transação atômica ### Local `src/infrastructure/sqlite/migrations.rs` — função `migration_v3` ### Situação Atual ```rust 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;", )?; Ok(()) } ``` `execute_batch` executa os dois `ALTER TABLE` sem transação explícita. Se o processo for interrompido após o primeiro e antes do segundo, o banco fica em estado parcial: - `indice_documento_tipo` existe, `pos_documento_tipo` não existe. - A versão no banco não é atualizada (o erro propaga antes). - Na próxima execução, `migration_v3` tenta adicionar `indice_documento_tipo` novamente → SQLite retorna `"duplicate column name"` → **o app nunca mais inicializa sem intervenção manual**. ### Correção Necessária Envolver cada migration em uma transação explícita e atualizar `schema_version` **dentro da mesma transação**: ```rust fn migration_v3(conn: &Connection) -> Result<()> { conn.execute_batch(" 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(()) } ``` > **Nota:** O SQLite suporta `ALTER TABLE` dentro de transação explícita desde a versão 3.x. `execute_batch` executa múltiplos statements quando delimitados por `;` dentro do mesmo bloco `BEGIN/COMMIT`. --- ## Problema 2 (Crítico) — `schema_version` atualizada fora da transação das migrations ### Local `src/infrastructure/sqlite/migrations.rs` — função `aplicar_migrations` ### Situação Atual O fluxo atual é: 1. Executa `migration_v1(conn)?` 2. Executa `migration_v2(conn)?` 3. Executa `migration_v3(conn)?` 4. **Depois** executa `INSERT OR REPLACE INTO schema_version ...` Se qualquer migration falhar após outras já terem sido aplicadas, a versão não é atualizada, causando re-execução problemática na próxima abertura. ### Correção Necessária Cada migration deve atualizar `schema_version` internamente (dentro de sua própria transação), como mostrado no Problema 1. A função `aplicar_migrations` não deve mais atualizar a versão centralmente — ela apenas chama as migrations que ainda não foram aplicadas. Adicionalmente, as migrations v1 e v2 devem seguir o mesmo padrão: ```rust fn migration_v1(conn: &Connection) -> Result<()> { conn.execute_batch(" BEGIN; CREATE TABLE IF NOT EXISTS layouts ( ... ); INSERT OR REPLACE INTO schema_version (id, versao) VALUES (1, 1); COMMIT; ")?; Ok(()) } fn migration_v2(conn: &Connection) -> Result<()> { conn.execute_batch(" BEGIN; CREATE UNIQUE INDEX IF NOT EXISTS idx_layouts_nome ON layouts (nome); -- renomeia duplicatas existentes se houver UPDATE schema_version SET versao = 2; COMMIT; ")?; Ok(()) } ``` --- ## Problema 3 (Crítico) — Erros SQL mapeados como `ErroLayout::JsonMalformado` ### Local `src/application/usecases/layouts.rs` — função `salvar_layout` e `atualizar_layout` ### Situação Atual ```rust layout_repository::salvar(conn, layout) .map_err(|e| ErroLayout::JsonMalformado(e.to_string()))?; layout_repository::atualizar(conn, layout) .map_err(|e| ErroLayout::JsonMalformado(e.to_string()))?; ``` Qualquer erro do banco de dados (violação de constraint, coluna ausente, banco travado) é apresentado ao usuário como **"JSON malformado"**, o que é completamente incorreto. Exemplos de mensagens enganosas que o usuário veria: - `"UNIQUE constraint failed: layouts.nome"` → exibido como "JSON malformado" - `"no such column: pos_documento_tipo"` → exibido como "JSON malformado" - `"attempt to write a readonly database"` → exibido como "JSON malformado" ### Correção Necessária 1. Adicionar variante própria em `domain/errors.rs`: ```rust pub enum ErroLayout { // ... variantes existentes ... ErroBanco(String), // erros genéricos de I/O do banco NomeConflitante, // já existe (para uso tanto em INSERT quanto em UPDATE) } ``` 2. No use case, inspecionar o erro antes de mapear: ```rust layout_repository::salvar(conn, layout) .map_err(|e| { let msg = e.to_string(); if msg.contains("UNIQUE constraint failed") { ErroLayout::NomeConflitante } else { ErroLayout::ErroBanco(msg) } })?; ``` 3. Na UI (`ui/screens/layouts.rs`), tratar `ErroBanco` com mensagem clara ao usuário: `"Erro ao salvar no banco de dados: {mensagem}"`. --- ## Problema 4 (Moderado) — `delimitador` TAB salvo como byte de controle no banco ### Local `src/infrastructure/sqlite/layout_repository.rs` — funções `salvar` e `atualizar` (CSV) ### Situação Atual O `char` `'\t'` é convertido para `String` via `.to_string()`, armazenando um caractere de tabulação literal (byte `0x09`) no campo `TEXT` do banco. Embora funcional, é opaco para inspeção manual do banco e incompatível com exports/backups que não preservam bytes de controle. ### Correção Necessária Normalizar o delimitador para um token legível antes de salvar: ```rust // Ao salvar: let delim_str = match config.delimitador { '\t' => "tab".to_string(), c => c.to_string(), }; // Ao ler: let delimitador = match delim_str.as_str() { "tab" => '\t', s => s.chars().next().unwrap_or(';'), }; ``` > Isso também melhora a legibilidade dos arquivos JSON de export/import de layouts. --- ## Problema 5 (Moderado) — `pos_numero` e `pos_serie` XLSX sem validação no fluxo de save da UI ### Local `src/application/usecases/layouts.rs` — função `salvar_layout` ### Situação Atual A validação atual verifica apenas que `nome` não esteja vazio. Os campos `pos_numero` e `pos_serie` de layouts XLSX são `String` obrigatórias (não `Option`), mas podem ser strings vazias `""`. O banco os aceita sem restrição, e o erro só apareceria na análise, sem indicar que o layout está incompleto. ### Correção Necessária Expandir a validação no use case `salvar_layout`: ```rust match layout { Layout::Csv { config, .. } => { // indice_numero e indice_serie são usize — sempre válidos se presentes // nenhuma validação adicional necessária aqui } Layout::Xlsx { config, .. } => { 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())); } } } ``` --- ## Problema 6 (Moderado) — Índices posicionais hardcoded em `row.get(N)` ### Local `src/infrastructure/sqlite/layout_repository.rs` — função `listar` ### Situação Atual O mapeamento usa índices numéricos (`row.get(0)`, `row.get(1)`, ..., `row.get(16)`). Qualquer reordenação das colunas no `SELECT` quebra silenciosamente o mapeamento sem erro de compilação. ### Correção Necessária Substituir por nomes de colunas usando `row.get::<_, T>(nome_coluna)`: ```rust // Em vez de: let id: i64 = row.get(0)?; let nome: String = row.get(1)?; // Usar: let id: i64 = row.get("id")?; let nome: String = row.get("nome")?; ``` O `rusqlite` suporta `row.get("nome_coluna")` desde a versão 0.26. A versão usada no projeto é 0.32, portanto compatível. --- ## Problema 7 (Menor) — Renomear para nome conflitante em UPDATE gera mensagem errada ### Local `src/application/usecases/layouts.rs` + `src/infrastructure/sqlite/layout_repository.rs` ### Situação Atual No fluxo de UPDATE (re-save de layout existente), não há verificação de conflito de nome antes de chamar `atualizar`. Se o usuário renomear um layout para um nome já em uso, a constraint `UNIQUE` do banco rejeita o UPDATE, o erro é mapeado como `JsonMalformado` (Problema 3 acima). ### Correção Necessária No use case `atualizar_layout`, verificar se o novo nome conflita com outro layout (excluindo o próprio ID): ```rust // Verificar conflito excluindo o próprio registro if layout_repository::existe_nome_excluindo_id(conn, layout.nome(), layout.id())? { return Err(ErroLayout::NomeConflitante); } ``` Adicionar função no repository: ```rust pub fn existe_nome_excluindo_id(conn: &Connection, nome: &str, id: Option) -> Result { match id { Some(id) => { 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) } None => layout_repository::existe_nome(conn, nome), } } ``` --- ## Problema 8 (Menor) — `SELECT 1` não detecta corrupção real do banco ### Local `src/infrastructure/sqlite/connection.rs` ### Situação Atual ```rust match conn.execute_batch("SELECT 1;") { Ok(_) => return Ok((conn, false)), Err(_) => { /* trata como banco corrompido */ } } ``` `SELECT 1` não acessa nenhuma página de dados do SQLite. Um banco com tabelas corrompidas, índices inválidos ou páginas com checksum errado passaria nessa verificação sem ser detectado, causando erros inesperados posteriormente. ### Correção Necessária ```rust match conn.execute_batch("PRAGMA quick_check;") { Ok(_) => return Ok((conn, false)), Err(_) => { /* banco corrompido */ } } ``` `PRAGMA quick_check` verifica a integridade estrutural do banco (sem verificar cada valor de dado, como `integrity_check` faz). É mais rápido que `integrity_check` e muito mais confiável que `SELECT 1`. --- ## Problema 9 (Menor) — UPDATE não zera campos do tipo oposto ### Local `src/infrastructure/sqlite/layout_repository.rs` — função `atualizar` ### Situação Atual O UPDATE de CSV não zera campos XLSX (`aba`, `pos_numero`, etc.) e o UPDATE de XLSX não zera campos CSV. Na prática não ocorre troca de tipo, mas se ocorrer (via importação JSON com mesmo nome e tipo diferente + sobrescrita), os campos do tipo anterior ficam no banco. ### Correção Necessária Adicionar `SET campo = NULL` explícito para os campos do tipo oposto em cada UPDATE: ```sql -- UPDATE CSV: zerar campos XLSX 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, -- zerar campos XLSX: aba = NULL, pos_numero = NULL, pos_serie = NULL, pos_valor = NULL, pos_data = NULL, pos_documento_tipo = NULL WHERE id = ?10 ``` ```sql -- UPDATE XLSX: zerar campos CSV UPDATE layouts SET nome = ?1, aba = ?2, pos_numero = ?3, pos_serie = ?4, pos_valor = ?5, pos_data = ?6, pos_documento_tipo = ?7, -- zerar campos CSV: 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 ``` --- ## Ordem de Implementação Recomendada 1. **Problema 1 + 2** (migrations com transação) — Risco de banco permanentemente inutilizável; implementar primeiro. 2. **Problema 3** (mapeamento de erros) — Sem isso o usuário não entende o que está errado. 3. **Problema 5** (validação de campos XLSX) — Evita layouts inválidos serem salvos. 4. **Problema 7** (conflito de nome em UPDATE) — Depende da solução do Problema 3. 5. **Problema 4** (delimitador TAB) — Melhoria de robustez; não causa crash. 6. **Problema 6** (índices posicionais) — Refactor preventivo. 7. **Problema 8** (PRAGMA quick_check) — Melhoria de confiabilidade. 8. **Problema 9** (zerar campos opostos) — Limpeza defensiva.