8 min

Verificación del esquema PostgreSQL antes de la primera migración

La verificación de esquemas PostgreSQL detecta mapeos erróneos, restricciones débiles, índices ausentes y cambios inseguros antes de que la primera migración toque datos.

Verificación del esquema PostgreSQL antes de la primera migración

Un generador con IA puede producir PostgreSQL válido y aun así inferir la base de datos equivocada. La sintaxis es la parte fácil. Los errores peligrosos parecen razonables: una relación opcional se vuelve obligatoria, una cadena de estado recibe una restricción CHECK incompleta, una eliminación se propaga a registros que deberían conservarse o una migración recrea una tabla y pierde silenciosamente una columna.

Por eso, la verificación de un esquema PostgreSQL debe probar por separado el significado, el comportamiento de la migración y la recuperación. Solo apruebo un esquema inferido después de que supera un conjunto de datos conocido, invariantes explícitas, consultas representativas, una revisión de cambios destructivos y un ensayo de restauración. Si falta cualquiera de estos elementos, la migración sigue siendo una propuesta.

La primera migración merece este nivel de revisión incluso cuando la base de datos de producción está vacía. Los errores iniciales de esquema se consolidan rápido porque el código de la aplicación, los datos iniciales, los informes y las migraciones posteriores empiezan a depender de ellos. Una revisión de quince minutos antes de la primera ejecución suele costar menos que explicar, seis meses después, por qué dos conceptos distintos comparten una sola columna de texto que admite valores nulos.

Un esquema inferido es una especificación no confiable

Trata el esquema inferido como un borrador de especificación, no como una verdad ejecutable. El generador vio indicaciones, pantallas de muestra, registros importados o código de aplicación generado. No presenció todas las excepciones de negocio, reglas de conservación, importaciones masivas, correcciones de soporte y pagos fallidos que acabará conteniendo la base de datos.

Empieza por separar tres preguntas que los equipos suelen mezclar. La corrección del esquema pregunta si las tablas y restricciones modelan el dominio. La seguridad de la migración pregunta si las operaciones propuestas conservan los datos existentes y mantienen la base de datos utilizable mientras se ejecutan. La preparación para la recuperación pregunta si puedes volver a un estado conocido tras un cambio parcial o semánticamente incorrecto. Superar una de estas pruebas dice poco sobre las otras dos.

Una instrucción CREATE TABLE puede describir la estructura final prevista y aun así llegar a ella mediante operaciones inseguras. Imagina que el generador cambia customer_name text por customer_id bigint. La clave foránea final puede tener sentido, pero una migración que elimina la columna de nombre antes de asociar los nombres históricos con clientes destruye la única evidencia necesaria para esa asociación. La revisión de esquema aprueba el destino; la revisión de migración examina el recorrido.

Lee el modelo propuesto en voz alta usando el lenguaje del dominio. Di que cada factura pertenece a exactamente un cliente legal, en vez de decir que invoices.customer_id hace referencia a customers.id. La primera frase invita a objeciones útiles: puede haber borradores antes de seleccionar un cliente, las facturas importadas pueden referirse a clientes archivados y los registros legales quizá deban conservar el nombre del cliente vigente al emitirlos. El vocabulario de SQL puede ocultar esos desacuerdos.

Exijo una nota de hipótesis junto a cada tabla inferida. Debe indicar qué significa una fila, cómo se identifica, quién la posee, si puede existir sin su padre aparente y qué significa eliminarla. Si el equipo no puede responder a estos puntos, el generador ha adivinado una base de datos que el equipo no ha diseñado.

Los registros conocidos revelan mapeos de tablas erróneos

Un conjunto de datos conocido debe incluir registros elegidos por su cobertura semántica, porque una muestra aleatoria grande suele repetir el mismo caso sencillo. Diez registros bien elegidos pueden revelar más que diez mil filas casi idénticas del caso ideal.

