@tanstack/db 0.0.27 → 0.0.29

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 (164) 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/index-options.d.cts +13 -0
  20. package/dist/cjs/indexes/lazy-index.cjs +193 -0
  21. package/dist/cjs/indexes/lazy-index.cjs.map +1 -0
  22. package/dist/cjs/indexes/lazy-index.d.cts +96 -0
  23. package/dist/cjs/indexes/ordered-index.cjs +227 -0
  24. package/dist/cjs/indexes/ordered-index.cjs.map +1 -0
  25. package/dist/cjs/indexes/ordered-index.d.cts +72 -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.cjs +18 -0
  61. package/dist/cjs/utils/array-utils.cjs.map +1 -0
  62. package/dist/cjs/utils/array-utils.d.cts +8 -0
  63. package/dist/cjs/utils/comparison.cjs +52 -0
  64. package/dist/cjs/utils/comparison.cjs.map +1 -0
  65. package/dist/cjs/utils/comparison.d.cts +11 -0
  66. package/dist/cjs/utils/index-optimization.cjs +270 -0
  67. package/dist/cjs/utils/index-optimization.cjs.map +1 -0
  68. package/dist/cjs/utils/index-optimization.d.cts +29 -0
  69. package/dist/esm/change-events.d.ts +49 -0
  70. package/dist/esm/change-events.js +141 -0
  71. package/dist/esm/change-events.js.map +1 -0
  72. package/dist/esm/collection.d.ts +95 -20
  73. package/dist/esm/collection.js +232 -84
  74. package/dist/esm/collection.js.map +1 -1
  75. package/dist/esm/errors.d.ts +225 -1
  76. package/dist/esm/errors.js +510 -2
  77. package/dist/esm/errors.js.map +1 -1
  78. package/dist/esm/index.d.ts +5 -1
  79. package/dist/esm/index.js +81 -2
  80. package/dist/esm/index.js.map +1 -1
  81. package/dist/esm/indexes/auto-index.d.ts +9 -0
  82. package/dist/esm/indexes/auto-index.js +64 -0
  83. package/dist/esm/indexes/auto-index.js.map +1 -0
  84. package/dist/esm/indexes/base-index.d.ts +54 -0
  85. package/dist/esm/indexes/base-index.js +46 -0
  86. package/dist/esm/indexes/base-index.js.map +1 -0
  87. package/dist/esm/indexes/index-options.d.ts +13 -0
  88. package/dist/esm/indexes/lazy-index.d.ts +96 -0
  89. package/dist/esm/indexes/lazy-index.js +193 -0
  90. package/dist/esm/indexes/lazy-index.js.map +1 -0
  91. package/dist/esm/indexes/ordered-index.d.ts +72 -0
  92. package/dist/esm/indexes/ordered-index.js +227 -0
  93. package/dist/esm/indexes/ordered-index.js.map +1 -0
  94. package/dist/esm/local-storage.js +9 -15
  95. package/dist/esm/local-storage.js.map +1 -1
  96. package/dist/esm/query/builder/functions.d.ts +4 -0
  97. package/dist/esm/query/builder/functions.js +11 -0
  98. package/dist/esm/query/builder/functions.js.map +1 -1
  99. package/dist/esm/query/builder/index.js +6 -7
  100. package/dist/esm/query/builder/index.js.map +1 -1
  101. package/dist/esm/query/builder/ref-proxy.d.ts +12 -0
  102. package/dist/esm/query/builder/ref-proxy.js +37 -0
  103. package/dist/esm/query/builder/ref-proxy.js.map +1 -1
  104. package/dist/esm/query/compiler/evaluators.d.ts +8 -0
  105. package/dist/esm/query/compiler/evaluators.js +84 -59
  106. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  107. package/dist/esm/query/compiler/expressions.d.ts +25 -0
  108. package/dist/esm/query/compiler/expressions.js +61 -0
  109. package/dist/esm/query/compiler/expressions.js.map +1 -0
  110. package/dist/esm/query/compiler/group-by.js +5 -10
  111. package/dist/esm/query/compiler/group-by.js.map +1 -1
  112. package/dist/esm/query/compiler/index.d.ts +12 -3
  113. package/dist/esm/query/compiler/index.js +23 -17
  114. package/dist/esm/query/compiler/index.js.map +1 -1
  115. package/dist/esm/query/compiler/joins.js +61 -12
  116. package/dist/esm/query/compiler/joins.js.map +1 -1
  117. package/dist/esm/query/compiler/order-by.js +1 -31
  118. package/dist/esm/query/compiler/order-by.js.map +1 -1
  119. package/dist/esm/query/compiler/types.d.ts +2 -2
  120. package/dist/esm/query/live-query-collection.js +54 -12
  121. package/dist/esm/query/live-query-collection.js.map +1 -1
  122. package/dist/esm/query/optimizer.d.ts +13 -3
  123. package/dist/esm/query/optimizer.js +40 -2
  124. package/dist/esm/query/optimizer.js.map +1 -1
  125. package/dist/esm/transactions.js +5 -4
  126. package/dist/esm/transactions.js.map +1 -1
  127. package/dist/esm/types.d.ts +31 -0
  128. package/dist/esm/utils/array-utils.d.ts +8 -0
  129. package/dist/esm/utils/array-utils.js +18 -0
  130. package/dist/esm/utils/array-utils.js.map +1 -0
  131. package/dist/esm/utils/comparison.d.ts +11 -0
  132. package/dist/esm/utils/comparison.js +52 -0
  133. package/dist/esm/utils/comparison.js.map +1 -0
  134. package/dist/esm/utils/index-optimization.d.ts +29 -0
  135. package/dist/esm/utils/index-optimization.js +270 -0
  136. package/dist/esm/utils/index-optimization.js.map +1 -0
  137. package/package.json +1 -1
  138. package/src/change-events.ts +257 -0
  139. package/src/collection.ts +318 -105
  140. package/src/errors.ts +545 -1
  141. package/src/index.ts +7 -1
  142. package/src/indexes/auto-index.ts +108 -0
  143. package/src/indexes/base-index.ts +119 -0
  144. package/src/indexes/index-options.ts +42 -0
  145. package/src/indexes/lazy-index.ts +251 -0
  146. package/src/indexes/ordered-index.ts +305 -0
  147. package/src/local-storage.ts +16 -17
  148. package/src/query/builder/functions.ts +14 -0
  149. package/src/query/builder/index.ts +12 -7
  150. package/src/query/builder/ref-proxy.ts +65 -0
  151. package/src/query/compiler/evaluators.ts +114 -62
  152. package/src/query/compiler/expressions.ts +92 -0
  153. package/src/query/compiler/group-by.ts +10 -10
  154. package/src/query/compiler/index.ts +52 -22
  155. package/src/query/compiler/joins.ts +114 -15
  156. package/src/query/compiler/order-by.ts +1 -45
  157. package/src/query/compiler/types.ts +2 -2
  158. package/src/query/live-query-collection.ts +95 -15
  159. package/src/query/optimizer.ts +94 -5
  160. package/src/transactions.ts +10 -4
  161. package/src/types.ts +38 -0
  162. package/src/utils/array-utils.ts +28 -0
  163. package/src/utils/comparison.ts +79 -0
  164. package/src/utils/index-optimization.ts +546 -0
