fix(measurements): rework dense intra-day fold to coexist with the canonical daily row

The dense intra-day retention fold minted its daily-mean stats: row with
an externalId-keyed upsert. On real data a row already occupies the
canonical local-noon instant — a sibling daily stats: row or a per-sample
HRV/PULSE reading that fell on noon — so the upsert lookup missed (the
existing row carries a different externalId) and the INSERT then violated
the (user_id, type, measured_at, source, sleep_stage) unique index with
P2002. The mocked unit tests could not surface it.

Resolve the canonical row by the measured_at composite the collision is on
(sleep_stage IS NULL for these continuous types, matched via the
NULLS NOT DISTINCT index) and adopt it in place — refresh the mean, stamp
the canonical stats: externalId, un-tombstone — or create it when absent.
Exclude the adopted row id from the per-sample soft-delete so a noon-instant
sample becomes the daily row instead of being tombstoned.

Add a testcontainer integration test that seeds the colliding shape (a
pre-existing canonical-instant row plus out-of-window per-sample rows) and
pins no-P2002, coexistence, the noon-sample case, idempotency, and the
retention bound. Update the unit coverage for the adopt-in-place path.
This commit is contained in:
Marc-André Bombeck committed 2026-06-03 07:41:20 +02:00
1 parent e6ade6b03d
commit f049eeb8b1
3 files changed
+434 -36

No files matched your search