Crea una matriz de mapeo antes de ejecutar DDL. Cada fila de la matriz debe seguir un concepto de origen hasta el destino propuesto y registrar el recuento o valor esperado. Para una aplicación de pedidos, el documento podría verse así:

Hecho conocidoDestino propuestoResultado esperado
El pedido A tiene dos líneasorders y order_itemsUna fila de pedido y dos filas hijas
El pedido B no tiene cuenta asignadaorders.account_idUna fila con cuenta NULL
Dos personas comparten un correocontacts.emailAmbas filas se conservan salvo que la unicidad sea una regla declarada
El código de producto tiene ceros inicialesproducts.codeEl valor de texto 00417 permanece sin cambios
Un pedido cancelado conserva los cargosorders y chargesLas filas de cargos se mantienen tras la cancelación

Esto detecta errores de mapeo de tablas antes de que los detalles de las restricciones distraigan la revisión. Los generadores de IA suelen normalizar objetos repetidos en tablas separadas, lo que normalmente tiene sentido, pero la repetición no demuestra identidad. Dos direcciones de envío con el mismo texto pueden ser instantáneas históricas y no referencias a una dirección editable. Unirlas hace que una edición posterior reescriba la historia.

También ocurre el error contrario. Un generador puede copiar campos del cliente en cada pedido porque la pantalla los muestra juntos. Algunos valores pertenecen al cliente, mientras que otros deben permanecer como instantánea del pedido. El diseño correcto puede incluir tanto customer_id como campos emitidos del documento, por ejemplo billing_name. Llamar a eso duplicación y eliminar una parte pierde la identidad actual o la verdad histórica.

Carga el conjunto de datos conocido en una base de datos desechable mediante la misma ruta de importación o datos iniciales que usará la aplicación. Después escribe aserciones sobre hechos, no solo sobre recuentos de filas:

SELECT
    (SELECT count(*) FROM orders WHERE external_id = 'ORDER-A') AS order_a,
    (SELECT count(*) FROM order_items i
       JOIN orders o ON o.id = i.order_id
      WHERE o.external_id = 'ORDER-A') AS order_a_items,
    (SELECT account_id IS NULL FROM orders
      WHERE external_id = 'ORDER-B') AS order_b_unassigned;

Una forma aprobada debe ser explícita:

 order_a | order_a_items | order_b_unassigned
---------+---------------+---------------------
       1 |             2 | t

No aceptes una diferencia sin explicación porque la aplicación generada todavía se muestre. Una interfaz puede ocultar padres duplicados, hijos eliminados, códigos truncados y valores predeterminados inventados. Reconcilia cada caso de prueba intencional antes de hablar del despliegue en producción.

Las restricciones deben codificar verdades del dominio

Una restricción de base de datos debe rechazar un estado que siempre es inválido, sin importar qué pantalla, API, importación o script de corrección escriba la fila. Si una regla tiene excepciones o depende de hechos externos cambiantes, forzarla en una restricción simple suele provocar trabajo bloqueado o datos deshonestos.

Las claves primarias identifican filas, pero no aportan automáticamente una identidad de negocio significativa. Un ID bigint interno puede coexistir con un número de pedido único dentro de cada tenant. Si el negocio dice que los números de pedido son únicos por tenant, UNIQUE (tenant_id, order_number) expresa esa regla. Una restricción única global rechazaría registros legítimos, mientras que no imponer ninguna permitiría ambigüedad durante los reintentos.

Las restricciones CHECK son adecuadas para hechos estables de una fila, como quantity > 0 o finished_at >= started_at. El manual de PostgreSQL explica que la base de datos supone que una expresión CHECK es inmutable durante toda la vida de la restricción. Por eso, un CHECK que llama a una función cuyo comportamiento cambia después puede dejar filas antiguas incumpliendo la regla aparente. Usa una expresión fija para una verdad fija. Coloca una política cambiante, como un conjunto permitido que los administradores controlan en ese momento, en una tabla referenciada o en el flujo de la aplicación.

Las restricciones de estado generadas requieren cautela. Un generador puede inspeccionar los ejemplos actuales y producir:

