@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.
- package/dist/cjs/collection/change-events.cjs +10 -12
- package/dist/cjs/collection/change-events.cjs.map +1 -1
- package/dist/cjs/collection/change-events.d.cts +1 -8
- package/dist/cjs/collection/index.cjs +18 -0
- package/dist/cjs/collection/index.cjs.map +1 -1
- package/dist/cjs/collection/index.d.cts +7 -5
- package/dist/cjs/index.cjs +21 -3
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +2 -0
- package/dist/cjs/indexes/auto-index.cjs +7 -3
- package/dist/cjs/indexes/auto-index.cjs.map +1 -1
- package/dist/cjs/local-storage.cjs.map +1 -1
- package/dist/cjs/local-storage.d.cts +2 -2
- package/dist/cjs/query/builder/functions.cjs +34 -0
- package/dist/cjs/query/builder/functions.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.d.cts +5 -0
- package/dist/cjs/query/builder/index.cjs +2 -2
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/builder/types.d.cts +3 -22
- package/dist/cjs/query/compiler/evaluators.cjs +57 -4
- package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
- package/dist/cjs/query/compiler/evaluators.d.cts +13 -0
- package/dist/cjs/query/compiler/expressions.cjs +4 -1
- package/dist/cjs/query/compiler/expressions.cjs.map +1 -1
- package/dist/cjs/query/compiler/group-by.cjs +3 -3
- package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.cjs +2 -2
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.cjs +18 -6
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.d.cts +7 -1
- package/dist/cjs/query/expression-helpers.cjs +217 -0
- package/dist/cjs/query/expression-helpers.cjs.map +1 -0
- package/dist/cjs/query/expression-helpers.d.cts +216 -0
- package/dist/cjs/query/index.d.cts +2 -0
- package/dist/cjs/query/live/collection-config-builder.cjs +13 -0
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.d.cts +1 -0
- package/dist/cjs/query/live/types.d.cts +6 -1
- package/dist/cjs/query/predicate-utils.cjs +816 -0
- package/dist/cjs/query/predicate-utils.cjs.map +1 -0
- package/dist/cjs/query/predicate-utils.d.cts +116 -0
- package/dist/cjs/query/subset-dedupe.cjs +111 -0
- package/dist/cjs/query/subset-dedupe.cjs.map +1 -0
- package/dist/cjs/query/subset-dedupe.d.cts +66 -0
- package/dist/cjs/types.d.cts +29 -0
- package/dist/cjs/utils/comparison.cjs +30 -0
- package/dist/cjs/utils/comparison.cjs.map +1 -1
- package/dist/cjs/utils/comparison.d.cts +7 -1
- package/dist/cjs/utils/index-optimization.cjs +26 -22
- package/dist/cjs/utils/index-optimization.cjs.map +1 -1
- package/dist/cjs/utils/index-optimization.d.cts +5 -4
- package/dist/esm/collection/change-events.d.ts +1 -8
- package/dist/esm/collection/change-events.js +7 -9
- package/dist/esm/collection/change-events.js.map +1 -1
- package/dist/esm/collection/index.d.ts +7 -5
- package/dist/esm/collection/index.js +18 -0
- package/dist/esm/collection/index.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +19 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/indexes/auto-index.js +7 -3
- package/dist/esm/indexes/auto-index.js.map +1 -1
- package/dist/esm/local-storage.d.ts +2 -2
- package/dist/esm/local-storage.js.map +1 -1
- package/dist/esm/query/builder/functions.d.ts +5 -0
- package/dist/esm/query/builder/functions.js +34 -0
- package/dist/esm/query/builder/functions.js.map +1 -1
- package/dist/esm/query/builder/index.js +2 -2
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/builder/types.d.ts +3 -22
- package/dist/esm/query/compiler/evaluators.d.ts +13 -0
- package/dist/esm/query/compiler/evaluators.js +59 -6
- package/dist/esm/query/compiler/evaluators.js.map +1 -1
- package/dist/esm/query/compiler/expressions.js +4 -1
- package/dist/esm/query/compiler/expressions.js.map +1 -1
- package/dist/esm/query/compiler/group-by.js +4 -4
- package/dist/esm/query/compiler/group-by.js.map +1 -1
- package/dist/esm/query/compiler/index.js +3 -3
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/order-by.d.ts +7 -1
- package/dist/esm/query/compiler/order-by.js +18 -6
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/expression-helpers.d.ts +216 -0
- package/dist/esm/query/expression-helpers.js +217 -0
- package/dist/esm/query/expression-helpers.js.map +1 -0
- package/dist/esm/query/index.d.ts +2 -0
- package/dist/esm/query/live/collection-config-builder.d.ts +1 -0
- package/dist/esm/query/live/collection-config-builder.js +13 -0
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/types.d.ts +6 -1
- package/dist/esm/query/predicate-utils.d.ts +116 -0
- package/dist/esm/query/predicate-utils.js +816 -0
- package/dist/esm/query/predicate-utils.js.map +1 -0
- package/dist/esm/query/subset-dedupe.d.ts +66 -0
- package/dist/esm/query/subset-dedupe.js +111 -0
- package/dist/esm/query/subset-dedupe.js.map +1 -0
- package/dist/esm/types.d.ts +29 -0
- package/dist/esm/utils/comparison.d.ts +7 -1
- package/dist/esm/utils/comparison.js +30 -0
- package/dist/esm/utils/comparison.js.map +1 -1
- package/dist/esm/utils/index-optimization.d.ts +5 -4
- package/dist/esm/utils/index-optimization.js +26 -22
- package/dist/esm/utils/index-optimization.js.map +1 -1
- package/package.json +2 -2
- package/src/collection/change-events.ts +14 -24
- package/src/collection/index.ts +32 -4
- package/src/index.ts +4 -0
- package/src/indexes/auto-index.ts +8 -4
- package/src/local-storage.ts +11 -3
- package/src/query/builder/functions.ts +39 -0
- package/src/query/builder/index.ts +2 -2
- package/src/query/builder/types.ts +3 -25
- package/src/query/compiler/evaluators.ts +103 -5
- package/src/query/compiler/expressions.ts +3 -0
- package/src/query/compiler/group-by.ts +4 -4
- package/src/query/compiler/index.ts +3 -3
- package/src/query/compiler/order-by.ts +33 -7
- package/src/query/expression-helpers.ts +522 -0
- package/src/query/index.ts +12 -0
- package/src/query/live/collection-config-builder.ts +27 -0
- package/src/query/live/types.ts +11 -1
- package/src/query/predicate-utils.ts +1415 -0
- package/src/query/subset-dedupe.ts +243 -0
- package/src/types.ts +39 -0
- package/src/utils/comparison.ts +70 -1
- 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
|
+
}
|
package/src/query/index.ts
CHANGED
|
@@ -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
|
*
|
package/src/query/live/types.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { D2, RootStreamBuilder } from "@tanstack/db-ivm"
|
|
2
|
-
import type {
|
|
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
|
}
|