@luanpdd/kit-mcp 1.21.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +914 -648
- package/kit/COMANDOS.md +138 -138
- package/kit/README.md +76 -52
- package/kit/agents/advisor-researcher.md +106 -106
- package/kit/agents/assumptions-analyzer.md +107 -107
- package/kit/agents/audit-log-implementer.md +138 -0
- package/kit/agents/auditor-consistencia-isolamento.md +413 -0
- package/kit/agents/codebase-mapper.md +768 -768
- package/kit/agents/crm-pipeline-implementer.md +106 -0
- package/kit/agents/debugger.md +813 -772
- package/kit/agents/detector-tenant-quente.md +337 -0
- package/kit/agents/evolution-go-integrator.md +21 -0
- package/kit/agents/example-reviewer.md +21 -21
- package/kit/agents/executor.md +564 -523
- package/kit/agents/integration-checker.md +200 -200
- package/kit/agents/invite-flow-implementer.md +52 -0
- package/kit/agents/lgpd-compliance-auditor.md +89 -0
- package/kit/agents/multi-tenant-isolation-auditor.md +10 -0
- package/kit/agents/multi-tenant-rls-writer.md +78 -0
- package/kit/agents/nyquist-auditor.md +178 -178
- package/kit/agents/org-onboarding-implementer.md +21 -0
- package/kit/agents/phase-researcher.md +696 -696
- package/kit/agents/plan-checker.md +272 -272
- package/kit/agents/planner.md +922 -891
- package/kit/agents/project-researcher.md +652 -652
- package/kit/agents/research-synthesizer.md +245 -245
- package/kit/agents/roadmapper.md +677 -677
- package/kit/agents/supabase-architect.md +27 -0
- package/kit/agents/supabase-auth-bootstrapper.md +80 -0
- package/kit/agents/supabase-column-privileges-writer.md +399 -0
- package/kit/agents/supabase-migration-writer.md +141 -14
- package/kit/agents/supabase-rbac-implementer.md +392 -0
- package/kit/agents/supabase-rls-hardener.md +521 -0
- package/kit/agents/supabase-rls-writer.md +105 -9
- package/kit/agents/supabase-roles-implementer.md +355 -0
- package/kit/agents/super-admin-implementer.md +99 -0
- package/kit/agents/ui-auditor.md +437 -437
- package/kit/agents/ui-checker.md +302 -302
- package/kit/agents/ui-researcher.md +355 -355
- package/kit/agents/user-profiler.md +175 -175
- package/kit/agents/validador-evolucao-schema.md +335 -0
- package/kit/agents/verifier.md +728 -728
- package/kit/commands/adicionar-backlog.md +75 -75
- package/kit/commands/adicionar-fase.md +42 -42
- package/kit/commands/adicionar-tarefa.md +45 -45
- package/kit/commands/adicionar-testes.md +41 -41
- package/kit/commands/ajuda.md +21 -21
- package/kit/commands/atualizar.md +37 -37
- package/kit/commands/auditar-marco.md +179 -179
- package/kit/commands/auditar-uat.md +23 -23
- package/kit/commands/autonomo.md +40 -40
- package/kit/commands/branch-pr.md +24 -24
- package/kit/commands/concluir-marco.md +247 -247
- package/kit/commands/configuracoes.md +36 -36
- package/kit/commands/dados-distribuidos.md +188 -0
- package/kit/commands/definir-perfil.md +10 -10
- package/kit/commands/depurar.md +190 -190
- package/kit/commands/discutir-fase.md +131 -131
- package/kit/commands/entrar-discord.md +17 -17
- package/kit/commands/estatisticas.md +18 -18
- package/kit/commands/example-greeting.md +33 -33
- package/kit/commands/executar-fase.md +58 -58
- package/kit/commands/expresso.md +56 -56
- package/kit/commands/fase-ui.md +34 -34
- package/kit/commands/fazer.md +57 -57
- package/kit/commands/fio.md +125 -125
- package/kit/commands/fluxos-trabalho.md +64 -64
- package/kit/commands/forense.md +176 -176
- package/kit/commands/gerenciador.md +38 -38
- package/kit/commands/inserir-fase.md +31 -31
- package/kit/commands/limpeza.md +17 -17
- package/kit/commands/listar-hipoteses-fase.md +45 -45
- package/kit/commands/listar-workspaces.md +18 -18
- package/kit/commands/mapear-codebase.md +70 -70
- package/kit/commands/nota.md +33 -33
- package/kit/commands/novo-marco.md +43 -43
- package/kit/commands/novo-projeto.md +41 -41
- package/kit/commands/novo-workspace.md +43 -43
- package/kit/commands/pausar-trabalho.md +37 -37
- package/kit/commands/perfil-usuario.md +45 -45
- package/kit/commands/pesquisar-fase.md +195 -195
- package/kit/commands/planejar-fase.md +67 -67
- package/kit/commands/planejar-lacunas.md +33 -33
- package/kit/commands/plantar-ideia.md +25 -25
- package/kit/commands/progresso.md +24 -24
- package/kit/commands/proximo.md +30 -30
- package/kit/commands/publicar.md +490 -490
- package/kit/commands/rapido.md +35 -35
- package/kit/commands/reaplicar-patches.md +124 -124
- package/kit/commands/relatorio-sessao.md +19 -19
- package/kit/commands/remover-fase.md +31 -31
- package/kit/commands/remover-workspace.md +26 -26
- package/kit/commands/resumo-marco.md +50 -50
- package/kit/commands/retomar-trabalho.md +40 -40
- package/kit/commands/revisar-backlog.md +60 -60
- package/kit/commands/revisar-ui.md +32 -32
- package/kit/commands/revisar.md +37 -37
- package/kit/commands/saude.md +21 -21
- package/kit/commands/setup-notion.md +93 -93
- package/kit/commands/supabase.md +55 -8
- package/kit/commands/sync-main.md +68 -68
- package/kit/commands/validar-fase.md +35 -35
- package/kit/commands/verificar-tarefas.md +44 -44
- package/kit/commands/verificar-trabalho.md +64 -64
- package/kit/file-manifest.json +52 -32
- package/kit/framework/bin/lib/commands.cjs +959 -959
- package/kit/framework/bin/lib/config.cjs +442 -442
- package/kit/framework/bin/lib/core.cjs +1230 -1230
- package/kit/framework/bin/lib/frontmatter.cjs +336 -336
- package/kit/framework/bin/lib/init.cjs +1442 -1442
- package/kit/framework/bin/lib/milestone.cjs +252 -252
- package/kit/framework/bin/lib/model-profiles.cjs +68 -68
- package/kit/framework/bin/lib/phase.cjs +888 -888
- package/kit/framework/bin/lib/profile-output.cjs +952 -952
- package/kit/framework/bin/lib/profile-pipeline.cjs +539 -539
- package/kit/framework/bin/lib/roadmap.cjs +329 -329
- package/kit/framework/bin/lib/security.cjs +382 -382
- package/kit/framework/bin/lib/state.cjs +1031 -1031
- package/kit/framework/bin/lib/template.cjs +222 -222
- package/kit/framework/bin/lib/uat.cjs +282 -282
- package/kit/framework/bin/lib/verify.cjs +888 -888
- package/kit/framework/bin/lib/workstream.cjs +491 -491
- package/kit/framework/bin/tools.cjs +918 -918
- package/kit/framework/commands/workstreams.md +63 -63
- package/kit/framework/references/checkpoints.md +778 -778
- package/kit/framework/references/continuation-format.md +249 -249
- package/kit/framework/references/decimal-phase-calculation.md +64 -64
- package/kit/framework/references/git-integration.md +295 -295
- package/kit/framework/references/git-planning-commit.md +38 -38
- package/kit/framework/references/model-profile-resolution.md +36 -36
- package/kit/framework/references/model-profiles.md +139 -139
- package/kit/framework/references/phase-argument-parsing.md +61 -61
- package/kit/framework/references/planning-config.md +202 -202
- package/kit/framework/references/questioning.md +162 -162
- package/kit/framework/references/tdd.md +263 -263
- package/kit/framework/references/ui-brand.md +160 -160
- package/kit/framework/references/user-profiling.md +657 -657
- package/kit/framework/references/verification-patterns.md +612 -612
- package/kit/framework/references/workstream-flag.md +58 -58
- package/kit/framework/templates/DEBUG.md +164 -164
- package/kit/framework/templates/UAT.md +265 -265
- package/kit/framework/templates/UI-SPEC.md +100 -100
- package/kit/framework/templates/VALIDATION.md +76 -76
- package/kit/framework/templates/claude-md.md +122 -122
- package/kit/framework/templates/codebase/architecture.md +185 -185
- package/kit/framework/templates/codebase/concerns.md +205 -205
- package/kit/framework/templates/codebase/conventions.md +204 -204
- package/kit/framework/templates/codebase/integrations.md +192 -192
- package/kit/framework/templates/codebase/stack.md +158 -158
- package/kit/framework/templates/codebase/structure.md +199 -199
- package/kit/framework/templates/codebase/testing.md +301 -301
- package/kit/framework/templates/config.json +44 -44
- package/kit/framework/templates/context.md +352 -352
- package/kit/framework/templates/continue-here.md +78 -78
- package/kit/framework/templates/copilot-instructions.md +7 -7
- package/kit/framework/templates/debug-subagent-prompt.md +91 -91
- package/kit/framework/templates/dev-preferences.md +20 -20
- package/kit/framework/templates/discovery.md +146 -146
- package/kit/framework/templates/discussion-log.md +63 -63
- package/kit/framework/templates/milestone-archive.md +123 -123
- package/kit/framework/templates/milestone.md +115 -115
- package/kit/framework/templates/phase-prompt.md +610 -610
- package/kit/framework/templates/planner-subagent-prompt.md +117 -117
- package/kit/framework/templates/project.md +186 -186
- package/kit/framework/templates/requirements.md +231 -231
- package/kit/framework/templates/research-project/ARCHITECTURE.md +204 -204
- package/kit/framework/templates/research-project/FEATURES.md +147 -147
- package/kit/framework/templates/research-project/PITFALLS.md +200 -200
- package/kit/framework/templates/research-project/STACK.md +120 -120
- package/kit/framework/templates/research-project/SUMMARY.md +170 -170
- package/kit/framework/templates/research.md +419 -419
- package/kit/framework/templates/retrospective.md +54 -54
- package/kit/framework/templates/roadmap.md +202 -202
- package/kit/framework/templates/state.md +176 -176
- package/kit/framework/templates/summary-complex.md +59 -59
- package/kit/framework/templates/summary-minimal.md +41 -41
- package/kit/framework/templates/summary-standard.md +48 -48
- package/kit/framework/templates/summary.md +209 -209
- package/kit/framework/templates/user-profile.md +146 -146
- package/kit/framework/templates/user-setup.md +256 -256
- package/kit/framework/templates/verification-report.md +258 -258
- package/kit/framework/workflows/add-phase.md +112 -112
- package/kit/framework/workflows/add-tests.md +351 -351
- package/kit/framework/workflows/add-todo.md +158 -158
- package/kit/framework/workflows/audit-milestone.md +340 -340
- package/kit/framework/workflows/audit-uat.md +109 -109
- package/kit/framework/workflows/autonomous.md +891 -891
- package/kit/framework/workflows/check-todos.md +177 -177
- package/kit/framework/workflows/cleanup.md +152 -152
- package/kit/framework/workflows/complete-milestone.md +696 -696
- package/kit/framework/workflows/diagnose-issues.md +231 -231
- package/kit/framework/workflows/discovery-phase.md +289 -289
- package/kit/framework/workflows/discuss-phase-assumptions.md +653 -653
- package/kit/framework/workflows/discuss-phase.md +784 -784
- package/kit/framework/workflows/do.md +104 -104
- package/kit/framework/workflows/execute-phase.md +838 -838
- package/kit/framework/workflows/execute-plan.md +510 -510
- package/kit/framework/workflows/fast.md +102 -102
- package/kit/framework/workflows/forensics.md +265 -265
- package/kit/framework/workflows/health.md +181 -181
- package/kit/framework/workflows/help.md +619 -619
- package/kit/framework/workflows/insert-phase.md +130 -130
- package/kit/framework/workflows/list-phase-assumptions.md +178 -178
- package/kit/framework/workflows/list-workspaces.md +56 -56
- package/kit/framework/workflows/manager.md +362 -362
- package/kit/framework/workflows/map-codebase.md +377 -377
- package/kit/framework/workflows/milestone-summary.md +223 -223
- package/kit/framework/workflows/new-milestone.md +486 -486
- package/kit/framework/workflows/new-project.md +1159 -1159
- package/kit/framework/workflows/new-workspace.md +237 -237
- package/kit/framework/workflows/next.md +97 -97
- package/kit/framework/workflows/node-repair.md +92 -92
- package/kit/framework/workflows/note.md +156 -156
- package/kit/framework/workflows/pause-work.md +176 -176
- package/kit/framework/workflows/plan-milestone-gaps.md +273 -273
- package/kit/framework/workflows/plan-phase.md +765 -765
- package/kit/framework/workflows/plant-seed.md +169 -169
- package/kit/framework/workflows/pr-branch.md +129 -129
- package/kit/framework/workflows/profile-user.md +450 -450
- package/kit/framework/workflows/progress.md +507 -507
- package/kit/framework/workflows/quick.md +757 -757
- package/kit/framework/workflows/remove-phase.md +155 -155
- package/kit/framework/workflows/remove-workspace.md +90 -90
- package/kit/framework/workflows/research-phase.md +82 -82
- package/kit/framework/workflows/resume-project.md +326 -326
- package/kit/framework/workflows/review.md +228 -228
- package/kit/framework/workflows/session-report.md +146 -146
- package/kit/framework/workflows/settings.md +283 -283
- package/kit/framework/workflows/ship.md +228 -228
- package/kit/framework/workflows/stats.md +60 -60
- package/kit/framework/workflows/transition.md +671 -671
- package/kit/framework/workflows/ui-phase.md +302 -302
- package/kit/framework/workflows/ui-review.md +165 -165
- package/kit/framework/workflows/update.md +323 -323
- package/kit/framework/workflows/validate-phase.md +174 -174
- package/kit/framework/workflows/verify-phase.md +252 -252
- package/kit/framework/workflows/verify-work.md +637 -637
- package/kit/hooks/check-update.js +118 -118
- package/kit/hooks/context-monitor.js +163 -163
- package/kit/hooks/prompt-guard.js +103 -103
- package/kit/hooks/statusline.js +125 -125
- package/kit/hooks/workflow-guard.js +101 -101
- package/kit/settings.json +45 -45
- package/kit/skills/_shared-dados-distribuidos/glossary.md +224 -0
- package/kit/skills/_shared-supabase/glossary.md +27 -0
- package/kit/skills/armadilhas-sistemas-distribuidos/SKILL.md +447 -0
- package/kit/skills/audit-log-multi-tenant/SKILL.md +6 -0
- package/kit/skills/cascading-failures/SKILL.md +4 -0
- package/kit/skills/consistencia-leitura-replica/SKILL.md +385 -0
- package/kit/skills/crm-lead-pipeline-patterns/SKILL.md +17 -0
- package/kit/skills/escolha-modelo-consistencia/SKILL.md +495 -0
- package/kit/skills/evolucao-schema-compativel/SKILL.md +448 -0
- package/kit/skills/example-skill/SKILL.md +42 -42
- package/kit/skills/multi-tenant-performance-scaling/SKILL.md +4 -0
- package/kit/skills/multi-tenant-rls-hierarchy/SKILL.md +4 -0
- package/kit/skills/postgres-isolamento-concorrencia/SKILL.md +552 -0
- package/kit/skills/rbac-permissions-matrix-supabase/SKILL.md +37 -0
- package/kit/skills/streams-eventos-cdc/SKILL.md +712 -0
- package/kit/skills/supabase-column-level-security/SKILL.md +426 -0
- package/kit/skills/supabase-cron-queues/SKILL.md +9 -0
- package/kit/skills/supabase-custom-claims-rbac/SKILL.md +472 -0
- package/kit/skills/supabase-database-functions/SKILL.md +85 -0
- package/kit/skills/supabase-migrations/SKILL.md +133 -11
- package/kit/skills/supabase-postgres-roles/SKILL.md +392 -0
- package/kit/skills/supabase-rls-defense-in-depth/SKILL.md +418 -0
- package/kit/skills/supabase-rls-policies/SKILL.md +462 -12
- package/kit/skills/super-admin-platform-pattern/SKILL.md +4 -0
- package/kit/skills/tenant-quente-mitigacao/SKILL.md +605 -0
- package/package.json +63 -63
- package/src/core/kit.js +216 -216
- package/src/core/reflect.js +247 -247
- package/src/core/reverse-sync.js +372 -372
- package/src/core/sync.js +418 -418
- package/src/core/watch.js +121 -121
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: streams-eventos-cdc
|
|
3
|
+
description: Use ao implementar event stream em Supabase — diferença AMQP/JMS-style (LISTEN/NOTIFY) vs log-based (pgmq) brokers, padrões CDC via wal2json + Realtime broadcast OU pglogical → Kafka, event sourcing em Postgres com tabela eventos source-of-truth + projeções via MV ou trigger, exactly-once em pgmq via dedup table com unique(event_id) + idempotency key + transactional outbox, 3 tipos stream join (stream-stream com janela, stream-table CDC+atividade, table-table merge changelogs), log compaction strategy (pgmq retention TTL + snapshot manual).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Streams, Eventos e CDC — Brokers, Event Sourcing, Exactly-Once em Postgres
|
|
7
|
+
|
|
8
|
+
## Quando usar
|
|
9
|
+
|
|
10
|
+
LLM carrega esta skill ao implementar pipeline event-driven em Supabase + Postgres. Trigger phrases:
|
|
11
|
+
|
|
12
|
+
- "event stream Postgres", "CDC Supabase", "wal2json + Realtime"
|
|
13
|
+
- "pgmq vs LISTEN/NOTIFY", "broker log-based vs AMQP"
|
|
14
|
+
- "event sourcing Postgres", "tabela append-only de eventos"
|
|
15
|
+
- "exactly-once pgmq", "dedup table idempotency"
|
|
16
|
+
- "stream join com janela", "stream-table CDC enrichment"
|
|
17
|
+
- "log compaction Postgres", "snapshot eventos"
|
|
18
|
+
- "projeção materialized view de eventos", "denormalization via trigger"
|
|
19
|
+
|
|
20
|
+
Esta skill **estende** [`audit-log-multi-tenant`](../audit-log-multi-tenant/SKILL.md) (v1.21) ao reconhecer audit_log como event sourcing parcial; [`supabase-cron-queues`](../supabase-cron-queues/SKILL.md) (v1.8) para pgmq pattern; e [`supabase-realtime`](../supabase-realtime/SKILL.md) (v1.8) para broadcast como CDC stream.
|
|
21
|
+
|
|
22
|
+
Material-fonte: *Designing Data-Intensive Applications*, Martin Kleppmann (O'Reilly 2017), capítulo 11 "Stream Processing" (linhas 17812-19637 do material extraído; summary 19408-19481). Termos canônicos PT-BR ↔ EN definidos em [`../_shared-dados-distribuidos/glossary.md`](../_shared-dados-distribuidos/glossary.md) seção (h).
|
|
23
|
+
|
|
24
|
+
## Regras absolutas
|
|
25
|
+
|
|
26
|
+
**REGRA #1 (broker log-based default para event sourcing):** Para event sourcing, CDC ou pipeline com replay obrigatório, escolher **log-based broker** (Kafka, pgmq) — mensagem retida (TTL configurável), múltiplos consumers tracked offset independente, replay possível. AMQP/JMS-style (RabbitMQ, LISTEN/NOTIFY) deletam mensagem após ack — sem replay, single-consumer.
|
|
27
|
+
|
|
28
|
+
**REGRA #2 (CDC via wal2json + Realtime é default Supabase):** Se ambiente é Supabase + use case é sync índice/desnormalização/multi-region, default é wal2json + Supabase Realtime broadcast. Zero infra extra. Apenas considerar pglogical → Kafka externo se warehousing analítico for o uso primário.
|
|
29
|
+
|
|
30
|
+
**REGRA #3 (event sourcing exige tabela append-only + projeções derivadas):** Tabela `events` deve ser **append-only** (REVOKE DELETE/UPDATE como audit_log v1.21). Estado atual = projeção derivada via Materialized View ou trigger-maintained denormalization — NUNCA escrever direto em "tabela de estado". Source of truth = stream de eventos.
|
|
31
|
+
|
|
32
|
+
**REGRA #4 (exactly-once pgmq exige dedup + idempotency + transactional outbox):** pgmq não garante exactly-once nativo (at-least-once entrega). Para semântica exactly-once: (a) **dedup table** com `unique(event_id)` rejeitando duplicatas; (b) **handler idempotente** (mesmo input → mesmo output, sem efeitos colaterais); (c) **transactional outbox** para cross-service writes.
|
|
33
|
+
|
|
34
|
+
**REGRA #5 (stream join exige janela temporal explícita):** Stream-stream join sem janela = memória cresce sem limite (cada evento aguarda match indefinidamente). Toda janela deve ter TTL explícito (tumbling, sliding, session). Default: tumbling 5min para business events; sliding 1min para latency-sensitive.
|
|
35
|
+
|
|
36
|
+
**REGRA #6 (log compaction não-trivial em pgmq — exige snapshot manual):** pgmq não tem log compaction nativa (Kafka tem). Para event sourcing com snapshot: criar tabela `snapshots` periodicamente, deletar `events.id < snapshot_lsn` correspondente. Sem snapshot = tabela `events` cresce sem limite, replay torna-se O(n) caro.
|
|
37
|
+
|
|
38
|
+
## Patterns canônicos
|
|
39
|
+
|
|
40
|
+
### REQ STREAMS-01 — Brokers AMQP/JMS-style vs log-based
|
|
41
|
+
|
|
42
|
+
| Tipo | Exemplos | Mensagem após ack | Multi-consumer | Replay | Use case |
|
|
43
|
+
|---|---|---|---|---|---|
|
|
44
|
+
| **AMQP/JMS-style** | RabbitMQ, postgres `LISTEN/NOTIFY`, ActiveMQ | Deletada (consumida) | Single (work queue — distribui rounds robin) | Não (gone after ack) | Task queue async (envio email, geração PDF) |
|
|
45
|
+
| **Log-based** | Kafka, pgmq, Redpanda, Pulsar | Retida (TTL configurável) | Multiple (cada consumer tracks offset independente) | Sim (replay desde offset N) | Event sourcing, CDC, audit, analytics |
|
|
46
|
+
|
|
47
|
+
**Como escolher:**
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
Use case precisa de replay? ─── Sim ──► log-based (pgmq, Kafka)
|
|
51
|
+
│
|
|
52
|
+
Não
|
|
53
|
+
│
|
|
54
|
+
▼
|
|
55
|
+
Múltiplos consumers veem mesma mensagem? ─── Sim ──► log-based
|
|
56
|
+
│
|
|
57
|
+
Não
|
|
58
|
+
│
|
|
59
|
+
▼
|
|
60
|
+
Mensagem é "task" descartável após processada? ─── Sim ──► AMQP/JMS-style (RabbitMQ, LISTEN/NOTIFY)
|
|
61
|
+
│
|
|
62
|
+
Não
|
|
63
|
+
│
|
|
64
|
+
▼
|
|
65
|
+
Default (event-driven em B2B SaaS): log-based (pgmq)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Exemplo postgres LISTEN/NOTIFY (AMQP-style — single consumer, sem replay):**
|
|
69
|
+
|
|
70
|
+
```sql
|
|
71
|
+
-- Producer
|
|
72
|
+
notify ch_orders, '{"order_id":"abc-123","status":"paid"}';
|
|
73
|
+
|
|
74
|
+
-- Consumer (Edge Function)
|
|
75
|
+
listen ch_orders;
|
|
76
|
+
-- Sleep até receber notification — single consumer recebe, mensagem some
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Exemplo pgmq (log-based — multi-consumer, replay):**
|
|
80
|
+
|
|
81
|
+
```sql
|
|
82
|
+
-- Setup (uma vez)
|
|
83
|
+
select pgmq.create('orders');
|
|
84
|
+
|
|
85
|
+
-- Producer
|
|
86
|
+
select pgmq.send('orders', '{"order_id":"abc-123","status":"paid"}');
|
|
87
|
+
|
|
88
|
+
-- Consumer 1 (worker A)
|
|
89
|
+
select * from pgmq.read('orders', 30, 1);
|
|
90
|
+
-- vt=30s (visibility timeout), 1 mensagem por leitura
|
|
91
|
+
-- Após ler: mensagem fica invisível por 30s — outro worker não pega
|
|
92
|
+
-- Worker A processa e dá ack:
|
|
93
|
+
select pgmq.delete('orders', msg_id);
|
|
94
|
+
-- Sem ack em 30s → mensagem volta à queue (at-least-once)
|
|
95
|
+
|
|
96
|
+
-- Consumer 2 (worker B / archive)
|
|
97
|
+
-- Se queue tem retention, archive table mantém histórico para replay
|
|
98
|
+
select * from pgmq.archive('orders', msg_id);
|
|
99
|
+
-- Mensagens em archive são replayable
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### REQ STREAMS-02 — 3 padrões CDC em Postgres
|
|
103
|
+
|
|
104
|
+
CDC (Change Data Capture) = capturar mudanças no DB como stream de eventos. 3 abordagens canônicas em Supabase:
|
|
105
|
+
|
|
106
|
+
**Abordagem 1: wal2json + Supabase Realtime broadcast** (default)
|
|
107
|
+
|
|
108
|
+
```sql
|
|
109
|
+
-- Habilitar replication identity (necessário para wal2json capturar UPDATE/DELETE com colunas)
|
|
110
|
+
alter table public.orders replica identity full;
|
|
111
|
+
|
|
112
|
+
-- Supabase Realtime já consome WAL via wal2json internamente
|
|
113
|
+
-- Cliente subscreve canal específico via JS client
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// Cliente Supabase consume CDC stream via Realtime
|
|
118
|
+
const channel = supabase
|
|
119
|
+
.channel('orders-cdc', { config: { private: true } })
|
|
120
|
+
.on(
|
|
121
|
+
'postgres_changes',
|
|
122
|
+
{ event: '*', schema: 'public', table: 'orders' },
|
|
123
|
+
(payload) => {
|
|
124
|
+
// payload.eventType: INSERT | UPDATE | DELETE
|
|
125
|
+
// payload.new: nova row (INSERT/UPDATE)
|
|
126
|
+
// payload.old: row antiga (UPDATE/DELETE — exige replica identity full)
|
|
127
|
+
console.log('CDC event:', payload);
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
.subscribe();
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Trade-offs:** zero infra extra; baixa latência (sub-segundo); RLS aplicada nas mensagens (cada cliente vê só rows permitidas). Limite: scale na ordem de milhares de subscribers por canal.
|
|
134
|
+
|
|
135
|
+
**Abordagem 2: pglogical → Kafka externo** (warehousing analítico)
|
|
136
|
+
|
|
137
|
+
```sql
|
|
138
|
+
-- Em Supabase Pro+ habilitar pglogical (extensão)
|
|
139
|
+
create extension if not exists pglogical;
|
|
140
|
+
|
|
141
|
+
-- Setup nó provider (Postgres source)
|
|
142
|
+
select pglogical.create_node(
|
|
143
|
+
node_name := 'supabase_prod',
|
|
144
|
+
dsn := 'host=db.xxx.supabase.co dbname=postgres'
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
-- Replication set para tabelas que viram stream
|
|
148
|
+
select pglogical.create_replication_set(set_name := 'cdc_set');
|
|
149
|
+
select pglogical.replication_set_add_table('cdc_set', 'public.orders', synchronize_data := false);
|
|
150
|
+
|
|
151
|
+
-- Conector Kafka (Debezium ou similar) consome pglogical → publica em Kafka topic
|
|
152
|
+
-- Trade-off: requer infra Kafka externa, latência maior (segundos), throughput muito maior
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Abordagem 3: Trigger-based** (casos custom onde wal2json não cobre)
|
|
156
|
+
|
|
157
|
+
```sql
|
|
158
|
+
-- Trigger que emite evento custom quando flag X muda
|
|
159
|
+
create or replace function public.emit_lead_qualified_event()
|
|
160
|
+
returns trigger
|
|
161
|
+
language plpgsql
|
|
162
|
+
security invoker
|
|
163
|
+
set search_path = ''
|
|
164
|
+
as $$
|
|
165
|
+
begin
|
|
166
|
+
if old.stage != 'qualified' and new.stage = 'qualified' then
|
|
167
|
+
insert into public.outbox (event_type, payload)
|
|
168
|
+
values (
|
|
169
|
+
'lead_qualified',
|
|
170
|
+
jsonb_build_object(
|
|
171
|
+
'lead_id', new.id,
|
|
172
|
+
'org_id', new.org_id,
|
|
173
|
+
'qualified_by', (select auth.uid()),
|
|
174
|
+
'qualified_at', now()
|
|
175
|
+
)
|
|
176
|
+
);
|
|
177
|
+
end if;
|
|
178
|
+
return new;
|
|
179
|
+
end;
|
|
180
|
+
$$;
|
|
181
|
+
|
|
182
|
+
create trigger lead_qualified_trigger
|
|
183
|
+
after update on public.leads
|
|
184
|
+
for each row
|
|
185
|
+
execute function public.emit_lead_qualified_event();
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Quando usar trigger-based:** semântica de evento mais rica que "linha mudou" (ex: business event "qualified" derivado de mudança específica). Worker async lê outbox e publica downstream.
|
|
189
|
+
|
|
190
|
+
**Use cases canônicos:**
|
|
191
|
+
|
|
192
|
+
| Use case | Abordagem recomendada |
|
|
193
|
+
|---|---|
|
|
194
|
+
| Sync índice de busca (Elasticsearch, Meilisearch) | wal2json + Realtime → função client que sincroniza |
|
|
195
|
+
| Desnormalização (Materialized View atualizada por evento) | Trigger-based (mais controle sobre quando refresh) |
|
|
196
|
+
| Sync multi-region cold standby | pglogical → Kafka → consumer remoto |
|
|
197
|
+
| Audit log retroativo + análise comportamental | wal2json (captura cru) → analytics warehouse |
|
|
198
|
+
| Notificação push (mobile app) | Realtime broadcast direto (zero step intermediário) |
|
|
199
|
+
|
|
200
|
+
### REQ STREAMS-03 — Event sourcing em Postgres
|
|
201
|
+
|
|
202
|
+
**Princípio canônico:** eventos imutáveis são source of truth; estado atual é projeção derivada.
|
|
203
|
+
|
|
204
|
+
**Schema canônico:**
|
|
205
|
+
|
|
206
|
+
```sql
|
|
207
|
+
-- Tabela events — source of truth (append-only)
|
|
208
|
+
create table public.events (
|
|
209
|
+
id bigserial primary key,
|
|
210
|
+
aggregate_id uuid not null, -- ID da entidade (order, user, ...)
|
|
211
|
+
aggregate_type text not null, -- Tipo da entidade ('order', 'user')
|
|
212
|
+
event_type text not null, -- 'order_created', 'order_paid', 'order_shipped'
|
|
213
|
+
payload jsonb not null, -- Detalhes do evento
|
|
214
|
+
metadata jsonb, -- actor_id, request_id, trace_id
|
|
215
|
+
created_at timestamptz not null default now()
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
-- Index canônico (para reproduzir histórico de uma entidade)
|
|
219
|
+
create index events_aggregate_idx on public.events (aggregate_id, id);
|
|
220
|
+
|
|
221
|
+
-- Index para query por tipo (analytics)
|
|
222
|
+
create index events_type_created_idx on public.events (event_type, created_at);
|
|
223
|
+
|
|
224
|
+
-- REGRA #3 — append-only: REVOKE DELETE/UPDATE
|
|
225
|
+
revoke delete, update on public.events from public, authenticated, anon, service_role;
|
|
226
|
+
-- Apenas postgres role pode deletar (cleanup com snapshot)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Cross-ref ATIVO** para [`audit-log-multi-tenant`](../audit-log-multi-tenant/SKILL.md) (v1.21) — audit_log É event sourcing semantics: append-only, imutável, retém histórico cronológico. Quem implementou audit_log já fez event sourcing parcial.
|
|
230
|
+
|
|
231
|
+
**Projeção via Materialized View:**
|
|
232
|
+
|
|
233
|
+
```sql
|
|
234
|
+
-- Projeção: estado atual de cada order derivado dos eventos
|
|
235
|
+
create materialized view public.order_state as
|
|
236
|
+
select
|
|
237
|
+
aggregate_id as order_id,
|
|
238
|
+
-- Reconstrói estado a partir dos eventos (último win)
|
|
239
|
+
(array_agg(payload->>'status' order by id desc))[1] as current_status,
|
|
240
|
+
(array_agg(payload->>'total' order by id desc))[1]::numeric as current_total,
|
|
241
|
+
min(created_at) as created_at,
|
|
242
|
+
max(created_at) as updated_at,
|
|
243
|
+
count(*) as event_count
|
|
244
|
+
from public.events
|
|
245
|
+
where aggregate_type = 'order'
|
|
246
|
+
group by aggregate_id;
|
|
247
|
+
|
|
248
|
+
create unique index on public.order_state (order_id);
|
|
249
|
+
|
|
250
|
+
-- Refresh (incremental via concurrent OR full)
|
|
251
|
+
refresh materialized view concurrently public.order_state;
|
|
252
|
+
-- Ou via pg_cron a cada N minutos
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Projeção via trigger-maintained denormalization:**
|
|
256
|
+
|
|
257
|
+
```sql
|
|
258
|
+
-- Tabela de estado mantida por trigger (atualizada por cada novo evento)
|
|
259
|
+
create table public.order_current_state (
|
|
260
|
+
order_id uuid primary key,
|
|
261
|
+
status text not null,
|
|
262
|
+
total numeric,
|
|
263
|
+
updated_at timestamptz not null default now()
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
create or replace function public.project_order_event()
|
|
267
|
+
returns trigger
|
|
268
|
+
language plpgsql
|
|
269
|
+
security invoker
|
|
270
|
+
set search_path = ''
|
|
271
|
+
as $$
|
|
272
|
+
begin
|
|
273
|
+
if new.aggregate_type = 'order' then
|
|
274
|
+
insert into public.order_current_state (order_id, status, total, updated_at)
|
|
275
|
+
values (
|
|
276
|
+
new.aggregate_id,
|
|
277
|
+
new.payload->>'status',
|
|
278
|
+
(new.payload->>'total')::numeric,
|
|
279
|
+
new.created_at
|
|
280
|
+
)
|
|
281
|
+
on conflict (order_id) do update
|
|
282
|
+
set status = excluded.status,
|
|
283
|
+
total = coalesce(excluded.total, public.order_current_state.total),
|
|
284
|
+
updated_at = excluded.updated_at;
|
|
285
|
+
end if;
|
|
286
|
+
return new;
|
|
287
|
+
end;
|
|
288
|
+
$$;
|
|
289
|
+
|
|
290
|
+
create trigger project_order_event_trigger
|
|
291
|
+
after insert on public.events
|
|
292
|
+
for each row
|
|
293
|
+
execute function public.project_order_event();
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Quando MV vs trigger:**
|
|
297
|
+
|
|
298
|
+
| Critério | MV concurrent refresh | Trigger denormalization |
|
|
299
|
+
|---|---|---|
|
|
300
|
+
| **Latência** | Periódica (minutos) | Imediata (no commit do evento) |
|
|
301
|
+
| **Custo write** | Baixo (write apenas em events) | Alto (write em events + state) |
|
|
302
|
+
| **Custo read** | Baixo (state já agregado) | Baixo |
|
|
303
|
+
| **Use case** | Analytics, dashboards | UI real-time, business state |
|
|
304
|
+
|
|
305
|
+
### REQ STREAMS-04 — Exactly-once em pgmq
|
|
306
|
+
|
|
307
|
+
pgmq oferece **at-least-once** nativo (mensagem reenviada se worker crash sem ack). Para semântica **exactly-once**, combinação de 3 técnicas:
|
|
308
|
+
|
|
309
|
+
**Técnica 1: Dedup table com unique(event_id)**
|
|
310
|
+
|
|
311
|
+
```sql
|
|
312
|
+
-- Tabela de eventos já processados
|
|
313
|
+
create table public.processed_events (
|
|
314
|
+
event_id uuid primary key,
|
|
315
|
+
processed_at timestamptz not null default now(),
|
|
316
|
+
processor text not null -- nome do worker para debug
|
|
317
|
+
);
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Técnica 2: Handler atomic — INSERT na dedup + processamento na MESMA transação**
|
|
321
|
+
|
|
322
|
+
```sql
|
|
323
|
+
-- Worker (Edge Function ou função PG)
|
|
324
|
+
create or replace function public.process_order_event(p_msg_id bigint)
|
|
325
|
+
returns void
|
|
326
|
+
language plpgsql
|
|
327
|
+
security definer -- worker tem privilégios elevados
|
|
328
|
+
set search_path = ''
|
|
329
|
+
as $$
|
|
330
|
+
declare
|
|
331
|
+
v_msg record;
|
|
332
|
+
v_event_id uuid;
|
|
333
|
+
begin
|
|
334
|
+
-- Lê mensagem da queue com visibility timeout
|
|
335
|
+
select msg_id, message into v_msg
|
|
336
|
+
from pgmq.read('orders', 30, 1)
|
|
337
|
+
limit 1;
|
|
338
|
+
|
|
339
|
+
if v_msg is null then return; end if;
|
|
340
|
+
|
|
341
|
+
v_event_id := (v_msg.message->>'event_id')::uuid;
|
|
342
|
+
|
|
343
|
+
begin
|
|
344
|
+
-- Atomic: INSERT dedup + processamento
|
|
345
|
+
insert into public.processed_events (event_id, processor)
|
|
346
|
+
values (v_event_id, 'process_order_event');
|
|
347
|
+
-- Falha (unique violation) se já processado → exception abort tudo
|
|
348
|
+
|
|
349
|
+
-- ... lógica de processamento idempotente ...
|
|
350
|
+
update public.orders set status = 'paid' where id = (v_msg.message->>'order_id')::uuid;
|
|
351
|
+
|
|
352
|
+
-- Ack — remove da queue
|
|
353
|
+
perform pgmq.delete('orders', v_msg.msg_id);
|
|
354
|
+
|
|
355
|
+
exception when unique_violation then
|
|
356
|
+
-- Já processado — apenas dar ack para remover da queue
|
|
357
|
+
perform pgmq.delete('orders', v_msg.msg_id);
|
|
358
|
+
end;
|
|
359
|
+
end;
|
|
360
|
+
$$;
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
**Técnica 3: Idempotency key no handler — mesmo input → mesmo output (sem efeitos colaterais)**
|
|
364
|
+
|
|
365
|
+
Idempotency = processar a mesma mensagem N vezes produz o mesmo resultado. Padrões:
|
|
366
|
+
|
|
367
|
+
```sql
|
|
368
|
+
-- Idempotente via UPDATE condicional (não muda se já está no estado)
|
|
369
|
+
update public.orders
|
|
370
|
+
set status = 'paid'
|
|
371
|
+
where id = $1 and status != 'paid';
|
|
372
|
+
-- Se já 'paid' → no-op, RETURNING vazio
|
|
373
|
+
|
|
374
|
+
-- Idempotente via INSERT ON CONFLICT
|
|
375
|
+
insert into public.payments (order_id, amount, transaction_id)
|
|
376
|
+
values ($1, $2, $3)
|
|
377
|
+
on conflict (transaction_id) do nothing;
|
|
378
|
+
-- Mesmo transaction_id → no-op
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**Cross-ref ATIVO** para [`escolha-modelo-consistencia`](../escolha-modelo-consistencia/SKILL.md) — pattern transactional outbox descrito lá é a base de exactly-once entre DB e broker (write atomic em mesma transação).
|
|
382
|
+
|
|
383
|
+
### REQ STREAMS-05 — 3 tipos de stream join com SQL exemplo
|
|
384
|
+
|
|
385
|
+
**Tipo 1: Stream-stream join (com janela temporal)**
|
|
386
|
+
|
|
387
|
+
Match de eventos de 2 streams dentro de uma janela. Ex: matching pedido + pagamento dentro de 5min via tumbling window.
|
|
388
|
+
|
|
389
|
+
```sql
|
|
390
|
+
-- Materialização: 2 tabelas event log + JOIN com window
|
|
391
|
+
create table public.order_events (
|
|
392
|
+
order_id uuid not null,
|
|
393
|
+
event_at timestamptz not null,
|
|
394
|
+
event_type text not null,
|
|
395
|
+
payload jsonb
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
create table public.payment_events (
|
|
399
|
+
payment_id uuid not null,
|
|
400
|
+
order_id uuid not null,
|
|
401
|
+
event_at timestamptz not null,
|
|
402
|
+
amount numeric
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
-- Stream-stream join via tumbling window 5min
|
|
406
|
+
create or replace view public.order_payment_join_5min as
|
|
407
|
+
select
|
|
408
|
+
o.order_id,
|
|
409
|
+
o.event_at as order_at,
|
|
410
|
+
p.event_at as paid_at,
|
|
411
|
+
p.amount,
|
|
412
|
+
date_trunc('minute', o.event_at) as window_start
|
|
413
|
+
from public.order_events o
|
|
414
|
+
join public.payment_events p on p.order_id = o.order_id
|
|
415
|
+
where o.event_type = 'order_created'
|
|
416
|
+
and p.event_at between o.event_at and o.event_at + interval '5 minutes'
|
|
417
|
+
order by o.event_at;
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
**Trade-off:** janela tumbling = não-overlapping, mais simples; sliding = overlapping, mais alertas; session = dinâmica, agrupada por user activity.
|
|
421
|
+
|
|
422
|
+
**Tipo 2: Stream-table join (CDC + atividade — enrichment)**
|
|
423
|
+
|
|
424
|
+
Stream de eventos enriquecido com lookup em tabela de referência atualizada por CDC.
|
|
425
|
+
|
|
426
|
+
```sql
|
|
427
|
+
-- Tabela users mantida atualizada via CDC (Realtime ou trigger)
|
|
428
|
+
-- Stream de eventos: clicks, logins, purchases — precisa enriched com user info
|
|
429
|
+
|
|
430
|
+
select
|
|
431
|
+
e.event_id,
|
|
432
|
+
e.event_type,
|
|
433
|
+
e.event_at,
|
|
434
|
+
-- Enrichment: lookup do user no momento atual (não do momento do evento)
|
|
435
|
+
u.email,
|
|
436
|
+
u.tier,
|
|
437
|
+
u.country
|
|
438
|
+
from public.user_events e
|
|
439
|
+
join public.users u on u.id = e.user_id
|
|
440
|
+
where e.event_at > now() - interval '1 hour';
|
|
441
|
+
|
|
442
|
+
-- Para latência baixa: keep tabela users em memória do worker (CDC stream → cache)
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**Cuidado canônico:** se a tabela mudou desde o evento, enrichment usa o estado **atual** do user, não o estado **no momento do evento**. Para histórico fiel: capturar snapshot no payload do evento (ex: `payload.user_email_at_event`).
|
|
446
|
+
|
|
447
|
+
**Tipo 3: Table-table join (merge de changelogs CDC)**
|
|
448
|
+
|
|
449
|
+
Merge de 2 changelogs CDC para produzir view denormalizada. Ex: orders changelog + customers changelog → view denormalizada de pedidos com info do cliente.
|
|
450
|
+
|
|
451
|
+
```sql
|
|
452
|
+
-- Materialized view derivada de 2 streams CDC mergeados
|
|
453
|
+
create materialized view public.orders_denorm as
|
|
454
|
+
select
|
|
455
|
+
o.order_id,
|
|
456
|
+
o.status,
|
|
457
|
+
o.total,
|
|
458
|
+
o.created_at as order_created_at,
|
|
459
|
+
c.email as customer_email,
|
|
460
|
+
c.tier as customer_tier,
|
|
461
|
+
c.country as customer_country
|
|
462
|
+
from public.orders o
|
|
463
|
+
join public.customers c on c.id = o.customer_id;
|
|
464
|
+
|
|
465
|
+
create unique index on public.orders_denorm (order_id);
|
|
466
|
+
|
|
467
|
+
-- Refresh disparado por CDC events em orders OU customers
|
|
468
|
+
create or replace function public.refresh_orders_denorm()
|
|
469
|
+
returns trigger
|
|
470
|
+
language plpgsql
|
|
471
|
+
as $$
|
|
472
|
+
begin
|
|
473
|
+
refresh materialized view concurrently public.orders_denorm;
|
|
474
|
+
return null;
|
|
475
|
+
end;
|
|
476
|
+
$$;
|
|
477
|
+
|
|
478
|
+
create trigger orders_changelog_trigger
|
|
479
|
+
after insert or update on public.orders
|
|
480
|
+
for each statement
|
|
481
|
+
execute function public.refresh_orders_denorm();
|
|
482
|
+
|
|
483
|
+
create trigger customers_changelog_trigger
|
|
484
|
+
after update on public.customers
|
|
485
|
+
for each statement
|
|
486
|
+
execute function public.refresh_orders_denorm();
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
**Trade-off:** refresh CONCURRENTLY exige unique index, latência maior. Para tabelas grandes, usar incremental refresh via trigger denormalization (REQ STREAMS-03).
|
|
490
|
+
|
|
491
|
+
### REQ STREAMS-06 — Log compaction strategy
|
|
492
|
+
|
|
493
|
+
Log compaction = para cada chave, manter apenas o último valor. Reduz storage sem perder estado atual.
|
|
494
|
+
|
|
495
|
+
**pgmq não tem nativa** — usa retention TTL via `vacuum_archive`:
|
|
496
|
+
|
|
497
|
+
```sql
|
|
498
|
+
-- pgmq archive movido para tabela archive periodicamente
|
|
499
|
+
select pgmq.archive('orders', 12345);
|
|
500
|
+
-- Após N dias na archive, vacuum_archive deleta hard
|
|
501
|
+
|
|
502
|
+
-- Configurar TTL via pg_cron
|
|
503
|
+
select cron.schedule(
|
|
504
|
+
'pgmq_vacuum_archive',
|
|
505
|
+
'0 2 * * *',
|
|
506
|
+
$$ select pgmq.purge_archive('orders', 30); $$
|
|
507
|
+
-- Deleta da archive mensagens > 30 dias
|
|
508
|
+
);
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
**Event sourcing exige snapshot periódico + compact:**
|
|
512
|
+
|
|
513
|
+
```sql
|
|
514
|
+
-- Tabela de snapshots — estado materializado a cada N eventos
|
|
515
|
+
create table public.snapshots (
|
|
516
|
+
aggregate_id uuid primary key,
|
|
517
|
+
snapshot_lsn bigint not null, -- até qual event.id este snapshot reflete
|
|
518
|
+
state jsonb not null, -- estado serializado
|
|
519
|
+
created_at timestamptz not null default now()
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
-- Função: criar snapshot para um aggregate quando event_count > threshold
|
|
523
|
+
create or replace function public.create_snapshot(p_aggregate_id uuid)
|
|
524
|
+
returns void
|
|
525
|
+
language plpgsql
|
|
526
|
+
security invoker
|
|
527
|
+
set search_path = ''
|
|
528
|
+
as $$
|
|
529
|
+
declare
|
|
530
|
+
v_state jsonb;
|
|
531
|
+
v_snapshot_lsn bigint;
|
|
532
|
+
begin
|
|
533
|
+
-- Reproduzir todos os eventos para construir estado atual
|
|
534
|
+
select
|
|
535
|
+
jsonb_build_object(
|
|
536
|
+
'status', (array_agg(payload->>'status' order by id desc))[1],
|
|
537
|
+
'total', (array_agg(payload->>'total' order by id desc))[1]::numeric,
|
|
538
|
+
'event_count', count(*)
|
|
539
|
+
),
|
|
540
|
+
max(id)
|
|
541
|
+
into v_state, v_snapshot_lsn
|
|
542
|
+
from public.events
|
|
543
|
+
where aggregate_id = p_aggregate_id;
|
|
544
|
+
|
|
545
|
+
-- Salvar snapshot (insert or update)
|
|
546
|
+
insert into public.snapshots (aggregate_id, snapshot_lsn, state)
|
|
547
|
+
values (p_aggregate_id, v_snapshot_lsn, v_state)
|
|
548
|
+
on conflict (aggregate_id) do update
|
|
549
|
+
set snapshot_lsn = excluded.snapshot_lsn,
|
|
550
|
+
state = excluded.state,
|
|
551
|
+
created_at = now();
|
|
552
|
+
end;
|
|
553
|
+
$$;
|
|
554
|
+
|
|
555
|
+
-- Compact: deletar eventos < snapshot_lsn (tomados em consideração no snapshot)
|
|
556
|
+
-- ATENÇÃO: requer privilégio especial (REGRA #3 — REVOKE DELETE em events)
|
|
557
|
+
-- Apenas postgres role + função SECURITY DEFINER
|
|
558
|
+
create or replace function public.compact_aggregate_events(p_aggregate_id uuid)
|
|
559
|
+
returns int
|
|
560
|
+
language plpgsql
|
|
561
|
+
security definer
|
|
562
|
+
set search_path = ''
|
|
563
|
+
as $$
|
|
564
|
+
declare
|
|
565
|
+
v_deleted int;
|
|
566
|
+
v_snapshot_lsn bigint;
|
|
567
|
+
begin
|
|
568
|
+
-- Confirmar que snapshot existe
|
|
569
|
+
select snapshot_lsn into v_snapshot_lsn
|
|
570
|
+
from public.snapshots
|
|
571
|
+
where aggregate_id = p_aggregate_id;
|
|
572
|
+
|
|
573
|
+
if v_snapshot_lsn is null then
|
|
574
|
+
raise exception 'Snapshot ausente para aggregate_id %', p_aggregate_id;
|
|
575
|
+
end if;
|
|
576
|
+
|
|
577
|
+
-- Deletar eventos antes do snapshot
|
|
578
|
+
delete from public.events
|
|
579
|
+
where aggregate_id = p_aggregate_id
|
|
580
|
+
and id <= v_snapshot_lsn;
|
|
581
|
+
|
|
582
|
+
get diagnostics v_deleted = row_count;
|
|
583
|
+
return v_deleted;
|
|
584
|
+
end;
|
|
585
|
+
$$;
|
|
586
|
+
|
|
587
|
+
revoke execute on function public.compact_aggregate_events from public, authenticated, anon;
|
|
588
|
+
-- Apenas service_role pode chamar
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
**Estratégia canônica:** snapshot a cada 1000 eventos por aggregate; compact após snapshot validado (replay do snapshot reproduz estado atual). Sem snapshot/compact, replay para reconstruir estado torna-se O(n) caro em aggregates antigos.
|
|
592
|
+
|
|
593
|
+
## Anti-patterns
|
|
594
|
+
|
|
595
|
+
### Anti-pattern 1: Usar LISTEN/NOTIFY para event sourcing
|
|
596
|
+
|
|
597
|
+
**Errado:**
|
|
598
|
+
|
|
599
|
+
```sql
|
|
600
|
+
-- ❌ LISTEN/NOTIFY como "event log"
|
|
601
|
+
notify ch_orders, '{"order_id":"abc","event":"paid"}';
|
|
602
|
+
-- Consumer offline → mensagem perdida
|
|
603
|
+
-- Sem replay, sem multi-consumer
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
**Por quê:** LISTEN/NOTIFY é AMQP/JMS-style — single consumer ativo recebe, mensagem some. Se consumer offline durante notify, evento perdido. Sem replay.
|
|
607
|
+
|
|
608
|
+
**Certo:** pgmq (log-based) ou tabela `events` append-only para event sourcing (REGRA #1).
|
|
609
|
+
|
|
610
|
+
### Anti-pattern 2: Event sourcing sem dedup → eventos duplicados
|
|
611
|
+
|
|
612
|
+
**Errado:**
|
|
613
|
+
|
|
614
|
+
```sql
|
|
615
|
+
-- ❌ Worker pgmq processa sem dedup table
|
|
616
|
+
create or replace function public.process_event(p_msg jsonb)
|
|
617
|
+
returns void
|
|
618
|
+
language plpgsql
|
|
619
|
+
as $$
|
|
620
|
+
begin
|
|
621
|
+
-- Processa direto, sem checar se já processado
|
|
622
|
+
update public.orders set status = 'paid' where id = (p_msg->>'order_id')::uuid;
|
|
623
|
+
-- Se mensagem reentregue (worker crash + redelivery) → status setado 2×
|
|
624
|
+
-- Se webhook externo → cobra cliente 2×
|
|
625
|
+
end;
|
|
626
|
+
$$;
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
**Por quê:** pgmq é at-least-once. Mensagem pode ser entregue >1× (worker crash sem ack, visibility timeout expirado). Sem dedup, processamento repetido = side effect duplicado.
|
|
630
|
+
|
|
631
|
+
**Certo:** dedup table + handler idempotente (REGRA #4). Mesmo input → mesmo output.
|
|
632
|
+
|
|
633
|
+
### Anti-pattern 3: Stream-stream join sem janela temporal
|
|
634
|
+
|
|
635
|
+
**Errado:**
|
|
636
|
+
|
|
637
|
+
```sql
|
|
638
|
+
-- ❌ Sem janela temporal: memória cresce indefinidamente
|
|
639
|
+
select o.order_id, p.payment_id
|
|
640
|
+
from public.order_events o
|
|
641
|
+
join public.payment_events p on p.order_id = o.order_id;
|
|
642
|
+
-- Cada evento aguarda match indefinido — payment de 3 anos atrás casa com order recente
|
|
643
|
+
-- Memória do worker cresce sem limite
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
**Por quê:** stream join sem TTL = sistema mantém eventos em memória aguardando match. Memória cresce linearmente com tempo, eventualmente OOM.
|
|
647
|
+
|
|
648
|
+
**Certo:** janela explícita (REGRA #5):
|
|
649
|
+
|
|
650
|
+
```sql
|
|
651
|
+
-- ✅ Tumbling window 5min
|
|
652
|
+
join public.payment_events p on p.order_id = o.order_id
|
|
653
|
+
where p.event_at between o.event_at and o.event_at + interval '5 minutes';
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### Anti-pattern 4: Materialized View sem CONCURRENTLY → bloqueio em refresh
|
|
657
|
+
|
|
658
|
+
**Errado:**
|
|
659
|
+
|
|
660
|
+
```sql
|
|
661
|
+
-- ❌ refresh sem CONCURRENTLY trava reads na MV durante refresh
|
|
662
|
+
refresh materialized view public.order_state;
|
|
663
|
+
-- Bloqueia SELECT na MV até terminar — minutos em MVs grandes
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
**Por quê:** refresh exclusivo locka a MV. Leitores ficam bloqueados.
|
|
667
|
+
|
|
668
|
+
**Certo:** CONCURRENTLY + unique index na MV:
|
|
669
|
+
|
|
670
|
+
```sql
|
|
671
|
+
-- ✅ Unique index obrigatório para CONCURRENTLY
|
|
672
|
+
create unique index on public.order_state (order_id);
|
|
673
|
+
|
|
674
|
+
refresh materialized view concurrently public.order_state;
|
|
675
|
+
-- Refresh em background; reads continuam funcionando
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
### Anti-pattern 5: Event sourcing sem snapshot → replay O(n) caro
|
|
679
|
+
|
|
680
|
+
**Errado:**
|
|
681
|
+
|
|
682
|
+
```sql
|
|
683
|
+
-- ❌ Reconstruir estado de aggregate antigo via replay completo
|
|
684
|
+
select * from public.events
|
|
685
|
+
where aggregate_id = $1
|
|
686
|
+
order by id;
|
|
687
|
+
-- Aggregate com 1M eventos → query lenta, alocação memória pesada
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
**Por quê:** sem snapshot, replay para reconstruir estado é O(n) onde n = número total de eventos do aggregate. Em aggregates antigos (orders de 5 anos), aggregação fica cara.
|
|
691
|
+
|
|
692
|
+
**Certo:** snapshot periódico + replay incremental (REGRA #6):
|
|
693
|
+
|
|
694
|
+
```sql
|
|
695
|
+
-- ✅ Carregar snapshot + replay apenas eventos posteriores
|
|
696
|
+
select state from public.snapshots where aggregate_id = $1;
|
|
697
|
+
-- Aplicar eventos com id > snapshot_lsn (poucos eventos recentes)
|
|
698
|
+
select * from public.events
|
|
699
|
+
where aggregate_id = $1 and id > (select snapshot_lsn from public.snapshots where aggregate_id = $1);
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
## Ver também
|
|
703
|
+
|
|
704
|
+
- [_shared-dados-distribuidos/glossary.md](../_shared-dados-distribuidos/glossary.md) — termos `AMQP/JMS-style broker`, `log-based broker`, `CDC`, `event sourcing`, `exactly-once semantics`, `at-least-once semantics`, `stream-stream join`, `stream-table join`, `table-table join`, `log compaction` (seção h)
|
|
705
|
+
- [audit-log-multi-tenant](../audit-log-multi-tenant/SKILL.md) — Phase 109 v1.21, audit_log É event sourcing semantics (REQ STREAMS-03 cross-ref ATIVO)
|
|
706
|
+
- [supabase-cron-queues](../supabase-cron-queues/SKILL.md) — v1.8, pgmq pattern + cleanup retention TTL
|
|
707
|
+
- [supabase-realtime](../supabase-realtime/SKILL.md) — v1.8, broadcast como CDC stream (REQ STREAMS-02 abordagem 1)
|
|
708
|
+
- [escolha-modelo-consistencia](../escolha-modelo-consistencia/SKILL.md) — Phase 121 (irmã), transactional outbox como base de exactly-once (REQ STREAMS-04 cross-ref ATIVO)
|
|
709
|
+
- [supabase-database-functions](../supabase-database-functions/SKILL.md) — v1.8, security invoker + search_path canônicos
|
|
710
|
+
- DDIA Ch 11 (Stream Processing, summary p.464) — material-fonte canônico
|
|
711
|
+
</content>
|
|
712
|
+
</invoke>
|