Files
comparador-notas/FIX_SALVAR_LAYOUT.md
T
2026-03-04 17:27:02 -03:00

13 KiB

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

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:

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:

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

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:
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)
}
  1. No use case, inspecionar o erro antes de mapear:
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)
        }
    })?;
  1. 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:

// 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:

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):

// 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):

// 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:

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

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

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:

-- 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
-- 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.