fix(rollups): boot-backfill discovery SQL uses bucket_start as NULL sentinel

v1.4.38.5 referenced `r."id" IS NULL` on the LEFT JOIN — but
`measurement_rollups` has a composite primary key
`(user_id, type, granularity, bucket_start)` and no surrogate `id`
column. Postgres rejected the query with `column r.id does not
exist` (SQL state 42703); the worker boot swallowed the error per
the helper's best-effort contract and the queue stayed empty.

Net effect: v1.4.38.5 deployed cleanly but delivered none of its
promised fast-path recovery. Marc's account stayed on the live
aggregator.

The fix uses `r."bucket_start" IS NULL` — any column from the
unmatched right side of the LEFT JOIN serves the "no matching row"
filter, and `bucket_start` is part of the composite PK so the
planner already has the column. Bumps to v1.4.38.6.
This commit is contained in:
Marc-André Bombeck committed 2026-05-18 06:15:56 +02:00
1 parent 9c13cba3f7
commit 7ce29899e0
3 files changed
+36 -6

No files matched your search

+29
View File
@@ -1,5 +1,34 @@
# Changelog
## [1.4.38.6] — 2026-05-18 — Boot-backfill discovery SQL fix
The v1.4.38.5 discovery rewrite filtered the LEFT JOIN with
`WHERE r."id" IS NULL` — but `measurement_rollups` has a composite
primary key `(user_id, type, granularity, bucket_start)` and no
surrogate `id` column. Postgres rejected the query with
`column r.id does not exist` (SQL state 42703), the worker boot
swallowed the error per the helper's best-effort contract, and the
queue stayed empty. Net effect: v1.4.38.5 deployed cleanly but
delivered none of its promised fast-path recovery.
### Fixed
- **Boot-backfill discovery now filters on `r."bucket_start" IS NULL`**
— any column from the right side of the LEFT JOIN serves as the
"no matching row" sentinel; `bucket_start` is part of the composite
primary key so the planner already touches the column. Verified
against the live schema: `42703` no longer raised on worker boot,
and the discovery query returns the expected per-type-missing
candidate set.
### Operator notes
- No new migration. No env-var change.
- On first boot after deploy the rollup-full-backfill queue picks
up users with any missing type-coverage and folds them. Power-
user accounts may see one slow `/api/analytics` cold-mount before
the next request lands on the restored fast path.
## [1.4.38.5] — 2026-05-18 — Analytics fast-path restored on long-running accounts
Hotfix on top of v1.4.38.4. `/api/analytics` and the dashboard
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "healthlog",
"version": "1.4.38.5",
"version": "1.4.38.6",
"description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.",
"license": "AGPL-3.0-only",
"homepage": "https://healthlog.dev",
+6 -5
View File
@@ -736,10 +736,11 @@ export async function enqueueBootTimeRollupBackfill(): Promise<{
// planner uses the `(user_id, type, measured_at)` index path; the
// LEFT JOIN onto `measurement_rollups` uses the
// `(user_id, type, granularity, bucket_start)` composite primary
// key. `r."id" IS NULL` filters the unmatched partitions. A user
// surfaces here when ANY type is missing — including the brand-
// new-type case that v1.4.35.1's zero-rollup-only shape silently
// stranded.
// key. `measurement_rollups` has no surrogate `id` column —
// `r."bucket_start" IS NULL` (any column from the right side of
// the LEFT JOIN) marks the unmatched partitions. A user surfaces
// here when ANY type is missing — including the brand-new-type
// case that v1.4.35.1's zero-rollup-only shape silently stranded.
const users = await prisma.$queryRaw<Array<{ id: string }>>`
SELECT DISTINCT mt."user_id" AS id
FROM (
@@ -750,7 +751,7 @@ export async function enqueueBootTimeRollupBackfill(): Promise<{
ON r."user_id" = mt."user_id"
AND r."type" = mt."type"
AND r."granularity" = 'DAY'
WHERE r."id" IS NULL
WHERE r."bucket_start" IS NULL
`;
if (users.length === 0) {