From db2d864a8eaca04853184c51a2abac7c118bad72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 21 May 2026 11:40:08 +0200 Subject: [PATCH] fix(rollups): skip soft-deleted measurements in DAY recompute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/lib/measurements/rollups.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lib/measurements/rollups.ts b/src/lib/measurements/rollups.ts index 74f9bb455..41c23336e 100644 --- a/src/lib/measurements/rollups.ts +++ b/src/lib/measurements/rollups.ts @@ -319,7 +319,7 @@ export async function isRollupFresh( newestComputedAt: Date | null, ): Promise { 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( @@ -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( @@ -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"