@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
@@ -1,5 +1,11 @@
1
1
  import { filter, groupBy, groupByOperators, map } from "@electric-sql/d2mini"
2
2
  import { Func, PropRef } from "../ir.js"
3
+ import {
4
+ AggregateFunctionNotInSelectError,
5
+ NonAggregateExpressionNotInGroupByError,
6
+ UnknownHavingExpressionTypeError,
7
+ UnsupportedAggregateFunctionError,
8
+ } from "../../errors.js"
3
9
  import { compileExpression } from "./evaluators.js"
4
10
  import type {
5
11
  Aggregate,
@@ -48,9 +54,7 @@ function validateAndCreateMapping(
48
54
  )
49
55
 
50
56
  if (groupIndex === -1) {
51
- throw new Error(
52
- `Non-aggregate expression '${alias}' in SELECT must also appear in GROUP BY clause`
53
- )
57
+ throw new NonAggregateExpressionNotInGroupByError(alias)
54
58
  }
55
59
 
56
60
  // Cache the mapping
@@ -354,7 +358,7 @@ function getAggregateFunction(aggExpr: Aggregate) {
354
358
  case `max`:
355
359
  return max(valueExtractor)
356
360
  default:
357
- throw new Error(`Unsupported aggregate function: ${aggExpr.name}`)
361
+ throw new UnsupportedAggregateFunctionError(aggExpr.name)
358
362
  }
359
363
  }
360
364
 
@@ -376,9 +380,7 @@ function transformHavingClause(
376
380
  }
377
381
  }
378
382
  // If no matching aggregate found in SELECT, throw error
379
- throw new Error(
380
- `Aggregate function in HAVING clause must also be in SELECT clause: ${aggExpr.name}`
381
- )
383
+ throw new AggregateFunctionNotInSelectError(aggExpr.name)
382
384
  }
383
385
 
384
386
  case `func`: {
@@ -410,9 +412,7 @@ function transformHavingClause(
410
412
  return havingExpr as BasicExpression
411
413
 
412
414
  default:
413
- throw new Error(
414
- `Unknown expression type in HAVING clause: ${(havingExpr as any).type}`
415
- )
415
+ throw new UnknownHavingExpressionTypeError((havingExpr as any).type)
416
416
  }
417
417
  }
418
418
 
@@ -1,11 +1,23 @@
1
1
  import { distinct, filter, map } from "@electric-sql/d2mini"
2
2
  import { optimizeQuery } from "../optimizer.js"
3
+ import {
4
+ CollectionInputNotFoundError,
5
+ DistinctRequiresSelectError,
6
+ HavingRequiresGroupByError,
7
+ LimitOffsetRequireOrderByError,
8
+ UnsupportedFromTypeError,
9
+ } from "../../errors.js"
3
10
  import { compileExpression } from "./evaluators.js"
4
11
  import { processJoins } from "./joins.js"
5
12
  import { processGroupBy } from "./group-by.js"
6
13
  import { processOrderBy } from "./order-by.js"
7
14
  import { processSelectToResults } from "./select.js"
