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