Aprende plantillas de Claude Code para migraciones PostgreSQL seguras: patrón expand-contract, backfills, planes de rollback y qué verificar en staging antes del release.

Un cambio de esquema en PostgreSQL parece simple hasta que se encuentra con tráfico real y datos reales. La parte riesgosa normalmente no es el SQL en sí. Es cuando el código de la app, el estado de la base de datos y el tiempo de despliegue dejan de coincidir.
La mayoría de las fallas son prácticas y dolorosas: un deploy se rompe porque código antiguo toca una columna nueva, una migración bloquea una tabla caliente y aumentan los timeouts, o un cambio “rápido” borra o reescribe datos sin avisar. Incluso cuando nada se cae, puedes enviar bugs sutiles como defaults incorrectos, constraints rotas o índices que nunca terminaron de construirse.
Las migraciones generadas por IA añaden otra capa de riesgo. Las herramientas pueden producir SQL válido que sigue siendo inseguro para tu carga, volumen de datos o proceso de release. También pueden adivinar nombres de tablas, pasar por alto locks de larga duración o minimizar el rollback porque las migraciones DOWN son difíciles. Si usas Claude Code para migraciones, necesitas barreras y contexto concreto.
Cuando en este post decimos que un cambio es “seguro”, significa tres cosas:
El objetivo es que las migraciones sean trabajo rutinario: predecible, comprobable y aburrido.
Empieza con unas cuantas reglas innegociables. Mantienen al modelo enfocado y te evitan enviar un cambio que solo funciona en tu laptop.
Divide el trabajo en pasos pequeños. Un cambio de esquema, un backfill de datos, un cambio de app y un paso de limpieza son riesgos distintos. Agruparlos complica ver qué se rompió y dificulta el rollback.
Prefiere cambios aditivos antes que destructivos. Añadir una columna, un índice o una tabla suele ser bajo riesgo. Renombrar o eliminar objetos es donde ocurren outages. Haz la parte segura primero, cambia la app y elimina lo antiguo solo cuando estés seguro de que no se usa.
Haz que la app tolere ambas formas durante un tiempo. El código debería poder leer la columna antigua o la nueva durante el despliegue. Esto evita la carrera común donde algunos servidores ejecutan código nuevo mientras la base de datos aún es vieja (o al revés).
Trata las migraciones como código de producción, no como un script rápido. Incluso si construyes con una plataforma como Koder.ai (backend en Go con PostgreSQL, más clientes en React o Flutter), la base de datos es compartida por todo. Los errores son costosos.
Si quieres un conjunto compacto de reglas para poner al inicio de cada solicitud de SQL, usa algo como:
Un ejemplo práctico: en lugar de renombrar una columna de la que depende tu app, añade la nueva columna, backfillea lentamente, despliega código que lea nueva y luego antigua, y solo más tarde elimina la columna vieja.
Claude puede escribir SQL decente a partir de una petición vaga, pero las migraciones seguras necesitan contexto. Trata tu prompt como un mini brief de diseño: muestra lo que existe, explica qué no debe romperse y define qué significa “seguro” para tu despliegue.
Empieza pegando solo los hechos de la base de datos que importan. Incluye la definición de la tabla más los índices y constraints relevantes (primary keys, unique constraints, foreign keys, check constraints, triggers). Si hay tablas relacionadas, incluye esos fragmentos también. Un extracto pequeño y exacto evita que el modelo adivine nombres o pase por alto una constraint importante.
Añade la escala del mundo real. Conteos de filas, tamaño de tabla, tasa de escrituras y tráfico pico deberían cambiar el plan. “200M rows y 1k writes/sec” es una migración diferente a “20k rows y mayormente lecturas.” También incluye tu versión de Postgres y cómo se ejecutan las migraciones en tu sistema (transacción única vs varios pasos).
Describe cómo la aplicación usa los datos: las lecturas, escrituras y jobs importantes. Ejemplos: “API lee por email”, “workers actualizan estado”, o “reportes escanean por created_at”. Esto determina si necesitas expand/contract, feature flags y cuán seguro será un backfill.
Finalmente, sé explícito sobre restricciones y entregables. Una estructura simple funciona bien:
Pedir tanto SQL como un plan de ejecución fuerza al modelo a pensar en orden, riesgo y qué revisar antes de enviar.
El patrón expand/contract cambia una base de datos PostgreSQL sin romper la app mientras el cambio está en progreso. En lugar de un switch arriesgado, haces que la base de datos soporte ambas formas durante un periodo.
Piénsalo así: añade cosas nuevas con seguridad (expand), mueve tráfico y datos gradualmente, y solo entonces elimina las partes antiguas (contract). Esto es especialmente útil para trabajo asistido por IA porque te obliga a planear el “medio desordenado”.
Un flujo práctico se ve así:
Usa este patrón siempre que usuarios puedan seguir en versiones antiguas de la app mientras la base cambia. Eso incluye despliegues con múltiples instancias, apps móviles que actualizan lentamente, o cualquier release donde una migración pueda durar minutos u horas.
Una táctica útil es planear dos releases. El Release 1 hace expand más compatibility para que nada rompa si el backfill está incompleto. El Release 2 hace el contract solo después de confirmar que el código y los datos nuevos están en su lugar.
Copia esta plantilla y rellena los corchetes. Empuja a Claude Code a producir SQL que puedas ejecutar, checks para probar que funcionó y un plan de rollback que puedas seguir.
You are helping me plan a PostgreSQL expand-contract migration.
Context
- App: [what the feature does, who uses it]
- Database: PostgreSQL [version if known]
- Table sizes: [rough row counts], write rate: [low/medium/high]
- Zero/near-zero downtime required: [yes/no]
Goal
- Change: [describe the schema change]
- Current schema (relevant parts):
[paste CREATE TABLE or \d output]
- How the app will change (expand phase and contract phase):
- Expand: [new columns/indexes/triggers, dual-write, read preference]
- Contract: [when/how we stop writing old fields and remove them]
Hard safety requirements
- Prefer lock-safe operations. Avoid full table rewrites on large tables when possible.
- If any step can block writes, call it out explicitly and suggest alternatives.
- Use small, reversible steps. No “big bang” changes.
Deliverables
1) UP migration SQL (expand)
- Use clear comments.
- If you propose indexes, tell me if they should be created CONCURRENTLY.
- If you propose constraints, tell me whether to add them NOT VALID then VALIDATE.
2) Verification queries
- Queries to confirm the new schema exists.
- Queries to confirm data is being written to both old and new structures (if dual-write).
- Queries to estimate whether the change caused bloat/slow queries/locks.
3) Rollback plan (realistic)
- DOWN migration SQL (only if it is truly safe).
- If down is not safe, write a rollback runbook:
- how to stop the app change
- how to switch reads back
- what data might be lost or need re-backfill
4) Runbook notes
- Exact order of operations (including app deploy steps).
- What to monitor during the run (errors, latency, deadlocks, lock waits).
- “Stop/continue” checkpoints.
Output format
- Separate sections titled: UP.sql, VERIFY.sql, DOWN.sql (or ROLLBACK.md), RUNBOOK.md
Dos líneas extra que ayudan en la práctica:
RISK: blocks writes, y cuándo ejecutarlo (off-peak vs anytime).Cambios pequeños de esquema aún pueden hacer daño si toman locks largos, reescriben tablas grandes o fallan a mitad. Cuando usas Claude Code para migraciones, pide SQL que evite reescrituras y mantenga la app funcionando mientras la base de datos se pone al día.
Añadir una columna nullable suele ser seguro. Añadir una columna con default no nulo puede ser riesgoso en versiones antiguas de Postgres porque puede reescribir toda la tabla.
Un enfoque más seguro es un cambio en dos pasos: añade la columna como NULL sin default, backfillea en lotes, luego establece el default para filas nuevas y añade NOT NULL una vez los datos estén limpios.
Si debes imponer un default inmediatamente, exige una explicación del comportamiento de locks para tu versión de Postgres y un plan de respaldo si el tiempo de ejecución es mayor al esperado.
Para índices en tablas grandes, pide CREATE INDEX CONCURRENTLY para que lecturas y escrituras sigan fluyendo. También exige una nota de que no puede ejecutarse dentro de una transacción, lo que implica que tu herramienta de migración necesita un paso no transaccional.
Para claves foráneas, la ruta más segura suele ser añadir la constraint como NOT VALID primero y validarla más tarde. Esto hace que el cambio inicial sea más rápido mientras se aplica la FK para escrituras nuevas.
Al endurecer reglas (NOT NULL, UNIQUE, CHECK), pide “limpiar primero, aplicar después.” La migración debería detectar filas malas, corregirlas y solo entonces habilitar la regla más estricta.
Si quieres una checklist corta para pegar en prompts, mantenla ajustada:
Los backfills son donde aparece la mayor parte del dolor de migraciones, no el ALTER TABLE. Los prompts más seguros tratan los backfills como jobs controlados: medibles, reanudables y suaves con producción.
Empieza con checks de aceptación fáciles de ejecutar y difíciles de discutir: conteos de filas esperados, una tasa objetivo de nulos y algunas comprobaciones puntuales (por ejemplo, comparar viejo vs nuevo para 20 IDs aleatorios).
Luego pide un plan por lotes. Los lotes mantienen los locks cortos y reducen sorpresas. Una buena petición especifica:
Requiere idempotencia porque los backfills fallan a mitad. El SQL debe ser seguro para re-ejecutar sin duplicar ni corromper datos. Patrones típicos: “update solo donde la columna nueva sea NULL” o reglas deterministas donde la misma entrada produce siempre la misma salida.
También explica cómo la app se mantiene correcta mientras corre el backfill. Si entran escrituras nuevas, necesitas un puente: dual-write en el código, un trigger temporal o lógica read-fallback (leer nuevo si existe, sino el viejo). Di qué enfoque puedes desplegar con seguridad.
Finalmente, integra pausa y reanudación en el diseño. Pide seguimiento de progreso y checkpoints, como una tabla pequeña que guarde el último ID procesado y una query que reporte progreso (filas actualizadas, último ID, tiempo de inicio).
Ejemplo: añades users.full_name derivado de first_name y last_name. Un backfill seguro actualiza solo filas donde full_name IS NULL, corre por rangos de ID, registra el último ID actualizado y mantiene nuevos registros correctos vía dual-write hasta que el switchover esté completo.
Un plan de rollback no es solo “escribe una migración down.” Son dos problemas: deshacer el cambio de esquema y manejar datos que cambiaron mientras la versión nueva estuvo activa. El rollback de esquema suele ser posible. El rollback de datos a menudo no, a menos que lo hayas planificado.
Sé explícito sobre qué significa rollback para tu cambio. Si borras una columna o reescribes valores en sitio, exige una respuesta realista como: “El rollback restaura compatibilidad de la app, pero los datos originales no se recuperan sin un snapshot.” Esa honestidad te mantiene seguro.
Pide triggers de rollback claros para que nadie discuta en un incidente. Ejemplos:
Requiere el paquete de rollback completo, no solo SQL: DOWN migration SQL (solo si es seguro), pasos de app para mantener compatibilidad y cómo parar jobs en background.
Este patrón de prompt suele ser suficiente:
Produce a rollback plan for this migration.
Include: down migration SQL, app config/code switches needed for compatibility, and the exact order of steps.
State what can be rolled back (schema) vs what cannot (data) and what evidence we need before deciding.
Include rollback triggers with thresholds.
Antes de enviar, captura una “instantánea de seguridad” ligera para poder comparar antes y después:
También deja claro cuándo no hacer rollback. Si solo añadiste una columna nullable y la app hace dual-write, una corrección hacia adelante (hotfix, pausar el backfill y reanudar) suele ser más segura que revertir y crear más drift.
La IA puede escribir SQL rápido, pero no puede ver tu base de datos de producción. La mayoría de fallas ocurren cuando el prompt es vago y el modelo rellena huecos.
Una trampa común es omitir el esquema actual. Si no pegas la definición de la tabla, índices y constraints, el SQL puede apuntar a columnas que no existen o pasar por alto una regla de unicidad que convierte un backfill en una operación lenta y bloqueante.
Otro error es enviar expand, backfill y contract en un solo deploy. Eso elimina tu vía de escape. Si el backfill tarda o falla a mitad, te quedas con una app esperando el estado final.
Los problemas que más aparecen:
Un ejemplo concreto: “renombrar una columna y actualizar la app.” Si el plan generado renombra y backfillea en una sola transacción, un backfill lento puede sostener locks y romper tráfico en vivo. Un prompt más seguro obliga a lotes pequeños, timeouts explícitos y queries de verificación antes de quitar la ruta antigua.
Staging es donde encuentras problemas que nunca aparecen en una base de datos de dev pequeña: locks largos, nulos sorpresa, índices faltantes y rutas de código olvidadas.
Primero, verifica que el esquema coincida con el plan después de la migración: columnas, tipos, defaults, constraints e índices. Una mirada rápida no es suficiente. Un índice faltante puede convertir un backfill seguro en un desastre lento.
Luego ejecuta la migración contra un dataset realista. Idealmente una copia reciente de producción con campos sensibles enmascarados. Si no puedes, al menos iguala volumen y hotspots de producción (tablas grandes, filas anchas, tablas con muchos índices). Registra los tiempos de cada paso para saber qué esperar en prod.
Una checklist corta para staging:
Finalmente, prueba flujos reales de usuario, no solo SQL. Crear, actualizar y leer registros tocados por el cambio. Si el plan es expand/contract, confirma que ambas formas funcionan hasta la limpieza final.
Imagina que tienes users.name que guarda nombres completos como “Ada Lovelace.” Quieres first_name y last_name, pero no puedes romper registros nuevos, perfiles o pantallas admin mientras el cambio se despliega.
Empieza con un paso expand que sea seguro incluso si no se despliega código nuevo todavía: añade columnas nullable, conserva la columna antigua y evita locks largos.
ALTER TABLE users ADD COLUMN first_name text;
ALTER TABLE users ADD COLUMN last_name text;
Luego actualiza el comportamiento de la app para soportar ambos esquemas. En el Release 1, la app debe leer las columnas nuevas cuando existan, caer a name cuando estén a NULL, y escribir en ambas para que los datos nuevos permanezcan consistentes.
Después viene el backfill. Ejecuta un job por lotes que actualice pequeños bloques de filas por ejecución, registre el progreso y pueda pausarse con seguridad. Por ejemplo: actualizar users donde first_name es null en orden ascendente de ID, 1,000 a la vez, y loguear cuántas filas cambiaron.
Antes de endurecer reglas, valida en staging:
first_name y last_name y aún ponen namenameusers no se vuelven notablemente más lentasEl Release 2 cambia las lecturas a las columnas nuevas únicamente. Solo después deberías añadir constraints (como SET NOT NULL) y eliminar name, idealmente en un deploy separado y posterior.
Para rollback, mantenlo simple. La app sigue leyendo name durante la transición, y el backfill es pausables. Si necesitas revertir Release 2, vuelve las lecturas a name y deja las columnas nuevas hasta estabilizar.
Trata cada cambio como un pequeño runbook. El objetivo no es un prompt perfecto. Es una rutina que fuerza los detalles correctos: esquema, constraints, plan de ejecución y rollback.
Estandariza lo que cada solicitud de migración debe incluir:
Decide quién es responsable de cada paso antes de ejecutar SQL. Una separación simple evita “todos pensaron que otro lo haría”: desarrolladores a cargo del prompt y código de migración, ops a cargo del timing y monitoreo en prod, QA verifica staging y casos límite, y una persona es la responsable final del go/no-go.
Si construyes apps vía chat, ayuda esbozar la secuencia antes de generar cualquier SQL. Para equipos que usan Koder.ai, Planning Mode es un lugar natural para escribir esa secuencia, y snapshots más rollback reducen la zona de impacto si algo inesperado ocurre durante el despliegue.
Después de enviar, programa la limpieza de contract inmediatamente mientras el contexto sigue fresco, para que las columnas antiguas y el código de compatibilidad temporal no se queden meses.
Un cambio de esquema es riesgoso cuando el código de la app, el estado de la base de datos y el tiempo de despliegue dejan de coincidir.
Modos de fallo comunes:
Usa el enfoque expand/contract:
Porque el modelo puede generar SQL válido pero inseguro para tu carga.
Riesgos típicos relacionados con IA:
Trata la salida de la IA como un borrador y exige plan de ejecución, verificaciones y pasos de rollback.
Incluye solo los hechos de los que depende la migración:
CREATE TABLE (más índices, FKs, UNIQUE/CHECK, triggers)Regla por defecto: sepáralos.
Una división práctica:
Agrupar todo hace que las fallas sean más difíciles de diagnosticar y revertir.
Prefiere este patrón:
ADD COLUMN ... NULL sin default (rápido)NOT NULL solo después de verificarAgregar un default no nulo puede ser riesgoso en algunas versiones porque podría reescribir la tabla completa. Si necesitas un default inmediato, pide notas sobre locking/runtime y un plan alternativo más seguro.
Pide:
CREATE INDEX CONCURRENTLY para tablas grandes/activasPara verificar, incluye una comprobación rápida de que el índice existe y se usa (por ejemplo, comparar un EXPLAIN antes/después en staging).
Usa NOT VALID primero, luego valida:
NOT VALID para que el paso inicial sea menos disruptivoVALIDATE CONSTRAINT en un paso separado cuando puedas vigilarloEsto sigue aplicando la FK para escrituras nuevas, mientras controlas cuándo ocurre la validación costosa.
Un buen backfill es por lotes, idempotente y reanudable.
Requisitos prácticos:
WHERE new_col IS NULL)Objetivo de rollback por defecto: restaurar la compatibilidad de la app rápidamente, incluso si los datos no se revierten perfectamente.
Un plan de rollback práctico debe incluir:
A menudo el rollback más seguro es volver a leer desde el campo antiguo mientras se mantienen las columnas nuevas.
Esto mantiene las versiones antigua y nueva de la app funcionando durante el despliegue.
Esto evita suposiciones y fuerza el orden correcto.
Esto hace que los backfills sobrevivan bajo carga real.