Pular para conteúdo

0014 — updated_at (chave-versão) nas tabelas do espelho X-Adm

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

Contexto. A decisão 0013 introduziu estoque.updated_at (V21) como chave-versão do tradutor Thoms e deixou como evolução aberta estender updated_at às demais tabelas espelho. O gatilho concreto veio do integrador-client (PowerSync on-premise), que processa os pendentes na ordem dos fatos com ORDER BY updated_at dentro de cada tabela. Para isso o servidor (dono das tabelas) precisa de uma chave-versão uniforme por linha.

Decisão. Dar a toda tabela do espelho X-Adm uma chave-versão updated_at uniforme, para qualquer consumidor externo (integrador-client, tradutores, sweeps) ordenar os fatos por ORDER BY updated_at em qualquer tabela — sem uma feature nova por tabela. Feito em duas migrações na mesma decisão:

  • V22 — tabelas do fluxo de entrada: contratos, propriedades, itensped, fones (novas) + alinhar estoque (V21 era nullable → NOT NULL).
  • V23 — demais tabelas de negócio: veiculos, municipio, loteest, itens, itenslote, itenspedamx, itpedamxdi, tbpcoest.

Fora: as tabelas de infra (request, pushenviada, pabast_*) — têm timestamps próprios e nenhum consumidor as ordena por updated_at.

  • Mecanismo = @DateUpdated (Micronaut Data), herdado do estoque/0013. Bumpa em toda escrita de entidade (repo.save/update): apply XADM (EscritaPorChaveNatural) e write-back PowerSync (PowersyncController) passam por repositório. Coluna de controle do servidor: @DateUpdated sobrescreve qualquer updated_at recebido de fora (PUT /api/v1/xadm ou write-back) — o pedido de "não aceitar updated_at externo" sai de graça, sem código de guarda.
  • Coluna TIMESTAMPTZ NOT NULL DEFAULT now() + índice idx_<tabela>_updated_at em cada uma.

Exceção notacompl. Ganha @DateUpdated na entidade (o updated_at, antes só do cliente/NULL, passa a ser server-controlled — mais confiável para o lastchange), mas a coluna permanece NULLABLE (V10, sem backfill): o /powersync/lastchange e a ordenação de vendas dependem de MAX(COALESCE(updated_at, created_at)) — linhas legadas com updated_at NULL caem no created_at. Um NOT NULL DEFAULT now() backfillaria as legadas para o instante da migração e quebraria esse fallback. O created_at (ordenação cronológica) continua manual, fora deste mecanismo.

Consequências / trade-offs.

  • @DateUpdated vs now() manual no upsert. Escolhido @DateUpdated: uniforme com o 0013, zero plumbing, rejeição de valor externo gratuita. Custo: bumpa também em reenvio idêntico / write-back (aceito — o consumidor é idempotente por updated_at). O @Query cru que não passa por entidade (de propósito) não bumpa.
  • NOT NULL DEFAULT now() vs nullable. Escolhido NOT NULL para ordenação limpa no cliente (sem NULLS FIRST/LAST). Custo: o backfill do estoque (linhas anteriores → now() da migração) faz o tradutor Thoms vê-las como "mudadas" uma vez no delta-sweep seguinte — re-sweep único e idempotente (ver comentário da V21).
  • Fora deste repo (operação humana): o integrador-client declara updated_at no schema local do SDK; o repo powersync (maxsul) precisa de rebuild da imagem para a coluna entrar nas sync rules (SELECT *).

Verificação. ./gradlew check verde (Testcontainers, Postgres real): schema NOT NULL + índice das 5 tabelas (UpdatedAtEntradaSchemaTest, EstoqueUpdatedAtIntegrationTest), @DateUpdated presente por reflexão, e round-trip real do bump via fones (FluxoEntradaIngestIntegrationTest).