# ADR-017 — Migrations seguras e dados desacoplados da UI

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

## Contexto

O `sucesu-cms/V2/Modules/Publicacoes/Noticias/Migrations/` tem um padrão sólido (transação +
`lock_timeout`, log estruturado, `down()` que renomeia para `backup_*`, `metadata jsonb`,
`search_vector` + GIN). Porém o legado `prefeituras_v5` guardava **HTML/CSS no banco** e usava
**seeds** como bootstrap. Migrations precisam ser **seguras** (sem perda) e **desacopladas da UI**.

## Decisão

Toda migration, desde a primeira (Blueprint), segue o padrão da referência e melhora o que prendia
os dados à apresentação:

1. **Contenção de perda:** `up()`/`down()` em transação com `SET LOCAL lock_timeout = '3s'`;
   `down()` **nunca** usa `DROP`/`dropColumn` — renomeia para `backup_<tabela>_<timestamp>`.
2. **Só o necessário:** colunas para dado de domínio; nada decorativo/derivável.
3. **JSONB quando cabível:** conteúdo rico (`corpo`), metadados (`metadados`), apresentação
   (`apresentacao`).
4. **Nunca HTML nem estilo no banco.**
5. **Apresentação fora dos dados** (tema/slots/preset — ADR-016).
6. **Tipos/integridade:** PK `uuid`; `timestamptz`; FK com `onDelete` explícito; `softDeletes`;
   índices nomeados.
7. **Idempotência/guardas:** `hasTable`/`hasColumn`; tabela de chaves de importação (`key_hash` único).
8. **Busca nativa em PG 18:** `search_vector` por **coluna gerada** + índice GIN.
9. **Expand & contract** em 2 releases.
10. **Seeds não definem o sistema:** dados de referência vêm da **migration/implementação**
    (idempotente); seeders são **só demonstração**, nunca pré-requisito, sem HTML/CSS — e vivem
    **fora** das camadas de produção, em `src/demonstration/{Module}/{Seed}.php` (ADR-041).
11. **Template único:** `declare(strict_types=1)`, `return new class extends Migration`, helpers
    `safely()`/`renameToBackup()`. Gate `tools/migration-check`.

## Justificativa (alternativas descartadas)

- **`down()` com `DROP`** — descartada: risco de perda irreversível.
- **HTML/CSS em colunas** — descartada: acopla UI aos dados (erro do legado).
- **Seeds como bootstrap** — descartada: ambiente novo fica dependente de dados de demo.
- **Full-text em tabela separada/materializada** — descartada: a **coluna gerada** do PG 18
  é mais simples e sempre consistente.

## Consequências

**Positivas**
- Zero perda de dados em rollback; UI evolui sem migration; ambiente novo sobe só com `migrate`.

**Custos / negativas**
- Remoções exigem 2 releases (expand & contract).
- Gate de migrations precisa ser mantido.

## Exemplo mínimo (comparativo)

```php
// ❌ down() destrutivo
public function down(): void { Schema::dropIfExists('noticia'); }

// ✅ down() preserva
public function down(): void {
    $this->safely(fn () => $this->renameToBackup('noticia'), 'preservar noticia');
}
// backup_noticia_2026_10_14_120000
```

## Verificação

- `tools/migration-check` falha em `dropColumn` no `down()`, coluna `*_html`, conteúdo rico fora
  de `jsonb` e recurso PG > 10.
- `migrate:fresh` sem seeder deixa o sistema utilizável; rodar 2× não duplica.

## Referências

- `sucesu-cms/V2/Modules/Publicacoes/Noticias/Migrations/`; `wiki/specs/10-migracoes-e-dados.md`.
