fix(comprehensive-aggregator): exclude soft-deleted in raw aggregate

The route's central analytics aggregator runs eight `FROM measurements`
clauses (window_stats CTE + main aggregate + DISTINCT ON latest +
firstAt probe, mirrored across the rollup-fresh and cold-fallback
branches) plus one `findMany` for the BP raw rows. None filtered on
`deleted_at`, so the Coach/Insights snapshot would see tombstoned
readings in every windowed avg, slope, anomaly count, and latest
attribution.

Add `AND m."deleted_at" IS NULL` to the eight raw SQL filters and
`deletedAt: null` to `fetchBpRawRows`. The `measurement_rollups`
findMany branch already rides the writer-side filter.
This commit is contained in:
Marc-André Bombeck committed 2026-05-21 11:43:29 +02:00
1 parent 37342744f9
commit c47b7dade3
1 file changed
+9
@@ -310,6 +310,7 @@ async function fetchBpRawRows(
userId,
type: { in: ["BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA"] },
measuredAt: { gte: ninetyDaysAgo },
deletedAt: null,
},
orderBy: { measuredAt: "asc" },
select: { type: true, measuredAt: true, value: true },
@@ -348,6 +349,7 @@ async function buildFromRollups(
FROM measurements m
WHERE m."user_id" = ${userId}
AND m."measured_at" >= ${ninetyDaysAgo}
AND m."deleted_at" IS NULL
GROUP BY m."type"
)
SELECT
@@ -414,6 +416,7 @@ async function buildFromRollups(
JOIN window_stats ws ON ws."type" = m."type"
WHERE m."user_id" = ${userId}
AND m."measured_at" >= ${ninetyDaysAgo}
AND m."deleted_at" IS NULL
GROUP BY m."type", ws.stddev_value
`),
timeSubquery(timings, "latests", () => prisma.$queryRaw<LatestRow[]>`
@@ -424,6 +427,7 @@ async function buildFromRollups(
FROM measurements m
WHERE m."user_id" = ${userId}
AND m."measured_at" >= ${ninetyDaysAgo}
AND m."deleted_at" IS NULL
ORDER BY m."type", m."measured_at" DESC
`),
// v1.4.35 — DAY rollup buckets for the trailing 90 days, every
@@ -450,6 +454,7 @@ async function buildFromRollups(
FROM measurements m
WHERE m."user_id" = ${userId}
AND m."measured_at" >= ${ninetyDaysAgo}
AND m."deleted_at" IS NULL
`),
]);
@@ -536,6 +541,7 @@ async function buildFromLiveAggregate(
FROM measurements m
WHERE m."user_id" = ${userId}
AND m."measured_at" >= ${ninetyDaysAgo}
AND m."deleted_at" IS NULL
GROUP BY m."type"
)
SELECT
@@ -602,6 +608,7 @@ async function buildFromLiveAggregate(
JOIN window_stats ws ON ws."type" = m."type"
WHERE m."user_id" = ${userId}
AND m."measured_at" >= ${ninetyDaysAgo}
AND m."deleted_at" IS NULL
GROUP BY m."type", ws.stddev_value
`),
timeSubquery(timings, "latests", () => prisma.$queryRaw<LatestRow[]>`
@@ -612,6 +619,7 @@ async function buildFromLiveAggregate(
FROM measurements m
WHERE m."user_id" = ${userId}
AND m."measured_at" >= ${ninetyDaysAgo}
AND m."deleted_at" IS NULL
ORDER BY m."type", m."measured_at" DESC
`),
// Buckets may exist for some types even when the COUNT probe came
@@ -672,6 +680,7 @@ async function buildFromLiveAggregate(
FROM measurements m
WHERE m."user_id" = ${userId}
AND m."measured_at" >= ${ninetyDaysAgo}
AND m."deleted_at" IS NULL
`,
);
firstMeasurementAt = firstRows[0]?.first_at