fix(rollups): skip soft-deleted measurements in DAY recompute

The `runRollupAggregate` SQL folded every row in `measurements`
regardless of `deleted_at`. Once iOS soft-deletes its first reading
through the by-external-ids path, the tombstoned value would still
contribute to the bucket count / mean / min / max, leaving dashboards
and analytics showing data the user thinks they removed.

Add `AND m."deleted_at" IS NULL` to both branches of `runRollupAggregate`
plus the watermark probes in `isRollupFresh` and `ensureUserRollupsFreshImpl`
so a stale `updatedAt` on a tombstoned row no longer keeps the rollup
flagged as stale forever. The boot-time backfill discovery is widened
the same way so deleted-only `(user, type, day)` tuples never enqueue.

Defense-in-depth: every reader will pick up the filter in follow-up
commits — this lands the writer-side cut so newly-folded buckets are
already clean.
This commit is contained in:
Marc-André Bombeck committed 2026-05-21 11:40:08 +02:00
1 parent 433874e390
commit db2d864a8e
1 file changed
+5 -2
+5 -2
View File
@@ -319,7 +319,7 @@ export async function isRollupFresh(
newestComputedAt: Date | null,
): Promise<boolean> {
const newest = await prisma.measurement.findFirst({
where: { userId, type },
where: { userId, type, deletedAt: null },
orderBy: { measuredAt: "desc" },
select: { measuredAt: true, updatedAt: true },
});
@@ -403,6 +403,7 @@ async function runRollupAggregate(input: {
AND m."type" IN (${typeList})
AND m."measured_at" >= $2
AND m."measured_at" < $3
AND m."deleted_at" IS NULL
GROUP BY m."type", ${dateTrunc}
`;
return prisma.$queryRawUnsafe<RollupRow[]>(
@@ -435,6 +436,7 @@ async function runRollupAggregate(input: {
WHERE m."user_id" = $1
AND m."measured_at" >= $2
AND m."measured_at" < $3
AND m."deleted_at" IS NULL
GROUP BY m."type", ${dateTrunc}
`;
return prisma.$queryRawUnsafe<RollupRow[]>(
@@ -656,7 +658,7 @@ async function ensureUserRollupsFreshImpl(
select: { computedAt: true },
}),
prisma.measurement.findFirst({
where: { userId },
where: { userId, deletedAt: null },
orderBy: { updatedAt: "desc" },
select: { updatedAt: true, measuredAt: true },
}),
@@ -775,6 +777,7 @@ export async function enqueueBootTimeRollupBackfill(): Promise<{
m."type",
date_trunc('day', m."measured_at") AS bucket_start
FROM measurements m
WHERE m."deleted_at" IS NULL
) mt
LEFT JOIN measurement_rollups r
ON r."user_id" = mt."user_id"