-- 002_src_tables.sql: таблицы слоя src (порядок — FK-зависимости). -- Идемпотентно (IF NOT EXISTS); состав — 1-docs\2-db\src\tables.md. CREATE TABLE IF NOT EXISTS src.athlete ( id serial PRIMARY KEY, name text NOT NULL, api_key_hash text NOT NULL UNIQUE, is_blocked boolean NOT NULL DEFAULT false, created_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS src.athlete_profile ( id serial PRIMARY KEY, athlete_id int NOT NULL UNIQUE REFERENCES src.athlete(id), sex text CHECK (sex IN ('m', 'f')), birth_year int, weight_kg numeric(5,1), hr_max int, updated_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS src.exercise ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), athlete_id int NOT NULL REFERENCES src.athlete(id), name text NOT NULL, group_id int NOT NULL REFERENCES ref.exercise_group(id), is_base boolean NOT NULL DEFAULT true, is_active boolean NOT NULL DEFAULT true, UNIQUE (athlete_id, name) ); CREATE TABLE IF NOT EXISTS src.exercise_replacement ( exercise_id uuid NOT NULL REFERENCES src.exercise(id), replacement_id uuid NOT NULL REFERENCES src.exercise(id), CHECK (exercise_id <> replacement_id), PRIMARY KEY (exercise_id, replacement_id) ); CREATE TABLE IF NOT EXISTS src.program ( id serial PRIMARY KEY, athlete_id int NOT NULL UNIQUE REFERENCES src.athlete(id), title text NOT NULL, created_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS src.program_version ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), program_id int NOT NULL REFERENCES src.program(id), version int NOT NULL, based_on_version int, status text NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'active', 'archived')), author text NOT NULL CHECK (author IN ('user', 'admin', 'rules', 'llm')), body jsonb NOT NULL, rationale text, created_at timestamptz NOT NULL DEFAULT now(), UNIQUE (program_id, version) ); -- 🛡 активная версия — ровно одна на программу CREATE UNIQUE INDEX IF NOT EXISTS uq_program_version_active ON src.program_version (program_id) WHERE status = 'active'; CREATE TABLE IF NOT EXISTS src.workout ( id uuid PRIMARY KEY, athlete_id int NOT NULL REFERENCES src.athlete(id), program_version_id uuid REFERENCES src.program_version(id), started_at timestamptz NOT NULL, ended_at timestamptz, status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'finished', 'cancelled')), session_kind text NOT NULL DEFAULT 'normal' CHECK (session_kind IN ('normal', 'return', 'deload', 'test')), avg_hr int, facts_json jsonb, fit_file_id text, hr_recovery_s int, UNIQUE (athlete_id, fit_file_id) ); CREATE TABLE IF NOT EXISTS src.workout_set ( id uuid PRIMARY KEY, workout_id uuid NOT NULL REFERENCES src.workout(id), athlete_id int NOT NULL REFERENCES src.athlete(id), exercise_id uuid REFERENCES src.exercise(id), seq int NOT NULL, weight_kg numeric(6,2) NOT NULL, reps int NOT NULL, done_at timestamptz NOT NULL, source text NOT NULL CHECK (source IN ('voice', 'watch', 'edit')), in_plan boolean NOT NULL, rpe int CHECK (rpe BETWEEN 1 AND 10), knee_pain int CHECK (knee_pain BETWEEN 0 AND 10) ); CREATE TABLE IF NOT EXISTS src.command_log ( id uuid PRIMARY KEY, workout_id uuid NOT NULL REFERENCES src.workout(id), t_offset_s int NOT NULL, recognized_text text NOT NULL, semantics jsonb NOT NULL, accepted boolean NOT NULL ); CREATE TABLE IF NOT EXISTS src.daily_log ( id uuid PRIMARY KEY, athlete_id int NOT NULL REFERENCES src.athlete(id), date date NOT NULL, bp_sys int, bp_dias int, weight_kg numeric(5,1), hr_resting int, waist_cm numeric(5,1), sleep_h numeric(3,1), wellbeing int CHECK (wellbeing BETWEEN 1 AND 5), UNIQUE (athlete_id, date) ); CREATE TABLE IF NOT EXISTS src.workout_metrics ( id serial PRIMARY KEY, workout_id uuid NOT NULL UNIQUE REFERENCES src.workout(id), athlete_id int NOT NULL REFERENCES src.athlete(id), adherence numeric(4,1), tonnage numeric(9,1), tonnage_by_group jsonb, e1pm_by_exercise jsonb, hr_metrics jsonb, red_flags jsonb, computed_at timestamptz NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS src.breaks ( id uuid PRIMARY KEY, athlete_id int NOT NULL REFERENCES src.athlete(id), kind text CHECK (kind IN ('illness', 'vacation', 'workload', 'injury', 'other')), source text NOT NULL DEFAULT 'auto' CHECK (source IN ('auto', 'manual')), started_on date NOT NULL, planned_return date, ended_on date, note text ); CREATE TABLE IF NOT EXISTS src.migration ( id serial PRIMARY KEY, filename text NOT NULL UNIQUE, applied_at timestamptz NOT NULL DEFAULT now() );