status text NOT NULL
    CHECK (status IN ('draft', 'active', 'closed'))

Eso solo es correcto si esos son todos los estados completos y duraderos. Pregunta por registros fallidos, cancelados, suspendidos, importados y heredados desconocidos. Si la máquina de estados sigue cambiando, una tabla de consulta puede hacer explícitas las adiciones, pero no sustituye la validación de transiciones. Que una fila pueda contener closed no dice nada sobre si puede pasar directamente de draft a closed.

Usa la unicidad con intención. PostgreSQL implementa una restricción única con un índice B-tree único, pero un índice único parcial expresa otra regla. La eliminación lógica suele requerir unicidad solo entre filas activas:

CREATE UNIQUE INDEX users_tenant_email_live_uq
    ON users (tenant_id, lower(email))
    WHERE deleted_at IS NULL;

Esto no equivale a UNIQUE (tenant_id, email, deleted_at). PostgreSQL trata los valores NULL según sus reglas de unicidad, y añadir la marca de tiempo de eliminación cambia la identidad que se impone. Revisa los casos concretos de duplicados con casos de prueba en vez de deducir el comportamiento a partir de la lista de columnas.

La nulabilidad es una decisión de negocio

Define una columna como NOT NULL solo cuando el dominio exige un valor para cada fila legítima y todas las rutas de escritura pueden proporcionarlo. El diseño de una pantalla es evidencia débil. Un campo obligatorio en el formulario actual no dice nada sobre importaciones, borradores, filas generadas por el sistema ni registros históricos.

Revisa por separado cuatro estados: el origen omitió el campo, el origen envió explícitamente null, el origen envió un valor vacío y el origen proporcionó un valor significativo. Las API JSON, los formularios, las importaciones CSV y PostgreSQL pueden tratar estos estados de forma distinta. Si la aplicación unifica los cuatro antes de insertar, la revisión de esquema debe mostrar esa decisión en vez de fingir que la base de datos la resolvió.

Los valores predeterminados merecen la misma atención. Un valor predeterminado proporciona un valor cuando un INSERT omite la columna; no corrige un NULL explícito ni demuestra que el valor sea cierto. country_code DEFAULT 'US' es peligroso si puede existir un país desconocido. La fila contiene entonces una mentira convincente en la que pueden confiar los informes y la lógica de cumplimiento.

Una migración generada común añade una columna obligatoria en una sola instrucción:

ALTER TABLE customers
    ADD COLUMN account_type text NOT NULL DEFAULT 'standard';

La instrucción puede ejecutarse, pero convierte a cada cliente histórico en estándar sin evidencia. Una secuencia más segura añade la columna que admite nulos, obtiene valores de datos conocidos, mide las filas sin resolver, impide nuevas omisiones en las escrituras de la aplicación y solo entonces añade NOT NULL si el dominio lo respalda. Si lo desconocido sigue siendo legítimo, conserva NULL y define cómo lo muestran las consultas y las interfaces.

PostgreSQL ofrece una separación útil para algunas restricciones. Se puede añadir un CHECK o una clave foránea como NOT VALID, lo que evita validar todas las filas existentes durante la creación, y comprobarla más tarde con VALIDATE CONSTRAINT. El manual documenta esto como una forma de posponer el análisis inicial de la tabla. No autoriza a ignorar violaciones antiguas: las nuevas escrituras se aplican, y la validación debe superarse antes de aprobar.

Antes de endurecer la nulabilidad, ejecuta una consulta de distribución que muestre las categorías reales:

SELECT
    count(*) AS total,
    count(*) FILTER (WHERE account_type IS NULL) AS nulls,
    count(*) FILTER (WHERE account_type = '') AS empty_strings,
    count(*) FILTER (WHERE account_type NOT IN
        ('standard', 'partner', 'internal')) AS unexpected
FROM customers;

Un valor predeterminado generado por IA puede hacer que esta consulta parezca limpia después de la migración. Ejecútala también antes del relleno de datos y conserva el resultado. De lo contrario, perderás la evidencia necesaria para distinguir los valores derivados de los inventados.