@@ -1,3 +1,11 @@
1
+ import {
2
+ InvalidStorageDataFormatError,
3
+ InvalidStorageObjectFormatError,
4
+ NoStorageAvailableError,
5
+ NoStorageEventApiError,
6
+ SerializationError,
7
+ StorageKeyRequiredError,
8
+ } from "./errors"
1
9
  import type {
2
10
  CollectionConfig,
3
11
  DeleteMutationFnParams,
@@ -136,10 +144,9 @@ function validateJsonSerializable(value: any, operation: string): void {
136
144
  try {
137
145
  JSON.stringify(value)
138
146
  } catch (error) {
139
- throw new Error(
140
- `Cannot ${operation} item because it cannot be JSON serialized: ${
141
- error instanceof Error ? error.message : String(error)
142
- }`
147
+ throw new SerializationError(
148
+ operation,
149
+ error instanceof Error ? error.message : String(error)
143
150
  )
144
151
  }
145
152
  }
@@ -204,7 +211,7 @@ export function localStorageCollectionOptions<
204
211
 
205
212
  // Validate required parameters
206
213
  if (!config.storageKey) {
207
- throw new Error(`[LocalStorageCollection] storageKey must be provided.`)
214
+ throw new StorageKeyRequiredError()
208
215
  }
209
216
 
210
217
  // Default to window.localStorage if no storage is provided
@@ -213,9 +220,7 @@ export function localStorageCollectionOptions<
213
220
  (typeof window !== `undefined` ? window.localStorage : null)
214
221
 
215
222
  if (!storage) {
216
- throw new Error(
217
- `[LocalStorageCollection] No storage available. Please provide a storage option or ensure window.localStorage is available.`
218
- )
223
+ throw new NoStorageAvailableError()
219
224
  }
220
225
 
221
226
  // Default to window for storage events if not provided
@@ -223,9 +228,7 @@ export function localStorageCollectionOptions<
223
228
  config.storageEventApi || (typeof window !== `undefined` ? window : null)
224
229
 
225
230
  if (!storageEventApi) {
226
- throw new Error(
227
- `[LocalStorageCollection] No storage event API available. Please provide a storageEventApi option or ensure window is available.`
228
- )
231
+ throw new NoStorageEventApiError()
229
232
  }
230
233
 
231
234
  // Track the last known state to detect changes
@@ -471,15 +474,11 @@ function loadFromStorage<T extends object>(
471
474
  const storedItem = value as StoredItem<T>
472
475
  dataMap.set(key, storedItem)
473
476
  } else {
474
- throw new Error(
475
- `[LocalStorageCollection] Invalid data format in storage key "${storageKey}" for key "${key}".`
476
- )
477
+ throw new InvalidStorageDataFormatError(storageKey, key)
477
478
  }
478
479
  })
