@tanstack/db 0.4.20 → 0.5.0

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 (127) hide show
  1. package/dist/cjs/collection/change-events.cjs +10 -12
  2. package/dist/cjs/collection/change-events.cjs.map +1 -1
  3. package/dist/cjs/collection/change-events.d.cts +1 -8
  4. package/dist/cjs/collection/index.cjs +18 -0
  5. package/dist/cjs/collection/index.cjs.map +1 -1
  6. package/dist/cjs/collection/index.d.cts +7 -5
  7. package/dist/cjs/index.cjs +21 -3
  8. package/dist/cjs/index.cjs.map +1 -1
  9. package/dist/cjs/index.d.cts +2 -0
  10. package/dist/cjs/indexes/auto-index.cjs +7 -3
  11. package/dist/cjs/indexes/auto-index.cjs.map +1 -1
  12. package/dist/cjs/local-storage.cjs.map +1 -1
  13. package/dist/cjs/local-storage.d.cts +2 -2
  14. package/dist/cjs/query/builder/functions.cjs +34 -0
  15. package/dist/cjs/query/builder/functions.cjs.map +1 -1
  16. package/dist/cjs/query/builder/functions.d.cts +5 -0
  17. package/dist/cjs/query/builder/index.cjs +2 -2
  18. package/dist/cjs/query/builder/index.cjs.map +1 -1
  19. package/dist/cjs/query/builder/types.d.cts +3 -22
  20. package/dist/cjs/query/compiler/evaluators.cjs +57 -4
  21. package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
  22. package/dist/cjs/query/compiler/evaluators.d.cts +13 -0
  23. package/dist/cjs/query/compiler/expressions.cjs +4 -1
  24. package/dist/cjs/query/compiler/expressions.cjs.map +1 -1
  25. package/dist/cjs/query/compiler/group-by.cjs +3 -3
  26. package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
  27. package/dist/cjs/query/compiler/index.cjs +2 -2
  28. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  29. package/dist/cjs/query/compiler/order-by.cjs +18 -6
  30. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  31. package/dist/cjs/query/compiler/order-by.d.cts +7 -1
  32. package/dist/cjs/query/expression-helpers.cjs +217 -0
  33. package/dist/cjs/query/expression-helpers.cjs.map +1 -0
  34. package/dist/cjs/query/expression-helpers.d.cts +216 -0
  35. package/dist/cjs/query/index.d.cts +2 -0
  36. package/dist/cjs/query/live/collection-config-builder.cjs +13 -0
  37. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  38. package/dist/cjs/query/live/collection-config-builder.d.cts +1 -0
  39. package/dist/cjs/query/live/types.d.cts +6 -1
  40. package/dist/cjs/query/predicate-utils.cjs +816 -0
  41. package/dist/cjs/query/predicate-utils.cjs.map +1 -0
  42. package/dist/cjs/query/predicate-utils.d.cts +116 -0
  43. package/dist/cjs/query/subset-dedupe.cjs +111 -0
  44. package/dist/cjs/query/subset-dedupe.cjs.map +1 -0
  45. package/dist/cjs/query/subset-dedupe.d.cts +66 -0
  46. package/dist/cjs/types.d.cts +29 -0
  47. package/dist/cjs/utils/comparison.cjs +30 -0
  48. package/dist/cjs/utils/comparison.cjs.map +1 -1
  49. package/dist/cjs/utils/comparison.d.cts +7 -1
  50. package/dist/cjs/utils/index-optimization.cjs +26 -22
  51. package/dist/cjs/utils/index-optimization.cjs.map +1 -1
  52. package/dist/cjs/utils/index-optimization.d.cts +5 -4
  53. package/dist/esm/collection/change-events.d.ts +1 -8
  54. package/dist/esm/collection/change-events.js +7 -9
  55. package/dist/esm/collection/change-events.js.map +1 -1
  56. package/dist/esm/collection/index.d.ts +7 -5
  57. package/dist/esm/collection/index.js +18 -0
  58. package/dist/esm/collection/index.js.map +1 -1
  59. package/dist/esm/index.d.ts +2 -0
  60. package/dist/esm/index.js +19 -1
  61. package/dist/esm/index.js.map +1 -1
  62. package/dist/esm/indexes/auto-index.js +7 -3
  63. package/dist/esm/indexes/auto-index.js.map +1 -1
  64. package/dist/esm/local-storage.d.ts +2 -2
  65. package/dist/esm/local-storage.js.map +1 -1
  66. package/dist/esm/query/builder/functions.d.ts +5 -0
  67. package/dist/esm/query/builder/functions.js +34 -0
  68. package/dist/esm/query/builder/functions.js.map +1 -1
  69. package/dist/esm/query/builder/index.js +2 -2
  70. package/dist/esm/query/builder/index.js.map +1 -1
  71. package/dist/esm/query/builder/types.d.ts +3 -22
  72. package/dist/esm/query/compiler/evaluators.d.ts +13 -0
  73. package/dist/esm/query/compiler/evaluators.js +59 -6
  74. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  75. package/dist/esm/query/compiler/expressions.js +4 -1
  76. package/dist/esm/query/compiler/expressions.js.map +1 -1
  77. package/dist/esm/query/compiler/group-by.js +4 -4
  78. package/dist/esm/query/compiler/group-by.js.map +1 -1
  79. package/dist/esm/query/compiler/index.js +3 -3
  80. package/dist/esm/query/compiler/index.js.map +1 -1
  81. package/dist/esm/query/compiler/order-by.d.ts +7 -1
  82. package/dist/esm/query/compiler/order-by.js +18 -6
  83. package/dist/esm/query/compiler/order-by.js.map +1 -1
  84. package/dist/esm/query/expression-helpers.d.ts +216 -0
  85. package/dist/esm/query/expression-helpers.js +217 -0
  86. package/dist/esm/query/expression-helpers.js.map +1 -0
  87. package/dist/esm/query/index.d.ts +2 -0
  88. package/dist/esm/query/live/collection-config-builder.d.ts +1 -0
  89. package/dist/esm/query/live/collection-config-builder.js +13 -0
  90. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  91. package/dist/esm/query/live/types.d.ts +6 -1
  92. package/dist/esm/query/predicate-utils.d.ts +116 -0
  93. package/dist/esm/query/predicate-utils.js +816 -0
  94. package/dist/esm/query/predicate-utils.js.map +1 -0
  95. package/dist/esm/query/subset-dedupe.d.ts +66 -0
  96. package/dist/esm/query/subset-dedupe.js +111 -0
  97. package/dist/esm/query/subset-dedupe.js.map +1 -0
  98. package/dist/esm/types.d.ts +29 -0
  99. package/dist/esm/utils/comparison.d.ts +7 -1
  100. package/dist/esm/utils/comparison.js +30 -0
  101. package/dist/esm/utils/comparison.js.map +1 -1
  102. package/dist/esm/utils/index-optimization.d.ts +5 -4
  103. package/dist/esm/utils/index-optimization.js +26 -22
  104. package/dist/esm/utils/index-optimization.js.map +1 -1
  105. package/package.json +2 -2
  106. package/src/collection/change-events.ts +14 -24
  107. package/src/collection/index.ts +32 -4
  108. package/src/index.ts +4 -0
  109. package/src/indexes/auto-index.ts +8 -4
  110. package/src/local-storage.ts +11 -3
  111. package/src/query/builder/functions.ts +39 -0
  112. package/src/query/builder/index.ts +2 -2
  113. package/src/query/builder/types.ts +3 -25
  114. package/src/query/compiler/evaluators.ts +103 -5
  115. package/src/query/compiler/expressions.ts +3 -0
  116. package/src/query/compiler/group-by.ts +4 -4
  117. package/src/query/compiler/index.ts +3 -3
  118. package/src/query/compiler/order-by.ts +33 -7
  119. package/src/query/expression-helpers.ts +522 -0
  120. package/src/query/index.ts +12 -0
  121. package/src/query/live/collection-config-builder.ts +27 -0
  122. package/src/query/live/types.ts +11 -1
  123. package/src/query/predicate-utils.ts +1415 -0
  124. package/src/query/subset-dedupe.ts +243 -0
  125. package/src/types.ts +39 -0
  126. package/src/utils/comparison.ts +70 -1
  127. package/src/utils/index-optimization.ts +77 -63
