Aprenda prompts para migrações PostgreSQL com Claude Code: como fazer mudanças expand-contract seguras, backfills e planos de rollback, e o que verificar em staging antes do release.

\nYou are helping me plan a PostgreSQL expand-contract migration.\n\nContext\n- App: [what the feature does, who uses it]\n- Database: PostgreSQL [version if known]\n- Table sizes: [rough row counts], write rate: [low/medium/high]\n- Zero/near-zero downtime required: [yes/no]\n\nGoal\n- Change: [describe the schema change]\n- Current schema (relevant parts):\n [paste CREATE TABLE or \\d output]\n- How the app will change (expand phase and contract phase):\n - Expand: [new columns/indexes/triggers, dual-write, read preference]\n - Contract: [when/how we stop writing old fields and remove them]\n\nHard safety requirements\n- Prefer lock-safe operations. Avoid full table rewrites on large tables when possible.\n- If any step can block writes, call it out explicitly and suggest alternatives.\n- Use small, reversible steps. No “big bang” changes.\n\nDeliverables\n1) UP migration SQL (expand)\n - Use clear comments.\n - If you propose indexes, tell me if they should be created CONCURRENTLY.\n - If you propose constraints, tell me whether to add them NOT VALID then VALIDATE.\n\n2) Verification queries\n - Queries to confirm the new schema exists.\n - Queries to confirm data is being written to both old and new structures (if dual-write).\n - Queries to estimate whether the change caused bloat/slow queries/locks.\n\n3) Rollback plan (realistic)\n - DOWN migration SQL (only if it is truly safe).\n - If down is not safe, write a rollback runbook:\n - how to stop the app change\n - how to switch reads back\n - what data might be lost or need re-backfill\n\n4) Runbook notes\n - Exact order of operations (including app deploy steps).\n - What to monitor during the run (errors, latency, deadlocks, lock waits).\n - “Stop/continue” checkpoints.\n\nOutput format\n- Separate sections titled: UP.sql, VERIFY.sql, DOWN.sql (or ROLLBACK.md), RUNBOOK.md\n\n\nDuas linhas extras que ajudam na prática:\n\n- Peça para rotular qualquer passo que bloqueie escrita como RISK: blocks writes, além de quando executá-lo (off-peak vs anytime).\n- Force honestidade sobre locks: "If you're not sure whether a statement takes an ACCESS EXCLUSIVE lock, say so and offer a safer option."\n\n## Operações comuns de esquema e como pedir SQL mais seguro\n\nPequenas mudanças de esquema ainda podem prejudicar se pegarem locks longos, reescreverem tabelas grandes ou falharem no meio. Ao usar Claude Code para migrações, peça SQL que evite reescritas e mantenha seu app funcionando enquanto o banco alcança o novo estado.\n\n### Adicionar colunas e defaults (sem locks longos)\n\nAdicionar uma coluna nullable normalmente é seguro. Adicionar uma coluna com default não-nulo pode ser arriscado em versões antigas do Postgres porque pode reescrever a tabela inteira.\n\nUma abordagem mais segura é em dois passos: adicionar a coluna como NULL sem default, backfill em lotes, então definir o default para novas linhas e adicionar NOT NULL quando os dados estiverem limpos.\n\nSe precisar aplicar um default imediatamente, exija uma explicação do comportamento de lock para sua versão do Postgres e um plano de fallback se o tempo de execução for maior que o esperado.\n\n### Índices, FKs, constraints, drops\n\nPara índices em tabelas grandes, solicite CREATE INDEX CONCURRENTLY para que leitura e escrita continuem fluindo. Também exija uma nota de que não pode rodar dentro de um bloco de transação, o que significa que sua ferramenta de migração precisa de um passo não transacional.\n\nPara foreign keys, o caminho mais seguro costuma ser adicionar a constraint como NOT VALID primeiro, depois validar mais tarde. Isso torna a mudança inicial mais rápida ao mesmo tempo que impõe a FK para novos writes.\n\nAo tornar constraints mais rígidas (NOT NULL, UNIQUE, CHECK), peça por “limpar primeiro, impor depois.” A migração deve detectar linhas erradas, corrigi-las e só então ativar a regra mais rígida.\n\nSe quiser um checklist curto para colar em prompts, mantenha-o enxuto:\n\n- Incluir notas sobre locks e tempo de execução esperado.\n- Usar CONCURRENTLY para índices grandes e citar limites de transação.\n- Preferir NOT VALID então VALIDATE para novas FKs.\n- Separar backfill de impor NOT NULL/UNIQUE.\n- Dropar objetos somente após um ciclo completo de release e confirmação de que ninguém os lê.\n\n## Pedindo backfills lentos, constantes e recuperáveis\n\nBackfills são onde a maior parte da dor aparece, não o ALTER TABLE. Pedidos seguros tratam backfills como jobs controlados: mensuráveis, reiniciáveis e gentis com a produção.\n\nComece com checagens de aceitação fáceis de rodar e difíceis de contestar: contagens de linhas esperadas, taxa de null alvo e algumas checagens pontuais (por exemplo, comparar valores antigos vs novos para 20 IDs aleatórios).\n\nDepois peça um plano de batching. Batches mantêm locks curtos e reduzem surpresas. Um bom pedido especifica:\n\n- Como batchar (ranges de PK ou janelas de tempo como created_at)\n- Tamanho de lote alvo (por exemplo, 5.000 a 50.000 linhas)\n- Se deve pausar entre batches em tabelas quentes\n- Que cada batch seja uma transação clara e curta (não uma transação enorme)\n\nExija idempotência porque backfills falham no meio. O SQL deve ser seguro para re-executar sem duplicar ou corromper dados. Padrões típicos são “update apenas onde a nova coluna é NULL” ou uma regra determinística onde a mesma entrada sempre gera a mesma saída.\n\nTambém detalhe como o app se mantém correto enquanto o backfill roda. Se novas escritas continuarem chegando, precisa de uma ponte: dual-write no código do app, um trigger temporário, ou lógica read-fallback (ler novo quando presente, caso contrário o antigo). Diga qual abordagem você pode implantar com segurança.\n\nPor fim, inclua pause e resume no desenho. Peça por tracking de progresso e checkpoints, como uma tabela pequena que armazena o último ID processado e uma query que relata progresso (linhas atualizadas, último ID, hora).\n\nExemplo: você adiciona users.full_name derivado de first_name e last_name. Um backfill seguro atualiza apenas linhas onde full_name IS NULL, roda em ranges de ID, registra o último ID atualizado e mantém novos cadastros corretos via dual-write até o switch-over completo.\n\n## Como pedir planos de rollback que funcionem na prática\n\nUm plano de rollback não é só “escreva um down migration.” São dois problemas: desfazer a mudança de esquema e lidar com quaisquer dados que mudaram enquanto a versão nova estava ativa. Rollback de esquema costuma ser possível. Rollback de dados frequentemente não é, a menos que você tenha planejado.\n\nSeja explícito sobre o que rollback significa para sua mudança. Se você está dropando uma coluna ou reescrevendo valores no lugar, exija uma resposta realista como: “Rollback restaura compatibilidade do app, mas dados originais não podem ser recuperados sem um snapshot.” Essa honestidade é o que mantém você seguro.\n\nPeça gatilhos de rollback claros para que ninguém discuta durante um incidente. Exemplos:\n\n- Taxa de erro ou latência cruza um limite definido por 10 minutos\n- Um plano de query crítico regrediu (ex.: seq scan numa tabela quente)\n- Job de backfill fica atrasado por mais de N horas\n- Checagens de dados falham (nulls onde não devem, duplicatas, linhas faltando)\n- Um passo de migração bloqueia writes por mais de X segundos\n\nExija o pacote completo de rollback, não só SQL: DOWN migration SQL (só se seguro), passos no app para manter compatibilidade, e como parar jobs em background.\n\nEsse padrão de prompt geralmente é suficiente:\n\n\nProduce a rollback plan for this migration.\nInclude: down migration SQL, app config/code switches needed for compatibility, and the exact order of steps.\nState what can be rolled back (schema) vs what cannot (data) and what evidence we need before deciding.\nInclude rollback triggers with thresholds.\n\n\nAntes de enviar, capture um “safety snapshot” leve para comparar antes/depois:\n\n- Contagens de linhas das tabelas afetadas (e subconjuntos chave)\n- Um pequeno conjunto de queries de amostra com resultados esperados\n- Agregados simples (sum, min/max) para colunas tocadas\n- Uma lista curta de IDs para checagens spot antes e depois\n\nTambém seja claro sobre quando não reverter. Se você só adicionou uma coluna nullable e o app faz dual-write, um fix para frente (hotfix no código, pausar o backfill, depois retomar) costuma ser mais seguro que reverter e criar mais divergência.\n\n## Erros comuns a observar com migrações assistidas por IA\n\nIA pode escrever SQL rapidamente, mas não vê seu banco de produção. A maioria das falhas acontece quando o prompt é vago e o modelo preenche lacunas.\n\nUma armadilha comum é pular o esquema atual. Se você não colar a definição da tabela, indexes e constraints, o SQL pode mirar colunas que não existem ou ignorar uma regra de unicidade que torna o backfill lento e bloqueante.\n\nOutro erro é enviar expand, backfill e contract em um único deploy. Isso elimina sua rota de escape. Se o backfill demora ou falha no meio, você fica preso com um app esperando o estado final.\n\nAs falhas que mais aparecem:\n\n- Backfills que não são idempotentes e não têm rastreamento de progresso\n- Adicionar NOT NULL, UNIQUE ou FKs antes de limpar e validar dados\n- Transações longas sem lock timeouts ou statement timeouts\n- Sem queries de verificação, então problemas ficam escondidos até os usuários acharem\n\nUm exemplo concreto: “renomear uma coluna e atualizar o app.” Se o plano gerado renomeia e backfill em uma única transação, um backfill lento pode segurar locks e quebrar o tráfego ao vivo. Um prompt mais seguro força lotes pequenos, timeouts explícitos e queries de verificação antes de remover o caminho antigo.\n\n## O que verificar em staging antes de enviar\n\nStaging é onde você encontra problemas que nunca aparecem num banco dev pequeno: locks longos, nulls surpresa, índices faltando e caminhos de código esquecidos.\n\nPrimeiro, verifique que o esquema bate com o plano após a migração: colunas, tipos, defaults, constraints e índices. Uma olhada rápida não basta. Um índice faltando pode transformar um backfill seguro em um caos lento.\n\nDepois rode a migração contra um dataset realista. Idealmente é uma cópia recente do production com campos sensíveis mascarados. Se não puder, ao menos iguale volume e hotspots de produção (tabelas grandes, linhas largas, tabelas com muitos índices). Registre tempos de cada passo para saber o que esperar em produção.\n\nUm checklist curto para staging:\n\n- Esquema corresponde ao plano (colunas, tipos, constraints, índices)\n- Tempos registrados com volume de dados realista\n- Compatibilidade testada: app antigo com novo esquema e app novo com esquema antigo (quando o plano diz que deve funcionar)\n- Queries de verificação rodadas: taxas de null, contagens de linhas, checagem de órfãos para novas FKs, leituras de amostra\n- Sinais operacionais observados durante a execução: locks, deadlocks, timeouts, queries lentas\n\nPor fim, teste fluxos reais de usuário, não só SQL. Crie, atualize e leia registros tocados pela mudança. Se expand/contract for o plano, confirme que ambas formas funcionam até a limpeza final.\n\n## Um exemplo realista: mudar uma coluna sem quebrar usuários\n\nImagine que você tem users.name que guarda nomes completos como “Ada Lovelace.” Você quer first_name e last_name, mas não pode quebrar cadastros, perfis ou telas administrativas enquanto a mudança é rolada.\n\nComece com um passo de expand que seja seguro mesmo que nenhum código novo seja deployado ainda: adicione colunas nullable, mantenha a coluna antiga e evite locks longos.\n\nsql\nALTER TABLE users ADD COLUMN first_name text;\nALTER TABLE users ADD COLUMN last_name text;\n\n\nDepois atualize o comportamento do app para suportar os dois esquemas. No Release 1, o app deve ler das colunas novas quando presente, recorrer para name quando elas forem null, e gravar em ambos para que novos dados fiquem consistentes.\n\nO próximo passo é o backfill. Rode um job em batches que atualiza um pequeno conjunto de linhas por execução, registra progresso e pode ser pausado com segurança. Por exemplo: atualize users onde first_name é null em ordem de ID ascendente, 1.000 por vez, e registre quantas linhas mudaram.\n\nAntes de endurecer regras, valide em staging:\n\n- Novos cadastros preenchem first_name e last_name e continuam setando name\n- Usuários existentes exibem corretamente mesmo se apenas name estiver presente\n- Backfill pode parar e reiniciar sem duplicar trabalho\n- Não restam nulls inesperados após o fim do backfill\n- Queries básicas em users não ficam visivelmente mais lentas\n\nO Release 2 muda leituras para as colunas novas apenas. Só depois disso aplique constraints (como SET NOT NULL) e remova name, idealmente em um deploy separado posterior.\n\nPara rollback, mantenha simples. O app continua lendo name durante a transição, e o backfill é pausável. Se precisar reverter o Release 2, volte as leituras para name e deixe as colunas novas até estabilizar novamente.\n\n## Próximos passos: transforme seus prompts numa rotina repetível de migração\n\nTrate cada mudança como um pequeno runbook. O objetivo não é um prompt perfeito. É uma rotina que exige os detalhes certos: esquema, constraints, plano de execução e rollback.\n\nPadronize o que todo pedido de migração deve incluir:\n\n- Esquema atual e a mudança exata (tabelas, colunas, índices)\n- Restrições e fatos de tráfego (tamanho da tabela, taxa de escrita, downtime permitido)\n- Sequência de release (expand, deploy do app, backfill, contract)\n- Como você vai observar progresso (queries/métricas, tempo esperado)\n- Passos de rollback (o que reverter primeiro, quais dados podem ficar para trás)\n\nDecida quem é responsável por cada passo antes de alguém rodar SQL. Uma divisão simples evita “todo mundo achou que outra pessoa fez”: desenvolvedores cuidam do prompt e do código de migração, ops cuida do timing e monitoramento em produção, QA verifica comportamento em staging e casos de borda, e uma pessoa decide go/no-go.\n\nSe você está construindo apps via chat, pode ajudar esboçar a sequência antes de gerar qualquer SQL. Para times que usam Koder.ai, o Planning Mode é um lugar natural para escrever essa sequência, e snapshots mais rollback podem reduzir o blast radius se algo inesperado acontecer durante o rollout.\n\nDepois de enviar, agende o cleanup do contract logo enquanto o contexto está fresco, para que colunas antigas e código de compatibilidade temporário não fiquem por meses.Uma mudança de esquema é arriscada quando o código do app, o estado do banco de dados e o momento do deploy deixam de coincidir.\n\nModos comuns de falha:\n\n- Código antigo do app acessa uma nova coluna/constraint e trava\n- Uma migração assume um lock forte numa tabela ocupada e as requisições time-out\n- Uma mudança “pequena” reescreve ou remove dados silenciosamente\n- Trabalho de índice/constraint leva muito mais tempo do que o esperado e causa queries lentas
Use a abordagem expand/contract:\n\n- Expand: adicione colunas/tabelas/indexes novos e compatíveis (nullable, NOT VALID quando aplicável)\n- Compatibility: faça deploy do app que saiba ler/escrever as duas formas\n- Backfill: copie dados em pequenos lotes com checkpoints\n- Contract: aplique constraints e remova campos antigos apenas depois de um ciclo de release\n\nIsso mantém versões antigas e novas do app funcionando durante o rollout.
O modelo pode gerar SQL válido, mas que é inseguro para sua carga de trabalho.\n\nRiscos típicos relacionados a IA:\n\n- Adivinhar nomes de tabelas/colunas ou esquecer uma constraint importante\n- Propor uma migração “big bang” que elimina opções de rollback\n- Ignorar comportamento de locks, limites de transação e builds de índice de longa duração\n- Simplificar demais o rollback (especialmente quando dados são transformados ou removidos)\n\nTrate a saída da IA como um rascunho e exija plano de execução, checagens e passos de rollback.
Inclua apenas os fatos dos quais a migração depende:\n\n- Trechos relevantes de CREATE TABLE (mais indexes, FKs, UNIQUE/CHECK, triggers)\n- Versão do Postgres e como as migrações rodam (transação única vs passos múltiplos)\n- Escala: contagem de linhas, tamanho da tabela, taxa de escrita, pico de tráfego\n- Como o app usa os dados (reads/writes/jobs críticos)\n- Restrições rígidas (sem downtime, evitar reescritas completas, limites de lock)\n- Entregáveis: UP SQL + queries de verificação + plano de rollback + runbook\n\nIsso evita adivinhações e força a ordem correta.
Regra padrão: separe-os.\n\nDivisão prática:\n\n- Migração 1: expand (novas colunas/tabelas, talvez constraints NOT VALID)\n- Deploy do app: código de compatibilidade (read-fallback ou dual-write)\n- Job de backfill: updates em lotes com rastreamento de progresso\n- Migração 2: contract (VALIDATE, SET NOT NULL, drop old columns)\n\nAgrupar tudo torna falhas mais difíceis de diagnosticar e reverter.
Prefira este padrão:\n\n1) ADD COLUMN ... NULL sem default (rápido)\n2) Backfill em lotes\n3) Definir default para novas linhas\n4) Adicionar NOT NULL só após verificação\n\nAdicionar um default não-nulo pode ser arriscado em algumas versões porque pode reescrever a tabela inteira. Se precisar do default imediato, peça notas sobre locks/tempo de execução e um fallback seguro.
Peça:\n\n- CREATE INDEX CONCURRENTLY para tabelas grandes/quentes\n- Uma observação de que não pode rodar dentro de um bloco de transação (sua ferramenta precisa suportar isso)\n- Tempo estimado e o que monitorar (espera de locks, latência de queries)\n\nPara verificação, inclua um check simples de que o índice existe e está sendo usado (por exemplo, compare um EXPLAIN antes/depois em staging).
Use NOT VALID primeiro, depois valide:\n\n- Adicione a FK como NOT VALID para que o passo inicial seja menos disruptivo\n- Rode VALIDATE CONSTRAINT em um passo separado quando puder observar\n\nIsso ainda garante a FK para novos writes, enquanto controla quando a validação cara acontece.
Um bom backfill é em lotes, idempotente e reiniciável.\n\nRequisitos práticos:\n\n- Fazer batch por ranges de PK ou janelas de tempo\n- Atualizar apenas linhas que ainda precisam (ex.: WHERE new_col IS NULL)\n- Manter batches em transações curtas; opcionalmente pausar entre batches\n- Rastrear progresso (último ID processado, linhas atualizadas, hora de início)\n- Garantir que o app continue correto enquanto o backfill roda (dual-write, trigger temporária ou read-fallback)\n\nIsso torna backfills suportáveis sob tráfego real.
Objetivo padrão de rollback: restaurar compatibilidade do app rapidamente, mesmo que os dados não voltem perfeitamente.\n\nUm plano de rollback viável deve incluir:\n\n- Se o DOWN SQL é realmente seguro; se não, um runbook em vez disso\n- A ordem exata: parar/pausar jobs de backfill, deploy do ajuste no app, depois passos de esquema\n- Triggers claros de rollback (taxa de erro, latência, espera de locks, checagens de dados falhando)\n- Declaração do que pode ser revertido (esquema) vs o que não pode (dados)\n\nFrequentemente o rollback mais seguro é voltar a leitura para o campo antigo enquanto deixa as colunas novas no lugar.