Los índices deben responder a patrones de acceso observados

Separa las pantallas de los hechos
Genera la aplicación a partir de un modelo escrito, en vez de dejar que los ejemplos de pantalla definan la base de datos.

Aprueba un índice cuando respalde una consulta conocida, imponga una regla de unicidad declarada o permita un requisito operativo. Indexar cada columna que parece identificadora desperdicia almacenamiento y trabajo de escritura, mientras que no incluir un índice compuesto importante puede convertir una página de lista habitual en un análisis cada vez mayor.

Empieza por las consultas que la aplicación generada realmente emite. Registra las columnas de filtro, el límite del tenant, las columnas de unión, el orden y el tamaño esperado del resultado. Para una página de pedidos recientes, esta forma de consulta importa más que el diagrama de tablas:

SELECT id, order_number, status, created_at
FROM orders
WHERE tenant_id = $1
  AND status = $2
ORDER BY created_at DESC
LIMIT 50;

Un índice solo sobre tenant_id todavía puede inspeccionar muchas filas del tenant y ordenarlas. Un índice sobre (tenant_id, status, created_at DESC) se ajusta más a este patrón de acceso. El orden de las columnas no es un concurso de popularidad: sigue las condiciones de igualdad, las condiciones de rango, el orden y la selectividad de la consulta real.

Ejecuta EXPLAIN (ANALYZE, BUFFERS) con datos representativos, pero no tomes un caso de prueba pequeño como prueba de rendimiento. PostgreSQL puede preferir correctamente un análisis secuencial en una tabla diminuta. La verificación debe confirmar que el índice previsto existe y que un ensayo de tamaño similar a producción ofrece al planificador una elección realista. Nunca desactives los análisis secuenciales para fabricar un análisis por índice que justifique la aprobación.

Las claves foráneas traen otra sorpresa habitual: PostgreSQL indexa las columnas primarias o únicas referenciadas, pero no crea automáticamente un índice en las columnas hijas que referencian. Por ello, eliminar o actualizar un padre puede analizar la tabla hija para comprobar referencias. Las uniones de hijo a padre también pueden necesitar ese índice hijo. Inspecciona cada relación según las lecturas esperadas y los cambios en el padre.

Rechaza índices duplicados y sin uso en la propuesta inicial. (tenant_id, status) puede ser redundante si ya existe un índice adecuado (tenant_id, status, created_at), aunque los detalles de la carga de trabajo pueden cambiar esa valoración. Compara definiciones, no nombres. Los generadores de IA suelen crear un índice por función y no advierten que varias funciones solicitaron las mismas columnas iniciales.

En bases de datos existentes con mucha actividad, recuerda que CREATE INDEX CONCURRENTLY no puede ejecutarse dentro de un bloque de transacción, requiere más trabajo y puede dejar un índice inválido tras un fallo. El manual de PostgreSQL detalla esas diferencias operativas. Un framework de migración que envuelve cada migración en una transacción necesita una excepción explícita y un procedimiento de limpieza, no sustituir una palabra clave con optimismo.

Las claves foráneas necesitan reglas de propiedad y eliminación

Una clave foránea solo es correcta después de que el equipo decide si la relación expresa propiedad, referencia, contexto opcional o atribución histórica. Columnas parecidas pueden requerir comportamientos de eliminación opuestos.

Considera projects.owner_user_id, invoices.customer_id y audit_events.actor_user_id. Un proyecto puede transferir su propiedad. Una factura puede tener que sobrevivir al cierre de la cuenta del cliente. Un evento de auditoría puede conservar el identificador anterior del actor incluso tras eliminar los datos de identidad. Aplicar ON DELETE CASCADE a los tres solo porque hacen referencia a users codificaría una ficción destructiva.

