CREATE SCHEMA "health"; --> statement-breakpoint CREATE TABLE "health"."appointments" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "date" date NOT NULL, "type" text DEFAULT 'appointment' NOT NULL, "provider" text, "notes" text, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, CONSTRAINT "appointments_type_check" CHECK ("type" IN ('appointment','checkup','condition','test','procedure','other')), CONSTRAINT "appointments_date_check" CHECK ("date" >= DATE '1900-01-01') ); --> statement-breakpoint CREATE TABLE "health"."body_measurements" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "date" date NOT NULL, "weight_kg" numeric(5,2), "waist_cm" numeric(5,1), "body_fat_pct" numeric(4,1), "notes" text, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, CONSTRAINT "body_measurements_any_check" CHECK ("weight_kg" IS NOT NULL OR "waist_cm" IS NOT NULL OR "body_fat_pct" IS NOT NULL), CONSTRAINT "body_measurements_weight_check" CHECK ("weight_kg" IS NULL OR "weight_kg" BETWEEN 20 AND 400), CONSTRAINT "body_measurements_waist_check" CHECK ("waist_cm" IS NULL OR "waist_cm" BETWEEN 30 AND 250), CONSTRAINT "body_measurements_fat_check" CHECK ("body_fat_pct" IS NULL OR "body_fat_pct" BETWEEN 1 AND 75), CONSTRAINT "body_measurements_date_check" CHECK ("date" >= DATE '1900-01-01') ); --> statement-breakpoint CREATE TABLE "health"."hair_progress" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "date" date NOT NULL, "photo_ref" text, "notes" text, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, CONSTRAINT "hair_progress_photo_ref_check" CHECK ("photo_ref" IS NULL OR ("photo_ref" !~* '^[a-z][a-z0-9+.-]*://' AND "photo_ref" !~ '^//')), CONSTRAINT "hair_progress_date_check" CHECK ("date" >= DATE '1900-01-01') ); --> statement-breakpoint CREATE TABLE "health"."medications" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "name" text NOT NULL, "kind" text DEFAULT 'medication' NOT NULL, "dose" text, "frequency" text, "start_date" date, "stop_date" date, "notes" text, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, CONSTRAINT "medications_name_check" CHECK (length(btrim("name")) > 0), CONSTRAINT "medications_kind_check" CHECK ("kind" IN ('medication','supplement')), CONSTRAINT "medications_dates_check" CHECK ("stop_date" IS NULL OR "start_date" IS NULL OR "stop_date" >= "start_date") ); --> statement-breakpoint CREATE TABLE "health"."nutrition_daily" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "date" date NOT NULL CONSTRAINT "nutrition_daily_date_key" UNIQUE, "kcal" integer, "protein_g" numeric(5,1), "notes" text, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, CONSTRAINT "nutrition_daily_kcal_check" CHECK ("kcal" IS NULL OR "kcal" BETWEEN 0 AND 20000), CONSTRAINT "nutrition_daily_protein_check" CHECK ("protein_g" IS NULL OR "protein_g" BETWEEN 0 AND 1000), CONSTRAINT "nutrition_daily_date_check" CHECK ("date" >= DATE '1900-01-01') ); --> statement-breakpoint CREATE TABLE "health"."sleep" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "date" date NOT NULL CONSTRAINT "sleep_date_key" UNIQUE, "bedtime" timestamp with time zone NOT NULL, "wake_time" timestamp with time zone NOT NULL, "hours" numeric(4,2) GENERATED ALWAYS AS (round((extract(epoch from (wake_time - bedtime)) / 3600)::numeric, 2)) STORED, "notes" text, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, CONSTRAINT "sleep_order_check" CHECK ("wake_time" > "bedtime"), CONSTRAINT "sleep_length_check" CHECK ("wake_time" - "bedtime" <= interval '24 hours'), CONSTRAINT "sleep_date_check" CHECK ("date" >= DATE '1900-01-01') ); --> statement-breakpoint CREATE TABLE "health"."vaccinations" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "vaccine" text NOT NULL, "target" text, "dose_no" integer, "date" date NOT NULL, "batch_lot" text, "provider" text, "country" text, "notes" text, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, CONSTRAINT "vaccinations_vaccine_check" CHECK (length(btrim("vaccine")) > 0), CONSTRAINT "vaccinations_dose_no_check" CHECK ("dose_no" IS NULL OR "dose_no" BETWEEN 1 AND 20), CONSTRAINT "vaccinations_country_check" CHECK ("country" IS NULL OR "country" ~ '^[A-Z]{2}$'), CONSTRAINT "vaccinations_date_check" CHECK ("date" >= DATE '1900-01-01') ); --> statement-breakpoint CREATE TABLE "health"."workouts" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), "date" date NOT NULL, "session_id" uuid, "exercise" text NOT NULL, "sets" integer, "reps" integer, "load_kg" numeric(6,2), "rpe" numeric(3,1), "notes" text, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, CONSTRAINT "workouts_exercise_check" CHECK (length(btrim("exercise")) > 0), CONSTRAINT "workouts_sets_check" CHECK ("sets" IS NULL OR "sets" BETWEEN 1 AND 100), CONSTRAINT "workouts_reps_check" CHECK ("reps" IS NULL OR "reps" BETWEEN 0 AND 1000), CONSTRAINT "workouts_load_check" CHECK ("load_kg" IS NULL OR "load_kg" BETWEEN 0 AND 1000), CONSTRAINT "workouts_rpe_check" CHECK ("rpe" IS NULL OR "rpe" BETWEEN 1 AND 10), CONSTRAINT "workouts_date_check" CHECK ("date" >= DATE '1900-01-01') ); --> statement-breakpoint CREATE INDEX "appointments_date_idx" ON "health"."appointments" ("date");--> statement-breakpoint CREATE INDEX "body_measurements_date_idx" ON "health"."body_measurements" ("date");--> statement-breakpoint CREATE INDEX "hair_progress_date_idx" ON "health"."hair_progress" ("date");--> statement-breakpoint CREATE INDEX "medications_start_date_idx" ON "health"."medications" ("start_date");--> statement-breakpoint CREATE INDEX "medications_stop_date_idx" ON "health"."medications" ("stop_date");--> statement-breakpoint CREATE INDEX "vaccinations_date_idx" ON "health"."vaccinations" ("date");--> statement-breakpoint CREATE INDEX "workouts_date_idx" ON "health"."workouts" ("date");--> statement-breakpoint CREATE INDEX "workouts_session_idx" ON "health"."workouts" ("session_id");--> statement-breakpoint -- Hand-written below this line (not generated by drizzle-kit). -- updated_at triggers, same as every other core table (07_triggers_meta_grants.sql). DO $$ DECLARE t text; BEGIN IF to_regprocedure('meta.touch_updated_at()') IS NOT NULL THEN FOREACH t IN ARRAY ARRAY['vaccinations','body_measurements','workouts','nutrition_daily','sleep','medications','hair_progress','appointments'] LOOP EXECUTE format('CREATE TRIGGER touch_updated_at BEFORE UPDATE ON health.%I FOR EACH ROW EXECUTE FUNCTION meta.touch_updated_at()', t); END LOOP; END IF; END $$; --> statement-breakpoint -- Privacy: health data is readable/writable by the app role only. Unlike the other core -- schemas it is deliberately NOT granted to personal_ro, and it is not registered in -- meta.tables (so the generic list/table GUI never shows it). -- Grants only run on prod (migrations run as the schema owner `personal`); the dev DB -- (owned by personal_dev) carries no prod grants, matching db-refresh-dev.sh. REVOKE ALL ON SCHEMA health FROM PUBLIC; --> statement-breakpoint DO $$ BEGIN IF current_user = 'personal' AND EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'personal_app') THEN GRANT USAGE ON SCHEMA health TO personal_app; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA health TO personal_app; ALTER DEFAULT PRIVILEGES FOR ROLE personal IN SCHEMA health GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO personal_app; ALTER DEFAULT PRIVILEGES FOR ROLE personal IN SCHEMA health GRANT USAGE, SELECT ON SEQUENCES TO personal_app; END IF; END $$;