Files
HealthLog/prisma/migrations/0259_workout_insights/migration.sql
T
Marc-André Bombeck 0dba15e902 feat(workouts): add the per-workout insight model
Its own table, cascading from the workout row, because workout deletes are
hard deletes. The paragraph is AES-256-GCM at rest and registered for key
rotation; the input hash and prompt version make a re-post of the same
session a free no-op.
2026-07-19 13:47:25 +02:00

53 lines
2.3 KiB
SQL

-- The per-workout Activity Insight paragraph.
--
-- Its own table rather than a column on `workouts` because a workout delete is
-- a HARD delete: the FK below cascades, so the paragraph can never outlive the
-- session it describes. Keeping it out of `workouts` also keeps the generation
-- provenance off the row every list read selects from.
--
-- Rows are written ONLY by the `workout-insight-generate` worker, dispatched by
-- the data-arrival spine when a workout genuinely lands. No read path creates
-- one, so every workout that predates this migration stays without a row — and
-- renders no card — forever, at zero provider cost.
--
-- `paragraph_encrypted` is AES-256-GCM at rest, following the
-- `insight_narratives.encrypted_content` precedent, and is registered in
-- `src/lib/crypto/encrypted-columns.ts` so key rotation covers it.
--
-- `input_hash` fingerprints the deterministic evidence the paragraph was built
-- from together with the prompt version: a device re-post of the same session
-- hashes identically and is refused before any provider is resolved.
CREATE TABLE "workout_insights" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"workout_id" TEXT NOT NULL,
"paragraph_encrypted" BYTEA NOT NULL,
"input_hash" TEXT NOT NULL,
"prompt_version" TEXT NOT NULL,
"provider_type" TEXT,
"locale" TEXT NOT NULL,
"generated_at" TIMESTAMP(3) NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "workout_insights_pkey" PRIMARY KEY ("id")
);
-- One paragraph per workout. This is also the second half of the
-- double-post defence: the singleton queue key collapses a burst, and this
-- constraint makes the claim durable even if two workers do run.
CREATE UNIQUE INDEX "workout_insights_workout_id_key"
ON "workout_insights"("workout_id");
-- The daily-cap count: rows for one user inside a generated_at window.
CREATE INDEX "workout_insights_user_id_generated_at_idx"
ON "workout_insights"("user_id", "generated_at");
ALTER TABLE "workout_insights"
ADD CONSTRAINT "workout_insights_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "workout_insights"
ADD CONSTRAINT "workout_insights_workout_id_fkey"
FOREIGN KEY ("workout_id") REFERENCES "workouts"("id") ON DELETE CASCADE ON UPDATE CASCADE;