Skip to content

Attribution is wrong when a builder is constructed and executed in different places #12

Closed

Description

@Truta446

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

  • CallSite pair on the finding — builtAt and executedAt, 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

  1. added 2 commits that reference this issue on Aug 13, 2026
    ba47af1
    9dda1dd
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    help wantedExtra attention is wanted

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions