Pular para conteúdo

0017 — Compras sem chave natural: surrogate PK + replace-por-período

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-07-29 · Decidido em: 2026-07-29

Contexto

O processador de Compras (spec 008, migração V16) grava o fato bi_compra (item de NF de compra combustível). Todo o resto do repo usa o padrão diff-aware: INSERT … ON CONFLICT (chave_natural) DO UPDATE … WHERE IS DISTINCT FROM + DELETE seletivo, que preserva id/xmin em linhas inalteradas e evita checkpoints PowerSync desnecessários (spec 003).

Esse padrão exige uma chave natural estável. Em bi_compra a candidata óbvia (cnpj, numero_nota, produto) colide em dados reais: no arquivo real, as NFs 802962/1, 803823/1 e 806243/1 aparecem 2× cada (mesmo fornecedor, mesma nota, mesmo produto, quantidades/valores diferentes — item de nota fracionado). O arquivo do ERP não traz discriminador de linha. Sem chave natural única não há ON CONFLICT.

Decisão

bi_compra usa PK surrogate uuidv7() e nenhuma UNIQUE de linha. A idempotência/correção fica no replace-por-período: numa transação, DELETE FROM bi_compra WHERE data_nf BETWEEN <min> AND <max> (intervalo de Data NF do arquivo) seguido de INSERT de todas as linhas combustível (CompraRepository). O upsert de bi_fornecedor (chave natural cnpj limpa, mantém diff-aware) roda antes, para satisfazer a FK cnpj_fornecedor → bi_fornecedor(cnpj).

O skip do reenvio idêntico continua vindo do checksum SHA-256 do xls_processamento (mecanismo type-agnostic já existente), então o DELETE+INSERT — que recicla id do período — só dispara em arquivo corrigido, não no reenvio igual.

Consequências

  • Diverge conscientemente do diff-aware do resto do repo; é a exceção documentada (ver a cobertura de DELETE no cap. 4 do livro). bi_fornecedor não diverge.
  • Premissa: um arquivo é o conjunto completo do seu período. Se um mês vier partido em 2 arquivos, o 2º apaga o 1º (sem chave natural não dá DELETE … AND NOT EXISTS por linha). A confirmar com quem exporta o ERP; a v1 assume completo.
  • Correção de período recicla id UUID das linhas daquele período → um checkpoint PowerSync no período. Aceito: só ocorre em arquivo corrigido (o checksum barra o reenvio idêntico), e não há discriminador de linha para fazer melhor.
  • Contrato com o cliente Flutter (bi-comercial, painel 015) e sync rules (../powersync/) já assumem esse schema — colunas alinhadas.

Alternativas consideradas

  • Chave natural (cnpj, numero_nota, produto) + diff-aware: descartada — colide em dados reais (3 casos no arquivo de amostra), quebraria o ON CONFLICT.
  • Adicionar um índice de linha sintético (ex. ordinal na NF) à chave: o arquivo não fornece ordinal estável entre reuploads; um contador de parse não é determinístico entre exportações. Descartado — daria falsa estabilidade.
  • Hash da linha inteira como chave: duas linhas idênticas de propósito (item fracionado com mesmos valores) colidiriam de novo; e qualquer mudança de valor criaria linha nova (churn). Descartado.

Correção 2026-09-14: o ponteiro para o CLAUDE.md/HOWTO-DEPLOY.md passou para a página dona do fato; os dois arquivos deixaram de carregar esse conteúdo.