Fix SalvarLayoutConfig
This commit is contained in:
@@ -1,353 +0,0 @@
|
||||
# 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<String>`), 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<i64>) -> Result<bool> {
|
||||
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.
|
||||
Reference in New Issue
Block a user