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:
2025-10-17 17:00:48 -06:00
commit f682c3db51
11 changed files with 2166 additions and 0 deletions

29
.gitignore vendored Normal file
View 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
View 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
View 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
View 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)';

View 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';

View 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';

View 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.'
);

View 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
View 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
View 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
View 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