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
| Carpeta | Qué contiene |
|---|---|
migrations/postgres/ddl/anf-<issue>-<tema>.sql | El 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/*.sql | SPs y tablas históricas (install_all_tables.sql, stored_procedures.sql, sp_*, create_*) |
src/migrations/*.ts | Seed / 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
- Escribí el
.sqlidempotente (ADD COLUMN IF NOT EXISTS,CREATE TABLE IF NOT EXISTS,CREATE INDEX IF NOT EXISTS) enmigrations/postgres/ddl/anf-<issue>-*.sql. - Agregá la columna o la tabla a la entidad TypeORM.
- Anotá en el plan de la feature que OPS tiene que correrlo antes del deploy, y en qué orden si hay varios archivos.
- Si el dato preexistente necesita backfill (típico: poblar
COMPANYIDoHEALTHCENTERID), va en el mismo archivo o en un-backfill.sqlaparte.
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
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
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.