479
480
  } else {
480
- throw new Error(
481
- `[LocalStorageCollection] Invalid data format in storage key "${storageKey}". Expected object format.`
482
- )
481
+ throw new InvalidStorageObjectFormatError(storageKey)
483
482
  }
484
483
 
485
484
  return dataMap
@@ -265,3 +265,17 @@ export function max(
265
265
  ): Aggregate<number> {
266
266
  return new Aggregate(`max`, [toExpression(arg)])
267
267
  }
268
+
269
+ /**
270
+ * List of comparison function names that can be used with indexes
271
+ */
272
+ export const comparisonFunctions = [
273
+ `eq`,
274
+ `gt`,
275
+ `gte`,
276
+ `lt`,
277
+ `lte`,
278
+ `in`,
279
+ `like`,
280
+ `ilike`,
281
+ ] as const
@@ -1,5 +1,12 @@
1
1
  import { CollectionImpl } from "../../collection.js"
2
2
  import { CollectionRef, QueryRef } from "../ir.js"
3
+ import {
4
+ InvalidSourceError,
5
+ JoinConditionMustBeEqualityError,
6
+ OnlyOneSourceAllowedError,
7
+ QueryMustHaveFromClauseError,
8
+ SubQueryMustHaveFromClauseError,
9
+ } from "../../errors.js"
3
10
  import { createRefProxy, isRefProxy, toExpression } from "./ref-proxy.js"
4
11
  import type { NamespacedRow } from "../../types.js"
5
12
  import type {
@@ -45,7 +52,7 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
45
52
  context: string
46
53
  ): [string, CollectionRef | QueryRef] {
47
54
  if (Object.keys(source).length !== 1) {
48
- throw new Error(`Only one source is allowed in the ${context}`)
55
+ throw new OnlyOneSourceAllowedError(context)
49
56
  }
50
57
 
51
58
  const alias = Object.keys(source)[0]!
@@ -58,13 +65,11 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
58
65
  } else if (sourceValue instanceof BaseQueryBuilder) {
59
66
  const subQuery = sourceValue._getQuery()
60
67
  if (!(subQuery as Partial<QueryIR>).from) {
61
- throw new Error(
62
- `A sub query passed to a ${context} must have a from clause itself`
63
- )
68
+ throw new SubQueryMustHaveFromClauseError(context)
64
69
  }
65
70
  ref = new QueryRef(subQuery, alias)
66
71
  } else {
67
- throw new Error(`Invalid source`)
72
+ throw new InvalidSourceError()
68
73
  }
