# Database - Documentación Completa Este directorio contiene los scripts SQL para inicializar y gestionar la base de datos PostgreSQL del sistema de trazabilidad. ## Tabla de Contenidos - [Archivos SQL](#archivos-sql) - [⚠️ Peculiaridades de Implementación](#️-peculiaridades-de-implementación) - [Autenticación PostgreSQL](#autenticación-postgresql) - [Inicialización de la Base de Datos](#inicialización-de-la-base-de-datos) - [Persistencia de Datos](#persistencia-de-datos) - [Conexión desde Node.js](#conexión-desde-nodejs) - [Endpoints de Debug](#endpoints-de-debug) - [Ejecución de Scripts](#ejecución-de-scripts) - [Troubleshooting](#troubleshooting) - [Queries Útiles](#queries-útiles) --- ## Archivos SQL ### `00_configure_auth.sh` Script Bash que configura la autenticación de PostgreSQL: - Elimina configuración `scram-sha-256` (incompatible con driver `pg` de Node.js) - Configura método `md5` en `pg_hba.conf` - Se ejecuta automáticamente durante inicialización de PostgreSQL ### `01_schema.sql` Crea el esquema completo de la base de datos: - Tablas: `lotes`, `operaciones`, `operacion_lotes` - Índices para optimización - Función recursiva `get_trazabilidad()` para queries de trazabilidad completa - Vista `vista_lotes_con_origen` - Constraints y validaciones - **Idempotente**: Usa `CREATE TABLE IF NOT EXISTS` ### `02_seed.sql` Datos de ejemplo que representan un flujo completo de trazabilidad: - Ingreso de uva (2086 kg) - Despulpado → primera, segunda, rechazos - Oreado (con error de registro intencional) - Ajuste de merma (1500 → 1480 kg) - Ajuste de tipo (oreado → presecado) - Reposo - Secado final (mezcla de 2 lotes = 2000 kg) - **Total**: 10 lotes, 7 operaciones, 16 relaciones - **Idempotente con TRUNCATE**: Limpia datos antes de insertar --- ## ⚠️ Peculiaridades de Implementación Esta sección documenta los desafíos técnicos encontrados y las soluciones implementadas durante el desarrollo. ### Autenticación PostgreSQL #### ❌ Problema: Incompatibilidad scram-sha-256 **Contexto**: PostgreSQL 16 usa por defecto el método de autenticación `scram-sha-256`, pero el driver `pg` de Node.js tiene problemas de compatibilidad que resultan en: ``` Error code: 28P01 FATAL: password authentication failed for user "seguidor" ``` #### ✅ Solución: Script de configuración automática **Archivo**: `00_configure_auth.sh` ```bash #!/bin/bash set -e echo "Configurando autenticación de PostgreSQL..." # Eliminar configuración scram-sha-256 sed -i '/scram-sha-256/d' "$PGDATA/pg_hba.conf" # Agregar configuración md5 if ! grep -q "host all all all md5" "$PGDATA/pg_hba.conf"; then echo "host all all all md5" >> "$PGDATA/pg_hba.conf" fi echo "✓ Configuración de autenticación aplicada (md5)" ``` **Notas importantes**: - El script se ejecuta **antes** de que PostgreSQL termine su inicialización - Los cambios en `pg_hba.conf` se aplican automáticamente al finalizar el inicio - No es necesario ejecutar `pg_ctl reload` manualmente - El método `md5` es compatible con el driver `pg` de Node.js --- ### Inicialización de la Base de Datos #### 📂 Scripts en `/docker-entrypoint-initdb.d/` Los scripts se ejecutan **solo si el volumen de datos está vacío** (primera vez): 1. **`00_configure_auth.sh`** - Configura autenticación md5 2. **`01_schema.sql`** - Crea estructura (tablas, funciones, vistas) 3. **`02_seed.sql`** - Inserta datos de ejemplo **Orden de ejecución**: Los archivos se ejecutan en orden alfabético/numérico. #### 🔄 Idempotencia de Scripts ##### `01_schema.sql` - Totalmente Idempotente ✅ ```sql CREATE TABLE IF NOT EXISTS lotes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- ... ); CREATE INDEX IF NOT EXISTS idx_lotes_tipo ON lotes(tipo); ``` - ✅ Puede ejecutarse múltiples veces sin errores - ✅ No duplica tablas ni índices - ✅ Seguro para re-ejecución ##### `02_seed.sql` - Idempotente con TRUNCATE ⚠️ ```sql -- Línea 17: Limpia datos antes de insertar TRUNCATE TABLE operacion_lotes, operaciones, lotes CASCADE; -- Luego inserta datos de ejemplo INSERT INTO lotes (codigo, tipo, ...) VALUES (...); ``` **IMPORTANTE**: - ⚠️ Si las tablas no existen, `TRUNCATE` falla - ✅ Por eso el endpoint `seed-database` ejecuta **primero** el schema - ✅ Puede ejecutarse múltiples veces si las tablas existen #### 🔍 Verificación en Workflow de Gitea Actions El workflow verifica si es necesario inicializar: ```bash TABLE_EXISTS=$(docker exec lotes-postgres psql -U seguidor -d seguidor_lotes \ -tAc "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'lotes');") if [ "$TABLE_EXISTS" = "f" ]; then echo "📝 Ejecutando scripts de inicialización..." docker exec -i lotes-postgres psql -U seguidor -d seguidor_lotes < nuxt4/server/database/01_schema.sql docker exec -i lotes-postgres psql -U seguidor -d seguidor_lotes < nuxt4/server/database/02_seed.sql fi ``` **Comportamiento**: - ✅ Primera vez: Ejecuta schema + seed - ✅ Redeploys: Solo actualiza contenedor de app, **datos persisten** - ✅ Después de `docker-compose down -v`: Reinicializa todo --- ### Persistencia de Datos #### 💾 Volumen Docker ```yaml # docker-compose.yml volumes: postgres_data: # Volumen nombrado services: postgres: volumes: - postgres_data:/var/lib/postgresql/data ``` **Comportamiento**: - ✅ Datos persisten entre `docker-compose down` y `docker-compose up` - ✅ Datos persisten entre redespliegues del workflow - ❌ Datos se pierden con `docker-compose down -v` (elimina volúmenes) #### 🔄 Resetear a Estado Inicial **Opción 1: Eliminar volumen (desde servidor)** ```bash docker-compose --project-name lotes down -v # Próximo deploy reinicializará automáticamente ``` **Opción 2: Usar botones de debug (desde web)** 1. Click en "🗑️ BORRAR TODA LA BD" → Elimina todas las tablas 2. Click en "🌱 CARGAR DATOS DE EJEMPLO" → Recrea schema + seed 3. Recarga la página **Opción 3: Trigger redeploy después de borrar** ```bash # 1. Click "🗑️ BORRAR TODA LA BD" en la web # 2. Trigger redeploy git commit --allow-empty -m "Trigger reinit DB" && git push # El workflow detectará tablas faltantes y ejecutará schema + seed ``` --- ### Conexión desde Node.js #### 🔌 Pool de Conexiones **Configuración en `server/utils/db.ts`**: ```typescript const config = { user: process.env.POSTGRES_USER || 'seguidor', password: process.env.POSTGRES_PASSWORD || 'seguidor_password', database: process.env.POSTGRES_DB || 'seguidor_lotes', host: process.env.POSTGRES_HOST || 'postgres', port: parseInt(process.env.POSTGRES_PORT || '5432'), max: 20, // Máximo de conexiones en el pool idleTimeoutMillis: 30000, // 30s timeout para conexiones idle connectionTimeoutMillis: 10000, // 10s timeout para establecer conexión } ``` **Peculiaridades**: 1. **Timeout de 10 segundos**: Necesario porque PostgreSQL puede tardar en aplicar configuración de autenticación 2. **Retry Logic Automático**: ```typescript // Verifica conexión inicial con 5 reintentos let retries = 5 const tryConnect = async () => { try { const client = await pool!.connect() console.log('✅ Conexión inicial a PostgreSQL exitosa') client.release() } catch (err: any) { retries-- if (retries > 0) { console.log(`⚠️ Conexión falló, reintentando... (${retries} intentos)`) setTimeout(tryConnect, 2000) // Reintentar en 2s } } } ``` 3. **Logs solo en desarrollo**: ```typescript if (process.env.NODE_ENV !== 'production') { pool.on('connect', () => { console.log('Nueva conexión establecida con PostgreSQL') }) } ``` #### ⚡ Race Condition: App vs PostgreSQL **Problema**: Aunque docker-compose tiene `depends_on` con `service_healthy`, el healthcheck solo verifica que PostgreSQL **responde**, no que terminó de ejecutar los scripts de inicialización. **Timeline Real**: ``` T+0s: PostgreSQL inicia T+2s: PostgreSQL responde a pg_isready → healthcheck ✅ T+2s: App Nuxt inicia e intenta conectarse T+3s: PostgreSQL ejecuta 00_configure_auth.sh (cambia auth) T+4s: Primera conexión de app FALLA (todavía usa scram-sha-256) T+5s: Script termina, auth está configurado T+7s: Retry logic de app conecta exitosamente ✅ ``` **Solución**: El retry logic compensa esta race condition automáticamente. --- ### Endpoints de Debug #### ⚠️ Endpoints Temporales Estos endpoints están marcados como **TEMPORALES** y **NO DEBEN ELIMINARSE** sin consultar a Dario/Draganel/nucleo000. #### `POST /api/debug/reset-database` **Función**: Elimina completamente todas las tablas de la base de datos. **Implementación**: ```typescript await client.query('DROP TABLE IF EXISTS operacion_lotes CASCADE') await client.query('DROP TABLE IF EXISTS operaciones CASCADE') await client.query('DROP TABLE IF EXISTS lotes CASCADE') await client.query('DROP FUNCTION IF EXISTS get_trazabilidad CASCADE') await client.query('DROP VIEW IF EXISTS vista_lotes_con_origen CASCADE') ``` **Notas**: - Usa `DROP TABLE` (no `TRUNCATE`) para que el workflow detecte la ausencia de tablas - Después de usar, el próximo deploy reinicializará automáticamente - Requiere confirmación del usuario en el frontend #### `POST /api/debug/seed-database` **Función**: Recrea schema y carga datos de ejemplo. **Implementación**: ```typescript // 1. Ejecutar schema (crea tablas si no existen) const schemaSQL = await readFile('server/database/01_schema.sql', 'utf-8') await client.query(schemaSQL) // 2. Ejecutar seed (truncate + insert) const seedSQL = await readFile('server/database/02_seed.sql', 'utf-8') await client.query(seedSQL) ``` **Peculiaridad**: Los archivos SQL deben estar en el contenedor de la app **Dockerfile**: ```dockerfile # Copy SQL files for seed endpoint COPY --from=builder /app/server/database /app/server/database ``` Sin esta línea, el endpoint devuelve `ENOENT: no such file or directory`. --- ## Ejecución de Scripts ### Automática (Docker Compose) ```yaml # docker-compose.yml volumes: - ./nuxt4/server/database:/docker-entrypoint-initdb.d:ro ``` PostgreSQL ejecuta todos los archivos `.sql` y `.sh` en orden alfabético dentro de `/docker-entrypoint-initdb.d/` **solo la primera vez**. ### Manual - Desde el contenedor Docker ```bash # Conectarse al contenedor docker exec -it lotes-postgres psql -U seguidor -d seguidor_lotes # Dentro de psql, ejecutar: \i /docker-entrypoint-initdb.d/01_schema.sql \i /docker-entrypoint-initdb.d/02_seed.sql ``` ### Manual - Desde tu máquina local ```bash # Asegúrate de tener psql instalado psql -h localhost -U seguidor -d seguidor_lotes -f 01_schema.sql psql -h localhost -U seguidor -d seguidor_lotes -f 02_seed.sql ``` ### Manual - Vía API (endpoints de debug) ```bash # Resetear BD curl -X POST https://lotes.nucleoriofrio.com/api/debug/reset-database # Cargar datos de ejemplo curl -X POST https://lotes.nucleoriofrio.com/api/debug/seed-database ``` --- ## Troubleshooting ### ❌ Error: "password authentication failed for user seguidor" **Causas posibles**: 1. **Primera carga del sistema** (normal) - PostgreSQL todavía está configurando autenticación - Solución: Esperar ~5 segundos, el retry logic se encarga - Logs: `⚠️ Conexión falló, reintentando...` seguido de `✅ Conexión exitosa` 2. **Volumen de postgres de un deploy anterior con scram-sha-256** - Solución: Eliminar volumen y redeploy ```bash docker-compose --project-name lotes down -v git commit --allow-empty -m "Reinit DB" && git push ``` 3. **Variables de entorno incorrectas** - Verificar en Gitea > Settings > Actions > Secrets/Variables - Variables requeridas: `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` ### ❌ Error: "relation 'lotes' does not exist" **Causas posibles**: 1. **Base de datos no inicializada** - Verificar logs del workflow: ¿se ejecutó la inicialización? - Solución: Click en "🌱 CARGAR DATOS DE EJEMPLO" 2. **Múltiples contenedores PostgreSQL** - App conectándose al contenedor equivocado - Solución: Limpiar contenedores huérfanos ```bash docker ps -a | grep postgres docker rm -f [contenedores antiguos] ``` 3. **Usaste TRUNCATE en lugar de DROP** - Si usaste el botón de borrar BD pero las tablas quedaron vacías - Solución: Click en "🌱 CARGAR DATOS DE EJEMPLO" (ahora ejecuta schema primero) ### ❌ Error: "ENOENT: no such file or directory, open '/app/server/database/02_seed.sql'" **Causa**: Archivos SQL no copiados al contenedor de la app **Solución**: Verificar que Dockerfile incluya: ```dockerfile COPY --from=builder /app/server/database /app/server/database ``` Luego rebuild: ```bash git commit -am "Fix Dockerfile" && git push ``` ### 📊 Logs: Múltiples "Nueva conexión establecida con PostgreSQL" **Comportamiento**: Es **normal y correcto** en desarrollo - El pool crea múltiples conexiones (configurado para max 20) - Cada endpoint que hace query puede obtener una conexión del pool **Si quieres reducir logs**: Ya está configurado para no mostrar en producción ```typescript if (process.env.NODE_ENV !== 'production') { pool.on('connect', () => console.log('Nueva conexión...')) } ``` --- ## Queries Útiles ### Verificar que todo está correcto ```sql -- 1. Listar tablas \dt -- 2. Contar registros SELECT (SELECT COUNT(*) FROM lotes) as total_lotes, (SELECT COUNT(*) FROM operaciones) as total_operaciones, (SELECT COUNT(*) FROM operacion_lotes) as total_relaciones; -- 3. Ver lotes creados SELECT codigo, tipo, cantidad_kg FROM lotes ORDER BY fecha_creado; -- 4. Probar función de trazabilidad SELECT * FROM get_trazabilidad( (SELECT id FROM lotes WHERE codigo = 'SEC-001') ); ``` ### Ver todas las operaciones de un lote ```sql SELECT o.tipo AS operacion, o.fecha, ol.rol, ol.cantidad_kg FROM operacion_lotes ol JOIN operaciones o ON o.id = ol.operacion_id WHERE ol.lote_id = (SELECT id FROM lotes WHERE codigo = 'SEC-001') ORDER BY o.fecha; ``` ### Ver inputs directos de un lote ```sql SELECT l.codigo, l.tipo, l.cantidad_kg, o.tipo AS operacion_tipo FROM lotes l JOIN operacion_lotes ol_in ON ol_in.lote_id = l.id JOIN operacion_lotes ol_out ON ol_out.operacion_id = ol_in.operacion_id JOIN operaciones o ON o.id = ol_out.operacion_id WHERE ol_out.lote_id = (SELECT id FROM lotes WHERE codigo = 'SEC-001') AND ol_out.rol = 'output' AND ol_in.rol = 'input'; ``` ### Estadísticas de operaciones ```sql SELECT tipo, COUNT(*) as total, SUM(cantidad_kg) as kg_totales FROM operaciones WHERE fecha >= NOW() - INTERVAL '30 days' GROUP BY tipo ORDER BY total DESC; ``` --- ## Arquitectura de Inicialización ``` ┌─────────────────────────────────────────────────────────────────┐ │ INICIALIZACIÓN COMPLETA (solo primera vez) │ └─────────────────────────────────────────────────────────────────┘ 1. docker-compose up ↓ 2. PostgreSQL container inicia ↓ 3. Volumen postgres_data vacío? → SÍ ↓ 4. Ejecuta /docker-entrypoint-initdb.d/ en orden: │ ├─ 00_configure_auth.sh → Cambia pg_hba.conf a md5 ├─ 01_schema.sql → Crea tablas, funciones, vistas └─ 02_seed.sql → Inserta 10 lotes, 7 ops, 16 rels ↓ 5. PostgreSQL termina inicio → healthcheck ✅ ↓ 6. App container inicia ↓ 7. App intenta conectar (puede fallar 1-2 veces) ↓ 8. Retry logic espera y reconecta ↓ 9. ✅ Sistema funcionando ┌─────────────────────────────────────────────────────────────────┐ │ REDESPLIEGUE (datos persisten) │ └─────────────────────────────────────────────────────────────────┘ 1. git push → Workflow inicia ↓ 2. Build nueva imagen Docker ↓ 3. docker-compose down (sin -v) → PostgreSQL se detiene ↓ 4. docker-compose up → PostgreSQL reinicia ↓ 5. Volumen postgres_data existe? → SÍ ↓ 6. PostgreSQL lee datos existentes ↓ 7. NO ejecuta scripts de /docker-entrypoint-initdb.d/ ↓ 8. App container inicia con nueva imagen ↓ 9. ✅ Sistema funcionando (con datos previos) ``` --- ## Backup y Restore ### Hacer backup ```bash # Backup completo docker exec lotes-postgres pg_dump -U seguidor seguidor_lotes > backup_$(date +%Y%m%d).sql # Backup solo schema (sin datos) docker exec lotes-postgres pg_dump -U seguidor -s seguidor_lotes > schema_$(date +%Y%m%d).sql # Backup solo datos docker exec lotes-postgres pg_dump -U seguidor -a seguidor_lotes > data_$(date +%Y%m%d).sql ``` ### Restaurar backup ```bash # Restaurar desde archivo cat backup_20251121.sql | docker exec -i lotes-postgres psql -U seguidor -d seguidor_lotes # Restaurar directo desde servidor docker exec -i lotes-postgres psql -U seguidor -d seguidor_lotes < backup.sql ``` --- ## Resumen de Decisiones Técnicas | Decisión | Razón | Alternativa Descartada | |----------|-------|------------------------| | md5 en lugar de scram-sha-256 | Compatibilidad con driver pg de Node.js | Actualizar driver (requiere cambios mayores) | | Retry logic en pool | Compensa race condition de inicialización | depends_on custom healthcheck (complejo) | | Timeout de 10s | PostgreSQL puede tardar en configurar auth | 2s (insuficiente) | | DROP TABLE en reset | Workflow detecta ausencia y reinicializa | TRUNCATE (deja tablas vacías) | | seed ejecuta schema primero | Funciona después de borrar BD | Solo seed (falla si no hay tablas) | | Scripts SQL en app container | Permite endpoint de seed | Solo en postgres (no accesible desde app) | | Volumen nombrado | Persistencia entre redeploys | Volumen bind mount (menos portable) | --- ## Referencias - **PostgreSQL Authentication**: https://www.postgresql.org/docs/16/auth-pg-hba-conf.html - **node-postgres (pg)**: https://node-postgres.com/ - **Docker Init Scripts**: https://hub.docker.com/_/postgres (sección "Initialization scripts") - **Pool de Conexiones**: https://node-postgres.com/apis/pool - **Recursive Queries (CTE)**: https://www.postgresql.org/docs/current/queries-with.html --- **Última actualización**: 2025-11-21 **Autor**: Claude Code (claudeCode0@nucleoriofrio.com) **Proyecto**: Seguidor de Lotes - Sistema de Trazabilidad de Café