@@ -64,18 +64,24 @@ export function createMockSqlOperators() {
6464 * Table-routed result queues.
6565 *
6666 * `queueTableRows(schemaMock.member, [rowA, rowB])` enqueues one result set for
67- * the next select chain whose `.from()` (or subsequent join) references that
68- * table. Each chain consumes at most one queued set (FIFO per table); chains
67+ * the next select chain whose `.from()` (or a subsequent `innerJoin`/
68+ * `leftJoin`) references that table. Each chain consumes at most one queued
69+ * set (FIFO per table, `.from()` table checked before join tables); chains
6970 * against tables with no queued sets resolve the chain-fn defaults (empty
7071 * array). Queues are cleared by `resetDbChainMock()`.
7172 *
7273 * The queue is keyed by table object identity, so pass the same schema-mock
73- * table object the code under test passes to `.from()`.
74+ * table object the code under test passes to `.from()` / the join.
75+ *
76+ * Routing assumes each select chain is built left-to-right before the next
77+ * chain starts — the norm for code under test (`Promise.all` over fully-built
78+ * chains is fine; interleaving *construction* of two chains is not). Mutation
79+ * chains (`update`/`delete`/`insert`) never consume select queues.
7480 */
7581const tableRowQueues = new Map < unknown , unknown [ ] [ ] > ( )
7682
77- /** The `.from()` table of the select chain currently being built. */
78- let activeTable : unknown
83+ /** Tables of the select chain currently being built: `.from()` first, then joins . */
84+ let activeTables : unknown [ ] = [ ]
7985
8086/** Rows dequeued for the current chain, shared by every downstream terminal. */
8187let activeRows : unknown [ ] | null = null
@@ -89,10 +95,13 @@ export function queueTableRows(table: unknown, rows: unknown[]): void {
8995 else tableRowQueues . set ( table , [ rows ] )
9096}
9197
92- function dequeueRows ( table : unknown ) : unknown [ ] | null {
93- const queue = tableRowQueues . get ( table )
94- if ( ! queue || queue . length === 0 ) return null
95- return queue . shift ( ) ?? null
98+ /** Dequeues the first queued set among the chain's tables (from before joins). */
99+ function dequeueActiveRows ( ) : unknown [ ] | null {
100+ for ( const table of activeTables ) {
101+ const queue = tableRowQueues . get ( table )
102+ if ( queue && queue . length > 0 ) return queue . shift ( ) ?? null
103+ }
104+ return null
96105}
97106
98107/**
@@ -178,7 +187,7 @@ const onConflictDoNothing = vi.fn(() => ({ returning }) as unknown as Promise<vo
178187const whereBuilder = ( ) => {
179188 // Dequeue table-routed rows when the where clause materializes; every
180189 // downstream terminal (limit/orderBy/...) then resolves the same rows.
181- activeRows = dequeueRows ( activeTable )
190+ activeRows = dequeueActiveRows ( )
182191 // Some call sites await the where directly (no limit/orderBy), so the
183192 // builder is itself a thenable.
184193 const thenable : any = chainRows ( )
@@ -191,15 +200,24 @@ const whereBuilder = () => {
191200}
192201const where = vi . fn ( whereBuilder )
193202
194- const joinBuilder = ( ) : { where : typeof where ; innerJoin : any ; leftJoin : any } => ( {
203+ // The from/join builder is itself a thenable so `await db.select().from(t)`
204+ // (no where clause) also resolves table-routed rows. Dequeue happens lazily at
205+ // await time, so a chain that continues into `.where()` never double-consumes.
206+ const joinBuilder = ( ) : { where : typeof where ; innerJoin : any ; leftJoin : any ; then : any } => ( {
195207 where,
196208 innerJoin,
197209 leftJoin,
210+ then : ( onFulfilled ?: ( rows : unknown [ ] ) => unknown , onRejected ?: ( reason : unknown ) => unknown ) =>
211+ Promise . resolve ( ( dequeueActiveRows ( ) ?? [ ] ) as unknown [ ] ) . then ( onFulfilled , onRejected ) ,
198212} )
199- const innerJoin : ReturnType < typeof vi . fn > = vi . fn ( joinBuilder )
200- const leftJoin : ReturnType < typeof vi . fn > = vi . fn ( joinBuilder )
213+ const joinStep = ( table ?: unknown ) => {
214+ activeTables . push ( table )
215+ return joinBuilder ( )
216+ }
217+ const innerJoin : ReturnType < typeof vi . fn > = vi . fn ( joinStep )
218+ const leftJoin : ReturnType < typeof vi . fn > = vi . fn ( joinStep )
201219const from = vi . fn ( ( table ?: unknown ) => {
202- activeTable = table
220+ activeTables = [ table ]
203221 activeRows = null
204222 return joinBuilder ( )
205223} )
@@ -209,9 +227,16 @@ const selectDistinct = vi.fn(() => ({ from }))
209227const selectDistinctOn = vi . fn ( ( ) => ( { from } ) )
210228const values = vi . fn ( ( ) => ( { returning, onConflictDoUpdate, onConflictDoNothing } ) )
211229const insert = vi . fn ( ( ) => ( { values } ) )
212- const set = vi . fn ( ( ) => ( { where } ) )
213- const update = vi . fn ( ( ) => ( { set } ) )
214- const del = vi . fn ( ( ) => ( { where } ) )
230+ // Mutation chains clear the routing context so their `where()` never consumes
231+ // rows queued for a select.
232+ const mutationStep = < T > ( next : T ) : T => {
233+ activeTables = [ ]
234+ activeRows = null
235+ return next
236+ }
237+ const set = vi . fn ( ( ) => mutationStep ( { where } ) )
238+ const update = vi . fn ( ( ) => mutationStep ( { set } ) )
239+ const del = vi . fn ( ( ) => mutationStep ( { where } ) )
215240const query = vi . fn ( ( ) => Promise . resolve ( [ ] as unknown [ ] ) )
216241const transaction : ReturnType < typeof vi . fn > = vi . fn (
217242 async ( cb : ( tx : any ) => unknown ) : Promise < unknown > => cb ( dbChainMock . db )
@@ -255,26 +280,26 @@ export const dbChainMockFns = {
255280 */
256281export function resetDbChainMock ( ) : void {
257282 tableRowQueues . clear ( )
258- activeTable = undefined
283+ activeTables = [ ]
259284 activeRows = null
260285 select . mockImplementation ( ( ) => ( { from } ) )
261286 selectDistinct . mockImplementation ( ( ) => ( { from } ) )
262287 selectDistinctOn . mockImplementation ( ( ) => ( { from } ) )
263288 from . mockImplementation ( ( table ?: unknown ) => {
264- activeTable = table
289+ activeTables = [ table ]
265290 activeRows = null
266291 return joinBuilder ( )
267292 } )
268- innerJoin . mockImplementation ( joinBuilder )
269- leftJoin . mockImplementation ( joinBuilder )
293+ innerJoin . mockImplementation ( joinStep )
294+ leftJoin . mockImplementation ( joinStep )
270295 where . mockImplementation ( whereBuilder )
271296 insert . mockImplementation ( ( ) => ( { values } ) )
272297 values . mockImplementation ( ( ) => ( { returning, onConflictDoUpdate, onConflictDoNothing } ) )
273298 onConflictDoUpdate . mockImplementation ( ( ) => ( { returning } ) as unknown as Promise < void > )
274299 onConflictDoNothing . mockImplementation ( ( ) => ( { returning } ) as unknown as Promise < void > )
275- update . mockImplementation ( ( ) => ( { set } ) )
276- set . mockImplementation ( ( ) => ( { where } ) )
277- del . mockImplementation ( ( ) => ( { where } ) )
300+ update . mockImplementation ( ( ) => mutationStep ( { set } ) )
301+ set . mockImplementation ( ( ) => mutationStep ( { where } ) )
302+ del . mockImplementation ( ( ) => mutationStep ( { where } ) )
278303 limit . mockImplementation ( limitBuilder )
279304 offset . mockImplementation ( chainRows )
280305 orderBy . mockImplementation ( terminalBuilder )
0 commit comments