@@ -0,0 +1,308 @@
/**
* v1.10.2 — real-Postgres integration coverage for
* `runDenseIntradayRetention()`. The mocked unit tests could not surface
* the v1.10.0.2 P2002 fold-coexistence bug: the fold mints a canonical
* daily-mean row at the user's local-noon instant, but a row may ALREADY
* sit on that `(userId, type, measuredAt, source, sleepStage)` unique
* index from another path. An externalId-keyed upsert misses the lookup
* and the INSERT then violates the measured_at index with P2002.
*
* This file seeds that exact colliding shape against a live Postgres and
* asserts the reworked fold coexists with the pre-existing daily row,
* stays idempotent, and preserves the dense-tier scope.
*/
import { beforeEach, describe, expect, it } from "vitest";
import { getPrismaClient, truncateAllTables } from "./setup";
import { runDenseIntradayRetention } from "@/lib/measurements/dense-intraday-retention";
import { canonicalDailyTimestamp } from "@/lib/measurements/consolidation-tz";
const TEST_USER_ID = "user-dense-retention";
const TZ = "Europe/Berlin";
const DAY_KEY = "2026-05-01";
// The canonical local-noon instant the fold writes for the seeded day.
const CANONICAL = canonicalDailyTimestamp(DAY_KEY, TZ);
const HRV_STATS_ID = "stats:HKQuantityTypeIdentifierHeartRateVariabilitySDNN:2026-05-01";
beforeEach(async () => {
await truncateAllTables(getPrismaClient());
await getPrismaClient().user.create({
data: {
id: TEST_USER_ID,
username: "dense-retention",
email: "dense-retention@example.test",
timezone: TZ,
},
});
});
describe("runDenseIntradayRetention (real Postgres)", () => {
it("folds out-of-window HRV per-sample rows into one daily MEAN and soft-deletes them", async () => {
const prisma = getPrismaClient();
await prisma.measurement.createMany({
data: [
{
userId: TEST_USER_ID,
type: "HEART_RATE_VARIABILITY",
value: 40,
unit: "ms",
source: "APPLE_HEALTH",
measuredAt: new Date("2026-05-01T08:00:00.000Z"),
externalId: "hk-hrv-1",
},
{
userId: TEST_USER_ID,
type: "HEART_RATE_VARIABILITY",
value: 60,
unit: "ms",
source: "APPLE_HEALTH",
measuredAt: new Date("2026-05-01T09:00:00.000Z"),
externalId: "hk-hrv-2",
},
],
});
const summary = await runDenseIntradayRetention(prisma, {
userId: TEST_USER_ID,
retentionDays: 0,
log: () => {},
});
expect(summary.totals.daysConsolidated).toBe(1);
expect(summary.totals.perSampleRowsSoftDeleted).toBe(2);
const live = await prisma.measurement.findMany({
where: {
userId: TEST_USER_ID,
type: "HEART_RATE_VARIABILITY",
deletedAt: null,
},
});
expect(live).toHaveLength(1);
// MEAN of (40, 60) = 50.
expect(live[0].value).toBeCloseTo(50, 6);
expect(live[0].externalId).toBe(HRV_STATS_ID);
expect(live[0].measuredAt.getTime()).toBe(CANONICAL.getTime());
});
it("coexists with a pre-existing daily row on the canonical instant (the P2002 case)", async () => {
const prisma = getPrismaClient();
// A previously-collapsed daily `stats:` row ALREADY sits on the
// canonical local-noon instant under a DIFFERENT HK stats id than the
// one the fold mints (e.g. a row minted by a sibling consolidation path,
// or a legacy stats id). The scan EXCLUDES it (NOT startsWith 'stats:'),
// so it is never folded into the mean — but pre-v1.10.2 the fold's
// externalId-keyed upsert missed it on lookup and the INSERT then
// collided on (userId, type, measuredAt, source, sleepStage) with P2002.
await prisma.measurement.create({
data: {
userId: TEST_USER_ID,
type: "HEART_RATE_VARIABILITY",
value: 999, // stale value to be overwritten by the fold
unit: "ms",
source: "APPLE_HEALTH",
measuredAt: CANONICAL,
externalId: "stats:legacy-hrv-id:2026-05-01",
},
});
// Out-of-window per-sample rows that should fold into the canonical row.
await prisma.measurement.createMany({
data: [
{
userId: TEST_USER_ID,
type: "HEART_RATE_VARIABILITY",
value: 40,
unit: "ms",
source: "APPLE_HEALTH",
measuredAt: new Date("2026-05-01T06:00:00.000Z"),
externalId: "hk-hrv-1",
},
{
userId: TEST_USER_ID,
type: "HEART_RATE_VARIABILITY",
value: 60,
unit: "ms",
source: "APPLE_HEALTH",
measuredAt: new Date("2026-05-01T18:00:00.000Z"),
externalId: "hk-hrv-2",
},
],
});
// Must not throw P2002.
const summary = await runDenseIntradayRetention(prisma, {
userId: TEST_USER_ID,
retentionDays: 0,
log: () => {},
});
expect(summary.totals.daysConsolidated).toBe(1);
const live = await prisma.measurement.findMany({
where: {
userId: TEST_USER_ID,
type: "HEART_RATE_VARIABILITY",
deletedAt: null,
},
});
// Exactly one live daily row: the pre-existing one, adopted in place.
expect(live).toHaveLength(1);
expect(live[0].measuredAt.getTime()).toBe(CANONICAL.getTime());
// The fold refreshed the value to the per-sample MEAN (50), overwriting
// the stale 999, and stamped the canonical `stats:` externalId.
expect(live[0].value).toBeCloseTo(50, 6);
expect(live[0].externalId).toBe(HRV_STATS_ID);
// The two out-of-window samples are tombstoned.
const tombstoned = await prisma.measurement.findMany({
where: {
userId: TEST_USER_ID,
type: "HEART_RATE_VARIABILITY",
deletedAt: { not: null },
},
});
expect(tombstoned).toHaveLength(2);
});
it("does not tombstone a per-sample row that happens to fall on the canonical instant", async () => {
const prisma = getPrismaClient();
// One per-sample row sits exactly on the canonical local-noon instant;
// it must become the adopted daily row, not get soft-deleted.
await prisma.measurement.createMany({
data: [
{
userId: TEST_USER_ID,
type: "HEART_RATE_VARIABILITY",
value: 50,
unit: "ms",
source: "APPLE_HEALTH",
measuredAt: CANONICAL,
externalId: "hk-hrv-noon",
},
{
userId: TEST_USER_ID,
type: "HEART_RATE_VARIABILITY",
value: 70,
unit: "ms",
source: "APPLE_HEALTH",
measuredAt: new Date("2026-05-01T06:00:00.000Z"),
externalId: "hk-hrv-morning",
},
],
});
const summary = await runDenseIntradayRetention(prisma, {
userId: TEST_USER_ID,
retentionDays: 0,
log: () => {},
});
expect(summary.totals.daysConsolidated).toBe(1);
const live = await prisma.measurement.findMany({
where: {
userId: TEST_USER_ID,
type: "HEART_RATE_VARIABILITY",
deletedAt: null,
},
});
expect(live).toHaveLength(1);
expect(live[0].measuredAt.getTime()).toBe(CANONICAL.getTime());
// MEAN of (50, 70) = 60 — the adopted row carries the day's mean.
expect(live[0].value).toBeCloseTo(60, 6);
expect(live[0].externalId).toBe(HRV_STATS_ID);
});
it("is idempotent — a second run is a no-op and the value stays correct", async () => {
const prisma = getPrismaClient();
await prisma.measurement.createMany({
data: [
{
userId: TEST_USER_ID,
type: "PULSE",
value: 60,
unit: "bpm",
source: "APPLE_HEALTH",
measuredAt: new Date("2026-05-01T06:00:00.000Z"),
externalId: "hk-pulse-1",
},
{
userId: TEST_USER_ID,
type: "PULSE",
value: 80,
unit: "bpm",
source: "APPLE_HEALTH",
measuredAt: new Date("2026-05-01T18:00:00.000Z"),
externalId: "hk-pulse-2",
},
],
});
const first = await runDenseIntradayRetention(prisma, {
userId: TEST_USER_ID,
retentionDays: 0,
log: () => {},
});
expect(first.totals.daysConsolidated).toBe(1);
expect(first.totals.perSampleRowsSoftDeleted).toBe(2);
// Second run must converge to zero work and never collide on the
// canonical row it minted on the first pass.
const second = await runDenseIntradayRetention(prisma, {
userId: TEST_USER_ID,
retentionDays: 0,
log: () => {},
});
expect(second.totals.daysConsolidated).toBe(0);
expect(second.totals.perSampleRowsSoftDeleted).toBe(0);
const live = await prisma.measurement.findMany({
where: { userId: TEST_USER_ID, type: "PULSE", deletedAt: null },
});
expect(live).toHaveLength(1);
// MEAN of (60, 80) = 70.
expect(live[0].value).toBeCloseTo(70, 6);
});
it("keeps in-window per-sample rows raw (retention bound)", async () => {
const prisma = getPrismaClient();
const recent = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000); // 2 days ago
await prisma.measurement.createMany({
data: [
{
userId: TEST_USER_ID,
type: "HEART_RATE_VARIABILITY",
value: 45,
unit: "ms",
source: "APPLE_HEALTH",
measuredAt: recent,
externalId: "hk-hrv-recent-1",
},
{
userId: TEST_USER_ID,
type: "HEART_RATE_VARIABILITY",
value: 55,
unit: "ms",
source: "APPLE_HEALTH",
measuredAt: new Date(recent.getTime() + 60 * 60 * 1000),
externalId: "hk-hrv-recent-2",
},
],
});
const summary = await runDenseIntradayRetention(prisma, {
userId: TEST_USER_ID,
// Default 14-day window — both rows are inside it.
log: () => {},
});
expect(summary.totals.daysConsolidated).toBe(0);
const live = await prisma.measurement.findMany({
where: {
userId: TEST_USER_ID,
type: "HEART_RATE_VARIABILITY",
deletedAt: null,
},
});
// Both raw samples survive — the intra-day shape is preserved.
expect(live).toHaveLength(2);
});
});