Documentar peculiaridades de implementación de PostgreSQL
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m4s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m4s
- Agregar sección completa sobre autenticación md5 vs scram-sha-256 - Documentar race condition entre app y PostgreSQL con retry logic - Explicar idempotencia de scripts (schema vs seed) - Documentar persistencia de datos y volúmenes Docker - Incluir troubleshooting detallado de errores comunes - Agregar diagramas de arquitectura de inicialización - Documentar endpoints de debug temporales - Tabla de decisiones técnicas con alternativas descartadas - Queries útiles y comandos de backup/restore
This commit is contained in:
@@ -1,62 +1,351 @@
|
|||||||
# Database - Scripts SQL
|
# Database - Documentación Completa
|
||||||
|
|
||||||
Este directorio contiene los scripts SQL para inicializar y gestionar la base de datos PostgreSQL del sistema de trazabilidad.
|
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
|
## 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`
|
### `01_schema.sql`
|
||||||
Crea el esquema completo de la base de datos:
|
Crea el esquema completo de la base de datos:
|
||||||
- Tablas: `lotes`, `operaciones`, `operacion_lotes`
|
- Tablas: `lotes`, `operaciones`, `operacion_lotes`
|
||||||
- Índices para optimización
|
- Índices para optimización
|
||||||
- Función `get_trazabilidad()` para queries recursivas
|
- Función recursiva `get_trazabilidad()` para queries de trazabilidad completa
|
||||||
- Vista `vista_lotes_con_origen`
|
- Vista `vista_lotes_con_origen`
|
||||||
- Constraints y validaciones
|
- Constraints y validaciones
|
||||||
|
- **Idempotente**: Usa `CREATE TABLE IF NOT EXISTS`
|
||||||
|
|
||||||
### `02_seed.sql`
|
### `02_seed.sql`
|
||||||
Datos de ejemplo que representan un flujo completo:
|
Datos de ejemplo que representan un flujo completo de trazabilidad:
|
||||||
- Ingreso de uva (2086 kg)
|
- Ingreso de uva (2086 kg)
|
||||||
- Despulpado → primera, segunda, rechazos
|
- Despulpado → primera, segunda, rechazos
|
||||||
- Oreado (con error de registro)
|
- Oreado (con error de registro intencional)
|
||||||
- Ajuste de merma (1500 → 1480 kg)
|
- Ajuste de merma (1500 → 1480 kg)
|
||||||
- Ajuste de tipo (oreado → presecado)
|
- Ajuste de tipo (oreado → presecado)
|
||||||
- Reposo
|
- Reposo
|
||||||
- Secado final (mezcla de 2 lotes = 2000 kg)
|
- Secado final (mezcla de 2 lotes = 2000 kg)
|
||||||
|
- **Total**: 10 lotes, 7 operaciones, 16 relaciones
|
||||||
|
- **Idempotente con TRUNCATE**: Limpia datos antes de insertar
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Ejecución Automática
|
## ⚠️ Peculiaridades de Implementación
|
||||||
|
|
||||||
Cuando usas **Docker Compose**, estos scripts se ejecutan automáticamente al iniciar PostgreSQL por primera vez gracias al montaje:
|
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
|
```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:
|
volumes:
|
||||||
- ./nuxt4/server/database:/docker-entrypoint-initdb.d:ro
|
- ./nuxt4/server/database:/docker-entrypoint-initdb.d:ro
|
||||||
```
|
```
|
||||||
|
|
||||||
PostgreSQL ejecuta todos los archivos `.sql` en orden alfabético dentro de `/docker-entrypoint-initdb.d/`.
|
PostgreSQL ejecuta todos los archivos `.sql` y `.sh` en orden alfabético dentro de `/docker-entrypoint-initdb.d/` **solo la primera vez**.
|
||||||
|
|
||||||
**Orden de ejecución:**
|
### Manual - Desde el contenedor Docker
|
||||||
1. `01_schema.sql` - Crea estructura
|
|
||||||
2. `02_seed.sql` - Inserta datos de ejemplo
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Ejecución Manual
|
|
||||||
|
|
||||||
### Opción 1: Desde el contenedor Docker
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Conectarse al contenedor
|
# Conectarse al contenedor
|
||||||
docker exec -it seguidorDeLotes-postgres psql -U seguidor -d seguidor_lotes
|
docker exec -it lotes-postgres psql -U seguidor -d seguidor_lotes
|
||||||
|
|
||||||
# Dentro de psql, ejecutar:
|
# Dentro de psql, ejecutar:
|
||||||
\i /docker-entrypoint-initdb.d/01_schema.sql
|
\i /docker-entrypoint-initdb.d/01_schema.sql
|
||||||
\i /docker-entrypoint-initdb.d/02_seed.sql
|
\i /docker-entrypoint-initdb.d/02_seed.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
### Opción 2: Desde tu máquina local
|
### Manual - Desde tu máquina local
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Asegúrate de tener psql instalado
|
# Asegúrate de tener psql instalado
|
||||||
@@ -64,147 +353,113 @@ psql -h localhost -U seguidor -d seguidor_lotes -f 01_schema.sql
|
|||||||
psql -h localhost -U seguidor -d seguidor_lotes -f 02_seed.sql
|
psql -h localhost -U seguidor -d seguidor_lotes -f 02_seed.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
### Opción 3: Copiar y pegar en pgAdmin o DBeaver
|
### Manual - Vía API (endpoints de debug)
|
||||||
|
|
||||||
Abre los archivos `.sql` en tu cliente SQL favorito y ejecútalos directamente.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Reiniciar la Base de Datos
|
|
||||||
|
|
||||||
Si necesitas empezar desde cero:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Detener contenedores y eliminar volúmenes
|
# Resetear BD
|
||||||
docker-compose down -v
|
curl -X POST https://lotes.nucleoriofrio.com/api/debug/reset-database
|
||||||
|
|
||||||
# Volver a iniciar (ejecutará scripts automáticamente)
|
# Cargar datos de ejemplo
|
||||||
docker-compose up -d
|
curl -X POST https://lotes.nucleoriofrio.com/api/debug/seed-database
|
||||||
|
|
||||||
# Ver logs para confirmar
|
|
||||||
docker logs -f seguidorDeLotes-postgres
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Verificar que todo está correcto
|
## Troubleshooting
|
||||||
|
|
||||||
### 1. Conectarse a la base de datos
|
### ❌ 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
|
```bash
|
||||||
docker exec -it seguidorDeLotes-postgres psql -U seguidor -d seguidor_lotes
|
git commit -am "Fix Dockerfile" && git push
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Listar tablas
|
### 📊 Logs: Múltiples "Nueva conexión establecida con PostgreSQL"
|
||||||
|
|
||||||
```sql
|
**Comportamiento**: Es **normal y correcto** en desarrollo
|
||||||
\dt
|
|
||||||
```
|
|
||||||
|
|
||||||
**Deberías ver:**
|
- El pool crea múltiples conexiones (configurado para max 20)
|
||||||
```
|
- Cada endpoint que hace query puede obtener una conexión del pool
|
||||||
List of relations
|
|
||||||
Schema | Name | Type | Owner
|
|
||||||
--------+------------------+-------+----------
|
|
||||||
public | lotes | table | seguidor
|
|
||||||
public | operacion_lotes | table | seguidor
|
|
||||||
public | operaciones | table | seguidor
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Contar registros
|
**Si quieres reducir logs**: Ya está configurado para no mostrar en producción
|
||||||
|
```typescript
|
||||||
```sql
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
SELECT
|
pool.on('connect', () => console.log('Nueva conexión...'))
|
||||||
(SELECT COUNT(*) FROM lotes) as total_lotes,
|
}
|
||||||
(SELECT COUNT(*) FROM operaciones) as total_operaciones,
|
|
||||||
(SELECT COUNT(*) FROM operacion_lotes) as total_relaciones;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Deberías ver algo como:**
|
|
||||||
```
|
|
||||||
total_lotes | total_operaciones | total_relaciones
|
|
||||||
-------------+-------------------+------------------
|
|
||||||
9 | 8 | 16
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Ver lotes creados
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT codigo, tipo, cantidad_kg FROM lotes ORDER BY fecha_creado;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Deberías ver:**
|
|
||||||
```
|
|
||||||
codigo | tipo | cantidad_kg
|
|
||||||
-----------+----------------------+-------------
|
|
||||||
UVA-001 | uva | 2086.00
|
|
||||||
PRIM-001 | despulpado_primera | 1500.00
|
|
||||||
SEG-001 | despulpado_segunda | 400.00
|
|
||||||
RECH-001 | despulpado_rechazos | 150.00
|
|
||||||
ORE-001 | oreado | 1500.00
|
|
||||||
ORE-001A | oreado | 1480.00
|
|
||||||
PRE-001 | presecado | 1480.00
|
|
||||||
REP-001 | reposo | 1480.00
|
|
||||||
REP-002 | reposo | 520.00
|
|
||||||
SEC-001 | secado | 2000.00
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Probar la función de trazabilidad
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT * FROM get_trazabilidad(
|
|
||||||
(SELECT id FROM lotes WHERE codigo = 'SEC-001')
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Deberías ver:** Todo el historial del lote `SEC-001` desde el ingreso de uva.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Estructura de las Tablas
|
|
||||||
|
|
||||||
### Tabla `lotes`
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE lotes (
|
|
||||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
||||||
codigo TEXT UNIQUE,
|
|
||||||
tipo TEXT NOT NULL,
|
|
||||||
fecha_creado TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
lugar_id INTEGER,
|
|
||||||
cantidad_kg NUMERIC(10,2),
|
|
||||||
meta JSONB
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tabla `operaciones`
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE operaciones (
|
|
||||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
||||||
tipo TEXT NOT NULL,
|
|
||||||
fecha TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
lugar_id INTEGER,
|
|
||||||
meta JSONB
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tabla `operacion_lotes`
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE operacion_lotes (
|
|
||||||
operacion_id UUID NOT NULL REFERENCES operaciones(id) ON DELETE CASCADE,
|
|
||||||
lote_id UUID NOT NULL REFERENCES lotes(id) ON DELETE CASCADE,
|
|
||||||
rol TEXT NOT NULL CHECK (rol IN ('input', 'output')),
|
|
||||||
cantidad_kg NUMERIC(10,2),
|
|
||||||
PRIMARY KEY (operacion_id, lote_id, rol)
|
|
||||||
);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Queries Útiles
|
## 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
|
### Ver todas las operaciones de un lote
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
@@ -219,10 +474,9 @@ WHERE ol.lote_id = (SELECT id FROM lotes WHERE codigo = 'SEC-001')
|
|||||||
ORDER BY o.fecha;
|
ORDER BY o.fecha;
|
||||||
```
|
```
|
||||||
|
|
||||||
### Ver lotes que se usaron para crear un lote específico (inputs directos)
|
### Ver inputs directos de un lote
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- Inputs directos del lote SEC-001
|
|
||||||
SELECT
|
SELECT
|
||||||
l.codigo,
|
l.codigo,
|
||||||
l.tipo,
|
l.tipo,
|
||||||
@@ -237,7 +491,7 @@ WHERE ol_out.lote_id = (SELECT id FROM lotes WHERE codigo = 'SEC-001')
|
|||||||
AND ol_in.rol = 'input';
|
AND ol_in.rol = 'input';
|
||||||
```
|
```
|
||||||
|
|
||||||
### Ver estadísticas de un período
|
### Estadísticas de operaciones
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SELECT
|
SELECT
|
||||||
@@ -252,29 +506,57 @@ ORDER BY total DESC;
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Migraciones Futuras
|
## Arquitectura de Inicialización
|
||||||
|
|
||||||
Cuando necesites hacer cambios al esquema en producción:
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ INICIALIZACIÓN COMPLETA (solo primera vez) │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
1. **Crear archivo de migración** (ej: `03_add_lugares_table.sql`)
|
1. docker-compose up
|
||||||
2. **NO modificar** `01_schema.sql` ni `02_seed.sql` directamente
|
↓
|
||||||
3. **Aplicar migración manualmente** en producción
|
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
|
||||||
|
|
||||||
Ejemplo de migración:
|
|
||||||
|
|
||||||
```sql
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
-- 03_add_lugares_table.sql
|
│ REDESPLIEGUE (datos persisten) │
|
||||||
CREATE TABLE IF NOT EXISTS lugares (
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
nombre TEXT NOT NULL,
|
|
||||||
tipo TEXT, -- patio, pila, bodega, etc.
|
|
||||||
capacidad_kg NUMERIC
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Agregar foreign key a lotes
|
1. git push → Workflow inicia
|
||||||
ALTER TABLE lotes
|
↓
|
||||||
ADD CONSTRAINT fk_lotes_lugar
|
2. Build nueva imagen Docker
|
||||||
FOREIGN KEY (lugar_id) REFERENCES lugares(id);
|
↓
|
||||||
|
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)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -284,55 +566,52 @@ ALTER TABLE lotes
|
|||||||
### Hacer backup
|
### Hacer backup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker exec seguidorDeLotes-postgres pg_dump -U seguidor seguidor_lotes > backup_$(date +%Y%m%d).sql
|
# 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
|
### Restaurar backup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cat backup_20251121.sql | docker exec -i seguidorDeLotes-postgres psql -U seguidor -d seguidor_lotes
|
# 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
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Troubleshooting
|
## Resumen de Decisiones Técnicas
|
||||||
|
|
||||||
### "relation lotes does not exist"
|
| Decisión | Razón | Alternativa Descartada |
|
||||||
|
|----------|-------|------------------------|
|
||||||
Los scripts no se ejecutaron. Verificar:
|
| md5 en lugar de scram-sha-256 | Compatibilidad con driver pg de Node.js | Actualizar driver (requiere cambios mayores) |
|
||||||
```bash
|
| Retry logic en pool | Compensa race condition de inicialización | depends_on custom healthcheck (complejo) |
|
||||||
docker logs seguidorDeLotes-postgres
|
| 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) |
|
||||||
Si ves errores, eliminar volumen y reiniciar:
|
| Scripts SQL en app container | Permite endpoint de seed | Solo en postgres (no accesible desde app) |
|
||||||
```bash
|
| Volumen nombrado | Persistencia entre redeploys | Volumen bind mount (menos portable) |
|
||||||
docker-compose down -v
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### "permission denied for schema public"
|
|
||||||
|
|
||||||
Problema de permisos. Conectarse como superuser:
|
|
||||||
```bash
|
|
||||||
docker exec -it seguidorDeLotes-postgres psql -U postgres -d seguidor_lotes
|
|
||||||
|
|
||||||
-- Dar permisos
|
|
||||||
GRANT ALL ON SCHEMA public TO seguidor;
|
|
||||||
GRANT ALL ON ALL TABLES IN SCHEMA public TO seguidor;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Los datos de ejemplo se duplican
|
|
||||||
|
|
||||||
`02_seed.sql` hace `TRUNCATE` al inicio. Si no quieres perder datos, comenta esa línea.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Referencias
|
## Referencias
|
||||||
|
|
||||||
- [PostgreSQL JSON Functions](https://www.postgresql.org/docs/current/functions-json.html)
|
- **PostgreSQL Authentication**: https://www.postgresql.org/docs/16/auth-pg-hba-conf.html
|
||||||
- [Recursive Queries (CTE)](https://www.postgresql.org/docs/current/queries-with.html)
|
- **node-postgres (pg)**: https://node-postgres.com/
|
||||||
- [Docker Init Scripts](https://hub.docker.com/_/postgres)
|
- **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
|
**Última actualización**: 2025-11-21
|
||||||
|
**Autor**: Claude Code (claudeCode0@nucleoriofrio.com)
|
||||||
|
**Proyecto**: Seguidor de Lotes - Sistema de Trazabilidad de Café
|
||||||
|
|||||||
Reference in New Issue
Block a user