Usa CASCADE cuando la fila hija no tenga significado sin el padre y eliminar al padre implique realmente eliminar todo el agregado. Las líneas de pedido suelen encajar. Los registros de pago, documentos emitidos, importaciones, registros de actividad y evidencia de moderación a menudo no. Para ellos puede encajar mejor el rechazo, el archivado, la anonimización controlada o una referencia que admita nulos junto con campos de instantánea conservados.

SET NULL también exige una revisión semántica. Conserva la fila hija, pero elimina la relación directa. Si el personal necesita explicar después qué cuenta creó un informe, una referencia nula puede ser insuficiente. Conservar un token histórico no identificable o una instantánea puede preservar la responsabilidad sin retener todos los datos personales, pero la decisión exacta de conservación corresponde a la política del producto, no a una suposición de IA.

Comprueba la cardinalidad en ambos sentidos. Un generador puede modelar uno a uno colocando una clave foránea sin una restricción única, lo que permite silenciosamente varias filas hijas. También puede imponer unicidad cuando el historial necesita varias versiones. Escribe casos de prueba para un padre con cero, uno y varios hijos, y después indica qué inserciones deben aprobarse.

Las restricciones aplazables necesitan una razón concreta. Pueden ayudar cuando una transacción debe incumplir temporalmente el orden de referencia o actualizar filas mutuamente dependientes, pero aplazar por defecto cada clave foránea mueve los errores al momento de confirmar y dificulta encontrar los fallos. Mantén la aplicación inmediata salvo que una secuencia real de transacciones requiera posponerla.

Inspecciona el catálogo después de aplicar la migración en la base de datos de ensayo:

SELECT
    conname,
    contype,
    convalidated,
    pg_get_constraintdef(oid) AS definition
FROM pg_constraint
WHERE conrelid = 'public.orders'::regclass
ORDER BY conname;

Una salida representativa tiene esta forma:

       conname        | contype | convalidated | definition
----------------------+---------+--------------+---------------------------------------
 orders_pkey          | p       | t            | PRIMARY KEY (id)
 orders_customer_fk   | f       | t            | FOREIGN KEY (customer_id) REFERENCES customers(id)
 orders_total_check   | c       | t            | CHECK ((total_cents >= 0))

Compara las definiciones obtenidas con las reglas de propiedad aprobadas. El éxito de la migración por sí solo no revela una acción ausente, un aplazamiento inesperado ni una restricción sin validar.

Los cambios destructivos se esconden en SQL razonable

Inspecciona el recorrido de los datos
Exporta el código fuente cuando una migración generada requiera revisar más de cerca conversiones y rellenos de datos.

Revisa la migración como una transformación de datos, porque un DDL aparentemente ordenado puede descartar significado sin usar un DROP TABLE evidente. Busca primero la destrucción directa y después inspecciona conversiones, rellenos de datos, reescrituras, cambios de nombre y sustituciones de restricciones.

Un cambio de nombre y una eliminación son operativamente distintos aunque el esquema final parezca idéntico. Si surname pasa a ser family_name, un cambio de nombre conserva los datos y las dependencias con más fidelidad. Eliminar la columna antigua y añadir la nueva produce el mismo diagrama, pero vacía todos los valores. Las migraciones generadas suelen inferir el estado final sin entender la continuidad.

Los cambios de tipo necesitan conversiones de muestra y casos de rechazo. Convertir identificadores de texto en enteros puede eliminar ceros iniciales o rechazar identificadores mixtos. Reducir la precisión numérica puede redondear valores. Convertir marcas de tiempo exige una hipótesis declarada sobre la zona horaria. Prueba la expresión USING real con valores mínimo, máximo, nulo, malformado e históricamente extraño antes de modificar la columna.

Trata estas operaciones como si requirieran justificación escrita: eliminar una tabla o columna, cambiar un tipo mediante una conversión con pérdida, sustituir una columna con datos, añadir CASCADE, establecer NOT NULL tras un relleno generado y reconstruir la unicidad con columnas diferentes. Revisa también SQL sin procesar incluido en funciones o callbacks de migración generados. La búsqueda de texto es un filtro inicial, no toda la revisión.

