Files
HealthLog/prisma/migrations/0311_mood_context/migration.sql
T
Marc-André Bombeck 7a4ee19c68 feat(mood): add the MoodContext sub-row and its encrypted note
- migration 0311 creates mood_contexts, keyed by the entry, cascading from
  both the entry and the owner
- work, contacts, leisure and one notable event; every field nullable, and an
  entry with nothing filled in gets no row at all
- notes_encrypted registered in ENCRYPTED_COLUMNS and in the rotation script,
  so a key rotation cannot strand it
2026-08-08 06:51:42 +02:00

85 lines
3.7 KiB
SQL

-- v1.38 — the day's context around a mood entry.
--
-- Work, contacts, leisure and a notable event, as an optional sub-row rather
-- than as twenty more columns on `mood_entries`. Level A went the other way in
-- migration 0310, and for the opposite reasons: those five values are on
-- nearly every row and every mood reader wants them, while this is sparse by
-- design (only the sections somebody opened produce anything at all) and only
-- the context surfaces read it.
--
-- An entry with nothing filled in gets NO row here. A table of all-NULL rows
-- would record "asked and answered nothing" where the truth is "never asked",
-- and the two are different facts.
--
-- Nothing in this table duplicates another module. Sleep, activity and vitals
-- are resolved at read time from the modules that own them, and body symptoms
-- stay in the illness tables. A stored copy would be a second home for the
-- same fact and would drift the first time somebody corrected the original.
--
-- The two multi-select lists are JSON arrays in TEXT columns, matching the
-- `mood_entries.tags` convention. Not queryable, read whole — acceptable
-- because every reader wants an entry's whole context anyway.
--
-- No CHECK constraints on the numeric ranges. Bounds are enforced server-side,
-- the same way `mood_entry_tag_links.rating` documents: a later change to a
-- scale must not strand rows written under the old one.
CREATE TABLE "mood_contexts" (
"mood_entry_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
-- Work. `work_status` is one of off / partial / regular / overtime.
"work_status" TEXT,
"work_minutes" INTEGER,
"overtime_minutes" INTEGER,
"work_load" INTEGER,
"work_satisfaction" INTEGER,
-- Contacts. `contact_circles` is a JSON array of circle keys; the count is
-- never scored, which is why the experienced quality is its own column.
"contact_circles" TEXT,
"contact_form" TEXT,
"contact_extent" TEXT,
"contact_quality" INTEGER,
"contact_support" INTEGER,
-- Leisure. `leisure_categories` is a JSON array of category keys.
"leisure_categories" TEXT,
"leisure_minutes" INTEGER,
"leisure_joy" INTEGER,
"leisure_recovery" INTEGER,
-- One notable event, if there was one.
"event_type" TEXT,
"event_valence" INTEGER,
"event_at" TIMESTAMPTZ(3),
-- One AES-256-GCM note for the whole context, not one per section: four
-- ciphertext columns would be four rotation entries and four ways to
-- forget one. Registered in ENCRYPTED_COLUMNS and in the rotation script.
"notes_encrypted" BYTEA,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(3) NOT NULL,
CONSTRAINT "mood_contexts_pkey" PRIMARY KEY ("mood_entry_id")
);
-- Deleting the entry deletes its context. The context has no meaning without
-- the entry it describes, and a row pointing at a deleted entry would be
-- readable by nobody and deletable by nobody. The user cascade matches every
-- other user-scoped table here.
ALTER TABLE "mood_contexts"
ADD CONSTRAINT "mood_contexts_mood_entry_id_fkey"
FOREIGN KEY ("mood_entry_id") REFERENCES "mood_entries"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "mood_contexts"
ADD CONSTRAINT "mood_contexts_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
-- The user-scoped sweep: the context analytics read a window of an account's
-- contexts, and the account-deletion path clears them by owner.
CREATE INDEX "mood_contexts_user_id_idx" ON "mood_contexts"("user_id");