@@ -198,6 +198,53 @@ helper.describePglite('distributed database mode', { timeout: 20000 }, function
198198 expect ( retried . state ) . toBe ( 'retry' )
199199 } )
200200
201+ it ( 'should retry, not fail, when the backend returns integer columns as strings' , async function ( ) {
202+ // Regression: reinsertFailedJobs read the raw SELECT * rows from the distributed fail path. On
203+ // CockroachDB, INT8 columns come back as strings, so `retry_count < retry_limit` was a
204+ // lexicographic compare — "9" < "10" is false — which permanently failed a job that still had
205+ // retries left. The DISTRIBUTED=true Postgres run can't catch this (node-pg returns numbers
206+ // there), so we simulate CockroachDB's string typing with a wrapper over a real connection.
207+ ctx . boss = await helper . start ( { ...ctx . bossConfig , __test__distributed : true } )
208+
209+ const jobId = await ctx . boss . send ( ctx . schema , { test : 'stringints' } , { retryLimit : 10 } )
210+ helper . assertTruthy ( jobId )
211+
212+ const [ job ] = await ctx . boss . fetch ( ctx . schema )
213+ expect ( job . id ) . toBe ( jobId )
214+
215+ const _db = await helper . getDb ( )
216+ try {
217+ // Put the job one retry short of its limit. Numerically 9 < 10, so it must still retry; only a
218+ // string comparison ("9" < "10" === false) would wrongly fail it.
219+ await _db . executeSql ( `UPDATE ${ ctx . schema } .job SET retry_count = 9 WHERE id = $1` , [ jobId ] )
220+
221+ // Wrap the connection so SELECT * rows return integer columns as strings, exactly as
222+ // CockroachDB's driver returns INT8. The fail path's select is the only SELECT * it issues.
223+ const integerColumns = [ 'priority' , 'retry_limit' , 'retry_count' , 'retry_delay' , 'retry_delay_max' , 'group_tier' , 'expire_seconds' , 'deletion_seconds' , 'pending_dependencies' ]
224+ const cockroachLike = {
225+ executeSql : async ( text : string , values ?: unknown [ ] ) => {
226+ const result = await _db . executeSql ( text , values )
227+ if ( / ^ \s * S E L E C T \* F R O M / i. test ( text ) ) {
228+ for ( const row of result . rows ) {
229+ for ( const col of integerColumns ) {
230+ if ( row [ col ] !== null && row [ col ] !== undefined ) row [ col ] = String ( row [ col ] )
231+ }
232+ }
233+ }
234+ return result
235+ }
236+ }
237+
238+ await ctx . boss . fail ( ctx . schema , jobId , null , { db : cockroachLike } )
239+
240+ const retried = await ctx . boss . getJobById ( ctx . schema , jobId )
241+ helper . assertTruthy ( retried )
242+ expect ( retried . state ) . toBe ( 'retry' )
243+ } finally {
244+ await _db . close ( )
245+ }
246+ } )
247+
201248 it ( 'should compose completeDistributed inside a caller transaction and roll back with it' , async function ( ) {
202249 ctx . boss = await helper . start ( { ...ctx . bossConfig , __test__distributed : true } )
203250
@@ -273,11 +320,29 @@ helper.describePglite('distributed database mode', { timeout: 20000 }, function
273320 expect ( completed . state ) . toBe ( 'completed' )
274321 } )
275322
276- it ( 'should work with noTablePartitioning mode' , async function ( ) {
277- // This test covers the noPartitioning path in plans.ts (lines 244, 260)
323+ it ( 'should return numeric stats counts under the cockroachdb backend' , async function ( ) {
324+ // getQueueStats has its own backend === 'cockroachdb' coercion: the stats counts come from a raw
325+ // stats query, not the normalized getQueues path, so they would otherwise be returned as strings
326+ // on CockroachDB. Selecting the cockroachdb backend on Postgres runs that coercion loop and lets
327+ // us assert the public counts come back as numbers.
328+ ctx . boss = await helper . start ( { ...ctx . bossConfig , backend : 'cockroachdb' } )
329+
330+ const jobId = await ctx . boss . send ( ctx . schema , { test : 'stats' } )
331+ helper . assertTruthy ( jobId )
332+
333+ const stats = await ctx . boss . getQueueStats ( ctx . schema )
334+ expect ( typeof stats . queuedCount ) . toBe ( 'number' )
335+ expect ( stats . totalCount ) . toBe ( 1 )
336+ } )
337+
338+ it ( 'should construct schema with the yugabytedb backend (no partitioning, no advisory locks)' , async function ( ) {
339+ // Exercises the noTablePartitioning + noAdvisoryLocks construction path on plain Postgres by
340+ // selecting the yugabytedb backend, whose only flags are those two (both PostgreSQL-compatible,
341+ // they just remove features). The compatibility flags are derived from `backend` and are not
342+ // settable directly — resolveBackend() overwrites them — so the backend is the only way in.
278343 ctx . boss = await helper . start ( {
279344 ...ctx . bossConfig ,
280- noTablePartitioning : true
345+ backend : 'yugabytedb'
281346 } )
282347
283348 // Basic send/fetch to verify everything works
0 commit comments