# 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)
  - [Conexión de la app](#conexión-de-la-app)
  - [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

### `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.

### Conexión de la app

- Se usa una **cadena de conexión única** (`NUXT_POSTGRES_URL`) consumida desde `runtimeConfig.postgresUrl`.
- Formato recomendado: `postgres://seguidor:seguidor_password@postgres:5432/seguidor_lotes`.
- El driver `pg` soporta `scram-sha-256` (default de Postgres 16), no se toca `pg_hba.conf`.

---

### 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. **`01_schema.sql`** - Crea estructura (tablas, funciones, vistas)
2. **`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 = useRuntimeConfig()
const pool = new Pool({
  connectionString: config.postgresUrl, // Ej: postgres://user:pass@host:5432/db
  max: 10,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 10000,
})
```

**Peculiaridades**:

1. **Cadena única**: evita discrepancias entre host/puerto/usuario/contraseña.
2. **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

Aunque docker-compose tiene `depends_on` con `service_healthy`, el healthcheck solo verifica que PostgreSQL responde. Si necesitas máxima robustez puedes añadir un pequeño retry en la app o un `pg_isready` antes de exponer el servicio, pero con la cadena de conexión única y el pool el arranque es estable.

---

### 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á terminando de iniciar
   - Solución: Esperar unos segundos y reintentar

2. **Volumen con contraseña diferente a la actual**
   - Solución: Eliminar volumen y redeploy para reinicializar
   ```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: `NUXT_POSTGRES_URL`, `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:
   │
   ├─ 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 |
|----------|-------|------------------------|
| Cadena de conexión única (`NUXT_POSTGRES_URL`) | Evita discrepancias entre host/puerto/usuario/contraseña | Variables sueltas (`POSTGRES_*`) en el runtime de la app |
| No tocar `pg_hba.conf` (usa scram por defecto) | Menos acoplamiento a la imagen y a volúmenes existentes | Forzar md5 en init scripts |
| Timeout de 10s en el pool | PostgreSQL puede tardar unos segundos en aceptar conexiones | Timeout bajo (más falsos negativos) |
| 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é
