@tanstack/db 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/cjs/collection.cjs +112 -6
  2. package/dist/cjs/collection.cjs.map +1 -1
  3. package/dist/cjs/collection.d.cts +3 -2
  4. package/dist/cjs/errors.cjs +6 -0
  5. package/dist/cjs/errors.cjs.map +1 -1
  6. package/dist/cjs/errors.d.cts +3 -0
  7. package/dist/cjs/index.cjs +1 -0
  8. package/dist/cjs/index.cjs.map +1 -1
  9. package/dist/cjs/indexes/auto-index.cjs +30 -19
  10. package/dist/cjs/indexes/auto-index.cjs.map +1 -1
  11. package/dist/cjs/indexes/auto-index.d.cts +1 -0
  12. package/dist/cjs/indexes/base-index.cjs.map +1 -1
  13. package/dist/cjs/indexes/base-index.d.cts +2 -1
  14. package/dist/cjs/indexes/btree-index.cjs +26 -0
  15. package/dist/cjs/indexes/btree-index.cjs.map +1 -1
  16. package/dist/cjs/indexes/btree-index.d.cts +7 -0
  17. package/dist/cjs/indexes/index-options.d.cts +1 -1
  18. package/dist/cjs/query/compiler/evaluators.cjs +2 -2
  19. package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
  20. package/dist/cjs/query/compiler/evaluators.d.cts +1 -1
  21. package/dist/cjs/query/compiler/group-by.cjs +3 -1
  22. package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
  23. package/dist/cjs/query/compiler/index.cjs +72 -6
  24. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  25. package/dist/cjs/query/compiler/index.d.cts +16 -2
  26. package/dist/cjs/query/compiler/joins.cjs +111 -12
  27. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  28. package/dist/cjs/query/compiler/joins.d.cts +9 -2
  29. package/dist/cjs/query/compiler/order-by.cjs +62 -3
  30. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  31. package/dist/cjs/query/compiler/order-by.d.cts +12 -2
  32. package/dist/cjs/query/live-query-collection.cjs +196 -23
  33. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  34. package/dist/cjs/types.d.cts +1 -0
  35. package/dist/cjs/utils/btree.cjs +15 -0
  36. package/dist/cjs/utils/btree.cjs.map +1 -1
  37. package/dist/cjs/utils/btree.d.cts +8 -0
  38. package/dist/esm/collection.d.ts +3 -2
  39. package/dist/esm/collection.js +113 -7
  40. package/dist/esm/collection.js.map +1 -1
  41. package/dist/esm/errors.d.ts +3 -0
  42. package/dist/esm/errors.js +6 -0
  43. package/dist/esm/errors.js.map +1 -1
  44. package/dist/esm/index.js +2 -1
  45. package/dist/esm/indexes/auto-index.d.ts +1 -0
  46. package/dist/esm/indexes/auto-index.js +31 -20
  47. package/dist/esm/indexes/auto-index.js.map +1 -1
  48. package/dist/esm/indexes/base-index.d.ts +2 -1
  49. package/dist/esm/indexes/base-index.js.map +1 -1
  50. package/dist/esm/indexes/btree-index.d.ts +7 -0
  51. package/dist/esm/indexes/btree-index.js +26 -0
  52. package/dist/esm/indexes/btree-index.js.map +1 -1
  53. package/dist/esm/indexes/index-options.d.ts +1 -1
  54. package/dist/esm/query/compiler/evaluators.d.ts +1 -1
  55. package/dist/esm/query/compiler/evaluators.js +2 -2
  56. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  57. package/dist/esm/query/compiler/group-by.js +3 -1
  58. package/dist/esm/query/compiler/group-by.js.map +1 -1
  59. package/dist/esm/query/compiler/index.d.ts +16 -2
  60. package/dist/esm/query/compiler/index.js +73 -7
  61. package/dist/esm/query/compiler/index.js.map +1 -1
  62. package/dist/esm/query/compiler/joins.d.ts +9 -2
  63. package/dist/esm/query/compiler/joins.js +114 -15
  64. package/dist/esm/query/compiler/joins.js.map +1 -1
  65. package/dist/esm/query/compiler/order-by.d.ts +12 -2
  66. package/dist/esm/query/compiler/order-by.js +62 -3
  67. package/dist/esm/query/compiler/order-by.js.map +1 -1
  68. package/dist/esm/query/live-query-collection.js +196 -23
  69. package/dist/esm/query/live-query-collection.js.map +1 -1
  70. package/dist/esm/types.d.ts +1 -0
  71. package/dist/esm/utils/btree.d.ts +8 -0
  72. package/dist/esm/utils/btree.js +15 -0
  73. package/dist/esm/utils/btree.js.map +1 -1
  74. package/package.json +2 -2
  75. package/src/collection.ts +163 -10
  76. package/src/errors.ts +6 -0
  77. package/src/indexes/auto-index.ts +53 -31
  78. package/src/indexes/base-index.ts +6 -1
  79. package/src/indexes/btree-index.ts +29 -0
  80. package/src/indexes/index-options.ts +2 -2
  81. package/src/query/compiler/evaluators.ts +6 -3
  82. package/src/query/compiler/group-by.ts +3 -1
  83. package/src/query/compiler/index.ts +112 -5
  84. package/src/query/compiler/joins.ts +216 -20
  85. package/src/query/compiler/order-by.ts +98 -3
  86. package/src/query/live-query-collection.ts +352 -24
  87. package/src/types.ts +1 -0
  88. package/src/utils/btree.ts +17 -0
