mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
fix(medications): end every medication and intake read on the row id
Postgres returns rows that tie on an ORDER BY in no particular order, and not the same one twice. The medication reads ordered by columns two rows share all the time: every read that hands schedules on included them with no order at all, medications were listed by createdAt alone (one import writes many in a millisecond), intakes by scheduledFor (a twice-daily medication puts every morning dose on one instant), and "the latest intake" lookups by takenAt. A client read the same account in a different order from one request to the next, and a latest-intake lookup picked either of two rows. Every findMany / findFirst on a medication table, nested includes included, now ends its order on the id in the direction of its last key, and schedules are read by window, then id (MEDICATION_SCHEDULE_ORDER, the order the backup already wrote). medication-read-order-guard.test.ts holds the tree to it. medication-read-order.test.ts writes tying rows in the opposite order to the one each read promises, through the medication list and today's intakes, and fails without the tie-breakers. The parity suite's own state reader had the same gap: it sorts rows by content with the ids blanked, and rows that tie then kept whatever order the unordered read returned, so their <id:N> labels swapped under a loaded run. Its reads are ordered by id, which for a cuid is the order the rows were written in. The JSON import's schema left the route, so its entry in the inline schema strictness register goes; the schema module says why its objects are not strict.
This commit is contained in:
1 parent
7936ab9a57
commit
111047491e
57 files changed
+476
-142
No files matched your search
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$comment": "Production files over 800 lines and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
|
||||
"count": 89,
|
||||
"total": 99600,
|
||||
"total": 99598,
|
||||
"entries": {
|
||||
"app/medications/page-client.tsx": 839,
|
||||
"components/charts/mood-chart.tsx": 1166,
|
||||
@@ -72,7 +72,7 @@
|
||||
"lib/jobs/medication-intake-import.ts": 819,
|
||||
"lib/jobs/offhost-backup.ts": 1147,
|
||||
"lib/jobs/reaction-line.ts": 824,
|
||||
"lib/jobs/reminder/medication-reminder-check.ts": 818,
|
||||
"lib/jobs/reminder/medication-reminder-check.ts": 816,
|
||||
"lib/managed-profiles/handover.ts": 986,
|
||||
"lib/mcp/record-search.ts": 1467,
|
||||
"lib/mcp/rich-reads.ts": 1467,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$comment": "Production functions over 200 lines, keyed \"file :: name\", and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
|
||||
"count": 354,
|
||||
"total": 114417,
|
||||
"total": 114410,
|
||||
"entries": {
|
||||
"app/achievements/page.tsx :: AchievementsPage": 206,
|
||||
"app/api/admin/backups/upload/route.ts :: apiHandler(arg)": 314,
|
||||
@@ -319,8 +319,8 @@
|
||||
"lib/jobs/medication-low-stock.ts :: runMedicationLowStockTick": 286,
|
||||
"lib/jobs/oura-backfill.ts :: runOuraBackfillForUser": 236,
|
||||
"lib/jobs/reaction-line.ts :: runReactionLine": 337,
|
||||
"lib/jobs/reminder/medication-reminder-check.ts :: handleReminderCheck": 619,
|
||||
"lib/jobs/reminder/medication-reminder-check.ts :: handleReminderCheck > withBackgroundEvent(arg)": 614,
|
||||
"lib/jobs/reminder/medication-reminder-check.ts :: handleReminderCheck": 616,
|
||||
"lib/jobs/reminder/medication-reminder-check.ts :: handleReminderCheck > withBackgroundEvent(arg)": 611,
|
||||
"lib/jobs/workout-insight-generate.ts :: runWorkoutInsightGenerate": 324,
|
||||
"lib/labs/biomarker-merge.ts :: runMerge": 254,
|
||||
"lib/labs/ocr-extract.ts :: runOcrExtraction": 207,
|
||||
@@ -337,7 +337,7 @@
|
||||
"lib/measurements/import-apple-health-export.ts :: streamParseExportXml > (anonymous) #2": 323,
|
||||
"lib/medications/efficacy/build-efficacy.ts :: buildMedicationEfficacy": 268,
|
||||
"lib/medications/intake-slot-dedup.ts :: dedupeUserIntakeSlots": 321,
|
||||
"lib/medications/list-read.ts :: buildMedicationsList": 310,
|
||||
"lib/medications/list-read.ts :: buildMedicationsList": 309,
|
||||
"lib/medications/scheduling/dose-history.ts :: reconstructDoseHistory": 218,
|
||||
"lib/multipart/stream-to-disk.ts :: streamMultipartToDisk": 272,
|
||||
"lib/notifications/dispatcher.ts :: dispatchNotification": 378,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$comment": "Comment lines that open with a release version, per file under src/. Maintained by src/__tests__/version-stamp-ratchet.test.ts.",
|
||||
"count": 2897,
|
||||
"total": 9290,
|
||||
"total": 9285,
|
||||
"entries": {
|
||||
"__tests__/account-payload-consumer-guard.test.ts": 2,
|
||||
"__tests__/acting-account-boundary-guard.test.ts": 7,
|
||||
@@ -2357,7 +2357,7 @@
|
||||
"lib/jobs/reminder/google-health-sync.ts": 1,
|
||||
"lib/jobs/reminder/insights-handlers.ts": 3,
|
||||
"lib/jobs/reminder/medication-maintenance.ts": 3,
|
||||
"lib/jobs/reminder/medication-reminder-check.ts": 13,
|
||||
"lib/jobs/reminder/medication-reminder-check.ts": 12,
|
||||
"lib/jobs/reminder/mood-cycle-checks.ts": 3,
|
||||
"lib/jobs/reminder/ops-handlers.ts": 3,
|
||||
"lib/jobs/reminder/oura-sync.ts": 1,
|
||||
@@ -2506,7 +2506,7 @@
|
||||
"lib/medications/injection-site-write.ts": 1,
|
||||
"lib/medications/injection-sites.ts": 6,
|
||||
"lib/medications/intake-actionable.ts": 1,
|
||||
"lib/medications/intake-slot-dedup.ts": 18,
|
||||
"lib/medications/intake-slot-dedup.ts": 17,
|
||||
"lib/medications/intake-tracking.ts": 1,
|
||||
"lib/medications/inventory/__tests__/consumption.test.ts": 1,
|
||||
"lib/medications/inventory/__tests__/summary.test.ts": 2,
|
||||
@@ -2515,7 +2515,7 @@
|
||||
"lib/medications/inventory/state-machine.ts": 2,
|
||||
"lib/medications/inventory/summary.ts": 4,
|
||||
"lib/medications/lifecycle.ts": 3,
|
||||
"lib/medications/list-read.ts": 26,
|
||||
"lib/medications/list-read.ts": 23,
|
||||
"lib/medications/med-target-map.ts": 1,
|
||||
"lib/medications/medication-order.ts": 1,
|
||||
"lib/medications/outstanding-doses.ts": 2,
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Every medication and intake read orders its ties.
|
||||
*
|
||||
* Postgres returns rows that tie on an `ORDER BY` in no particular order, and
|
||||
* not the same one twice. The medication reads ordered by columns two rows
|
||||
* share all the time: a schedule list with no order at all, medications by
|
||||
* `createdAt` (one import writes many in a millisecond), intakes by
|
||||
* `scheduledFor` (a twice-daily medication puts every morning dose on the same
|
||||
* instant), "the latest intake" by `takenAt`. A client then read the same
|
||||
* account in a different order from one request to the next, and a "latest"
|
||||
* lookup picked either of two rows. The parity suite saw it as `<id:N>`
|
||||
* labels swapping under a loaded run.
|
||||
*
|
||||
* So every `findMany` / `findFirst` on a medication table, its nested
|
||||
* includes too, states an order that ends on a unique key, and a medication's
|
||||
* schedules are never included unordered (`MEDICATION_SCHEDULE_ORDER`). An
|
||||
* order held in a variable is checked where it is built, and listed below.
|
||||
*/
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
readSourceText,
|
||||
requireFloor,
|
||||
stripComments,
|
||||
walkSourceFiles,
|
||||
} from "./helpers/source-files";
|
||||
|
||||
const SRC = join(__dirname, "..");
|
||||
|
||||
const READ =
|
||||
/\b(?:prisma|tx|db|client)\s*\.\s*(medication|medicationSchedule|medicationIntakeEvent|medicationScheduleRevision|medicationPauseEra|medicationCourse|medicationDoseChange|medicationInventoryItem|medicationInventoryEvent|medicationSideEffect|medicationIngredient|medicationCategoryAssignment)\s*\.\s*(findMany|findFirst|findFirstOrThrow)\s*\(/g;
|
||||
|
||||
/** Unique keys an order may end on, beyond `id`. */
|
||||
const UNIQUE_LAST_KEYS: Record<string, string> = {
|
||||
medicationCategoryAssignment: "medicationId",
|
||||
};
|
||||
|
||||
/** Orders built in a variable, by file, with where the tie-breaker lives. */
|
||||
const ORDER_VARIABLES: Record<string, string> = {
|
||||
"lib/medications/list-intake-events.ts":
|
||||
"`orderBy` ends on `{ id: sortDir }` in both arms, built just above the read.",
|
||||
};
|
||||
|
||||
function closing(text: string, at: number, open: string, close: string) {
|
||||
let depth = 0;
|
||||
for (let i = at; i < text.length; i++) {
|
||||
if (text[i] === open) depth++;
|
||||
else if (text[i] === close && --depth === 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
interface Read {
|
||||
where: string;
|
||||
model: string;
|
||||
orders: string[];
|
||||
variableOrder: boolean;
|
||||
unorderedSchedules: boolean;
|
||||
}
|
||||
|
||||
function reads(): Read[] {
|
||||
const out: Read[] = [];
|
||||
const files = walkSourceFiles(SRC, { floor: 2000 }).filter(
|
||||
(rel) => !rel.startsWith("generated/") && !rel.includes("__tests__"),
|
||||
);
|
||||
for (const rel of files) {
|
||||
const text = stripComments(readSourceText(join(SRC, rel)), rel);
|
||||
for (const match of text.matchAll(READ)) {
|
||||
const open = match.index! + match[0].length - 1;
|
||||
const end = closing(text, open, "(", ")");
|
||||
const call = text.slice(open, end + 1);
|
||||
const orders: string[] = [];
|
||||
for (const order of call.matchAll(/orderBy\s*:\s*([[{])/g)) {
|
||||
const start = order.index! + order[0].length - 1;
|
||||
const bracket = order[1]!;
|
||||
const stop = closing(call, start, bracket, bracket === "[" ? "]" : "}");
|
||||
orders.push(call.slice(start, stop + 1).replace(/\s+/g, " "));
|
||||
}
|
||||
out.push({
|
||||
where: `${rel}:${text.slice(0, match.index).split("\n").length}`,
|
||||
model: match[1]!,
|
||||
orders,
|
||||
variableOrder: /\borderBy\s*(?:,|\})/.test(call),
|
||||
unorderedSchedules: /\bschedules\s*:\s*true\b/.test(call),
|
||||
});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const ALL = reads();
|
||||
|
||||
/** Whether an order's last key is unique for the read's model. */
|
||||
function endsUnique(order: string, model: string): boolean {
|
||||
const keys = [...order.matchAll(/(\w+)\s*:\s*(?:"(?:asc|desc)"|\{|\w)/g)]
|
||||
.map((m) => m[1]!)
|
||||
.filter((key) => key !== "sort" && key !== "nulls");
|
||||
const last = keys.at(-1);
|
||||
return last === "id" || last === UNIQUE_LAST_KEYS[model];
|
||||
}
|
||||
|
||||
describe("medication reads order their ties", () => {
|
||||
it("finds the reads it is about", () => {
|
||||
requireFloor("medication reads", ALL, 150);
|
||||
requireFloor(
|
||||
"ordered medication reads",
|
||||
ALL.filter((read) => read.orders.length > 0),
|
||||
60,
|
||||
);
|
||||
});
|
||||
|
||||
it("ends every stated order on a unique key", () => {
|
||||
const loose = ALL.flatMap((read) =>
|
||||
read.orders
|
||||
.filter((order) => !endsUnique(order, read.model))
|
||||
.map((order) => `${read.where} ${read.model}: ${order}`),
|
||||
);
|
||||
expect(loose).toEqual([]);
|
||||
});
|
||||
|
||||
it("never includes a medication's schedules unordered", () => {
|
||||
expect(
|
||||
ALL.filter((read) => read.unorderedSchedules).map((read) => read.where),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("names every order held in a variable, and nothing else", () => {
|
||||
const files = [
|
||||
...new Set(
|
||||
ALL.filter((read) => read.variableOrder).map(
|
||||
(read) => read.where.split(":")[0]!,
|
||||
),
|
||||
),
|
||||
].sort();
|
||||
expect(files).toEqual(Object.keys(ORDER_VARIABLES).sort());
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user