mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
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.
53 lines
2.3 KiB
SQL
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;
|