feat(rollups): make measurement rollups source-aware (migration 0115)

Rollup rows were grouped by (type, day) only, so two sources reporting
the same standard vital for one day (e.g. WHOOP + Apple Watch resting
heart rate) were AVG-blended into a single row instead of resolving to
the source-priority ladder's canonical reading.

Re-grain the rollup table to (type, day, source): the writer now groups
by source and delete-then-inserts each affected partition (so a source
that stops reporting a bucket cannot strand a stale row), and the read
path will collapse overlapping sources to the canonical reading in a
follow-up commit. Cumulative metrics (steps, energy) keep summing per
source. Migration 0115 purges the legacy source-blind aggregates; the
boot backfill + read-time self-heal re-mint per-source rows.

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-04 09:29:49 +02:00
1 parent dc9da4fd7c
commit ef74ee13f7
5 files changed
+164 -95

No files matched your search

@@ -289,14 +289,15 @@ describe("Measurement.deletedAt — tombstone invisibility (v1.4.40 W-DELETED)",
noonUtc.getUTCDate(),
),
);
const bucket = await prisma.measurementRollup.findUnique({
// v1.11.1 — rollups are source-aware; both seeded rows share the default
// MANUAL source so they still collapse to one DAY row. findFirst keeps the
// assertion source-agnostic.
const bucket = await prisma.measurementRollup.findFirst({
where: {
userId_type_granularity_bucketStart: {
userId: user.id,
type: "WEIGHT",
granularity: "DAY",
bucketStart: dayStartUtc,
},
userId: user.id,
type: "WEIGHT",
granularity: "DAY",
bucketStart: dayStartUtc,
},
});
expect(bucket).not.toBeNull();