# Spec 10 — Migrações e Dados

> **Dono:** Core/Persistência · **Status:** Vigente · **Plano:** 08
> **Relaciona:** ADR-007, ADR-017, ADR-028, ADR-030
> **Objetivo:** contrato único de schema e dados — migrations **seguras** (nunca perdem dado),
> **idempotentes**, com dado **desacoplado da UI** e **determinístico** (ADR-001), validado pelo
> gate `tools/migration-check`.

Este contrato é **normativo**: toda migration em `src/**/Database/Migrations` segue o template e o
checklist abaixo. Conflito com uma ADR = parar e abrir ADR (AGENTS.md §0).

## 1. Princípios

1. **Só o necessário.** Cada coluna é dado de domínio; nada decorativo, duplicado ou derivável.
2. **`jsonb` quando cabível.** Conteúdo rico (AST `corpo`), metadados extensíveis (`metadados`) e
   apresentação (`apresentacao`). A UI evolui sem migration.
3. **Nunca HTML/CSS/estilo no banco.** O editor grava estrutura atômica (ADR-028); a view renderiza.
4. **Apresentação fora do schema.** "destacado", cor, ícone, ordem e largura vivem em
   `apresentacao` JSONB / `SlotConfig` (ADR-016) — **nunca** como coluna/flag.
5. **Tipos/integridade (ADR-007):** PK `uuid` com `uuidv7()` nativo; `timestamptz`; `numeric` para
   dinheiro; `text` + limite na app/`CHECK` (não `varchar(n)`); `NOT NULL` por padrão; FK com
   `onDelete` explícito; `softDeletes` em entidade editorial; índice nomeado `tabela_coluna_idx`.
6. **Determinismo:** ordem explícita, sem `now()`/`rand()` fora do contexto de migration;
   `migrate:fresh` sem seeder tem de deixar o sistema utilizável.

## 2. Template canônico

```php
<?php

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;

/** @see wiki/specs/10-migracoes-e-dados.md */
return new class extends Migration
{
    private const string TABLE = 'exemplo';

    public function up(): void
    {
        DB::statement("SET lock_timeout = '3s';");

        // Idempotente: reexecutar não recria (ADR-017 §7).
        if (Schema::hasTable(self::TABLE)) {
            return;
        }

        DB::beginTransaction();

        try {
            Schema::create(self::TABLE, function (Blueprint $table): void {
                $id = $table->uuid('id')->primary();
                $id->default(new Expression('uuidv7()'));          // PK gerada pelo banco
                $table->text('titulo');                             // text, não varchar(n)
                $table->jsonb('corpo')->nullable();                 // AST estruturada, nunca HTML
                $table->jsonb('metadados')->nullable();
                $table->timestampTz('publicado_em')->nullable();
                $table->timestampsTz();
                $table->softDeletesTz();
            });

            DB::commit();
        } catch (Throwable $e) {
            DB::rollBack();
            Log::emergency('FALHA NA MIGRAÇÃO '.self::TABLE.': '.$e->getMessage());
            throw $e;
        }
    }

    public function down(): void
    {
        // down() NUNCA apaga: renomeia para backup_<tabela>_<timestamp> (ADR-017).
        $backup = 'backup_'.self::TABLE.'_'.date('Y_m_d_His');

        if (Schema::hasTable(self::TABLE)) {
            Schema::rename(self::TABLE, $backup);
            Log::alert("ROLLBACK: '".self::TABLE."' preservada como '{$backup}'.");
        }
    }
};
```

**Helpers opcionais** (`safely(callable, string)` / `renameToBackup(string)`) são aceitos quando
reduzem repetição; o comportamento é o mesmo: transação + `SET lock_timeout` + log estruturado +
`down()` sem perda.

## 3. Idempotência e guardas (ADR-017 §7)

- `Schema::hasTable()` antes de `Schema::create()`; `Schema::hasColumn()` antes de adicionar coluna.
- DDL cru idempotente: `CREATE TABLE IF NOT EXISTS`, `CREATE INDEX IF NOT EXISTS`,
  `DROP TRIGGER IF EXISTS` antes de `CREATE TRIGGER`, `CREATE OR REPLACE FUNCTION`.
