@tanstack/db 0.0.27 → 0.0.30

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 (167) hide show
  1. package/dist/cjs/change-events.cjs +141 -0
  2. package/dist/cjs/change-events.cjs.map +1 -0
  3. package/dist/cjs/change-events.d.cts +49 -0
  4. package/dist/cjs/collection.cjs +234 -86
  5. package/dist/cjs/collection.cjs.map +1 -1
  6. package/dist/cjs/collection.d.cts +95 -20
  7. package/dist/cjs/errors.cjs +509 -1
  8. package/dist/cjs/errors.cjs.map +1 -1
  9. package/dist/cjs/errors.d.cts +225 -1
  10. package/dist/cjs/index.cjs +82 -3
  11. package/dist/cjs/index.cjs.map +1 -1
  12. package/dist/cjs/index.d.cts +5 -1
  13. package/dist/cjs/indexes/auto-index.cjs +64 -0
  14. package/dist/cjs/indexes/auto-index.cjs.map +1 -0
  15. package/dist/cjs/indexes/auto-index.d.cts +9 -0
  16. package/dist/cjs/indexes/base-index.cjs +46 -0
  17. package/dist/cjs/indexes/base-index.cjs.map +1 -0
  18. package/dist/cjs/indexes/base-index.d.cts +54 -0
  19. package/dist/cjs/indexes/btree-index.cjs +191 -0
  20. package/dist/cjs/indexes/btree-index.cjs.map +1 -0
  21. package/dist/cjs/indexes/btree-index.d.cts +74 -0
  22. package/dist/cjs/indexes/index-options.d.cts +13 -0
  23. package/dist/cjs/indexes/lazy-index.cjs +193 -0
  24. package/dist/cjs/indexes/lazy-index.cjs.map +1 -0
  25. package/dist/cjs/indexes/lazy-index.d.cts +96 -0
  26. package/dist/cjs/local-storage.cjs +9 -15
  27. package/dist/cjs/local-storage.cjs.map +1 -1
  28. package/dist/cjs/query/builder/functions.cjs +11 -0
  29. package/dist/cjs/query/builder/functions.cjs.map +1 -1
  30. package/dist/cjs/query/builder/functions.d.cts +4 -0
  31. package/dist/cjs/query/builder/index.cjs +6 -7
  32. package/dist/cjs/query/builder/index.cjs.map +1 -1
  33. package/dist/cjs/query/builder/ref-proxy.cjs +37 -0
  34. package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
  35. package/dist/cjs/query/builder/ref-proxy.d.cts +12 -0
  36. package/dist/cjs/query/compiler/evaluators.cjs +83 -58
  37. package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
  38. package/dist/cjs/query/compiler/evaluators.d.cts +8 -0
  39. package/dist/cjs/query/compiler/expressions.cjs +61 -0
  40. package/dist/cjs/query/compiler/expressions.cjs.map +1 -0
  41. package/dist/cjs/query/compiler/expressions.d.cts +25 -0
  42. package/dist/cjs/query/compiler/group-by.cjs +5 -10
  43. package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
  44. package/dist/cjs/query/compiler/index.cjs +23 -17
  45. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  46. package/dist/cjs/query/compiler/index.d.cts +12 -3
  47. package/dist/cjs/query/compiler/joins.cjs +61 -12
  48. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  49. package/dist/cjs/query/compiler/order-by.cjs +4 -34
  50. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  51. package/dist/cjs/query/compiler/types.d.cts +2 -2
  52. package/dist/cjs/query/live-query-collection.cjs +54 -12
  53. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  54. package/dist/cjs/query/optimizer.cjs +45 -7
  55. package/dist/cjs/query/optimizer.cjs.map +1 -1
  56. package/dist/cjs/query/optimizer.d.cts +13 -3
  57. package/dist/cjs/transactions.cjs +5 -4
  58. package/dist/cjs/transactions.cjs.map +1 -1
  59. package/dist/cjs/types.d.cts +31 -0
  60. package/dist/cjs/utils/array-utils.d.cts +8 -0
  61. package/dist/cjs/utils/btree.cjs +677 -0
  62. package/dist/cjs/utils/btree.cjs.map +1 -0
  63. package/dist/cjs/utils/btree.d.cts +197 -0
  64. package/dist/cjs/utils/comparison.cjs +52 -0
  65. package/dist/cjs/utils/comparison.cjs.map +1 -0
  66. package/dist/cjs/utils/comparison.d.cts +11 -0
  67. package/dist/cjs/utils/index-optimization.cjs +270 -0
  68. package/dist/cjs/utils/index-optimization.cjs.map +1 -0
  69. package/dist/cjs/utils/index-optimization.d.cts +29 -0
  70. package/dist/esm/change-events.d.ts +49 -0
  71. package/dist/esm/change-events.js +141 -0
  72. package/dist/esm/change-events.js.map +1 -0
  73. package/dist/esm/collection.d.ts +95 -20
  74. package/dist/esm/collection.js +232 -84
  75. package/dist/esm/collection.js.map +1 -1
  76. package/dist/esm/errors.d.ts +225 -1
  77. package/dist/esm/errors.js +510 -2
  78. package/dist/esm/errors.js.map +1 -1
  79. package/dist/esm/index.d.ts +5 -1
  80. package/dist/esm/index.js +81 -2
  81. package/dist/esm/index.js.map +1 -1
  82. package/dist/esm/indexes/auto-index.d.ts +9 -0
  83. package/dist/esm/indexes/auto-index.js +64 -0
  84. package/dist/esm/indexes/auto-index.js.map +1 -0
  85. package/dist/esm/indexes/base-index.d.ts +54 -0
  86. package/dist/esm/indexes/base-index.js +46 -0
  87. package/dist/esm/indexes/base-index.js.map +1 -0
  88. package/dist/esm/indexes/btree-index.d.ts +74 -0
  89. package/dist/esm/indexes/btree-index.js +191 -0
  90. package/dist/esm/indexes/btree-index.js.map +1 -0
  91. package/dist/esm/indexes/index-options.d.ts +13 -0
  92. package/dist/esm/indexes/lazy-index.d.ts +96 -0
  93. package/dist/esm/indexes/lazy-index.js +193 -0
  94. package/dist/esm/indexes/lazy-index.js.map +1 -0
  95. package/dist/esm/local-storage.js +9 -15
  96. package/dist/esm/local-storage.js.map +1 -1
  97. package/dist/esm/query/builder/functions.d.ts +4 -0
  98. package/dist/esm/query/builder/functions.js +11 -0
  99. package/dist/esm/query/builder/functions.js.map +1 -1
  100. package/dist/esm/query/builder/index.js +6 -7
  101. package/dist/esm/query/builder/index.js.map +1 -1
  102. package/dist/esm/query/builder/ref-proxy.d.ts +12 -0
  103. package/dist/esm/query/builder/ref-proxy.js +37 -0
  104. package/dist/esm/query/builder/ref-proxy.js.map +1 -1
  105. package/dist/esm/query/compiler/evaluators.d.ts +8 -0
  106. package/dist/esm/query/compiler/evaluators.js +84 -59
  107. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  108. package/dist/esm/query/compiler/expressions.d.ts +25 -0
  109. package/dist/esm/query/compiler/expressions.js +61 -0
  110. package/dist/esm/query/compiler/expressions.js.map +1 -0
  111. package/dist/esm/query/compiler/group-by.js +5 -10
  112. package/dist/esm/query/compiler/group-by.js.map +1 -1
  113. package/dist/esm/query/compiler/index.d.ts +12 -3
  114. package/dist/esm/query/compiler/index.js +23 -17
  115. package/dist/esm/query/compiler/index.js.map +1 -1
  116. package/dist/esm/query/compiler/joins.js +61 -12
  117. package/dist/esm/query/compiler/joins.js.map +1 -1
  118. package/dist/esm/query/compiler/order-by.js +1 -31
  119. package/dist/esm/query/compiler/order-by.js.map +1 -1
  120. package/dist/esm/query/compiler/types.d.ts +2 -2
  121. package/dist/esm/query/live-query-collection.js +54 -12
  122. package/dist/esm/query/live-query-collection.js.map +1 -1
  123. package/dist/esm/query/optimizer.d.ts +13 -3
  124. package/dist/esm/query/optimizer.js +40 -2
  125. package/dist/esm/query/optimizer.js.map +1 -1
  126. package/dist/esm/transactions.js +5 -4
  127. package/dist/esm/transactions.js.map +1 -1
  128. package/dist/esm/types.d.ts +31 -0
  129. package/dist/esm/utils/array-utils.d.ts +8 -0
  130. package/dist/esm/utils/btree.d.ts +197 -0
  131. package/dist/esm/utils/btree.js +677 -0
  132. package/dist/esm/utils/btree.js.map +1 -0
  133. package/dist/esm/utils/comparison.d.ts +11 -0
  134. package/dist/esm/utils/comparison.js +52 -0
  135. package/dist/esm/utils/comparison.js.map +1 -0
  136. package/dist/esm/utils/index-optimization.d.ts +29 -0
  137. package/dist/esm/utils/index-optimization.js +270 -0
  138. package/dist/esm/utils/index-optimization.js.map +1 -0
  139. package/package.json +1 -1
  140. package/src/change-events.ts +257 -0
  141. package/src/collection.ts +316 -105
  142. package/src/errors.ts +545 -1
  143. package/src/index.ts +7 -1
  144. package/src/indexes/auto-index.ts +108 -0
  145. package/src/indexes/base-index.ts +119 -0
  146. package/src/indexes/btree-index.ts +263 -0
  147. package/src/indexes/index-options.ts +42 -0
  148. package/src/indexes/lazy-index.ts +251 -0
  149. package/src/local-storage.ts +16 -17
  150. package/src/query/builder/functions.ts +14 -0
  151. package/src/query/builder/index.ts +12 -7
  152. package/src/query/builder/ref-proxy.ts +65 -0
  153. package/src/query/compiler/evaluators.ts +114 -62
  154. package/src/query/compiler/expressions.ts +92 -0
  155. package/src/query/compiler/group-by.ts +10 -10
  156. package/src/query/compiler/index.ts +52 -22
  157. package/src/query/compiler/joins.ts +114 -15
  158. package/src/query/compiler/order-by.ts +1 -45
  159. package/src/query/compiler/types.ts +2 -2
  160. package/src/query/live-query-collection.ts +95 -15
  161. package/src/query/optimizer.ts +94 -5
  162. package/src/transactions.ts +10 -4
  163. package/src/types.ts +38 -0
  164. package/src/utils/array-utils.ts +28 -0
  165. package/src/utils/btree.ts +1010 -0
  166. package/src/utils/comparison.ts +79 -0
  167. package/src/utils/index-optimization.ts +546 -0
