Files
HealthLog/prisma/migrations/0129_v1150_cycle_symptom_taxonomy/migration.sql
T
Marc-André BombeckandClaude Opus 4.8 a5946e6209 feat(cycle): add cycle-tracking schema foundation
Introduce the persistence layer for cycle tracking: eight enums
(CycleTrackingGoal, FlowLevel, OvulationTest, CervicalMucus,
HomeTestResult, ContraceptiveKind, CyclePhase, PredictionMethod) and
seven models.

CycleProfile is a 1:1 per-user settings row (lazily upserted on first
access via getOrCreateCycleProfile). MenstrualCycle and CycleDayLog are
the capture rows, carrying the MoodEntry soft-delete + syncVersion shape
so they ride the sync delta feed; CycleDayLog repeats the MoodEntry
dual-unique — the canonical (userId, date) key plus the NULL-distinct
(userId, source, externalId) key for HealthKit re-ingest. Free-text
notes encrypt at rest; queryable physiology (flow, BBT, OPK, mucus,
dates) stays plaintext for the rollup and correlation tier. The symptom
taxonomy (CycleSymptomCategory / CycleSymptom / CycleSymptomLink) clones
the mood-tag trio and seeds fifteen symptoms across three categories
with cycle.symptom.* i18n keys across all six locales. CyclePrediction
caches one forecast per user, regenerated debounced.

Migrations 0128-0130 are additive; the testcontainer integration suite
applies the full history from scratch and passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 08:19:32 +02:00

113 lines
5.8 KiB
SQL

