feat(db): o baseline instala em Postgres 15, e o gate testa no piso

O projeto exigia pg17 sem que ninguém tivesse decidido isso. O `baseline.sql` é
um `pg_dump --schema-only` tirado de um projeto Supabase gerenciado rodando
pg17 (supabase/migrations/MANIFEST.md:3), e ao serializar o ACL das quatro
tabelas append-only o dump emitiu, sozinho, nove `GRANT … MAINTAIN` —
privilégio que só existe a partir do pg17.

Medido antes de mexer: essas nove linhas são a ÚNICA construção acima de pg15
em todo o arquivo (17.079 linhas) e nas 185 migrations. O piso real é pg15,
por `security_invoker` em view (baseline.sql:1215). Nenhum código do projeto
usa MAINTAIN; nenhum teste o assertava.

O custo era desproporcional à causa. O instalador aplica o baseline com
`ON_ERROR_STOP=1`, então um token que o servidor não conhece aborta o ARQUIVO
INTEIRO: quem apontasse para um Postgres 15 ou 16 — o padrão de boa parte dos
templates prontos de Supabase — não ficava com schema incompleto, ficava sem
schema. E o `config.toml` já tinha CEDIDO ao dump uma vez: era
`major_version = 15` e foi empurrado para 17 (docs/testing/HANDOFF-vps-qa.md:61-63)
porque contribuidor rodando `supabase start` pegava pg15 e o baseline quebrava.
Este commit resolve na direção contrária, que é a que preserva instalações.

O que muda
----------
- baseline.sql: sai o token `MAINTAIN` das nove listas de privilégio. O resto
  de cada lista fica intacto, então o append-only continua vindo de onde sempre
  veio — da AUSÊNCIA de UPDATE/DELETE, não da presença de MAINTAIN.
- config.toml: major_version 17 → 15.
- scripts/test-db.sh e scripts/smoke-llm.sh: pgvector:pg17 → pg15. Testar em
  pg17 um produto que dizemos instalar em pg15 mede a instalação mais rica que
  temos à mão, não a mais pobre que prometemos.

O hostgator-setup-kit NÃO muda: seus 14 `postgres:17-alpine` são CLIENTE psql e
pg_dump, e cliente 17 contra servidor 15 é suportado (a direção proibida é a
inversa).

Prova
-----
tests/unit/baseline-no-piso-do-postgres.test.ts, 13 casos. Ele varre o baseline
por nove famílias de construção acima de pg15 e cobra a coerência entre o
arquivo, o config.toml e a imagem do gate de banco.

A causa é estrutural e vai voltar: toda regeneração do baseline a partir de um
Supabase mais novo pode trazer sintaxe nova que ninguém pediu, num arquivo de
17 mil linhas que ninguém revisa. Por isso o gate é mecânico e traz DOIS
controles — um negativo (o arquivo foi mesmo lido: tamanho e marco conhecido) e
um positivo (cada padrão é confrontado com uma isca que ele tem de reprovar).
Sem o positivo, um regex quebrado deixaria a suíte verde medindo o vazio.

