I was building a runtime N+1 query detector for Node.
The detection part worked on the first afternoon.
Getting it to tell you which line of your code caused the problem took considerably longer, and taught me something about how ORMs execute queries that I had not thought about before.
This is that story, and the fix.
The symptom The detector instruments your database driver.
When the same query shape runs many times inside one request, it reports it — along with the file and line that issued it, which is the part that actually saves you time: That worked.
Then I pointed it at an app using Drizzle and got this instead: Detected, counted, and attributed to nothing.
Do not theorise.
Dump the stack My first instinct was that my frame filter was too aggressive — it skips , , and the library's own frames, so maybe it was eating something it should not have.
Rather than guess, I printed the whole stack at the exact moment the driver was called: Here is what came back for a single : Twelve frames.
Not one of them belongs to the application.
My filter was innocent — there was nothing to find.
Why the frames are gone Look at frame 11: .
A Drizzle query is a lazy thenable. does not run anything — it builds an object.
The query executes when something calls on it, and when you write , the thing calling is the JavaScript runtime, not your code.
By that point your function has already returned.
Its frame is gone from the stack.
The runtime picks the thenable up from the microtask queue and calls into Drizzle, and the whole call chain from there down belongs to the ORM.
So the information is not being filtered out.
It no longer exists.
TypeORM does not have this problem This is where it gets interesting, because I assumed every ORM would behave the same way.
I measured instead of assuming, and TypeORM came out fine: Line numbers, function names, everything.
The difference is who triggers execution. is an function that you call.
Node keeps async stack traces across boundaries inside that chain, so your frame survives all the way down to the driver.
Drizzle's is on a thenable you built but did not call.
That is the distinction — not "Drizzle is worse", but "lazy execution moves the call out of your stack".
Worth remembering next time you look at a stack trace and it seems too short.
Getting the line back The stack is useless at execution time.
But there is a moment when the caller is on the stack: while the query is being built. is a plain synchronous chain of method calls.
So: capture the call site during construction, carry it to execution, and let the driver-level instrumentation use it instead of walking the stack.
Carrying it is the interesting half, because construction and execution are separated by an .
That is exactly what is for: The driver adapter then prefers the ambient value over its own stack walk: And the ORM side wraps the builder so that chaining preserves the call site, and executing publishes it: The call site is captured once, when you call , and travels with the builder through every chained method until something executes it.
Net result: the SQL comes from the driver, the line number comes from the ORM.
The result Same query, same app, before and after: That is the actual before and after from the same script, against PostgreSQL 16, Drizzle 0.45.2 and 3.4.9 — real database, not fixtures.
Line 35 is where is called inside the loop.
One trap worth knowing The first version of this had a bug that took a while to see: I treated as an execution method.
In , executes a query.
In Drizzle, builds an insert.
Treating it as execution broke the chain, so every insert silently lost its attribution — while selects kept working perfectly.
If you write something like this, be precise about which methods in your target library actually execute, and test inserts separately from selects.
A bug that only affects half your cases is worse than one that breaks everything, because you will not notice it.
Two things I would tell past me Measu