@@ -543,4 +543,103 @@ model Post {
543543 }
544544 } ) ;
545545 } ) ;
546+
547+ // Regression for https://github.com/zenstackhq/zenstack/issues/2589 —
548+ // `@db.Time` values were returned as raw strings instead of Date when fetched through
549+ // a nested include (the lateral-join JSON path where pg's per-OID parsers don't fire).
550+ describe ( '@db.Time fields' , ( ) => {
551+ const schema = `
552+ model Exchange {
553+ id Int @id @default(autoincrement())
554+ name String
555+ tradingWindows ExchangeTradingWindow[]
556+ }
557+
558+ model ExchangeTradingWindow {
559+ id Int @id @default(autoincrement())
560+ exchangeId Int
561+ exchange Exchange @relation(fields: [exchangeId], references: [id], onDelete: Cascade)
562+ open DateTime @db.Time(6)
563+ close DateTime @db.Time(6)
564+ openTz DateTime @db.Timetz(6)
565+ effectiveOn DateTime @db.Date
566+ }
567+ ` ;
568+
569+ let client : any ;
570+
571+ beforeEach ( async ( ) => {
572+ client = await createTestClient ( schema , {
573+ usePrismaPush : true ,
574+ provider : 'postgresql' ,
575+ } ) ;
576+ } ) ;
577+
578+ afterEach ( async ( ) => {
579+ await client ?. $disconnect ( ) ;
580+ } ) ;
581+
582+ it ( 'returns @db.Time / @db.Timetz / @db.Date fields as Date via nested include' , async ( ) => {
583+ const exchange = await client . exchange . create ( { data : { name : 'NYSE' } } ) ;
584+
585+ await client . $qb
586+ . insertInto ( 'ExchangeTradingWindow' )
587+ . values ( {
588+ exchangeId : exchange . id ,
589+ open : '09:30:00' ,
590+ close : '16:00:00' ,
591+ openTz : '09:30:00+00' ,
592+ effectiveOn : '2024-06-15' ,
593+ } )
594+ . execute ( ) ;
595+
596+ const result = await client . exchange . findUnique ( {
597+ where : { id : exchange . id } ,
598+ include : { tradingWindows : true } ,
599+ } ) ;
600+
601+ expect ( result . tradingWindows ) . toHaveLength ( 1 ) ;
602+ const win = result . tradingWindows [ 0 ] ;
603+
604+ expect ( win . open ) . toBeInstanceOf ( Date ) ;
605+ expect ( win . open . toISOString ( ) ) . toBe ( '1970-01-01T09:30:00.000Z' ) ;
606+ expect ( win . close ) . toBeInstanceOf ( Date ) ;
607+ expect ( win . close . toISOString ( ) ) . toBe ( '1970-01-01T16:00:00.000Z' ) ;
608+ expect ( win . openTz ) . toBeInstanceOf ( Date ) ;
609+ expect ( win . openTz . toISOString ( ) ) . toBe ( '1970-01-01T09:30:00.000Z' ) ;
610+ // @db .Date must not be corrupted by the tz-offset expansion (guarding
611+ // against `2024-06-15` being rewritten to `2024-06-15:00`).
612+ expect ( win . effectiveOn ) . toBeInstanceOf ( Date ) ;
613+ expect ( win . effectiveOn . toISOString ( ) ) . toBe ( '2024-06-15T00:00:00.000Z' ) ;
614+ } ) ;
615+
616+ it ( 'returns @db.Time / @db.Date fields as Date on a direct select' , async ( ) => {
617+ const exchange = await client . exchange . create ( { data : { name : 'NYSE' } } ) ;
618+
619+ await client . $qb
620+ . insertInto ( 'ExchangeTradingWindow' )
621+ . values ( {
622+ exchangeId : exchange . id ,
623+ open : '09:30:00' ,
624+ close : '16:00:00' ,
625+ openTz : '09:30:00+00' ,
626+ effectiveOn : '2024-06-15' ,
627+ } )
628+ . execute ( ) ;
629+
630+ const windows = await client . exchangeTradingWindow . findMany ( {
631+ where : { exchangeId : exchange . id } ,
632+ } ) ;
633+
634+ expect ( windows ) . toHaveLength ( 1 ) ;
635+ expect ( windows [ 0 ] . open ) . toBeInstanceOf ( Date ) ;
636+ expect ( windows [ 0 ] . open . toISOString ( ) ) . toBe ( '1970-01-01T09:30:00.000Z' ) ;
637+ expect ( windows [ 0 ] . close ) . toBeInstanceOf ( Date ) ;
638+ expect ( windows [ 0 ] . openTz ) . toBeInstanceOf ( Date ) ;
639+ // On direct select pg's default DATE parser returns a Date anchored in local
640+ // time, so we only assert the instance type here — the include path above
641+ // exercises the string branch (which is where the offset-expansion bug lived).
642+ expect ( windows [ 0 ] . effectiveOn ) . toBeInstanceOf ( Date ) ;
643+ } ) ;
644+ } ) ;
546645} ) ;
0 commit comments