@@ -91,6 +91,7 @@ async function createRun(
9191 prisma : PrismaClient ,
9292 ctx : SeedContext ,
9393 run : {
94+ id ?: string ;
9495 friendlyId : string ;
9596 taskIdentifier ?: string ;
9697 status ?: any ;
@@ -100,6 +101,7 @@ async function createRun(
100101) {
101102 return prisma . taskRun . create ( {
102103 data : {
104+ ...( run . id ? { id : run . id } : { } ) ,
103105 friendlyId : run . friendlyId ,
104106 taskIdentifier : run . taskIdentifier ?? "my-task" ,
105107 status : run . status ?? "PENDING" ,
@@ -349,4 +351,86 @@ describe("RunsRepository read-through id-set hydrate (PG14 legacy + PG17 new)",
349351 }
350352 }
351353 ) ;
354+
355+ // Full-keyset walk over interleaved cuid + ksuid ids: hydration must preserve the ClickHouse
356+ // (created_at DESC, run_id DESC) order across the id-space seam. A hydrate that reverts to lexical
357+ // `id desc` splits the two id-spaces into separate blocks, so it would fail this walk.
358+ replicationContainerTest (
359+ "paginating the full keyset enumerates every interleaved cuid/ksuid id once, in CH keyset order, with no empty page" ,
360+ async ( { clickhouseContainer, redisOptions, postgresContainer, prisma } ) => {
361+ const { clickhouse } = await setupClickhouseReplication ( {
362+ prisma,
363+ databaseUrl : postgresContainer . getConnectionUri ( ) ,
364+ clickhouseUrl : clickhouseContainer . getConnectionUrl ( ) ,
365+ redisOptions,
366+ } ) ;
367+
368+ const ctx = await seedParents ( prisma , "keysetwalk" ) ;
369+
370+ // cuid-shaped ids (25 chars, "c" prefix) and ksuid-shaped ids (27 chars, "2" prefix). Lexical
371+ // `id desc` groups all "c" ids ahead of all "2" ids; the created_at order below interleaves
372+ // them, so the two orders genuinely differ across the seam.
373+ const cuid = ( n : number ) => `c${ String ( n ) . padStart ( 24 , "0" ) } ` ;
374+ const ksuid = ( n : number ) => `2${ String ( n ) . padStart ( 26 , "0" ) } ` ;
375+
376+ // created_at DESC order (index 0 = most recent) interleaves the id-spaces: ksuid, cuid,
377+ // ksuid, cuid, ksuid, cuid.
378+ const now = Date . now ( ) ;
379+ const seeds = [
380+ { id : ksuid ( 6 ) , friendlyId : "run_k6" , createdAt : new Date ( now - 0 * 60_000 ) } ,
381+ { id : cuid ( 5 ) , friendlyId : "run_c5" , createdAt : new Date ( now - 1 * 60_000 ) } ,
382+ { id : ksuid ( 4 ) , friendlyId : "run_k4" , createdAt : new Date ( now - 2 * 60_000 ) } ,
383+ { id : cuid ( 3 ) , friendlyId : "run_c3" , createdAt : new Date ( now - 3 * 60_000 ) } ,
384+ { id : ksuid ( 2 ) , friendlyId : "run_k2" , createdAt : new Date ( now - 4 * 60_000 ) } ,
385+ { id : cuid ( 1 ) , friendlyId : "run_c1" , createdAt : new Date ( now - 5 * 60_000 ) } ,
386+ ] ;
387+ for ( const s of seeds ) {
388+ await createRun ( prisma , ctx , s ) ;
389+ }
390+
391+ await setTimeout ( 1500 ) ;
392+
393+ const runsRepository = new RunsRepository ( { prisma, clickhouse } ) ;
394+
395+ // The authoritative order the hydrate must reproduce: exactly the CH keyset the id-list scan
396+ // returns (created_at DESC, run_id DESC). Lexical id-desc of the same ids differs from this.
397+ const chOrder = await runsRepository . listRunIds ( {
398+ page : { size : 100 } ,
399+ projectId : ctx . projectId ,
400+ environmentId : ctx . environmentId ,
401+ organizationId : ctx . organizationId ,
402+ } ) ;
403+ const expectedOrder = chOrder . runIds ;
404+ const lexicalIdDesc = [ ...expectedOrder ] . sort ( ( a , b ) => ( a < b ? 1 : a > b ? - 1 : 0 ) ) ;
405+ expect ( expectedOrder ) . not . toEqual ( lexicalIdDesc ) ; // the seam actually separates the two orders
406+
407+ // Walk the whole keyset a page at a time.
408+ const walked : string [ ] = [ ] ;
409+ let cursor : string | undefined ;
410+ let pages = 0 ;
411+ while ( true ) {
412+ const { runs, pagination } = await runsRepository . listRuns ( {
413+ page : { size : 2 , cursor } ,
414+ projectId : ctx . projectId ,
415+ environmentId : ctx . environmentId ,
416+ organizationId : ctx . organizationId ,
417+ } ) ;
418+ pages ++ ;
419+ expect ( pages ) . toBeLessThan ( 20 ) ; // guard against a non-terminating walk
420+
421+ for ( const r of runs ) walked . push ( r . id ) ;
422+
423+ if ( ! pagination . nextCursor ) break ;
424+ // No empty page may be returned while more pages exist.
425+ expect ( runs . length ) . toBeGreaterThan ( 0 ) ;
426+ cursor = pagination . nextCursor ;
427+ }
428+
429+ // Every seeded id enumerated exactly once.
430+ expect ( walked . slice ( ) . sort ( ) ) . toEqual ( seeds . map ( ( s ) => s . id ) . sort ( ) ) ;
431+ expect ( new Set ( walked ) . size ) . toBe ( seeds . length ) ;
432+ // The emitted order equals the CH keyset order across the id-space seam.
433+ expect ( walked ) . toEqual ( expectedOrder ) ;
434+ }
435+ ) ;
352436} ) ;
0 commit comments