@@ -0,0 +1,79 @@
1
+ // WeakMap to store stable IDs for objects
2
+ const objectIds = new WeakMap<object, number>()
3
+ let nextObjectId = 1
4
+
5
+ /**
6
+ * Get or create a stable ID for an object
7
+ */
8
+ function getObjectId(obj: object): number {
9
+ if (objectIds.has(obj)) {
10
+ return objectIds.get(obj)!
11
+ }
12
+ const id = nextObjectId++
13
+ objectIds.set(obj, id)
14
+ return id
15
+ }
16
+
17
+ /**
18
+ * Universal comparison function for all data types
19
+ * Handles null/undefined, strings, arrays, dates, objects, and primitives
20
+ * Always sorts null/undefined values first
21
+ */
22
+ export const ascComparator = (a: any, b: any): number => {
23
+ // Handle null/undefined
24
+ if (a == null && b == null) return 0
25
+ if (a == null) return -1
26
+ if (b == null) return 1
27
+
28
+ // if a and b are both strings, compare them based on locale
29
+ if (typeof a === `string` && typeof b === `string`) {
30
+ return a.localeCompare(b)
31
+ }
32
+
33
+ // if a and b are both arrays, compare them element by element
34
+ if (Array.isArray(a) && Array.isArray(b)) {
35
+ for (let i = 0; i < Math.min(a.length, b.length); i++) {
36
+ const result = ascComparator(a[i], b[i])
37
+ if (result !== 0) {
38
+ return result
39
+ }
40
+ }
41
+ // All elements are equal up to the minimum length
42
+ return a.length - b.length
43
+ }
44
+
45
+ // If both are dates, compare them
46
+ if (a instanceof Date && b instanceof Date) {
47
+ return a.getTime() - b.getTime()
48
+ }
49
+
50
+ // If at least one of the values is an object, use stable IDs for comparison
51
+ const aIsObject = typeof a === `object`
52
+ const bIsObject = typeof b === `object`
53
+
54
+ if (aIsObject || bIsObject) {
55
+ // If both are objects, compare their stable IDs
56
+ if (aIsObject && bIsObject) {
57
+ const aId = getObjectId(a)
58
+ const bId = getObjectId(b)
59
+ return aId - bId
60
+ }
61
+
62
+ // If only one is an object, objects come after primitives
63
+ if (aIsObject) return 1
64
+ if (bIsObject) return -1
65
+ }
66
+
67
+ // For primitive values, use direct comparison
68
+ if (a < b) return -1
69
+ if (a > b) return 1
70
+ return 0
71
+ }
72
+
73
+ /**
74
+ * Descending comparator function for ordering values
75
+ * Handles null/undefined as largest values (opposite of ascending)
76
+ */
77
+ export const descComparator = (a: unknown, b: unknown): number => {
78
+ return ascComparator(b, a)
79
+ }
@@ -0,0 +1,546 @@
1
+ /**
2
+ * # Index-Based Query Optimization
3
+ *
4
+ * This module provides utilities for optimizing query expressions by leveraging
5
+ * available indexes to quickly find matching keys instead of scanning all data.
6
+ *
7
+ * This is different from the query structure optimizer in `query/optimizer.ts`
8
+ * which rewrites query IR structure. This module focuses on using indexes during
9
+ * query execution to speed up data filtering.
10
+ *
11
+ * ## Key Features:
12
+ * - Uses indexes to find matching keys for WHERE conditions
13
+ * - Supports AND/OR logic with set operations
14
+ * - Handles range queries (eq, gt, gte, lt, lte)
15
+ * - Optimizes IN array expressions
16
+ */
17
+
18
+ import type { BaseIndex, IndexOperation } from "../indexes/base-index.js"
19
+ import type { BasicExpression } from "../query/ir.js"
20
+
21
+ /**
22
+ * Result of index-based query optimization
23
+ */
24
+ export interface OptimizationResult<TKey> {
25
+ canOptimize: boolean
26
+ matchingKeys: Set<TKey>
27
+ }
28
+
29
+ /**
30
+ * Finds an index that matches a given field path
31
+ */
32
+ export function findIndexForField<TKey extends string | number>(
33
+ indexes: Map<number, BaseIndex<TKey>>,
34
+ fieldPath: Array<string>
35
+ ): BaseIndex<TKey> | undefined {
36
+ for (const index of indexes.values()) {
37
+ if (index.matchesField(fieldPath)) {
38
+ return index
39
+ }
40
+ }
41
+ return undefined
42
+ }
43
+
44
+ /**
45
+ * Intersects multiple sets (AND logic)
46
+ */
47
+ export function intersectSets<T>(sets: Array<Set<T>>): Set<T> {
48
+ if (sets.length === 0) return new Set()
49
+ if (sets.length === 1) return new Set(sets[0])
50
+
51
+ let result = new Set(sets[0])
52
+ for (let i = 1; i < sets.length; i++) {
53
+ const newResult = new Set<T>()
54
+ for (const item of result) {
55
+ if (sets[i]!.has(item)) {
56
+ newResult.add(item)
57
+ }
58
+ }
59
+ result = newResult
60
+ }
61
+ return result
62
+ }
63
+
64
+ /**
65
+ * Unions multiple sets (OR logic)
66
+ */
67
+ export function unionSets<T>(sets: Array<Set<T>>): Set<T> {
68
+ const result = new Set<T>()
69
+ for (const set of sets) {
70
+ for (const item of set) {
71
+ result.add(item)
72
+ }
73
+ }
74
+ return result
75
+ }
76
+
77
+ /**
78
+ * Optimizes a query expression using available indexes to find matching keys
79
+ */
80
+ export function optimizeExpressionWithIndexes<TKey extends string | number>(
81
+ expression: BasicExpression,
82
+ indexes: Map<number, BaseIndex<TKey>>
83
+ ): OptimizationResult<TKey> {
84
+ return optimizeQueryRecursive(expression, indexes)
85
+ }
86
+
87
+ /**
88
+ * Recursively optimizes query expressions
89
+ */
90
+ function optimizeQueryRecursive<TKey extends string | number>(
91
+ expression: BasicExpression,
92
+ indexes: Map<number, BaseIndex<TKey>>
93
+ ): OptimizationResult<TKey> {
94
+ if (expression.type === `func`) {
95
+ switch (expression.name) {
96
+ case `eq`:
97
+ case `gt`:
98
+ case `gte`:
99
+ case `lt`:
100
+ case `lte`:
101
+ return optimizeSimpleComparison(expression, indexes)
102
+
103
+ case `and`:
104
+ return optimizeAndExpression(expression, indexes)
105
+
106
+ case `or`:
107
+ return optimizeOrExpression(expression, indexes)
108
+
109
+ case `in`:
110
+ return optimizeInArrayExpression(expression, indexes)
111
+ }
112
+ }
113
+
114
+ return { canOptimize: false, matchingKeys: new Set() }
115
+ }
116
+
117
+ /**
118
+ * Checks if an expression can be optimized
119
+ */
120
+ export function canOptimizeExpression<TKey extends string | number>(
121
+ expression: BasicExpression,
122
+ indexes: Map<number, BaseIndex<TKey>>
123
+ ): boolean {
124
+ if (expression.type === `func`) {
125
+ switch (expression.name) {
126
+ case `eq`:
127
+ case `gt`:
128
+ case `gte`:
129
+ case `lt`:
130
+ case `lte`:
131
+ return canOptimizeSimpleComparison(expression, indexes)
132
+
133
+ case `and`:
134
+ return canOptimizeAndExpression(expression, indexes)
135
+
136
+ case `or`:
137
+ return canOptimizeOrExpression(expression, indexes)
138
+
139
+ case `in`:
140
+ return canOptimizeInArrayExpression(expression, indexes)
141
+ }
142
+ }
143
+
144
+ return false
145
+ }
146
+
147
+ /**
148
+ * Optimizes compound range queries on the same field
149
+ * Example: WHERE age > 5 AND age < 10
150
+ */
151
+ function optimizeCompoundRangeQuery<TKey extends string | number>(
152
+ expression: BasicExpression,
153
+ indexes: Map<number, BaseIndex<TKey>>
154
+ ): OptimizationResult<TKey> {
155
+ if (expression.type !== `func` || expression.args.length < 2) {
156
+ return { canOptimize: false, matchingKeys: new Set() }
157
+ }
158
+
159
+ // Group range operations by field
160
+ const fieldOperations = new Map<
161
+ string,
162
+ Array<{
163
+ operation: `gt` | `gte` | `lt` | `lte`
164
+ value: any
165
+ }>
166
+ >()
167
+
168
+ // Collect all range operations from AND arguments
169
+ for (const arg of expression.args) {
170
+ if (arg.type === `func` && [`gt`, `gte`, `lt`, `lte`].includes(arg.name)) {
171
+ const rangeOp = arg as any
172
+ if (rangeOp.args.length === 2) {
173
+ const leftArg = rangeOp.args[0]!
174
+ const rightArg = rangeOp.args[1]!
175
+
176
+ // Check both directions: field op value AND value op field
177
+ let fieldArg: BasicExpression | null = null
178
+ let valueArg: BasicExpression | null = null
179
+ let operation = rangeOp.name as `gt` | `gte` | `lt` | `lte`
180
+
181
+ if (leftArg.type === `ref` && rightArg.type === `val`) {
182
+ // field op value
183
+ fieldArg = leftArg
184
+ valueArg = rightArg
185
+ } else if (leftArg.type === `val` && rightArg.type === `ref`) {
186
+ // value op field - need to flip the operation
187
+ fieldArg = rightArg
188
+ valueArg = leftArg
189
+
190
+ // Flip the operation for reverse comparison
191
+ switch (operation) {
192
+ case `gt`:
193
+ operation = `lt`
194
+ break
195
+ case `gte`:
196
+ operation = `lte`
197
+ break
198
+ case `lt`:
199
+ operation = `gt`
200
+ break
201
+ case `lte`:
202
+ operation = `gte`
203
+ break
204
+ }
205
+ }
206
+
207
+ if (fieldArg && valueArg) {
208
+ const fieldPath = (fieldArg as any).path
209
+ const fieldKey = fieldPath.join(`.`)
210
+ const value = (valueArg as any).value
211
+
212
+ if (!fieldOperations.has(fieldKey)) {
213
+ fieldOperations.set(fieldKey, [])
214
+ }
215
+ fieldOperations.get(fieldKey)!.push({ operation, value })
216
+ }
217
+ }
218
+ }
219
+ }
220
+
221
+ // Check if we have multiple operations on the same field
222
+ for (const [fieldKey, operations] of fieldOperations) {
223
+ if (operations.length >= 2) {
224
+ const fieldPath = fieldKey.split(`.`)
225
+ const index = findIndexForField(indexes, fieldPath)
226
+
227
+ if (index && index.supports(`gt`) && index.supports(`lt`)) {
228
+ // Build range query options
229
+ let from: any = undefined
230
+ let to: any = undefined
231
+ let fromInclusive = true
232
+ let toInclusive = true
233
+
234
+ for (const { operation, value } of operations) {
235
+ switch (operation) {
236
+ case `gt`:
237
+ if (from === undefined || value > from) {
238
+ from = value
239
+ fromInclusive = false
240
+ }
241
+ break
242
+ case `gte`:
243
+ if (from === undefined || value > from) {
244
+ from = value
245
+ fromInclusive = true
246
+ }
247
+ break
248
+ case `lt`:
249
+ if (to === undefined || value < to) {
250
+ to = value
251
+ toInclusive = false
252
+ }
253
+ break
254
+ case `lte`:
255
+ if (to === undefined || value < to) {
256
+ to = value
257
+ toInclusive = true
258
+ }
259
+ break
260
+ }
261
+ }
262
+
263
+ const matchingKeys = (index as any).rangeQuery({
264
+ from,
265
+ to,
266
+ fromInclusive,
267
+ toInclusive,
268
+ })
269
+
270
+ return { canOptimize: true, matchingKeys }
271
+ }
272
+ }
273
+ }
274
+
275
+ return { canOptimize: false, matchingKeys: new Set() }
276
+ }
277
+
278
+ /**
279
+ * Optimizes simple comparison expressions (eq, gt, gte, lt, lte)
280
+ */
281
+ function optimizeSimpleComparison<TKey extends string | number>(
282
+ expression: BasicExpression,
283
+ indexes: Map<number, BaseIndex<TKey>>
284
+ ): OptimizationResult<TKey> {
285
+ if (expression.type !== `func` || expression.args.length !== 2) {
286
+ return { canOptimize: false, matchingKeys: new Set() }
287
+ }
288
+
289
+ const leftArg = expression.args[0]!
290
+ const rightArg = expression.args[1]!
291
+
292
+ // Check both directions: field op value AND value op field
293
+ let fieldArg: BasicExpression | null = null
294
+ let valueArg: BasicExpression | null = null
295
+ let operation = expression.name as `eq` | `gt` | `gte` | `lt` | `lte`
296
+
297
+ if (leftArg.type === `ref` && rightArg.type === `val`) {
298
+ // field op value
299
+ fieldArg = leftArg
300
+ valueArg = rightArg
301
+ } else if (leftArg.type === `val` && rightArg.type === `ref`) {
302
+ // value op field - need to flip the operation
303
+ fieldArg = rightArg
304
+ valueArg = leftArg
305
+
306
+ // Flip the operation for reverse comparison
307
+ switch (operation) {
308
+ case `gt`:
309
+ operation = `lt`
310
+ break
311
+ case `gte`:
312
+ operation = `lte`
313
+ break
314
+ case `lt`:
315
+ operation = `gt`
316
+ break
317
+ case `lte`:
318
+ operation = `gte`
319
+ break
320
+ // eq stays the same
321
+ }
322
+ }
323
+
324
+ if (fieldArg && valueArg) {
325
+ const fieldPath = (fieldArg as any).path
326
+ const index = findIndexForField(indexes, fieldPath)
327
+
328
+ if (index) {
329
+ const queryValue = (valueArg as any).value
330
+
331
+ // Map operation to IndexOperation enum
332
+ const indexOperation = operation as IndexOperation
333
+
334
+ // Check if the index supports this operation
335
+ if (!index.supports(indexOperation)) {
336
+ return { canOptimize: false, matchingKeys: new Set() }
337
+ }
338
+
339
+ const matchingKeys = index.lookup(indexOperation, queryValue)
340
+ return { canOptimize: true, matchingKeys }
341
+ }
342
+ }
343
+
344
+ return { canOptimize: false, matchingKeys: new Set() }
345
+ }
346
+
347
+ /**
348
+ * Checks if a simple comparison can be optimized
349
+ */
350
+ function canOptimizeSimpleComparison<TKey extends string | number>(
351
+ expression: BasicExpression,
352
+ indexes: Map<number, BaseIndex<TKey>>
353
+ ): boolean {
354
+ if (expression.type !== `func` || expression.args.length !== 2) {
355
+ return false
356
+ }
357
+
358
+ const leftArg = expression.args[0]!
359
+ const rightArg = expression.args[1]!
360
+
361
+ // Check both directions: field op value AND value op field
362
+ let fieldPath: Array<string> | null = null
363
+
364
+ if (leftArg.type === `ref` && rightArg.type === `val`) {
365
+ fieldPath = (leftArg as any).path
366
+ } else if (leftArg.type === `val` && rightArg.type === `ref`) {
367
+ fieldPath = (rightArg as any).path
368
+ }
369
+
370
+ if (fieldPath) {
371
+ const index = findIndexForField(indexes, fieldPath)
372
+ return index !== undefined
373
+ }
374
+
375
+ return false
376
+ }
377
+
378
+ /**
379
+ * Optimizes AND expressions
380
+ */
381
+ function optimizeAndExpression<TKey extends string | number>(
382
+ expression: BasicExpression,
383
+ indexes: Map<number, BaseIndex<TKey>>
384
+ ): OptimizationResult<TKey> {
385
+ if (expression.type !== `func` || expression.args.length < 2) {
386
+ return { canOptimize: false, matchingKeys: new Set() }
387
+ }
388
+
389
+ // First, try to optimize compound range queries on the same field
390
+ const compoundRangeResult = optimizeCompoundRangeQuery(expression, indexes)
391
+ if (compoundRangeResult.canOptimize) {
392
+ return compoundRangeResult
393
+ }
394
+
395
+ const results: Array<OptimizationResult<TKey>> = []
396
+
397
+ // Try to optimize each part, keep the optimizable ones
398
+ for (const arg of expression.args) {
399
+ const result = optimizeQueryRecursive(arg, indexes)
400
+ if (result.canOptimize) {
401
+ results.push(result)
402
+ }
403
+ }
404
+
405
+ if (results.length > 0) {
406
+ // Use intersectSets utility for AND logic
407
+ const allMatchingSets = results.map((r) => r.matchingKeys)
408
+ const intersectedKeys = intersectSets(allMatchingSets)
409
+ return { canOptimize: true, matchingKeys: intersectedKeys }
410
+ }
411
+
412
+ return { canOptimize: false, matchingKeys: new Set() }
413
+ }
414
+
415
+ /**
416
+ * Checks if an AND expression can be optimized
417
+ */
418
+ function canOptimizeAndExpression<TKey extends string | number>(
419
+ expression: BasicExpression,
420
+ indexes: Map<number, BaseIndex<TKey>>
421
+ ): boolean {
422
+ if (expression.type !== `func` || expression.args.length < 2) {
423
+ return false
424
+ }
425
+
426
+ // If any argument can be optimized, we can gain some speedup
427
+ return expression.args.some((arg) => canOptimizeExpression(arg, indexes))
428
+ }
429
+
430
+ /**
431
+ * Optimizes OR expressions
432
+ */
433
+ function optimizeOrExpression<TKey extends string | number>(
434
+ expression: BasicExpression,
435
+ indexes: Map<number, BaseIndex<TKey>>
436
+ ): OptimizationResult<TKey> {
437
+ if (expression.type !== `func` || expression.args.length < 2) {
438
+ return { canOptimize: false, matchingKeys: new Set() }
439
+ }
440
+
441
+ const results: Array<OptimizationResult<TKey>> = []
442
+
443
+ // Try to optimize each part, keep the optimizable ones
444
+ for (const arg of expression.args) {
445
+ const result = optimizeQueryRecursive(arg, indexes)
446
+ if (result.canOptimize) {
447
+ results.push(result)
448
+ }
449
+ }
450
+
451
+ if (results.length > 0) {
452
+ // Use unionSets utility for OR logic
453
+ const allMatchingSets = results.map((r) => r.matchingKeys)
454
+ const unionedKeys = unionSets(allMatchingSets)
455
+ return { canOptimize: true, matchingKeys: unionedKeys }
456
+ }
457
+
458
+ return { canOptimize: false, matchingKeys: new Set() }
459
+ }
460
+
461
+ /**
462
+ * Checks if an OR expression can be optimized
463
+ */
464
+ function canOptimizeOrExpression<TKey extends string | number>(
465
+ expression: BasicExpression,
466
+ indexes: Map<number, BaseIndex<TKey>>
467
+ ): boolean {
468
+ if (expression.type !== `func` || expression.args.length < 2) {
469
+ return false
470
+ }
471
+
472
+ // If any argument can be optimized, we can gain some speedup
473
+ return expression.args.some((arg) => canOptimizeExpression(arg, indexes))
474
+ }
475
+
476
+ /**
477
+ * Optimizes IN array expressions
478
+ */
479
+ function optimizeInArrayExpression<TKey extends string | number>(
480
+ expression: BasicExpression,
481
+ indexes: Map<number, BaseIndex<TKey>>
482
+ ): OptimizationResult<TKey> {
483
+ if (expression.type !== `func` || expression.args.length !== 2) {
484
+ return { canOptimize: false, matchingKeys: new Set() }
485
+ }
486
+
487
+ const fieldArg = expression.args[0]!
488
+ const arrayArg = expression.args[1]!
489
+
490
+ if (
491
+ fieldArg.type === `ref` &&
492
+ arrayArg.type === `val` &&
493
+ Array.isArray((arrayArg as any).value)
494
+ ) {
495
+ const fieldPath = (fieldArg as any).path
496
+ const values = (arrayArg as any).value
497
+ const index = findIndexForField(indexes, fieldPath)
498
+
499
+ if (index) {
500
+ // Check if the index supports IN operation
501
+ if (index.supports(`in`)) {
502
+ const matchingKeys = index.lookup(`in`, values)
503
+ return { canOptimize: true, matchingKeys }
504
+ } else if (index.supports(`eq`)) {
505
+ // Fallback to multiple equality lookups
506
+ const matchingKeys = new Set<TKey>()
507
+ for (const value of values) {
508
+ const keysForValue = index.lookup(`eq`, value)
509
+ for (const key of keysForValue) {
510
+ matchingKeys.add(key)
511
+ }
512
+ }
513
+ return { canOptimize: true, matchingKeys }
514
+ }
515
+ }
516
+ }
517
+
518
+ return { canOptimize: false, matchingKeys: new Set() }
519
+ }
520
+
521
+ /**
522
+ * Checks if an IN array expression can be optimized
523
+ */
524
+ function canOptimizeInArrayExpression<TKey extends string | number>(
525
+ expression: BasicExpression,
526
+ indexes: Map<number, BaseIndex<TKey>>
527
+ ): boolean {
528
+ if (expression.type !== `func` || expression.args.length !== 2) {
529
+ return false
530
+ }
531
+
532
+ const fieldArg = expression.args[0]!
533
+ const arrayArg = expression.args[1]!
534
+
535
+ if (
536
+ fieldArg.type === `ref` &&
537
+ arrayArg.type === `val` &&
538
+ Array.isArray((arrayArg as any).value)
539
+ ) {
540
+ const fieldPath = (fieldArg as any).path
541
+ const index = findIndexForField(indexes, fieldPath)
542
+ return index !== undefined
543
+ }
544
+
545
+ return false
546
+ }