@@ -0,0 +1,522 @@
1
+ /**
2
+ * Expression Helpers for TanStack DB
3
+ *
4
+ * These utilities help parse LoadSubsetOptions (where, orderBy, limit) from TanStack DB
5
+ * into formats suitable for your API backend. They provide a generic way to traverse
6
+ * expression trees without having to implement your own parser.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { parseWhereExpression, parseOrderByExpression } from '@tanstack/db'
11
+ *
12
+ * queryFn: async (ctx) => {
13
+ * const { limit, where, orderBy } = ctx.meta?.loadSubsetOptions ?? {}
14
+ *
15
+ * // Convert expression tree to filters
16
+ * const filters = parseWhereExpression(where, {
17
+ * eq: (field, value) => ({ [field]: value }),
18
+ * lt: (field, value) => ({ [`${field}_lt`]: value }),
19
+ * and: (filters) => Object.assign({}, ...filters)
20
+ * })
21
+ *
22
+ * // Extract sort information
23
+ * const sort = parseOrderByExpression(orderBy)
24
+ *
25
+ * return api.getProducts({ ...filters, sort, limit })
26
+ * }
27
+ * ```
28
+ */
29
+
30
+ import type { IR, OperatorName } from "../index.js"
31
+
32
+ type BasicExpression<T = any> = IR.BasicExpression<T>
33
+ type OrderBy = IR.OrderBy
34
+
35
+ /**
36
+ * Represents a simple field path extracted from an expression.
37
+ * Can include string keys for object properties and numbers for array indices.
38
+ */
39
+ export type FieldPath = Array<string | number>
40
+
41
+ /**
42
+ * Represents a simple comparison operation
43
+ */
44
+ export interface SimpleComparison {
45
+ field: FieldPath
46
+ operator: string
47
+ value?: any // Optional for operators like isNull and isUndefined that don't have a value
48
+ }
49
+
50
+ /**
51
+ * Options for customizing how WHERE expressions are parsed
52
+ */
53
+ export interface ParseWhereOptions<T = any> {
54
+ /**
55
+ * Handler functions for different operators.
56
+ * Each handler receives the parsed field path(s) and value(s) and returns your custom format.
57
+ *
58
+ * Supported operators from TanStack DB:
59
+ * - Comparison: eq, gt, gte, lt, lte, in, like, ilike
60
+ * - Logical: and, or, not
61
+ * - Null checking: isNull, isUndefined
62
+ * - String functions: upper, lower, length, concat
63
+ * - Numeric: add
64
+ * - Utility: coalesce
65
+ * - Aggregates: count, avg, sum, min, max
66
+ */
67
+ handlers: {
68
+ [K in OperatorName]?: (...args: Array<any>) => T
69
+ } & {
70
+ [key: string]: (...args: Array<any>) => T
71
+ }
72
+ /**
73
+ * Optional handler for when an unknown operator is encountered.
74
+ * If not provided, unknown operators throw an error.
75
+ */
76
+ onUnknownOperator?: (operator: string, args: Array<any>) => T
77
+ }
78
+
79
+ /**
80
+ * Result of parsing an ORDER BY expression
81
+ */
82
+ export interface ParsedOrderBy {
83
+ field: FieldPath
84
+ direction: `asc` | `desc`
85
+ nulls: `first` | `last`
86
+ /** String sorting method: 'lexical' (default) or 'locale' (locale-aware) */
87
+ stringSort?: `lexical` | `locale`
88
+ /** Locale for locale-aware string sorting (e.g., 'en-US') */
89
+ locale?: string
90
+ /** Additional options for locale-aware sorting */
91
+ localeOptions?: object
92
+ }
93
+
94
+ /**
95
+ * Extracts the field path from a PropRef expression.
96
+ * Returns null for non-ref expressions.
97
+ *
98
+ * @param expr - The expression to extract from
99
+ * @returns The field path array, or null
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * const field = extractFieldPath(someExpression)
104
+ * // Returns: ['product', 'category']
105
+ * ```
106
+ */
107
+ export function extractFieldPath(expr: BasicExpression): FieldPath | null {
108
+ if (expr.type === `ref`) {
109
+ return expr.path
110
+ }
111
+ return null
112
+ }
113
+
114
+ /**
115
+ * Extracts the value from a Value expression.
116
+ * Returns undefined for non-value expressions.
117
+ *
118
+ * @param expr - The expression to extract from
119
+ * @returns The extracted value
120
+ *
121
+ * @example
122
+ * ```typescript
123
+ * const val = extractValue(someExpression)
124
+ * // Returns: 'electronics'
125
+ * ```
126
+ */
127
+ export function extractValue(expr: BasicExpression): any {
128
+ if (expr.type === `val`) {
129
+ return expr.value
130
+ }
131
+ return undefined
132
+ }
133
+
134
+ /**
135
+ * Generic expression tree walker that visits each node in the expression.
136
+ * Useful for implementing custom parsing logic.
137
+ *
138
+ * @param expr - The expression to walk
139
+ * @param visitor - Visitor function called for each node
140
+ *
141
+ * @example
142
+ * ```typescript
143
+ * walkExpression(whereExpr, (node) => {
144
+ * if (node.type === 'func' && node.name === 'eq') {
145
+ * console.log('Found equality comparison')
146
+ * }
147
+ * })
148
+ * ```
149
+ */
150
+ export function walkExpression(
151
+ expr: BasicExpression | undefined | null,
152
+ visitor: (node: BasicExpression) => void
153
+ ): void {
154
+ if (!expr) return
155
+
156
+ visitor(expr)
157
+
158
+ if (expr.type === `func`) {
159
+ expr.args.forEach((arg: BasicExpression) => walkExpression(arg, visitor))
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Parses a WHERE expression into a custom format using provided handlers.
165
+ *
166
+ * This is the main helper for converting TanStack DB where clauses into your API's filter format.
167
+ * You provide handlers for each operator, and this function traverses the expression tree
168
+ * and calls the appropriate handlers.
169
+ *
170
+ * @param expr - The WHERE expression to parse
171
+ * @param options - Configuration with handler functions for each operator
172
+ * @returns The parsed result in your custom format
173
+ *
174
+ * @example
175
+ * ```typescript
176
+ * // REST API with query parameters
177
+ * const filters = parseWhereExpression(where, {
178
+ * handlers: {
179
+ * eq: (field, value) => ({ [field.join('.')]: value }),
180
+ * lt: (field, value) => ({ [`${field.join('.')}_lt`]: value }),
181
+ * gt: (field, value) => ({ [`${field.join('.')}_gt`]: value }),
182
+ * and: (...filters) => Object.assign({}, ...filters),
183
+ * or: (...filters) => ({ $or: filters })
184
+ * }
185
+ * })
186
+ * // Returns: { category: 'electronics', price_lt: 100 }
187
+ * ```
188
+ *
189
+ * @example
190
+ * ```typescript
191
+ * // GraphQL where clause
192
+ * const where = parseWhereExpression(whereExpr, {
193
+ * handlers: {
194
+ * eq: (field, value) => ({ [field.join('_')]: { _eq: value } }),
195
+ * lt: (field, value) => ({ [field.join('_')]: { _lt: value } }),
196
+ * and: (...filters) => ({ _and: filters })
197
+ * }
198
+ * })
199
+ * ```
200
+ */
201
+ export function parseWhereExpression<T = any>(
202
+ expr: BasicExpression<boolean> | undefined | null,
203
+ options: ParseWhereOptions<T>
204
+ ): T | null {
205
+ if (!expr) return null
206
+
207
+ const { handlers, onUnknownOperator } = options
208
+
209
+ // Handle value expressions
210
+ if (expr.type === `val`) {
211
+ return expr.value as unknown as T
212
+ }
213
+
214
+ // Handle property references
215
+ if (expr.type === `ref`) {
216
+ return expr.path as unknown as T
217
+ }
218
+
219
+ // Handle function expressions
220
+ // After checking val and ref, expr must be func
221
+ const { name, args } = expr
222
+ const handler = handlers[name]
223
+
224
+ if (!handler) {
225
+ if (onUnknownOperator) {
226
+ return onUnknownOperator(name, args)
227
+ }
228
+ throw new Error(
229
+ `No handler provided for operator: ${name}. Available handlers: ${Object.keys(handlers).join(`, `)}`
230
+ )
231
+ }
232
+
233
+ // Parse arguments recursively
234
+ const parsedArgs = args.map((arg: BasicExpression) => {
235
+ // For refs, extract the field path
236
+ if (arg.type === `ref`) {
237
+ return arg.path
238
+ }
239
+ // For values, extract the value
240
+ if (arg.type === `val`) {
241
+ return arg.value
242
+ }
243
+ // For nested functions, recurse (after checking ref and val, must be func)
244
+ return parseWhereExpression(arg, options)
245
+ })
246
+
247
+ return handler(...parsedArgs)
248
+ }
249
+
250
+ /**
251
+ * Parses an ORDER BY expression into a simple array of sort specifications.
252
+ *
253
+ * @param orderBy - The ORDER BY expression array
254
+ * @returns Array of parsed order by specifications
255
+ *
256
+ * @example
257
+ * ```typescript
258
+ * const sorts = parseOrderByExpression(orderBy)
259
+ * // Returns: [
260
+ * // { field: ['category'], direction: 'asc', nulls: 'last' },
261
+ * // { field: ['price'], direction: 'desc', nulls: 'last' }
262
+ * // ]
263
+ * ```
264
+ */
265
+ export function parseOrderByExpression(
266
+ orderBy: OrderBy | undefined | null
267
+ ): Array<ParsedOrderBy> {
268
+ if (!orderBy || orderBy.length === 0) {
269
+ return []
270
+ }
271
+
272
+ return orderBy.map((clause: IR.OrderByClause) => {
273
+ const field = extractFieldPath(clause.expression)
274
+
275
+ if (!field) {
276
+ throw new Error(
277
+ `ORDER BY expression must be a field reference, got: ${clause.expression.type}`
278
+ )
279
+ }
280
+
281
+ const { direction, nulls } = clause.compareOptions
282
+ const result: ParsedOrderBy = {
283
+ field,
284
+ direction,
285
+ nulls,
286
+ }
287
+
288
+ // Add string collation options if present (discriminated union)
289
+ if (`stringSort` in clause.compareOptions) {
290
+ result.stringSort = clause.compareOptions.stringSort
291
+ }
292
+ if (`locale` in clause.compareOptions) {
293
+ result.locale = clause.compareOptions.locale
294
+ }
295
+ if (`localeOptions` in clause.compareOptions) {
296
+ result.localeOptions = clause.compareOptions.localeOptions
297
+ }
298
+
299
+ return result
300
+ })
301
+ }
302
+
303
+ /**
304
+ * Extracts all simple comparisons from a WHERE expression.
305
+ * This is useful for simple APIs that only support basic filters.
306
+ *
307
+ * Note: This only works for simple AND-ed conditions and NOT-wrapped comparisons.
308
+ * Throws an error if it encounters unsupported operations like OR or complex nested expressions.
309
+ *
310
+ * NOT operators are flattened by prefixing the operator name (e.g., `not(eq(...))` becomes `not_eq`).
311
+ *
312
+ * @param expr - The WHERE expression to parse
313
+ * @returns Array of simple comparisons
314
+ * @throws Error if expression contains OR or other unsupported operations
315
+ *
316
+ * @example
317
+ * ```typescript
318
+ * const comparisons = extractSimpleComparisons(where)
319
+ * // Returns: [
320
+ * // { field: ['category'], operator: 'eq', value: 'electronics' },
321
+ * // { field: ['price'], operator: 'lt', value: 100 },
322
+ * // { field: ['email'], operator: 'isNull' }, // No value for null checks
323
+ * // { field: ['status'], operator: 'not_eq', value: 'archived' }
324
+ * // ]
325
+ * ```
326
+ */
327
+ export function extractSimpleComparisons(
328
+ expr: BasicExpression<boolean> | undefined | null
329
+ ): Array<SimpleComparison> {
330
+ if (!expr) return []
331
+
332
+ const comparisons: Array<SimpleComparison> = []
333
+
334
+ function extract(e: BasicExpression): void {
335
+ if (e.type === `func`) {
336
+ // Handle AND - recurse into both sides
337
+ if (e.name === `and`) {
338
+ e.args.forEach((arg: BasicExpression) => extract(arg))
339
+ return
340
+ }
341
+
342
+ // Handle NOT - recurse into argument and prefix operator with 'not_'
343
+ if (e.name === `not`) {
344
+ const [arg] = e.args
345
+ if (!arg || arg.type !== `func`) {
346
+ throw new Error(
347
+ `extractSimpleComparisons requires a comparison or null check inside 'not' operator.`
348
+ )
349
+ }
350
+
351
+ // Handle NOT with null/undefined checks
352
+ const nullCheckOps = [`isNull`, `isUndefined`]
353
+ if (nullCheckOps.includes(arg.name)) {
354
+ const [fieldArg] = arg.args
355
+ const field = fieldArg?.type === `ref` ? fieldArg.path : null
356
+
357
+ if (field) {
358
+ comparisons.push({
359
+ field,
360
+ operator: `not_${arg.name}`,
361
+ // No value for null/undefined checks
362
+ })
363
+ } else {
364
+ throw new Error(
365
+ `extractSimpleComparisons requires a field reference for '${arg.name}' operator.`
366
+ )
367
+ }
368
+ return
369
+ }
370
+
371
+ // Handle NOT with comparison operators
372
+ const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `in`]
373
+ if (comparisonOps.includes(arg.name)) {
374
+ const [leftArg, rightArg] = arg.args
375
+ const field = leftArg?.type === `ref` ? leftArg.path : null
376
+ const value = rightArg?.type === `val` ? rightArg.value : null
377
+
378
+ if (field && value !== undefined) {
379
+ comparisons.push({
380
+ field,
381
+ operator: `not_${arg.name}`,
382
+ value,
383
+ })
384
+ } else {
385
+ throw new Error(
386
+ `extractSimpleComparisons requires simple field-value comparisons. Found complex expression for 'not(${arg.name})' operator.`
387
+ )
388
+ }
389
+ return
390
+ }
391
+
392
+ // NOT can only wrap simple comparisons or null checks
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).`
395
+ )
396
+ }
397
+
398
+ // Throw on unsupported operations
399
+ const unsupportedOps = [
400
+ `or`,
401
+ `like`,
402
+ `ilike`,
403
+ `upper`,
404
+ `lower`,
405
+ `length`,
406
+ `concat`,
407
+ `add`,
408
+ `coalesce`,
409
+ `count`,
410
+ `avg`,
411
+ `sum`,
412
+ `min`,
413
+ `max`,
414
+ ]
415
+ if (unsupportedOps.includes(e.name)) {
416
+ throw new Error(
417
+ `extractSimpleComparisons does not support '${e.name}' operator. Use parseWhereExpression with custom handlers for complex expressions.`
418
+ )
419
+ }
420
+
421
+ // Handle null/undefined check operators (single argument, no value)
422
+ const nullCheckOps = [`isNull`, `isUndefined`]
423
+ if (nullCheckOps.includes(e.name)) {
424
+ const [fieldArg] = e.args
425
+
426
+ // Extract field (must be a ref)
427
+ const field = fieldArg?.type === `ref` ? fieldArg.path : null
428
+
429
+ if (field) {
430
+ comparisons.push({
431
+ field,
432
+ operator: e.name,
433
+ // No value for null/undefined checks
434
+ })
435
+ } else {
436
+ throw new Error(
437
+ `extractSimpleComparisons requires a field reference for '${e.name}' operator.`
438
+ )
439
+ }
440
+ return
441
+ }
442
+
443
+ // Handle comparison operators
444
+ const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `in`]
445
+ if (comparisonOps.includes(e.name)) {
446
+ const [leftArg, rightArg] = e.args
447
+
448
+ // Extract field and value
449
+ const field = leftArg?.type === `ref` ? leftArg.path : null
450
+ const value = rightArg?.type === `val` ? rightArg.value : null
451
+
452
+ if (field && value !== undefined) {
453
+ comparisons.push({
454
+ field,
455
+ operator: e.name,
456
+ value,
457
+ })
458
+ } else {
459
+ throw new Error(
460
+ `extractSimpleComparisons requires simple field-value comparisons. Found complex expression for '${e.name}' operator.`
461
+ )
462
+ }
463
+ } else {
464
+ // Unknown operator
465
+ throw new Error(
466
+ `extractSimpleComparisons encountered unknown operator: '${e.name}'`
467
+ )
468
+ }
469
+ }
470
+ }
471
+
472
+ extract(expr)
473
+ return comparisons
474
+ }
475
+
476
+ /**
477
+ * Convenience function to get all LoadSubsetOptions in a pre-parsed format.
478
+ * Good starting point for simple use cases.
479
+ *
480
+ * @param options - The LoadSubsetOptions from ctx.meta
481
+ * @returns Pre-parsed filters, sorts, and limit
482
+ *
483
+ * @example
484
+ * ```typescript
485
+ * queryFn: async (ctx) => {
486
+ * const parsed = parseLoadSubsetOptions(ctx.meta?.loadSubsetOptions)
487
+ *
488
+ * // Convert to your API format
489
+ * return api.getProducts({
490
+ * ...Object.fromEntries(
491
+ * parsed.filters.map(f => [`${f.field.join('.')}_${f.operator}`, f.value])
492
+ * ),
493
+ * sort: parsed.sorts.map(s => `${s.field.join('.')}:${s.direction}`).join(','),
494
+ * limit: parsed.limit
495
+ * })
496
+ * }
497
+ * ```
498
+ */
499
+ export function parseLoadSubsetOptions(
500
+ options:
501
+ | {
502
+ where?: BasicExpression<boolean>
503
+ orderBy?: OrderBy
504
+ limit?: number
505
+ }
506
+ | undefined
507
+ | null
508
+ ): {
509
+ filters: Array<SimpleComparison>
510
+ sorts: Array<ParsedOrderBy>
511
+ limit?: number
512
+ } {
513
+ if (!options) {
514
+ return { filters: [], sorts: [] }
515
+ }
516
+
517
+ return {
518
+ filters: extractSimpleComparisons(options.where),
519
+ sorts: parseOrderByExpression(options.orderBy),
520
+ limit: options.limit,
521
+ }
522
+ }
@@ -57,3 +57,15 @@ export {
57
57
 
58
58
  export { type LiveQueryCollectionConfig } from "./live/types.js"
59
59
  export { type LiveQueryCollectionUtils } from "./live/collection-config-builder.js"
60
+
61
+ // Predicate utilities for predicate push-down
62
+ export {
63
+ isWhereSubset,
64
+ unionWherePredicates,
65
+ minusWherePredicates,
66
+ isOrderBySubset,
67
+ isLimitSubset,
68
+ isPredicateSubset,
69
+ } from "./predicate-utils.js"
70
+
71
+ export { DeduplicatedLoadSubset } from "./subset-dedupe.js"
@@ -21,6 +21,7 @@ import type {
21
21
  CollectionConfigSingleRowOption,
22
22
  KeyedStream,
23
23
  ResultStream,
24
+ StringCollationConfig,
24
25
  SyncConfig,
25
26
  UtilsRecord,
26
27
  } from "../../types.js"
@@ -83,6 +84,7 @@ export class CollectionConfigBuilder<
83
84
  private readonly orderByIndices = new WeakMap<object, string>()
84
85
 
85
86
  private readonly compare?: (val1: TResult, val2: TResult) => number
87
+ private readonly compareOptions?: StringCollationConfig
86
88
 
87
89
  private isGraphRunning = false
88
90
  private runCount = 0
@@ -170,6 +172,11 @@ export class CollectionConfigBuilder<
170
172
  this.compare = createOrderByComparator<TResult>(this.orderByIndices)
171
173
  }
172
174
 
175
+ // Use explicitly provided compareOptions if available, otherwise inherit from FROM collection
176
+ this.compareOptions =
177
+ this.config.defaultStringCollation ??
178
+ extractCollectionFromSource(this.query).compareOptions
179
+
173
180
  // Compile the base pipeline once initially
174
181
  // This is done to ensure that any errors are thrown immediately and synchronously
175
182
  this.compileBasePipeline()
@@ -204,6 +211,7 @@ export class CollectionConfigBuilder<
204
211
  ((item) => this.resultKeys.get(item) as string | number),
205
212
  sync: this.getSyncConfig(),
206
213
  compare: this.compare,
214
+ defaultStringCollation: this.compareOptions,
207
215
  gcTime: this.config.gcTime || 5000, // 5 seconds by default for live queries
208
216
  schema: this.config.schema,
209
217
  onInsert: this.config.onInsert,
@@ -951,6 +959,25 @@ function extractCollectionsFromQuery(
951
959
  return collections
952
960
  }
953
961
 
962
+ /**
963
+ * Helper function to extract the collection that is referenced in the query's FROM clause.
964
+ * The FROM clause may refer directly to a collection or indirectly to a subquery.
965
+ */
966
+ function extractCollectionFromSource(query: any): Collection<any, any, any> {
967
+ const from = query.from
968
+
969
+ if (from.type === `collectionRef`) {
970
+ return from.collection
971
+ } else if (from.type === `queryRef`) {
972
+ // Recursively extract from subquery
973
+ return extractCollectionFromSource(from.query)
974
+ }
975
+
976
+ throw new Error(
977
+ `Failed to extract collection. Invalid FROM clause: ${JSON.stringify(query)}`
978
+ )
979
+ }
980
+
954
981
  /**
955
982
  * Extracts all aliases used for each collection across the entire query tree.
956
983
  *
@@ -1,5 +1,9 @@
1
1
  import type { D2, RootStreamBuilder } from "@tanstack/db-ivm"
2
- import type { CollectionConfig, ResultStream } from "../../types.js"
2
+ import type {
3
+ CollectionConfig,
4
+ ResultStream,
5
+ StringCollationConfig,
6
+ } from "../../types.js"
3
7
  import type { InitialQueryBuilder, QueryBuilder } from "../builder/index.js"
4
8
  import type { Context, GetResult } from "../builder/types.js"
5
9
 
@@ -95,4 +99,10 @@ export interface LiveQueryCollectionConfig<
95
99
  * If enabled the collection will return a single object instead of an array
96
100
  */
97
101
  singleResult?: true
102
+
103
+ /**
104
+ * Optional compare options for string sorting.
105
+ * If provided, these will be used instead of inheriting from the FROM collection.
106
+ */
107
+ defaultStringCollation?: StringCollationConfig
98
108
  }