@@ -124,7 +124,9 @@ const JOB_QUEUE: ObjectDef = {
124124 updated_at : Field . datetime ( { label : 'Updated At' , required : false } ) ,
125125 } ,
126126 indexes : [
127- { fields : [ 'queue' , 'status' , 'scheduled_for' ] } ,
127+ // [#17612] Widened from `['queue','status','scheduled_for']` — verbatim
128+ // from `sys-job-queue.object.ts`, which is where the reasoning lives.
129+ { fields : [ 'queue' , 'status' , 'priority' , 'scheduled_for' , 'id' ] } ,
128130 { fields : [ 'idempotency_key' , 'queue' ] } ,
129131 { fields : [ 'status' ] } ,
130132 ] ,
@@ -168,7 +170,7 @@ const EXPECTED_NAMES: Record<string, string[]> = {
168170 `sqlite_autoindex_${ DELIVERY . name } _1` ,
169171 ] . sort ( ) ,
170172 [ JOB_QUEUE . name ] : [
171- buildIndexName ( JOB_QUEUE . name , [ 'queue' , 'status' , 'scheduled_for' ] , false ) ,
173+ buildIndexName ( JOB_QUEUE . name , [ 'queue' , 'status' , 'priority' , ' scheduled_for' , 'id '] , false ) ,
172174 buildIndexName ( JOB_QUEUE . name , [ 'idempotency_key' , 'queue' ] , false ) ,
173175 buildIndexName ( JOB_QUEUE . name , [ 'status' ] , false ) ,
174176 `sqlite_autoindex_${ JOB_QUEUE . name } _1` ,
@@ -185,6 +187,18 @@ const EXPECTED_NAMES: Record<string, string[]> = {
185187} ;
186188
187189const CLAIM_INDEX = buildIndexName ( DELIVERY . name , [ 'status' , 'partition_key' , 'next_attempt_at' ] , false ) ;
190+ /** [#17612] The index `sys_job_queue`'s claim path must be served by, end to end. */
191+ const JOB_CLAIM_INDEX = buildIndexName (
192+ JOB_QUEUE . name ,
193+ [ 'queue' , 'status' , 'priority' , 'scheduled_for' , 'id' ] ,
194+ false ,
195+ ) ;
196+ /** The shape the remote face provisioned BEFORE #17612 — this file's negative control. */
197+ const JOB_QUEUE_PRE_17612 : ObjectDef [ 'indexes' ] = [
198+ { fields : [ 'queue' , 'status' , 'scheduled_for' ] } ,
199+ { fields : [ 'idempotency_key' , 'queue' ] } ,
200+ { fields : [ 'status' ] } ,
201+ ] ;
188202const DEDUP_INDEX = buildIndexName ( DELIVERY . name , [ 'notification_id' , 'recipient_id' , 'channel' ] , true ) ;
189203
190204/** A fresh copy per sync — neither face may see a definition the other one mutated. */
@@ -275,20 +289,27 @@ async function remoteFace(client: Client = createClient({ url: 'file::memory:' }
275289}
276290
277291/** Every statement the driver hands the client, in order, with the call that carried it. */
278- function countingClient ( inner : Client ) : { client : Client ; calls : Array < { via : 'execute' | 'batch' ; sql : string [ ] } > } {
279- const calls : Array < { via : 'execute' | 'batch' ; sql : string [ ] } > = [ ] ;
292+ function countingClient ( inner : Client ) : {
293+ client : Client ;
294+ calls : Array < { via : 'execute' | 'batch' ; sql : string [ ] ; args : unknown [ ] [ ] } > ;
295+ } {
296+ const calls : Array < { via : 'execute' | 'batch' ; sql : string [ ] ; args : unknown [ ] [ ] } > = [ ] ;
280297 const sqlOf = ( s : InStatement ) => ( typeof s === 'string' ? s : s . sql ) ;
298+ // [#17612] The bound values, alongside the text — a captured statement is only
299+ // re-runnable (`EXPLAIN QUERY PLAN`) if its placeholders can be filled again.
300+ const argsOf = ( s : InStatement ) : unknown [ ] =>
301+ typeof s === 'string' ? [ ] : Array . isArray ( s . args ) ? [ ...s . args ] : [ ] ;
281302 const client = new Proxy ( inner , {
282303 get ( target , prop ) {
283304 if ( prop === 'execute' ) {
284305 return ( stmt : InStatement , ...rest : unknown [ ] ) => {
285- calls . push ( { via : 'execute' , sql : [ sqlOf ( stmt ) ] } ) ;
306+ calls . push ( { via : 'execute' , sql : [ sqlOf ( stmt ) ] , args : [ argsOf ( stmt ) ] } ) ;
286307 return ( target . execute as ( ...a : unknown [ ] ) => unknown ) . call ( target , stmt , ...rest ) ;
287308 } ;
288309 }
289310 if ( prop === 'batch' ) {
290311 return ( stmts : InStatement [ ] , ...rest : unknown [ ] ) => {
291- calls . push ( { via : 'batch' , sql : stmts . map ( sqlOf ) } ) ;
312+ calls . push ( { via : 'batch' , sql : stmts . map ( sqlOf ) , args : stmts . map ( argsOf ) } ) ;
292313 return ( target . batch as ( ...a : unknown [ ] ) => unknown ) . call ( target , stmts , ...rest ) ;
293314 } ;
294315 }
@@ -299,6 +320,34 @@ function countingClient(inner: Client): { client: Client; calls: Array<{ via: 'e
299320 return { client, calls } ;
300321}
301322
323+ /**
324+ * [#17612] The job-claim SELECT as the driver EMITS it, with its bound values —
325+ * driven from `DbQueueAdapter.claimBatch`'s query shape through a throwaway
326+ * remote face, so the statement under test is the driver's own rather than a
327+ * second copy of it maintained here.
328+ */
329+ async function capturedJobClaim ( ) : Promise < { sql : string ; args : unknown [ ] } > {
330+ const { client, calls } = countingClient ( createClient ( { url : 'file::memory:' } ) ) ;
331+ const face = await remoteFace ( client ) ;
332+ await face . driver . initObjects ( [ fresh ( JOB_QUEUE ) ] ) ;
333+ calls . length = 0 ;
334+ await face . driver . find ( JOB_QUEUE . name , {
335+ where : {
336+ queue : 'q' ,
337+ status : 'pending' ,
338+ $or : [ { scheduled_for : null } , { scheduled_for : { $lte : '2026-09-14T00:00:00.000Z' } } ] ,
339+ } ,
340+ limit : 30 ,
341+ orderBy : [
342+ { field : 'priority' , order : 'asc' } ,
343+ { field : 'scheduled_for' , order : 'asc' } ,
344+ ] ,
345+ } as never ) ;
346+ const select = calls . find ( ( c ) => / ^ S E L E C T \b / i. test ( c . sql [ 0 ] ?? '' ) ) ;
347+ if ( ! select ) throw new Error ( '[#17612] no SELECT reached the client — the capture is void' ) ;
348+ return { sql : select . sql [ 0 ] ! , args : select . args [ 0 ] ! } ;
349+ }
350+
302351const INDEX_DDL = / ^ \s * C R E A T E \s + ( U N I Q U E \s + ) ? I N D E X \b / i;
303352const INDEX_NAME_READ = / F R O M \s + s q l i t e _ m a s t e r \s + W H E R E \s + t y p e \s * = \s * ' i n d e x ' / i;
304353
@@ -432,6 +481,43 @@ describe('[#17609] declared object-level indexes land identically on both TursoD
432481 expect ( remotePlan ) . not . toMatch ( / S C A N s y s _ n o t i f i c a t i o n _ d e l i v e r y / ) ;
433482 expect ( remotePlan ) . toBe ( await plan ( local . query ) ) ;
434483 } ) ;
484+
485+ it ( '[#17612] serves the job claim query with NO sorter — every ORDER BY term indexed, both faces' , async ( ) => {
486+ // The statement is CAPTURED from the driver, never retyped: the ORDER BY
487+ // the planner sees carries one term `DbQueueAdapter.claimBatch` never
488+ // wrote — `id`, the deterministic-paging tie-breaker `SqlDriver.orderKeysFor`
489+ // appends to every paged read (ADR-0053 D-A1 / objectstack#4363) — and that
490+ // term is exactly why an index stopping at `scheduled_for` still left a
491+ // sorter in the plan. A retyped literal would have hidden it.
492+ const claim = await capturedJobClaim ( ) ;
493+ expect ( claim . sql ) . toContain ( 'ORDER BY "priority" ASC, "scheduled_for" ASC, "id" ASC' ) ;
494+
495+ const local = await localFace ( ) ;
496+ const remote = await remoteFace ( ) ;
497+ await local . driver . initObjects ( [ fresh ( JOB_QUEUE ) ] ) ;
498+ await remote . driver . initObjects ( [ fresh ( JOB_QUEUE ) ] ) ;
499+
500+ const plan = async ( query : Query ) =>
501+ ( await query ( `EXPLAIN QUERY PLAN ${ claim . sql } ` , claim . args ) ) . map ( ( r ) => String ( r . detail ) ) . join ( '\n' ) ;
502+ const remotePlan = await plan ( remote . query ) ;
503+
504+ expect ( remotePlan ) . toContain ( `SEARCH ${ JOB_QUEUE . name } USING INDEX ${ JOB_CLAIM_INDEX } (queue=? AND status=?)` ) ;
505+ // ⛔ No sorter at all — not "a cheaper sorter". The whole ORDER BY is index order.
506+ expect ( remotePlan ) . not . toMatch ( / U S E T E M P B - T R E E / ) ;
507+ expect ( remotePlan ) . not . toMatch ( new RegExp ( `SCAN ${ JOB_QUEUE . name } ` ) ) ;
508+ expect ( remotePlan ) . toBe ( await plan ( local . query ) ) ;
509+
510+ // NEGATIVE CONTROL — the SAME statement against the index set this table
511+ // carried before #17612. If this reads clean, the assertions above are
512+ // measuring nothing.
513+ const before = await remoteFace ( ) ;
514+ await before . driver . initObjects ( [ { ...fresh ( JOB_QUEUE ) , indexes : JOB_QUEUE_PRE_17612 ! . map ( ( i ) => ( { ...i } ) ) } ] ) ;
515+ const beforePlan = ( await before . query ( `EXPLAIN QUERY PLAN ${ claim . sql } ` , claim . args ) )
516+ . map ( ( r ) => String ( r . detail ) )
517+ . join ( '\n' ) ;
518+ expect ( beforePlan ) . toMatch ( / U S E T E M P B - T R E E F O R O R D E R B Y / ) ;
519+ expect ( beforePlan ) . not . toContain ( JOB_CLAIM_INDEX ) ;
520+ } ) ;
435521} ) ;
436522
437523describe ( '[#17609] a declared index the retrofit cannot create is reported at `error`, never forced' , ( ) => {
0 commit comments