8
- import type { CollectionRef, QueryIR, QueryRef } from "../ir.js"
15
+ import type {
16
+ BasicExpression,
17
+ CollectionRef,
18
+ QueryIR,
19
+ QueryRef,
20
+ } from "../ir.js"
9
21
  import type {
10
22
  KeyedStream,
11
23
  NamespacedAndKeyedStream,
@@ -13,20 +25,30 @@ import type {
13
25
  } from "../../types.js"
14
26
  import type { QueryCache, QueryMapping } from "./types.js"
15
27
 
28
+ /**
29
+ * Result of query compilation including both the pipeline and collection-specific WHERE clauses
30
+ */
31
+ export interface CompilationResult {
32
+ /** The compiled query pipeline */
33
+ pipeline: ResultStream
34
+ /** Map of collection aliases to their WHERE clauses for index optimization */
35
+ collectionWhereClauses: Map<string, BasicExpression<boolean>>
36
+ }
37
+
16
38
  /**
17
39
  * Compiles a query2 IR into a D2 pipeline
18
40
  * @param rawQuery The query IR to compile
19
41
  * @param inputs Mapping of collection names to input streams
20
42
  * @param cache Optional cache for compiled subqueries (used internally for recursion)
21
43
  * @param queryMapping Optional mapping from optimized queries to original queries
22
- * @returns A stream builder representing the compiled query
44
+ * @returns A CompilationResult with the pipeline and collection WHERE clauses
23
45
  */
24
46
  export function compileQuery(
25
47
  rawQuery: QueryIR,
26
48
  inputs: Record<string, KeyedStream>,
27
49
  cache: QueryCache = new WeakMap(),
28
50
  queryMapping: QueryMapping = new WeakMap()
29
- ): ResultStream {
51
+ ): CompilationResult {
30
52
  // Check if the original raw query has already been compiled
31
53
  const cachedResult = cache.get(rawQuery)
32
54
  if (cachedResult) {
@@ -34,7 +56,8 @@ export function compileQuery(
34
56
  }
35
57
 
36
58
  // Optimize the query before compilation
37
- const query = optimizeQuery(rawQuery)
59
+ const { optimizedQuery: query, collectionWhereClauses } =
60
+ optimizeQuery(rawQuery)
38
61
 
39
62
  // Create mapping from optimized query to original for caching
40
63
  queryMapping.set(query, rawQuery)
@@ -82,11 +105,9 @@ export function compileQuery(
82
105
 
83
106
  // Process the WHERE clause if it exists
84
107
  if (query.where && query.where.length > 0) {
85
- // Compile all WHERE expressions
86
- const compiledWheres = query.where.map((where) => compileExpression(where))
87
-
88
108
  // Apply each WHERE condition as a filter (they are ANDed together)
89
- for (const compiledWhere of compiledWheres) {
109
+ for (const where of query.where) {
110
+ const compiledWhere = compileExpression(where)
90
111
  pipeline = pipeline.pipe(
91
112
  filter(([_key, namespacedRow]) => {
92
113
  return compiledWhere(namespacedRow)
@@ -107,7 +128,7 @@ export function compileQuery(
107
128
  }
108
129
 
109
130
  if (query.distinct && !query.fnSelect && !query.select) {
110
- throw new Error(`DISTINCT requires a SELECT clause.`)
131
+ throw new DistinctRequiresSelectError()
111
132
  }
112
133
 
113
134
  // Process the SELECT clause early - always create __select_results
@@ -182,7 +203,7 @@ export function compileQuery(
182
203
  : false
183
204
 
184
205
  if (!hasAggregates) {
185
- throw new Error(`HAVING clause requires GROUP BY clause`)
206
+ throw new HavingRequiresGroupByError()
186
207
  }
187
208
  }
188
209
 
@@ -227,13 +248,16 @@ export function compileQuery(
227
248
 
228
249
  const result = resultPipeline
229
250
  // Cache the result before returning (use original query as key)
230
- cache.set(rawQuery, result)
231
- return result
251
+ const compilationResult = {
252
+ pipeline: result,
253
+ collectionWhereClauses,
254
+ }
255
+ cache.set(rawQuery, compilationResult)
256
+
257
+ return compilationResult
232
258
  } else if (query.limit !== undefined || query.offset !== undefined) {
233
259
  // If there's a limit or offset without orderBy, throw an error
234
- throw new Error(
235
- `LIMIT and OFFSET require an ORDER BY clause to ensure deterministic results`
236
- )
260
+ throw new LimitOffsetRequireOrderByError()
237
261
  }
238
262
 
239
263
  // Final step: extract the __select_results and return tuple format (no orderBy)
@@ -250,8 +274,13 @@ export function compileQuery(
250
274
 
251
275
  const result = resultPipeline
252
276
  // Cache the result before returning (use original query as key)
253
- cache.set(rawQuery, result)
254
- return result
277
+ const compilationResult = {
278
+ pipeline: result,
279
+ collectionWhereClauses,
280
+ }
281
+ cache.set(rawQuery, compilationResult)
282
+
283
+ return compilationResult
255
284
  }
256
285
 
257
286
  /**
@@ -267,9 +296,7 @@ function processFrom(
267
296
  case `collectionRef`: {
268
297
  const input = allInputs[from.collection.id]
269
298
  if (!input) {
270
- throw new Error(
271
- `Input for collection "${from.collection.id}" not found in inputs map`
272
- )
299
+ throw new CollectionInputNotFoundError(from.collection.id)
273
300
  }
274
301
  return { alias: from.alias, input }
275
302
  }
@@ -278,13 +305,16 @@ function processFrom(
278
305
  const originalQuery = queryMapping.get(from.query) || from.query
279
306
 
280
307
  // Recursively compile the sub-query with cache
281
- const subQueryInput = compileQuery(
308
+ const subQueryResult = compileQuery(
282
309
  originalQuery,
283
310
  allInputs,
284
311
  cache,
285
312
  queryMapping
286
313
  )
287
314
 
315
+ // Extract the pipeline from the compilation result
316
+ const subQueryInput = subQueryResult.pipeline
317
+
288
318
  // Subqueries may return [key, [value, orderByIndex]] (with ORDER BY) or [key, value] (without ORDER BY)
289
319
  // We need to extract just the value for use in parent queries
290
320
  const extractedInput = subQueryInput.pipe(
@@ -297,7 +327,7 @@ function processFrom(
297
327
  return { alias: from.alias, input: extractedInput }
298
328
  }
299
329
  default:
300
- throw new Error(`Unsupported FROM type: ${(from as any).type}`)
330
+ throw new UnsupportedFromTypeError((from as any).type)
301
331
  }
302
332
  }
303
333
 
@@ -4,10 +4,23 @@ import {
4
4
  join as joinOperator,
5
5
  map,
6
6
  } from "@electric-sql/d2mini"
7
+ import {
8
+ CollectionInputNotFoundError,
9
+ InvalidJoinConditionSameTableError,
10
+ InvalidJoinConditionTableMismatchError,
11
+ InvalidJoinConditionWrongTablesError,
12
+ UnsupportedJoinSourceTypeError,
13
+ UnsupportedJoinTypeError,
14
+ } from "../../errors.js"
7
15
  import { compileExpression } from "./evaluators.js"
8
16
  import { compileQuery } from "./index.js"
9
17
  import type { IStreamBuilder, JoinType } from "@electric-sql/d2mini"
10
- import type { CollectionRef, JoinClause, QueryRef } from "../ir.js"
18
+ import type {
19
+ BasicExpression,
20
+ CollectionRef,
21
+ JoinClause,
22
+ QueryRef,
23
+ } from "../ir.js"
11
24
  import type {
12
25
  KeyedStream,
13
26
  NamespacedAndKeyedStream,
@@ -75,18 +88,26 @@ function processJoin(
75
88
  ? `full`
76
89
  : (joinClause.type as JoinType)
77
90
 
91
+ // Analyze which table each expression refers to and swap if necessary
92
+ const { mainExpr, joinedExpr } = analyzeJoinExpressions(
93
+ joinClause.left,
94
+ joinClause.right,
95
+ mainTableAlias,
96
+ joinedTableAlias
97
+ )
98
+
78
99
  // Pre-compile the join expressions
79
- const compiledLeftExpr = compileExpression(joinClause.left)
80
- const compiledRightExpr = compileExpression(joinClause.right)
100
+ const compiledMainExpr = compileExpression(mainExpr)
101
+ const compiledJoinedExpr = compileExpression(joinedExpr)
81
102
 
82
103
  // Prepare the main pipeline for joining
83
104
  const mainPipeline = pipeline.pipe(
84
105
  map(([currentKey, namespacedRow]) => {
85
- // Extract the join key from the left side of the join condition
86
- const leftKey = compiledLeftExpr(namespacedRow)
106
+ // Extract the join key from the main table expression
107
+ const mainKey = compiledMainExpr(namespacedRow)
87
108
 
88
109
  // Return [joinKey, [originalKey, namespacedRow]]
89
- return [leftKey, [currentKey, namespacedRow]] as [
110
+ return [mainKey, [currentKey, namespacedRow]] as [
90
111
  unknown,
91
112
  [string, typeof namespacedRow],
92
113
  ]
@@ -99,11 +120,11 @@ function processJoin(
99
120
  // Wrap the row in a namespaced structure
100
121
  const namespacedRow: NamespacedRow = { [joinedTableAlias]: row }
101
122
 
102
- // Extract the join key from the right side of the join condition
103
- const rightKey = compiledRightExpr(namespacedRow)
123
+ // Extract the join key from the joined table expression
124
+ const joinedKey = compiledJoinedExpr(namespacedRow)
104
125
 
105
126
  // Return [joinKey, [originalKey, namespacedRow]]
106
- return [rightKey, [currentKey, namespacedRow]] as [
127
+ return [joinedKey, [currentKey, namespacedRow]] as [
107
128
  unknown,
108
129
  [string, typeof namespacedRow],
109
130
  ]
@@ -112,7 +133,7 @@ function processJoin(
112
133
 
113
134
  // Apply the join operation
114
135
  if (![`inner`, `left`, `right`, `full`].includes(joinType)) {
115
- throw new Error(`Unsupported join type: ${joinClause.type}`)
136
+ throw new UnsupportedJoinTypeError(joinClause.type)
116
137
  }
117
138
  return mainPipeline.pipe(
118
139
  joinOperator(joinedPipeline, joinType),
@@ -121,6 +142,83 @@ function processJoin(
121
142
  )
122
143
  }
123
144
 
145
+ /**
146
+ * Analyzes join expressions to determine which refers to which table
147
+ * and returns them in the correct order (main table expression first, joined table expression second)
148
+ */
149
+ function analyzeJoinExpressions(
150
+ left: BasicExpression,
151
+ right: BasicExpression,
152
+ mainTableAlias: string,
153
+ joinedTableAlias: string
154
+ ): { mainExpr: BasicExpression; joinedExpr: BasicExpression } {
155
+ const leftTableAlias = getTableAliasFromExpression(left)
156
+ const rightTableAlias = getTableAliasFromExpression(right)
157
+
158
+ // If left expression refers to main table and right refers to joined table, keep as is
159
+ if (
160
+ leftTableAlias === mainTableAlias &&
161
+ rightTableAlias === joinedTableAlias
162
+ ) {
163
+ return { mainExpr: left, joinedExpr: right }
164
+ }
165
+
166
+ // If left expression refers to joined table and right refers to main table, swap them
167
+ if (
168
+ leftTableAlias === joinedTableAlias &&
169
+ rightTableAlias === mainTableAlias
170
+ ) {
171
+ return { mainExpr: right, joinedExpr: left }
172
+ }
173
+
174
+ // If both expressions refer to the same alias, this is an invalid join
175
+ if (leftTableAlias === rightTableAlias) {
176
+ throw new InvalidJoinConditionSameTableError(leftTableAlias || `unknown`)
177
+ }
178
+
179
+ // If one expression doesn't refer to either table, this is an invalid join
180
+ if (!leftTableAlias || !rightTableAlias) {
181
+ throw new InvalidJoinConditionTableMismatchError(
182
+ mainTableAlias,
183
+ joinedTableAlias
184
+ )
185
+ }
186
+
187
+ // If expressions refer to tables not involved in this join, this is an invalid join
188
+ throw new InvalidJoinConditionWrongTablesError(
189
+ leftTableAlias,
190
+ rightTableAlias,
191
+ mainTableAlias,
192
+ joinedTableAlias
193
+ )
194
+ }
195
+
196
+ /**
197
+ * Extracts the table alias from a join expression
198
+ */
199
+ function getTableAliasFromExpression(expr: BasicExpression): string | null {
200
+ switch (expr.type) {
201
+ case `ref`:
202
+ // PropRef path has the table alias as the first element
203
+ return expr.path[0] || null
204
+ case `func`: {
205
+ // For function expressions, we need to check if all arguments refer to the same table
206
+ const tableAliases = new Set<string>()
207
+ for (const arg of expr.args) {
208
+ const alias = getTableAliasFromExpression(arg)
209
+ if (alias) {
210
+ tableAliases.add(alias)
211
+ }
212
+ }
213
+ // If all arguments refer to the same table, return that table alias
214
+ return tableAliases.size === 1 ? Array.from(tableAliases)[0]! : null
215
+ }
216
+ default:
217
+ // Values (type='val') don't reference any table
218
+ return null
219
+ }
220
+ }
221
+
124
222
  /**
125
223
  * Processes the join source (collection or sub-query)
126
224
  */
@@ -134,9 +232,7 @@ function processJoinSource(
134
232
  case `collectionRef`: {
135
233
  const input = allInputs[from.collection.id]
136
234
  if (!input) {
137
- throw new Error(
138
- `Input for collection "${from.collection.id}" not found in inputs map`
139
- )
235
+ throw new CollectionInputNotFoundError(from.collection.id)
140
236
  }
141
237
  return { alias: from.alias, input }
142
238
  }
@@ -145,13 +241,16 @@ function processJoinSource(
145
241
  const originalQuery = queryMapping.get(from.query) || from.query
146
242
 
147
243
  // Recursively compile the sub-query with cache
148
- const subQueryInput = compileQuery(
244
+ const subQueryResult = compileQuery(
149
245
  originalQuery,
150
246
  allInputs,
151
247
  cache,
152
248
  queryMapping
153
249
  )
154
250
 
251
+ // Extract the pipeline from the compilation result
252
+ const subQueryInput = subQueryResult.pipeline
253
+
155
254
  // Subqueries may return [key, [value, orderByIndex]] (with ORDER BY) or [key, value] (without ORDER BY)
156
255
  // We need to extract just the value for use in parent queries
157
256
  const extractedInput = subQueryInput.pipe(
@@ -164,7 +263,7 @@ function processJoinSource(
164
263
  return { alias: from.alias, input: extractedInput as KeyedStream }
165
264
  }
166
265
  default:
167
- throw new Error(`Unsupported join source type: ${(from as any).type}`)
266
+ throw new UnsupportedJoinSourceTypeError((from as any).type)
168
267
  }
169
268
  }
170
269
 
@@ -1,4 +1,5 @@
1
1
  import { orderByWithFractionalIndex } from "@electric-sql/d2mini"
2
+ import { ascComparator, descComparator } from "../../utils/comparison.js"
2
3
  import { compileExpression } from "./evaluators.js"
3
4
  import type { OrderByClause } from "../ir.js"
4
5
  import type { NamespacedAndKeyedStream, NamespacedRow } from "../../types.js"
@@ -51,51 +52,6 @@ export function processOrderBy(
51
52
  return null
52
53
  }
53
54
 
54
- // Create comparator functions
55
- const ascComparator = (a: any, b: any): number => {
56
- // Handle null/undefined
57
- if (a == null && b == null) return 0
58
- if (a == null) return -1
59
- if (b == null) return 1
60
-
61
- // if a and b are both strings, compare them based on locale
62
- if (typeof a === `string` && typeof b === `string`) {
63
- return a.localeCompare(b)
64
- }
65
-
66
- // if a and b are both arrays, compare them element by element
67
- if (Array.isArray(a) && Array.isArray(b)) {
68
- for (let i = 0; i < Math.min(a.length, b.length); i++) {
69
- const result = ascComparator(a[i], b[i])
70
- if (result !== 0) {
71
- return result
72
- }
73
- }
74
- // All elements are equal up to the minimum length
75
- return a.length - b.length
76
- }
77
-
78
- // If both are dates, compare them
79
- if (a instanceof Date && b instanceof Date) {
80
- return a.getTime() - b.getTime()
81
- }
82
-
83
- // If at least one of the values is an object, convert to strings
84
- const bothObjects = typeof a === `object` && typeof b === `object`
85
- const notNull = a !== null && b !== null
86
- if (bothObjects && notNull) {
87
- return a.toString().localeCompare(b.toString())
88
- }
89
-
90
- if (a < b) return -1
91
- if (a > b) return 1
92
- return 0
93
- }
94
-
95
- const descComparator = (a: unknown, b: unknown): number => {
96
- return ascComparator(b, a)
97
- }
98
-
99
55
  // Create a multi-property comparator that respects the order and direction of each property
100
56
  const makeComparator = () => {
101
57
  return (a: unknown, b: unknown) => {
@@ -1,10 +1,10 @@
1
1
  import type { QueryIR } from "../ir.js"
2
- import type { ResultStream } from "../../types.js"
2
+ import type { CompilationResult } from "./index.js"
3
3
 
4
4
  /**
5
5
  * Cache for compiled subqueries to avoid duplicate compilation
6
6
  */
7
- export type QueryCache = WeakMap<QueryIR, ResultStream>
7
+ export type QueryCache = WeakMap<QueryIR, CompilationResult>
8
8
 
9
9
  /**
10
10
  * Mapping from optimized queries back to their original queries for caching
@@ -2,6 +2,7 @@ import { D2, MultiSet, output } from "@electric-sql/d2mini"
2
2
  import { createCollection } from "../collection.js"
3
3
  import { compileQuery } from "./compiler/index.js"
4
4
  import { buildQuery, getQueryIR } from "./builder/index.js"
5
+ import { convertToBasicExpression } from "./compiler/expressions.js"
5
6
  import type { InitialQueryBuilder, QueryBuilder } from "./builder/index.js"
6
7
  import type { Collection } from "../collection.js"
7
8
  import type {
@@ -14,6 +15,7 @@ import type {
14
15
  } from "../types.js"
15
16
  import type { Context, GetResult } from "./builder/types.js"
16
17
  import type { MultiSetArray, RootStreamBuilder } from "@electric-sql/d2mini"
18
+ import type { BasicExpression } from "./ir.js"
17
19
 
18
20
  // Global counter for auto-generated collection IDs
19
21
  let liveQueryCollectionCounter = 0
@@ -170,6 +172,9 @@ export function liveQueryCollectionOptions<
170
172
  let graphCache: D2 | undefined
171
173
  let inputsCache: Record<string, RootStreamBuilder<unknown>> | undefined
172
174
  let pipelineCache: ResultStream | undefined
175
+ let collectionWhereClausesCache:
176
+ | Map<string, BasicExpression<boolean>>
177
+ | undefined
173
178
 
174
179
  const compileBasePipeline = () => {
175
180
  graphCache = new D2()
@@ -179,10 +184,12 @@ export function liveQueryCollectionOptions<
179
184
  graphCache!.newInput<any>(),
180
185
  ])
181
186
  )
182
- pipelineCache = compileQuery(
183
- query,
184
- inputsCache as Record<string, KeyedStream>
185
- )
187
+
188
+ // Compile the query and get both pipeline and collection WHERE clauses
189
+ ;({
190
+ pipeline: pipelineCache,
191
+ collectionWhereClauses: collectionWhereClausesCache,
192
+ } = compileQuery(query, inputsCache as Record<string, KeyedStream>))
186
193
  }
187
194
 
188
195
  const maybeCompileBasePipeline = () => {
@@ -303,19 +310,54 @@ export function liveQueryCollectionOptions<
303
310
  // Unsubscribe callbacks
304
311
  const unsubscribeCallbacks = new Set<() => void>()
305
312
 
306
- // Set up data flow from input collections to the compiled query
313
+ // Subscribe to all collections, using WHERE clause optimization when available
307
314
  Object.entries(collections).forEach(([collectionId, collection]) => {
308
315
  const input = inputs[collectionId]!
309
-
310
- // Subscribe to changes
311
- const unsubscribe = collection.subscribeChanges(
312
- (changes: Array<ChangeMessage>) => {
313
- sendChangesToInput(input, changes, collection.config.getKey)
314
- maybeRunGraph()
315
- },
316
- { includeInitialState: true }
317
- )
318
- unsubscribeCallbacks.add(unsubscribe)
316
+ const collectionAlias = findCollectionAlias(collectionId, query)
317
+ const whereClause =
318
+ collectionAlias && collectionWhereClausesCache
319
+ ? collectionWhereClausesCache.get(collectionAlias)
320
+ : undefined
321
+
322
+ if (whereClause) {
323
+ // Convert WHERE clause to BasicExpression format for collection subscription
324
+ const whereExpression = convertToBasicExpression(
325
+ whereClause,
326
+ collectionAlias!
327
+ )
328
+
329
+ if (whereExpression) {
330
+ // Use index optimization for this collection
331
+ const subscription = collection.subscribeChanges(
332
+ (changes) => {
333
+ sendChangesToInput(input, changes, collection.config.getKey)
334
+ maybeRunGraph()
335
+ },
336
+ {
337
+ includeInitialState: true,
338
+ whereExpression: whereExpression,
339
+ }
340
+ )
341
+ unsubscribeCallbacks.add(subscription)
342
+ } else {
343
+ // This should not happen - if we have a whereClause but can't create whereExpression,
344
+ // it indicates a bug in our optimization logic
345
+ throw new Error(
346
+ `Failed to convert WHERE clause to collection filter for collection '${collectionId}'. ` +
347
+ `This indicates a bug in the query optimization logic.`
348
+ )
349
+ }
350
+ } else {
351
+ // No WHERE clause for this collection, use regular subscription
352
+ const subscription = collection.subscribeChanges(
353
+ (changes) => {
354
+ sendChangesToInput(input, changes, collection.config.getKey)
355
+ maybeRunGraph()
356
+ },
357
+ { includeInitialState: true }
358
+ )
359
+ unsubscribeCallbacks.add(subscription)
360
+ }
319
361
  })
320
362
 
321
363
  // Initial run
@@ -513,3 +555,41 @@ function extractCollectionsFromQuery(
513
555
 
514
556
  return collections
515
557
  }
558
+
559
+ /**
560
+ * Converts WHERE expressions from the query IR into a BasicExpression for subscribeChanges
561
+ *
562
+ * @param whereExpressions Array of WHERE expressions to convert
563
+ * @param tableAlias The table alias used in the expressions
564
+ * @returns A BasicExpression that can be used with the collection's index system
565
+ */
566
+
567
+ /**
568
+ * Finds the alias for a collection ID in the query
569
+ */
570
+ function findCollectionAlias(
571
+ collectionId: string,
572
+ query: any
573
+ ): string | undefined {
574
+ // Check FROM clause
575
+ if (
576
+ query.from?.type === `collectionRef` &&
577
+ query.from.collection?.id === collectionId
578
+ ) {
579
+ return query.from.alias
580
+ }
581
+
582
+ // Check JOIN clauses
583
+ if (query.join) {
584
+ for (const joinClause of query.join) {
585
+ if (
586
+ joinClause.from?.type === `collectionRef` &&
587
+ joinClause.from.collection?.id === collectionId
588
+ ) {
589
+ return joinClause.from.alias
590
+ }
591
+ }
592
+ }
593
+
594
+ return undefined
595
+ }