mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
fix(rollups): step one tier finer when the coarse tier comes back empty
The granularity router read only the coarsest tier the window cleared, and a coarse bucket that starts before the window is left out. Data that sat only in that leading partial bucket, such as three months at the start of a 731-day window, read as no data at all. The router now steps one tier finer until a tier has buckets, down to DAY.
This commit is contained in:
1 parent
bb12d942c0
commit
6769f90f4c
2 files changed
+52
-19
No files matched your search
@@ -213,7 +213,33 @@ describe("readBestGranularityRollups", () => {
|
||||
it("returns null when the window holds no buckets", async () => {
|
||||
const result = await readBestGranularityRollups("user", "WEIGHT", 1095);
|
||||
expect(result).toBeNull();
|
||||
expect(queryRaw).toHaveBeenCalledTimes(1);
|
||||
// YEAR, MONTH and WEEK folds, then the DAY tier.
|
||||
expect(queryRaw).toHaveBeenCalledTimes(3);
|
||||
expect(findMany).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("steps one tier finer when the data sits only in the leading partial bucket", async () => {
|
||||
// A 731-day window on 2026-09-26 starts 2024-09-25. October to December
|
||||
// 2024 fold into the YEAR bucket 2024-01-01, which starts before the
|
||||
// window and is left out; the MONTH tier starts inside it.
|
||||
vi.setSystemTime(new Date("2026-09-26T12:00:00.000Z"));
|
||||
const readings: Array<[string, number]> = [
|
||||
["2024-10-05", 80],
|
||||
["2024-11-05", 81],
|
||||
["2024-12-05", 82],
|
||||
];
|
||||
queryRaw
|
||||
.mockResolvedValueOnce([folded("2024-01-01T00:00:00.000Z", readings)])
|
||||
.mockResolvedValueOnce(
|
||||
readings.map(([day, value]) =>
|
||||
folded(`${day.slice(0, 7)}-01T00:00:00.000Z`, [[day, value]]),
|
||||
),
|
||||
);
|
||||
const result = await readBestGranularityRollups("user", "WEIGHT", 731);
|
||||
expect(result?.granularity).toBe("MONTH");
|
||||
expect(result?.rows.map((r) => r.mean)).toEqual([80, 81, 82]);
|
||||
expect(sqlOf(queryRaw.mock.calls[0])).toContain("'year'");
|
||||
expect(sqlOf(queryRaw.mock.calls[1])).toContain("'month'");
|
||||
});
|
||||
|
||||
it("composes a coarse bucket's mean, spread and slope from its days", async () => {
|
||||
|
||||
@@ -125,25 +125,32 @@ export async function readBestGranularityRollups(
|
||||
userPriorityJson !== undefined
|
||||
? userPriorityJson
|
||||
: await loadUserSourcePriority(userId);
|
||||
// Every tier is folded from the same canonical DAY buckets, so a tier
|
||||
// either has coverage or none has: pick the coarsest floor the window
|
||||
// clears and read it once.
|
||||
const floor = GRANULARITY_FLOORS.find((f) => windowDays >= f.minWindowDays);
|
||||
if (!floor) return null;
|
||||
const rows = await readGranularity(
|
||||
userId,
|
||||
type,
|
||||
floor.granularity,
|
||||
since,
|
||||
// Trailing-window semantics: no upper bound. This router serves the
|
||||
// "last N days to now" probes (summaries-slice / health-score); the
|
||||
// requested-window bounding lives on `readTieredRollupSeries`.
|
||||
null,
|
||||
priority,
|
||||
// Every tier is folded from the same canonical DAY buckets, but a coarse
|
||||
// bucket that starts before `since` is left out (it holds only part of its
|
||||
// days). Data that sits only in that leading partial bucket, say three
|
||||
// months at the start of a 731-day window, reads as empty at YEAR: step one
|
||||
// tier finer until a tier has buckets, down to DAY, which starts inside the
|
||||
// window by construction.
|
||||
const floors = GRANULARITY_FLOORS.filter(
|
||||
(f) => windowDays >= f.minWindowDays,
|
||||
);
|
||||
return rows && rows.length > 0
|
||||
? { granularity: floor.granularity, rows }
|
||||
: null;
|
||||
for (const floor of floors) {
|
||||
const rows = await readGranularity(
|
||||
userId,
|
||||
type,
|
||||
floor.granularity,
|
||||
since,
|
||||
// Trailing-window semantics: no upper bound. This router serves the
|
||||
// "last N days to now" probes (summaries-slice / health-score); the
|
||||
// requested-window bounding lives on `readTieredRollupSeries`.
|
||||
null,
|
||||
priority,
|
||||
);
|
||||
if (rows && rows.length > 0) {
|
||||
return { granularity: floor.granularity, rows };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user