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

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