diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d9048470..aaf417289 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/package.json b/package.json index 446fdfdfe..45536d17b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/lib/measurements/rollups.ts b/src/lib/measurements/rollups.ts index f704335a9..897ce2f21 100644 --- a/src/lib/measurements/rollups.ts +++ b/src/lib/measurements/rollups.ts @@ -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>` 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) {