@tanstack/db 0.4.20 → 0.5.1

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 (132) 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/strategies/queueStrategy.cjs +19 -16
  47. package/dist/cjs/strategies/queueStrategy.cjs.map +1 -1
  48. package/dist/cjs/types.d.cts +29 -0
  49. package/dist/cjs/utils/comparison.cjs +30 -0
  50. package/dist/cjs/utils/comparison.cjs.map +1 -1
  51. package/dist/cjs/utils/comparison.d.cts +7 -1
  52. package/dist/cjs/utils/index-optimization.cjs +26 -22
  53. package/dist/cjs/utils/index-optimization.cjs.map +1 -1
  54. package/dist/cjs/utils/index-optimization.d.cts +5 -4
  55. package/dist/esm/collection/change-events.d.ts +1 -8
  56. package/dist/esm/collection/change-events.js +7 -9
  57. package/dist/esm/collection/change-events.js.map +1 -1
  58. package/dist/esm/collection/index.d.ts +7 -5
  59. package/dist/esm/collection/index.js +18 -0
  60. package/dist/esm/collection/index.js.map +1 -1
  61. package/dist/esm/index.d.ts +2 -0
  62. package/dist/esm/index.js +19 -1
  63. package/dist/esm/index.js.map +1 -1
  64. package/dist/esm/indexes/auto-index.js +7 -3
  65. package/dist/esm/indexes/auto-index.js.map +1 -1
  66. package/dist/esm/local-storage.d.ts +2 -2
  67. package/dist/esm/local-storage.js.map +1 -1
  68. package/dist/esm/query/builder/functions.d.ts +5 -0
  69. package/dist/esm/query/builder/functions.js +34 -0
  70. package/dist/esm/query/builder/functions.js.map +1 -1
  71. package/dist/esm/query/builder/index.js +2 -2
  72. package/dist/esm/query/builder/index.js.map +1 -1
  73. package/dist/esm/query/builder/types.d.ts +3 -22
  74. package/dist/esm/query/compiler/evaluators.d.ts +13 -0
  75. package/dist/esm/query/compiler/evaluators.js +59 -6
  76. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  77. package/dist/esm/query/compiler/expressions.js +4 -1
  78. package/dist/esm/query/compiler/expressions.js.map +1 -1
  79. package/dist/esm/query/compiler/group-by.js +4 -4
  80. package/dist/esm/query/compiler/group-by.js.map +1 -1
  81. package/dist/esm/query/compiler/index.js +3 -3
  82. package/dist/esm/query/compiler/index.js.map +1 -1
  83. package/dist/esm/query/compiler/order-by.d.ts +7 -1
  84. package/dist/esm/query/compiler/order-by.js +18 -6
  85. package/dist/esm/query/compiler/order-by.js.map +1 -1
  86. package/dist/esm/query/expression-helpers.d.ts +216 -0
  87. package/dist/esm/query/expression-helpers.js +217 -0
  88. package/dist/esm/query/expression-helpers.js.map +1 -0
  89. package/dist/esm/query/index.d.ts +2 -0
  90. package/dist/esm/query/live/collection-config-builder.d.ts +1 -0
  91. package/dist/esm/query/live/collection-config-builder.js +13 -0
  92. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  93. package/dist/esm/query/live/types.d.ts +6 -1
  94. package/dist/esm/query/predicate-utils.d.ts +116 -0
  95. package/dist/esm/query/predicate-utils.js +816 -0
  96. package/dist/esm/query/predicate-utils.js.map +1 -0
  97. package/dist/esm/query/subset-dedupe.d.ts +66 -0
  98. package/dist/esm/query/subset-dedupe.js +111 -0
  99. package/dist/esm/query/subset-dedupe.js.map +1 -0
  100. package/dist/esm/strategies/queueStrategy.js +19 -16
  101. package/dist/esm/strategies/queueStrategy.js.map +1 -1
  102. package/dist/esm/types.d.ts +29 -0
  103. package/dist/esm/utils/comparison.d.ts +7 -1
  104. package/dist/esm/utils/comparison.js +30 -0
  105. package/dist/esm/utils/comparison.js.map +1 -1
  106. package/dist/esm/utils/index-optimization.d.ts +5 -4
  107. package/dist/esm/utils/index-optimization.js +26 -22
  108. package/dist/esm/utils/index-optimization.js.map +1 -1
  109. package/package.json +4 -4
  110. package/src/collection/change-events.ts +14 -24
  111. package/src/collection/index.ts +32 -4
  112. package/src/index.ts +4 -0
  113. package/src/indexes/auto-index.ts +8 -4
  114. package/src/local-storage.ts +11 -3
  115. package/src/query/builder/functions.ts +39 -0
  116. package/src/query/builder/index.ts +2 -2
  117. package/src/query/builder/types.ts +3 -25
  118. package/src/query/compiler/evaluators.ts +103 -5
  119. package/src/query/compiler/expressions.ts +3 -0
  120. package/src/query/compiler/group-by.ts +4 -4
  121. package/src/query/compiler/index.ts +3 -3
  122. package/src/query/compiler/order-by.ts +33 -7
  123. package/src/query/expression-helpers.ts +522 -0
  124. package/src/query/index.ts +12 -0
  125. package/src/query/live/collection-config-builder.ts +27 -0
  126. package/src/query/live/types.ts +11 -1
  127. package/src/query/predicate-utils.ts +1415 -0
  128. package/src/query/subset-dedupe.ts +243 -0
  129. package/src/strategies/queueStrategy.ts +18 -15
  130. package/src/types.ts +39 -0
  131. package/src/utils/comparison.ts +70 -1
  132. package/src/utils/index-optimization.ts +77 -63
