Repository navigation
Description
Raised by Mads Hansen in the comments of the write-up, and confirmed by measurement.
The Drizzle adapter captures the call site when the query is built — that is what recovers the line at all, since it is gone by execution time. But construction and execution are not always in the same place, and when they differ we report the wrong one.
Measured
Three shapes, against the adapter as it stands today:
| Shape | Reported | Where the N+1 actually is |
|---|---|---|
| Helper builds, route executes in a loop | line 33, inside baseQuery |
the loop, line 38 |
| Builder created once, reused in a loop | line 43, outside the loop | the loop, line 45 |
Promise.all with inline construction |
line 53 ✅ | line 53 |
The first is the common one in real code:
// repositories/items.ts
function baseQuery() {
return db.select().from(items); // <- we report this line
}
// routes/orders.ts
for (const order of orders) {
await baseQuery().where(eq(items.orderId, order.id)); // <- the N+1 lives here
}The report names the generic helper, which is shared by every caller and tells you nothing about which one is looping. Reused and prepared builders make it worse: the attribution points outside the loop entirely.
Direction
Mads' suggestion is to keep two origins rather than one:
builtAt— where the builder was constructed (what we have today)executedAt— the call site at the moment execution was triggered
The second is recoverable more often than it sounds. In the helper case, .where(i) is called from the loop, synchronously, so the caller is on the stack right then. Capturing on the last chained call before execution — rather than only on the first — would fix shapes 1 and 2.
It does not fix everything. If the whole chain lives inside the helper (function q(id) { return db.select().from(items).where(eq(items.id, id)) }), the last construction call is still in the helper. That is why keeping both origins, and reporting the more specific one, beats replacing one heuristic with another.
What this needs
-
CallSitepair on the finding —builtAtandexecutedAt, with the report showing the useful one and mentioning the other when they differ - An explicit override so an adapter that knows the semantic call site can set it
- Regression tests for: helper-built, reused builder, nested scopes, transactions, inserts, and concurrent execution proving attribution cannot bleed across requests
That last one matters most. AsyncLocalStorage should isolate concurrent requests correctly, but "should" is not a test, and a wrong line number is worse than no line number — it sends someone to the wrong file with confidence.
Note
Shapes 1 and 2 currently produce a confidently wrong answer, which is the worst failure mode for this kind of tool. Until this is fixed, that is worth knowing about.
Activity
Fix attribution across build/execute, add a query budget (Truta446#14)
Add a Contributors section to the README (#21)
Release 0.7.0: value sampling, a benchmark, and Kysely measured (#28)