@tanstack/db 0.5.11 → 0.5.12

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 (221) hide show
  1. package/dist/cjs/SortedMap.cjs +40 -26
  2. package/dist/cjs/SortedMap.cjs.map +1 -1
  3. package/dist/cjs/SortedMap.d.cts +10 -15
  4. package/dist/cjs/collection/change-events.cjs.map +1 -1
  5. package/dist/cjs/collection/changes.cjs.map +1 -1
  6. package/dist/cjs/collection/events.cjs.map +1 -1
  7. package/dist/cjs/collection/events.d.cts +12 -4
  8. package/dist/cjs/collection/index.cjs +2 -1
  9. package/dist/cjs/collection/index.cjs.map +1 -1
  10. package/dist/cjs/collection/indexes.cjs.map +1 -1
  11. package/dist/cjs/collection/lifecycle.cjs.map +1 -1
  12. package/dist/cjs/collection/mutations.cjs +5 -2
  13. package/dist/cjs/collection/mutations.cjs.map +1 -1
  14. package/dist/cjs/collection/state.cjs +6 -5
  15. package/dist/cjs/collection/state.cjs.map +1 -1
  16. package/dist/cjs/collection/state.d.cts +4 -1
  17. package/dist/cjs/collection/subscription.cjs +60 -53
  18. package/dist/cjs/collection/subscription.cjs.map +1 -1
  19. package/dist/cjs/collection/subscription.d.cts +18 -4
  20. package/dist/cjs/collection/sync.cjs.map +1 -1
  21. package/dist/cjs/errors.cjs +9 -0
  22. package/dist/cjs/errors.cjs.map +1 -1
  23. package/dist/cjs/errors.d.cts +3 -0
  24. package/dist/cjs/event-emitter.cjs.map +1 -1
  25. package/dist/cjs/index.cjs +2 -0
  26. package/dist/cjs/index.cjs.map +1 -1
  27. package/dist/cjs/index.d.cts +1 -1
  28. package/dist/cjs/indexes/auto-index.cjs.map +1 -1
  29. package/dist/cjs/indexes/base-index.cjs.map +1 -1
  30. package/dist/cjs/indexes/btree-index.cjs +8 -6
  31. package/dist/cjs/indexes/btree-index.cjs.map +1 -1
  32. package/dist/cjs/indexes/lazy-index.cjs.map +1 -1
  33. package/dist/cjs/indexes/reverse-index.cjs.map +1 -1
  34. package/dist/cjs/local-only.cjs.map +1 -1
  35. package/dist/cjs/local-storage.cjs.map +1 -1
  36. package/dist/cjs/optimistic-action.cjs.map +1 -1
  37. package/dist/cjs/paced-mutations.cjs.map +1 -1
  38. package/dist/cjs/proxy.cjs.map +1 -1
  39. package/dist/cjs/query/builder/functions.cjs.map +1 -1
  40. package/dist/cjs/query/builder/index.cjs.map +1 -1
  41. package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
  42. package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
  43. package/dist/cjs/query/compiler/expressions.cjs.map +1 -1
  44. package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
  45. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  46. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  47. package/dist/cjs/query/compiler/order-by.cjs +91 -38
  48. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  49. package/dist/cjs/query/compiler/order-by.d.cts +6 -2
  50. package/dist/cjs/query/compiler/select.cjs.map +1 -1
  51. package/dist/cjs/query/expression-helpers.cjs.map +1 -1
  52. package/dist/cjs/query/index.d.cts +1 -1
  53. package/dist/cjs/query/ir.cjs.map +1 -1
  54. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  55. package/dist/cjs/query/live/collection-registry.cjs.map +1 -1
  56. package/dist/cjs/query/live/collection-subscriber.cjs +30 -15
  57. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  58. package/dist/cjs/query/live/internal.cjs.map +1 -1
  59. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  60. package/dist/cjs/query/optimizer.cjs.map +1 -1
  61. package/dist/cjs/query/predicate-utils.cjs +19 -2
  62. package/dist/cjs/query/predicate-utils.cjs.map +1 -1
  63. package/dist/cjs/query/predicate-utils.d.cts +32 -1
  64. package/dist/cjs/query/subset-dedupe.cjs.map +1 -1
  65. package/dist/cjs/scheduler.cjs.map +1 -1
  66. package/dist/cjs/strategies/debounceStrategy.cjs.map +1 -1
  67. package/dist/cjs/strategies/queueStrategy.cjs.map +1 -1
  68. package/dist/cjs/strategies/throttleStrategy.cjs.map +1 -1
  69. package/dist/cjs/transactions.cjs.map +1 -1
  70. package/dist/cjs/types.d.cts +43 -5
  71. package/dist/cjs/utils/browser-polyfills.cjs.map +1 -1
  72. package/dist/cjs/utils/btree.cjs.map +1 -1
  73. package/dist/cjs/utils/comparison.cjs.map +1 -1
  74. package/dist/cjs/utils/cursor.cjs +39 -0
  75. package/dist/cjs/utils/cursor.cjs.map +1 -0
  76. package/dist/cjs/utils/cursor.d.cts +18 -0
  77. package/dist/cjs/utils/index-optimization.cjs.map +1 -1
  78. package/dist/cjs/utils.cjs.map +1 -1
  79. package/dist/esm/SortedMap.d.ts +10 -15
  80. package/dist/esm/SortedMap.js +40 -26
  81. package/dist/esm/SortedMap.js.map +1 -1
  82. package/dist/esm/collection/change-events.js.map +1 -1
  83. package/dist/esm/collection/changes.js.map +1 -1
  84. package/dist/esm/collection/events.d.ts +12 -4
  85. package/dist/esm/collection/events.js.map +1 -1
  86. package/dist/esm/collection/index.js +2 -1
  87. package/dist/esm/collection/index.js.map +1 -1
  88. package/dist/esm/collection/indexes.js.map +1 -1
  89. package/dist/esm/collection/lifecycle.js.map +1 -1
  90. package/dist/esm/collection/mutations.js +6 -3
  91. package/dist/esm/collection/mutations.js.map +1 -1
  92. package/dist/esm/collection/state.d.ts +4 -1
  93. package/dist/esm/collection/state.js +6 -5
  94. package/dist/esm/collection/state.js.map +1 -1
  95. package/dist/esm/collection/subscription.d.ts +18 -4
  96. package/dist/esm/collection/subscription.js +61 -54
  97. package/dist/esm/collection/subscription.js.map +1 -1
  98. package/dist/esm/collection/sync.js.map +1 -1
  99. package/dist/esm/errors.d.ts +3 -0
  100. package/dist/esm/errors.js +9 -0
  101. package/dist/esm/errors.js.map +1 -1
  102. package/dist/esm/event-emitter.js.map +1 -1
  103. package/dist/esm/index.d.ts +1 -1
  104. package/dist/esm/index.js +4 -2
  105. package/dist/esm/indexes/auto-index.js.map +1 -1
  106. package/dist/esm/indexes/base-index.js.map +1 -1
  107. package/dist/esm/indexes/btree-index.js +8 -6
  108. package/dist/esm/indexes/btree-index.js.map +1 -1
  109. package/dist/esm/indexes/lazy-index.js.map +1 -1
  110. package/dist/esm/indexes/reverse-index.js.map +1 -1
  111. package/dist/esm/local-only.js.map +1 -1
  112. package/dist/esm/local-storage.js.map +1 -1
  113. package/dist/esm/optimistic-action.js.map +1 -1
  114. package/dist/esm/paced-mutations.js.map +1 -1
  115. package/dist/esm/proxy.js.map +1 -1
  116. package/dist/esm/query/builder/functions.js.map +1 -1
  117. package/dist/esm/query/builder/index.js.map +1 -1
  118. package/dist/esm/query/builder/ref-proxy.js.map +1 -1
  119. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  120. package/dist/esm/query/compiler/expressions.js.map +1 -1
  121. package/dist/esm/query/compiler/group-by.js.map +1 -1
  122. package/dist/esm/query/compiler/index.js.map +1 -1
  123. package/dist/esm/query/compiler/joins.js.map +1 -1
  124. package/dist/esm/query/compiler/order-by.d.ts +6 -2
  125. package/dist/esm/query/compiler/order-by.js +91 -38
  126. package/dist/esm/query/compiler/order-by.js.map +1 -1
  127. package/dist/esm/query/compiler/select.js.map +1 -1
  128. package/dist/esm/query/expression-helpers.js.map +1 -1
  129. package/dist/esm/query/index.d.ts +1 -1
  130. package/dist/esm/query/ir.js.map +1 -1
  131. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  132. package/dist/esm/query/live/collection-registry.js.map +1 -1
  133. package/dist/esm/query/live/collection-subscriber.js +30 -15
  134. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  135. package/dist/esm/query/live/internal.js.map +1 -1
  136. package/dist/esm/query/live-query-collection.js.map +1 -1
  137. package/dist/esm/query/optimizer.js.map +1 -1
  138. package/dist/esm/query/predicate-utils.d.ts +32 -1
  139. package/dist/esm/query/predicate-utils.js +19 -2
  140. package/dist/esm/query/predicate-utils.js.map +1 -1
  141. package/dist/esm/query/subset-dedupe.js.map +1 -1
  142. package/dist/esm/scheduler.js.map +1 -1
  143. package/dist/esm/strategies/debounceStrategy.js.map +1 -1
  144. package/dist/esm/strategies/queueStrategy.js.map +1 -1
  145. package/dist/esm/strategies/throttleStrategy.js.map +1 -1
  146. package/dist/esm/transactions.js.map +1 -1
  147. package/dist/esm/types.d.ts +43 -5
  148. package/dist/esm/utils/browser-polyfills.js.map +1 -1
  149. package/dist/esm/utils/btree.js.map +1 -1
  150. package/dist/esm/utils/comparison.js.map +1 -1
  151. package/dist/esm/utils/cursor.d.ts +18 -0
  152. package/dist/esm/utils/cursor.js +39 -0
  153. package/dist/esm/utils/cursor.js.map +1 -0
  154. package/dist/esm/utils/index-optimization.js.map +1 -1
  155. package/dist/esm/utils.js.map +1 -1
  156. package/package.json +30 -28
  157. package/src/SortedMap.ts +50 -31
  158. package/src/collection/change-events.ts +20 -20
  159. package/src/collection/changes.ts +12 -12
  160. package/src/collection/events.ts +20 -10
  161. package/src/collection/index.ts +47 -46
  162. package/src/collection/indexes.ts +14 -14
  163. package/src/collection/lifecycle.ts +16 -16
  164. package/src/collection/mutations.ts +25 -20
  165. package/src/collection/state.ts +43 -36
  166. package/src/collection/subscription.ts +114 -83
  167. package/src/collection/sync.ts +13 -13
  168. package/src/duplicate-instance-check.ts +1 -1
  169. package/src/errors.ts +49 -40
  170. package/src/event-emitter.ts +5 -5
  171. package/src/index.ts +21 -21
  172. package/src/indexes/auto-index.ts +11 -11
  173. package/src/indexes/base-index.ts +13 -13
  174. package/src/indexes/btree-index.ts +21 -17
  175. package/src/indexes/index-options.ts +3 -3
  176. package/src/indexes/lazy-index.ts +8 -8
  177. package/src/indexes/reverse-index.ts +5 -5
  178. package/src/local-only.ts +12 -12
  179. package/src/local-storage.ts +17 -17
  180. package/src/optimistic-action.ts +5 -5
  181. package/src/paced-mutations.ts +6 -6
  182. package/src/proxy.ts +43 -43
  183. package/src/query/builder/functions.ts +28 -28
  184. package/src/query/builder/index.ts +22 -22
  185. package/src/query/builder/ref-proxy.ts +4 -4
  186. package/src/query/builder/types.ts +8 -8
  187. package/src/query/compiler/evaluators.ts +9 -9
  188. package/src/query/compiler/expressions.ts +6 -6
  189. package/src/query/compiler/group-by.ts +24 -24
  190. package/src/query/compiler/index.ts +44 -44
  191. package/src/query/compiler/joins.ts +37 -37
  192. package/src/query/compiler/order-by.ts +170 -77
  193. package/src/query/compiler/select.ts +13 -13
  194. package/src/query/compiler/types.ts +2 -2
  195. package/src/query/expression-helpers.ts +16 -16
  196. package/src/query/index.ts +10 -9
  197. package/src/query/ir.ts +13 -13
  198. package/src/query/live/collection-config-builder.ts +53 -53
  199. package/src/query/live/collection-registry.ts +6 -6
  200. package/src/query/live/collection-subscriber.ts +87 -48
  201. package/src/query/live/internal.ts +1 -1
  202. package/src/query/live/types.ts +4 -4
  203. package/src/query/live-query-collection.ts +15 -15
  204. package/src/query/optimizer.ts +29 -29
  205. package/src/query/predicate-utils.ts +105 -50
  206. package/src/query/subset-dedupe.ts +6 -6
  207. package/src/scheduler.ts +3 -3
  208. package/src/strategies/debounceStrategy.ts +6 -6
  209. package/src/strategies/index.ts +4 -4
  210. package/src/strategies/queueStrategy.ts +5 -5
  211. package/src/strategies/throttleStrategy.ts +6 -6
  212. package/src/strategies/types.ts +2 -2
  213. package/src/transactions.ts +9 -9
  214. package/src/types.ts +51 -12
  215. package/src/utils/array-utils.ts +1 -1
  216. package/src/utils/browser-polyfills.ts +2 -2
  217. package/src/utils/btree.ts +22 -22
  218. package/src/utils/comparison.ts +3 -3
  219. package/src/utils/cursor.ts +78 -0
  220. package/src/utils/index-optimization.ts +14 -14
  221. package/src/utils.ts +4 -4