@@ -0,0 +1,1415 @@
1
+ import { Func, Value } from "./ir.js"
2
+ import type { BasicExpression, OrderBy, PropRef } from "./ir.js"
3
+ import type { LoadSubsetOptions } from "../types.js"
4
+
5
+ /**
6
+ * Check if one where clause is a logical subset of another.
7
+ * Returns true if the subset predicate is more restrictive than (or equal to) the superset predicate.
8
+ *
9
+ * @example
10
+ * // age > 20 is subset of age > 10 (more restrictive)
11
+ * isWhereSubset(gt(ref('age'), val(20)), gt(ref('age'), val(10))) // true
12
+ *
13
+ * @example
14
+ * // age > 10 AND name = 'X' is subset of age > 10 (more conditions)
15
+ * isWhereSubset(and(gt(ref('age'), val(10)), eq(ref('name'), val('X'))), gt(ref('age'), val(10))) // true
16
+ *
17
+ * @param subset - The potentially more restrictive predicate
18
+ * @param superset - The potentially less restrictive predicate
19
+ * @returns true if subset logically implies superset
20
+ */
21
+ export function isWhereSubset(
22
+ subset: BasicExpression<boolean> | undefined,
23
+ superset: BasicExpression<boolean> | undefined
24
+ ): boolean {
25
+ // undefined/missing where clause means "no filter" (all data)
26
+ // Both undefined means subset relationship holds (all data ⊆ all data)
27
+ if (subset === undefined && superset === undefined) {
28
+ return true
29
+ }
30
+
31
+ // If subset is undefined but superset is not, we're requesting ALL data
32
+ // but have only loaded SOME data - subset relationship does NOT hold
33
+ if (subset === undefined && superset !== undefined) {
34
+ return false
35
+ }
36
+
37
+ // If superset is undefined (no filter = all data loaded),
38
+ // then any constrained subset is contained
39
+ if (superset === undefined && subset !== undefined) {
40
+ return true
41
+ }
42
+
43
+ return isWhereSubsetInternal(subset!, superset!)
44
+ }
45
+
46
+ function makeDisjunction(
47
+ preds: Array<BasicExpression<boolean>>
48
+ ): BasicExpression<boolean> {
49
+ if (preds.length === 0) {
50
+ return new Value(false)
51
+ }
52
+ if (preds.length === 1) {
53
+ return preds[0]!
54
+ }
55
+ return new Func(`or`, preds)
56
+ }
57
+
58
+ function convertInToOr(inField: InField) {
59
+ const equalities = inField.values.map(
60
+ (value) => new Func(`eq`, [inField.ref, new Value(value)])
61
+ )
62
+ return makeDisjunction(equalities)
63
+ }
64
+
65
+ function isWhereSubsetInternal(
66
+ subset: BasicExpression<boolean>,
67
+ superset: BasicExpression<boolean>
68
+ ): boolean {
69
+ // If subset is false it is requesting no data,
70
+ // thus the result set is empty
71
+ // and the empty set is a subset of any set
72
+ if (subset.type === `val` && subset.value === false) {
73
+ return true
74
+ }
75
+
76
+ // If expressions are structurally equal, subset relationship holds
77
+ if (areExpressionsEqual(subset, superset)) {
78
+ return true
79
+ }
80
+
81
+ // Handle superset being an AND: subset must imply ALL conjuncts
82
+ // If superset is (A AND B), then subset ⊆ (A AND B) only if subset ⊆ A AND subset ⊆ B
83
+ // Example: (age > 20) ⊆ (age > 10 AND status = 'active') is false (doesn't imply status condition)
84
+ if (superset.type === `func` && superset.name === `and`) {
85
+ return superset.args.every((arg) =>
86
+ isWhereSubsetInternal(subset, arg as BasicExpression<boolean>)
87
+ )
88
+ }
89
+
90
+ // Handle subset being an AND: (A AND B) implies both A and B
91
+ if (subset.type === `func` && subset.name === `and`) {
92
+ // For (A AND B) ⊆ C, since (A AND B) implies A, we check if any conjunct implies C
93
+ return subset.args.some((arg) =>
94
+ isWhereSubsetInternal(arg as BasicExpression<boolean>, superset)
95
+ )
96
+ }
97
+
98
+ // Turn x IN [A, B, C] into x = A OR x = B OR x = C
99
+ // for unified handling of IN and OR
100
+ if (subset.type === `func` && subset.name === `in`) {
101
+ const inField = extractInField(subset)
102
+ if (inField) {
103
+ return isWhereSubsetInternal(convertInToOr(inField), superset)
104
+ }
105
+ }
106
+
107
+ if (superset.type === `func` && superset.name === `in`) {
108
+ const inField = extractInField(superset)
109
+ if (inField) {
110
+ return isWhereSubsetInternal(subset, convertInToOr(inField))
111
+ }
112
+ }
113
+
114
+ // Handle OR in subset: (A OR B) is subset of C only if both A and B are subsets of C
115
+ if (subset.type === `func` && subset.name === `or`) {
116
+ return subset.args.every((arg) =>
117
+ isWhereSubsetInternal(arg as BasicExpression<boolean>, superset)
118
+ )
119
+ }
120
+
121
+ // Handle OR in superset: subset ⊆ (A OR B) if subset ⊆ A or subset ⊆ B
122
+ // (A OR B) as superset means data can satisfy A or B
123
+ // If subset is contained in any disjunct, it's contained in the union
124
+ if (superset.type === `func` && superset.name === `or`) {
125
+ return superset.args.some((arg) =>
126
+ isWhereSubsetInternal(subset, arg as BasicExpression<boolean>)
127
+ )
128
+ }
129
+
130
+ // Handle comparison operators on the same field
131
+ if (subset.type === `func` && superset.type === `func`) {
132
+ const subsetFunc = subset as Func
133
+ const supersetFunc = superset as Func
134
+
135
+ // Check if both are comparisons on the same field
136
+ const subsetField = extractComparisonField(subsetFunc)
137
+ const supersetField = extractComparisonField(supersetFunc)
138
+
139
+ if (
140
+ subsetField &&
141
+ supersetField &&
142
+ areRefsEqual(subsetField.ref, supersetField.ref)
143
+ ) {
144
+ return isComparisonSubset(
145
+ subsetFunc,
146
+ subsetField.value,
147
+ supersetFunc,
148
+ supersetField.value
149
+ )
150
+ }
151
+
152
+ /*
153
+ // Handle eq vs in
154
+ if (subsetFunc.name === `eq` && supersetFunc.name === `in`) {
155
+ const subsetFieldEq = extractEqualityField(subsetFunc)
156
+ const supersetFieldIn = extractInField(supersetFunc)
157
+ if (
158
+ subsetFieldEq &&
159
+ supersetFieldIn &&
160
+ areRefsEqual(subsetFieldEq.ref, supersetFieldIn.ref)
161
+ ) {
162
+ // field = X is subset of field IN [X, Y, Z] if X is in the array
163
+ // Use cached primitive set and metadata from extraction
164
+ return arrayIncludesWithSet(
165
+ supersetFieldIn.values,
166
+ subsetFieldEq.value,
167
+ supersetFieldIn.primitiveSet ?? null,
168
+ supersetFieldIn.areAllPrimitives
169
+ )
170
+ }
171
+ }
172
+
173
+ // Handle in vs in
174
+ if (subsetFunc.name === `in` && supersetFunc.name === `in`) {
175
+ const subsetFieldIn = extractInField(subsetFunc)
176
+ const supersetFieldIn = extractInField(supersetFunc)
177
+ if (
178
+ subsetFieldIn &&
179
+ supersetFieldIn &&
180
+ areRefsEqual(subsetFieldIn.ref, supersetFieldIn.ref)
181
+ ) {
182
+ // field IN [A, B] is subset of field IN [A, B, C] if all values in subset are in superset
183
+ // Use cached primitive set and metadata from extraction
184
+ return subsetFieldIn.values.every((subVal) =>
185
+ arrayIncludesWithSet(
186
+ supersetFieldIn.values,
187
+ subVal,
188
+ supersetFieldIn.primitiveSet ?? null,
189
+ supersetFieldIn.areAllPrimitives
190
+ )
191
+ )
192
+ }
193
+ }
194
+ */
195
+ }
196
+
197
+ // Conservative: if we can't determine, return false
198
+ return false
199
+ }
200
+
201
+ /**
202
+ * Helper to combine where predicates with common logic for AND/OR operations
203
+ */
204
+ function combineWherePredicates(
205
+ predicates: Array<BasicExpression<boolean>>,
206
+ operation: `and` | `or`,
207
+ simplifyFn: (
208
+ preds: Array<BasicExpression<boolean>>
209
+ ) => BasicExpression<boolean> | null
210
+ ): BasicExpression<boolean> {
211
+ const emptyValue = operation === `and` ? true : false
212
+ const identityValue = operation === `and` ? true : false
213
+
214
+ if (predicates.length === 0) {
215
+ return { type: `val`, value: emptyValue } as BasicExpression<boolean>
216
+ }
217
+
218
+ if (predicates.length === 1) {
219
+ return predicates[0]!
220
+ }
221
+
222
+ // Flatten nested expressions of the same operation
223
+ const flatPredicates: Array<BasicExpression<boolean>> = []
224
+ for (const pred of predicates) {
225
+ if (pred.type === `func` && pred.name === operation) {
226
+ flatPredicates.push(...pred.args)
227
+ } else {
228
+ flatPredicates.push(pred)
229
+ }
230
+ }
231
+
232
+ // Group predicates by field for simplification
233
+ const grouped = groupPredicatesByField(flatPredicates)
234
+
235
+ // Simplify each group
236
+ const simplified: Array<BasicExpression<boolean>> = []
237
+ for (const [field, preds] of grouped.entries()) {
238
+ if (field === null) {
239
+ // Complex predicates that we can't group by field
240
+ simplified.push(...preds)
241
+ } else {
242
+ // Try to simplify same-field predicates
243
+ const result = simplifyFn(preds)
244
+
245
+ // For intersection: check for empty set (contradiction)
246
+ if (
247
+ operation === `and` &&
248
+ result &&
249
+ result.type === `val` &&
250
+ result.value === false
251
+ ) {
252
+ // Intersection is empty (conflicting constraints) - entire AND is false
253
+ return { type: `val`, value: false } as BasicExpression<boolean>
254
+ }
255
+
256
+ // For union: result may be null if simplification failed
257
+ if (result) {
258
+ simplified.push(result)
259
+ }
260
+ }
261
+ }
262
+
263
+ if (simplified.length === 0) {
264
+ return { type: `val`, value: identityValue } as BasicExpression<boolean>
265
+ }
266
+
267
+ if (simplified.length === 1) {
268
+ return simplified[0]!
269
+ }
270
+
271
+ // Return combined predicate
272
+ return {
273
+ type: `func`,
274
+ name: operation,
275
+ args: simplified,
276
+ } as BasicExpression<boolean>
277
+ }
278
+
279
+ /**
280
+ * Combine multiple where predicates with OR logic (union).
281
+ * Returns a predicate that is satisfied when any input predicate is satisfied.
282
+ * Simplifies when possible (e.g., age > 10 OR age > 20 → age > 10).
283
+ *
284
+ * @example
285
+ * // Take least restrictive
286
+ * unionWherePredicates([gt(ref('age'), val(10)), gt(ref('age'), val(20))]) // age > 10
287
+ *
288
+ * @example
289
+ * // Combine equals into IN
290
+ * unionWherePredicates([eq(ref('age'), val(5)), eq(ref('age'), val(10))]) // age IN [5, 10]
291
+ *
292
+ * @param predicates - Array of where predicates to union
293
+ * @returns Combined predicate representing the union
294
+ */
295
+ export function unionWherePredicates(
296
+ predicates: Array<BasicExpression<boolean>>
297
+ ): BasicExpression<boolean> {
298
+ return combineWherePredicates(predicates, `or`, unionSameFieldPredicates)
299
+ }
300
+
301
+ /**
302
+ * Compute the difference between two where predicates: `fromPredicate AND NOT(subtractPredicate)`.
303
+ * Returns the simplified predicate, or null if the difference cannot be simplified
304
+ * (in which case the caller should fetch the full fromPredicate).
305
+ *
306
+ * @example
307
+ * // Range difference
308
+ * minusWherePredicates(
309
+ * gt(ref('age'), val(10)), // age > 10
310
+ * gt(ref('age'), val(20)) // age > 20
311
+ * ) // → age > 10 AND age <= 20
312
+ *
313
+ * @example
314
+ * // Set difference
315
+ * minusWherePredicates(
316
+ * inOp(ref('status'), ['A', 'B', 'C', 'D']), // status IN ['A','B','C','D']
317
+ * inOp(ref('status'), ['B', 'C']) // status IN ['B','C']
318
+ * ) // → status IN ['A', 'D']
319
+ *
320
+ * @example
321
+ * // Common conditions
322
+ * minusWherePredicates(
323
+ * and(gt(ref('age'), val(10)), eq(ref('status'), val('active'))), // age > 10 AND status = 'active'
324
+ * and(gt(ref('age'), val(20)), eq(ref('status'), val('active'))) // age > 20 AND status = 'active'
325
+ * ) // → age > 10 AND age <= 20 AND status = 'active'
326
+ *
327
+ * @example
328
+ * // Complete overlap - empty result
329
+ * minusWherePredicates(
330
+ * gt(ref('age'), val(20)), // age > 20
331
+ * gt(ref('age'), val(10)) // age > 10
332
+ * ) // → {type: 'val', value: false} (empty set)
333
+ *
334
+ * @param fromPredicate - The predicate to subtract from
335
+ * @param subtractPredicate - The predicate to subtract
336
+ * @returns The simplified difference, or null if cannot be simplified
337
+ */
338
+ export function minusWherePredicates(
339
+ fromPredicate: BasicExpression<boolean> | undefined,
340
+ subtractPredicate: BasicExpression<boolean> | undefined
341
+ ): BasicExpression<boolean> | null {
342
+ // If nothing to subtract, return the original
343
+ if (subtractPredicate === undefined) {
344
+ return (
345
+ fromPredicate ??
346
+ ({ type: `val`, value: true } as BasicExpression<boolean>)
347
+ )
348
+ }
349
+
350
+ // If from is undefined then we are asking for all data
351
+ // so we need to load all data minus what we already loaded
352
+ // i.e. we need to load NOT(subtractPredicate)
353
+ if (fromPredicate === undefined) {
354
+ return {
355
+ type: `func`,
356
+ name: `not`,
357
+ args: [subtractPredicate],
358
+ } as BasicExpression<boolean>
359
+ }
360
+
361
+ // Check if fromPredicate is entirely contained in subtractPredicate
362
+ // In that case, fromPredicate AND NOT(subtractPredicate) = empty set
363
+ if (isWhereSubset(fromPredicate, subtractPredicate)) {
364
+ return { type: `val`, value: false } as BasicExpression<boolean>
365
+ }
366
+
367
+ // Try to detect and handle common conditions
368
+ const commonConditions = findCommonConditions(
369
+ fromPredicate,
370
+ subtractPredicate
371
+ )
372
+ if (commonConditions.length > 0) {
373
+ // Extract predicates without common conditions
374
+ const fromWithoutCommon = removeConditions(fromPredicate, commonConditions)
375
+ const subtractWithoutCommon = removeConditions(
376
+ subtractPredicate,
377
+ commonConditions
378
+ )
379
+
380
+ // Recursively compute difference on simplified predicates
381
+ const simplifiedDifference = minusWherePredicates(
382
+ fromWithoutCommon,
383
+ subtractWithoutCommon
384
+ )
385
+
386
+ if (simplifiedDifference !== null) {
387
+ // Combine the simplified difference with common conditions
388
+ return combineConditions([...commonConditions, simplifiedDifference])
389
+ }
390
+ }
391
+
392
+ // Check if they are on the same field - if so, we can try to simplify
393
+ if (fromPredicate.type === `func` && subtractPredicate.type === `func`) {
394
+ const result = minusSameFieldPredicates(fromPredicate, subtractPredicate)
395
+ if (result !== null) {
396
+ return result
397
+ }
398
+ }
399
+
400
+ // Can't simplify - return null to indicate caller should fetch full fromPredicate
401
+ return null
402
+ }
403
+
404
+ /**
405
+ * Helper function to compute difference for same-field predicates
406
+ */
407
+ function minusSameFieldPredicates(
408
+ fromPred: Func,
409
+ subtractPred: Func
410
+ ): BasicExpression<boolean> | null {
411
+ // Extract field information
412
+ const fromField =
413
+ extractComparisonField(fromPred) ||
414
+ extractEqualityField(fromPred) ||
415
+ extractInField(fromPred)
416
+ const subtractField =
417
+ extractComparisonField(subtractPred) ||
418
+ extractEqualityField(subtractPred) ||
419
+ extractInField(subtractPred)
420
+
421
+ // Must be on the same field
422
+ if (
423
+ !fromField ||
424
+ !subtractField ||
425
+ !areRefsEqual(fromField.ref, subtractField.ref)
426
+ ) {
427
+ return null
428
+ }
429
+
430
+ // Handle IN minus IN: status IN [A,B,C,D] - status IN [B,C] = status IN [A,D]
431
+ if (fromPred.name === `in` && subtractPred.name === `in`) {
432
+ const fromInField = fromField as InField
433
+ const subtractInField = subtractField as InField
434
+
435
+ // Filter out values that are in the subtract set
436
+ const remainingValues = fromInField.values.filter(
437
+ (v) =>
438
+ !arrayIncludesWithSet(
439
+ subtractInField.values,
440
+ v,
441
+ subtractInField.primitiveSet ?? null,
442
+ subtractInField.areAllPrimitives
443
+ )
444
+ )
445
+
446
+ if (remainingValues.length === 0) {
447
+ return { type: `val`, value: false } as BasicExpression<boolean>
448
+ }
449
+
450
+ if (remainingValues.length === 1) {
451
+ return {
452
+ type: `func`,
453
+ name: `eq`,
454
+ args: [fromField.ref, { type: `val`, value: remainingValues[0] }],
455
+ } as BasicExpression<boolean>
456
+ }
457
+
458
+ return {
459
+ type: `func`,
460
+ name: `in`,
461
+ args: [fromField.ref, { type: `val`, value: remainingValues }],
462
+ } as BasicExpression<boolean>
463
+ }
464
+
465
+ // Handle IN minus equality: status IN [A,B,C] - status = B = status IN [A,C]
466
+ if (fromPred.name === `in` && subtractPred.name === `eq`) {
467
+ const fromInField = fromField as InField
468
+ const subtractValue = (subtractField as { ref: PropRef; value: any }).value
469
+
470
+ const remainingValues = fromInField.values.filter(
471
+ (v) => !areValuesEqual(v, subtractValue)
472
+ )
473
+
474
+ if (remainingValues.length === 0) {
475
+ return { type: `val`, value: false } as BasicExpression<boolean>
476
+ }
477
+
478
+ if (remainingValues.length === 1) {
479
+ return {
480
+ type: `func`,
481
+ name: `eq`,
482
+ args: [fromField.ref, { type: `val`, value: remainingValues[0] }],
483
+ } as BasicExpression<boolean>
484
+ }
485
+
486
+ return {
487
+ type: `func`,
488
+ name: `in`,
489
+ args: [fromField.ref, { type: `val`, value: remainingValues }],
490
+ } as BasicExpression<boolean>
491
+ }
492
+
493
+ // Handle equality minus equality: age = 15 - age = 15 = empty, age = 15 - age = 20 = age = 15
494
+ if (fromPred.name === `eq` && subtractPred.name === `eq`) {
495
+ const fromValue = (fromField as { ref: PropRef; value: any }).value
496
+ const subtractValue = (subtractField as { ref: PropRef; value: any }).value
497
+
498
+ if (areValuesEqual(fromValue, subtractValue)) {
499
+ return { type: `val`, value: false } as BasicExpression<boolean>
500
+ }
501
+
502
+ // No overlap - return original
503
+ return fromPred as BasicExpression<boolean>
504
+ }
505
+
506
+ // Handle range minus range: age > 10 - age > 20 = age > 10 AND age <= 20
507
+ const fromComp = extractComparisonField(fromPred)
508
+ const subtractComp = extractComparisonField(subtractPred)
509
+
510
+ if (
511
+ fromComp &&
512
+ subtractComp &&
513
+ areRefsEqual(fromComp.ref, subtractComp.ref)
514
+ ) {
515
+ // Try to compute the difference using range logic
516
+ const result = minusRangePredicates(
517
+ fromPred,
518
+ fromComp.value,
519
+ subtractPred,
520
+ subtractComp.value
521
+ )
522
+ return result
523
+ }
524
+
525
+ // Can't simplify
526
+ return null
527
+ }
528
+
529
+ /**
530
+ * Helper to compute difference between range predicates
531
+ */
532
+ function minusRangePredicates(
533
+ fromFunc: Func,
534
+ fromValue: any,
535
+ subtractFunc: Func,
536
+ subtractValue: any
537
+ ): BasicExpression<boolean> | null {
538
+ const fromOp = fromFunc.name as `gt` | `gte` | `lt` | `lte` | `eq`
539
+ const subtractOp = subtractFunc.name as `gt` | `gte` | `lt` | `lte` | `eq`
540
+ const ref = (extractComparisonField(fromFunc) ||
541
+ extractEqualityField(fromFunc))!.ref
542
+
543
+ // age > 10 - age > 20 = (age > 10 AND age <= 20)
544
+ if (fromOp === `gt` && subtractOp === `gt`) {
545
+ if (fromValue < subtractValue) {
546
+ // Result is: fromValue < field <= subtractValue
547
+ return {
548
+ type: `func`,
549
+ name: `and`,
550
+ args: [
551
+ fromFunc as BasicExpression<boolean>,
552
+ {
553
+ type: `func`,
554
+ name: `lte`,
555
+ args: [ref, { type: `val`, value: subtractValue }],
556
+ } as BasicExpression<boolean>,
557
+ ],
558
+ } as BasicExpression<boolean>
559
+ }
560
+ // fromValue >= subtractValue means no overlap
561
+ return fromFunc as BasicExpression<boolean>
562
+ }
563
+
564
+ // age >= 10 - age >= 20 = (age >= 10 AND age < 20)
565
+ if (fromOp === `gte` && subtractOp === `gte`) {
566
+ if (fromValue < subtractValue) {
567
+ return {
568
+ type: `func`,
569
+ name: `and`,
570
+ args: [
571
+ fromFunc as BasicExpression<boolean>,
572
+ {
573
+ type: `func`,
574
+ name: `lt`,
575
+ args: [ref, { type: `val`, value: subtractValue }],
576
+ } as BasicExpression<boolean>,
577
+ ],
578
+ } as BasicExpression<boolean>
579
+ }
580
+ return fromFunc as BasicExpression<boolean>
581
+ }
582
+
583
+ // age > 10 - age >= 20 = (age > 10 AND age < 20)
584
+ if (fromOp === `gt` && subtractOp === `gte`) {
585
+ if (fromValue < subtractValue) {
586
+ return {
587
+ type: `func`,
588
+ name: `and`,
589
+ args: [
590
+ fromFunc as BasicExpression<boolean>,
591
+ {
592
+ type: `func`,
593
+ name: `lt`,
594
+ args: [ref, { type: `val`, value: subtractValue }],
595
+ } as BasicExpression<boolean>,
596
+ ],
597
+ } as BasicExpression<boolean>
598
+ }
599
+ return fromFunc as BasicExpression<boolean>
600
+ }
601
+
602
+ // age >= 10 - age > 20 = (age >= 10 AND age <= 20)
603
+ if (fromOp === `gte` && subtractOp === `gt`) {
604
+ if (fromValue <= subtractValue) {
605
+ return {
606
+ type: `func`,
607
+ name: `and`,
608
+ args: [
609
+ fromFunc as BasicExpression<boolean>,
610
+ {
611
+ type: `func`,
612
+ name: `lte`,
613
+ args: [ref, { type: `val`, value: subtractValue }],
614
+ } as BasicExpression<boolean>,
615
+ ],
616
+ } as BasicExpression<boolean>
617
+ }
618
+ return fromFunc as BasicExpression<boolean>
619
+ }
620
+
621
+ // age < 30 - age < 20 = (age >= 20 AND age < 30)
622
+ if (fromOp === `lt` && subtractOp === `lt`) {
623
+ if (fromValue > subtractValue) {
624
+ return {
625
+ type: `func`,
626
+ name: `and`,
627
+ args: [
628
+ {
629
+ type: `func`,
630
+ name: `gte`,
631
+ args: [ref, { type: `val`, value: subtractValue }],
632
+ } as BasicExpression<boolean>,
633
+ fromFunc as BasicExpression<boolean>,
634
+ ],
635
+ } as BasicExpression<boolean>
636
+ }
637
+ return fromFunc as BasicExpression<boolean>
638
+ }
639
+
640
+ // age <= 30 - age <= 20 = (age > 20 AND age <= 30)
641
+ if (fromOp === `lte` && subtractOp === `lte`) {
642
+ if (fromValue > subtractValue) {
643
+ return {
644
+ type: `func`,
645
+ name: `and`,
646
+ args: [
647
+ {
648
+ type: `func`,
649
+ name: `gt`,
650
+ args: [ref, { type: `val`, value: subtractValue }],
651
+ } as BasicExpression<boolean>,
652
+ fromFunc as BasicExpression<boolean>,
653
+ ],
654
+ } as BasicExpression<boolean>
655
+ }
656
+ return fromFunc as BasicExpression<boolean>
657
+ }
658
+
659
+ // age < 30 - age <= 20 = (age > 20 AND age < 30)
660
+ if (fromOp === `lt` && subtractOp === `lte`) {
661
+ if (fromValue > subtractValue) {
662
+ return {
663
+ type: `func`,
664
+ name: `and`,
665
+ args: [
666
+ {
667
+ type: `func`,
668
+ name: `gt`,
669
+ args: [ref, { type: `val`, value: subtractValue }],
670
+ } as BasicExpression<boolean>,
671
+ fromFunc as BasicExpression<boolean>,
672
+ ],
673
+ } as BasicExpression<boolean>
674
+ }
675
+ return fromFunc as BasicExpression<boolean>
676
+ }
677
+
678
+ // age <= 30 - age < 20 = (age >= 20 AND age <= 30)
679
+ if (fromOp === `lte` && subtractOp === `lt`) {
680
+ if (fromValue >= subtractValue) {
681
+ return {
682
+ type: `func`,
683
+ name: `and`,
684
+ args: [
685
+ {
686
+ type: `func`,
687
+ name: `gte`,
688
+ args: [ref, { type: `val`, value: subtractValue }],
689
+ } as BasicExpression<boolean>,
690
+ fromFunc as BasicExpression<boolean>,
691
+ ],
692
+ } as BasicExpression<boolean>
693
+ }
694
+ return fromFunc as BasicExpression<boolean>
695
+ }
696
+
697
+ // Can't simplify other combinations
698
+ return null
699
+ }
700
+
701
+ /**
702
+ * Check if one orderBy clause is a subset of another.
703
+ * Returns true if the subset ordering requirements are satisfied by the superset ordering.
704
+ *
705
+ * @example
706
+ * // Subset is prefix of superset
707
+ * isOrderBySubset([{expr: age, asc}], [{expr: age, asc}, {expr: name, desc}]) // true
708
+ *
709
+ * @param subset - The ordering requirements to check
710
+ * @param superset - The ordering that might satisfy the requirements
711
+ * @returns true if subset is satisfied by superset
712
+ */
713
+ export function isOrderBySubset(
714
+ subset: OrderBy | undefined,
715
+ superset: OrderBy | undefined
716
+ ): boolean {
717
+ // No ordering requirement is always satisfied
718
+ if (!subset || subset.length === 0) {
719
+ return true
720
+ }
721
+
722
+ // If there's no superset ordering but subset requires ordering, not satisfied
723
+ if (!superset || superset.length === 0) {
724
+ return false
725
+ }
726
+
727
+ // Check if subset is a prefix of superset with matching expressions and compare options
728
+ if (subset.length > superset.length) {
729
+ return false
730
+ }
731
+
732
+ for (let i = 0; i < subset.length; i++) {
733
+ const subClause = subset[i]!
734
+ const superClause = superset[i]!
735
+
736
+ // Check if expressions match
737
+ if (!areExpressionsEqual(subClause.expression, superClause.expression)) {
738
+ return false
739
+ }
740
+
741
+ // Check if compare options match
742
+ if (
743
+ !areCompareOptionsEqual(
744
+ subClause.compareOptions,
745
+ superClause.compareOptions
746
+ )
747
+ ) {
748
+ return false
749
+ }
750
+ }
751
+
752
+ return true
753
+ }
754
+
755
+ /**
756
+ * Check if one limit is a subset of another.
757
+ * Returns true if the subset limit requirements are satisfied by the superset limit.
758
+ *
759
+ * @example
760
+ * isLimitSubset(10, 20) // true (requesting 10 items when 20 are available)
761
+ * isLimitSubset(20, 10) // false (requesting 20 items when only 10 are available)
762
+ * isLimitSubset(10, undefined) // true (requesting 10 items when unlimited are available)
763
+ *
764
+ * @param subset - The limit requirement to check
765
+ * @param superset - The limit that might satisfy the requirement
766
+ * @returns true if subset is satisfied by superset
767
+ */
768
+ export function isLimitSubset(
769
+ subset: number | undefined,
770
+ superset: number | undefined
771
+ ): boolean {
772
+ // Unlimited superset satisfies any limit requirement
773
+ if (superset === undefined) {
774
+ return true
775
+ }
776
+
777
+ // If requesting all data (no limit), we need unlimited data to satisfy it
778
+ // But we know superset is not unlimited so we return false
779
+ if (subset === undefined) {
780
+ return false
781
+ }
782
+
783
+ // Otherwise, subset must be less than or equal to superset
784
+ return subset <= superset
785
+ }
786
+
787
+ /**
788
+ * Check if one predicate (where + orderBy + limit) is a subset of another.
789
+ * Returns true if all aspects of the subset predicate are satisfied by the superset.
790
+ *
791
+ * @example
792
+ * isPredicateSubset(
793
+ * { where: gt(ref('age'), val(20)), limit: 10 },
794
+ * { where: gt(ref('age'), val(10)), limit: 20 }
795
+ * ) // true
796
+ *
797
+ * @param subset - The predicate requirements to check
798
+ * @param superset - The predicate that might satisfy the requirements
799
+ * @returns true if subset is satisfied by superset
800
+ */
801
+ export function isPredicateSubset(
802
+ subset: LoadSubsetOptions,
803
+ superset: LoadSubsetOptions
804
+ ): boolean {
805
+ return (
806
+ isWhereSubset(subset.where, superset.where) &&
807
+ isOrderBySubset(subset.orderBy, superset.orderBy) &&
808
+ isLimitSubset(subset.limit, superset.limit)
809
+ )
810
+ }
811
+
812
+ // ============================================================================
813
+ // Helper functions
814
+ // ============================================================================
815
+
816
+ /**
817
+ * Find common conditions between two predicates.
818
+ * Returns an array of conditions that appear in both predicates.
819
+ */
820
+ function findCommonConditions(
821
+ predicate1: BasicExpression<boolean>,
822
+ predicate2: BasicExpression<boolean>
823
+ ): Array<BasicExpression<boolean>> {
824
+ const conditions1 = extractAllConditions(predicate1)
825
+ const conditions2 = extractAllConditions(predicate2)
826
+
827
+ const common: Array<BasicExpression<boolean>> = []
828
+
829
+ for (const cond1 of conditions1) {
830
+ for (const cond2 of conditions2) {
831
+ if (areExpressionsEqual(cond1, cond2)) {
832
+ // Avoid duplicates
833
+ if (!common.some((c) => areExpressionsEqual(c, cond1))) {
834
+ common.push(cond1)
835
+ }
836
+ break
837
+ }
838
+ }
839
+ }
840
+
841
+ return common
842
+ }
843
+
844
+ /**
845
+ * Extract all individual conditions from a predicate, flattening AND operations.
846
+ */
847
+ function extractAllConditions(
848
+ predicate: BasicExpression<boolean>
849
+ ): Array<BasicExpression<boolean>> {
850
+ if (predicate.type === `func` && predicate.name === `and`) {
851
+ const conditions: Array<BasicExpression<boolean>> = []
852
+ for (const arg of predicate.args) {
853
+ conditions.push(...extractAllConditions(arg as BasicExpression<boolean>))
854
+ }
855
+ return conditions
856
+ }
857
+
858
+ return [predicate]
859
+ }
860
+
861
+ /**
862
+ * Remove specified conditions from a predicate.
863
+ * Returns the predicate with the specified conditions removed, or undefined if all conditions are removed.
864
+ */
865
+ function removeConditions(
866
+ predicate: BasicExpression<boolean>,
867
+ conditionsToRemove: Array<BasicExpression<boolean>>
868
+ ): BasicExpression<boolean> | undefined {
869
+ if (predicate.type === `func` && predicate.name === `and`) {
870
+ const remainingArgs = predicate.args.filter(
871
+ (arg) =>
872
+ !conditionsToRemove.some((cond) =>
873
+ areExpressionsEqual(arg as BasicExpression<boolean>, cond)
874
+ )
875
+ )
876
+
877
+ if (remainingArgs.length === 0) {
878
+ return undefined
879
+ } else if (remainingArgs.length === 1) {
880
+ return remainingArgs[0]!
881
+ } else {
882
+ return {
883
+ type: `func`,
884
+ name: `and`,
885
+ args: remainingArgs,
886
+ } as BasicExpression<boolean>
887
+ }
888
+ }
889
+
890
+ // For non-AND predicates, don't remove anything
891
+ return predicate
892
+ }
893
+
894
+ /**
895
+ * Combine multiple conditions into a single predicate using AND logic.
896
+ * Flattens nested AND operations to avoid unnecessary nesting.
897
+ */
898
+ function combineConditions(
899
+ conditions: Array<BasicExpression<boolean>>
900
+ ): BasicExpression<boolean> {
901
+ if (conditions.length === 0) {
902
+ return { type: `val`, value: true } as BasicExpression<boolean>
903
+ } else if (conditions.length === 1) {
904
+ return conditions[0]!
905
+ } else {
906
+ // Flatten all conditions, including those that are already AND operations
907
+ const flattenedConditions: Array<BasicExpression<boolean>> = []
908
+
909
+ for (const condition of conditions) {
910
+ if (condition.type === `func` && condition.name === `and`) {
911
+ // Flatten nested AND operations
912
+ flattenedConditions.push(...condition.args)
913
+ } else {
914
+ flattenedConditions.push(condition)
915
+ }
916
+ }
917
+
918
+ if (flattenedConditions.length === 1) {
919
+ return flattenedConditions[0]!
920
+ } else {
921
+ return {
922
+ type: `func`,
923
+ name: `and`,
924
+ args: flattenedConditions,
925
+ } as BasicExpression<boolean>
926
+ }
927
+ }
928
+ }
929
+
930
+ /**
931
+ * Find a predicate with a specific operator and value
932
+ */
933
+ function findPredicateWithOperator(
934
+ predicates: Array<BasicExpression<boolean>>,
935
+ operator: string,
936
+ value: any
937
+ ): BasicExpression<boolean> | undefined {
938
+ return predicates.find((p) => {
939
+ if (p.type === `func`) {
940
+ const f = p as Func
941
+ const field = extractComparisonField(f)
942
+ return f.name === operator && field && areValuesEqual(field.value, value)
943
+ }
944
+ return false
945
+ })
946
+ }
947
+
948
+ function areExpressionsEqual(a: BasicExpression, b: BasicExpression): boolean {
949
+ if (a.type !== b.type) {
950
+ return false
951
+ }
952
+
953
+ if (a.type === `val` && b.type === `val`) {
954
+ return areValuesEqual(a.value, b.value)
955
+ }
956
+
957
+ if (a.type === `ref` && b.type === `ref`) {
958
+ return areRefsEqual(a, b)
959
+ }
960
+
961
+ if (a.type === `func` && b.type === `func`) {
962
+ const aFunc = a
963
+ const bFunc = b
964
+ if (aFunc.name !== bFunc.name) {
965
+ return false
966
+ }
967
+ if (aFunc.args.length !== bFunc.args.length) {
968
+ return false
969
+ }
970
+ return aFunc.args.every((arg, i) =>
971
+ areExpressionsEqual(arg, bFunc.args[i]!)
972
+ )
973
+ }
974
+
975
+ return false
976
+ }
977
+
978
+ function areValuesEqual(a: any, b: any): boolean {
979
+ // Simple equality check - could be enhanced for deep object comparison
980
+ if (a === b) {
981
+ return true
982
+ }
983
+
984
+ // Handle NaN
985
+ if (typeof a === `number` && typeof b === `number` && isNaN(a) && isNaN(b)) {
986
+ return true
987
+ }
988
+
989
+ // Handle Date objects
990
+ if (a instanceof Date && b instanceof Date) {
991
+ return a.getTime() === b.getTime()
992
+ }
993
+
994
+ // For arrays and objects, use reference equality
995
+ // (In practice, we don't need deep equality for these cases -
996
+ // same object reference means same value for our use case)
997
+ if (
998
+ typeof a === `object` &&
999
+ typeof b === `object` &&
1000
+ a !== null &&
1001
+ b !== null
1002
+ ) {
1003
+ return a === b
1004
+ }
1005
+
1006
+ return false
1007
+ }
1008
+
1009
+ function areRefsEqual(a: PropRef, b: PropRef): boolean {
1010
+ if (a.path.length !== b.path.length) {
1011
+ return false
1012
+ }
1013
+ return a.path.every((segment, i) => segment === b.path[i])
1014
+ }
1015
+
1016
+ /**
1017
+ * Check if a value is a primitive (string, number, boolean, null, undefined)
1018
+ * Primitives can use Set for fast lookups
1019
+ */
1020
+ function isPrimitive(value: any): boolean {
1021
+ return (
1022
+ value === null ||
1023
+ value === undefined ||
1024
+ typeof value === `string` ||
1025
+ typeof value === `number` ||
1026
+ typeof value === `boolean`
1027
+ )
1028
+ }
1029
+
1030
+ /**
1031
+ * Check if all values in an array are primitives
1032
+ */
1033
+ function areAllPrimitives(values: Array<any>): boolean {
1034
+ return values.every(isPrimitive)
1035
+ }
1036
+
1037
+ /**
1038
+ * Check if a value is in an array, with optional pre-built Set for optimization.
1039
+ * The primitiveSet is cached in InField during extraction and reused for all lookups.
1040
+ */
1041
+ function arrayIncludesWithSet(
1042
+ array: Array<any>,
1043
+ value: any,
1044
+ primitiveSet: Set<any> | null,
1045
+ arrayIsAllPrimitives?: boolean
1046
+ ): boolean {
1047
+ // Fast path: use pre-built Set for O(1) lookup
1048
+ if (primitiveSet) {
1049
+ // Skip isPrimitive check if we know the value must be primitive for a match
1050
+ // (if array is all primitives, only primitives can match)
1051
+ if (arrayIsAllPrimitives || isPrimitive(value)) {
1052
+ return primitiveSet.has(value)
1053
+ }
1054
+ return false // Non-primitive can't be in primitive-only set
1055
+ }
1056
+
1057
+ // Fallback: use areValuesEqual for Dates and objects
1058
+ return array.some((v) => areValuesEqual(v, value))
1059
+ }
1060
+
1061
+ /**
1062
+ * Get the maximum of two values, handling both numbers and Dates
1063
+ */
1064
+ function maxValue(a: any, b: any): any {
1065
+ if (a instanceof Date && b instanceof Date) {
1066
+ return a.getTime() > b.getTime() ? a : b
1067
+ }
1068
+ return Math.max(a, b)
1069
+ }
1070
+
1071
+ /**
1072
+ * Get the minimum of two values, handling both numbers and Dates
1073
+ */
1074
+ function minValue(a: any, b: any): any {
1075
+ if (a instanceof Date && b instanceof Date) {
1076
+ return a.getTime() < b.getTime() ? a : b
1077
+ }
1078
+ return Math.min(a, b)
1079
+ }
1080
+
1081
+ function areCompareOptionsEqual(
1082
+ a: { direction?: `asc` | `desc`; [key: string]: any },
1083
+ b: { direction?: `asc` | `desc`; [key: string]: any }
1084
+ ): boolean {
1085
+ // For now, just compare direction - could be enhanced for other options
1086
+ return a.direction === b.direction
1087
+ }
1088
+
1089
+ interface ComparisonField {
1090
+ ref: PropRef
1091
+ value: any
1092
+ }
1093
+
1094
+ function extractComparisonField(func: Func): ComparisonField | null {
1095
+ // Handle comparison operators: eq, gt, gte, lt, lte
1096
+ if ([`eq`, `gt`, `gte`, `lt`, `lte`].includes(func.name)) {
1097
+ // Assume first arg is ref, second is value
1098
+ const firstArg = func.args[0]
1099
+ const secondArg = func.args[1]
1100
+
1101
+ if (firstArg?.type === `ref` && secondArg?.type === `val`) {
1102
+ return {
1103
+ ref: firstArg,
1104
+ value: secondArg.value,
1105
+ }
1106
+ }
1107
+ }
1108
+
1109
+ return null
1110
+ }
1111
+
1112
+ function extractEqualityField(func: Func): ComparisonField | null {
1113
+ if (func.name === `eq`) {
1114
+ const firstArg = func.args[0]
1115
+ const secondArg = func.args[1]
1116
+
1117
+ if (firstArg?.type === `ref` && secondArg?.type === `val`) {
1118
+ return {
1119
+ ref: firstArg,
1120
+ value: secondArg.value,
1121
+ }
1122
+ }
1123
+ }
1124
+ return null
1125
+ }
1126
+
1127
+ interface InField {
1128
+ ref: PropRef
1129
+ values: Array<any>
1130
+ // Cached optimization data (computed once, reused many times)
1131
+ areAllPrimitives?: boolean
1132
+ primitiveSet?: Set<any> | null
1133
+ }
1134
+
1135
+ function extractInField(func: Func): InField | null {
1136
+ if (func.name === `in`) {
1137
+ const firstArg = func.args[0]
1138
+ const secondArg = func.args[1]
1139
+
1140
+ if (
1141
+ firstArg?.type === `ref` &&
1142
+ secondArg?.type === `val` &&
1143
+ Array.isArray(secondArg.value)
1144
+ ) {
1145
+ let values = secondArg.value
1146
+ // Precompute optimization metadata once
1147
+ const allPrimitives = areAllPrimitives(values)
1148
+ let primitiveSet: Set<any> | null = null
1149
+
1150
+ if (allPrimitives && values.length > 10) {
1151
+ // Build Set and dedupe values at the same time
1152
+ primitiveSet = new Set(values)
1153
+ // If we found duplicates, use the deduped array going forward
1154
+ if (primitiveSet.size < values.length) {
1155
+ values = Array.from(primitiveSet)
1156
+ }
1157
+ }
1158
+
1159
+ return {
1160
+ ref: firstArg,
1161
+ values,
1162
+ areAllPrimitives: allPrimitives,
1163
+ primitiveSet,
1164
+ }
1165
+ }
1166
+ }
1167
+ return null
1168
+ }
1169
+
1170
+ function isComparisonSubset(
1171
+ subsetFunc: Func,
1172
+ subsetValue: any,
1173
+ supersetFunc: Func,
1174
+ supersetValue: any
1175
+ ): boolean {
1176
+ const subOp = subsetFunc.name
1177
+ const superOp = supersetFunc.name
1178
+
1179
+ // Handle same operator
1180
+ if (subOp === superOp) {
1181
+ if (subOp === `eq`) {
1182
+ // field = X is subset of field = X only
1183
+ // Fast path: primitives can use strict equality
1184
+ if (isPrimitive(subsetValue) && isPrimitive(supersetValue)) {
1185
+ return subsetValue === supersetValue
1186
+ }
1187
+ return areValuesEqual(subsetValue, supersetValue)
1188
+ } else if (subOp === `gt`) {
1189
+ // field > 20 is subset of field > 10 if 20 > 10
1190
+ return subsetValue >= supersetValue
1191
+ } else if (subOp === `gte`) {
1192
+ // field >= 20 is subset of field >= 10 if 20 >= 10
1193
+ return subsetValue >= supersetValue
1194
+ } else if (subOp === `lt`) {
1195
+ // field < 10 is subset of field < 20 if 10 <= 20
1196
+ return subsetValue <= supersetValue
1197
+ } else if (subOp === `lte`) {
1198
+ // field <= 10 is subset of field <= 20 if 10 <= 20
1199
+ return subsetValue <= supersetValue
1200
+ }
1201
+ }
1202
+
1203
+ // Handle different operators on same field
1204
+ // eq vs gt/gte: field = 15 is subset of field > 10 if 15 > 10
1205
+ if (subOp === `eq` && superOp === `gt`) {
1206
+ return subsetValue > supersetValue
1207
+ }
1208
+ if (subOp === `eq` && superOp === `gte`) {
1209
+ return subsetValue >= supersetValue
1210
+ }
1211
+ if (subOp === `eq` && superOp === `lt`) {
1212
+ return subsetValue < supersetValue
1213
+ }
1214
+ if (subOp === `eq` && superOp === `lte`) {
1215
+ return subsetValue <= supersetValue
1216
+ }
1217
+
1218
+ // gt/gte vs gte/gt
1219
+ if (subOp === `gt` && superOp === `gte`) {
1220
+ // field > 10 is subset of field >= 10 if 10 >= 10 (always true for same value)
1221
+ return subsetValue >= supersetValue
1222
+ }
1223
+ if (subOp === `gte` && superOp === `gt`) {
1224
+ // field >= 11 is subset of field > 10 if 11 > 10
1225
+ return subsetValue > supersetValue
1226
+ }
1227
+
1228
+ // lt/lte vs lte/lt
1229
+ if (subOp === `lt` && superOp === `lte`) {
1230
+ // field < 10 is subset of field <= 10 if 10 <= 10
1231
+ return subsetValue <= supersetValue
1232
+ }
1233
+ if (subOp === `lte` && superOp === `lt`) {
1234
+ // field <= 9 is subset of field < 10 if 9 < 10
1235
+ return subsetValue < supersetValue
1236
+ }
1237
+
1238
+ return false
1239
+ }
1240
+
1241
+ function groupPredicatesByField(
1242
+ predicates: Array<BasicExpression<boolean>>
1243
+ ): Map<string | null, Array<BasicExpression<boolean>>> {
1244
+ const groups = new Map<string | null, Array<BasicExpression<boolean>>>()
1245
+
1246
+ for (const pred of predicates) {
1247
+ let fieldKey: string | null = null
1248
+
1249
+ if (pred.type === `func`) {
1250
+ const func = pred as Func
1251
+ const field =
1252
+ extractComparisonField(func) ||
1253
+ extractEqualityField(func) ||
1254
+ extractInField(func)
1255
+ if (field) {
1256
+ fieldKey = field.ref.path.join(`.`)
1257
+ }
1258
+ }
1259
+
1260
+ const group = groups.get(fieldKey) || []
1261
+ group.push(pred)
1262
+ groups.set(fieldKey, group)
1263
+ }
1264
+
1265
+ return groups
1266
+ }
1267
+
1268
+ function unionSameFieldPredicates(
1269
+ predicates: Array<BasicExpression<boolean>>
1270
+ ): BasicExpression<boolean> | null {
1271
+ if (predicates.length === 1) {
1272
+ return predicates[0]!
1273
+ }
1274
+
1275
+ // Try to extract range constraints
1276
+ let maxGt: number | null = null
1277
+ let maxGte: number | null = null
1278
+ let minLt: number | null = null
1279
+ let minLte: number | null = null
1280
+ const eqValues: Set<any> = new Set()
1281
+ const inValues: Set<any> = new Set()
1282
+ const otherPredicates: Array<BasicExpression<boolean>> = []
1283
+
1284
+ for (const pred of predicates) {
1285
+ if (pred.type === `func`) {
1286
+ const func = pred as Func
1287
+ const field = extractComparisonField(func)
1288
+
1289
+ if (field) {
1290
+ const value = field.value
1291
+ if (func.name === `gt`) {
1292
+ maxGt = maxGt === null ? value : minValue(maxGt, value)
1293
+ } else if (func.name === `gte`) {
1294
+ maxGte = maxGte === null ? value : minValue(maxGte, value)
1295
+ } else if (func.name === `lt`) {
1296
+ minLt = minLt === null ? value : maxValue(minLt, value)
1297
+ } else if (func.name === `lte`) {
1298
+ minLte = minLte === null ? value : maxValue(minLte, value)
1299
+ } else if (func.name === `eq`) {
1300
+ eqValues.add(value)
1301
+ } else {
1302
+ otherPredicates.push(pred)
1303
+ }
1304
+ } else {
1305
+ const inField = extractInField(func)
1306
+ if (inField) {
1307
+ for (const val of inField.values) {
1308
+ inValues.add(val)
1309
+ }
1310
+ } else {
1311
+ otherPredicates.push(pred)
1312
+ }
1313
+ }
1314
+ } else {
1315
+ otherPredicates.push(pred)
1316
+ }
1317
+ }
1318
+
1319
+ // If we have multiple equality values, combine into IN
1320
+ if (eqValues.size > 1 || (eqValues.size > 0 && inValues.size > 0)) {
1321
+ const allValues = [...eqValues, ...inValues]
1322
+ const ref = predicates.find((p) => {
1323
+ if (p.type === `func`) {
1324
+ const field =
1325
+ extractComparisonField(p as Func) || extractInField(p as Func)
1326
+ return field !== null
1327
+ }
1328
+ return false
1329
+ })
1330
+
1331
+ if (ref && ref.type === `func`) {
1332
+ const field =
1333
+ extractComparisonField(ref as Func) || extractInField(ref as Func)
1334
+ if (field) {
1335
+ return {
1336
+ type: `func`,
1337
+ name: `in`,
1338
+ args: [
1339
+ field.ref,
1340
+ { type: `val`, value: allValues } as BasicExpression,
1341
+ ],
1342
+ } as BasicExpression<boolean>
1343
+ }
1344
+ }
1345
+ }
1346
+
1347
+ // Build the least restrictive range
1348
+ const result: Array<BasicExpression<boolean>> = []
1349
+
1350
+ // Choose the least restrictive lower bound
1351
+ if (maxGt !== null && maxGte !== null) {
1352
+ // Take the smaller one (less restrictive)
1353
+ const pred =
1354
+ maxGte <= maxGt
1355
+ ? findPredicateWithOperator(predicates, `gte`, maxGte)
1356
+ : findPredicateWithOperator(predicates, `gt`, maxGt)
1357
+ if (pred) result.push(pred)
1358
+ } else if (maxGt !== null) {
1359
+ const pred = findPredicateWithOperator(predicates, `gt`, maxGt)
1360
+ if (pred) result.push(pred)
1361
+ } else if (maxGte !== null) {
1362
+ const pred = findPredicateWithOperator(predicates, `gte`, maxGte)
1363
+ if (pred) result.push(pred)
1364
+ }
1365
+
1366
+ // Choose the least restrictive upper bound
1367
+ if (minLt !== null && minLte !== null) {
1368
+ const pred =
1369
+ minLte >= minLt
1370
+ ? findPredicateWithOperator(predicates, `lte`, minLte)
1371
+ : findPredicateWithOperator(predicates, `lt`, minLt)
1372
+ if (pred) result.push(pred)
1373
+ } else if (minLt !== null) {
1374
+ const pred = findPredicateWithOperator(predicates, `lt`, minLt)
1375
+ if (pred) result.push(pred)
1376
+ } else if (minLte !== null) {
1377
+ const pred = findPredicateWithOperator(predicates, `lte`, minLte)
1378
+ if (pred) result.push(pred)
1379
+ }
1380
+
1381
+ // Add single eq value
1382
+ if (eqValues.size === 1 && inValues.size === 0) {
1383
+ const pred = findPredicateWithOperator(predicates, `eq`, [...eqValues][0])
1384
+ if (pred) result.push(pred)
1385
+ }
1386
+
1387
+ // Add IN if only IN values
1388
+ if (eqValues.size === 0 && inValues.size > 0) {
1389
+ result.push(
1390
+ predicates.find((p) => {
1391
+ if (p.type === `func`) {
1392
+ return (p as Func).name === `in`
1393
+ }
1394
+ return false
1395
+ })!
1396
+ )
1397
+ }
1398
+
1399
+ // Add other predicates
1400
+ result.push(...otherPredicates)
1401
+
1402
+ if (result.length === 0) {
1403
+ return { type: `val`, value: true } as BasicExpression<boolean>
1404
+ }
1405
+
1406
+ if (result.length === 1) {
1407
+ return result[0]!
1408
+ }
1409
+
1410
+ return {
1411
+ type: `func`,
1412
+ name: `or`,
1413
+ args: result,
1414
+ } as BasicExpression<boolean>
1415
+ }