Primer commit - Sprint 1
This commit is contained in:
135
supabase/migrations/20260531000001_create_tables.sql
Normal file
135
supabase/migrations/20260531000001_create_tables.sql
Normal file
@@ -0,0 +1,135 @@
|
||||
-- ============================================================
|
||||
-- Migration 001: Create base tables
|
||||
-- Contract: DATA_MODEL.md — do not add tables without approval (Nivel 3)
|
||||
-- Dependency order: organizations, consent_versions → surveys → questions
|
||||
-- → consent_records → responses → answers
|
||||
-- ============================================================
|
||||
|
||||
-- organizations: empresas cliente
|
||||
CREATE TABLE IF NOT EXISTS public.organizations (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name text NOT NULL,
|
||||
sector text,
|
||||
size_bucket text,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- consent_versions: textos de consentimiento versionados (INMUTABLES — ver migration 004)
|
||||
CREATE TABLE IF NOT EXISTS public.consent_versions (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
version text NOT NULL UNIQUE,
|
||||
body text NOT NULL,
|
||||
scopes jsonb NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- surveys: definición de cada estudio
|
||||
-- Dos ejes de clasificación ortogonales (ADR-001 §2):
|
||||
-- data_sensitivity → consentimiento / seguridad legal
|
||||
-- security_class → RLS / cifrado (ORTOGONAL al anterior)
|
||||
CREATE TABLE IF NOT EXISTS public.surveys (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
org_id uuid NOT NULL REFERENCES public.organizations(id),
|
||||
title text NOT NULL,
|
||||
status text NOT NULL DEFAULT 'draft'
|
||||
CHECK (status IN ('draft', 'live', 'closed')),
|
||||
-- Eje 1: anonimato — irreversible una vez publicada
|
||||
is_anonymous boolean NOT NULL,
|
||||
-- Eje 2a: sensibilidad legal → alimenta consentimiento
|
||||
data_sensitivity text NOT NULL
|
||||
CHECK (data_sensitivity IN ('comun', 'sensible', 'anonimizado')),
|
||||
-- Eje 2b: seguridad → alimenta RLS/cifrado (ORTOGONAL al anterior)
|
||||
security_class text NOT NULL
|
||||
CHECK (security_class IN ('publico', 'interno', 'confidencial')),
|
||||
-- Rol de responsable (Modo A = nosotros; Modo B = empresa. v1 solo usa A)
|
||||
controller_role text NOT NULL DEFAULT 'A'
|
||||
CHECK (controller_role IN ('A', 'B')),
|
||||
consent_version_id uuid REFERENCES public.consent_versions(id),
|
||||
brand_config jsonb,
|
||||
-- Umbral de k-anonimato mandatorio (regla no negociable #3 CLAUDE.md)
|
||||
k_threshold int NOT NULL DEFAULT 5 CHECK (k_threshold > 0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
published_at timestamptz
|
||||
);
|
||||
|
||||
-- questions: preguntas de la encuesta
|
||||
-- is_sensitive: auto-true para tipos salud/discapacidad (ver trigger en migration 004)
|
||||
CREATE TABLE IF NOT EXISTS public.questions (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
survey_id uuid NOT NULL REFERENCES public.surveys(id) ON DELETE CASCADE,
|
||||
code text NOT NULL,
|
||||
type text NOT NULL
|
||||
CHECK (type IN (
|
||||
'text_short', 'text_long',
|
||||
'single_choice', 'multiple_choice',
|
||||
'rating', 'nps', 'matrix', 'ranking',
|
||||
'date', 'slider', 'yes_no', 'section'
|
||||
)),
|
||||
prompt text NOT NULL,
|
||||
position int NOT NULL,
|
||||
is_sensitive boolean NOT NULL DEFAULT false,
|
||||
max_select int,
|
||||
options jsonb,
|
||||
-- Formato: [{ "if_question": "CODE", "op": "eq", "value": "yes", "action": "show|hide|skip", "target": "CODE" }]
|
||||
conditional_rules jsonb,
|
||||
required boolean NOT NULL DEFAULT false,
|
||||
UNIQUE (survey_id, code),
|
||||
UNIQUE (survey_id, position)
|
||||
);
|
||||
|
||||
-- consent_records: qué consintió cada respuesta (INMUTABLE — ver trigger en migration 004)
|
||||
-- Sin UPDATE/DELETE — se inserta una vez junto con cada response
|
||||
CREATE TABLE IF NOT EXISTS public.consent_records (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
consent_version_id uuid NOT NULL REFERENCES public.consent_versions(id),
|
||||
granted_scopes jsonb NOT NULL,
|
||||
granted_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- responses: una submission
|
||||
-- respondent_id RESERVADO y NULL en encuestas anónimas (regla no negociable #2 CLAUDE.md)
|
||||
-- Trigger en migration 004 rechaza respondent_id no nulo cuando is_anonymous=true
|
||||
-- Cuasi-identificadores en bucket — minimización: rango etario, no fecha exacta
|
||||
CREATE TABLE IF NOT EXISTS public.responses (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
survey_id uuid NOT NULL REFERENCES public.surveys(id),
|
||||
company_id uuid NOT NULL REFERENCES public.organizations(id),
|
||||
respondent_id uuid, -- RESERVADO, NULL en v1 (encuestas anónimas)
|
||||
ip_hash text, -- SHA-256 + salt, dedup sin identidad
|
||||
consent_record_id uuid NOT NULL REFERENCES public.consent_records(id),
|
||||
submitted_at timestamptz NOT NULL DEFAULT now(),
|
||||
-- Cuasi-identificadores denormalizados para segmentación eficiente
|
||||
qi_age_bucket text,
|
||||
qi_sector text,
|
||||
qi_zone text
|
||||
);
|
||||
|
||||
-- answers: valor de cada pregunta en una submission
|
||||
-- El texto libre (text_long) NO se expone en agg_segment (regla no negociable #4 CLAUDE.md)
|
||||
CREATE TABLE IF NOT EXISTS public.answers (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
response_id uuid NOT NULL REFERENCES public.responses(id) ON DELETE CASCADE,
|
||||
question_id uuid NOT NULL REFERENCES public.questions(id),
|
||||
value jsonb NOT NULL,
|
||||
UNIQUE (response_id, question_id)
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- Indexes
|
||||
-- ============================================================
|
||||
CREATE INDEX IF NOT EXISTS idx_surveys_org_id ON public.surveys(org_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_surveys_status ON public.surveys(status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_questions_survey ON public.questions(survey_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_questions_position ON public.questions(survey_id, position);
|
||||
|
||||
-- Índice compuesto para agg_segment: filtro por encuesta y agrupación por QIs
|
||||
CREATE INDEX IF NOT EXISTS idx_responses_survey ON public.responses(survey_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_responses_company ON public.responses(company_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_responses_ip_hash ON public.responses(ip_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_responses_qi_seg ON public.responses(survey_id, qi_age_bucket, qi_sector, qi_zone);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_answers_response ON public.answers(response_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_answers_question ON public.answers(question_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_consent_rec_cv ON public.consent_records(consent_version_id);
|
||||
Reference in New Issue
Block a user