From f682c3db5109337439b223205c9af3be4848b3aa Mon Sep 17 00:00:00 2001 From: josedario87 Date: Fri, 17 Oct 2025 17:00:48 -0600 Subject: [PATCH] =?UTF-8?q?Inicializar=20rioCata=20-=20Sistema=20de=20Cata?= =?UTF-8?q?ci=C3=B3n=20de=20Caf=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .gitignore | 29 ++ README.md | 347 ++++++++++++++++++++ docker-compose.yml | 32 ++ postgres/init/01_schema.sql | 123 +++++++ postgres/init/02_functions.sql | 105 ++++++ postgres/init/03_indexes.sql | 77 +++++ postgres/init/04_sample_data.sql | 244 ++++++++++++++ postgres/tests/example_queries.sql | 296 +++++++++++++++++ postgres/tests/test_all.sql | 493 +++++++++++++++++++++++++++++ scripts/README.md | 253 +++++++++++++++ scripts/riocata.sh | 167 ++++++++++ 11 files changed, 2166 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 postgres/init/01_schema.sql create mode 100644 postgres/init/02_functions.sql create mode 100644 postgres/init/03_indexes.sql create mode 100644 postgres/init/04_sample_data.sql create mode 100644 postgres/tests/example_queries.sql create mode 100644 postgres/tests/test_all.sql create mode 100644 scripts/README.md create mode 100755 scripts/riocata.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ac93dd --- /dev/null +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..c7029b8 --- /dev/null +++ b/README.md @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5f9421e --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/postgres/init/01_schema.sql b/postgres/init/01_schema.sql new file mode 100644 index 0000000..ea597e3 --- /dev/null +++ b/postgres/init/01_schema.sql @@ -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)'; diff --git a/postgres/init/02_functions.sql b/postgres/init/02_functions.sql new file mode 100644 index 0000000..ef5fda0 --- /dev/null +++ b/postgres/init/02_functions.sql @@ -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'; diff --git a/postgres/init/03_indexes.sql b/postgres/init/03_indexes.sql new file mode 100644 index 0000000..c454252 --- /dev/null +++ b/postgres/init/03_indexes.sql @@ -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'; diff --git a/postgres/init/04_sample_data.sql b/postgres/init/04_sample_data.sql new file mode 100644 index 0000000..973ef8f --- /dev/null +++ b/postgres/init/04_sample_data.sql @@ -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.' +); diff --git a/postgres/tests/example_queries.sql b/postgres/tests/example_queries.sql new file mode 100644 index 0000000..b4b68d9 --- /dev/null +++ b/postgres/tests/example_queries.sql @@ -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 '==========================================' diff --git a/postgres/tests/test_all.sql b/postgres/tests/test_all.sql new file mode 100644 index 0000000..6149b46 --- /dev/null +++ b/postgres/tests/test_all.sql @@ -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 '' diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..5f646d4 --- /dev/null +++ b/scripts/README.md @@ -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 `** - 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** diff --git a/scripts/riocata.sh b/scripts/riocata.sh new file mode 100755 index 0000000..f94ea80 --- /dev/null +++ b/scripts/riocata.sh @@ -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 " + 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