# ADR-007 — PostgreSQL 18 em container, sem porta exposta

| Campo | Valor |
|---|---|
| **Status** | Aceito (2026-10-16) |
| **Data** | 2026-10-14 |
| **Plano de origem** | `wiki/plan/01-analise-e-decisoes.md` |
| **Relacionadas** | ADR-005, ADR-017 |

## Contexto

Na primeira hipótese, o banco era o **PostgreSQL 10.23 do servidor cPanel** — tratado como
**inegociável** e responsável por uma longa lista de restrições (sem colunas geradas, sem
`INCLUDE`, sem `MERGE`, full-text só por trigger, alteração de coluna em 3 passos). Com a
mudança para **containers independentes** (ADR-005), o banco deixa de depender do WHM: roda
**inteiramente em container**, com a versão que escolhermos, **sem porta publicada**.

O PostgreSQL **18** é a versão estável atual e traz ganhos diretos: **`uuidv7()` nativo**,
**I/O assíncrono** (`io_method`), *skip scan*, e os recursos modernos que antes estavam
proibidos. As skills de referência já descrevem o PG 18.

## Decisão

- **Alvo: `postgres:18-trixie@sha256:74935e72241653ca55e0414067e6d8763aceb8a810eb51b452253ec3dcfc4336`**
  (pin por **digest**; matriz no ADR-031), **um container por instância**,
  volume de dados próprio, **sem porta publicada** (só rede interna do Compose).
- **Tipos e convenções** (mantidos por clareza/interoperabilidade): PK `uuid`; `timestamptz`
  para instantes; `numeric` para dinheiro; `text` com `CHECK` (não `varchar(n)`); `jsonb`
  para conteúdo; `NOT NULL` por padrão quando a ausência não é significativa.
- **Recursos do PG 18 agora permitidos** (não mais proibidos):
  - `uuidv7()` **nativo** — PK gerado no banco por padrão (dispensa geração na app);
  - **colunas geradas** `GENERATED ... STORED` quando eliminam código de sincronização;
  - `CREATE INDEX ... INCLUDE`, *skip scan*, `MERGE` (usar **com parcimônia** — simplicidade);
  - full-text por coluna gerada **ou** trigger; índice **GIN** quando fizer sentido.
- **Regras que continuam valendo** (segurança de dados, ADR-017):
  `down()` só renomeia para `backup_*`; `CREATE INDEX CONCURRENTLY` **fora** de transação;
  alteração de coluna em passos seguros (add nullable → backfill em lotes → `SET NOT NULL`);
  papéis de banco com **privilégio mínimo**; auditoria imutável por trigger; WORM opcional.
- **Tuning do container** (arquivo `docker/postgres/postgresql.conf` montado read-only),
  dimensionado por variável de ambiente em dev e por orçamento do host em prod:
  `shared_buffers`, `effective_cache_size`, `work_mem`, `maintenance_work_mem`,
  `random_page_cost=1.1` (SSD), `effective_io_concurrency`, `io_method=worker` (I/O assíncrono
  do PG 18), `wal_compression=on`, `max_wal_size`, `checkpoint_timeout`, `jit=on`,
  `pg_stat_statements` ligado.
- `pg_stat_statements` + `auto_explain` (dev/`debug`) para validar índices com
  `EXPLAIN (ANALYZE, BUFFERS)`.
- `tools/migration-check` deixa de validar o **teto PG 10** e passa a validar o **piso PG 18**
  (rejeitar sintaxe removida/inexistente) e as regras de segurança de dados do ADR-017.

## Justificativa (alternativas descartadas)

- **PostgreSQL 10.23 do WHM** — descartada: era limitação do host, não requisito de produto;
  agora o banco é nosso e independente.
- **PostgreSQL 17** — descartada: o 18 é estável e traz `uuidv7()` e I/O assíncrono.
- **`varchar(n)`** — descartada: `text` + `CHECK` é mais flexível e interoperável.
- **Expor a porta do banco ao host** — descartada: desnecessário e amplia a superfície;
  acesso via `docker compose exec`/`docker compose exec postgres psql`.

## Consequências

**Positivas**
- Banco moderno, independente do WHM, **não exposto** na rede do host.
- `uuidv7()` nativo simplifica a geração de PK; I/O assíncrono melhora throughput.
- Sem a "lista de proibições" do PG 10 — modelagem mais direta.

**Custos / negativas**
- Backups/restore são **nossos** (não do cPanel) — ver `wiki/backlog/backup-recuperacao.md`.
- ~~Migrar dados legados de um PG 10 para o PG 18 é etapa própria (plan 15).~~
  **Emendado pela [ADR-042](ADR-042-sem-retrocompatibilidade-conteudo-migrado-a-mao.md):** não há
  migração do PG 10 — a base do V7 é nova (nasce de `migrate`) e o conteúdo do legado entra
  **manualmente** pelo CMS. O que resta é backup/restore/PITR do próprio PG 18.
- Recursos PG 18 tentam; o uso continua subordinado à **simplicidade** (ADR-001).

## Exemplo mínimo (comparativo)

```sql
-- ✅ PG 18: PK gerada pelo banco
CREATE TABLE noticia (
  id        uuid PRIMARY KEY DEFAULT uuidv7(),
  titulo    text NOT NULL,
  conteudo  jsonb NOT NULL DEFAULT '{}'::jsonb,
  publicado timestamptz
);

-- ✅ PG 18: full-text por coluna gerada (não precisa mais de trigger)
ALTER TABLE noticia ADD COLUMN busca tsvector
  GENERATED ALWAYS AS (to_tsvector('portuguese', titulo)) STORED;
CREATE INDEX noticia_busca_idx ON noticia USING GIN (busca);
```

## Verificação

- `docker compose up` sobe o PG 18; `ss -ltn` no host **não** mostra a porta do banco.
- `SELECT version();` = PostgreSQL 18.x; `SELECT uuidv7();` funciona.
- `EXPLAIN (ANALYZE, BUFFERS)` usa os índices esperados; `pg_stat_statements` ativo.
- Teste: nenhuma migration usa sintaxe inválida para o PG 18 (`tools/migration-check`).

## Referências

- `wiki/plan/08-servicos-headless-persistencia.md`; `wiki/adr/ADR-017-migrations-seguras-dados-desacoplados.md`;
  PostgreSQL 18 release notes.
