mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
Migration 0121 replaces the MedicationIntakeEvent unique constraint with a partial unique index `WHERE deleted_at IS NULL`. A slot the user previously deleted (tombstoned) no longer occupies the unique slot, so a re-take re-creates the row cleanly instead of P2002-ing against the tombstone and 500-ing. The migration dedups any pre-existing duplicate live rows first (keep newest, tombstone the rest) so the unique build can't fail on real data. Measurement and Workout deliberately stay full uniques: Measurement's compound-key writes use prisma.upsert, which Prisma 7 compiles to native INSERT ... ON CONFLICT and Postgres cannot arbiter against a partial unique (and consolidate-daily-mean intentionally resurrects a tombstoned canonical row, which needs the full unique); Workout has no deleted_at column. The rationale is documented in the migration header and the schema comments. Migration 0122 adds MoodEntry.externalId plus a NULL-distinct `@@unique([userId, source, externalId])`. The moodLog webhook, the pull sync, and the JSON import now carry the source's stable id into externalId and upsert on the new key when present, so a re-import is idempotent even when moodLoggedAt re-rounds or re-zones. Native/MANUAL entries keep their NULL externalId and stay on the legacy (userId, date, moodLoggedAt) path. Migration 0123 adds AuditLog(userId, action, createdAt desc) and MoodEntry(userId, moodLoggedAt) composite indexes for the status-cache and mood-insights read paths. Also corrects the stale "v1.6.0 drops this column" note on MedicationSchedule.daysOfWeek (the column is still dual-written and read by live paths, so it is retained) and documents the UTC-only timezone storage invariant in a schema header comment. Integration tests cover the intake re-take after delete (no resurrection / no 500) and mood re-import idempotency (with and without an external id) on real Postgres. (cherry picked from commit 9a6723f469bdec063a99cd08df132e07ec3c95a5)
32 lines
1.6 KiB
SQL
32 lines
1.6 KiB
SQL
-- v1.12.1 — stable external dedup key for MoodEntry.
|
|
--
|
|
-- Problem. Unlike `Measurement`, `MoodEntry` carries no `external_id`.
|
|
-- The moodLog webhook + pull sync + JSON import all upsert/insert on
|
|
-- `(user_id, date, mood_logged_at)`. If the upstream re-emits the same
|
|
-- entry with a re-rounded or re-zoned `mood_logged_at` (a Daylio export
|
|
-- re-import, or a tz change moving `date`), the composite key differs
|
|
-- and a SECOND row lands for the same physical mood entry. Measurements
|
|
-- solved exactly this with `external_id`; mood did not.
|
|
--
|
|
-- Fix. Add a nullable `external_id` column + a NULL-DISTINCT unique on
|
|
-- `(user_id, source, external_id)`. Postgres treats NULLs as distinct in
|
|
-- a unique index by default, so every MANUAL/native entry (which carries
|
|
-- a NULL `external_id`) never collides on the new key and keeps the
|
|
-- legacy `(user_id, date, mood_logged_at)` path. Importers that receive
|
|
-- a source-stable id (the moodLog upstream entry id, a Daylio row id)
|
|
-- write it into `external_id` and upsert on the new key, making a
|
|
-- re-import idempotent regardless of `mood_logged_at` drift.
|
|
--
|
|
-- Strictly additive: a new nullable column + a NULL-distinct unique
|
|
-- index. No backfill (legacy rows stay NULL → distinct → untouched), no
|
|
-- rewrite of `mood_entries`.
|
|
|
|
ALTER TABLE "mood_entries"
|
|
ADD COLUMN IF NOT EXISTS "external_id" TEXT;
|
|
|
|
-- NULL-distinct unique: only entries that carry a real source id are
|
|
-- constrained; the long tail of native/MANUAL rows (NULL external_id)
|
|
-- never collides here.
|
|
CREATE UNIQUE INDEX IF NOT EXISTS "mood_entries_user_id_source_external_id_key"
|
|
ON "mood_entries" ("user_id", "source", "external_id");
|