Skip to content

Commit 8d0f845

Browse files
committed
fix(budgets): address review findings — explain sig, concurrency, dead code, shouldBlock
- Fix DriverWithExplain to use single-object `explain(request)` matching the actual postgres driver signature (was silently failing via catch block) - Replace shared `observedRows` closure variable with per-plan WeakMap tracking to prevent cross-request row count interference in concurrent executions - Remove dead `void ctx.now()` call whose return value was discarded - Align latency shouldBlock to use OR logic (like row budgets) so strict mode consistently blocks for all budget types
1 parent a001e80 commit 8d0f845

2 files changed

Lines changed: 62 additions & 16 deletions

File tree

packages/2-sql/5-runtime/src/plugins/budgets.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ export interface BudgetsOptions {
1717
}
1818

1919
interface DriverWithExplain {
20-
explain?(
21-
sql: string,
22-
params: unknown[],
23-
): Promise<{ rows: ReadonlyArray<Record<string, unknown>> }>;
20+
explain?(request: {
21+
sql: string;
22+
params: readonly unknown[];
23+
}): Promise<{ rows: ReadonlyArray<Record<string, unknown>> }>;
2424
}
2525

2626
async function computeEstimatedRows(
@@ -32,7 +32,7 @@ async function computeEstimatedRows(
3232
}
3333

3434
try {
35-
const result = await driver.explain(plan.sql, [...plan.params]);
35+
const result = await driver.explain({ sql: plan.sql, params: plan.params });
3636
return extractEstimatedRows(result.rows);
3737
} catch {
3838
return undefined;
@@ -196,14 +196,13 @@ export function budgets<TContract = unknown, TAdapter = unknown, TDriver = unkno
196196
const rowSeverity = options?.severities?.rowCount ?? 'error';
197197
const latencySeverity = options?.severities?.latency ?? 'warn';
198198

199-
let observedRows = 0;
199+
const observedRowsByPlan = new WeakMap<ExecutionPlan, { count: number }>();
200200

201201
return Object.freeze({
202202
name: 'budgets',
203203

204204
async beforeExecute(plan: ExecutionPlan, ctx: PluginContext<TContract, TAdapter, TDriver>) {
205-
observedRows = 0;
206-
void ctx.now();
205+
observedRowsByPlan.set(plan, { count: 0 });
207206

208207
if (isQueryAst(plan.ast)) {
209208
if (plan.ast.kind === 'select') {
@@ -217,17 +216,16 @@ export function budgets<TContract = unknown, TAdapter = unknown, TDriver = unkno
217216

218217
async onRow(
219218
_row: Record<string, unknown>,
220-
_plan: ExecutionPlan,
219+
plan: ExecutionPlan,
221220
_ctx: PluginContext<TContract, TAdapter, TDriver>,
222221
) {
223-
void _row;
224-
void _plan;
225-
void _ctx;
226-
observedRows += 1;
227-
if (observedRows > maxRows) {
222+
const state = observedRowsByPlan.get(plan);
223+
if (!state) return;
224+
state.count += 1;
225+
if (state.count > maxRows) {
228226
throw budgetError('BUDGET.ROWS_EXCEEDED', 'Observed row count exceeds budget', {
229227
source: 'observed',
230-
observedRows,
228+
observedRows: state.count,
231229
maxRows,
232230
});
233231
}
@@ -240,7 +238,7 @@ export function budgets<TContract = unknown, TAdapter = unknown, TDriver = unkno
240238
) {
241239
const latencyMs = result.latencyMs;
242240
if (latencyMs > maxLatencyMs) {
243-
const shouldBlock = latencySeverity === 'error' && ctx.mode === 'strict';
241+
const shouldBlock = latencySeverity === 'error' || ctx.mode === 'strict';
244242
emitBudgetViolation(
245243
budgetError('BUDGET.TIME_EXCEEDED', 'Query latency exceeds budget', {
246244
latencyMs,

packages/2-sql/5-runtime/test/budgets.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,33 @@ describe('budgets plugin', () => {
182182
},
183183
timeouts.default,
184184
);
185+
186+
it(
187+
'tracks row counts independently per execution plan',
188+
async () => {
189+
const plugin = budgets({ maxRows: 2 });
190+
const planA = createPlan({ sql: 'INSERT INTO "user" (id) VALUES ($1)' });
191+
const planB = createPlan({ sql: 'INSERT INTO "user" (id) VALUES ($2)' });
192+
const ctxA = createPluginContext();
193+
const ctxB = createPluginContext();
194+
195+
await plugin.beforeExecute?.(planA, ctxA);
196+
await plugin.beforeExecute?.(planB, ctxB);
197+
198+
await plugin.onRow?.({}, planA, ctxA);
199+
await plugin.onRow?.({}, planB, ctxB);
200+
await plugin.onRow?.({}, planA, ctxA);
201+
await plugin.onRow?.({}, planB, ctxB);
202+
203+
await expect(plugin.onRow?.({}, planA, ctxA)).rejects.toMatchObject({
204+
code: 'BUDGET.ROWS_EXCEEDED',
205+
});
206+
await expect(plugin.onRow?.({}, planB, ctxB)).rejects.toMatchObject({
207+
code: 'BUDGET.ROWS_EXCEEDED',
208+
});
209+
},
210+
timeouts.default,
211+
);
185212
});
186213

187214
describe('latency budget (afterExecute)', () => {
@@ -217,6 +244,22 @@ describe('budgets plugin', () => {
217244
timeouts.default,
218245
);
219246

247+
it(
248+
'throws when latency exceeds budget in strict mode even with warn severity',
249+
async () => {
250+
const plugin = budgets({ maxLatencyMs: 100, severities: { latency: 'warn' } });
251+
const plan = createPlan({ sql: 'SELECT 1', meta: { annotations: { limit: 1 } } });
252+
const ctx = createPluginContext({ mode: 'strict' });
253+
const result: AfterExecuteResult = { rowCount: 1, latencyMs: 200, completed: true };
254+
255+
await expect(plugin.afterExecute?.(plan, result, ctx)).rejects.toMatchObject({
256+
code: 'BUDGET.TIME_EXCEEDED',
257+
category: 'BUDGET',
258+
});
259+
},
260+
timeouts.default,
261+
);
262+
220263
it(
221264
'does not warn when latency is within budget',
222265
async () => {
@@ -243,6 +286,7 @@ describe('budgets plugin', () => {
243286
};
244287
const plan = createPlan({
245288
sql: 'SELECT id FROM "user" LIMIT 100',
289+
params: ['a', 'b'],
246290
meta: { annotations: { limit: 100 } },
247291
});
248292
const plugin = budgets({ maxRows: 10_000, explain: { enabled: true } });
@@ -252,6 +296,10 @@ describe('budgets plugin', () => {
252296
code: 'BUDGET.ROWS_EXCEEDED',
253297
details: expect.objectContaining({ source: 'explain' }),
254298
});
299+
expect(explainDriver.explain).toHaveBeenCalledWith({
300+
sql: plan.sql,
301+
params: plan.params,
302+
});
255303
},
256304
timeouts.default,
257305
);

0 commit comments

Comments
 (0)