Skip to main content

DDL y ops en Postgres

La regla: el schema no se deriva de las entidades (synchronize está OFF). Todo cambio de schema es un .sql idempotente que alguien corre a mano antes de levantar el código que lo necesita.

Dónde van los archivos

CarpetaQué contiene
migrations/postgres/ddl/anf-<issue>-<tema>.sqlEl DDL de cada feature. Convención vigente: un archivo por issue
migrations/postgres/configuration/, migrations/postgres/finances/Objetos por schema
migrations/configuration/sps/ (12), migrations/finances/sps/ (20)Stored procedures
scripts/*.sqlSPs y tablas históricas (install_all_tables.sql, stored_procedures.sql, sp_*, create_*)
src/migrations/*.tsSeed / bootstrap idempotente en TypeScript, corre solo al arrancar. NO es DDL

Ejemplos del naming: anf-407-rules-engine.sql, anf-437-permissions.sql, anf-435-health-centers.sql, anf-439-patient-unique.sql, anf-408-patient-companyid.sql.

Checklist al cambiar el schema

  1. Escribí el .sql idempotente (ADD COLUMN IF NOT EXISTS, CREATE TABLE IF NOT EXISTS, CREATE INDEX IF NOT EXISTS) en migrations/postgres/ddl/anf-<issue>-*.sql.
  2. Agregá la columna o la tabla a la entidad TypeORM.
  3. Anotá en el plan de la feature que OPS tiene que correrlo antes del deploy, y en qué orden si hay varios archivos.
  4. Si el dato preexistente necesita backfill (típico: poblar COMPANYID o HEALTHCENTERID), va en el mismo archivo o en un -backfill.sql aparte.
Sin backfill, el filtro fail-closed deja al usuario sin ver nada

Los dos scopings del sistema son fail-closed: sin COMPANYID poblado, un usuario no privilegiado ve cero filas; sin HEALTHCENTERID, los episodios viejos desaparecen de la bandeja. Ver Multi-tenant y Centro médico activo.

Gotchas

Si el DDL no corrió, el síntoma no es "falta una columna"

Es que todos los endpoints que tocan esa entidad devuelven error — no solo los que usan la columna nueva. Es el primer sospechoso cuando un módulo entero se cae después de un deploy.

Índices únicos y NULL

En Postgres dos NULL se consideran distintos en un índice único: UNIQUE (COMPANYID, CODE) no impide dos filas con COMPANYID IS NULL y el mismo CODE.

Si la columna del scope es nullable, indexá COALESCE(COMPANYID, 0). Y para unicidad case-insensitive y a prueba de espacios, indexá UPPER(TRIM(CODE))upper() y btrim() son IMMUTABLE, así que se pueden indexar. (Verificado contra Postgres 16.)

La validación de aplicación tiene que espejar la expresión del índice EXACTAMENTE. Si el índice normaliza y el service compara el valor crudo, el usuario ve un 23505 de Postgres en vez del mensaje de negocio. Y validá también en el update, no solo en el create.

CREATE INDEX CONCURRENTLY

Es el equivalente Postgres del ALGORITHM=INPLACE, LOCK=NONE de MySQL: no toma lock de escritura. Dos caveats:

  • no corre dentro de una transacción;
  • si falla, deja el índice INVALID → hay que dropearlo y reintentar.

Índices de prefijo no existen

USUARIO(60) es exclusivo de MySQL. En Postgres no existe y casi nunca hace falta. Donde MySQL usaba un índice compuesto para filtrar por una constante, suele ser mejor un índice parcial (... WHERE TIPOVALIDACION = 'PRACTICA').

Los stored procedures no viajan con el código

Después de restaurar o migrar la base, hay que re-aplicar los SPs

Un SP modificado no viaja con el deploy. Y varias reglas de negocio (entre ellas MÓDULO vs COMÚN) viven a la vez en la aplicación y en un SP: cambiar una sin la otra produce números que no cierran. Ver Facturación y débitos.

Los tests de caracterización de SPs necesitan una base real: npm run test:postgres / npm run test:mysql. Ver Testing.

El seed corre al boot

El seed de permisos y roles (RolesPermissionMigrations) corre solo al arrancar la app. Si agregás permisos al catálogo, hay que reiniciar para que existan en la tabla. Ver Permisos y roles.

Headers nuevos: CORS y nginx

Un header custom (como X-Health-Center-Id) tiene que estar permitido en el CORS del backend y en nginx. Si falta en cualquiera de los dos, todos los requests fallan por preflight.