feat(api): add source filter to measurement and mood lists

Add a source filter to both management lists. Mood gains an optional
`source` param (validated against the mood source set); measurements gain
`sourceEq` (validated against MeasurementSource) — the `source` key on the
measurements list is the rollup-tier opt-in, so the filter rides a distinct
param. Both thread into the list `where` alongside the pinned `userId` +
`deletedAt: null`.

Migration 0136 adds two additive indexes — `(userId, source, measuredAt)`
on measurements and `(userId, source, moodLoggedAt)` on mood — so a source
filter + recency sort is an index range scan rather than a userId scan plus
in-memory sort at 100-200k rows per user. Pure `CREATE INDEX IF NOT EXISTS`,
no backfill.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.8 committed 2026-06-07 19:23:59 +02:00
1 parent 1fd875b83c
commit 0d69611bbf
8 files changed
+117 -1

No files matched your search

@@ -0,0 +1,15 @@
-- v1.15.13 — source-filter support indexes for the measurements + mood
-- management lists. The new list surfaces add a source filter (and the
-- existing source/value sort). No index leads with `(user_id, source)`
-- today, so a source filter forces a `user_id` scan + in-memory sort —
-- the landmine at 100–200k rows per user. These two additive indexes turn
-- a source filter + recency sort into an index range scan.
--
-- Pure additive `CREATE INDEX IF NOT EXISTS`, no backfill — same low-risk
-- shape as migration 0123.
CREATE INDEX IF NOT EXISTS "measurements_user_id_source_measured_at_idx"
ON "measurements" ("user_id", "source", "measured_at" DESC);
CREATE INDEX IF NOT EXISTS "mood_entries_user_id_source_mood_logged_at_idx"
ON "mood_entries" ("user_id", "source", "mood_logged_at" DESC);
+8
View File
@@ -850,6 +850,10 @@ model Measurement {
// (Postgres semantics) so manual entries don't collide here.
@@unique([userId, type, source, externalId])
@@index([userId, type, measuredAt])
// v1.15.13 — leads with `(userId, source)` so the management list's
// source filter + recency sort is an index range scan instead of a
// `userId` scan + in-memory sort. See migration 0136.
@@index([userId, source, measuredAt])
@@index([externalId])
// v1.7.0 — supports the `/api/sync/changes` keyset feed, which walks
// `where userId` ordered by `(updatedAt asc, id asc)` with `take
@@ -2082,6 +2086,10 @@ model MoodEntry {
// null, moodLoggedAt: { gte } }` ordered by `moodLoggedAt desc`; this
// index turns that into a range scan. See migration 0123.
@@index([userId, moodLoggedAt])
// v1.15.13 — leads with `(userId, source)` so the mood management
// list's source filter + recency sort is an index range scan. See
// migration 0136.
@@index([userId, source, moodLoggedAt])
// v1.7.0 sync — keyset support for the `/api/sync/changes` mood feed
// (`where userId` ordered by `(updatedAt asc, id asc)`).
@@index([userId, updatedAt, id])