Un patrón de fallo aparece repetidamente. Los datos conocidos contienen contactos con empresas opcionales, pero la pantalla de muestra solo muestra contactos empresariales. El generador hace que contacts.company_id sea NOT NULL e inserta una empresa generada llamada Unknown para las filas sin coincidencia. La migración se completa, los recuentos cuadran y todas las claves foráneas se validan. Los datos siguen estando mal: los contactos particulares ahora parecen pertenecer a una empresa, los informes agrupan a personas no relacionadas y eliminar el marcador de posición puede propagarse a contactos reales.

La solución no es otro valor predeterminado. Restaura el estado de origen, permite nulos en la relación, migra solo las coincidencias respaldadas por evidencia y añade una aserción que confirme que el conjunto sin coincidencia equivale a los contactos particulares conocidos. Por eso los casos de prueba semánticos deben registrar las relaciones esperadas, no solo los recuentos esperados de filas.

Las herramientas de diff de esquemas ayudan, pero no aprobaría basándome solo en un diff. La recomendación es popular porque un diff es compacto y fácil de revisar. Como única barrera es insuficiente porque muestra el cambio estructural, no la procedencia de los valores rellenados, los límites de transacción, el comportamiento de bloqueos ni la verdad posterior a la migración.

Un ensayo debe demostrar resultados y comportamiento ante fallos

Ejecuta la migración completa sobre una restauración desechable del conjunto de datos conocido, y luego prueba tanto el resultado previsto como una ruta interrumpida o rechazada. Una base de datos nueva y vacía ayuda a detectar errores de orden, pero no puede revelar conversiones con pérdida, filas históricas inválidas o validaciones lentas.

Usa esta secuencia de ensayo como documento de lanzamiento:

  1. Restaura el conjunto de datos anterior al cambio en una base de datos aislada y registra los recuentos de filas junto con las aserciones semánticas.
  2. Captura el esquema actual, aplica el artefacto de migración exacto y guarda toda la salida con los tiempos y límites de transacción.
  3. Ejecuta comprobaciones del catálogo, aserciones de mapeo, pruebas de rechazo de restricciones y consultas representativas de la aplicación.
  4. Compara los valores importantes con las expectativas registradas, incluidas las categorías sin coincidencia y nulas.
  5. Ejecuta el método de recuperación documentado y vuelve a ejecutar las aserciones previas a la migración contra la base de datos recuperada.

Captura un volcado solo del esquema antes y después:

pg_dump --schema-only --no-owner --no-privileges \
  --dbname "$DATABASE_URL" > schema.sql

Revisa las tablas, secuencias, índices, restricciones, funciones, triggers, extensiones y privilegios relevantes para la aplicación. Un diff del modelo ORM puede omitir objetos de base de datos que la aplicación no modela, especialmente triggers, índices de expresiones, índices parciales y funciones instaladas manualmente.

Añade pruebas negativas que demuestren que las restricciones rechazan estados inválidos. Una transacción de prueba puede intentar una inserción inválida y revertirse sin importar el resultado:

BEGIN;

INSERT INTO order_items (order_id, quantity, unit_price_cents)
VALUES (1001, 0, 2500);

ROLLBACK;

La salida esperada debe nombrar la restricción infringida, por ejemplo:

ERROR:  new row for relation "order_items" violates check constraint "order_items_quantity_check"
DETAIL:  Failing row contains (..., 0, 2500, ...).

No compares el texto completo del error entre todos los entornos porque los detalles pueden variar. En las pruebas automatizadas, comprueba el SQLSTATE o la identidad de la restricción y conserva una salida legible para quien revise.

Mide los bloqueos y la duración con un conjunto de datos lo bastante grande para parecerse al despliegue previsto. Una operación que termina al instante con cincuenta filas puede bloquear escrituras al validar millones. En una base de datos de producción inicial vacía, el riesgo inmediato es menor, pero el ensayo sigue probando los datos iniciales importados y crea una referencia para cambios posteriores.

