Files
Marc-André Bombeck d640eea32a test(integration): hold statement budgets on the hot routes
tests/integration/query-counter.ts counts the statements the app sends
while a function runs, by wrapping Client.prototype.query on the copy of pg
the Prisma adapter itself resolves (so a second pg in node_modules cannot
leave it reading zero; a window with no statement throws). Background tasks
are settled before the window closes, so fire-and-forget writes count with
the request that started them.

query-budget-hot-routes.test.ts seeds a small account (four months of the
main types with overlapping sources, stats rows, sleep, glucose, a
medication, workouts and mood), warms each route once, evicts the user's
caches and counts the next call. Budgets, measured on this fixture:
/api/auth/me 18, dashboard summary 86, dashboard snapshot 103, pulse series
30/365/3650 days 6 each, day view 48, insights health status 60,
correlations 44, a 500-row batch ingest 31. A failure prints every
statement grouped by text.

Proof: one extra `SELECT 1` before every handler in apiHandler failed all
ten budgets by exactly one (18 → 19, 86 → 87, … 31 → 32), which also shows
the counts are deterministic; with the probe removed all ten pass.
2026-10-22 05:38:45 +02:00

89 lines
3.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Counts the SQL statements the application sends to Postgres while a
* function runs.
*
* Every statement the Prisma client sends goes through `pg`'s
* `Client.prototype.query` (the driver adapter's pool hands out `pg`
* clients, raw queries included, transactions' BEGIN/COMMIT included). The
* prototype is patched once, on the `pg` instance the adapter itself
* resolves, so a second copy of `pg` elsewhere in node_modules cannot leave
* the counter reading zero; `countStatements` refuses a window that saw no
* statement at all for the same reason.
*
* Background work a request starts and does not await (the fire-and-forget
* tasks) is settled before the window closes, so it is counted with the
* request that caused it.
*/
import { createRequire } from "node:module";
interface PgClientPrototype {
query: (...args: unknown[]) => unknown;
}
const requireHere = createRequire(import.meta.url);
const adapterEntry = requireHere.resolve("@prisma/adapter-pg");
const pg = createRequire(adapterEntry)("pg") as {
Client: { prototype: PgClientPrototype };
};
const state = { active: false, statements: [] as string[] };
const PATCHED = Symbol.for("healthlog.queryCounter");
const proto = pg.Client.prototype as PgClientPrototype & {
[PATCHED]?: boolean;
};
if (!proto[PATCHED]) {
const original = proto.query;
proto.query = function patchedQuery(this: unknown, ...args: unknown[]) {
if (state.active) {
const first = args[0] as string | { text?: string } | undefined;
const text = typeof first === "string" ? first : (first?.text ?? "?");
state.statements.push(text.replace(/\s+/g, " ").trim().slice(0, 160));
}
return original.apply(this, args);
};
proto[PATCHED] = true;
}
export interface StatementCount<T> {
result: T;
count: number;
statements: string[];
}
export async function countStatements<T>(
fn: () => Promise<T>,
): Promise<StatementCount<T>> {
const { settleBackgroundTasks } =
await import("@/lib/logging/background-tasks");
await settleBackgroundTasks();
state.statements = [];
state.active = true;
let result: T;
try {
result = await fn();
await settleBackgroundTasks();
} finally {
state.active = false;
}
const statements = state.statements;
if (statements.length === 0) {
throw new Error(
"countStatements saw no statement at all. Either the call reads " +
"nothing from Postgres, or the counter is patched on a different " +
"copy of pg than the one the Prisma adapter uses.",
);
}
return { result, count: statements.length, statements };
}
/** The statements grouped by text, most frequent first, for a failure message. */
export function summariseStatements(statements: readonly string[]): string {
const counts = new Map<string, number>();
for (const s of statements) counts.set(s, (counts.get(s) ?? 0) + 1);
return [...counts.entries()]
.sort((a, b) => b[1] - a[1])
.map(([s, n]) => `${String(n).padStart(4)} × ${s}`)
.join("\n");
}