@@ -3,30 +3,45 @@ import {
3
3
  filter,
4
4
  join as joinOperator,
5
5
  map,
6
+ tap,
6
7
  } from "@tanstack/db-ivm"
7
8
  import {
8
9
  CollectionInputNotFoundError,
9
10
  InvalidJoinConditionSameTableError,
10
11
  InvalidJoinConditionTableMismatchError,
11
12
  InvalidJoinConditionWrongTablesError,
13
+ JoinCollectionNotFoundError,
12
14
  UnsupportedJoinSourceTypeError,
13
15
  UnsupportedJoinTypeError,
14
16
  } from "../../errors.js"
17
+ import { findIndexForField } from "../../utils/index-optimization.js"
18
+ import { ensureIndexForField } from "../../indexes/auto-index.js"
15
19
  import { compileExpression } from "./evaluators.js"
16
- import { compileQuery } from "./index.js"
17
- import type { IStreamBuilder, JoinType } from "@tanstack/db-ivm"
20
+ import { compileQuery, followRef } from "./index.js"
21
+ import type { OrderByOptimizationInfo } from "./order-by.js"
18
22
  import type {
19
23
  BasicExpression,
20
24
  CollectionRef,
21
25
  JoinClause,
26
+ PropRef,
27
+ QueryIR,
22
28
  QueryRef,
23
29
  } from "../ir.js"
30
+ import type { IStreamBuilder, JoinType } from "@tanstack/db-ivm"
31
+ import type { Collection } from "../../collection.js"
24
32
  import type {
25
33
  KeyedStream,
26
34
  NamespacedAndKeyedStream,
27
35
  NamespacedRow,
28
36
  } from "../../types.js"
29
37
  import type { QueryCache, QueryMapping } from "./types.js"
38
+ import type { BaseIndex } from "../../indexes/base-index.js"
39
+
40
+ export type LoadKeysFn = (key: Set<string | number>) => void
41
+ export type LazyCollectionCallbacks = {
42
+ loadKeys: LoadKeysFn
43
+ loadInitialState: () => void
44
+ }
30
45
 