La recuperación necesita más que una migración inversa

Ensaya la recuperación
Usa instantáneas y reversión como parte de una ruta de recuperación probada para aplicaciones generadas.

La recuperación es creíble solo cuando restaura los datos y la compatibilidad de la aplicación dentro del tiempo que el servicio puede tolerar. Una migración inversa que recrea columnas eliminadas no recupera sus valores anteriores.

Elige la unidad de recuperación antes de ejecutar. Para una base de datos inicial vacía, eliminar y recrear la base puede ser aceptable si todavía no han empezado las escrituras de usuarios. Cuando ya existen escrituras reales, la recuperación puede requerir una instantánea de base de datos, una copia de seguridad lógica, columnas antiguas conservadas o una corrección hacia adelante. El método correcto depende de cuántos datos nuevos puedan llegar durante y después de la migración.

Prueba los comandos de restauración y las credenciales antes de depender de ellos. Una copia de seguridad que existe pero que el operador de despliegue no puede restaurar no es un plan de recuperación. Restaura en una base de datos independiente, verifica la propiedad y las extensiones, y luego ejecuta las mismas aserciones conocidas que se usaron antes de la migración.

Las instantáneas y la reversión de transacciones resuelven fallos distintos. Una transacción puede deshacer instrucciones cuando una migración falla antes de confirmar, siempre que todas las operaciones participen en ella. Una instantánea puede devolver toda la base de datos a un estado anterior, pero al hacerlo puede descartar escrituras legítimas realizadas después de la instantánea. Ninguno de los dos mecanismos reconcilia automáticamente esas escrituras.

Prefiere cambios aditivos cuando siga habiendo incertidumbre. Añade la nueva columna o tabla, copia datos con reglas medibles, ejecuta ambas rutas de código durante un periodo controlado si es necesario y elimina la estructura antigua solo después de verificar. Este enfoque de expansión y contracción requiere trabajo adicional, pero conserva la evidencia. Mantener una columna antigua renombrada durante una versión suele costar menos que reconstruirla a partir de registros.

Escribe por adelantado los desencadenantes de recuperación. Algunos ejemplos son una aserción semántica fallida, registros inesperados sin coincidencia, una restricción inválida, una migración que supere su ventana de bloqueo aprobada o errores de aplicación provocados por incompatibilidad de versiones. El operador no debería inventar la decisión mientras los usuarios esperan.

Registra el punto a partir del cual restaurar la base de datos anterior también exige restaurar la aplicación anterior. Una aplicación nueva puede depender de una columna nueva, mientras que una aplicación antigua puede rechazar un valor enum nuevo o escribir la forma anterior. La recuperación de la base de datos y de la aplicación debe usar versiones compatibles.

La aprobación exige evidencia, no confianza

Aprueba la primera migración solo cuando otra persona pueda reproducir por qué es segura a partir de los artefactos almacenados. La confianza que nace de una revisión de código limpia o de una interfaz generada pulida no resiste la primera discrepancia de datos sin explicación.

El registro de aprobación debe contener las hipótesis inferidas, la matriz de mapeo de tablas, la identidad del conjunto de datos conocido, el diff del esquema, la migración exacta, las consultas y resultados de validación, las pruebas de entradas rechazadas, el razonamiento de índices, las justificaciones de operaciones destructivas y el procedimiento de recuperación probado. Identifica a quien revisó y conserva como bloqueadores las decisiones pendientes en vez de enterrarlas en el historial del chat.

Cuando se genera una aplicación en Koder.ai, usa el modo de planificación para documentar estas decisiones de esquema antes de permitir la migración, exporta el código fuente para revisarlo y trata las instantáneas y la reversión como herramientas de recuperación que todavía requieren un ensayo con el conjunto de datos conocido.

No dejes que el generador apruebe su propia inferencia regenerando código hasta que las pruebas pasen. Ese ciclo puede hacer que la aplicación se adapte a un esquema erróneo en vez de corregir el modelo. Una persona debe decidir si la base de datos coincide con el dominio, especialmente en torno a identidad, eliminación, conservación y valores desconocidos.