@@ -1,22 +1,22 @@
1
- import { orderByWithFractionalIndex } from "@tanstack/db-ivm"
2
- import { defaultComparator, makeComparator } from "../../utils/comparison.js"
3
- import { PropRef, followRef } from "../ir.js"
4
- import { ensureIndexForField } from "../../indexes/auto-index.js"
5
- import { findIndexForField } from "../../utils/index-optimization.js"
6
- import { compileExpression } from "./evaluators.js"
7
- import { replaceAggregatesByRefs } from "./group-by.js"
8
- import type { CompareOptions } from "../builder/types.js"
9
- import type { WindowOptions } from "./types.js"
10
- import type { CompiledSingleRowExpression } from "./evaluators.js"
11
- import type { OrderBy, OrderByClause, QueryIR, Select } from "../ir.js"
1
+ import { orderByWithFractionalIndex } from '@tanstack/db-ivm'
2
+ import { defaultComparator, makeComparator } from '../../utils/comparison.js'
3
+ import { PropRef, followRef } from '../ir.js'
4
+ import { ensureIndexForField } from '../../indexes/auto-index.js'
5
+ import { findIndexForField } from '../../utils/index-optimization.js'
6
+ import { compileExpression } from './evaluators.js'
7
+ import { replaceAggregatesByRefs } from './group-by.js'
8
+ import type { CompareOptions } from '../builder/types.js'
9
+ import type { WindowOptions } from './types.js'
10
+ import type { CompiledSingleRowExpression } from './evaluators.js'
11
+ import type { OrderBy, OrderByClause, QueryIR, Select } from '../ir.js'
12
12
  import type {
13
13
  CollectionLike,
14
14
  NamespacedAndKeyedStream,
15
15
  NamespacedRow,
16
- } from "../../types.js"
17
- import type { IStreamBuilder, KeyValue } from "@tanstack/db-ivm"
18
- import type { IndexInterface } from "../../indexes/base-index.js"
19
- import type { Collection } from "../../collection/index.js"
16
+ } from '../../types.js'
17
+ import type { IStreamBuilder, KeyValue } from '@tanstack/db-ivm'
18
+ import type { IndexInterface } from '../../indexes/base-index.js'
19
+ import type { Collection } from '../../collection/index.js'
20
20
 
