Pular para conteúdo

Etapa 02 — Idempotência: UPSERT diff-aware

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-06-15

Estende a base da etapa 01: aqui o schema dos fatos vira contrato de UPSERT (chave natural + métricas). A preservação das colunas veiculares é a etapa 04.

1. Contexto e escopo

Os fatos (bi_faturamento, bi_movimento) sincronizam para clientes móveis via PowerSync. Reprocessar um período (reupload do mesmo mês) não pode gerar churn de checkpoint: cada linha tocada no Postgres re-propaga para os celulares, gastando banda e bateria mesmo sem mudança real.

Dentro do escopo: chaves naturais por fato; pipeline diff-aware (COPY → TEMP TABLE → UPSERT → DELETE seletivo); métricas finas por upload; preservação de id/xmin nas linhas inalteradas.

Fora do escopo: preservação das colunas de cadastro veicular (regra especial) → etapa 04.

2. Modelo de dados

A identidade de cada fato é uma UNIQUE composta NULLS NOT DISTINCT sobre as colunas do ERP que o definem — a chave natural (criadas em V6__chaves_naturais.sql). As PKs são BIGSERIAL (decisão 0005); xmin (system column do Postgres) é o que o PowerSync observa para propagar mudanças.

erDiagram
    bi_faturamento {
        bigserial id PK
        date dt_frete "idx"
        varchar placa "idx"
        decimal valor
    }
    bi_movimento {
        bigserial id PK
        date data_lcto "idx"
        decimal valor
        smallint seq_dentro_grupo "desambiguador (V6)"
    }

Chave natural — bi_faturamento (uq_bi_faturamento_natural, 10 colunas): nro_frete, dt_frete, nro_docto, dt_em, placa, segmento, seq_proj, cod_proj, espec_veic, cod_mot. A V6 fez dedup destrutivo prévio mantendo MIN(id) por grupo (duplicatas no faturamento eram ruído).

Chave natural — bi_movimento (uq_bi_movimento_natural, 17 colunas): as 16 contábeis (classif_ctb, ident_ger, ident_custo, atrib_custo, ident_rat, db_cr, placa, segmento, seq_proj, cod_proj, filial, data_lcto, espec_veic, cod_conta, classif_conta, valor) + seq_dentro_grupo (SMALLINT, NOT NULL, backfill ROW_NUMBER em V6). O desambiguador existe porque colisões nas 16 colunas são lançamentos legítimos (mesmo custo repetido no período), não duplicatas — por isso aqui não houve dedup destrutivo.

Métricas por upload (8 colunas em xls_processamento, V7): para cada fato, *_inseridas (INSERT puro, xmax=0), *_atualizadas (UPDATE in-place, xmax≠0), *_inalteradas (enviadas − inseridas − atualizadas), *_removidas (DELETE seletivo). NULL = upload anterior à spec de métricas.

3. Fluxos principais

Pipeline diff-aware na ordem UPSERT → DELETE, por período, dentro de uma transação (decisão 0007 — COPY + TEMP TABLE no BulkUpserter):

flowchart TD
    A[Lote do upload] --> B[COPY → TEMP TABLE]
    B --> C["INSERT ... ON CONFLICT (chave natural)<br/>DO UPDATE WHERE ... IS DISTINCT FROM"]
    C --> D{Linha mudou?}
    D -- sim --> E[UPDATE: novo xmin → PowerSync propaga]
    D -- não --> F[no-op: id e xmin preservados]
    C --> G["DELETE seletivo: linhas do período<br/>ausentes no upload (usa idx data)"]
  • O WHERE ... IS DISTINCT FROM garante que só linhas realmente alteradas trocam de xmin — as inalteradas não re-propagam (zero churn no reupload).
  • O DELETE seletivo remove só o que sumiu dentro do período do upload (apoiado no índice por data), nunca o período inteiro nem dados de outros meses.
  • O ProcessamentoTransactionalHelper.mesclarDadosDoPeriodo orquestra UPSERT + DELETE e devolve o MesclaResumo com as 8 métricas, gravadas em xls_processamento no sucesso.

4. Decisões

Nº Decisão
0005 BIGSERIAL como PK nas bi_*
0006 Chaves naturais + UPSERT diff-aware
0007 COPY + TEMP TABLE no BulkUpserter
0016 Período do DELETE vem da aba 10

5. Qualidade e observabilidade

  • Testes BulkUpserterFaturamentoTest / BulkUpserterMovimentoTest: reupload sem mudança não bumpa xmin (regressão de churn).
  • As 8 métricas por upload (visíveis no detalhe e na API) tornam o efeito de cada processamento auditável: quantas linhas de fato mudaram.

6. Riscos

  • Chave natural mal definida → colisão de linhas legítimas; mitigado pelo seq_dentro_grupo em bi_movimento.
  • UPSERT cego (sem IS DISTINCT FROM) reintroduziria churn → coberto pelos testes de regressão acima.
  • GRANT faltando ao powersync_role numa bi_* nova → replicação para (42501); ver decisão 0013 e o gotcha no CLAUDE.md.