31
46
  /**
32
47
  * Processes all join clauses in a query
@@ -35,10 +50,16 @@ export function processJoins(
35
50
  pipeline: NamespacedAndKeyedStream,
36
51
  joinClauses: Array<JoinClause>,
37
52
  tables: Record<string, KeyedStream>,
53
+ mainTableId: string,
38
54
  mainTableAlias: string,
39
55
  allInputs: Record<string, KeyedStream>,
40
56
  cache: QueryCache,
41
- queryMapping: QueryMapping
57
+ queryMapping: QueryMapping,
58
+ collections: Record<string, Collection>,
59
+ callbacks: Record<string, LazyCollectionCallbacks>,
60
+ lazyCollections: Set<string>,
61
+ optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
62
+ rawQuery: QueryIR
42
63
  ): NamespacedAndKeyedStream {
43
64
  let resultPipeline = pipeline
44
65
 
@@ -47,10 +68,16 @@ export function processJoins(
47
68
  resultPipeline,
48
69
  joinClause,
49
70
  tables,
71
+ mainTableId,
50
72
  mainTableAlias,
51
73
  allInputs,
52
74
  cache,
53
- queryMapping
75
+ queryMapping,
76
+ collections,
77
+ callbacks,
78
+ lazyCollections,
79
+ optimizableOrderByCollections,
80
+ rawQuery
54
81
  )
55
82
  }
56
83
 
@@ -64,15 +91,29 @@ function processJoin(
64
91
  pipeline: NamespacedAndKeyedStream,
65
92
  joinClause: JoinClause,
66
93
  tables: Record<string, KeyedStream>,
94
+ mainTableId: string,
67
95
  mainTableAlias: string,
68
96
  allInputs: Record<string, KeyedStream>,
69
97
  cache: QueryCache,
70
- queryMapping: QueryMapping
98
+ queryMapping: QueryMapping,
99
+ collections: Record<string, Collection>,
100
+ callbacks: Record<string, LazyCollectionCallbacks>,
101
+ lazyCollections: Set<string>,
102
+ optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
103
+ rawQuery: QueryIR
71
104
  ): NamespacedAndKeyedStream {
72
105
  // Get the joined table alias and input stream
73
- const { alias: joinedTableAlias, input: joinedInput } = processJoinSource(
106
+ const {
107
+ alias: joinedTableAlias,
108
+ input: joinedInput,
109
+ collectionId: joinedCollectionId,
110
+ } = processJoinSource(
74
111
  joinClause.from,
75
112
  allInputs,
113
+ collections,
114
+ callbacks,
115
+ lazyCollections,
116
+ optimizableOrderByCollections,
76
117
  cache,
77
118
  queryMapping
78
119
  )
@@ -80,13 +121,22 @@ function processJoin(
80
121
  // Add the joined table to the tables map
81
122
  tables[joinedTableAlias] = joinedInput
82
123
 
83
- // Convert join type to D2 join type
84
- const joinType: JoinType =
85
- joinClause.type === `cross`
86
- ? `inner`
87
- : joinClause.type === `outer`
88
- ? `full`
89
- : (joinClause.type as JoinType)
124
+ const mainCollection = collections[mainTableId]
125
+ const joinedCollection = collections[joinedCollectionId]
126
+
127
+ if (!mainCollection) {
128
+ throw new JoinCollectionNotFoundError(mainTableId)
129
+ }
130
+
131
+ if (!joinedCollection) {
132
+ throw new JoinCollectionNotFoundError(joinedCollectionId)
133
+ }
134
+
135
+ const { activeCollection, lazyCollection } = getActiveAndLazyCollections(
136
+ joinClause.type,
137
+ mainCollection,
138
+ joinedCollection
139
+ )
90
140
 
91
141
  // Analyze which table each expression refers to and swap if necessary
92
142
  const { mainExpr, joinedExpr } = analyzeJoinExpressions(
@@ -101,7 +151,7 @@ function processJoin(
101
151
  const compiledJoinedExpr = compileExpression(joinedExpr)
102
152
 
103
153
  // Prepare the main pipeline for joining
104
- const mainPipeline = pipeline.pipe(
154
+ let mainPipeline = pipeline.pipe(
105
155
  map(([currentKey, namespacedRow]) => {
106
156
  // Extract the join key from the main table expression
107
157
  const mainKey = compiledMainExpr(namespacedRow)
@@ -115,7 +165,7 @@ function processJoin(
115
165
  )
116
166
 
117
167
  // Prepare the joined pipeline
118
- const joinedPipeline = joinedInput.pipe(
168
+ let joinedPipeline = joinedInput.pipe(
119
169
  map(([currentKey, row]) => {
120
170
  // Wrap the row in a namespaced structure
121
171
  const namespacedRow: NamespacedRow = { [joinedTableAlias]: row }
@@ -132,11 +182,103 @@ function processJoin(
132
182
  )
133
183
 
134
184
  // Apply the join operation
135
- if (![`inner`, `left`, `right`, `full`].includes(joinType)) {
185
+ if (![`inner`, `left`, `right`, `full`].includes(joinClause.type)) {
136
186
  throw new UnsupportedJoinTypeError(joinClause.type)
137
187
  }
188
+
189
+ if (activeCollection) {
190
+ // This join can be optimized by having the active collection
191
+ // dynamically load keys into the lazy collection
192
+ // based on the value of the joinKey and by looking up
193
+ // matching rows in the index of the lazy collection
194
+
195
+ // Mark the lazy collection as lazy
196
+ // this Set is passed by the liveQueryCollection to the compiler
197
+ // such that the liveQueryCollection can check it after compilation
198
+ // to know which collections are lazy collections
199
+ lazyCollections.add(lazyCollection.id)
200
+
201
+ const activePipeline =
202
+ activeCollection === `main` ? mainPipeline : joinedPipeline
203
+
204
+ let index: BaseIndex<string | number> | undefined
205
+
206
+ const lazyCollectionJoinExpr =
207
+ activeCollection === `main`
208
+ ? (joinedExpr as PropRef)
209
+ : (mainExpr as PropRef)
210
+
211
+ const activeColl =
212
+ activeCollection === `main` ? collections[mainTableId]! : lazyCollection
213
+
214
+ const followRefResult = followRef(
215
+ rawQuery,
216
+ lazyCollectionJoinExpr,
217
+ activeColl
218
+ )!
219
+ const followRefCollection = followRefResult.collection
220
+
221
+ const fieldName = followRefResult.path[0]
222
+ if (fieldName) {
223
+ ensureIndexForField(fieldName, followRefResult.path, followRefCollection)
224
+ }
225
+
226
+ let deoptimized = false
227
+
228
+ const activePipelineWithLoading: IStreamBuilder<
229
+ [key: unknown, [originalKey: string, namespacedRow: NamespacedRow]]
230
+ > = activePipeline.pipe(
231
+ tap(([joinKey, _]) => {
232
+ if (deoptimized) {
233
+ return
234
+ }
235
+
236
+ // Find the index for the path we join on
237
+ // we need to find the index inside the map operator
238
+ // because the indexes are only available after the initial sync
239
+ // so we can't fetch it during compilation
240
+ index ??= findIndexForField(
241
+ followRefCollection.indexes,
242
+ followRefResult.path
243
+ )
244
+
245
+ // The `callbacks` object is passed by the liveQueryCollection to the compiler.
246
+ // It contains a function to lazy load keys for each lazy collection
247
+ // as well as a function to switch back to a regular collection
248
+ // (useful when there's no index for available for lazily loading the collection)
249
+ const collectionCallbacks = callbacks[lazyCollection.id]
250
+ if (!collectionCallbacks) {
251
+ throw new Error(
252
+ `Internal error: callbacks for collection are missing in join pipeline. Make sure the live query collection sets them before running the pipeline.`
253
+ )
254
+ }
255
+
256
+ const { loadKeys, loadInitialState } = collectionCallbacks
257
+
258
+ if (index && index.supports(`eq`)) {
259
+ // Use the index to fetch the PKs of the rows in the lazy collection
260
+ // that match this row from the active collection based on the value of the joinKey
261
+ const matchingKeys = index.lookup(`eq`, joinKey)
262
+ // Inform the lazy collection that those keys need to be loaded
263
+ loadKeys(matchingKeys)
264
+ } else {
265
+ // We can't optimize the join because there is no index for the join key
266
+ // on the lazy collection, so we load the initial state
267
+ deoptimized = true
268
+ loadInitialState()
269
+ }
270
+ })
271
+ )
272
+
273
+ if (activeCollection === `main`) {
274
+ mainPipeline = activePipelineWithLoading
275
+ } else {
276
+ joinedPipeline = activePipelineWithLoading
277
+ }
278
+ }
279
+
138
280
  return mainPipeline.pipe(
139
- joinOperator(joinedPipeline, joinType),
281
+ joinOperator(joinedPipeline, joinClause.type as JoinType),
140
282
  consolidate(),
141
283
  processJoinResults(joinClause.type)
142
284
  )
@@ -225,16 +367,20 @@ function getTableAliasFromExpression(expr: BasicExpression): string | null {
225
367
  function processJoinSource(
226
368
  from: CollectionRef | QueryRef,
227
369
  allInputs: Record<string, KeyedStream>,
370
+ collections: Record<string, Collection>,
371
+ callbacks: Record<string, LazyCollectionCallbacks>,
372
+ lazyCollections: Set<string>,
373
+ optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
228
374
  cache: QueryCache,
229
375
  queryMapping: QueryMapping
230
- ): { alias: string; input: KeyedStream } {
376
+ ): { alias: string; input: KeyedStream; collectionId: string } {
231
377
  switch (from.type) {
232
378
  case `collectionRef`: {
233
379
  const input = allInputs[from.collection.id]
234
380
  if (!input) {
235
381
  throw new CollectionInputNotFoundError(from.collection.id)
236
382
  }
237
- return { alias: from.alias, input }
383
+ return { alias: from.alias, input, collectionId: from.collection.id }
238
384
  }
239
385
  case `queryRef`: {
240
386
  // Find the original query for caching purposes
@@ -244,6 +390,10 @@ function processJoinSource(
244
390
  const subQueryResult = compileQuery(
245
391
  originalQuery,
246
392
  allInputs,
393
+ collections,
394
+ callbacks,
395
+ lazyCollections,
396
+ optimizableOrderByCollections,
247
397
  cache,
248
398
  queryMapping
249
399
  )
@@ -260,7 +410,11 @@ function processJoinSource(
260
410
  })
261
411
  )
262
412
 
263
- return { alias: from.alias, input: extractedInput as KeyedStream }
413
+ return {
414
+ alias: from.alias,
415
+ input: extractedInput as KeyedStream,
416
+ collectionId: subQueryResult.collectionId,
417
+ }
264
418
  }
265
419
  default:
266
420
  throw new UnsupportedJoinSourceTypeError((from as any).type)
@@ -333,3 +487,45 @@ function processJoinResults(joinType: string) {
333
487
  )
334
488
  }
335
489
  }
490
+
491
+ /**
492
+ * Returns the active and lazy collections for a join clause.
493
+ * The active collection is the one that we need to fully iterate over
494
+ * and it can be the main table (i.e. left collection) or the joined table (i.e. right collection).
495
+ * The lazy collection is the one that we should join-in lazily based on matches in the active collection.
496
+ * @param joinClause - The join clause to analyze
497
+ * @param leftCollection - The left collection
498
+ * @param rightCollection - The right collection
499
+ * @returns The active and lazy collections. They are undefined if we need to loop over both collections (i.e. both are active)
500
+ */
501
+ function getActiveAndLazyCollections(
502
+ joinType: JoinClause[`type`],
503
+ leftCollection: Collection,
504
+ rightCollection: Collection
505
+ ):
506
+ | { activeCollection: `main` | `joined`; lazyCollection: Collection }
507
+ | { activeCollection: undefined; lazyCollection: undefined } {
508
+ if (leftCollection.id === rightCollection.id) {
509
+ // We can't apply this optimization if there's only one collection
510
+ // because `liveQueryCollection` will detect that the collection is lazy
511
+ // and treat it lazily (because the collection is shared)
512
+ // and thus it will not load any keys because both sides of the join
513
+ // will be handled lazily
514
+ return { activeCollection: undefined, lazyCollection: undefined }
515
+ }
516
+
517
+ switch (joinType) {
518
+ case `left`:
519
+ return { activeCollection: `main`, lazyCollection: rightCollection }
520
+ case `right`:
521
+ return { activeCollection: `joined`, lazyCollection: leftCollection }
522
+ case `inner`:
523
+ // The smallest collection should be the active collection
524
+ // and the biggest collection should be lazy
525
+ return leftCollection.size < rightCollection.size
526
+ ? { activeCollection: `main`, lazyCollection: rightCollection }
527
+ : { activeCollection: `joined`, lazyCollection: leftCollection }
528
+ default:
529
+ return { activeCollection: undefined, lazyCollection: undefined }
530
+ }
531
+ }
@@ -1,9 +1,28 @@
1
1
  import { orderByWithFractionalIndex } from "@tanstack/db-ivm"
