@@ -2923,3 +2923,157 @@ describe(`OrderBy with duplicate values`, () => {
29232923
29242924 createOrderByBugTests ( `eager` )
29252925} )
2926+
2927+ describe ( `OrderBy with Date values and precision differences` , ( ) => {
2928+ type TestItemWithDate = {
2929+ id : number
2930+ createdAt : Date
2931+ keep : boolean
2932+ }
2933+
2934+ it ( `should use range query for Date values to handle backend precision differences` , async ( ) => {
2935+ // This test verifies that when paginating with Date orderBy values,
2936+ // the code uses a range query (gte/lt) instead of exact equality (eq)
2937+ // to handle backends with higher precision than JavaScript's millisecond precision.
2938+ //
2939+ // The bug: PostgreSQL stores timestamps with microsecond precision.
2940+ // When JS has a Date "2024-01-15T10:30:45.123Z", the backend might have multiple
2941+ // rows with 123.000μs, 123.100μs, 123.200μs, etc. Using eq() would only match
2942+ // 123.000μs, missing the others. The fix uses gte/lt to match the full 1ms range.
2943+
2944+ const baseTime = new Date ( `2024-01-15T10:30:45.123Z` )
2945+
2946+ const testData : Array < TestItemWithDate > = [
2947+ { id : 1 , createdAt : new Date ( `2024-01-15T10:30:45.120Z` ) , keep : true } ,
2948+ { id : 2 , createdAt : new Date ( `2024-01-15T10:30:45.121Z` ) , keep : true } ,
2949+ { id : 3 , createdAt : new Date ( `2024-01-15T10:30:45.122Z` ) , keep : true } ,
2950+ { id : 4 , createdAt : new Date ( `2024-01-15T10:30:45.122Z` ) , keep : true } ,
2951+ { id : 5 , createdAt : baseTime , keep : true } ,
2952+ { id : 6 , createdAt : baseTime , keep : true } ,
2953+ { id : 7 , createdAt : baseTime , keep : true } ,
2954+ { id : 8 , createdAt : baseTime , keep : true } ,
2955+ { id : 9 , createdAt : baseTime , keep : true } ,
2956+ { id : 10 , createdAt : baseTime , keep : true } ,
2957+ { id : 11 , createdAt : new Date ( `2024-01-15T10:30:45.124Z` ) , keep : true } ,
2958+ { id : 12 , createdAt : new Date ( `2024-01-15T10:30:45.125Z` ) , keep : true } ,
2959+ ]
2960+
2961+ const initialData = testData . slice ( 0 , 5 )
2962+
2963+ // Track the WHERE clauses sent to loadSubset
2964+ const loadSubsetWhereClauses : Array < any > = [ ]
2965+
2966+ const sourceCollection = createCollection (
2967+ mockSyncCollectionOptions < TestItemWithDate > ( {
2968+ id : `test-date-precision-query` ,
2969+ getKey : ( item ) => item . id ,
2970+ initialData,
2971+ autoIndex : `eager` ,
2972+ syncMode : `on-demand` ,
2973+ sync : {
2974+ sync : ( { begin, write, commit, markReady } ) => {
2975+ begin ( )
2976+ initialData . forEach ( ( item ) => {
2977+ write ( { type : `insert` , value : item } )
2978+ } )
2979+ commit ( )
2980+ markReady ( )
2981+
2982+ return {
2983+ loadSubset : ( options ) => {
2984+ // Capture the WHERE clause for inspection
2985+ loadSubsetWhereClauses . push ( options . where )
2986+
2987+ return new Promise < void > ( ( resolve ) => {
2988+ setTimeout ( ( ) => {
2989+ begin ( )
2990+ const sortedData = [ ...testData ] . sort (
2991+ ( a , b ) => a . createdAt . getTime ( ) - b . createdAt . getTime ( )
2992+ )
2993+
2994+ let filteredData = sortedData
2995+ if ( options . where ) {
2996+ try {
2997+ const filterFn = createFilterFunctionFromExpression (
2998+ options . where
2999+ )
3000+ filteredData = sortedData . filter ( filterFn )
3001+ } catch {
3002+ filteredData = sortedData
3003+ }
3004+ }
3005+
3006+ const { limit } = options
3007+ const dataToLoad = limit
3008+ ? filteredData . slice ( 0 , limit )
3009+ : filteredData
3010+
3011+ dataToLoad . forEach ( ( item ) => {
3012+ write ( { type : `insert` , value : item } )
3013+ } )
3014+
3015+ commit ( )
3016+ resolve ( )
3017+ } , 10 )
3018+ } )
3019+ } ,
3020+ }
3021+ } ,
3022+ } ,
3023+ } )
3024+ )
3025+
3026+ const collection = createLiveQueryCollection ( ( q ) =>
3027+ q
3028+ . from ( { items : sourceCollection } )
3029+ . where ( ( { items } ) => eq ( items . keep , true ) )
3030+ . orderBy ( ( { items } ) => items . createdAt , `asc` )
3031+ . offset ( 0 )
3032+ . limit ( 5 )
3033+ . select ( ( { items } ) => ( {
3034+ id : items . id ,
3035+ createdAt : items . createdAt ,
3036+ keep : items . keep ,
3037+ } ) )
3038+ )
3039+ await collection . preload ( )
3040+
3041+ // First page loads
3042+ const results = Array . from ( collection . values ( ) ) . sort ( ( a , b ) => a . id - b . id )
3043+ expect ( results . map ( ( r ) => r . id ) ) . toEqual ( [ 1 , 2 , 3 , 4 , 5 ] )
3044+
3045+ // Clear tracked clauses before moving to next page
3046+ loadSubsetWhereClauses . length = 0
3047+
3048+ // Move to next page - this should trigger the Date precision handling
3049+ const moveToSecondPage = collection . utils . setWindow ( { offset : 5 , limit : 5 } )
3050+ await moveToSecondPage
3051+
3052+ // Find the WHERE clause that queries for the "equal values" (the minValue query)
3053+ // With the fix, this should be: and(gte(createdAt, baseTime), lt(createdAt, baseTime+1ms))
3054+ // Without the fix, this would be: eq(createdAt, baseTime)
3055+ const equalValuesQuery = loadSubsetWhereClauses . find ( ( clause ) => {
3056+ if ( ! clause ) return false
3057+ // Check if it's an 'and' with 'gte' and 'lt' (the fix)
3058+ if ( clause . name === `and` && clause . args ?. length === 2 ) {
3059+ const [ first , second ] = clause . args
3060+ return first ?. name === `gte` && second ?. name === `lt`
3061+ }
3062+ return false
3063+ } )
3064+
3065+ // The fix should produce a range query (and(gte, lt)) for Date values
3066+ // instead of an exact equality query (eq)
3067+ expect ( equalValuesQuery ) . toBeDefined ( )
3068+ expect ( equalValuesQuery . name ) . toBe ( `and` )
3069+ expect ( equalValuesQuery . args [ 0 ] . name ) . toBe ( `gte` )
3070+ expect ( equalValuesQuery . args [ 1 ] . name ) . toBe ( `lt` )
3071+
3072+ // Verify the range is exactly 1ms
3073+ const gteValue = equalValuesQuery . args [ 0 ] . args [ 1 ] . value
3074+ const ltValue = equalValuesQuery . args [ 1 ] . args [ 1 ] . value
3075+ expect ( gteValue ) . toBeInstanceOf ( Date )
3076+ expect ( ltValue ) . toBeInstanceOf ( Date )
3077+ expect ( ltValue . getTime ( ) - gteValue . getTime ( ) ) . toBe ( 1 ) // 1ms difference
3078+ } )
3079+ } )
0 commit comments