Pular para conteúdo

0023 — Segmento denormalizado em bi_produto/bi_movimento (role-gating PowerSync)

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-02 · Decidido em: 2026-09-02

Contexto

O PowerSync vai passar a filtrar o bucket por role do JWT (central-backend 016): COMERCIAL vê só combustível, LUBRIFICANTE só lubrificante, ADMIN/DIRETORIA tudo. bi_movimento mistura os dois segmentos → o corte precisa ser row-level. A data-query do PowerSync não faz JOIN nem subquery cross-table — a sync rule só enxerga a própria linha. Hoje bi_movimento só tem cod_produto; o segmento vive no nome do produto (regra que hoje é regex no cliente Flutter, produto_labels.dart). Migração V20.

Decisão

  1. Coluna segmento TEXT denormalizada em bi_produto e bi_movimento (combustivel / lubrificante / NULL). A sync rule lê WHERE segmento = 'combustivel' direto na linha de bi_movimento — sem JOIN.

  2. Regra derivada do nome, dono único Segmento.doNome (processing/): combustivel ← ^\s*ONU\s+\d+; lubrificante ← MAXON\s+OIL ou \bON\s*LUB; senão NULL. combustivel tem precedência. Autoridade = handoff integracao/powersync (porta do produto_labels.dart, que vive no cliente Flutter, fora deste repo).

  3. População inline na pipeline, diff-aware — não UPDATE bi_movimento … FROM bi_produto por carga. Os três readers que constroem Produto+Movimento no mesmo loop (Relatorio18Processor, MargemDiaProcessor.MovimentoSheetReader reusado por MargemConsolidadaProcessor) chamam Segmento.doNome(nome) e setam nos dois; segmento entra no WHERE IS DISTINCT FROM dos upserts.

  4. Backfill one-shot no V20 (ALTER ADD COLUMN IF NOT EXISTS + UPDATE das duas + índice idx_movimento_segmento). PG usa ~* com \y; a paridade com o regex Java (\b) é garantida por teste (SegmentoBackfillParityTest, Postgres real), não por diff contra o Dart.

  5. NULL (Arla/Ureia) fica fora do COMERCIAL — a sync rule dá ao COMERCIAL segmento='combustivel'; linha NULL só chega em ADMIN/DIRETORIA.

Consequências

  • Diff-aware preservado: UPDATE cross-table por carga reescreveria a linha mesmo sem mudança (bumpa xmin → checkpoint PowerSync desnecessário em todo movimento tocado). Inline + WHERE IS DISTINCT FROM não toca linha inalterada — coerente com a doutrina do BulkUpserter. Teste pipelineNasceComSegmentoEReuploadNaoBumpaXmin prova (reupload exato → xmin intacto).
  • Sem GRANT novo pro powersync_role: coluna em tabela existente herda o GRANT SELECT table-level (V13/default privileges); a regra de GRANT-cinto para tabela nova está no modelo de dados.
  • Mudança futura do regex não recomputa segmento de produto com nome estável (o nome IS DISTINCT gate bloqueia) → exigiria nova migration de backfill, não só redeploy. Aceitável (regra de segmento muda raro e deliberado).
  • Divergência de nome entre arquivos (pré-existente do nome diff-aware): segmento reflete o último arquivo a escrever o nome. Não introduzido aqui; só vira trabalho se aparecer segmento errado em prod (aí, precedência de fonte pro bi_produto).
  • Fora deste repo: sync rule (../powersync/), token com claim role + gating (central-backend 016), login do bi-comercial enviando app_id. A sync rule só aplica o YAML vivo depois de segmento populado em prod.

Alternativas consideradas

  • UPDATE bi_movimento m SET segmento = p.segmento FROM bi_produto p a cada carga (SQL literal do handoff): descartada — UPDATE puro bumpa xmin em todo movimento tocado = churn de checkpoint PowerSync, contra a doutrina diff-aware. Inline no processador é o caminho da casa.
  • Regex/sets de cod_produto na SQL da sync rule (sem coluna): impossível — movimento não tem o nome, e lista curada de cod_produto não cobre código novo (conflito Paulínia). Denormalizar é o único corte confiável row-level.
  • Unificar com o filtro "só combustível" do ComprasProcessor (startsWith("ONU")): descartada — aquele classifica fornecedor de compra (bi_compra/bi_fornecedor), com propósito e bordas próprios; Segmento classifica produto/movimento pro role-gating. Unificar arrastaria COMPRAS pro escopo e exigiria re-testar, sem ganho funcional. Regras separadas de propósito (spec 013, L9).

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.

Correção 2026-09-14: o andaime .ia/013 saiu do repo depois de destilado; o durável está nesta decisão e no modelo de dados.