Inicializar rioCata - Sistema de Catación de Café
- Base de datos PostgreSQL 16 con extensiones JSONB y arrays - Docker Compose para containerización - Scripts SQL de inicialización (schema, funciones, índices, datos de prueba) - Suite de tests de validación (18 tests) - Queries de ejemplo (17 queries) - Script helper para gestión (scripts/riocata.sh) - Documentación completa en README.md Estructura: - 4 tablas principales: sesion, auth.users, sesion_participante, muestra, evaluacion - Tipo ENUM para defectos - 2 triggers automáticos (updated_at, puntaje_final) - 19 índices de optimización (GIN, B-tree, funcionales) - Constraints de validación para arrays y JSONB - 2 funciones auxiliares para análisis
This commit is contained in:
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Backups
|
||||||
|
*.sql
|
||||||
|
!postgres/init/*.sql
|
||||||
|
!postgres/tests/*.sql
|
||||||
|
|
||||||
|
# Docker volumes
|
||||||
|
postgres_data/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
347
README.md
Normal file
347
README.md
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
# rioCata - Sistema de Catación de Café
|
||||||
|
|
||||||
|
Sistema digital para realizar sesiones de catación de café de manera ordenada y profesional.
|
||||||
|
|
||||||
|
## Arquitectura
|
||||||
|
|
||||||
|
- **Base de datos**: PostgreSQL 16 con extensiones para JSONB y arrays
|
||||||
|
- **Containerización**: Docker Compose
|
||||||
|
- **Estructura de datos**: Modelo relacional optimizado para catación profesional
|
||||||
|
|
||||||
|
## Estructura del Proyecto
|
||||||
|
|
||||||
|
```
|
||||||
|
rioCata/
|
||||||
|
├── docker-compose.yml # Configuración de servicios
|
||||||
|
├── scripts/ # Scripts de gestión y administración
|
||||||
|
│ ├── riocata.sh # Script helper principal
|
||||||
|
│ └── README.md # Documentación de scripts
|
||||||
|
├── postgres/
|
||||||
|
│ ├── init/ # Scripts de inicialización (ejecutados automáticamente)
|
||||||
|
│ │ ├── 01_schema.sql # Tipos y tablas
|
||||||
|
│ │ ├── 02_functions.sql # Funciones y triggers
|
||||||
|
│ │ ├── 03_indexes.sql # Índices de optimización
|
||||||
|
│ │ └── 04_sample_data.sql # Datos de prueba
|
||||||
|
│ └── tests/ # Scripts de testing
|
||||||
|
│ ├── test_all.sql # Suite completa de tests
|
||||||
|
│ └── example_queries.sql # Queries de ejemplo
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Inicio Rápido
|
||||||
|
|
||||||
|
### 1. Levantar el entorno
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Esto iniciará PostgreSQL con todos los scripts de inicialización ejecutados automáticamente.
|
||||||
|
|
||||||
|
### 2. Verificar que el servicio está corriendo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
Deberías ver `riocata_postgres` en estado `Up`.
|
||||||
|
|
||||||
|
### 3. Conectarse a la base de datos
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec postgres psql -U riocata_user -d riocata
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Ejecutar los tests
|
||||||
|
|
||||||
|
Una vez dentro de psql:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
\i postgres/tests/test_all.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
Esto ejecutará 18 tests que validan:
|
||||||
|
- Existencia de tablas y tipos
|
||||||
|
- Funcionamiento de triggers
|
||||||
|
- Constraints de validación
|
||||||
|
- Queries típicas
|
||||||
|
- Funciones auxiliares
|
||||||
|
|
||||||
|
### 5. Explorar los datos de ejemplo
|
||||||
|
|
||||||
|
```sql
|
||||||
|
\i postgres/tests/example_queries.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
Esto ejecutará 17 queries de ejemplo que muestran:
|
||||||
|
- Listado de sesiones, muestras y evaluaciones
|
||||||
|
- Top evaluaciones por puntaje
|
||||||
|
- Evaluaciones con defectos
|
||||||
|
- Promedios de parámetros por muestra
|
||||||
|
- Análisis de notas de fragancia y sabor
|
||||||
|
- Estadísticas por catador
|
||||||
|
|
||||||
|
## Scripts Helper
|
||||||
|
|
||||||
|
Para facilitar la gestión del proyecto, hay scripts helper disponibles en la carpeta `scripts/`.
|
||||||
|
|
||||||
|
### Script Principal: riocata.sh
|
||||||
|
|
||||||
|
El script `scripts/riocata.sh` proporciona comandos útiles para gestión:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ver ayuda
|
||||||
|
./scripts/riocata.sh help
|
||||||
|
|
||||||
|
# Iniciar servicios
|
||||||
|
./scripts/riocata.sh start
|
||||||
|
|
||||||
|
# Conectarse a PostgreSQL
|
||||||
|
./scripts/riocata.sh psql
|
||||||
|
|
||||||
|
# Ejecutar tests
|
||||||
|
./scripts/riocata.sh test
|
||||||
|
|
||||||
|
# Ejecutar queries de ejemplo
|
||||||
|
./scripts/riocata.sh queries
|
||||||
|
|
||||||
|
# Ver estado
|
||||||
|
./scripts/riocata.sh status
|
||||||
|
|
||||||
|
# Crear backup
|
||||||
|
./scripts/riocata.sh backup
|
||||||
|
|
||||||
|
# Restaurar backup
|
||||||
|
./scripts/riocata.sh restore backup_20251017_143022.sql
|
||||||
|
|
||||||
|
# Detener servicios
|
||||||
|
./scripts/riocata.sh stop
|
||||||
|
|
||||||
|
# Reiniciar todo (borra datos - ¡CUIDADO!)
|
||||||
|
./scripts/riocata.sh reset
|
||||||
|
```
|
||||||
|
|
||||||
|
**Documentación completa**: Ver `scripts/README.md` para información detallada sobre todos los comandos disponibles.
|
||||||
|
|
||||||
|
## Modelo de Datos
|
||||||
|
|
||||||
|
### Tablas Principales
|
||||||
|
|
||||||
|
#### `sesion`
|
||||||
|
Representa una sesión de catación.
|
||||||
|
- `id`: UUID (PK)
|
||||||
|
- `codigo`: Código único (ej. "S-2025-10-17-01")
|
||||||
|
- `fecha`: Fecha de la catación
|
||||||
|
- `nombre`: Nombre descriptivo (ej. "Mesa Laboratorio #1")
|
||||||
|
|
||||||
|
#### `auth.users`
|
||||||
|
Usuarios/catadores del sistema (simula Supabase auth.users).
|
||||||
|
- `id`: UUID (PK)
|
||||||
|
- `email`: Email único
|
||||||
|
- `nombre`: Nombre del catador
|
||||||
|
|
||||||
|
#### `sesion_participante`
|
||||||
|
Relación de catadores participantes en una sesión.
|
||||||
|
- `id`: UUID (PK)
|
||||||
|
- `sesion_id`: FK a sesion
|
||||||
|
- `catador_id`: FK a auth.users
|
||||||
|
- `rol`: Rol del participante (catador, juez, etc.)
|
||||||
|
|
||||||
|
#### `muestra`
|
||||||
|
Muestras de café evaluadas en una sesión.
|
||||||
|
- `id`: UUID (PK)
|
||||||
|
- `sesion_id`: FK a sesion
|
||||||
|
- `codigo`: Código de la muestra (ej. "Muestra A-101")
|
||||||
|
- `posicion`: Orden físico en la mesa
|
||||||
|
|
||||||
|
#### `evaluacion`
|
||||||
|
Evaluación completa de una muestra por un catador.
|
||||||
|
|
||||||
|
**Campos de intensidades (JSONB)**:
|
||||||
|
- `intensidades`: JSON con 8 parámetros (descriptiva 1-15, afectiva 1-10):
|
||||||
|
- fragancia, aroma, sabor, saborResidual
|
||||||
|
- acidez, dulzor, sensacionBoca, impresionGlobal
|
||||||
|
|
||||||
|
**Campos de notas (JSONB arrays)**:
|
||||||
|
- `fragancia_aroma_notas`: Array de objetos {categoria, subcategoria, notaEspecifica}
|
||||||
|
- `sabor_notas`: Array de objetos {categoria, subcategoria, notaEspecifica}
|
||||||
|
|
||||||
|
**Campos de arrays**:
|
||||||
|
- `tazas_no_uniformes`: smallint[] (valores 1-5)
|
||||||
|
- `tazas_defectuosas`: smallint[] (valores 1-5)
|
||||||
|
- `sensacion_en_boca`: text[] (Áspero, Suave, Aceitoso, Metálico, Astringente)
|
||||||
|
- `gustos_predominantes`: text[] (1-2 elementos: Salado, Amargo, Ácido, Dulce, Umami)
|
||||||
|
|
||||||
|
**Otros campos**:
|
||||||
|
- `defecto`: ENUM (Mohoso, Fenólico, Papa)
|
||||||
|
- `otras_notas`: Texto libre
|
||||||
|
- `puntaje_final`: int (calculado automáticamente por trigger)
|
||||||
|
|
||||||
|
### Triggers Automáticos
|
||||||
|
|
||||||
|
1. **`trg_eval_updated_at`**: Actualiza `updated_at` en cada UPDATE
|
||||||
|
2. **`trg_eval_score_bi`**: Calcula `puntaje_final` como suma de valores afectivos
|
||||||
|
|
||||||
|
### Constraints de Validación
|
||||||
|
|
||||||
|
- Tazas no uniformes/defectuosas: solo valores 1-5
|
||||||
|
- Sensaciones en boca: solo valores permitidos
|
||||||
|
- Gustos predominantes: máximo 2 elementos
|
||||||
|
- Intensidades descriptivas: rango 1-15
|
||||||
|
- Intensidades afectivas: rango 1-10
|
||||||
|
- Una evaluación única por participante/muestra
|
||||||
|
|
||||||
|
## Funciones Auxiliares
|
||||||
|
|
||||||
|
### `get_promedio_parametro_afectivo(sesion_id, parametro)`
|
||||||
|
Obtiene el promedio de un parámetro afectivo para toda una sesión.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT get_promedio_parametro_afectivo(
|
||||||
|
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
|
||||||
|
'acidez'
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### `get_top_muestras(sesion_id, limit)`
|
||||||
|
Obtiene las mejores muestras de una sesión ordenadas por puntaje.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT * FROM get_top_muestras(
|
||||||
|
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
|
||||||
|
3
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Queries Típicas
|
||||||
|
|
||||||
|
### Promedio de dulzor por sesión
|
||||||
|
```sql
|
||||||
|
SELECT s.id, AVG(((e.intensidades->'dulzor'->>'afectiva')::int))
|
||||||
|
FROM sesion s
|
||||||
|
JOIN muestra m ON m.sesion_id = s.id
|
||||||
|
JOIN evaluacion e ON e.muestra_id = m.id
|
||||||
|
WHERE s.id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
|
||||||
|
GROUP BY s.id;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Buscar evaluaciones con defecto específico
|
||||||
|
```sql
|
||||||
|
SELECT e.*
|
||||||
|
FROM evaluacion e
|
||||||
|
WHERE e.defecto = 'Fenólico';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Buscar taza defectuosa específica (usando índice GIN)
|
||||||
|
```sql
|
||||||
|
SELECT e.*
|
||||||
|
FROM evaluacion e
|
||||||
|
WHERE e.tazas_defectuosas @> ARRAY[5]::smallint[];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Top 3 muestras por puntaje
|
||||||
|
```sql
|
||||||
|
SELECT m.codigo, e.puntaje_final, sp.catador_id
|
||||||
|
FROM muestra m
|
||||||
|
JOIN evaluacion e ON e.muestra_id = m.id
|
||||||
|
JOIN sesion_participante sp ON sp.id = e.sesion_participante_id
|
||||||
|
WHERE m.sesion_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
|
||||||
|
ORDER BY e.puntaje_final DESC
|
||||||
|
LIMIT 3;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filtrar por acidez alta (usa índice funcional)
|
||||||
|
```sql
|
||||||
|
SELECT *
|
||||||
|
FROM evaluacion
|
||||||
|
WHERE ((intensidades->'acidez'->>'afectiva')::int) >= 8;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Datos de Prueba
|
||||||
|
|
||||||
|
El sistema viene con datos de prueba precargados:
|
||||||
|
|
||||||
|
- **3 usuarios**: Dario, Juan Pérez, María González
|
||||||
|
- **1 sesión**: "Mesa Laboratorio #1 - Lotes Lavados"
|
||||||
|
- **3 muestras**: A-101, B-202, C-303
|
||||||
|
- **5 evaluaciones**: Con variedad de puntajes, defectos, y características
|
||||||
|
|
||||||
|
## Configuración
|
||||||
|
|
||||||
|
### Credenciales por defecto (docker-compose.yml)
|
||||||
|
- **Base de datos**: `riocata`
|
||||||
|
- **Usuario**: `riocata_user`
|
||||||
|
- **Contraseña**: `riocata_password`
|
||||||
|
- **Puerto**: `5432`
|
||||||
|
|
||||||
|
### Modificar credenciales
|
||||||
|
|
||||||
|
Edita `docker-compose.yml` y cambia las variables de entorno:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: tu_base_de_datos
|
||||||
|
POSTGRES_USER: tu_usuario
|
||||||
|
POSTGRES_PASSWORD: tu_contraseña
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comandos Útiles
|
||||||
|
|
||||||
|
### Ver logs de PostgreSQL
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reiniciar la base de datos (¡CUIDADO! Borra todos los datos)
|
||||||
|
```bash
|
||||||
|
docker-compose down -v
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backup de la base de datos
|
||||||
|
```bash
|
||||||
|
docker-compose exec postgres pg_dump -U riocata_user riocata > backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restaurar backup
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T postgres psql -U riocata_user -d riocata < backup.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ejecutar script SQL desde el host
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T postgres psql -U riocata_user -d riocata < mi_script.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## Optimizaciones
|
||||||
|
|
||||||
|
El esquema incluye múltiples índices para optimizar queries comunes:
|
||||||
|
|
||||||
|
- **Índices B-tree** en claves foráneas y puntaje_final
|
||||||
|
- **Índices GIN** en JSONB y arrays (para operadores @>, &&, etc.)
|
||||||
|
- **Índices funcionales** para valores específicos en JSONB
|
||||||
|
- **Índices de texto** para búsqueda full-text en español
|
||||||
|
|
||||||
|
## Próximos Pasos
|
||||||
|
|
||||||
|
1. **Frontend**: Desarrollar UI en Nuxt 4
|
||||||
|
2. **API**: Crear endpoints REST/GraphQL
|
||||||
|
3. **Autenticación**: Integrar con Supabase Auth
|
||||||
|
4. **Análisis**: Dashboards y reportes estadísticos
|
||||||
|
5. **Export**: Generar PDFs de sesiones de catación
|
||||||
|
|
||||||
|
## Desarrollo
|
||||||
|
|
||||||
|
Para desarrollar sobre este esquema:
|
||||||
|
|
||||||
|
1. Los scripts en `postgres/init/` solo se ejecutan la primera vez
|
||||||
|
2. Para aplicar cambios, usa migrations o reinicia el contenedor con `-v`
|
||||||
|
3. Los tests en `postgres/tests/` son re-ejecutables sin problemas
|
||||||
|
|
||||||
|
## Soporte
|
||||||
|
|
||||||
|
Para reportar problemas o sugerencias, contacta al equipo de Nucleo Rio Frio.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Desarrollado por Nucleo Rio Frio** con Claude Code
|
||||||
32
docker-compose.yml
Normal file
32
docker-compose.yml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: riocata_postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: riocata
|
||||||
|
POSTGRES_USER: riocata_user
|
||||||
|
POSTGRES_PASSWORD: riocata_password
|
||||||
|
PGDATA: /var/lib/postgresql/data/pgdata
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./postgres/init:/docker-entrypoint-initdb.d
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U riocata_user -d riocata"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- riocata_network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
riocata_network:
|
||||||
|
driver: bridge
|
||||||
123
postgres/init/01_schema.sql
Normal file
123
postgres/init/01_schema.sql
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- rioCata - Sistema de Catación de Café
|
||||||
|
-- Schema de Base de Datos
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- Habilitar extensiones necesarias
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- TIPOS ENUMERADOS
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- Tipo de defecto en la catación
|
||||||
|
CREATE TYPE defecto_tipo AS ENUM ('Mohoso', 'Fenólico', 'Papa');
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- TABLAS
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- Sesiones de catación
|
||||||
|
CREATE TABLE sesion (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
codigo text UNIQUE, -- opcional: código visible (ej. "S-2025-10-17-01")
|
||||||
|
fecha date NOT NULL,
|
||||||
|
nombre text, -- ej. "Mesa #3 Lotes Lavados"
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE sesion IS 'Sesiones de catación de café';
|
||||||
|
COMMENT ON COLUMN sesion.codigo IS 'Código único identificador de la sesión';
|
||||||
|
COMMENT ON COLUMN sesion.fecha IS 'Fecha en que se realiza la catación';
|
||||||
|
COMMENT ON COLUMN sesion.nombre IS 'Nombre descriptivo de la sesión';
|
||||||
|
|
||||||
|
-- Crear schema auth si no existe (antes de crear la tabla)
|
||||||
|
CREATE SCHEMA IF NOT EXISTS auth;
|
||||||
|
|
||||||
|
-- Tabla de usuarios (simula auth.users de Supabase para desarrollo local)
|
||||||
|
-- En producción se usaría directamente auth.users de Supabase
|
||||||
|
CREATE TABLE auth.users (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
email text UNIQUE NOT NULL,
|
||||||
|
nombre text,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE auth.users IS 'Tabla de usuarios (simula auth.users de Supabase para desarrollo)';
|
||||||
|
|
||||||
|
-- Participantes (catadores) por sesión
|
||||||
|
CREATE TABLE sesion_participante (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
sesion_id uuid NOT NULL REFERENCES sesion(id) ON DELETE CASCADE,
|
||||||
|
catador_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE RESTRICT,
|
||||||
|
rol text DEFAULT 'catador', -- opcional: juez, invitado, etc.
|
||||||
|
UNIQUE (sesion_id, catador_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE sesion_participante IS 'Relación de participantes (catadores) en sesiones de catación';
|
||||||
|
COMMENT ON COLUMN sesion_participante.rol IS 'Rol del participante: catador, juez, invitado, etc.';
|
||||||
|
|
||||||
|
-- Muestras pertenecen a una sesión
|
||||||
|
CREATE TABLE muestra (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
sesion_id uuid NOT NULL REFERENCES sesion(id) ON DELETE CASCADE,
|
||||||
|
codigo text NOT NULL, -- "Muestra 1", "A-101", etc.
|
||||||
|
posicion int, -- ej. orden físico en mesa
|
||||||
|
UNIQUE (sesion_id, codigo)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE muestra IS 'Muestras de café a ser evaluadas en una sesión';
|
||||||
|
COMMENT ON COLUMN muestra.codigo IS 'Código identificador de la muestra dentro de la sesión';
|
||||||
|
COMMENT ON COLUMN muestra.posicion IS 'Orden físico de la muestra en la mesa de catación';
|
||||||
|
|
||||||
|
-- Evaluación de una muestra hecha por un participante específico de esa sesión
|
||||||
|
CREATE TABLE evaluacion (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
muestra_id uuid NOT NULL REFERENCES muestra(id) ON DELETE CASCADE,
|
||||||
|
sesion_participante_id uuid NOT NULL REFERENCES sesion_participante(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
-- JSONB según lo acordado
|
||||||
|
intensidades jsonb NOT NULL, -- { fragancia: {descriptiva, afectiva}, ... }
|
||||||
|
fragancia_aroma_notas jsonb DEFAULT '[]', -- array de objetos [{categoria, subcategoria, notaEspecifica}]
|
||||||
|
sabor_notas jsonb DEFAULT '[]', -- array de objetos
|
||||||
|
|
||||||
|
-- Arrays según lo acordado
|
||||||
|
tazas_no_uniformes smallint[] DEFAULT '{}', -- valores 1..5
|
||||||
|
tazas_defectuosas smallint[] DEFAULT '{}', -- valores 1..5
|
||||||
|
sensacion_en_boca text[] DEFAULT '{}', -- Áspero, Suave, Aceitoso, Metálico, Astringente
|
||||||
|
gustos_predominantes text[] DEFAULT '{}', -- 1..2 elementos entre Salado, Amargo, Ácido, Dulce, Umami
|
||||||
|
|
||||||
|
defecto defecto_tipo, -- opcional
|
||||||
|
otras_notas text,
|
||||||
|
puntaje_final int, -- guardado por trigger
|
||||||
|
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
|
||||||
|
-- Una evaluación por participante por muestra
|
||||||
|
UNIQUE (muestra_id, sesion_participante_id),
|
||||||
|
|
||||||
|
-- Constraints de validez
|
||||||
|
CHECK (tazas_no_uniformes <@ ARRAY[1,2,3,4,5]::smallint[]),
|
||||||
|
CHECK (tazas_defectuosas <@ ARRAY[1,2,3,4,5]::smallint[]),
|
||||||
|
|
||||||
|
CHECK (sensacion_en_boca <@ ARRAY['Áspero','Suave','Aceitoso','Metálico','Astringente']::text[]),
|
||||||
|
CHECK (gustos_predominantes <@ ARRAY['Salado','Amargo','Ácido','Dulce','Umami']::text[]),
|
||||||
|
CHECK (COALESCE(array_length(gustos_predominantes,1),0) BETWEEN 0 AND 2),
|
||||||
|
|
||||||
|
-- Validación de rangos en intensidades (descriptiva 1..15, afectiva 1..10)
|
||||||
|
CHECK (NOT jsonb_path_exists(intensidades, '$.*.descriptiva ? (@ < 1 || @ > 15)')),
|
||||||
|
CHECK (NOT jsonb_path_exists(intensidades, '$.*.afectiva ? (@ < 1 || @ > 10)'))
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE evaluacion IS 'Evaluaciones de muestras realizadas por catadores';
|
||||||
|
COMMENT ON COLUMN evaluacion.intensidades IS 'Intensidades descriptivas y afectivas de los 8 parámetros evaluados';
|
||||||
|
COMMENT ON COLUMN evaluacion.fragancia_aroma_notas IS 'Array de familias de fragancia/aroma (categoría, subcategoría, nota específica)';
|
||||||
|
COMMENT ON COLUMN evaluacion.sabor_notas IS 'Array de familias de sabor (categoría, subcategoría, nota específica)';
|
||||||
|
COMMENT ON COLUMN evaluacion.tazas_no_uniformes IS 'Números de taza (1-5) que no fueron uniformes';
|
||||||
|
COMMENT ON COLUMN evaluacion.tazas_defectuosas IS 'Números de taza (1-5) con defectos graves';
|
||||||
|
COMMENT ON COLUMN evaluacion.sensacion_en_boca IS 'Sensaciones táctiles: Áspero, Suave, Aceitoso, Metálico, Astringente';
|
||||||
|
COMMENT ON COLUMN evaluacion.gustos_predominantes IS 'Gustos básicos predominantes (1-2): Salado, Amargo, Ácido, Dulce, Umami';
|
||||||
|
COMMENT ON COLUMN evaluacion.defecto IS 'Tipo de defecto identificado si aplica';
|
||||||
|
COMMENT ON COLUMN evaluacion.otras_notas IS 'Notas adicionales del catador en texto libre';
|
||||||
|
COMMENT ON COLUMN evaluacion.puntaje_final IS 'Suma de los valores afectivos (calculado automáticamente por trigger)';
|
||||||
105
postgres/init/02_functions.sql
Normal file
105
postgres/init/02_functions.sql
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- rioCata - Funciones y Triggers
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- FUNCIÓN: Actualizar updated_at
|
||||||
|
-- ============================================
|
||||||
|
CREATE OR REPLACE FUNCTION set_updated_at()
|
||||||
|
RETURNS trigger LANGUAGE plpgsql AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at := now();
|
||||||
|
RETURN NEW;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION set_updated_at() IS 'Actualiza automáticamente el campo updated_at al timestamp actual';
|
||||||
|
|
||||||
|
-- Trigger para actualizar updated_at en evaluacion
|
||||||
|
CREATE TRIGGER trg_eval_updated_at
|
||||||
|
BEFORE UPDATE ON evaluacion
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- FUNCIÓN: Calcular puntaje final
|
||||||
|
-- ============================================
|
||||||
|
-- Calcula el puntaje_final sumando SOLO los valores afectivos presentes
|
||||||
|
CREATE OR REPLACE FUNCTION eval_compute_score()
|
||||||
|
RETURNS trigger LANGUAGE plpgsql AS $$
|
||||||
|
DECLARE
|
||||||
|
s int := 0;
|
||||||
|
BEGIN
|
||||||
|
-- Suma todos los valores afectivos presentes en intensidades
|
||||||
|
-- Si alguno es null, COALESCE lo convierte en 0
|
||||||
|
s := s + COALESCE((NEW.intensidades->'fragancia' ->>'afectiva')::int, 0);
|
||||||
|
s := s + COALESCE((NEW.intensidades->'aroma' ->>'afectiva')::int, 0);
|
||||||
|
s := s + COALESCE((NEW.intensidades->'sabor' ->>'afectiva')::int, 0);
|
||||||
|
s := s + COALESCE((NEW.intensidades->'saborResidual' ->>'afectiva')::int, 0);
|
||||||
|
s := s + COALESCE((NEW.intensidades->'acidez' ->>'afectiva')::int, 0);
|
||||||
|
s := s + COALESCE((NEW.intensidades->'dulzor' ->>'afectiva')::int, 0);
|
||||||
|
s := s + COALESCE((NEW.intensidades->'sensacionBoca' ->>'afectiva')::int, 0);
|
||||||
|
s := s + COALESCE((NEW.intensidades->'impresionGlobal'->>'afectiva')::int, 0);
|
||||||
|
|
||||||
|
NEW.puntaje_final := s;
|
||||||
|
RETURN NEW;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION eval_compute_score() IS 'Calcula automáticamente el puntaje_final como suma de valores afectivos';
|
||||||
|
|
||||||
|
-- Trigger para calcular puntaje_final en INSERT y UPDATE
|
||||||
|
CREATE TRIGGER trg_eval_score_bi
|
||||||
|
BEFORE INSERT OR UPDATE OF intensidades
|
||||||
|
ON evaluacion
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION eval_compute_score();
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- FUNCIONES AUXILIARES DE CONSULTA
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- Función para obtener el promedio de un parámetro afectivo en una sesión
|
||||||
|
CREATE OR REPLACE FUNCTION get_promedio_parametro_afectivo(
|
||||||
|
p_sesion_id uuid,
|
||||||
|
p_parametro text -- 'fragancia', 'aroma', 'sabor', etc.
|
||||||
|
)
|
||||||
|
RETURNS numeric AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN (
|
||||||
|
SELECT AVG(((e.intensidades -> p_parametro ->>'afectiva')::int))
|
||||||
|
FROM sesion s
|
||||||
|
JOIN muestra m ON m.sesion_id = s.id
|
||||||
|
JOIN evaluacion e ON e.muestra_id = m.id
|
||||||
|
WHERE s.id = p_sesion_id
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION get_promedio_parametro_afectivo(uuid, text) IS 'Obtiene el promedio de un parámetro afectivo para una sesión';
|
||||||
|
|
||||||
|
-- Función para obtener las top N muestras de una sesión por puntaje
|
||||||
|
CREATE OR REPLACE FUNCTION get_top_muestras(
|
||||||
|
p_sesion_id uuid,
|
||||||
|
p_limit int DEFAULT 3
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
muestra_codigo text,
|
||||||
|
puntaje_final int,
|
||||||
|
catador_email text,
|
||||||
|
catador_nombre text
|
||||||
|
) AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
m.codigo,
|
||||||
|
e.puntaje_final,
|
||||||
|
u.email,
|
||||||
|
u.nombre
|
||||||
|
FROM muestra m
|
||||||
|
JOIN evaluacion e ON e.muestra_id = m.id
|
||||||
|
JOIN sesion_participante sp ON sp.id = e.sesion_participante_id
|
||||||
|
JOIN auth.users u ON u.id = sp.catador_id
|
||||||
|
WHERE m.sesion_id = p_sesion_id
|
||||||
|
ORDER BY e.puntaje_final DESC
|
||||||
|
LIMIT p_limit;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
COMMENT ON FUNCTION get_top_muestras(uuid, int) IS 'Obtiene las muestras con mejor puntaje de una sesión';
|
||||||
77
postgres/init/03_indexes.sql
Normal file
77
postgres/init/03_indexes.sql
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- rioCata - Índices para Optimización
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- ÍNDICES EN CLAVES FORÁNEAS
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
CREATE INDEX idx_sesion_participante_sesion_id ON sesion_participante(sesion_id);
|
||||||
|
CREATE INDEX idx_sesion_participante_catador_id ON sesion_participante(catador_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_muestra_sesion ON muestra(sesion_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_eval_muestra ON evaluacion(muestra_id);
|
||||||
|
CREATE INDEX idx_eval_participante ON evaluacion(sesion_participante_id);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- ÍNDICES EN CAMPOS ESPECÍFICOS
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- Índice para búsquedas por tipo de defecto
|
||||||
|
CREATE INDEX idx_eval_defecto ON evaluacion(defecto);
|
||||||
|
|
||||||
|
-- Índice para búsquedas por puntaje final
|
||||||
|
CREATE INDEX idx_eval_puntaje_final ON evaluacion(puntaje_final DESC);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- ÍNDICES GIN PARA JSONB Y ARRAYS
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- Índices GIN para consultas de contención en JSONB
|
||||||
|
CREATE INDEX idx_eval_json_intensidades ON evaluacion USING GIN (intensidades jsonb_path_ops);
|
||||||
|
CREATE INDEX idx_eval_aroma_notas ON evaluacion USING GIN (fragancia_aroma_notas);
|
||||||
|
CREATE INDEX idx_eval_sabor_notas ON evaluacion USING GIN (sabor_notas);
|
||||||
|
|
||||||
|
-- Índices GIN para arrays (permite consultas con @>, &&, etc.)
|
||||||
|
CREATE INDEX idx_eval_tazas_defectuosas ON evaluacion USING GIN (tazas_defectuosas);
|
||||||
|
CREATE INDEX idx_eval_tazas_no_uniformes ON evaluacion USING GIN (tazas_no_uniformes);
|
||||||
|
CREATE INDEX idx_eval_sensacion_boca ON evaluacion USING GIN (sensacion_en_boca);
|
||||||
|
CREATE INDEX idx_eval_gustos_predominantes ON evaluacion USING GIN (gustos_predominantes);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- ÍNDICES FUNCIONALES (OPCIONALES)
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- Índices funcionales para consultas frecuentes sobre valores específicos en intensidades
|
||||||
|
-- Estos son opcionales pero mejoran el rendimiento de queries que filtran por valores afectivos específicos
|
||||||
|
|
||||||
|
-- Ejemplo: filtrar por acidez afectiva >= 8
|
||||||
|
CREATE INDEX idx_eval_int_acidez_afectiva
|
||||||
|
ON evaluacion ( ((intensidades->'acidez'->>'afectiva')::int) );
|
||||||
|
|
||||||
|
-- Ejemplo: filtrar por dulzor afectivo >= 8
|
||||||
|
CREATE INDEX idx_eval_int_dulzor_afectivo
|
||||||
|
ON evaluacion ( ((intensidades->'dulzor'->>'afectiva')::int) );
|
||||||
|
|
||||||
|
-- Ejemplo: filtrar por sabor afectivo >= 8
|
||||||
|
CREATE INDEX idx_eval_int_sabor_afectivo
|
||||||
|
ON evaluacion ( ((intensidades->'sabor'->>'afectiva')::int) );
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- ÍNDICES DE TEXTO (PARA BÚSQUEDA)
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- Índice para búsqueda de texto en otras_notas
|
||||||
|
CREATE INDEX idx_eval_otras_notas_gin ON evaluacion USING GIN (to_tsvector('spanish', COALESCE(otras_notas, '')));
|
||||||
|
|
||||||
|
-- Índice para búsqueda en nombres de sesión
|
||||||
|
CREATE INDEX idx_sesion_nombre_gin ON sesion USING GIN (to_tsvector('spanish', COALESCE(nombre, '')));
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- COMENTARIOS
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
COMMENT ON INDEX idx_eval_json_intensidades IS 'Índice GIN para consultas de contención en intensidades (jsonb_path_ops)';
|
||||||
|
COMMENT ON INDEX idx_eval_tazas_defectuosas IS 'Índice GIN para búsquedas de tazas defectuosas específicas';
|
||||||
|
COMMENT ON INDEX idx_eval_int_acidez_afectiva IS 'Índice funcional para filtros por valor afectivo de acidez';
|
||||||
244
postgres/init/04_sample_data.sql
Normal file
244
postgres/init/04_sample_data.sql
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- rioCata - Datos de Prueba
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- USUARIOS DE PRUEBA
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
INSERT INTO auth.users (id, email, nombre) VALUES
|
||||||
|
('11111111-1111-1111-1111-111111111111', 'dario@nucleoriofrio.com', 'Dario'),
|
||||||
|
('22222222-2222-2222-2222-222222222222', 'juan.catador@example.com', 'Juan Pérez'),
|
||||||
|
('33333333-3333-3333-3333-333333333333', 'maria.juez@example.com', 'María González');
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- SESIÓN DE CATACIÓN DE PRUEBA
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
INSERT INTO sesion (id, codigo, fecha, nombre) VALUES
|
||||||
|
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'S-2025-10-17-01', '2025-10-17', 'Mesa Laboratorio #1 - Lotes Lavados');
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- PARTICIPANTES DE LA SESIÓN
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
INSERT INTO sesion_participante (id, sesion_id, catador_id, rol) VALUES
|
||||||
|
('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '11111111-1111-1111-1111-111111111111', 'catador'),
|
||||||
|
('cccccccc-cccc-cccc-cccc-cccccccccccc', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '22222222-2222-2222-2222-222222222222', 'catador'),
|
||||||
|
('dddddddd-dddd-dddd-dddd-dddddddddddd', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', '33333333-3333-3333-3333-333333333333', 'juez');
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- MUESTRAS DE CAFÉ
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
INSERT INTO muestra (id, sesion_id, codigo, posicion) VALUES
|
||||||
|
('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'Muestra A-101', 1),
|
||||||
|
('ffffffff-ffff-ffff-ffff-ffffffffffff', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'Muestra B-202', 2),
|
||||||
|
('00000000-0000-0000-0000-000000000001', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'Muestra C-303', 3);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- EVALUACIONES DE PRUEBA
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- Evaluación 1: Dario evalúa Muestra A-101 (café excelente)
|
||||||
|
INSERT INTO evaluacion (
|
||||||
|
muestra_id,
|
||||||
|
sesion_participante_id,
|
||||||
|
intensidades,
|
||||||
|
fragancia_aroma_notas,
|
||||||
|
sabor_notas,
|
||||||
|
tazas_no_uniformes,
|
||||||
|
tazas_defectuosas,
|
||||||
|
sensacion_en_boca,
|
||||||
|
gustos_predominantes,
|
||||||
|
defecto,
|
||||||
|
otras_notas
|
||||||
|
) VALUES (
|
||||||
|
'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee',
|
||||||
|
'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
|
||||||
|
'{
|
||||||
|
"fragancia": {"descriptiva": 8, "afectiva": 9},
|
||||||
|
"aroma": {"descriptiva": 9, "afectiva": 9},
|
||||||
|
"sabor": {"descriptiva": 10, "afectiva": 10},
|
||||||
|
"saborResidual": {"descriptiva": 8, "afectiva": 9},
|
||||||
|
"acidez": {"descriptiva": 9, "afectiva": 9},
|
||||||
|
"dulzor": {"descriptiva": 10, "afectiva": 10},
|
||||||
|
"sensacionBoca": {"descriptiva": 8, "afectiva": 9},
|
||||||
|
"impresionGlobal": {"descriptiva": null, "afectiva": 10}
|
||||||
|
}'::jsonb,
|
||||||
|
'[
|
||||||
|
{"categoria": "Afrutado", "subcategoria": "Cítricos", "notaEspecifica": "Naranja dulce"},
|
||||||
|
{"categoria": "Floral", "subcategoria": "Jazmín", "notaEspecifica": null}
|
||||||
|
]'::jsonb,
|
||||||
|
'[
|
||||||
|
{"categoria": "Afrutado", "subcategoria": "Bayas", "notaEspecifica": "Fresa madura"},
|
||||||
|
{"categoria": "Chocolatado", "subcategoria": "Chocolate con leche", "notaEspecifica": null}
|
||||||
|
]'::jsonb,
|
||||||
|
ARRAY[]::smallint[],
|
||||||
|
ARRAY[]::smallint[],
|
||||||
|
ARRAY['Suave', 'Aceitoso']::text[],
|
||||||
|
ARRAY['Ácido', 'Dulce']::text[],
|
||||||
|
null,
|
||||||
|
'Café excepcional. Muy balanceado, con dulzor prominente y acidez brillante. Notas a vino tinto en el retrogusto. Cuerpo medio-alto, muy limpio.'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Evaluación 2: Juan evalúa Muestra A-101 (también buena evaluación)
|
||||||
|
INSERT INTO evaluacion (
|
||||||
|
muestra_id,
|
||||||
|
sesion_participante_id,
|
||||||
|
intensidades,
|
||||||
|
fragancia_aroma_notas,
|
||||||
|
sabor_notas,
|
||||||
|
tazas_no_uniformes,
|
||||||
|
tazas_defectuosas,
|
||||||
|
sensacion_en_boca,
|
||||||
|
gustos_predominantes,
|
||||||
|
defecto,
|
||||||
|
otras_notas
|
||||||
|
) VALUES (
|
||||||
|
'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee',
|
||||||
|
'cccccccc-cccc-cccc-cccc-cccccccccccc',
|
||||||
|
'{
|
||||||
|
"fragancia": {"descriptiva": 7, "afectiva": 8},
|
||||||
|
"aroma": {"descriptiva": 8, "afectiva": 9},
|
||||||
|
"sabor": {"descriptiva": 9, "afectiva": 9},
|
||||||
|
"saborResidual": {"descriptiva": 7, "afectiva": 8},
|
||||||
|
"acidez": {"descriptiva": 8, "afectiva": 8},
|
||||||
|
"dulzor": {"descriptiva": 9, "afectiva": 9},
|
||||||
|
"sensacionBoca": {"descriptiva": 7, "afectiva": 8},
|
||||||
|
"impresionGlobal": {"descriptiva": null, "afectiva": 9}
|
||||||
|
}'::jsonb,
|
||||||
|
'[
|
||||||
|
{"categoria": "Afrutado", "subcategoria": "Cítricos", "notaEspecifica": "Mandarina"}
|
||||||
|
]'::jsonb,
|
||||||
|
'[
|
||||||
|
{"categoria": "Afrutado", "subcategoria": "Bayas", "notaEspecifica": "Mora"},
|
||||||
|
{"categoria": "Caramelizado", "subcategoria": "Caramelo", "notaEspecifica": null}
|
||||||
|
]'::jsonb,
|
||||||
|
ARRAY[]::smallint[],
|
||||||
|
ARRAY[]::smallint[],
|
||||||
|
ARRAY['Suave']::text[],
|
||||||
|
ARRAY['Dulce', 'Ácido']::text[],
|
||||||
|
null,
|
||||||
|
'Muy buen café. Dulce y complejo. Buena acidez cítrica. Cuerpo medio.'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Evaluación 3: Dario evalúa Muestra B-202 (café con problemas de uniformidad)
|
||||||
|
INSERT INTO evaluacion (
|
||||||
|
muestra_id,
|
||||||
|
sesion_participante_id,
|
||||||
|
intensidades,
|
||||||
|
fragancia_aroma_notas,
|
||||||
|
sabor_notas,
|
||||||
|
tazas_no_uniformes,
|
||||||
|
tazas_defectuosas,
|
||||||
|
sensacion_en_boca,
|
||||||
|
gustos_predominantes,
|
||||||
|
defecto,
|
||||||
|
otras_notas
|
||||||
|
) VALUES (
|
||||||
|
'ffffffff-ffff-ffff-ffff-ffffffffffff',
|
||||||
|
'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
|
||||||
|
'{
|
||||||
|
"fragancia": {"descriptiva": 5, "afectiva": 6},
|
||||||
|
"aroma": {"descriptiva": 6, "afectiva": 7},
|
||||||
|
"sabor": {"descriptiva": 6, "afectiva": 7},
|
||||||
|
"saborResidual": {"descriptiva": 5, "afectiva": 6},
|
||||||
|
"acidez": {"descriptiva": 7, "afectiva": 7},
|
||||||
|
"dulzor": {"descriptiva": 6, "afectiva": 7},
|
||||||
|
"sensacionBoca": {"descriptiva": 5, "afectiva": 6},
|
||||||
|
"impresionGlobal": {"descriptiva": null, "afectiva": 6}
|
||||||
|
}'::jsonb,
|
||||||
|
'[
|
||||||
|
{"categoria": "Especiado", "subcategoria": "Canela", "notaEspecifica": null}
|
||||||
|
]'::jsonb,
|
||||||
|
'[
|
||||||
|
{"categoria": "Nueces", "subcategoria": "Almendra", "notaEspecifica": null}
|
||||||
|
]'::jsonb,
|
||||||
|
ARRAY[2, 5]::smallint[],
|
||||||
|
ARRAY[]::smallint[],
|
||||||
|
ARRAY['Áspero']::text[],
|
||||||
|
ARRAY['Ácido']::text[],
|
||||||
|
null,
|
||||||
|
'Café promedio. Las tazas 2 y 5 no estaban uniformes con las demás. Algo de astringencia. Cuerpo ligero-medio.'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Evaluación 4: María evalúa Muestra C-303 (café con defecto fenólico)
|
||||||
|
INSERT INTO evaluacion (
|
||||||
|
muestra_id,
|
||||||
|
sesion_participante_id,
|
||||||
|
intensidades,
|
||||||
|
fragancia_aroma_notas,
|
||||||
|
sabor_notas,
|
||||||
|
tazas_no_uniformes,
|
||||||
|
tazas_defectuosas,
|
||||||
|
sensacion_en_boca,
|
||||||
|
gustos_predominantes,
|
||||||
|
defecto,
|
||||||
|
otras_notas
|
||||||
|
) VALUES (
|
||||||
|
'00000000-0000-0000-0000-000000000001',
|
||||||
|
'dddddddd-dddd-dddd-dddd-dddddddddddd',
|
||||||
|
'{
|
||||||
|
"fragancia": {"descriptiva": 4, "afectiva": 4},
|
||||||
|
"aroma": {"descriptiva": 3, "afectiva": 3},
|
||||||
|
"sabor": {"descriptiva": 3, "afectiva": 3},
|
||||||
|
"saborResidual": {"descriptiva": 2, "afectiva": 2},
|
||||||
|
"acidez": {"descriptiva": 5, "afectiva": 5},
|
||||||
|
"dulzor": {"descriptiva": 4, "afectiva": 4},
|
||||||
|
"sensacionBoca": {"descriptiva": 3, "afectiva": 3},
|
||||||
|
"impresionGlobal": {"descriptiva": null, "afectiva": 2}
|
||||||
|
}'::jsonb,
|
||||||
|
'[
|
||||||
|
{"categoria": "Otros", "subcategoria": "Químico", "notaEspecifica": "Medicinal"}
|
||||||
|
]'::jsonb,
|
||||||
|
'[
|
||||||
|
{"categoria": "Otros", "subcategoria": "Químico", "notaEspecifica": "Fenólico"}
|
||||||
|
]'::jsonb,
|
||||||
|
ARRAY[3, 4, 5]::smallint[],
|
||||||
|
ARRAY[5]::smallint[],
|
||||||
|
ARRAY['Áspero', 'Astringente']::text[],
|
||||||
|
ARRAY['Amargo']::text[],
|
||||||
|
'Fenólico',
|
||||||
|
'Café con defecto fenólico grave en taza 5. Sabor medicinal/químico. No apto para comercialización.'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Evaluación 5: Juan evalúa Muestra B-202
|
||||||
|
INSERT INTO evaluacion (
|
||||||
|
muestra_id,
|
||||||
|
sesion_participante_id,
|
||||||
|
intensidades,
|
||||||
|
fragancia_aroma_notas,
|
||||||
|
sabor_notas,
|
||||||
|
tazas_no_uniformes,
|
||||||
|
tazas_defectuosas,
|
||||||
|
sensacion_en_boca,
|
||||||
|
gustos_predominantes,
|
||||||
|
defecto,
|
||||||
|
otras_notas
|
||||||
|
) VALUES (
|
||||||
|
'ffffffff-ffff-ffff-ffff-ffffffffffff',
|
||||||
|
'cccccccc-cccc-cccc-cccc-cccccccccccc',
|
||||||
|
'{
|
||||||
|
"fragancia": {"descriptiva": 6, "afectiva": 7},
|
||||||
|
"aroma": {"descriptiva": 6, "afectiva": 7},
|
||||||
|
"sabor": {"descriptiva": 7, "afectiva": 7},
|
||||||
|
"saborResidual": {"descriptiva": 6, "afectiva": 7},
|
||||||
|
"acidez": {"descriptiva": 6, "afectiva": 7},
|
||||||
|
"dulzor": {"descriptiva": 7, "afectiva": 7},
|
||||||
|
"sensacionBoca": {"descriptiva": 6, "afectiva": 7},
|
||||||
|
"impresionGlobal": {"descriptiva": null, "afectiva": 7}
|
||||||
|
}'::jsonb,
|
||||||
|
'[
|
||||||
|
{"categoria": "Nueces", "subcategoria": "Avellana", "notaEspecifica": null}
|
||||||
|
]'::jsonb,
|
||||||
|
'[
|
||||||
|
{"categoria": "Caramelizado", "subcategoria": "Miel", "notaEspecifica": null}
|
||||||
|
]'::jsonb,
|
||||||
|
ARRAY[3]::smallint[],
|
||||||
|
ARRAY[]::smallint[],
|
||||||
|
ARRAY['Suave']::text[],
|
||||||
|
ARRAY['Dulce']::text[],
|
||||||
|
null,
|
||||||
|
'Café correcto. Taza 3 ligeramente diferente. Notas a nueces tostadas. Cuerpo medio.'
|
||||||
|
);
|
||||||
296
postgres/tests/example_queries.sql
Normal file
296
postgres/tests/example_queries.sql
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- rioCata - Queries de Ejemplo
|
||||||
|
-- ============================================
|
||||||
|
-- Este archivo contiene queries de ejemplo
|
||||||
|
-- para explorar los datos de catación
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
\echo '=========================================='
|
||||||
|
\echo 'Queries de Ejemplo - rioCata'
|
||||||
|
\echo '=========================================='
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- QUERY 1: Listar todas las sesiones
|
||||||
|
-- ============================================
|
||||||
|
\echo '[QUERY 1] Sesiones de catación:'
|
||||||
|
SELECT
|
||||||
|
codigo,
|
||||||
|
fecha,
|
||||||
|
nombre,
|
||||||
|
created_at
|
||||||
|
FROM sesion
|
||||||
|
ORDER BY fecha DESC;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- QUERY 2: Participantes de una sesión
|
||||||
|
-- ============================================
|
||||||
|
\echo '[QUERY 2] Participantes de la sesión S-2025-10-17-01:'
|
||||||
|
SELECT
|
||||||
|
u.nombre AS catador,
|
||||||
|
u.email,
|
||||||
|
sp.rol
|
||||||
|
FROM sesion s
|
||||||
|
JOIN sesion_participante sp ON sp.sesion_id = s.id
|
||||||
|
JOIN auth.users u ON u.id = sp.catador_id
|
||||||
|
WHERE s.codigo = 'S-2025-10-17-01';
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- QUERY 3: Muestras de una sesión
|
||||||
|
-- ============================================
|
||||||
|
\echo '[QUERY 3] Muestras de la sesión S-2025-10-17-01:'
|
||||||
|
SELECT
|
||||||
|
m.codigo AS muestra,
|
||||||
|
m.posicion,
|
||||||
|
COUNT(e.id) AS num_evaluaciones
|
||||||
|
FROM sesion s
|
||||||
|
JOIN muestra m ON m.sesion_id = s.id
|
||||||
|
LEFT JOIN evaluacion e ON e.muestra_id = m.id
|
||||||
|
WHERE s.codigo = 'S-2025-10-17-01'
|
||||||
|
GROUP BY m.id, m.codigo, m.posicion
|
||||||
|
ORDER BY m.posicion;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- QUERY 4: Top 5 evaluaciones por puntaje
|
||||||
|
-- ============================================
|
||||||
|
\echo '[QUERY 4] Top 5 evaluaciones por puntaje final:'
|
||||||
|
SELECT
|
||||||
|
m.codigo AS muestra,
|
||||||
|
u.nombre AS catador,
|
||||||
|
e.puntaje_final,
|
||||||
|
e.otras_notas
|
||||||
|
FROM evaluacion e
|
||||||
|
JOIN muestra m ON m.id = e.muestra_id
|
||||||
|
JOIN sesion_participante sp ON sp.id = e.sesion_participante_id
|
||||||
|
JOIN auth.users u ON u.id = sp.catador_id
|
||||||
|
ORDER BY e.puntaje_final DESC
|
||||||
|
LIMIT 5;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- QUERY 5: Evaluaciones con defectos
|
||||||
|
-- ============================================
|
||||||
|
\echo '[QUERY 5] Evaluaciones con defectos:'
|
||||||
|
SELECT
|
||||||
|
m.codigo AS muestra,
|
||||||
|
u.nombre AS catador,
|
||||||
|
e.defecto,
|
||||||
|
e.tazas_defectuosas,
|
||||||
|
e.puntaje_final
|
||||||
|
FROM evaluacion e
|
||||||
|
JOIN muestra m ON m.id = e.muestra_id
|
||||||
|
JOIN sesion_participante sp ON sp.id = e.sesion_participante_id
|
||||||
|
JOIN auth.users u ON u.id = sp.catador_id
|
||||||
|
WHERE e.defecto IS NOT NULL
|
||||||
|
ORDER BY e.created_at;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- QUERY 6: Promedio de puntajes por muestra
|
||||||
|
-- ============================================
|
||||||
|
\echo '[QUERY 6] Promedio de puntajes por muestra:'
|
||||||
|
SELECT
|
||||||
|
m.codigo AS muestra,
|
||||||
|
COUNT(e.id) AS num_evaluaciones,
|
||||||
|
ROUND(AVG(e.puntaje_final), 2) AS puntaje_promedio,
|
||||||
|
MIN(e.puntaje_final) AS puntaje_minimo,
|
||||||
|
MAX(e.puntaje_final) AS puntaje_maximo
|
||||||
|
FROM muestra m
|
||||||
|
LEFT JOIN evaluacion e ON e.muestra_id = m.id
|
||||||
|
GROUP BY m.id, m.codigo
|
||||||
|
ORDER BY puntaje_promedio DESC NULLS LAST;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- QUERY 7: Promedios de parámetros afectivos por muestra
|
||||||
|
-- ============================================
|
||||||
|
\echo '[QUERY 7] Promedios de parámetros afectivos por muestra:'
|
||||||
|
SELECT
|
||||||
|
m.codigo AS muestra,
|
||||||
|
ROUND(AVG((e.intensidades->'fragancia'->>'afectiva')::int), 2) AS fragancia,
|
||||||
|
ROUND(AVG((e.intensidades->'aroma'->>'afectiva')::int), 2) AS aroma,
|
||||||
|
ROUND(AVG((e.intensidades->'sabor'->>'afectiva')::int), 2) AS sabor,
|
||||||
|
ROUND(AVG((e.intensidades->'acidez'->>'afectiva')::int), 2) AS acidez,
|
||||||
|
ROUND(AVG((e.intensidades->'dulzor'->>'afectiva')::int), 2) AS dulzor
|
||||||
|
FROM muestra m
|
||||||
|
JOIN evaluacion e ON e.muestra_id = m.id
|
||||||
|
GROUP BY m.id, m.codigo
|
||||||
|
ORDER BY m.codigo;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- QUERY 8: Evaluaciones con tazas no uniformes
|
||||||
|
-- ============================================
|
||||||
|
\echo '[QUERY 8] Evaluaciones con tazas no uniformes:'
|
||||||
|
SELECT
|
||||||
|
m.codigo AS muestra,
|
||||||
|
u.nombre AS catador,
|
||||||
|
e.tazas_no_uniformes,
|
||||||
|
e.puntaje_final
|
||||||
|
FROM evaluacion e
|
||||||
|
JOIN muestra m ON m.id = e.muestra_id
|
||||||
|
JOIN sesion_participante sp ON sp.id = e.sesion_participante_id
|
||||||
|
JOIN auth.users u ON u.id = sp.catador_id
|
||||||
|
WHERE array_length(e.tazas_no_uniformes, 1) > 0
|
||||||
|
ORDER BY array_length(e.tazas_no_uniformes, 1) DESC;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- QUERY 9: Notas de fragancia/aroma más comunes
|
||||||
|
-- ============================================
|
||||||
|
\echo '[QUERY 9] Categorías de fragancia/aroma más frecuentes:'
|
||||||
|
SELECT
|
||||||
|
nota->>'categoria' AS categoria,
|
||||||
|
COUNT(*) AS frecuencia
|
||||||
|
FROM evaluacion e,
|
||||||
|
jsonb_array_elements(e.fragancia_aroma_notas) AS nota
|
||||||
|
GROUP BY nota->>'categoria'
|
||||||
|
ORDER BY frecuencia DESC;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- QUERY 10: Notas de sabor más comunes
|
||||||
|
-- ============================================
|
||||||
|
\echo '[QUERY 10] Categorías de sabor más frecuentes:'
|
||||||
|
SELECT
|
||||||
|
nota->>'categoria' AS categoria,
|
||||||
|
COUNT(*) AS frecuencia
|
||||||
|
FROM evaluacion e,
|
||||||
|
jsonb_array_elements(e.sabor_notas) AS nota
|
||||||
|
GROUP BY nota->>'categoria'
|
||||||
|
ORDER BY frecuencia DESC;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- QUERY 11: Gustos predominantes más comunes
|
||||||
|
-- ============================================
|
||||||
|
\echo '[QUERY 11] Gustos predominantes más frecuentes:'
|
||||||
|
SELECT
|
||||||
|
UNNEST(gustos_predominantes) AS gusto,
|
||||||
|
COUNT(*) AS frecuencia
|
||||||
|
FROM evaluacion
|
||||||
|
GROUP BY gusto
|
||||||
|
ORDER BY frecuencia DESC;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- QUERY 12: Sensaciones en boca más comunes
|
||||||
|
-- ============================================
|
||||||
|
\echo '[QUERY 12] Sensaciones en boca más frecuentes:'
|
||||||
|
SELECT
|
||||||
|
UNNEST(sensacion_en_boca) AS sensacion,
|
||||||
|
COUNT(*) AS frecuencia
|
||||||
|
FROM evaluacion
|
||||||
|
GROUP BY sensacion
|
||||||
|
ORDER BY frecuencia DESC;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- QUERY 13: Comparación de catadores
|
||||||
|
-- ============================================
|
||||||
|
\echo '[QUERY 13] Estadísticas por catador:'
|
||||||
|
SELECT
|
||||||
|
u.nombre AS catador,
|
||||||
|
COUNT(e.id) AS num_evaluaciones,
|
||||||
|
ROUND(AVG(e.puntaje_final), 2) AS puntaje_promedio,
|
||||||
|
MIN(e.puntaje_final) AS puntaje_minimo,
|
||||||
|
MAX(e.puntaje_final) AS puntaje_maximo
|
||||||
|
FROM auth.users u
|
||||||
|
JOIN sesion_participante sp ON sp.catador_id = u.id
|
||||||
|
LEFT JOIN evaluacion e ON e.sesion_participante_id = sp.id
|
||||||
|
GROUP BY u.id, u.nombre
|
||||||
|
ORDER BY puntaje_promedio DESC;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- QUERY 14: Usando función auxiliar get_top_muestras
|
||||||
|
-- ============================================
|
||||||
|
\echo '[QUERY 14] Top 3 muestras usando función auxiliar:'
|
||||||
|
SELECT * FROM get_top_muestras('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 3);
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- QUERY 15: Evaluaciones con alta acidez (>= 8)
|
||||||
|
-- ============================================
|
||||||
|
\echo '[QUERY 15] Evaluaciones con acidez afectiva >= 8:'
|
||||||
|
SELECT
|
||||||
|
m.codigo AS muestra,
|
||||||
|
u.nombre AS catador,
|
||||||
|
(e.intensidades->'acidez'->>'afectiva')::int AS acidez_afectiva,
|
||||||
|
e.puntaje_final
|
||||||
|
FROM evaluacion e
|
||||||
|
JOIN muestra m ON m.id = e.muestra_id
|
||||||
|
JOIN sesion_participante sp ON sp.id = e.sesion_participante_id
|
||||||
|
JOIN auth.users u ON u.id = sp.catador_id
|
||||||
|
WHERE ((e.intensidades->'acidez'->>'afectiva')::int) >= 8
|
||||||
|
ORDER BY acidez_afectiva DESC;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- QUERY 16: Búsqueda de texto en otras_notas
|
||||||
|
-- ============================================
|
||||||
|
\echo '[QUERY 16] Evaluaciones que mencionan "balanceado" en otras_notas:'
|
||||||
|
SELECT
|
||||||
|
m.codigo AS muestra,
|
||||||
|
u.nombre AS catador,
|
||||||
|
e.puntaje_final,
|
||||||
|
SUBSTRING(e.otras_notas, 1, 80) AS nota_preview
|
||||||
|
FROM evaluacion e
|
||||||
|
JOIN muestra m ON m.id = e.muestra_id
|
||||||
|
JOIN sesion_participante sp ON sp.id = e.sesion_participante_id
|
||||||
|
JOIN auth.users u ON u.id = sp.catador_id
|
||||||
|
WHERE to_tsvector('spanish', COALESCE(e.otras_notas, '')) @@ to_tsquery('spanish', 'balanceado')
|
||||||
|
ORDER BY e.puntaje_final DESC;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- QUERY 17: Detalle completo de una evaluación
|
||||||
|
-- ============================================
|
||||||
|
\echo '[QUERY 17] Detalle completo de la mejor evaluación:'
|
||||||
|
SELECT
|
||||||
|
s.nombre AS sesion,
|
||||||
|
s.fecha,
|
||||||
|
m.codigo AS muestra,
|
||||||
|
u.nombre AS catador,
|
||||||
|
u.email,
|
||||||
|
e.puntaje_final,
|
||||||
|
jsonb_pretty(e.intensidades) AS intensidades,
|
||||||
|
jsonb_pretty(e.fragancia_aroma_notas) AS fragancia_aroma,
|
||||||
|
jsonb_pretty(e.sabor_notas) AS sabor,
|
||||||
|
e.tazas_no_uniformes,
|
||||||
|
e.tazas_defectuosas,
|
||||||
|
e.sensacion_en_boca,
|
||||||
|
e.gustos_predominantes,
|
||||||
|
e.defecto,
|
||||||
|
e.otras_notas
|
||||||
|
FROM evaluacion e
|
||||||
|
JOIN muestra m ON m.id = e.muestra_id
|
||||||
|
JOIN sesion s ON s.id = m.sesion_id
|
||||||
|
JOIN sesion_participante sp ON sp.id = e.sesion_participante_id
|
||||||
|
JOIN auth.users u ON u.id = sp.catador_id
|
||||||
|
ORDER BY e.puntaje_final DESC
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=========================================='
|
||||||
|
\echo 'Fin de queries de ejemplo'
|
||||||
|
\echo '=========================================='
|
||||||
493
postgres/tests/test_all.sql
Normal file
493
postgres/tests/test_all.sql
Normal file
@@ -0,0 +1,493 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- rioCata - Tests de Validación
|
||||||
|
-- ============================================
|
||||||
|
-- Este script ejecuta tests para validar:
|
||||||
|
-- 1. Estructura de tablas y constraints
|
||||||
|
-- 2. Triggers (updated_at, puntaje_final)
|
||||||
|
-- 3. Validaciones de arrays y JSONB
|
||||||
|
-- 4. Queries típicas
|
||||||
|
-- 5. Funciones auxiliares
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
\echo '=========================================='
|
||||||
|
\echo 'rioCata - Test Suite'
|
||||||
|
\echo '=========================================='
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- TEST 1: Verificar que las tablas existen
|
||||||
|
-- ============================================
|
||||||
|
\echo '[TEST 1] Verificando existencia de tablas...'
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
tabla_count int;
|
||||||
|
BEGIN
|
||||||
|
SELECT COUNT(*) INTO tabla_count
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name IN ('sesion', 'sesion_participante', 'muestra', 'evaluacion');
|
||||||
|
|
||||||
|
IF tabla_count = 4 THEN
|
||||||
|
RAISE NOTICE '✓ Todas las tablas principales existen (4/4)';
|
||||||
|
ELSE
|
||||||
|
RAISE EXCEPTION '✗ Faltan tablas. Encontradas: %/4', tabla_count;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Verificar tabla auth.users
|
||||||
|
SELECT COUNT(*) INTO tabla_count
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'auth' AND table_name = 'users';
|
||||||
|
|
||||||
|
IF tabla_count = 1 THEN
|
||||||
|
RAISE NOTICE '✓ Tabla auth.users existe';
|
||||||
|
ELSE
|
||||||
|
RAISE EXCEPTION '✗ Tabla auth.users no existe';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- TEST 2: Verificar tipo ENUM defecto_tipo
|
||||||
|
-- ============================================
|
||||||
|
\echo '[TEST 2] Verificando tipo ENUM defecto_tipo...'
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
enum_exists boolean;
|
||||||
|
BEGIN
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM pg_type WHERE typname = 'defecto_tipo'
|
||||||
|
) INTO enum_exists;
|
||||||
|
|
||||||
|
IF enum_exists THEN
|
||||||
|
RAISE NOTICE '✓ Tipo ENUM defecto_tipo existe';
|
||||||
|
ELSE
|
||||||
|
RAISE EXCEPTION '✗ Tipo ENUM defecto_tipo no existe';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- TEST 3: Verificar triggers
|
||||||
|
-- ============================================
|
||||||
|
\echo '[TEST 3] Verificando triggers...'
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
trigger_count int;
|
||||||
|
BEGIN
|
||||||
|
SELECT COUNT(*) INTO trigger_count
|
||||||
|
FROM information_schema.triggers
|
||||||
|
WHERE trigger_name IN ('trg_eval_updated_at', 'trg_eval_score_bi');
|
||||||
|
|
||||||
|
IF trigger_count = 2 THEN
|
||||||
|
RAISE NOTICE '✓ Todos los triggers existen (2/2)';
|
||||||
|
ELSE
|
||||||
|
RAISE EXCEPTION '✗ Faltan triggers. Encontrados: %/2', trigger_count;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- TEST 4: Verificar índices
|
||||||
|
-- ============================================
|
||||||
|
\echo '[TEST 4] Verificando índices...'
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
index_count int;
|
||||||
|
BEGIN
|
||||||
|
SELECT COUNT(*) INTO index_count
|
||||||
|
FROM pg_indexes
|
||||||
|
WHERE schemaname = 'public'
|
||||||
|
AND indexname LIKE 'idx_%';
|
||||||
|
|
||||||
|
IF index_count >= 15 THEN
|
||||||
|
RAISE NOTICE '✓ Índices creados correctamente (% encontrados)', index_count;
|
||||||
|
ELSE
|
||||||
|
RAISE WARNING '⚠ Se esperaban al menos 15 índices, se encontraron: %', index_count;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- TEST 5: Test de constraint - tazas_no_uniformes válidas
|
||||||
|
-- ============================================
|
||||||
|
\echo '[TEST 5] Validando constraint tazas_no_uniformes (valores 1-5)...'
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
-- Intentar insertar valor inválido (6)
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO evaluacion (
|
||||||
|
muestra_id, sesion_participante_id, intensidades, tazas_no_uniformes
|
||||||
|
) VALUES (
|
||||||
|
'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee',
|
||||||
|
'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
|
||||||
|
'{"fragancia":{"descriptiva":1,"afectiva":1}}'::jsonb,
|
||||||
|
ARRAY[6]::smallint[]
|
||||||
|
);
|
||||||
|
RAISE EXCEPTION '✗ El constraint tazas_no_uniformes NO funcionó (aceptó valor 6)';
|
||||||
|
EXCEPTION
|
||||||
|
WHEN check_violation THEN
|
||||||
|
RAISE NOTICE '✓ Constraint tazas_no_uniformes funciona correctamente';
|
||||||
|
END;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- TEST 6: Test de constraint - gustos_predominantes (máximo 2)
|
||||||
|
-- ============================================
|
||||||
|
\echo '[TEST 6] Validando constraint gustos_predominantes (máximo 2 elementos)...'
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
-- Intentar insertar 3 gustos (debería fallar)
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO evaluacion (
|
||||||
|
muestra_id, sesion_participante_id, intensidades, gustos_predominantes
|
||||||
|
) VALUES (
|
||||||
|
'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee',
|
||||||
|
'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
|
||||||
|
'{"fragancia":{"descriptiva":1,"afectiva":1}}'::jsonb,
|
||||||
|
ARRAY['Ácido', 'Dulce', 'Amargo']::text[]
|
||||||
|
);
|
||||||
|
RAISE EXCEPTION '✗ El constraint gustos_predominantes NO funcionó (aceptó 3 elementos)';
|
||||||
|
EXCEPTION
|
||||||
|
WHEN check_violation THEN
|
||||||
|
RAISE NOTICE '✓ Constraint gustos_predominantes funciona correctamente';
|
||||||
|
END;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- TEST 7: Test de constraint - sensacion_en_boca (valores válidos)
|
||||||
|
-- ============================================
|
||||||
|
\echo '[TEST 7] Validando constraint sensacion_en_boca (valores permitidos)...'
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
-- Intentar insertar valor no permitido
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO evaluacion (
|
||||||
|
muestra_id, sesion_participante_id, intensidades, sensacion_en_boca
|
||||||
|
) VALUES (
|
||||||
|
'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee',
|
||||||
|
'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
|
||||||
|
'{"fragancia":{"descriptiva":1,"afectiva":1}}'::jsonb,
|
||||||
|
ARRAY['Raro', 'Extraño']::text[]
|
||||||
|
);
|
||||||
|
RAISE EXCEPTION '✗ El constraint sensacion_en_boca NO funcionó';
|
||||||
|
EXCEPTION
|
||||||
|
WHEN check_violation THEN
|
||||||
|
RAISE NOTICE '✓ Constraint sensacion_en_boca funciona correctamente';
|
||||||
|
END;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- TEST 8: Test de constraint - rangos de intensidades
|
||||||
|
-- ============================================
|
||||||
|
\echo '[TEST 8] Validando constraint rangos de intensidades (descriptiva 1-15, afectiva 1-10)...'
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
-- Intentar insertar descriptiva > 15
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO evaluacion (
|
||||||
|
muestra_id, sesion_participante_id, intensidades
|
||||||
|
) VALUES (
|
||||||
|
'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee',
|
||||||
|
'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
|
||||||
|
'{"fragancia":{"descriptiva":20,"afectiva":5}}'::jsonb
|
||||||
|
);
|
||||||
|
RAISE EXCEPTION '✗ El constraint de rangos NO funcionó (aceptó descriptiva=20)';
|
||||||
|
EXCEPTION
|
||||||
|
WHEN check_violation THEN
|
||||||
|
RAISE NOTICE '✓ Constraint rangos de intensidades funciona (descriptiva)';
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- Intentar insertar afectiva > 10
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO evaluacion (
|
||||||
|
muestra_id, sesion_participante_id, intensidades
|
||||||
|
) VALUES (
|
||||||
|
'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee',
|
||||||
|
'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
|
||||||
|
'{"fragancia":{"descriptiva":5,"afectiva":15}}'::jsonb
|
||||||
|
);
|
||||||
|
RAISE EXCEPTION '✗ El constraint de rangos NO funcionó (aceptó afectiva=15)';
|
||||||
|
EXCEPTION
|
||||||
|
WHEN check_violation THEN
|
||||||
|
RAISE NOTICE '✓ Constraint rangos de intensidades funciona (afectiva)';
|
||||||
|
END;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- TEST 9: Test de trigger - puntaje_final se calcula automáticamente
|
||||||
|
-- ============================================
|
||||||
|
\echo '[TEST 9] Validando trigger de cálculo automático de puntaje_final...'
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
puntaje_calculado int;
|
||||||
|
puntaje_esperado int := 75; -- Suma de afectivos: 9+9+10+9+9+10+9+10 = 75
|
||||||
|
BEGIN
|
||||||
|
-- Verificar evaluación existente
|
||||||
|
SELECT puntaje_final INTO puntaje_calculado
|
||||||
|
FROM evaluacion
|
||||||
|
WHERE muestra_id = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
|
||||||
|
AND sesion_participante_id = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb';
|
||||||
|
|
||||||
|
IF puntaje_calculado = puntaje_esperado THEN
|
||||||
|
RAISE NOTICE '✓ Trigger puntaje_final funciona correctamente (% = %)', puntaje_calculado, puntaje_esperado;
|
||||||
|
ELSE
|
||||||
|
RAISE EXCEPTION '✗ Puntaje calculado (%) no coincide con esperado (%)', puntaje_calculado, puntaje_esperado;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- TEST 10: Test de constraint UNIQUE - una evaluación por participante por muestra
|
||||||
|
-- ============================================
|
||||||
|
\echo '[TEST 10] Validando constraint UNIQUE (una evaluación por participante/muestra)...'
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
-- Intentar insertar evaluación duplicada
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO evaluacion (
|
||||||
|
muestra_id, sesion_participante_id, intensidades
|
||||||
|
) VALUES (
|
||||||
|
'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee',
|
||||||
|
'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
|
||||||
|
'{"fragancia":{"descriptiva":1,"afectiva":1}}'::jsonb
|
||||||
|
);
|
||||||
|
RAISE EXCEPTION '✗ El constraint UNIQUE NO funcionó (permitió duplicado)';
|
||||||
|
EXCEPTION
|
||||||
|
WHEN unique_violation THEN
|
||||||
|
RAISE NOTICE '✓ Constraint UNIQUE funciona correctamente';
|
||||||
|
END;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- TEST 11: Query típica - Promedio de parámetro afectivo
|
||||||
|
-- ============================================
|
||||||
|
\echo '[TEST 11] Probando query: Promedio de dulzor afectivo por sesión...'
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
avg_dulzor numeric;
|
||||||
|
BEGIN
|
||||||
|
SELECT AVG( ((e.intensidades->'dulzor'->>'afectiva')::int) )
|
||||||
|
INTO avg_dulzor
|
||||||
|
FROM sesion s
|
||||||
|
JOIN muestra m ON m.sesion_id = s.id
|
||||||
|
JOIN evaluacion e ON e.muestra_id = m.id
|
||||||
|
WHERE s.id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
|
||||||
|
|
||||||
|
IF avg_dulzor IS NOT NULL THEN
|
||||||
|
RAISE NOTICE '✓ Query promedio funciona. Dulzor promedio: %', ROUND(avg_dulzor, 2);
|
||||||
|
ELSE
|
||||||
|
RAISE EXCEPTION '✗ Query promedio falló';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- TEST 12: Query típica - Buscar por defecto
|
||||||
|
-- ============================================
|
||||||
|
\echo '[TEST 12] Probando query: Buscar evaluaciones con defecto Fenólico...'
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
defecto_count int;
|
||||||
|
BEGIN
|
||||||
|
SELECT COUNT(*) INTO defecto_count
|
||||||
|
FROM evaluacion
|
||||||
|
WHERE defecto = 'Fenólico';
|
||||||
|
|
||||||
|
IF defecto_count > 0 THEN
|
||||||
|
RAISE NOTICE '✓ Query por defecto funciona. Evaluaciones con defecto Fenólico: %', defecto_count;
|
||||||
|
ELSE
|
||||||
|
RAISE WARNING '⚠ No se encontraron evaluaciones con defecto Fenólico (puede ser normal)';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- TEST 13: Query típica - Buscar por taza defectuosa específica
|
||||||
|
-- ============================================
|
||||||
|
\echo '[TEST 13] Probando query: Buscar evaluaciones donde taza 5 fue defectuosa...'
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
taza_count int;
|
||||||
|
BEGIN
|
||||||
|
SELECT COUNT(*) INTO taza_count
|
||||||
|
FROM evaluacion
|
||||||
|
WHERE tazas_defectuosas @> ARRAY[5]::smallint[];
|
||||||
|
|
||||||
|
IF taza_count > 0 THEN
|
||||||
|
RAISE NOTICE '✓ Query con array @> funciona. Evaluaciones con taza 5 defectuosa: %', taza_count;
|
||||||
|
ELSE
|
||||||
|
RAISE WARNING '⚠ No se encontraron evaluaciones con taza 5 defectuosa';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- TEST 14: Query típica - Top muestras por puntaje
|
||||||
|
-- ============================================
|
||||||
|
\echo '[TEST 14] Probando query: Top 3 muestras por puntaje final...'
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
top_muestra text;
|
||||||
|
top_puntaje int;
|
||||||
|
BEGIN
|
||||||
|
SELECT m.codigo, e.puntaje_final
|
||||||
|
INTO top_muestra, top_puntaje
|
||||||
|
FROM muestra m
|
||||||
|
JOIN evaluacion e ON e.muestra_id = m.id
|
||||||
|
JOIN sesion_participante sp ON sp.id = e.sesion_participante_id
|
||||||
|
WHERE m.sesion_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
|
||||||
|
ORDER BY e.puntaje_final DESC
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF top_muestra IS NOT NULL THEN
|
||||||
|
RAISE NOTICE '✓ Query top muestras funciona. Mejor muestra: % (puntaje: %)', top_muestra, top_puntaje;
|
||||||
|
ELSE
|
||||||
|
RAISE EXCEPTION '✗ Query top muestras falló';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- TEST 15: Función auxiliar - get_promedio_parametro_afectivo
|
||||||
|
-- ============================================
|
||||||
|
\echo '[TEST 15] Probando función: get_promedio_parametro_afectivo...'
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
avg_acidez numeric;
|
||||||
|
BEGIN
|
||||||
|
SELECT get_promedio_parametro_afectivo(
|
||||||
|
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
|
||||||
|
'acidez'
|
||||||
|
) INTO avg_acidez;
|
||||||
|
|
||||||
|
IF avg_acidez IS NOT NULL THEN
|
||||||
|
RAISE NOTICE '✓ Función get_promedio_parametro_afectivo funciona. Acidez promedio: %', ROUND(avg_acidez, 2);
|
||||||
|
ELSE
|
||||||
|
RAISE EXCEPTION '✗ Función get_promedio_parametro_afectivo falló';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- TEST 16: Función auxiliar - get_top_muestras
|
||||||
|
-- ============================================
|
||||||
|
\echo '[TEST 16] Probando función: get_top_muestras...'
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
resultado record;
|
||||||
|
count_resultados int := 0;
|
||||||
|
BEGIN
|
||||||
|
FOR resultado IN
|
||||||
|
SELECT * FROM get_top_muestras('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 3)
|
||||||
|
LOOP
|
||||||
|
count_resultados := count_resultados + 1;
|
||||||
|
RAISE NOTICE ' - %: % puntos (catador: %)', resultado.muestra_codigo, resultado.puntaje_final, resultado.catador_nombre;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
IF count_resultados > 0 THEN
|
||||||
|
RAISE NOTICE '✓ Función get_top_muestras funciona. Resultados: %', count_resultados;
|
||||||
|
ELSE
|
||||||
|
RAISE EXCEPTION '✗ Función get_top_muestras no devolvió resultados';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- TEST 17: Query con índice funcional - filtrar por acidez afectiva
|
||||||
|
-- ============================================
|
||||||
|
\echo '[TEST 17] Probando query con índice funcional: acidez afectiva >= 8...'
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
count_acidez int;
|
||||||
|
BEGIN
|
||||||
|
SELECT COUNT(*) INTO count_acidez
|
||||||
|
FROM evaluacion
|
||||||
|
WHERE ((intensidades->'acidez'->>'afectiva')::int) >= 8;
|
||||||
|
|
||||||
|
IF count_acidez >= 0 THEN
|
||||||
|
RAISE NOTICE '✓ Query con índice funcional funciona. Evaluaciones con acidez >= 8: %', count_acidez;
|
||||||
|
ELSE
|
||||||
|
RAISE EXCEPTION '✗ Query con índice funcional falló';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- TEST 18: Validar datos de prueba cargados
|
||||||
|
-- ============================================
|
||||||
|
\echo '[TEST 18] Validando que los datos de prueba se cargaron correctamente...'
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
count_users int;
|
||||||
|
count_sesiones int;
|
||||||
|
count_muestras int;
|
||||||
|
count_evaluaciones int;
|
||||||
|
BEGIN
|
||||||
|
SELECT COUNT(*) INTO count_users FROM auth.users;
|
||||||
|
SELECT COUNT(*) INTO count_sesiones FROM sesion;
|
||||||
|
SELECT COUNT(*) INTO count_muestras FROM muestra;
|
||||||
|
SELECT COUNT(*) INTO count_evaluaciones FROM evaluacion;
|
||||||
|
|
||||||
|
IF count_users >= 3 AND count_sesiones >= 1 AND count_muestras >= 3 AND count_evaluaciones >= 5 THEN
|
||||||
|
RAISE NOTICE '✓ Datos de prueba cargados:';
|
||||||
|
RAISE NOTICE ' - Usuarios: %', count_users;
|
||||||
|
RAISE NOTICE ' - Sesiones: %', count_sesiones;
|
||||||
|
RAISE NOTICE ' - Muestras: %', count_muestras;
|
||||||
|
RAISE NOTICE ' - Evaluaciones: %', count_evaluaciones;
|
||||||
|
ELSE
|
||||||
|
RAISE WARNING '⚠ Algunos datos de prueba pueden faltar:';
|
||||||
|
RAISE WARNING ' - Usuarios: % (esperado: >= 3)', count_users;
|
||||||
|
RAISE WARNING ' - Sesiones: % (esperado: >= 1)', count_sesiones;
|
||||||
|
RAISE WARNING ' - Muestras: % (esperado: >= 3)', count_muestras;
|
||||||
|
RAISE WARNING ' - Evaluaciones: % (esperado: >= 5)', count_evaluaciones;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
\echo ''
|
||||||
|
\echo '=========================================='
|
||||||
|
\echo 'Tests completados'
|
||||||
|
\echo '=========================================='
|
||||||
|
\echo ''
|
||||||
|
\echo 'Ejecuta queries de ejemplo con:'
|
||||||
|
\echo ' \i postgres/tests/example_queries.sql'
|
||||||
|
\echo ''
|
||||||
253
scripts/README.md
Normal file
253
scripts/README.md
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
# Scripts de rioCata
|
||||||
|
|
||||||
|
Esta carpeta contiene scripts helper para facilitar la gestión de la base de datos y servicios de rioCata.
|
||||||
|
|
||||||
|
## Contenido
|
||||||
|
|
||||||
|
- **riocata.sh** - Script principal de gestión y administración
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## riocata.sh
|
||||||
|
|
||||||
|
Script bash para gestionar servicios de Docker Compose y operaciones comunes de la base de datos.
|
||||||
|
|
||||||
|
### Requisitos
|
||||||
|
|
||||||
|
- Docker y Docker Compose instalados
|
||||||
|
- Bash 4.0 o superior
|
||||||
|
- Permisos de ejecución (`chmod +x riocata.sh`)
|
||||||
|
|
||||||
|
### Uso
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./riocata.sh [comando]
|
||||||
|
```
|
||||||
|
|
||||||
|
O desde el directorio raíz del proyecto:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/riocata.sh [comando]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comandos Disponibles
|
||||||
|
|
||||||
|
#### Gestión de Servicios
|
||||||
|
|
||||||
|
**`start`** - Iniciar todos los servicios
|
||||||
|
```bash
|
||||||
|
./riocata.sh start
|
||||||
|
```
|
||||||
|
Levanta los contenedores de Docker Compose y espera a que PostgreSQL esté listo. Muestra el estado de los servicios al finalizar.
|
||||||
|
|
||||||
|
**`stop`** - Detener todos los servicios
|
||||||
|
```bash
|
||||||
|
./riocata.sh stop
|
||||||
|
```
|
||||||
|
Detiene los contenedores sin eliminar los volúmenes. Los datos se preservan.
|
||||||
|
|
||||||
|
**`restart`** - Reiniciar todos los servicios
|
||||||
|
```bash
|
||||||
|
./riocata.sh restart
|
||||||
|
```
|
||||||
|
Reinicia los contenedores manteniendo los datos. Útil después de cambios en configuración.
|
||||||
|
|
||||||
|
**`reset`** - Reiniciar servicios y borrar TODOS los datos
|
||||||
|
```bash
|
||||||
|
./riocata.sh reset
|
||||||
|
```
|
||||||
|
⚠️ **ADVERTENCIA**: Este comando elimina todos los volúmenes de Docker, borrando completamente la base de datos. Los scripts de inicialización se ejecutarán de nuevo, restaurando los datos de prueba.
|
||||||
|
|
||||||
|
Requiere confirmación explícita escribiendo `SI`.
|
||||||
|
|
||||||
|
**`status`** - Ver estado de servicios
|
||||||
|
```bash
|
||||||
|
./riocata.sh status
|
||||||
|
```
|
||||||
|
Muestra el estado actual de todos los contenedores.
|
||||||
|
|
||||||
|
**`logs`** - Ver logs de PostgreSQL en tiempo real
|
||||||
|
```bash
|
||||||
|
./riocata.sh logs
|
||||||
|
```
|
||||||
|
Muestra los logs de PostgreSQL en modo follow. Presiona `Ctrl+C` para salir.
|
||||||
|
|
||||||
|
#### Base de Datos
|
||||||
|
|
||||||
|
**`psql`** - Conectarse a PostgreSQL
|
||||||
|
```bash
|
||||||
|
./riocata.sh psql
|
||||||
|
```
|
||||||
|
Abre una sesión interactiva de psql conectada a la base de datos rioCata.
|
||||||
|
|
||||||
|
Comandos útiles dentro de psql:
|
||||||
|
- `\dt` - Listar tablas
|
||||||
|
- `\d nombre_tabla` - Describir estructura de una tabla
|
||||||
|
- `\df` - Listar funciones
|
||||||
|
- `\q` - Salir
|
||||||
|
|
||||||
|
**`test`** - Ejecutar suite de tests
|
||||||
|
```bash
|
||||||
|
./riocata.sh test
|
||||||
|
```
|
||||||
|
Ejecuta el archivo `postgres/tests/test_all.sql` que contiene 18 tests de validación:
|
||||||
|
- Existencia de tablas y tipos
|
||||||
|
- Funcionamiento de triggers
|
||||||
|
- Validación de constraints
|
||||||
|
- Queries típicas
|
||||||
|
- Funciones auxiliares
|
||||||
|
|
||||||
|
**`queries`** - Ejecutar queries de ejemplo
|
||||||
|
```bash
|
||||||
|
./riocata.sh queries
|
||||||
|
```
|
||||||
|
Ejecuta el archivo `postgres/tests/example_queries.sql` que contiene 17 queries de ejemplo mostrando:
|
||||||
|
- Sesiones, muestras y evaluaciones
|
||||||
|
- Top evaluaciones por puntaje
|
||||||
|
- Evaluaciones con defectos
|
||||||
|
- Análisis de notas y sabores
|
||||||
|
- Estadísticas por catador
|
||||||
|
|
||||||
|
#### Backup y Restauración
|
||||||
|
|
||||||
|
**`backup`** - Crear backup de la base de datos
|
||||||
|
```bash
|
||||||
|
./riocata.sh backup
|
||||||
|
```
|
||||||
|
Crea un archivo de backup con formato `backup_YYYYMMDD_HHMMSS.sql` en el directorio actual usando `pg_dump`.
|
||||||
|
|
||||||
|
Ejemplo:
|
||||||
|
```bash
|
||||||
|
./riocata.sh backup
|
||||||
|
# Crea: backup_20251017_143022.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
**`restore <archivo>`** - Restaurar backup
|
||||||
|
```bash
|
||||||
|
./riocata.sh restore backup_20251017_143022.sql
|
||||||
|
```
|
||||||
|
Restaura la base de datos desde un archivo de backup SQL.
|
||||||
|
|
||||||
|
⚠️ **Nota**: La restauración no elimina datos existentes. Si necesitas una restauración limpia, primero ejecuta `reset` y luego `restore`.
|
||||||
|
|
||||||
|
#### Ayuda
|
||||||
|
|
||||||
|
**`help`** - Mostrar ayuda
|
||||||
|
```bash
|
||||||
|
./riocata.sh help
|
||||||
|
```
|
||||||
|
Muestra la lista de comandos disponibles.
|
||||||
|
|
||||||
|
### Ejemplos de Uso
|
||||||
|
|
||||||
|
#### Iniciar el proyecto por primera vez
|
||||||
|
```bash
|
||||||
|
./riocata.sh start
|
||||||
|
./riocata.sh test # Verificar que todo funciona
|
||||||
|
./riocata.sh queries # Ver datos de ejemplo
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Desarrollo diario
|
||||||
|
```bash
|
||||||
|
# Iniciar
|
||||||
|
./riocata.sh start
|
||||||
|
|
||||||
|
# Trabajar con la base de datos
|
||||||
|
./riocata.sh psql
|
||||||
|
|
||||||
|
# Ver logs si hay problemas
|
||||||
|
./riocata.sh logs
|
||||||
|
|
||||||
|
# Detener al terminar
|
||||||
|
./riocata.sh stop
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Crear backup antes de cambios importantes
|
||||||
|
```bash
|
||||||
|
./riocata.sh backup
|
||||||
|
# Hacer cambios...
|
||||||
|
# Si algo sale mal:
|
||||||
|
./riocata.sh restore backup_20251017_143022.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Reiniciar completamente el proyecto
|
||||||
|
```bash
|
||||||
|
./riocata.sh reset
|
||||||
|
# Escribir 'SI' cuando lo solicite
|
||||||
|
./riocata.sh test # Verificar
|
||||||
|
```
|
||||||
|
|
||||||
|
### Variables de Configuración
|
||||||
|
|
||||||
|
El script utiliza las siguientes variables configurables al inicio del archivo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
COMPOSE="docker-compose" # Comando de docker-compose
|
||||||
|
DB_USER="riocata_user" # Usuario de PostgreSQL
|
||||||
|
DB_NAME="riocata" # Nombre de la base de datos
|
||||||
|
```
|
||||||
|
|
||||||
|
Si necesitas cambiar las credenciales o nombres, edita estas variables en el script.
|
||||||
|
|
||||||
|
### Códigos de Salida
|
||||||
|
|
||||||
|
- `0` - Ejecución exitosa
|
||||||
|
- `1` - Error (comando desconocido, archivo no encontrado, etc.)
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
**Error: "docker-compose: command not found"**
|
||||||
|
- Solución: Instala Docker Compose o cambia la variable `COMPOSE` a `docker compose` (sin guión)
|
||||||
|
|
||||||
|
**Error al conectar con psql**
|
||||||
|
- Verifica que los servicios estén corriendo: `./riocata.sh status`
|
||||||
|
- Revisa los logs: `./riocata.sh logs`
|
||||||
|
- Intenta reiniciar: `./riocata.sh restart`
|
||||||
|
|
||||||
|
**Los tests fallan**
|
||||||
|
- Si es la primera vez, puede ser que PostgreSQL aún no haya terminado de inicializar
|
||||||
|
- Espera unos segundos y vuelve a ejecutar: `./riocata.sh test`
|
||||||
|
- Si persiste, revisa los logs: `./riocata.sh logs`
|
||||||
|
|
||||||
|
**El backup falla**
|
||||||
|
- Verifica que los servicios estén corriendo: `./riocata.sh status`
|
||||||
|
- Verifica que tengas permisos de escritura en el directorio actual
|
||||||
|
|
||||||
|
### Personalización
|
||||||
|
|
||||||
|
Puedes agregar tus propios comandos al script editando el `case` statement al final del archivo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
case "${1:-help}" in
|
||||||
|
mi_comando)
|
||||||
|
mi_funcion
|
||||||
|
;;
|
||||||
|
# ... otros comandos
|
||||||
|
esac
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contribuir
|
||||||
|
|
||||||
|
Si agregas nuevos scripts útiles a esta carpeta:
|
||||||
|
|
||||||
|
1. Hazlos ejecutables: `chmod +x nombre_script.sh`
|
||||||
|
2. Documéntalos en este README
|
||||||
|
3. Usa las mismas variables de configuración que `riocata.sh`
|
||||||
|
4. Sigue el mismo formato de colores y mensajes
|
||||||
|
|
||||||
|
### Notas de Seguridad
|
||||||
|
|
||||||
|
- ⚠️ No uses estos scripts en producción sin revisar las credenciales
|
||||||
|
- ⚠️ Los backups NO están encriptados
|
||||||
|
- ⚠️ El comando `reset` es destructivo y no se puede deshacer
|
||||||
|
- ⚠️ Almacena los backups en un lugar seguro fuera del repositorio
|
||||||
|
|
||||||
|
### Información Adicional
|
||||||
|
|
||||||
|
- Las credenciales por defecto están en `docker-compose.yml`
|
||||||
|
- Los scripts SQL están en `postgres/init/` y `postgres/tests/`
|
||||||
|
- La documentación general está en `README.md` en el directorio raíz
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Desarrollado para Nucleo Rio Frio**
|
||||||
167
scripts/riocata.sh
Executable file
167
scripts/riocata.sh
Executable file
@@ -0,0 +1,167 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# rioCata - Script de ayuda para gestión de la base de datos
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
COMPOSE="docker-compose"
|
||||||
|
DB_USER="riocata_user"
|
||||||
|
DB_NAME="riocata"
|
||||||
|
|
||||||
|
# Colores
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
function show_help() {
|
||||||
|
echo "rioCata - Sistema de Catación de Café"
|
||||||
|
echo ""
|
||||||
|
echo "Uso: ./riocata.sh [comando]"
|
||||||
|
echo ""
|
||||||
|
echo "Comandos disponibles:"
|
||||||
|
echo " start - Iniciar servicios"
|
||||||
|
echo " stop - Detener servicios"
|
||||||
|
echo " restart - Reiniciar servicios"
|
||||||
|
echo " reset - Reiniciar servicios y borrar datos (¡CUIDADO!)"
|
||||||
|
echo " psql - Conectarse a PostgreSQL con psql"
|
||||||
|
echo " test - Ejecutar tests de validación"
|
||||||
|
echo " queries - Ejecutar queries de ejemplo"
|
||||||
|
echo " logs - Ver logs de PostgreSQL"
|
||||||
|
echo " status - Ver estado de servicios"
|
||||||
|
echo " backup - Crear backup de la base de datos"
|
||||||
|
echo " restore FILE - Restaurar backup desde archivo"
|
||||||
|
echo " help - Mostrar esta ayuda"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function start_services() {
|
||||||
|
echo -e "${GREEN}Iniciando servicios...${NC}"
|
||||||
|
$COMPOSE up -d
|
||||||
|
echo -e "${GREEN}Esperando que PostgreSQL esté listo...${NC}"
|
||||||
|
sleep 3
|
||||||
|
$COMPOSE ps
|
||||||
|
}
|
||||||
|
|
||||||
|
function stop_services() {
|
||||||
|
echo -e "${YELLOW}Deteniendo servicios...${NC}"
|
||||||
|
$COMPOSE down
|
||||||
|
}
|
||||||
|
|
||||||
|
function restart_services() {
|
||||||
|
echo -e "${YELLOW}Reiniciando servicios...${NC}"
|
||||||
|
$COMPOSE restart
|
||||||
|
sleep 3
|
||||||
|
$COMPOSE ps
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset_all() {
|
||||||
|
echo -e "${RED}¡ADVERTENCIA! Esto borrará TODOS los datos.${NC}"
|
||||||
|
read -p "¿Estás seguro? (escribe 'SI' para confirmar): " confirm
|
||||||
|
if [ "$confirm" = "SI" ]; then
|
||||||
|
echo -e "${RED}Eliminando servicios y datos...${NC}"
|
||||||
|
$COMPOSE down -v
|
||||||
|
echo -e "${GREEN}Reiniciando servicios...${NC}"
|
||||||
|
$COMPOSE up -d
|
||||||
|
sleep 5
|
||||||
|
echo -e "${GREEN}Base de datos reinicializada con datos de prueba.${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}Operación cancelada.${NC}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect_psql() {
|
||||||
|
echo -e "${GREEN}Conectando a PostgreSQL...${NC}"
|
||||||
|
echo -e "${YELLOW}Tip: usa \\q para salir${NC}"
|
||||||
|
$COMPOSE exec postgres psql -U $DB_USER -d $DB_NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
function run_tests() {
|
||||||
|
echo -e "${GREEN}Ejecutando tests de validación...${NC}"
|
||||||
|
$COMPOSE exec -T postgres psql -U $DB_USER -d $DB_NAME < postgres/tests/test_all.sql
|
||||||
|
}
|
||||||
|
|
||||||
|
function run_queries() {
|
||||||
|
echo -e "${GREEN}Ejecutando queries de ejemplo...${NC}"
|
||||||
|
$COMPOSE exec -T postgres psql -U $DB_USER -d $DB_NAME < postgres/tests/example_queries.sql
|
||||||
|
}
|
||||||
|
|
||||||
|
function show_logs() {
|
||||||
|
echo -e "${GREEN}Mostrando logs de PostgreSQL...${NC}"
|
||||||
|
$COMPOSE logs -f postgres
|
||||||
|
}
|
||||||
|
|
||||||
|
function show_status() {
|
||||||
|
echo -e "${GREEN}Estado de servicios:${NC}"
|
||||||
|
$COMPOSE ps
|
||||||
|
}
|
||||||
|
|
||||||
|
function create_backup() {
|
||||||
|
BACKUP_FILE="backup_$(date +%Y%m%d_%H%M%S).sql"
|
||||||
|
echo -e "${GREEN}Creando backup en $BACKUP_FILE...${NC}"
|
||||||
|
$COMPOSE exec -T postgres pg_dump -U $DB_USER $DB_NAME > $BACKUP_FILE
|
||||||
|
echo -e "${GREEN}Backup creado exitosamente: $BACKUP_FILE${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function restore_backup() {
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo -e "${RED}Error: Debes especificar el archivo de backup${NC}"
|
||||||
|
echo "Uso: ./riocata.sh restore <archivo.sql>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$1" ]; then
|
||||||
|
echo -e "${RED}Error: El archivo $1 no existe${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${YELLOW}Restaurando backup desde $1...${NC}"
|
||||||
|
$COMPOSE exec -T postgres psql -U $DB_USER -d $DB_NAME < "$1"
|
||||||
|
echo -e "${GREEN}Backup restaurado exitosamente${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main
|
||||||
|
case "${1:-help}" in
|
||||||
|
start)
|
||||||
|
start_services
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
stop_services
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
restart_services
|
||||||
|
;;
|
||||||
|
reset)
|
||||||
|
reset_all
|
||||||
|
;;
|
||||||
|
psql)
|
||||||
|
connect_psql
|
||||||
|
;;
|
||||||
|
test)
|
||||||
|
run_tests
|
||||||
|
;;
|
||||||
|
queries)
|
||||||
|
run_queries
|
||||||
|
;;
|
||||||
|
logs)
|
||||||
|
show_logs
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
show_status
|
||||||
|
;;
|
||||||
|
backup)
|
||||||
|
create_backup
|
||||||
|
;;
|
||||||
|
restore)
|
||||||
|
restore_backup "$2"
|
||||||
|
;;
|
||||||
|
help|--help|-h)
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}Comando desconocido: $1${NC}"
|
||||||
|
echo ""
|
||||||
|
show_help
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user