2
2
  import { defaultComparator, makeComparator } from "../../utils/comparison.js"
3
+ import { PropRef } from "../ir.js"
4
+ import { ensureIndexForField } from "../../indexes/auto-index.js"
5
+ import { findIndexForField } from "../../utils/index-optimization.js"
3
6
  import { compileExpression } from "./evaluators.js"
4
- import type { OrderByClause } from "../ir.js"
7
+ import { followRef } from "./index.js"
8
+ import type { CompiledSingleRowExpression } from "./evaluators.js"
9
+ import type { OrderByClause, QueryIR } from "../ir.js"
5
10
  import type { NamespacedAndKeyedStream, NamespacedRow } from "../../types.js"
6
11
  import type { IStreamBuilder, KeyValue } from "@tanstack/db-ivm"
12
+ import type { BaseIndex } from "../../indexes/base-index.js"
13
+ import type { Collection } from "../../collection.js"
14
+
15
+ export type OrderByOptimizationInfo = {
16
+ offset: number
17
+ limit: number
18
+ comparator: (
19
+ a: Record<string, unknown> | null | undefined,
20
+ b: Record<string, unknown> | null | undefined
21
+ ) => number
22
+ valueExtractorForRawRow: (row: Record<string, unknown>) => any
23
+ index: BaseIndex<string | number>
24
+ dataNeeded?: () => number
25
+ }
7
26
 