- Dado de referência com `ON CONFLICT ... DO NOTHING` / `insertOrIgnore` (nunca duplica).
- **Teste obrigatório:** `tests/Feature/MigrationIdempotencyTest` — reexecutar `up()` não lança;
  `migrate:fresh` sem seeder funciona; `down()` preserva dados.

## 4. Dados de referência × seeds

- **Dado necessário à operação vem da migration/implementação**, idempotente (ex.: perfil
  `suporte`, presets, categorias de sistema, flags). Nunca é pré-requisito de seeder.
- **Seeders são exclusivamente demonstração** (`db:seed`/`make reset`), nunca chamados
  por `make up` ou produção, e vivem **fora** das camadas de produção:
  `src/demonstration/{Module}/{Seed}.php` (`Demonstration\{Module}` — ADR-041).
- Sem HTML/CSS em seeders.

## 5. Busca e índices

- Full-text em PG 18: coluna **gerada** `GENERATED ALWAYS AS (...) STORED` (`tsvector`,
  `portuguese`/`portuguese` config) + índice **GIN** — trigger só quando a expressão não couber.
- `CREATE INDEX CONCURRENTLY` **fora** de transação/helper transacional.
- Índices parciais para filtros comuns (ex.: `WHERE deleted_at IS NULL`).

## 6. LGPD e PII

- PII real (CPF, endereço, contato) usa o cast **`encrypted` nativo** do Laravel.
- **Blind index** (função **BLAKE3**, ADR-029) **só** quando houver busca sobre campo cifrado.
- Sem criptografia/cast customizado. Anonimização de IP (não cripto) pode usar SHA-256.

## 7. Auditoria imutável

- Trilha em tabela particionada (`core.core_audit_logs`) com trigger que **bloqueia UPDATE/DELETE**
  (PL/pgSQL). `down()` preserva via `backup_*`.

## 8. Gate `tools/migration-check`

Roda como selftest (fixtures `valid`/`invalid`) + árvore real. **Falha** quando:

| Regra | Falha em |
|---|---|
| `no-drop` | `Schema::drop*`, `dropColumn`, `DROP TABLE/COLUMN` dentro de `down()` |
| `ui-column` | coluna `*_html`/`*_css`/`*_style`/`*_class`/`style` |
| `rich-jsonb` | conteúdo rico (`corpo`/`content`/`body`/`conteudo`/`blocks`/`schema`) como `string`/`text` |
| `presentation-flag` | `is_highlighted` / `is_super_highlighted` como coluna |

- **Alerta:** `CREATE INDEX CONCURRENTLY` dentro de transação.
- **Exceção justificada:** comentar `@migration-check-exception: <regra>` (sem regra = arquivo),
  com o motivo e o prazo/ADR que a remove.

## 9. Checklist de PR (migration)

- [ ] Segue o template (§2): transação + `lock_timeout` + log; `down()` só `backup_*`.
- [ ] Guardas de idempotência (§3) e refs via `ON CONFLICT`/`insertOrIgnore`.
- [ ] Nenhuma coluna de UI/HTML/CSS; conteúdo rico é `jsonb`.
- [ ] FK com `onDelete` explícito; índices nomeados; `uuidv7()`/`timestamptz` (ADR-007).
- [ ] Alteração de coluna em passos seguros (expand & contract, spec 12 §4).
- [ ] `make exec cmd='php tools/migration-check'` verde; teste de execução `up`/`down` quando aplicável.

## 10. Verificação

- `make exec cmd='php tools/migration-check'` (gate) e `make exec cmd='bash tools/gates-selftest.sh'` (selftest em fixtures) verdes.
- `MigrationIdempotencyTest` verde: `migrate:fresh` sem seeder, refs idempotentes, `down()`→`backup_*`.
- Nenhum dado operacional depende de seeder; `make reset` (dev) rodado 2× não duplica.
