panels-origin/db/RUNBOOK-corte.md
Cursor Agent d36c287a82
db/infra: ETL de datos, backups y limpieza de despliegue (fase 5-7)
Fase 5 (migracion de datos):
- api/scripts/migrate-sqlite-to-postgres.ts: ETL unico SQLite -> Postgres.
  Pre-flight (CURP/RFC duplicados case-insensitive, FKs huerfanas,
  tenant_id invalido, fechas mal formateadas) -> carga por base/esquema
  con credenciales _owner (setval de secuencias, OVERRIDING SYSTEM VALUE,
  ON CONFLICT DO NOTHING idempotente) -> verificacion de conteos y sumas
  de dinero con tolerancia. Probado end-to-end contra un dataset SQLite
  legacy sintetico y un Postgres limpio (solo schema+catalogos, sin
  contexto dev): 100% de las filas migradas, sumas de dinero exactas,
  snapshot de uploaded_by/created_by resuelto correctamente.
- db/RUNBOOK-corte.md: checklist go/no-go para el corte por ambiente.
- Fix de bug real encontrado al probar: sin --context-filter explicito,
  Liquibase corre TAMBIEN los changesets context=dev (comportamiento por
  defecto, no "modo seguro") -- documentado en db/README.md con el
  ejemplo correcto (--context-filter='!dev' para staging/produccion).

Fase 5b: patron de compensacion para createTenant ya resuelto en el
rewrite de saas.ts (fase 3/4).

Fase 6 (backups): db/backups/ con plantillas pgBackRest por base,
politica de retencion, nota de persistencia minima de Redis (RDB, sin
retencion de negocio), checklist de simulacro de restauracion mensual,
y la validacion pendiente de si Coolify permite WAL archiving antes de
comprometerse a PITR real.

Fase 7 (limpieza y CI):
- Dockerfile.api simplificado (sin JRE/Liquibase/FFI). Dockerfile.migrate
  y Dockerfile.provision nuevos, de un solo uso, para el paso explicito
  de deploy (nunca sirven trafico).
- docker-compose.yml: postgres+redis+provision+migrate para dev local
  end-to-end; api ya no arranca hasta que migrate termina bien.
- docs/coolify.md y .env.example actualizados al modelo de 2 bases +
  Redis + Contabo.
- api/schema.sql eliminado (desincronizado, competia con Liquibase como
  fuente de verdad).
- .github/workflows/ci.yml: Postgres+Redis de servicio, aprovisiona
  roles/ACLs, dry-run de Liquibase (updateSQL) antes de aplicar,
  verify-isolation.sh, deno check + deno test, build de los dos frontends.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-02 21:05:07 +00:00

3.8 KiB

Runbook de corte SQLite → Postgres (Fase 5)

Checklist operativo para migrar UN ambiente (dev, luego staging, luego producción -- nunca saltar directo a producción). Usa api/scripts/migrate-sqlite-to-postgres.ts.

0. Antes de empezar

  • Postgres del ambiente aprovisionado (db/provision/) y migrado sin datos de demo: ./db/update.sh all --context-filter='!dev'.
  • Backup fresco del volumen SQLite actual (data/app.db, data/platform.db) guardado aparte, fuera del volumen que se va a apagar.
  • Credenciales _owner de los 3 módulos disponibles en el entorno (DATABASE_URL_PLATFORM_OWNER, DATABASE_URL_IAM_OWNER, DATABASE_URL_CORE_OWNER).

1. Pre-flight (sin ventana de mantenimiento, se puede correr en caliente)

cd api
deno run --allow-ffi --allow-net --allow-read --allow-write --allow-env \
  scripts/migrate-sqlite-to-postgres.ts --dry-run \
  --app-db=../data/app.db --platform-db=../data/platform.db

Si el pre-flight reporta [FAIL], no continuar -- corregir los datos en SQLite (duplicados, huérfanas, tenant_id inválido, fechas mal formateadas) y repetir hasta que todo salga [OK].

2. Ventana de mantenimiento (corte real)

Hoy no existe un "modo mantenimiento" en la app. Opciones, de menor a mayor invasividad:

  • Parar el contenedor/proceso api (nadie puede escribir mientras está abajo -- los fronts mostrarán error de conexión).
  • Responder 503 temporal en nginx para /v1/* mientras se corre el ETL.

Elegir una, documentar la hora exacta de inicio.

3. Migración

cd api
set -a && source /ruta/al/.env.del.ambiente && set +a
deno run --allow-ffi --allow-net --allow-read --allow-write --allow-env \
  scripts/migrate-sqlite-to-postgres.ts \
  --app-db=/ruta/a/app.db --platform-db=/ruta/a/platform.db

El script hace: pre-flight → carga (una transacción por base/esquema, todo o nada) → setval() de secuencias → verificación de conteos y sumas de dinero. Si CUALQUIER paso falla, no queda un estado a medias en Postgres (la transacción de esa base se revierte completa), pero SQLite sigue siendo la fuente de verdad -- no se ha cortado nada todavía.

4. Criterios go/no-go

Antes de apuntar la app a Postgres y apagar SQLite:

  • El script terminó con Migración completa y verificada. (exit code 0).
  • Conteos de filas origen=destino en todas las tablas listadas (no solo las 9 de ejemplo del script -- ampliar verifyCounts() si el ambiente tiene datos en tablas no cubiertas ahí).
  • Diferencia de sumas de dinero dentro de la tolerancia ($0.05) -- si no cuadra, decidir explícitamente si se acepta el redondeo REAL→NUMERIC o se investiga antes de continuar.
  • Spot-check manual de 2-3 registros conocidos (un trabajador, un preupuesto, un préstamo) comparando app vieja vs. Postgres.

Si algo no cuadra: no cortar. Volver a levantar la app contra SQLite (no se tocó), investigar, y repetir desde el paso 1 en otro intento.

5. Cutover

  • Actualizar DATABASE_URL_*/REDIS_URL_* del servicio api a los valores del ambiente Postgres/Redis recién migrado.
  • Levantar api -- el fail-fast de arranque (/v1/health) debe responder {"ok":true,...} antes de reabrir tráfico.
  • Reabrir tráfico (quitar el 503/levantar el contenedor).
  • Login de prueba con un usuario real del ambiente.

6. Después del corte

  • Conservar el volumen SQLite (data/) como respaldo frío por un período de retención definido (ej. 30 días) antes de borrarlo.
  • Correr ./db/provision/verify-isolation.sh contra el ambiente para confirmar que el aislamiento de roles/RLS/ACLs sigue intacto.
  • Repetir todo el runbook en el siguiente ambiente (dev → staging → producción), nunca en paralelo.