mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
fix(insights): align cold-fallback slope window to the rollup day boundary
The rollup read path windows the 7/30/90-day regression on
startOfUtcDay(now - N days), while the live-SQL fallback filtered the
REGR_* aggregates on the wall-clock NOW() - INTERVAL bound. An account
crossing a coverage boundary (warm to cold) therefore saw the boundary
day's readings flip in or out of the slope window depending only on
whether the rollup tier was populated, returning a cache-dependent
slope for the same request.
Anchor both cold-fallback paths on date_trunc('day', NOW() AT TIME ZONE
'UTC') - INTERVAL 'N days', the SQL twin of startOfUtcDay, so warm and
cold return identical boundary-day membership. The avg7/avg30/median
windows stay wall-clock since the warm narrow query computes them the
same way.
This commit is contained in:
1 parent
8471384d01
commit
86d917c5e2
3 files changed
+88
-12
No files matched your search
@@ -652,4 +652,54 @@ describe("computeSummariesSlice", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* A2-M2 — read-swap boundary-day consistency. The warm
|
||||
* `computeFromRollups` path windows the regression on UTC-midnight
|
||||
* (`startOfUtcDay(now − N days)` + `composeWindowedRegression`). The
|
||||
* cold-fallback `windowed` query must anchor its REGR_* FILTERs on the
|
||||
* SAME UTC-midnight boundary — `date_trunc('day', NOW() AT TIME ZONE
|
||||
* 'UTC') − INTERVAL 'N days'` — so a warm→cold coverage transition
|
||||
* returns identical boundary-day membership for slope7/30/90 instead of
|
||||
* a cache-dependent answer to the same request.
|
||||
*/
|
||||
describe("A2-M2 regression-window boundary alignment", () => {
|
||||
it("anchors the cold-fallback slope FILTERs on the UTC-midnight day boundary, not the wall-clock instant", async () => {
|
||||
const queries: string[] = [];
|
||||
UNSAFE.mockImplementation((sql: string) => {
|
||||
queries.push(sql);
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
|
||||
// Empty coverage ⇒ cold-fallback path ⇒ the `windowed` query runs.
|
||||
RAW.mockResolvedValueOnce([]);
|
||||
await computeSummariesSlice("user-a2m2");
|
||||
|
||||
const windowedSql = queries.find(
|
||||
(sql) => sql.includes("AS slope7") && sql.includes("REGR_SLOPE"),
|
||||
);
|
||||
expect(windowedSql).toBeDefined();
|
||||
const sql = windowedSql as string;
|
||||
|
||||
// Every regression window (7/30/90 for slope + r²) anchors on the
|
||||
// day-truncated UTC boundary. Six FILTERs total.
|
||||
for (const days of ["7 days", "30 days", "90 days"]) {
|
||||
expect(sql).toContain(
|
||||
`(date_trunc('day', NOW() AT TIME ZONE 'UTC') - INTERVAL '${days}') AT TIME ZONE 'UTC'`,
|
||||
);
|
||||
}
|
||||
const anchored = sql.match(
|
||||
/date_trunc\('day', NOW\(\) AT TIME ZONE 'UTC'\) - INTERVAL '(?:7|30|90) days'\) AT TIME ZONE 'UTC'/g,
|
||||
);
|
||||
expect(anchored?.length).toBe(6);
|
||||
|
||||
// The regression windows must NOT fall back to the bare wall-clock
|
||||
// `NOW() - INTERVAL 'N days'` bound the warm path never uses for the
|
||||
// slope. Isolate the slope columns (everything from the first
|
||||
// REGR_SLOPE onward) so the avg7/avg30 windows above — which DO stay
|
||||
// wall-clock by design — don't trip the assertion.
|
||||
const slopeBlock = sql.slice(sql.indexOf("REGR_SLOPE"));
|
||||
expect(slopeBlock).not.toMatch(/WHERE m\."measured_at" >= NOW\(\) -/);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -772,41 +772,54 @@ async function computeFromLiveAggregate(
|
||||
) FILTER (
|
||||
WHERE m."measured_at" >= NOW() - INTERVAL '90 days'
|
||||
)::double precision AS median,
|
||||
-- A2-M2 — anchor the regression windows on the UTC-midnight day
|
||||
-- boundary so this cold-fallback path returns the SAME boundary-day
|
||||
-- membership the warm computeFromRollups composes via
|
||||
-- startOfUtcDay(now - N days) + composeWindowedRegression. The
|
||||
-- expression date_trunc('day', NOW() AT TIME ZONE 'UTC') - INTERVAL
|
||||
-- 'N days' is the SQL twin of startOfUtcDay; the trailing AT TIME
|
||||
-- ZONE 'UTC' re-materialises the naive truncation as a UTC instant
|
||||
-- so the measured_at (timestamptz) comparison lands on the same wall
|
||||
-- the rollup bucketStart sits on. The wall-clock NOW() - INTERVAL
|
||||
-- bound let the boundary day flip in/out of the slope window across
|
||||
-- a warm-to-cold coverage transition (cache-dependent slope on the
|
||||
-- same request); the avg7/30/median windows above stay wall-clock
|
||||
-- because the warm narrow query computes THOSE the same way.
|
||||
REGR_SLOPE(
|
||||
m."value",
|
||||
EXTRACT(EPOCH FROM m."measured_at") / 86400.0
|
||||
) FILTER (
|
||||
WHERE m."measured_at" >= NOW() - INTERVAL '7 days'
|
||||
WHERE m."measured_at" >= (date_trunc('day', NOW() AT TIME ZONE 'UTC') - INTERVAL '7 days') AT TIME ZONE 'UTC'
|
||||
)::double precision AS slope7,
|
||||
REGR_R2(
|
||||
m."value",
|
||||
EXTRACT(EPOCH FROM m."measured_at") / 86400.0
|
||||
) FILTER (
|
||||
WHERE m."measured_at" >= NOW() - INTERVAL '7 days'
|
||||
WHERE m."measured_at" >= (date_trunc('day', NOW() AT TIME ZONE 'UTC') - INTERVAL '7 days') AT TIME ZONE 'UTC'
|
||||
)::double precision AS r2_7,
|
||||
REGR_SLOPE(
|
||||
m."value",
|
||||
EXTRACT(EPOCH FROM m."measured_at") / 86400.0
|
||||
) FILTER (
|
||||
WHERE m."measured_at" >= NOW() - INTERVAL '30 days'
|
||||
WHERE m."measured_at" >= (date_trunc('day', NOW() AT TIME ZONE 'UTC') - INTERVAL '30 days') AT TIME ZONE 'UTC'
|
||||
)::double precision AS slope30,
|
||||
REGR_R2(
|
||||
m."value",
|
||||
EXTRACT(EPOCH FROM m."measured_at") / 86400.0
|
||||
) FILTER (
|
||||
WHERE m."measured_at" >= NOW() - INTERVAL '30 days'
|
||||
WHERE m."measured_at" >= (date_trunc('day', NOW() AT TIME ZONE 'UTC') - INTERVAL '30 days') AT TIME ZONE 'UTC'
|
||||
)::double precision AS r2_30,
|
||||
REGR_SLOPE(
|
||||
m."value",
|
||||
EXTRACT(EPOCH FROM m."measured_at") / 86400.0
|
||||
) FILTER (
|
||||
WHERE m."measured_at" >= NOW() - INTERVAL '90 days'
|
||||
WHERE m."measured_at" >= (date_trunc('day', NOW() AT TIME ZONE 'UTC') - INTERVAL '90 days') AT TIME ZONE 'UTC'
|
||||
)::double precision AS slope90,
|
||||
REGR_R2(
|
||||
m."value",
|
||||
EXTRACT(EPOCH FROM m."measured_at") / 86400.0
|
||||
) FILTER (
|
||||
WHERE m."measured_at" >= NOW() - INTERVAL '90 days'
|
||||
WHERE m."measured_at" >= (date_trunc('day', NOW() AT TIME ZONE 'UTC') - INTERVAL '90 days') AT TIME ZONE 'UTC'
|
||||
)::double precision AS r2_90
|
||||
FROM ${canonicalMeasurementsFrom(rankUnqualified, "90 days")}
|
||||
GROUP BY m."type"
|
||||
|
||||
@@ -682,41 +682,54 @@ async function buildFromLiveAggregate(
|
||||
WHERE m."measured_at" >= NOW() - INTERVAL '60 days'
|
||||
AND m."measured_at" < NOW() - INTERVAL '30 days'
|
||||
)::double precision AS avg30_last_month,
|
||||
-- A2-M2 — the regression windows anchor on the UTC-midnight day
|
||||
-- boundary, NOT the wall-clock instant, so this cold-fallback path
|
||||
-- returns the SAME boundary-day membership the warm rollup path
|
||||
-- composes from (startOfUtcDay(now - N days) +
|
||||
-- composeWindowedRegression). The expression
|
||||
-- date_trunc('day', NOW() AT TIME ZONE 'UTC') - INTERVAL 'N days'
|
||||
-- is the SQL twin of startOfUtcDay; the trailing AT TIME ZONE 'UTC'
|
||||
-- re-materialises the naive truncation as a UTC instant so the
|
||||
-- measured_at (timestamptz) comparison is on the same wall the
|
||||
-- rollup bucketStart sits on. Without this anchor an account that
|
||||
-- crosses a coverage boundary (warm to cold) saw the boundary day
|
||||
-- flip in/out of the slope window, giving a cache-dependent answer
|
||||
-- to the same request.
|
||||
REGR_SLOPE(
|
||||
m."value",
|
||||
EXTRACT(EPOCH FROM m."measured_at") / 86400.0
|
||||
) FILTER (
|
||||
WHERE m."measured_at" >= NOW() - INTERVAL '7 days'
|
||||
WHERE m."measured_at" >= (date_trunc('day', NOW() AT TIME ZONE 'UTC') - INTERVAL '7 days') AT TIME ZONE 'UTC'
|
||||
)::double precision AS slope7,
|
||||
REGR_R2(
|
||||
m."value",
|
||||
EXTRACT(EPOCH FROM m."measured_at") / 86400.0
|
||||
) FILTER (
|
||||
WHERE m."measured_at" >= NOW() - INTERVAL '7 days'
|
||||
WHERE m."measured_at" >= (date_trunc('day', NOW() AT TIME ZONE 'UTC') - INTERVAL '7 days') AT TIME ZONE 'UTC'
|
||||
)::double precision AS r2_7,
|
||||
REGR_SLOPE(
|
||||
m."value",
|
||||
EXTRACT(EPOCH FROM m."measured_at") / 86400.0
|
||||
) FILTER (
|
||||
WHERE m."measured_at" >= NOW() - INTERVAL '30 days'
|
||||
WHERE m."measured_at" >= (date_trunc('day', NOW() AT TIME ZONE 'UTC') - INTERVAL '30 days') AT TIME ZONE 'UTC'
|
||||
)::double precision AS slope30,
|
||||
REGR_R2(
|
||||
m."value",
|
||||
EXTRACT(EPOCH FROM m."measured_at") / 86400.0
|
||||
) FILTER (
|
||||
WHERE m."measured_at" >= NOW() - INTERVAL '30 days'
|
||||
WHERE m."measured_at" >= (date_trunc('day', NOW() AT TIME ZONE 'UTC') - INTERVAL '30 days') AT TIME ZONE 'UTC'
|
||||
)::double precision AS r2_30,
|
||||
REGR_SLOPE(
|
||||
m."value",
|
||||
EXTRACT(EPOCH FROM m."measured_at") / 86400.0
|
||||
) FILTER (
|
||||
WHERE m."measured_at" >= NOW() - INTERVAL '90 days'
|
||||
WHERE m."measured_at" >= (date_trunc('day', NOW() AT TIME ZONE 'UTC') - INTERVAL '90 days') AT TIME ZONE 'UTC'
|
||||
)::double precision AS slope90,
|
||||
REGR_R2(
|
||||
m."value",
|
||||
EXTRACT(EPOCH FROM m."measured_at") / 86400.0
|
||||
) FILTER (
|
||||
WHERE m."measured_at" >= NOW() - INTERVAL '90 days'
|
||||
WHERE m."measured_at" >= (date_trunc('day', NOW() AT TIME ZONE 'UTC') - INTERVAL '90 days') AT TIME ZONE 'UTC'
|
||||
)::double precision AS r2_90
|
||||
FROM cm m
|
||||
JOIN window_stats ws ON ws."type" = m."type"
|
||||
|
||||
Reference in new issue
Block a user