Skip to content

Diretrizes para Migrations — Multi-Ambiente

Regra fundamental: Toda migration DEVE executar com sucesso tanto em produção quanto em staging (e em qualquer banco vazio). Migrations que assumem a existência de dados específicos quebram o pipeline de CI.


1. Nunca hardcodar UUIDs sem guard

Todo INSERT que referencia usuario_id, escola_id ou papel_id com UUID literal DEVE usar WHERE EXISTS para cada FK referenciada.

❌ Anti-padrão (quebra em staging)

sql
INSERT INTO usuarios_escola_permissoes (usuario_id, escola_id, papel_id, permissao)
SELECT 'uuid-usuario'::uuid, 'uuid-escola'::uuid, 'uuid-papel'::uuid,
       unnest(ARRAY['perm1','perm2']::permissao_area[])
ON CONFLICT (usuario_id, escola_id, papel_id, permissao) DO NOTHING;

O ON CONFLICT DO NOTHING não protege contra FK inexistente — o Postgres valida a foreign key antes do conflict check. Se escola_id não existe, a migration falha com:

ERROR: insert or update on table "..." violates foreign key constraint

✅ Padrão correto (resiliente)

sql
INSERT INTO usuarios_escola_permissoes (usuario_id, escola_id, papel_id, permissao)
SELECT 'uuid-usuario'::uuid, 'uuid-escola'::uuid, 'uuid-papel'::uuid,
       unnest(ARRAY['perm1','perm2']::permissao_area[])
WHERE EXISTS (SELECT 1 FROM escolas WHERE id = 'uuid-escola'::uuid)
  AND EXISTS (SELECT 1 FROM usuarios WHERE id = 'uuid-usuario'::uuid)
  AND EXISTS (SELECT 1 FROM papeis WHERE id = 'uuid-papel'::uuid)
ON CONFLICT (usuario_id, escola_id, papel_id, permissao) DO NOTHING;

Se qualquer referência não existir, o SELECT retorna vazio, o INSERT não executa, e a migration passa sem erro.


2. Preferir JOINs dinâmicos

Em vez de hardcodar UUIDs, derive dados de tabelas existentes:

sql
-- ✅ Seguro: só insere para dados que existem
INSERT INTO usuarios_escola_permissoes (usuario_id, escola_id, papel_id, permissao)
SELECT up.usuario_id, up.escola_id, up.papel_id,
       unnest(ARRAY['perm1','perm2']::permissao_area[])
FROM usuario_papeis up
JOIN papeis p ON p.id = up.papel_id
WHERE p.nome = 'coordenador'
  AND up.ativo = true
ON CONFLICT (usuario_id, escola_id, papel_id, permissao) DO NOTHING;

Este padrão é inerentemente seguro — se não há dados, não insere nada.


3. Ambientes divergentes

AmbienteDadosEscolasUsuários
ProduçãoReais~20+~50+
StagingSeed de teste3 (Monteiro Lobato, Nova Era, Rede Futuro)7 (Dev *)

Migrations de seed com UUIDs de produção sempre falharão em staging se não tiverem guards.


4. Regras para sub-permissões

INSERTs derivados que fazem FROM usuarios_escola_permissoes são seguros por design — se nenhuma permissão pai foi inserida, o SELECT retorna vazio. Não precisam de guard adicional.


5. Checklist pré-commit para migrations com dados

Antes de commitar uma migration que contém INSERT, UPDATE ou DELETE com dados:

  • [ ] Contém UUIDs literais? → Cada UUID referenciando FK deve ter WHERE EXISTS guard
  • [ ] Usa JOINs dinâmicos? → Preferível; verificar que as tabelas-fonte existem
  • [ ] Testado mentalmente contra banco vazio? → A migration passaria em um banco só com schema?
  • [ ] ON CONFLICT presente? → Idempotência para re-execução
  • [ ] Sem INSERT INTO ... VALUES com FK hardcoded? → Usar SELECT ... WHERE EXISTS em vez de VALUES

6. Migrations de schema vs dados

TipoExemploRisco multi-ambiente
SchemaCREATE TABLE, ALTER TABLE, CREATE INDEXBaixo — schema é igual
DadosINSERT INTO, UPDATE, DELETEAlto — dados divergem
SeedPermissões, configurações iniciaisMuito alto — UUIDs específicos

Migrations de dados e seed exigem atenção redobrada com guards.


Referências

  • Incidente original: migration 20260405192035 com 20 INSERTs hardcoded quebrando staging
  • Padrão seguro adotado: WHERE EXISTS + ON CONFLICT DO NOTHING
  • Padrão ideal: JOINs dinâmicos (usado em migrations anteriores com sucesso)