21
21
  export type OrderByOptimizationInfo = {
22
22
  alias: string
@@ -25,10 +25,14 @@ export type OrderByOptimizationInfo = {
25
25
  limit: number
26
26
  comparator: (
27
27
  a: Record<string, unknown> | null | undefined,
28
- b: Record<string, unknown> | null | undefined
28
+ b: Record<string, unknown> | null | undefined,
29
29
  ) => number
30
- valueExtractorForRawRow: (row: Record<string, unknown>) => any
31
- index: IndexInterface<string | number>
30
+ /** Extracts all orderBy column values from a raw row (array for multi-column) */
31
+ valueExtractorForRawRow: (row: Record<string, unknown>) => unknown
32
+ /** Extracts only the first column value - used for index-based cursor */
33
+ firstColumnValueExtractor: (row: Record<string, unknown>) => unknown
34
+ /** Index on the first orderBy column - used for lazy loading */
35
+ index?: IndexInterface<string | number>
32
36
  dataNeeded?: () => number
33
37
  }
34
38
 
@@ -46,14 +50,14 @@ export function processOrderBy(
46
50
  optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
47
51
  setWindowFn: (windowFn: (options: WindowOptions) => void) => void,
48
52
  limit?: number,
49
- offset?: number
53
+ offset?: number,
50
54
  ): IStreamBuilder<KeyValue<unknown, [NamespacedRow, string]>> {
51
55
  // Pre-compile all order by expressions
52
56
  const compiledOrderBy = orderByClause.map((clause) => {
53
57
  const clauseWithoutAggregates = replaceAggregatesByRefs(
54
58
  clause.expression,
55
59
  selectClause,
56
- `__select_results`
60
+ `__select_results`,
57
61
  )
58
62
 
59
63
  return {
@@ -74,7 +78,7 @@ export function processOrderBy(
74
78
  if (orderByClause.length > 1) {
75
79
  // For multiple orderBy columns, create a composite key
76
80
  return compiledOrderBy.map((compiled) =>
77
- compiled.compiledExpression(orderByContext)
81
+ compiled.compiledExpression(orderByContext),
78
82
  )
79
83
  } else if (orderByClause.length === 1) {
80
84
  // For a single orderBy column, use the value directly
@@ -117,76 +121,165 @@ export function processOrderBy(
117
121
 
118
122
  let orderByOptimizationInfo: OrderByOptimizationInfo | undefined
119
123
 
120
- // Optimize the orderBy operator to lazily load elements
121
- // by using the range index of the collection.
122
- // Only for orderBy clause on a single column for now (no composite ordering)
123
- if (limit && orderByClause.length === 1) {
124
- const clause = orderByClause[0]!
125
- const orderByExpression = clause.expression
124
+ // When there's a limit, we create orderByOptimizationInfo to pass orderBy/limit
125
+ // to loadSubset so the sync layer can optimize the query.
126
+ // We try to use an index on the FIRST orderBy column for lazy loading,
127
+ // even for multi-column orderBy (using wider bounds on first column).
128
+ if (limit) {
129
+ let index: IndexInterface<string | number> | undefined
130
+ let followRefCollection: Collection | undefined
131
+ let firstColumnValueExtractor: CompiledSingleRowExpression | undefined
132
+ let orderByAlias: string = rawQuery.from.alias
126
133
 
127
- if (orderByExpression.type === `ref`) {
134
+ // Try to create/find an index on the FIRST orderBy column for lazy loading
135
+ const firstClause = orderByClause[0]!
136
+ const firstOrderByExpression = firstClause.expression
137
+
138
+ if (firstOrderByExpression.type === `ref`) {
128
139
  const followRefResult = followRef(
129
140
  rawQuery,
130
- orderByExpression,
131
- collection
132
- )!
133
-
134
- const followRefCollection = followRefResult.collection
135
- const fieldName = followRefResult.path[0]
136
- const compareOpts = buildCompareOptions(clause, followRefCollection)
137
- if (fieldName) {
138
- ensureIndexForField(
139
- fieldName,
140
- followRefResult.path,
141
+ firstOrderByExpression,
142
+ collection,
143
+ )
144
+
145
+ if (followRefResult) {
146
+ followRefCollection = followRefResult.collection
147
+ const fieldName = followRefResult.path[0]
148
+ const compareOpts = buildCompareOptions(
149
+ firstClause,
141
150
  followRefCollection,
142
- compareOpts,
143
- compare
144
151
  )
145
- }
146
152
 
147
- const valueExtractorForRawRow = compileExpression(
148
- new PropRef(followRefResult.path),
149
- true
150
- ) as CompiledSingleRowExpression
153
+ if (fieldName) {
154
+ ensureIndexForField(
155
+ fieldName,
156
+ followRefResult.path,
157
+ followRefCollection,
158
+ compareOpts,
159
+ compare,
160
+ )
161
+ }
151
162
 
152
- const comparator = (
153
- a: Record<string, unknown> | null | undefined,
154
- b: Record<string, unknown> | null | undefined
155
- ) => {
156
- const extractedA = a ? valueExtractorForRawRow(a) : a
157
- const extractedB = b ? valueExtractorForRawRow(b) : b
158
- return compare(extractedA, extractedB)
159
- }
163
+ // First column value extractor - used for index cursor
164
+ firstColumnValueExtractor = compileExpression(
165
+ new PropRef(followRefResult.path),
166
+ true,
167
+ ) as CompiledSingleRowExpression
160
168
 
161
- const index: IndexInterface<string | number> | undefined =
162
- findIndexForField(
169
+ index = findIndexForField(
163
170
  followRefCollection,
164
171
  followRefResult.path,
165
- compareOpts
172
+ compareOpts,
166
173
  )
167
174
 
168
- if (index && index.supports(`gt`)) {
169
- // We found an index that we can use to lazily load ordered data
170
- const orderByAlias =
171
- orderByExpression.path.length > 1
172
- ? String(orderByExpression.path[0])
175
+ // Only use the index if it supports range queries
176
+ if (!index?.supports(`gt`)) {
177
+ index = undefined
178
+ }
179
+
180
+ orderByAlias =
181
+ firstOrderByExpression.path.length > 1
182
+ ? String(firstOrderByExpression.path[0])
173
183
  : rawQuery.from.alias
184
+ }
185
+ }
174
186
 
175
- orderByOptimizationInfo = {
176
- alias: orderByAlias,
177
- offset: offset ?? 0,
178
- limit,
179
- comparator,
180
- valueExtractorForRawRow,
181
- index,
182
- orderBy: orderByClause,
187
+ // Only create comparator and value extractors if the first column is a ref expression
188
+ // For aggregate or computed expressions, we can't extract values from raw collection rows
189
+ if (!firstColumnValueExtractor) {
190
+ // Skip optimization for non-ref expressions (aggregates, computed values, etc.)
191
+ // The query will still work, but without lazy loading optimization
192
+ } else {
193
+ // Build value extractors for all columns (must all be ref expressions for multi-column)
194
+ // Check if all orderBy expressions are ref types (required for multi-column extraction)
195
+ const allColumnsAreRefs = orderByClause.every(
196
+ (clause) => clause.expression.type === `ref`,
197
+ )
198
+
199
+ // Create extractors for all columns if they're all refs
200
+ const allColumnExtractors:
201
+ | Array<CompiledSingleRowExpression>
202
+ | undefined = allColumnsAreRefs
203
+ ? orderByClause.map((clause) => {
204
+ // We know it's a ref since we checked allColumnsAreRefs
205
+ const refExpr = clause.expression as PropRef
206
+ const followResult = followRef(rawQuery, refExpr, collection)
207
+ if (followResult) {
208
+ return compileExpression(
209
+ new PropRef(followResult.path),
210
+ true,
211
+ ) as CompiledSingleRowExpression
212
+ }
213
+ // Fallback for refs that don't follow
214
+ return compileExpression(
215
+ clause.expression,
216
+ true,
217
+ ) as CompiledSingleRowExpression
218
+ })
219
+ : undefined
220
+
221
+ // Create a comparator for raw rows (used for tracking sent values)
222
+ // This compares ALL orderBy columns for proper ordering
223
+ const comparator = (
224
+ a: Record<string, unknown> | null | undefined,
225
+ b: Record<string, unknown> | null | undefined,
226
+ ) => {
227
+ if (orderByClause.length === 1) {
228
+ // Single column: extract and compare
229
+ const extractedA = a ? firstColumnValueExtractor(a) : a
230
+ const extractedB = b ? firstColumnValueExtractor(b) : b
231
+ return compare(extractedA, extractedB)
232
+ }
233
+ if (allColumnExtractors) {
234
+ // Multi-column with all refs: extract all values and compare
235
+ const extractAll = (
236
+ row: Record<string, unknown> | null | undefined,
237
+ ) => {
238
+ if (!row) return row
239
+ return allColumnExtractors.map((extractor) => extractor(row))
240
+ }
241
+ return compare(extractAll(a), extractAll(b))
183
242
  }
243
+ // Fallback: can't compare (shouldn't happen since we skip non-ref cases)
244
+ return 0
245
+ }
246
+
247
+ // Create a value extractor for raw rows that extracts ALL orderBy column values
248
+ // This is used for tracking sent values and building composite cursors
249
+ const rawRowValueExtractor = (row: Record<string, unknown>): unknown => {
250
+ if (orderByClause.length === 1) {
251
+ // Single column: return single value
252
+ return firstColumnValueExtractor(row)
253
+ }
254
+ if (allColumnExtractors) {
255
+ // Multi-column: return array of all values
256
+ return allColumnExtractors.map((extractor) => extractor(row))
257
+ }
258
+ // Fallback (shouldn't happen)
259
+ return undefined
260
+ }
261
+
262
+ orderByOptimizationInfo = {
263
+ alias: orderByAlias,
264
+ offset: offset ?? 0,
265
+ limit,
266
+ comparator,
267
+ valueExtractorForRawRow: rawRowValueExtractor,
268
+ firstColumnValueExtractor: firstColumnValueExtractor,
269
+ index,
270
+ orderBy: orderByClause,
271
+ }
184
272
 
185
- optimizableOrderByCollections[followRefCollection.id] =
186
- orderByOptimizationInfo
273
+ // Store the optimization info keyed by collection ID
274
+ // Use the followed collection if available, otherwise use the main collection
275
+ const targetCollectionId = followRefCollection?.id ?? collection.id
276
+ optimizableOrderByCollections[targetCollectionId] =
277
+ orderByOptimizationInfo
187
278
 
279
+ // Set up lazy loading callback if we have an index
280
+ if (index) {
188
281
  setSizeCallback = (getSize: () => number) => {
189
- optimizableOrderByCollections[followRefCollection.id]![`dataNeeded`] =
282
+ optimizableOrderByCollections[targetCollectionId]![`dataNeeded`] =
190
283
  () => {
191
284
  const size = getSize()
192
285
  return Math.max(0, orderByOptimizationInfo!.limit - size)
@@ -204,7 +297,7 @@ export function processOrderBy(
204
297
  comparator: compare,
205
298
  setSizeCallback,
206
299
  setWindowFn: (
207
- windowFn: (options: { offset?: number; limit?: number }) => void
300
+ windowFn: (options: { offset?: number; limit?: number }) => void,
208
301
  ) => {
209
302
  setWindowFn(
210
303
  // We wrap the move function such that we update the orderByOptimizationInfo
@@ -217,10 +310,10 @@ export function processOrderBy(
217
310
  orderByOptimizationInfo.limit =
218
311
  options.limit ?? orderByOptimizationInfo.limit
219
312
  }
220
- }
313
+ },
221
314
  )
222
315
  },
223
- })
316
+ }),
224
317
  // orderByWithFractionalIndex returns [key, [value, index]] - we keep this format
225
318
  )
226
319
  }
@@ -231,7 +324,7 @@ export function processOrderBy(
231
324
  */
232
325
  export function buildCompareOptions(
233
326
  clause: OrderByClause,
234
- collection: CollectionLike<any, any>
327
+ collection: CollectionLike<any, any>,
235
328
  ): CompareOptions {
236
329
  if (clause.compareOptions.stringSort !== undefined) {
237
330
  return clause.compareOptions
@@ -1,13 +1,13 @@
1
- import { map } from "@tanstack/db-ivm"
2
- import { PropRef, Value as ValClass, isExpressionLike } from "../ir.js"
3
- import { AggregateNotSupportedError } from "../../errors.js"
4
- import { compileExpression } from "./evaluators.js"
5
- import type { Aggregate, BasicExpression, Select } from "../ir.js"
1
+ import { map } from '@tanstack/db-ivm'
2
+ import { PropRef, Value as ValClass, isExpressionLike } from '../ir.js'
3
+ import { AggregateNotSupportedError } from '../../errors.js'
4
+ import { compileExpression } from './evaluators.js'
5
+ import type { Aggregate, BasicExpression, Select } from '../ir.js'
6
6
  import type {
7
7
  KeyedStream,
8
8
  NamespacedAndKeyedStream,
9
9
  NamespacedRow,
10
- } from "../../types.js"
10
+ } from '../../types.js'
11
11
 
12
12
  /**
13
13
  * Type for operations array used in select processing
@@ -34,7 +34,7 @@ function unwrapVal(input: any): any {
34
34
  function processMerge(
35
35
  op: Extract<SelectOp, { kind: `merge` }>,
36
36
  namespacedRow: NamespacedRow,
37
- selectResults: Record<string, any>
37
+ selectResults: Record<string, any>,
38
38
  ): void {
39
39
  const value = op.source(namespacedRow)
40
40
  if (value && typeof value === `object`) {
@@ -74,7 +74,7 @@ function processMerge(
74
74
  function processNonMergeOp(
75
75
  op: Extract<SelectOp, { kind: `field` }>,
76
76
  namespacedRow: NamespacedRow,
77
- selectResults: Record<string, any>
77
+ selectResults: Record<string, any>,
78
78
  ): void {
79
79
  // Support nested alias paths like "meta.author.name"
80
80
  const path = op.alias.split(`.`)
@@ -99,7 +99,7 @@ function processNonMergeOp(
99
99
  */
100
100
  function processRow(
101
101
  [key, namespacedRow]: [unknown, NamespacedRow],
102
- ops: Array<SelectOp>
102
+ ops: Array<SelectOp>,
103
103
  ): [unknown, typeof namespacedRow & { __select_results: any }] {
104
104
  const selectResults: Record<string, any> = {}
105
105
 
@@ -131,7 +131,7 @@ function processRow(
131
131
  export function processSelect(
132
132
  pipeline: NamespacedAndKeyedStream,
133
133
  select: Select,
134
- _allInputs: Record<string, KeyedStream>
134
+ _allInputs: Record<string, KeyedStream>,
135
135
  ): NamespacedAndKeyedStream {
136
136
  // Build ordered operations to preserve authoring order (spreads and fields)
137
137
  const ops: Array<SelectOp> = []
@@ -145,7 +145,7 @@ export function processSelect(
145
145
  * Helper function to check if an expression is an aggregate
146
146
  */
147
147
  function isAggregateExpression(
148
- expr: BasicExpression | Aggregate
148
+ expr: BasicExpression | Aggregate,
149
149
  ): expr is Aggregate {
150
150
  return expr.type === `agg`
151
151
  }
@@ -155,7 +155,7 @@ function isAggregateExpression(
155
155
  */
156
156
  export function processArgument(
157
157
  arg: BasicExpression | Aggregate,
158
- namespacedRow: NamespacedRow
158
+ namespacedRow: NamespacedRow,
159
159
  ): any {
160
160
  if (isAggregateExpression(arg)) {
161
161
  throw new AggregateNotSupportedError()
@@ -189,7 +189,7 @@ function isNestedSelectObject(obj: any): boolean {
189
189
  function addFromObject(
190
190
  prefixPath: Array<string>,
191
191
  obj: any,
192
- ops: Array<SelectOp>
192
+ ops: Array<SelectOp>,
193
193
  ) {
194
194
  for (const [key, value] of Object.entries(obj)) {
195
195
  if (key.startsWith(`__SPREAD_SENTINEL__`)) {
@@ -1,5 +1,5 @@
1
- import type { QueryIR } from "../ir.js"
2
- import type { CompilationResult } from "./index.js"
1
+ import type { QueryIR } from '../ir.js'
2
+ import type { CompilationResult } from './index.js'
3
3
 
4
4
  /**
5
5
  * Cache for compiled subqueries to avoid duplicate compilation
@@ -27,7 +27,7 @@
27
27
  * ```
28
28
  */
29
29
 
30
- import type { IR, OperatorName } from "../index.js"
30
+ import type { IR, OperatorName } from '../index.js'
31
31
 
32
32
  type BasicExpression<T = any> = IR.BasicExpression<T>
33
33
  type OrderBy = IR.OrderBy
@@ -149,7 +149,7 @@ export function extractValue(expr: BasicExpression): any {
149
149
  */
150
150
  export function walkExpression(
151
151
  expr: BasicExpression | undefined | null,
152
- visitor: (node: BasicExpression) => void
152
+ visitor: (node: BasicExpression) => void,
153
153
  ): void {
154
154
  if (!expr) return
155
155
 
@@ -200,7 +200,7 @@ export function walkExpression(
200
200
  */
201
201
  export function parseWhereExpression<T = any>(
202
202
  expr: BasicExpression<boolean> | undefined | null,
203
- options: ParseWhereOptions<T>
203
+ options: ParseWhereOptions<T>,
204
204
  ): T | null {
205
205
  if (!expr) return null
206
206
 
@@ -226,7 +226,7 @@ export function parseWhereExpression<T = any>(
226
226
  return onUnknownOperator(name, args)
227
227
  }
228
228
  throw new Error(
229
- `No handler provided for operator: ${name}. Available handlers: ${Object.keys(handlers).join(`, `)}`
229
+ `No handler provided for operator: ${name}. Available handlers: ${Object.keys(handlers).join(`, `)}`,
230
230
  )
231
231
  }
232
232
 
@@ -263,7 +263,7 @@ export function parseWhereExpression<T = any>(
263
263
  * ```
264
264
  */
265
265
  export function parseOrderByExpression(
266
- orderBy: OrderBy | undefined | null
266
+ orderBy: OrderBy | undefined | null,
267
267
  ): Array<ParsedOrderBy> {
268
268
  if (!orderBy || orderBy.length === 0) {
269
269
  return []
@@ -274,7 +274,7 @@ export function parseOrderByExpression(
274
274
 
275
275
  if (!field) {
276
276
  throw new Error(
277
- `ORDER BY expression must be a field reference, got: ${clause.expression.type}`
277
+ `ORDER BY expression must be a field reference, got: ${clause.expression.type}`,
278
278
  )
279
279
  }
280
280
 
@@ -325,7 +325,7 @@ export function parseOrderByExpression(
325
325
  * ```
326
326
  */
327
327
  export function extractSimpleComparisons(
328
- expr: BasicExpression<boolean> | undefined | null
328
+ expr: BasicExpression<boolean> | undefined | null,
329
329
  ): Array<SimpleComparison> {
330
330
  if (!expr) return []
331
331
 
@@ -344,7 +344,7 @@ export function extractSimpleComparisons(
344
344
  const [arg] = e.args
345
345
  if (!arg || arg.type !== `func`) {
346
346
  throw new Error(
347
- `extractSimpleComparisons requires a comparison or null check inside 'not' operator.`
347
+ `extractSimpleComparisons requires a comparison or null check inside 'not' operator.`,
348
348
  )
349
349
  }
350
350
 
@@ -362,7 +362,7 @@ export function extractSimpleComparisons(
362
362
  })
363
363
  } else {
364
364
  throw new Error(
365
- `extractSimpleComparisons requires a field reference for '${arg.name}' operator.`
365
+ `extractSimpleComparisons requires a field reference for '${arg.name}' operator.`,
366
366
  )
367
367
  }
368
368
  return
@@ -383,7 +383,7 @@ export function extractSimpleComparisons(
383
383
  })
384
384
  } else {
385
385
  throw new Error(
386
- `extractSimpleComparisons requires simple field-value comparisons. Found complex expression for 'not(${arg.name})' operator.`
386
+ `extractSimpleComparisons requires simple field-value comparisons. Found complex expression for 'not(${arg.name})' operator.`,
387
387
  )
388
388
  }
389
389
  return
@@ -391,7 +391,7 @@ export function extractSimpleComparisons(
391
391
 
392
392
  // NOT can only wrap simple comparisons or null checks
393
393
  throw new Error(
394
- `extractSimpleComparisons does not support 'not(${arg.name})'. NOT can only wrap comparison operators (eq, gt, gte, lt, lte, in) or null checks (isNull, isUndefined).`
394
+ `extractSimpleComparisons does not support 'not(${arg.name})'. NOT can only wrap comparison operators (eq, gt, gte, lt, lte, in) or null checks (isNull, isUndefined).`,
395
395
  )
396
396
  }
397
397
 
@@ -414,7 +414,7 @@ export function extractSimpleComparisons(
414
414
  ]
415
415
  if (unsupportedOps.includes(e.name)) {
416
416
  throw new Error(
417
- `extractSimpleComparisons does not support '${e.name}' operator. Use parseWhereExpression with custom handlers for complex expressions.`
417
+ `extractSimpleComparisons does not support '${e.name}' operator. Use parseWhereExpression with custom handlers for complex expressions.`,
418
418
  )
419
419
  }
420
420
 
@@ -434,7 +434,7 @@ export function extractSimpleComparisons(
434
434
  })
435
435
  } else {
436
436
  throw new Error(
437
- `extractSimpleComparisons requires a field reference for '${e.name}' operator.`
437
+ `extractSimpleComparisons requires a field reference for '${e.name}' operator.`,
438
438
  )
439
439
  }
440
440
  return
@@ -457,13 +457,13 @@ export function extractSimpleComparisons(
457
457
  })
458
458
  } else {
459
459
  throw new Error(
460
- `extractSimpleComparisons requires simple field-value comparisons. Found complex expression for '${e.name}' operator.`
460
+ `extractSimpleComparisons requires simple field-value comparisons. Found complex expression for '${e.name}' operator.`,
461
461
  )
462
462
  }
463
463
  } else {
464
464
  // Unknown operator
465
465
  throw new Error(
466
- `extractSimpleComparisons encountered unknown operator: '${e.name}'`
466
+ `extractSimpleComparisons encountered unknown operator: '${e.name}'`,
467
467
  )
468
468
  }
469
469
  }
@@ -504,7 +504,7 @@ export function parseLoadSubsetOptions(
504
504
  limit?: number
505
505
  }
506
506
  | undefined
507
- | null
507
+ | null,
508
508
  ): {
509
509
  filters: Array<SimpleComparison>
510
510
  sorts: Array<ParsedOrderBy>
@@ -10,7 +10,7 @@ export {
10
10
  type Source,
11
11
  type GetResult,
12
12
  type InferResultType,
13
- } from "./builder/index.js"
13
+ } from './builder/index.js'
14
14
 
15
15
  // Expression functions exports
16
16
  export {
@@ -41,22 +41,22 @@ export {
41
41
  sum,
42
42
  min,
43
43
  max,
44
- } from "./builder/functions.js"
44
+ } from './builder/functions.js'
45
45
 
46
46
  // Ref proxy utilities
47
- export type { Ref } from "./builder/types.js"
47
+ export type { Ref } from './builder/types.js'
48
48
 
49
49
  // Compiler
50
- export { compileQuery } from "./compiler/index.js"
50
+ export { compileQuery } from './compiler/index.js'
51
51
 
52
52
  // Live query collection utilities
53
53
  export {
54
54
  createLiveQueryCollection,
55
55
  liveQueryCollectionOptions,
56
- } from "./live-query-collection.js"
56
+ } from './live-query-collection.js'
57
57
 
58
- export { type LiveQueryCollectionConfig } from "./live/types.js"
59
- export { type LiveQueryCollectionUtils } from "./live/collection-config-builder.js"
58
+ export { type LiveQueryCollectionConfig } from './live/types.js'
59
+ export { type LiveQueryCollectionUtils } from './live/collection-config-builder.js'
60
60
 
61
61
  // Predicate utilities for predicate push-down
62
62
  export {
@@ -65,7 +65,8 @@ export {
65
65
  minusWherePredicates,
66
66
  isOrderBySubset,
67
67
  isLimitSubset,
68
+ isOffsetLimitSubset,
68
69
  isPredicateSubset,
69
- } from "./predicate-utils.js"
70
+ } from './predicate-utils.js'
70
71
 
71
- export { DeduplicatedLoadSubset } from "./subset-dedupe.js"
72
+ export { DeduplicatedLoadSubset } from './subset-dedupe.js'