Sabotagem verificada: devolvendo os MAINTAIN e o major_version=17, o teste
reprova exatamente esses dois casos e mais nenhum.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
gabigurgeldev
2026-08-29 12:01:11 -03:00
co-authored by Claude Opus 5
parent 26d384c672
commit 6bfb48d539
9 changed files with 240 additions and 18 deletions
+24
View File
@@ -0,0 +1,24 @@
---
impacto: capacidade_nova
secao: alterado
titulo: O CRM instala em Postgres 15, não só em 17
---
Até agora a instalação exigia Postgres 17. Quem tentasse usar um banco 15 ou 16
— o padrão de boa parte dos painéis de VPS e dos templates prontos de Supabase
— via a montagem do banco parar no meio, e a instalação terminava sem as
tabelas.
A exigência nunca foi uma decisão de projeto. O arquivo que monta o banco é
gerado automaticamente a partir de um servidor de referência, e esse servidor
rodava a versão 17; ao ser gerado, o arquivo levou junto nove linhas com uma
permissão que só existe nessa versão. Nenhuma parte do sistema usa essa
permissão. Bastava o banco não reconhecê-la para o arquivo inteiro ser
recusado — e um arquivo recusado é um banco vazio, não um banco incompleto.
As nove linhas saíram. A permissão que sobrou em cada uma é exatamente a mesma
de antes, então nada muda no comportamento nem na proteção das tabelas de
auditoria, que continuam não aceitando alteração nem exclusão.
Quem já roda o CRM não precisa fazer nada: o Postgres 17 segue funcionando
igual. O que mudou é que 15 e 16 passaram a funcionar também.
+1 -1
View File
@@ -81,7 +81,7 @@ mudança toca schema, RLS ou UI, `gov:verify` verde **não** é prova — rode `
**O que o CI cobre.** `.github/workflows/ci.yml`: `verify` = typecheck + lint + test:unit;
`invariants` = `pnpm test:db` (isolamento RLS + invariantes de governança contra Postgres
efêmero pg17). `.github/workflows/perf.yml`: `build-and-size` = `pnpm build`.
efêmero pg15). `.github/workflows/perf.yml`: `build-and-size` = `pnpm build`.
`.github/workflows/e2e.yml` roda **45 das 46 specs** Playwright contra um Supabase local de
verdade com o `baseline.sql` aplicado — o mesmo banco que o self-hoster tem. **É check
obrigatório desde 2026-08-08.** A **única** de fora é `vps-fresh-onboarding` (WAHA + Redis +
+3 -3
View File
@@ -335,7 +335,7 @@ No CI não há `UPSTASH` nenhum, então lá o caminho é o contador em memória
Checks **obrigatórios** na branch protection da `main` (verificado na configuração, não só no papel):
- **`verify`** (`ci.yml`) — typecheck + lint + test:unit.
- **`invariants`** (`ci.yml`) — `pnpm test:db`: sobe `pgvector/pgvector:pg17`, aplica `supabase/baseline.sql` em modo install (`ON_ERROR_STOP=1`) e update (idempotência), e roda os testes de invariante, incluindo o de isolamento RLS entre 2 organizações.
- **`invariants`** (`ci.yml`) — `pnpm test:db`: sobe `pgvector/pgvector:pg15` — o PISO que dizemos suportar, não a versão mais rica que temos à mão —, aplica `supabase/baseline.sql` em modo install (`ON_ERROR_STOP=1`) e update (idempotência), e roda os testes de invariante, incluindo o de isolamento RLS entre 2 organizações.
- **`build-and-size`** (`perf.yml`) — `pnpm build` em Node 22.
- **`e2e`** (`e2e.yml`) — sobe Supabase local, aplica o `baseline.sql` e roda **todas as specs Playwright menos uma**. O número saiu daqui de propósito: ele apodreceu **cinco** vezes (a quinta em 2026-08-24, quando `inbox-quem-manda.spec.ts` entrou), e a condição que o PR #242 pôs para parar de recontar já tinha vencido na quarta. Quem precisa do número roda o comando abaixo — comando não envelhece. A **única** de fora é `vps-fresh-onboarding` (precisa de WAHA + Redis + Resend + Nuvemshop) — e ela é a **P0** da doutrina de QA Visual, ou seja, `e2e` verde **não** prova a jornada de instalação fresca, que é o produto que se vende.
@@ -390,7 +390,7 @@ Ao mexer em schema, RLS, RBAC, atribuição, escopo, roteamento, follow-up, webh
**Medidas de front-end por ferramenta, nunca a olho** (`getBoundingClientRect`/`getComputedStyle` no Playwright). Ver `feedback_protocolo_execucao_visivel` na memória.
**Receita de ambiente fresco (não-óbvia):** banco = `baseline.sql` num Supabase local **pg17** (`config.toml major_version = 17`; o baseline usa `GRANT MAINTAIN`, privilégio pg17+); `next build` + `next start` (produção — `next dev` compila lento demais e o Turbopack quebra `cookies()`); **worktree com `node_modules` real, nunca symlink** (Turbopack rejeita symlink "out of filesystem root") e **fora de `/tmp`** (é limpo no meio da sessão — commite cada marco). Detalhes em [[project_invite_e2e_and_bugs]].
**Receita de ambiente fresco (não-óbvia):** banco = `baseline.sql` num Supabase local **pg15** (`config.toml major_version = 15`). Já foi pg17, por causa de 9 `GRANT MAINTAIN` que o `pg_dump` emitiu sozinho; hoje quem guarda o piso é `tests/unit/baseline-no-piso-do-postgres.test.ts`; `next build` + `next start` (produção — `next dev` compila lento demais e o Turbopack quebra `cookies()`); **worktree com `node_modules` real, nunca symlink** (Turbopack rejeita symlink "out of filesystem root") e **fora de `/tmp`** (é limpo no meio da sessão — commite cada marco). Detalhes em [[project_invite_e2e_and_bugs]].
---
@@ -418,7 +418,7 @@ Processo padrão (siga sempre):
4. **Data migrations genéricas**: se a migration corrige/deduplica dados, escreva pensando em QUALQUER banco de clone (não hardcode IDs do seu tenant). Repointe FKs conferindo o catálogo (`information_schema` FK map) para não perder histórico.
5. **Registre no MANIFEST**: adicione uma linha em `supabase/migrations/MANIFEST.md` (tabela "Applied") descrevendo versão, nome e o QUÊ/PORQUÊ.
6. **Reflita no `supabase/baseline.sql` (OBRIGATÓRIO — é o que o kit self-host aplica).** O baseline é um dump `--schema-only` + um **apêndice idempotente** no fim do arquivo (blocos rotulados `-- ---- <coisa> (migration NNNN) ----`). O kit HostGator aplica **só o baseline.sql**, tanto no `install.sh` (banco novo, `ON_ERROR_STOP=1`) quanto no `update.sh` (re-aplica em banco existente, **sem** `ON_ERROR_STOP`). Então toda mudança de schema pós-snapshot DEVE ser acrescentada ao apêndice, **idempotente e auto-curativa**: `add column if not exists`, `create ... if not exists`, `create or replace function`, e — se a mudança adiciona constraint — **deduplicar/corrigir os dados ANTES** de criar a constraint (senão o `update.sh` de um clone bugado quebra). Sem isto, clones não recebem a mudança (ou quebram ao atualizar). Migração adicionada só em `migrations/` mas não no baseline **não chega aos self-hosters**.
7. **Aplique e prove**: aplique via `mcp__plugin_supabase_supabase__apply_migration` (ou `supabase db push`), capture o estado ANTES/DEPOIS e prove invariantes (ex.: contagem de linhas que não pode mudar). Se mexeu em contrato, regenere `lib/database.types.ts`. Para mudanças de schema no kit, valide o baseline num Postgres descartável (`pgvector/pgvector:pg17` + extensões) aplicando `install` (fresh, `ON_ERROR_STOP=1`) e `update` (re-aplicar, sem a flag) — ambos têm que passar.
7. **Aplique e prove**: aplique via `mcp__plugin_supabase_supabase__apply_migration` (ou `supabase db push`), capture o estado ANTES/DEPOIS e prove invariantes (ex.: contagem de linhas que não pode mudar). Se mexeu em contrato, regenere `lib/database.types.ts`. Para mudanças de schema no kit, valide o baseline num Postgres descartável (`pgvector/pgvector:pg15` + extensões) aplicando `install` (fresh, `ON_ERROR_STOP=1`) e `update` (re-aplicar, sem a flag) — ambos têm que passar.
8. **Backfill de dados quebrados existentes**: constraint nova falha se os dados atuais a violam — a migration (e o apêndice do baseline) deve deduplicar/corrigir ANTES de criar a constraint.
9. **Função nova em `public` nasce EXPOSTA — revogue as DUAS origens.** Toda `create function` no schema `public` termina com:
+2 -2
View File
@@ -53,7 +53,7 @@ verificados por leitura de arquivo, config e workflow.
| H2 — Reproduzível | ✅ | Quickstart no README, `docs/SETUP.md`, `.nvmrc` (22), `packageManager` fixo, `pnpm-lock.yaml`, `docker-compose.yml`, `install.sh` do kit self-host, `baseline.sql` |
| H3 — Verificável | ✅ | `lint` + `typecheck` + `test:unit` + `build`; CI roda os 3 primeiros em PR |
| H4 — Preparado para agentes | ✅ | `CLAUDE.md` doutrinal forte; `AGENTS.md` **criado nesta auditoria**; documentação técnica extensa; **e o CI roda o gate de isolamento RLS** (job `invariants` → `pnpm test:db`) |
| H5 — Automação avançada | ⚠️ **parcial** | CI confiável e ambiente isolado ✅ (Postgres efêmero pg17, worktrees, gov-loop com maker≠checker e hash-check). Falta: **1 das 46 specs E2E fora do CI** (45 rodam via `e2e.yml`, **obrigatório desde 2026-08-08**; a de fora é `vps-fresh-onboarding`, que é justamente a P0), `format:check` fora do CI, e o comando único local (`gov:verify`) não cobre `test:db`/`test:e2e`. *(Números recontados em 2026-08-14 @ `741c4ec8`; a redação anterior — "4 das 32, não-obrigatório" — apodreceu.)* |
| H5 — Automação avançada | ⚠️ **parcial** | CI confiável e ambiente isolado ✅ (Postgres efêmero pg15, worktrees, gov-loop com maker≠checker e hash-check). Falta: **1 das 46 specs E2E fora do CI** (45 rodam via `e2e.yml`, **obrigatório desde 2026-08-08**; a de fora é `vps-fresh-onboarding`, que é justamente a P0), `format:check` fora do CI, e o comando único local (`gov:verify`) não cobre `test:db`/`test:e2e`. *(Números recontados em 2026-08-14 @ `741c4ec8`; a redação anterior — "4 das 32, não-obrigatório" — apodreceu.)* |
**Por que H4 e não H5:** a instrução da auditoria é explícita — não atribuir nível só
porque os arquivos existem, avaliar se o processo está implementado. Aqui está: o gate de
@@ -100,7 +100,7 @@ Legenda: ✅ existente e funcional · ⚠️ existente mas incompleto · ❌ nã
| 17 | Documentação arquitetural | ✅ | `ARCHITECTURE.md` (1 página) + `docs/specs/` (16 docs com schema e payloads) + `docs/architecture/agent-turn` + `graphify-out/` |
| 18 | Regras para agentes de IA | ✅ | `CLAUDE.md` doutrinal (convenções não-negociáveis, anti-patterns, doutrinas de migration/QA/branch), `.claude/agents/` com frota especializada, `loop/` com maker≠checker. **`AGENTS.md` criado nesta auditoria** — antes, agentes não-Claude entravam sem contexto |
| 19 | Critérios de conclusão de tarefa | ✅ | Definition of Done de 13 itens em `CLAUDE.md`; `docs/doctrine/sistema-vivo.md` com o Living System Checklist; template de PR com o checklist |
| 20 | Ambiente reproduzível | ✅ | `docker-compose.yml` (dev), `.prod.yml`, `Dockerfile` + `Dockerfile.worker`, `baseline.sql` auto-curativo cobrindo até a migration 0092, `scripts/test-db.sh` com Postgres efêmero pg17 rodando em CI. ⚠️ A receita de ambiente fresco tem armadilhas que só existem em doc (pg17 obrigatório, `node_modules` real e não symlink, fora de `/tmp`) — reproduzível, mas com conhecimento tácito |
| 20 | Ambiente reproduzível | ✅ | `docker-compose.yml` (dev), `.prod.yml`, `Dockerfile` + `Dockerfile.worker`, `baseline.sql` auto-curativo cobrindo até a migration 0092, `scripts/test-db.sh` com Postgres efêmero pg15 rodando em CI. ⚠️ A receita de ambiente fresco tem armadilhas que só existem em doc (`node_modules` real e não symlink, fora de `/tmp`) — reproduzível, mas com conhecimento tácito |
---
+1 -1
View File
@@ -7,7 +7,7 @@ set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PORT="${SMOKE_DB_PORT:-54331}"
CONTAINER="deskcomm-smoke-db-$$"
IMAGE="pgvector/pgvector:pg17"
IMAGE="pgvector/pgvector:pg15"
[ -n "${ANTHROPIC_API_KEY:-}" ] || { echo "FATAL: exporte ANTHROPIC_API_KEY (o smoke usa o modelo real)" >&2; exit 1; }
+8 -1
View File
@@ -36,7 +36,14 @@ PUBLICACAO="127.0.0.1::5432"
DONO_WORKTREE="$ROOT"
DONO_BRANCH="$(git -C "$ROOT" branch --show-current 2>/dev/null || echo desconhecida)"
CONTAINER="deskcomm-test-db-$$"
IMAGE="pgvector/pgvector:pg17"
# pg15 e não pg17: o piso real do baseline é pg15 (`security_invoker` em view,
# baseline.sql:1215). O 17 vinha de 9 `GRANT … MAINTAIN` que o `pg_dump` de um
# projeto Supabase pg17 emitiu sozinho ao serializar o ACL das tabelas
# append-only — ninguém os escreveu, e nenhum código do projeto usa o
# privilégio. Testar no piso é o que faz este gate cobrir a instalação mais
# pobre que dizemos suportar, em vez da mais rica que temos à mão.
# Quem guarda o piso é tests/unit/baseline-no-piso-do-postgres.test.ts.
IMAGE="pgvector/pgvector:pg15"
# O baseline é aplicado UMA vez, num banco-MOLDE. Cada ARQUIVO de tests/invariants
# recebe uma cópia nova dele — `create database postgres template $TEMPLATE`, ~0,2s
# medidos — feita pelo setupFile declarado em vitest.db.config.ts.
+9 -9
View File
@@ -4712,9 +4712,9 @@ GRANT ALL ON TABLE "public"."ai_provider_credentials_safe" TO "service_role";
GRANT SELECT,INSERT,REFERENCES,TRIGGER,TRUNCATE,MAINTAIN ON TABLE "public"."api_audit_log" TO "anon";
GRANT SELECT,INSERT,REFERENCES,TRIGGER,TRUNCATE,MAINTAIN ON TABLE "public"."api_audit_log" TO "authenticated";
GRANT SELECT,INSERT,REFERENCES,TRIGGER,TRUNCATE,MAINTAIN ON TABLE "public"."api_audit_log" TO "service_role";
GRANT SELECT,INSERT,REFERENCES,TRIGGER,TRUNCATE ON TABLE "public"."api_audit_log" TO "anon";
GRANT SELECT,INSERT,REFERENCES,TRIGGER,TRUNCATE ON TABLE "public"."api_audit_log" TO "authenticated";
GRANT SELECT,INSERT,REFERENCES,TRIGGER,TRUNCATE ON TABLE "public"."api_audit_log" TO "service_role";
@@ -4748,8 +4748,8 @@ GRANT ALL ON TABLE "public"."conversations" TO "service_role";
GRANT SELECT,INSERT,REFERENCES,TRIGGER,TRUNCATE,MAINTAIN ON TABLE "public"."crm_lead_activities" TO "anon";
GRANT SELECT,INSERT,REFERENCES,TRIGGER,TRUNCATE,MAINTAIN ON TABLE "public"."crm_lead_activities" TO "authenticated";
GRANT SELECT,INSERT,REFERENCES,TRIGGER,TRUNCATE ON TABLE "public"."crm_lead_activities" TO "anon";
GRANT SELECT,INSERT,REFERENCES,TRIGGER,TRUNCATE ON TABLE "public"."crm_lead_activities" TO "authenticated";
GRANT ALL ON TABLE "public"."crm_lead_activities" TO "service_role";
@@ -4778,8 +4778,8 @@ GRANT ALL ON TABLE "public"."crm_stages" TO "service_role";
GRANT SELECT,REFERENCES,TRIGGER,TRUNCATE,MAINTAIN ON TABLE "public"."event_log" TO "anon";
GRANT SELECT,REFERENCES,TRIGGER,TRUNCATE,MAINTAIN ON TABLE "public"."event_log" TO "authenticated";
GRANT SELECT,REFERENCES,TRIGGER,TRUNCATE ON TABLE "public"."event_log" TO "anon";
GRANT SELECT,REFERENCES,TRIGGER,TRUNCATE ON TABLE "public"."event_log" TO "authenticated";
GRANT ALL ON TABLE "public"."event_log" TO "service_role";
@@ -4861,8 +4861,8 @@ GRANT ALL ON TABLE "public"."user_recovery_codes" TO "service_role";
GRANT SELECT,REFERENCES,TRIGGER,TRUNCATE,MAINTAIN ON TABLE "public"."webhook_events_log" TO "anon";
GRANT SELECT,REFERENCES,TRIGGER,TRUNCATE,MAINTAIN ON TABLE "public"."webhook_events_log" TO "authenticated";
GRANT SELECT,REFERENCES,TRIGGER,TRUNCATE ON TABLE "public"."webhook_events_log" TO "anon";
GRANT SELECT,REFERENCES,TRIGGER,TRUNCATE ON TABLE "public"."webhook_events_log" TO "authenticated";
GRANT ALL ON TABLE "public"."webhook_events_log" TO "service_role";
+1 -1
View File
@@ -14,7 +14,7 @@ max_rows = 1000
[db]
port = 54322
shadow_port = 54320
major_version = 17
major_version = 15
[db.pooler]
enabled = false
@@ -0,0 +1,191 @@
/**
* O `baseline.sql` não pode exigir Postgres acima do piso que dizemos suportar.
*
* POR QUE ESTE GATE EXISTE
*
* O piso real do arquivo é **pg15** — `security_invoker` em view
* (`baseline.sql:1215`), pg15+. Mesmo assim o projeto passou a exigir **pg17**,
* e não por decisão de ninguém: o baseline é um `pg_dump --schema-only` tirado
* de um projeto Supabase gerenciado rodando pg17 (`supabase/migrations/MANIFEST.md:3`),
* e o dump serializou o ACL das 4 tabelas append-only emitindo `GRANT … MAINTAIN`
* — privilégio que só existe a partir do pg17. Nove tokens. Nenhum autor os
* escreveu, nenhum código do projeto usa o privilégio, nenhum teste o assertava.
*
* O `supabase/config.toml` então CEDEU ao dump: era `major_version = 15` e foi
* empurrado para 17 (`docs/testing/HANDOFF-vps-qa.md:61-63`) porque contribuidor
* rodando `supabase start` pegava pg15 e o baseline quebrava.
*
* O acoplamento é de PARSE, não de runtime: `install.sh` aplica o baseline com
* `ON_ERROR_STOP=1`, então UM token desconhecido aborta o arquivo inteiro e o
* clone fica sem schema. Não é degradação — é instalação que não acontece.
*
* A causa é estrutural e vai se repetir: toda vez que alguém regerar o baseline
* a partir de um Supabase mais novo, o `pg_dump` pode emitir sintaxe nova sem
* que ninguém tenha pedido. Prosa não pega isso — o arquivo tem 17 mil linhas e
* ninguém revisa um dump. Por isso o gate é mecânico.
*
* O QUE ELE NÃO É
*
* Não é um parser de SQL nem um validador de versão. É uma catraca sobre uma
* lista de tokens conhecidos, deliberadamente estreita: prefere deixar passar
* uma construção que ninguém previu a reprovar um dump legítimo por falso
* positivo. Quando um token novo aparecer, ele entra nesta lista.
*/
import { readFileSync } from "node:fs";
import path from "node:path";
import { assert, describe, expect, it } from "vitest";
const RAIZ = path.resolve(__dirname, "../..");
const BASELINE = path.join(RAIZ, "supabase/baseline.sql");
/**
* Cada entrada: o que procurar, em que versão apareceu, e o que dizer a quem
* for consertar. A mensagem importa tanto quanto a detecção — quem topa com
* isto meses depois não tem o contexto que temos hoje.
*/
const ACIMA_DO_PISO: ReadonlyArray<{
nome: string;
desde: string;
padrao: RegExp;
saida: string;
}> = [
{
nome: "GRANT … MAINTAIN",
desde: "pg17",
padrao: /\bMAINTAIN\b/g,
saida:
"remova só o token da lista de privilégios (`,MAINTAIN` → nada). " +
"Ele concede VACUUM/ANALYZE/REINDEX e nenhum código usa; o append-only " +
"das tabelas de auditoria vem da AUSÊNCIA de UPDATE/DELETE, não daqui.",
},
{
nome: "JSON_TABLE / JSON_VALUE / JSON_QUERY / JSON_EXISTS",
desde: "pg17",
padrao: /\bJSON_(TABLE|VALUE|QUERY|EXISTS)\s*\(/gi,
saida: "reescreva com os operadores jsonb clássicos (`->`, `->>`, `jsonb_path_query`).",
},
{
nome: "random() com dois argumentos",
desde: "pg17",
padrao: /\brandom\s*\([^)]+,/gi,
saida: "use `random()` sem argumentos e faça a escala em SQL.",
},
{
nome: "COPY … ON_ERROR",
desde: "pg17",
padrao: /\bON_ERROR\s+(ignore|stop)\b/gi,
saida: "o baseline não deveria carregar dados por COPY tolerante a erro.",
},
{
nome: "SPLIT PARTITION / MERGE PARTITIONS",
desde: "pg17",
padrao: /\b(SPLIT\s+PARTITION|MERGE\s+PARTITIONS)\b/gi,
saida: "o schema não tem partições; se passar a ter, o piso sobe junto.",
},
{
nome: "uuid_extract_timestamp / to_bin / to_oct / pg_basetype",
desde: "pg17",
padrao: /\b(uuid_extract_timestamp|to_bin|to_oct|pg_basetype)\s*\(/gi,
saida: "calcule na aplicação, não no schema.",
},
{
nome: "SYSTEM_USER",
desde: "pg16",
padrao: /\bSYSTEM_USER\b/g,
saida: "use `current_user` / `session_user`.",
},
{
nome: "pg_input_is_valid",
desde: "pg16",
padrao: /\bpg_input_is_valid\s*\(/gi,
saida: "valide com CHECK explícito ou na aplicação.",
},
{
nome: "any_value / array_shuffle / array_sample",
desde: "pg16",
padrao: /\b(any_value|array_shuffle|array_sample)\s*\(/gi,
saida: "use min()/max() ou ordene na aplicação.",
},
];
function ocorrencias(sql: string, padrao: RegExp): number[] {
const linhas: number[] = [];
sql.split("\n").forEach((linha, i) => {
// `lastIndex` de um regex /g é estado: sem zerar, a busca da linha N+1
// começa de onde a da linha N parou e some com achados.
padrao.lastIndex = 0;
if (padrao.test(linha)) linhas.push(i + 1);
});
return linhas;
}
describe("o baseline fica no piso de Postgres que dizemos suportar", () => {
const sql = readFileSync(BASELINE, "utf8");
it("o arquivo foi lido de verdade — controle negativo antes de qualquer conclusão", () => {
// Um `readFileSync` que devolvesse vazio faria TODOS os casos abaixo
// passarem, medindo o nada. Duas âncoras: tamanho e um marco conhecido.
expect(sql.length).toBeGreaterThan(500_000);
expect(sql).toContain("public.organizations");
});
it.each(ACIMA_DO_PISO)("não usa $nome ($desde)", ({ nome, desde, padrao, saida }) => {
const linhas = ocorrencias(sql, padrao);
expect(
linhas,
linhas.length === 0
? ""
: `supabase/baseline.sql usa ${nome}, que exige ${desde}, nas linhas ` +
`${linhas.slice(0, 12).join(", ")}${linhas.length > 12 ? ` (+${linhas.length - 12})` : ""}.\n` +
`O instalador aplica o baseline com ON_ERROR_STOP=1: um token que o servidor ` +
`não conhece aborta o ARQUIVO INTEIRO e o clone fica sem schema.\n` +
`Saída: ${saida}`,
).toEqual([]);
});
it("os padrões estão vivos — controle positivo, senão o gate mede o vazio", () => {
// Sem isto, um regex quebrado (ou um `ocorrencias` que sempre devolve [])
// deixaria a suíte verde para sempre. Cada padrão é confrontado com um
// trecho que ELE tem de reprovar.
const iscas: Record<string, string> = {
"GRANT … MAINTAIN": 'GRANT SELECT,MAINTAIN ON TABLE "public"."x" TO "anon";',
"JSON_TABLE / JSON_VALUE / JSON_QUERY / JSON_EXISTS": "select JSON_VALUE(a, '$.b') from t;",
"random() com dois argumentos": "select random(1, 10);",
"COPY … ON_ERROR": "COPY t FROM stdin WITH (ON_ERROR ignore);",
"SPLIT PARTITION / MERGE PARTITIONS": "ALTER TABLE t SPLIT PARTITION p INTO (x);",
"uuid_extract_timestamp / to_bin / to_oct / pg_basetype": "select to_bin(42);",
SYSTEM_USER: "select SYSTEM_USER;",
pg_input_is_valid: "select pg_input_is_valid('x', 'integer');",
"any_value / array_shuffle / array_sample": "select any_value(x) from t;",
};
for (const { nome, padrao } of ACIMA_DO_PISO) {
const isca = iscas[nome];
// `assert` e não `expect(...).toBeDefined()`: só o assert estreita o tipo
// para o `ocorrencias` abaixo — com o expect, o `undefined` sobrevive ao
// typecheck e o erro só apareceria no CI.
assert(isca !== undefined, `falta isca para "${nome}"`);
expect(
ocorrencias(isca, padrao),
`o padrão de "${nome}" não reprovou a própria isca — ele está cego`,
).toEqual([1]);
}
});
it("o piso declarado no config.toml acompanha", () => {
// O config.toml já cedeu ao baseline uma vez (15 → 17). Se ele voltar a
// divergir, contribuidor roda `supabase start` e pega um servidor que não
// corresponde ao que o CI testa — foi exatamente o achado M1 de
// docs/testing/user-journey-map.md.
const toml = readFileSync(path.join(RAIZ, "supabase/config.toml"), "utf8");
expect(toml).toMatch(/^\s*major_version\s*=\s*15\s*$/m);
});
it("o gate de banco roda no piso, não acima dele", () => {
// Testar em pg17 um produto que dizemos instalar em pg15 é medir a
// instalação mais rica que temos à mão, não a mais pobre que prometemos.
const script = readFileSync(path.join(RAIZ, "scripts/test-db.sh"), "utf8");
expect(script).toMatch(/^IMAGE="pgvector\/pgvector:pg15"$/m);
});
});