@@ -188,40 +188,71 @@ const transaction: ReturnType<typeof vi.fn> = vi.fn(
188188 async ( cb : ( tx : any ) => unknown ) : Promise < unknown > => cb ( dbChainMock . db )
189189)
190190
191- const rowsPromise = ( rows : unknown [ ] | null ) => Promise . resolve ( ( rows ?? [ ] ) as unknown [ ] )
191+ /**
192+ * Lazy per-chain rows supplier: dequeues once, at the moment the FIRST default
193+ * thenable actually resolves. A chain whose result comes from a per-test
194+ * override never reaches a default resolution, so its queued set stays
195+ * available for the next chain on that table.
196+ */
197+ type RowsSupplier = ( ) => unknown [ ] | null
198+
199+ const chainRowsSupplier = ( tables : unknown [ ] ) : RowsSupplier => {
200+ let consumed = false
201+ let rows : unknown [ ] | null = null
202+ return ( ) => {
203+ if ( ! consumed ) {
204+ consumed = true
205+ rows = dequeueChainRows ( tables )
206+ }
207+ return rows
208+ }
209+ }
210+
211+ const noRows : RowsSupplier = ( ) => null
212+
213+ /** An awaitable chain step that resolves `getRows()` only when actually awaited. */
214+ const lazyRowsThenable = ( getRows : RowsSupplier ) : any => ( {
215+ then : ( onFulfilled ?: ( rows : unknown [ ] ) => unknown , onRejected ?: ( reason : unknown ) => unknown ) =>
216+ Promise . resolve ( ( getRows ( ) ?? [ ] ) as unknown [ ] ) . then ( onFulfilled , onRejected ) ,
217+ catch : ( onRejected ?: ( reason : unknown ) => unknown ) =>
218+ Promise . resolve ( ( getRows ( ) ?? [ ] ) as unknown [ ] ) . catch ( onRejected ) ,
219+ finally : ( onFinally ?: ( ) => void ) =>
220+ Promise . resolve ( ( getRows ( ) ?? [ ] ) as unknown [ ] ) . finally ( onFinally ) ,
221+ } )
192222
193223// `.limit()` returns a builder that is awaitable and also exposes `.offset()`
194224// for keyset/OFFSET paging (`.limit(n).offset(m)`).
195- const limitBuilder = ( rows : unknown [ ] | null ) => {
196- const thenable : any = rowsPromise ( rows )
197- thenable . offset = spyOrDefault ( offset , ( ) => rowsPromise ( rows ) )
225+ const limitBuilder = ( getRows : RowsSupplier ) => {
226+ const thenable = lazyRowsThenable ( getRows )
227+ thenable . offset = spyOrDefault ( offset , ( ) => lazyRowsThenable ( getRows ) )
198228 return thenable
199229}
200230
201- const terminalBuilder = ( rows : unknown [ ] | null ) : any => {
202- const thenable : any = rowsPromise ( rows )
203- thenable . limit = spyOrDefault ( limit , ( ) => limitBuilder ( rows ) )
204- thenable . orderBy = spyOrDefault ( orderBy , ( ) => terminalBuilder ( rows ) )
231+ const terminalBuilder = ( getRows : RowsSupplier ) : any => {
232+ const thenable = lazyRowsThenable ( getRows )
233+ thenable . limit = spyOrDefault ( limit , ( ) => limitBuilder ( getRows ) )
234+ thenable . orderBy = spyOrDefault ( orderBy , ( ) => terminalBuilder ( getRows ) )
205235 thenable . returning = returning
206236 thenable . groupBy = spyOrDefault ( groupBy , ( ) => {
207- const builder = terminalBuilder ( rows )
208- builder . having = spyOrDefault ( having , ( ) => terminalBuilder ( rows ) )
237+ const builder = terminalBuilder ( getRows )
238+ builder . having = spyOrDefault ( having , ( ) => terminalBuilder ( getRows ) )
209239 return builder
210240 } )
211- thenable . for = spyOrDefault ( forClause , ( ) => terminalBuilder ( rows ) )
241+ thenable . for = spyOrDefault ( forClause , ( ) => terminalBuilder ( getRows ) )
212242 return thenable
213243}
214244
215245// The from/join builder is itself a thenable so `await db.select().from(t)`
216- // (no where clause) also resolves table-routed rows; dequeue happens lazily at
217- // await (or where()) time, so a chain never double-consumes.
218- const joinBuilder = ( tables : unknown [ ] ) : any => ( {
219- where : spyOrDefault ( where , ( ) => terminalBuilder ( dequeueChainRows ( tables ) ) ) ,
220- innerJoin : spyOrDefault ( innerJoin , ( table : unknown ) => joinBuilder ( [ ...tables , table ] ) ) ,
221- leftJoin : spyOrDefault ( leftJoin , ( table : unknown ) => joinBuilder ( [ ...tables , table ] ) ) ,
222- then : ( onFulfilled ?: ( rows : unknown [ ] ) => unknown , onRejected ?: ( reason : unknown ) => unknown ) =>
223- rowsPromise ( dequeueChainRows ( tables ) ) . then ( onFulfilled , onRejected ) ,
224- } )
246+ // (no where clause) also resolves table-routed rows; the chain's single lazy
247+ // supplier means it never double-consumes no matter which step is awaited.
248+ const joinBuilder = ( tables : unknown [ ] ) : any => {
249+ const getRows = chainRowsSupplier ( tables )
250+ const builder = lazyRowsThenable ( getRows )
251+ builder . where = spyOrDefault ( where , ( ) => terminalBuilder ( getRows ) )
252+ builder . innerJoin = spyOrDefault ( innerJoin , ( table : unknown ) => joinBuilder ( [ ...tables , table ] ) )
253+ builder . leftJoin = spyOrDefault ( leftJoin , ( table : unknown ) => joinBuilder ( [ ...tables , table ] ) )
254+ return builder
255+ }
225256
226257const selectBuilder = ( ) => ( {
227258 from : spyOrDefault ( from , ( table : unknown ) => joinBuilder ( [ table ] ) ) ,
@@ -230,7 +261,7 @@ const selectBuilder = () => ({
230261// Mutation chains route nothing: their where() resolves the plain default so a
231262// mutation can never consume rows queued for a select.
232263const mutationWhere = ( ) => ( {
233- where : spyOrDefault ( where , ( ) => terminalBuilder ( null ) ) ,
264+ where : spyOrDefault ( where , ( ) => terminalBuilder ( noRows ) ) ,
234265} )
235266
236267export const dbChainMockFns = {
@@ -301,6 +332,19 @@ export function resetDbChainMock(): void {
301332 transaction . mockImplementation ( async ( cb : ( tx : typeof dbChainMock . db ) => unknown ) =>
302333 cb ( dbChainMock . db )
303334 )
335+ // The stable db-instance entry points are wrappers around the spies above; a
336+ // suite may have overridden them directly, so restore their original
337+ // implementations too (mockReset restores the fn passed to vi.fn()).
338+ for ( const key of [
339+ 'select' ,
340+ 'selectDistinct' ,
341+ 'selectDistinctOn' ,
342+ 'insert' ,
343+ 'update' ,
344+ 'delete' ,
345+ ] as const ) {
346+ ; ( dbInstance [ key ] as ChainSpy ) . mockReset ( )
347+ }
304348}
305349
306350/**
0 commit comments