La consulta de aprobación final debe ser aburrida. Cada hecho conocido se asigna a un resultado esperado, cada restricción rechaza el contraejemplo previsto, cada operación destructiva tiene una razón y la restauración reproduce las aserciones previas a la migración. Si la evidencia necesita una explicación persuasiva para justificar una discrepancia, detén la migración. PostgreSQL aplicará el esquema con precisión, incluidas las partes que el generador haya adivinado mal.

Preguntas frecuentes

¿Qué debo comprobar en un esquema PostgreSQL generado por IA?

Revisa el DDL generado, las operaciones de migración y las hipótesis que sustentan ambos. Un esquema final correcto puede alcanzarse mediante una migración que borra datos, bloquea escrituras o inventa valores predeterminados engañosos.

¿Qué tamaño debe tener un conjunto de datos para verificar un esquema?

Usa un conjunto de datos pequeño con filas habituales, valores límite, relaciones ausentes, duplicados, valores nulos, cadenas vacías y casos históricos poco comunes. Su finalidad no es aportar volumen, sino refutar las hipótesis del generador antes de que lo hagan los datos de producción.

¿Una migración de prueba exitosa demuestra que el esquema es seguro?

No. Una migración correcta demuestra que PostgreSQL aceptó las instrucciones para ese estado de la base de datos. No demuestra que los mapeos de tablas sean correctos, que los datos conservaran su significado, que los índices sirvan para las consultas reales ni que la recuperación funcione.

¿Cuándo debe ser NOT NULL una columna de PostgreSQL?

Un campo debe ser NOT NULL solo si todos los registros legítimos tienen valor y la aplicación puede proporcionarlo en todas las rutas de escritura. No uses un valor predeterminado inventado solo para cumplir la restricción, porque sustituye datos ausentes visibles por datos falsos pero creíbles.

¿Debo usar una restricción única o un índice único?

Una restricción única expresa una regla a la que pueden hacer referencia otros objetos de la base de datos, y PostgreSQL la respalda con un índice. Un índice único sirve cuando la unicidad se aplica solo a filas o expresiones concretas, como registros no eliminados o direcciones de correo normalizadas.

¿Las claves foráneas de PostgreSQL crean índices automáticamente?

Indexa las columnas que sirven para localizar filas padre, filtrar consultas frecuentes, unir tablas grandes o imponer unicidad. PostgreSQL no crea automáticamente un índice en el lado que referencia una clave foránea, así que revisa las columnas hijas por separado en vez de asumir que la restricción lo resolvió.

¿Cuándo es seguro usar ON DELETE CASCADE?

Usa CASCADE solo cuando la fila hija pierde todo significado independiente al desaparecer su padre. Si eliminarla requiere una decisión de negocio o la fila es evidencia, como una factura o un registro de auditoría, rechaza o procesa explícitamente la eliminación.

¿Cómo puedo detectar cambios destructivos en una migración?

Trata cada DROP, conversión con pérdida, reescritura de tabla, nueva columna obligatoria y sustitución de restricción como potencialmente destructivos. Busca en el texto de la migración, pero revisa también las funciones generadas y el SQL sin procesar, porque ahí puede ocultarse comportamiento destructivo.

¿Cuál es la forma más segura de probar la recuperación de una migración?

Restaura la base de datos anterior al cambio en una ubicación independiente, ejecuta allí la migración, realiza consultas de verificación semántica y compara los resultados con las expectativas registradas. Probar solo la migración inversa no recupera datos eliminados y puede dar una falsa sensación de seguridad.

¿Qué evidencias debo guardar después de aprobar un esquema?

Guarda juntos el DDL generado, el texto de la migración, el diff del esquema, las consultas y resultados de verificación, el procedimiento de recuperación y la identidad de quien revisó. Ese registro explica qué se aprobó y ayuda a detectar hipótesis que hayan cambiado en la siguiente revisión.

Related posts