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)
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)
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:
-- ✅ 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
| Ambiente | Dados | Escolas | Usuários |
|---|---|---|---|
| Produção | Reais | ~20+ | ~50+ |
| Staging | Seed de teste | 3 (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 EXISTSguard - [ ] 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 CONFLICTpresente? → Idempotência para re-execução - [ ] Sem
INSERT INTO ... VALUEScom FK hardcoded? → UsarSELECT ... WHERE EXISTSem vez deVALUES
6. Migrations de schema vs dados
| Tipo | Exemplo | Risco multi-ambiente |
|---|---|---|
| Schema | CREATE TABLE, ALTER TABLE, CREATE INDEX | Baixo — schema é igual |
| Dados | INSERT INTO, UPDATE, DELETE | Alto — dados divergem |
| Seed | Permissões, configurações iniciais | Muito alto — UUIDs específicos |
Migrations de dados e seed exigem atenção redobrada com guards.
Referências
- Incidente original: migration
20260405192035com 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)