69
74
 
70
75
  return [alias, ref]
@@ -166,7 +171,7 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
166
171
  left = onExpression.args[0]!
167
172
  right = onExpression.args[1]!
168
173
  } else {
169
- throw new Error(`Join condition must be an equality expression`)
174
+ throw new JoinConditionMustBeEqualityError()
170
175
  }
171
176
 
172
177
  const joinClause: JoinClause = {
@@ -725,7 +730,7 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
725
730
 
726
731
  _getQuery(): QueryIR {
727
732
  if (!this.query.from) {
728
- throw new Error(`Query must have a from clause`)
733
+ throw new QueryMustHaveFromClauseError()
729
734
  }
730
735
  return this.query as QueryIR
731
736
  }
@@ -10,6 +10,71 @@ export interface RefProxy<T = any> {
10
10
  readonly __type: T
11
11
  }
12
12
 
13
+ /**
14
+ * Type for creating a RefProxy for a single row/type without namespacing
15
+ * Used in collection indexes and where clauses
16
+ */
17
+ export type SingleRowRefProxy<T> =
18
+ T extends Record<string, any>
19
+ ? {
20
+ [K in keyof T]: T[K] extends Record<string, any>
21
+ ? SingleRowRefProxy<T[K]> & RefProxy<T[K]>
22
+ : RefProxy<T[K]>
23
+ } & RefProxy<T>
24
+ : RefProxy<T>
25
+
26
+ /**
27
+ * Creates a proxy object that records property access paths for a single row
28
+ * Used in collection indexes and where clauses
29
+ */
30
+ export function createSingleRowRefProxy<
31
+ T extends Record<string, any>,
32
+ >(): SingleRowRefProxy<T> {
33
+ const cache = new Map<string, any>()
34
+
35
+ function createProxy(path: Array<string>): any {
36
+ const pathKey = path.join(`.`)
37
+ if (cache.has(pathKey)) {
38
+ return cache.get(pathKey)
39
+ }
40
+
41
+ const proxy = new Proxy({} as any, {
42
+ get(target, prop, receiver) {
43
+ if (prop === `__refProxy`) return true
44
+ if (prop === `__path`) return path
45
+ if (prop === `__type`) return undefined // Type is only for TypeScript inference
46
+ if (typeof prop === `symbol`) return Reflect.get(target, prop, receiver)
47
+
48
+ const newPath = [...path, String(prop)]
49
+ return createProxy(newPath)
50
+ },
51
+
52
+ has(target, prop) {
53
+ if (prop === `__refProxy` || prop === `__path` || prop === `__type`)
54
+ return true
55
+ return Reflect.has(target, prop)
56
+ },
57
+
58
+ ownKeys(target) {
59
+ return Reflect.ownKeys(target)
60
+ },
61
+
62
+ getOwnPropertyDescriptor(target, prop) {
63
+ if (prop === `__refProxy` || prop === `__path` || prop === `__type`) {
64
+ return { enumerable: false, configurable: true }
65
+ }
66
+ return Reflect.getOwnPropertyDescriptor(target, prop)
67
+ },
68
+ })
69
+
70
+ cache.set(pathKey, proxy)
71
+ return proxy
72
+ }
73
+
74
+ // Return the root proxy that starts with an empty path
75
+ return createProxy([]) as SingleRowRefProxy<T>
76
+ }
77
+
13
78
  /**
14
79
  * Creates a proxy object that records property access paths
15
80
  * Used in callbacks like where, select, etc. to create type-safe references
@@ -1,3 +1,8 @@
1
+ import {
2
+ EmptyReferencePathError,
3
+ UnknownExpressionTypeError,
4
+ UnknownFunctionError,
5
+ } from "../../errors.js"
1
6
  import type { BasicExpression, Func, PropRef } from "../ir.js"
2
7
  import type { NamespacedRow } from "../../types.js"
3
8
 
@@ -6,11 +11,37 @@ import type { NamespacedRow } from "../../types.js"
6
11
  */
7
12
  export type CompiledExpression = (namespacedRow: NamespacedRow) => any
8
13
 
14
+ /**
15
+ * Compiled single-row expression evaluator function type
16
+ */
17
+ export type CompiledSingleRowExpression = (item: Record<string, unknown>) => any
18
+
9
19
  /**
10
20
  * Compiles an expression into an optimized evaluator function.
11
21
  * This eliminates branching during evaluation by pre-compiling the expression structure.
12
22
  */
13
23
  export function compileExpression(expr: BasicExpression): CompiledExpression {
24
+ const compiledFn = compileExpressionInternal(expr, false)
25
+ return compiledFn as CompiledExpression
26
+ }
27
+
28
+ /**
29
+ * Compiles a single-row expression into an optimized evaluator function.
30
+ */
31
+ export function compileSingleRowExpression(
32
+ expr: BasicExpression
33
+ ): CompiledSingleRowExpression {
34
+ const compiledFn = compileExpressionInternal(expr, true)
35
+ return compiledFn as CompiledSingleRowExpression
36
+ }
37
+
38
+ /**
39
+ * Internal unified expression compiler that handles both namespaced and single-row evaluation
40
+ */
41
+ function compileExpressionInternal(
42
+ expr: BasicExpression,
43
+ isSingleRow: boolean
44
+ ): (data: any) => any {
14
45
  switch (expr.type) {
15
46
  case `val`: {
16
47
  // For constant values, return a function that just returns the value
@@ -19,17 +50,17 @@ export function compileExpression(expr: BasicExpression): CompiledExpression {
19
50
  }
20
51
 
21
52
  case `ref`: {
22
- // For references, pre-compile the property path navigation
23
- return compileRef(expr)
53
+ // For references, compile based on evaluation mode
54
+ return isSingleRow ? compileSingleRowRef(expr) : compileRef(expr)
24
55
  }
25
56
 
26
57
  case `func`: {
27
- // For functions, pre-compile the function and its arguments
28
- return compileFunction(expr)
58
+ // For functions, use the unified compiler
59
+ return compileFunction(expr, isSingleRow)
29
60
  }
30
61
 
31
62
  default:
32
- throw new Error(`Unknown expression type: ${(expr as any).type}`)
63
+ throw new UnknownExpressionTypeError((expr as any).type)
33
64
  }
34
65
  }
35
66
 
@@ -40,7 +71,7 @@ function compileRef(ref: PropRef): CompiledExpression {
40
71
  const [tableAlias, ...propertyPath] = ref.path
41
72
 
42
73
  if (!tableAlias) {
43
- throw new Error(`Reference path cannot be empty`)
74
+ throw new EmptyReferencePathError()
44
75
  }
45
76
 
46
77
  // Pre-compile the property path navigation
@@ -75,74 +106,95 @@ function compileRef(ref: PropRef): CompiledExpression {
75
106
  }
76
107
 
77
108
  /**
78
- * Compiles a function expression into an optimized evaluator
109
+ * Compiles a reference expression for single-row evaluation
110
+ */
111
+ function compileSingleRowRef(ref: PropRef): CompiledSingleRowExpression {
112
+ const propertyPath = ref.path
113
+
114
+ // This function works for all path lengths including empty path
115
+ return (item) => {
116
+ let value: any = item
117
+ for (const prop of propertyPath) {
118
+ if (value == null) {
119
+ return value
120
+ }
121
+ value = value[prop]
122
+ }
123
+ return value
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Compiles a function expression for both namespaced and single-row evaluation
79
129
  */
80
- function compileFunction(func: Func): CompiledExpression {
81
- // Pre-compile all arguments
82
- const compiledArgs = func.args.map(compileExpression)
130
+ function compileFunction(func: Func, isSingleRow: boolean): (data: any) => any {
131
+ // Pre-compile all arguments using the appropriate compiler
132
+ const compiledArgs = func.args.map((arg) =>
133
+ compileExpressionInternal(arg, isSingleRow)
134
+ )
83
135
 
84
136
  switch (func.name) {
85
137
  // Comparison operators
86
138
  case `eq`: {
87
139
  const argA = compiledArgs[0]!
88
140
  const argB = compiledArgs[1]!
89
- return (namespacedRow) => {
90
- const a = argA(namespacedRow)
91
- const b = argB(namespacedRow)
141
+ return (data) => {
142
+ const a = argA(data)
143
+ const b = argB(data)
92
144
  return a === b
93
145
  }
94
146
  }
95
147
  case `gt`: {
96
148
  const argA = compiledArgs[0]!
97
149
  const argB = compiledArgs[1]!
98
- return (namespacedRow) => {
99
- const a = argA(namespacedRow)
100
- const b = argB(namespacedRow)
150
+ return (data) => {
151
+ const a = argA(data)
152
+ const b = argB(data)
101
153
  return a > b
102
154
  }
103
155
  }
104
156
  case `gte`: {
105
157
  const argA = compiledArgs[0]!
106
158
  const argB = compiledArgs[1]!
107
- return (namespacedRow) => {
108
- const a = argA(namespacedRow)
109
- const b = argB(namespacedRow)
159
+ return (data) => {
160
+ const a = argA(data)
161
+ const b = argB(data)
110
162
  return a >= b
111
163
  }
112
164
  }
113
165
  case `lt`: {
114
166
  const argA = compiledArgs[0]!
115
167
  const argB = compiledArgs[1]!
116
- return (namespacedRow) => {
117
- const a = argA(namespacedRow)
118
- const b = argB(namespacedRow)
168
+ return (data) => {
169
+ const a = argA(data)
170
+ const b = argB(data)
119
171
  return a < b
120
172
  }
121
173
  }
122
174
  case `lte`: {
123
175
  const argA = compiledArgs[0]!
124
176
  const argB = compiledArgs[1]!
125
- return (namespacedRow) => {
126
- const a = argA(namespacedRow)
127
- const b = argB(namespacedRow)
177
+ return (data) => {
178
+ const a = argA(data)
179
+ const b = argB(data)
128
180
  return a <= b
129
181
  }
130
182
  }
131
183
 
132
184
  // Boolean operators
133
185
  case `and`:
134
- return (namespacedRow) => {
186
+ return (data) => {
135
187
  for (const compiledArg of compiledArgs) {
136
- if (!compiledArg(namespacedRow)) {
188
+ if (!compiledArg(data)) {
137
189
  return false
138
190
  }
139
191
  }
140
192
  return true
141
193
  }
142
194
  case `or`:
143
- return (namespacedRow) => {
195
+ return (data) => {
144
196
  for (const compiledArg of compiledArgs) {
145
- if (compiledArg(namespacedRow)) {
197
+ if (compiledArg(data)) {
146
198
  return true
147
199
  }
148
200
  }
@@ -150,16 +202,16 @@ function compileFunction(func: Func): CompiledExpression {
150
202
  }
151
203
  case `not`: {
152
204
  const arg = compiledArgs[0]!
153
- return (namespacedRow) => !arg(namespacedRow)
205
+ return (data) => !arg(data)
154
206
  }
155
207
 
156
208
  // Array operators
157
209
  case `in`: {
158
210
  const valueEvaluator = compiledArgs[0]!
159
211
  const arrayEvaluator = compiledArgs[1]!
160
- return (namespacedRow) => {
161
- const value = valueEvaluator(namespacedRow)
162
- const array = arrayEvaluator(namespacedRow)
212
+ return (data) => {
213
+ const value = valueEvaluator(data)
214
+ const array = arrayEvaluator(data)
163
215
  if (!Array.isArray(array)) {
164
216
  return false
165
217
  }
@@ -171,18 +223,18 @@ function compileFunction(func: Func): CompiledExpression {
171
223
  case `like`: {
172
224
  const valueEvaluator = compiledArgs[0]!
173
225
  const patternEvaluator = compiledArgs[1]!
174
- return (namespacedRow) => {
175
- const value = valueEvaluator(namespacedRow)
176
- const pattern = patternEvaluator(namespacedRow)
226
+ return (data) => {
227
+ const value = valueEvaluator(data)
228
+ const pattern = patternEvaluator(data)
177
229
  return evaluateLike(value, pattern, false)
178
230
  }
179
231
  }
180
232
  case `ilike`: {
181
233
  const valueEvaluator = compiledArgs[0]!
182
234
  const patternEvaluator = compiledArgs[1]!
183
- return (namespacedRow) => {
184
- const value = valueEvaluator(namespacedRow)
185
- const pattern = patternEvaluator(namespacedRow)
235
+ return (data) => {
236
+ const value = valueEvaluator(data)
237
+ const pattern = patternEvaluator(data)
186
238
  return evaluateLike(value, pattern, true)
187
239
  }
188
240
  }
@@ -190,22 +242,22 @@ function compileFunction(func: Func): CompiledExpression {
190
242
  // String functions
191
243
  case `upper`: {
192
244
  const arg = compiledArgs[0]!
193
- return (namespacedRow) => {
194
- const value = arg(namespacedRow)
245
+ return (data) => {
246
+ const value = arg(data)
195
247
  return typeof value === `string` ? value.toUpperCase() : value
196
248
  }
197
249
  }
198
250
  case `lower`: {
199
251
  const arg = compiledArgs[0]!
200
- return (namespacedRow) => {
201
- const value = arg(namespacedRow)
252
+ return (data) => {
253
+ const value = arg(data)
202
254
  return typeof value === `string` ? value.toLowerCase() : value
203
255
  }
204
256
  }
205
257
  case `length`: {
206
258
  const arg = compiledArgs[0]!
207
- return (namespacedRow) => {
208
- const value = arg(namespacedRow)
259
+ return (data) => {
260
+ const value = arg(data)
209
261
  if (typeof value === `string`) {
210
262
  return value.length
211
263
  }
@@ -216,10 +268,10 @@ function compileFunction(func: Func): CompiledExpression {
216
268
  }
217
269
  }
218
270
  case `concat`:
219
- return (namespacedRow) => {
271
+ return (data) => {
220
272
  return compiledArgs
221
273
  .map((evaluator) => {
222
- const arg = evaluator(namespacedRow)
274
+ const arg = evaluator(data)
223
275
  try {
224
276
  return String(arg ?? ``)
225
277
  } catch {
@@ -233,9 +285,9 @@ function compileFunction(func: Func): CompiledExpression {
233
285
  .join(``)
234
286
  }
235
287
  case `coalesce`:
236
- return (namespacedRow) => {
288
+ return (data) => {
237
289
  for (const evaluator of compiledArgs) {
238
- const value = evaluator(namespacedRow)
290
+ const value = evaluator(data)
239
291
  if (value !== null && value !== undefined) {
240
292
  return value
241
293
  }
@@ -247,43 +299,43 @@ function compileFunction(func: Func): CompiledExpression {
247
299
  case `add`: {
248
300
  const argA = compiledArgs[0]!
249
301
  const argB = compiledArgs[1]!
250
- return (namespacedRow) => {
251
- const a = argA(namespacedRow)
252
- const b = argB(namespacedRow)
302
+ return (data) => {
303
+ const a = argA(data)
304
+ const b = argB(data)
253
305
  return (a ?? 0) + (b ?? 0)
254
306
  }
255
307
  }
256
308
  case `subtract`: {
257
309
  const argA = compiledArgs[0]!
258
310
  const argB = compiledArgs[1]!
259
- return (namespacedRow) => {
260
- const a = argA(namespacedRow)
261
- const b = argB(namespacedRow)
311
+ return (data) => {
312
+ const a = argA(data)
313
+ const b = argB(data)
262
314
  return (a ?? 0) - (b ?? 0)
263
315
  }
264
316
  }
265
317
  case `multiply`: {
266
318
  const argA = compiledArgs[0]!
267
319
  const argB = compiledArgs[1]!
268
- return (namespacedRow) => {
269
- const a = argA(namespacedRow)
270
- const b = argB(namespacedRow)
320
+ return (data) => {
321
+ const a = argA(data)
322
+ const b = argB(data)
271
323
  return (a ?? 0) * (b ?? 0)
272
324
  }
273
325
  }
274
326
  case `divide`: {
275
327
  const argA = compiledArgs[0]!
276
328
  const argB = compiledArgs[1]!
277
- return (namespacedRow) => {
278
- const a = argA(namespacedRow)
279
- const b = argB(namespacedRow)
329
+ return (data) => {
330
+ const a = argA(data)
331
+ const b = argB(data)
280
332
  const divisor = b ?? 0
281
333
  return divisor !== 0 ? (a ?? 0) / divisor : null
282
334
  }
283
335
  }
284
336
 
285
337
  default:
286
- throw new Error(`Unknown function: ${func.name}`)
338
+ throw new UnknownFunctionError(func.name)
287
339
  }
288
340
  }
289
341
 
@@ -0,0 +1,92 @@
1
+ import { Func, PropRef, Value } from "../ir.js"
2
+ import type { BasicExpression } from "../ir.js"
3
+
4
+ /**
5
+ * Functions supported by the collection index system.
6
+ * These are the only functions that can be used in WHERE clauses
7
+ * that are pushed down to collection subscriptions for index optimization.
8
+ */
9
+ export const SUPPORTED_COLLECTION_FUNCS = new Set([
10
+ `eq`,
11
+ `gt`,
12
+ `lt`,
13
+ `gte`,
14
+ `lte`,
15
+ `and`,
16
+ `or`,
17
+ `in`,
18
+ ])
19
+
20
+ /**
21
+ * Determines if a WHERE clause can be converted to collection-compatible BasicExpression format.
22
+ * This checks if the expression only uses functions supported by the collection index system.
23
+ *
24
+ * @param whereClause - The WHERE clause to check
25
+ * @returns True if the clause can be converted for collection index optimization
26
+ */
27
+ export function isConvertibleToCollectionFilter(
28
+ whereClause: BasicExpression<boolean>
29
+ ): boolean {
30
+ const tpe = whereClause.type
31
+ if (tpe === `func`) {
32
+ // Check if this function is supported
33
+ if (!SUPPORTED_COLLECTION_FUNCS.has(whereClause.name)) {
34
+ return false
35
+ }
36
+ // Recursively check all arguments
37
+ return whereClause.args.every((arg) =>
38
+ isConvertibleToCollectionFilter(arg as BasicExpression<boolean>)
39
+ )
40
+ }
41
+ return [`val`, `ref`].includes(tpe)
42
+ }
43
+
44
+ /**
45
+ * Converts a WHERE clause to BasicExpression format compatible with collection indexes.
46
+ * This function creates proper BasicExpression class instances that the collection
47
+ * index system can understand.
48
+ *
49
+ * @param whereClause - The WHERE clause to convert
50
+ * @param collectionAlias - The alias of the collection being filtered
51
+ * @returns The converted BasicExpression or null if conversion fails
52
+ */
53
+ export function convertToBasicExpression(
54
+ whereClause: BasicExpression<boolean>,
55
+ collectionAlias: string
56
+ ): BasicExpression<boolean> | null {
57
+ const tpe = whereClause.type
58
+ if (tpe === `val`) {
59
+ return new Value(whereClause.value)
60
+ } else if (tpe === `ref`) {
61
+ const path = whereClause.path
62
+ if (Array.isArray(path)) {
63
+ if (path[0] === collectionAlias && path.length > 1) {
64
+ // Remove the table alias from the path for single-collection queries
65
+ return new PropRef(path.slice(1))
66
+ } else if (path.length === 1 && path[0] !== undefined) {
67
+ // Single field reference
68
+ return new PropRef([path[0]])
69
+ }
70
+ }
71
+ // Fallback for non-array paths
72
+ return new PropRef(Array.isArray(path) ? path : [String(path)])
73
+ } else {
74
+ // Check if this function is supported
75
+ if (!SUPPORTED_COLLECTION_FUNCS.has(whereClause.name)) {
76
+ return null
77
+ }
78
+ // Recursively convert all arguments
79
+ const args: Array<BasicExpression> = []
80
+ for (const arg of whereClause.args) {
81
+ const convertedArg = convertToBasicExpression(
82
+ arg as BasicExpression<boolean>,
83
+ collectionAlias
84
+ )
85
+ if (convertedArg == null) {
86
+ return null
87
+ }
88
+ args.push(convertedArg)
89
+ }
90
+ return new Func(whereClause.name, args)
91
+ }
92
+ }