-- v1.15.0 — cycle symptom taxonomy.
--
-- Clones the mood-tag trio (0101): a global Category -> Symptom catalog
-- plus a per-day-log join. `CycleSymptom` additionally carries
-- `user_id` + `label_encrypted` so a future release can mint per-user
-- custom symptoms; v1.15.0 ships seeded-catalogue-only.
--
-- `label_key` columns carry an i18n message key (resolved client-side
-- against all six locales) rather than a hard-coded label, so the seeded
-- catalog renders localised everywhere. The seed at the bottom uses
-- deterministic ids + ON CONFLICT DO NOTHING on `key` so re-running is a
-- no-op (idempotent).
--
-- Reversibility (down):
-- DROP TABLE IF EXISTS "cycle_symptom_links";
-- DROP TABLE IF EXISTS "cycle_symptoms";
-- DROP TABLE IF EXISTS "cycle_symptom_categories";
-- CreateTable
CREATE TABLE "cycle_symptom_categories" (
"id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"label_key" TEXT NOT NULL,
"icon" TEXT,
"sort_order" INTEGER NOT NULL DEFAULT 0,
"is_active" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "cycle_symptom_categories_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "cycle_symptoms" (
"id" TEXT NOT NULL,
"category_id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"label_key" TEXT NOT NULL,
"icon" TEXT,
"sort_order" INTEGER NOT NULL DEFAULT 0,
"is_active" BOOLEAN NOT NULL DEFAULT true,
"user_id" TEXT,
"label_encrypted" TEXT,
CONSTRAINT "cycle_symptoms_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "cycle_symptom_links" (
"day_log_id" TEXT NOT NULL,
"symptom_id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "cycle_symptom_links_pkey" PRIMARY KEY ("day_log_id","symptom_id")
);
-- CreateIndex
CREATE UNIQUE INDEX "cycle_symptom_categories_key_key" ON "cycle_symptom_categories"("key");
-- CreateIndex
CREATE UNIQUE INDEX "cycle_symptoms_key_key" ON "cycle_symptoms"("key");
-- CreateIndex
CREATE INDEX "cycle_symptoms_category_id_sort_order_idx" ON "cycle_symptoms"("category_id", "sort_order");
-- CreateIndex
CREATE INDEX "cycle_symptoms_user_id_idx" ON "cycle_symptoms"("user_id");
-- CreateIndex
CREATE INDEX "cycle_symptom_links_symptom_id_idx" ON "cycle_symptom_links"("symptom_id");
-- AddForeignKey
ALTER TABLE "cycle_symptoms" ADD CONSTRAINT "cycle_symptoms_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "cycle_symptom_categories"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "cycle_symptoms" ADD CONSTRAINT "cycle_symptoms_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "cycle_symptom_links" ADD CONSTRAINT "cycle_symptom_links_day_log_id_fkey" FOREIGN KEY ("day_log_id") REFERENCES "cycle_day_logs"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "cycle_symptom_links" ADD CONSTRAINT "cycle_symptom_links_symptom_id_fkey" FOREIGN KEY ("symptom_id") REFERENCES "cycle_symptoms"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- ─── Seed the default symptom taxonomy ───────────────────────────────
-- Idempotent: deterministic ids + ON CONFLICT ("key") DO NOTHING (the
-- mood-tag seed precedent 0101 / 0118 / 0126). Three categories
-- (physical, emotional, digestive) with ~15 high-signal symptoms. Keep
-- in sync with `src/lib/cycle/symptom-taxonomy.ts` if either changes.
-- Categories
INSERT INTO "cycle_symptom_categories" ("id", "key", "label_key", "icon", "sort_order", "is_active") VALUES
('csc_physical', 'physical', 'cycle.symptomCategory.physical', 'Activity', 0, true),
('csc_emotional', 'emotional', 'cycle.symptomCategory.emotional', 'Heart', 1, true),
('csc_digestive', 'digestive', 'cycle.symptomCategory.digestive', 'Soup', 2, true)
ON CONFLICT ("key") DO NOTHING;
-- Symptoms
INSERT INTO "cycle_symptoms" ("id", "category_id", "key", "label_key", "icon", "sort_order", "is_active") VALUES
('cs_cramps', 'csc_physical', 'cramps', 'cycle.symptom.cramps', 'Zap', 0, true),
('cs_headache', 'csc_physical', 'headache', 'cycle.symptom.headache', 'Brain', 1, true),
('cs_bloating', 'csc_physical', 'bloating', 'cycle.symptom.bloating', 'CircleDot', 2, true),
('cs_acne', 'csc_physical', 'acne', 'cycle.symptom.acne', 'Droplet', 3, true),
('cs_breast_tenderness', 'csc_physical', 'breast_tenderness', 'cycle.symptom.breastTenderness', 'HeartPulse', 4, true),
('cs_fatigue', 'csc_physical', 'fatigue', 'cycle.symptom.fatigue', 'BatteryLow', 5, true),
('cs_back_pain', 'csc_physical', 'back_pain', 'cycle.symptom.backPain', 'PersonStanding', 6, true),
('cs_insomnia', 'csc_physical', 'insomnia', 'cycle.symptom.insomnia', 'MoonStar', 7, true),
('cs_libido_high', 'csc_emotional', 'libido_high', 'cycle.symptom.libidoHigh', 'Flame', 0, true),
('cs_libido_low', 'csc_emotional', 'libido_low', 'cycle.symptom.libidoLow', 'Snowflake', 1, true),
('cs_mood_swings', 'csc_emotional', 'mood_swings', 'cycle.symptom.moodSwings', 'Drama', 2, true),
('cs_food_cravings', 'csc_digestive', 'food_cravings', 'cycle.symptom.foodCravings', 'Cookie', 0, true),
('cs_nausea', 'csc_digestive', 'nausea', 'cycle.symptom.nausea', 'Frown', 1, true),
('cs_diarrhea', 'csc_digestive', 'diarrhea', 'cycle.symptom.diarrhea', 'Toilet', 2, true),
('cs_constipation', 'csc_digestive', 'constipation', 'cycle.symptom.constipation', 'CircleSlash', 3, true)
ON CONFLICT ("key") DO NOTHING;