8
27
  /**
9
28
  * Processes the ORDER BY clause
@@ -11,8 +30,11 @@ import type { IStreamBuilder, KeyValue } from "@tanstack/db-ivm"
11
30
  * Always uses fractional indexing and adds the index as __ordering_index to the result
12
31
  */
13
32
  export function processOrderBy(
33
+ rawQuery: QueryIR,
14
34
  pipeline: NamespacedAndKeyedStream,
15
35
  orderByClause: Array<OrderByClause>,
36
+ collection: Collection,
37
+ optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
16
38
  limit?: number,
17
39
  offset?: number
18
40
  ): IStreamBuilder<KeyValue<unknown, [NamespacedRow, string]>> {
@@ -53,7 +75,7 @@ export function processOrderBy(
53
75
  }
54
76
 
55
77
  // Create a multi-property comparator that respects the order and direction of each property
56
- const comparator = (a: unknown, b: unknown) => {
78
+ const compare = (a: unknown, b: unknown) => {
57
79
  // If we're comparing arrays (multiple properties), compare each property in order
58
80
  if (orderByClause.length > 1) {
59
81
  const arrayA = a as Array<unknown>
@@ -79,12 +101,85 @@ export function processOrderBy(
79
101
  return defaultComparator(a, b)
80
102
  }
81
103
 
104
+ let setSizeCallback: ((getSize: () => number) => void) | undefined
105
+
106
+ // Optimize the orderBy operator to lazily load elements
107
+ // by using the range index of the collection.
108
+ // Only for orderBy clause on a single column for now (no composite ordering)
109
+ if (limit && orderByClause.length === 1) {
110
+ const clause = orderByClause[0]!
111
+ const orderByExpression = clause.expression
112
+
113
+ if (orderByExpression.type === `ref`) {
114
+ const followRefResult = followRef(
115
+ rawQuery,
116
+ orderByExpression,
117
+ collection
118
+ )!
119
+
120
+ const followRefCollection = followRefResult.collection
121
+ const fieldName = followRefResult.path[0]
122
+ if (fieldName) {
123
+ ensureIndexForField(
124
+ fieldName,
125
+ followRefResult.path,
126
+ followRefCollection,
127
+ compare
128
+ )
129
+ }
130
+
131
+ const valueExtractorForRawRow = compileExpression(
132
+ new PropRef(followRefResult.path),
133
+ true
134
+ ) as CompiledSingleRowExpression
135
+
136
+ const comparator = (
137
+ a: Record<string, unknown> | null | undefined,
138
+ b: Record<string, unknown> | null | undefined
139
+ ) => {
140
+ const extractedA = a ? valueExtractorForRawRow(a) : a
141
+ const extractedB = b ? valueExtractorForRawRow(b) : b
142
+ return compare(extractedA, extractedB)
143
+ }
144
+
145
+ const index: BaseIndex<string | number> | undefined = findIndexForField(
146
+ followRefCollection.indexes,
147
+ followRefResult.path
148
+ )
149
+
150
+ if (index && index.supports(`gt`)) {
151
+ // We found an index that we can use to lazily load ordered data
152
+ const orderByOptimizationInfo = {
153
+ offset: offset ?? 0,
154
+ limit,
155
+ comparator,
156
+ valueExtractorForRawRow,
157
+ index,
158
+ }
159
+
160
+ optimizableOrderByCollections[followRefCollection.id] =
161
+ orderByOptimizationInfo
162
+
163
+ setSizeCallback = (getSize: () => number) => {
164
+ optimizableOrderByCollections[followRefCollection.id] = {
165
+ ...optimizableOrderByCollections[followRefCollection.id]!,
166
+ dataNeeded: () => {
167
+ const size = getSize()
168
+ return Math.max(0, limit - size)
169
+ },
170
+ }
171
+ }
172
+ }
173
+ }
174
+ }
175
+
82
176
  // Use fractional indexing and return the tuple [value, index]
83
177
  return pipeline.pipe(
84
178
  orderByWithFractionalIndex(valueExtractor, {
85
179
  limit,
86
180
  offset,
87
- comparator,
181
+ comparator: compare,
182
+ setSizeCallback,
88
183
  })
89
184
  // orderByWithFractionalIndex returns [key, [value, index]] - we keep this format
90
185
  )