@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.
- package/dist/cjs/change-events.cjs +141 -0
- package/dist/cjs/change-events.cjs.map +1 -0
- package/dist/cjs/change-events.d.cts +49 -0
- package/dist/cjs/collection.cjs +234 -86
- package/dist/cjs/collection.cjs.map +1 -1
- package/dist/cjs/collection.d.cts +95 -20
- package/dist/cjs/errors.cjs +509 -1
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +225 -1
- package/dist/cjs/index.cjs +82 -3
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +5 -1
- package/dist/cjs/indexes/auto-index.cjs +64 -0
- package/dist/cjs/indexes/auto-index.cjs.map +1 -0
- package/dist/cjs/indexes/auto-index.d.cts +9 -0
- package/dist/cjs/indexes/base-index.cjs +46 -0
- package/dist/cjs/indexes/base-index.cjs.map +1 -0
- package/dist/cjs/indexes/base-index.d.cts +54 -0
- package/dist/cjs/indexes/btree-index.cjs +191 -0
- package/dist/cjs/indexes/btree-index.cjs.map +1 -0
- package/dist/cjs/indexes/btree-index.d.cts +74 -0
- package/dist/cjs/indexes/index-options.d.cts +13 -0
- package/dist/cjs/indexes/lazy-index.cjs +193 -0
- package/dist/cjs/indexes/lazy-index.cjs.map +1 -0
- package/dist/cjs/indexes/lazy-index.d.cts +96 -0
- package/dist/cjs/local-storage.cjs +9 -15
- package/dist/cjs/local-storage.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.cjs +11 -0
- package/dist/cjs/query/builder/functions.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.d.cts +4 -0
- package/dist/cjs/query/builder/index.cjs +6 -7
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/builder/ref-proxy.cjs +37 -0
- package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
- package/dist/cjs/query/builder/ref-proxy.d.cts +12 -0
- package/dist/cjs/query/compiler/evaluators.cjs +83 -58
- package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
- package/dist/cjs/query/compiler/evaluators.d.cts +8 -0
- package/dist/cjs/query/compiler/expressions.cjs +61 -0
- package/dist/cjs/query/compiler/expressions.cjs.map +1 -0
- package/dist/cjs/query/compiler/expressions.d.cts +25 -0
- package/dist/cjs/query/compiler/group-by.cjs +5 -10
- package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.cjs +23 -17
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.d.cts +12 -3
- package/dist/cjs/query/compiler/joins.cjs +61 -12
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.cjs +4 -34
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/types.d.cts +2 -2
- package/dist/cjs/query/live-query-collection.cjs +54 -12
- package/dist/cjs/query/live-query-collection.cjs.map +1 -1
- package/dist/cjs/query/optimizer.cjs +45 -7
- package/dist/cjs/query/optimizer.cjs.map +1 -1
- package/dist/cjs/query/optimizer.d.cts +13 -3
- package/dist/cjs/transactions.cjs +5 -4
- package/dist/cjs/transactions.cjs.map +1 -1
- package/dist/cjs/types.d.cts +31 -0
- package/dist/cjs/utils/array-utils.d.cts +8 -0
- package/dist/cjs/utils/btree.cjs +677 -0
- package/dist/cjs/utils/btree.cjs.map +1 -0
- package/dist/cjs/utils/btree.d.cts +197 -0
- package/dist/cjs/utils/comparison.cjs +52 -0
- package/dist/cjs/utils/comparison.cjs.map +1 -0
- package/dist/cjs/utils/comparison.d.cts +11 -0
- package/dist/cjs/utils/index-optimization.cjs +270 -0
- package/dist/cjs/utils/index-optimization.cjs.map +1 -0
- package/dist/cjs/utils/index-optimization.d.cts +29 -0
- package/dist/esm/change-events.d.ts +49 -0
- package/dist/esm/change-events.js +141 -0
- package/dist/esm/change-events.js.map +1 -0
- package/dist/esm/collection.d.ts +95 -20
- package/dist/esm/collection.js +232 -84
- package/dist/esm/collection.js.map +1 -1
- package/dist/esm/errors.d.ts +225 -1
- package/dist/esm/errors.js +510 -2
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +5 -1
- package/dist/esm/index.js +81 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/indexes/auto-index.d.ts +9 -0
- package/dist/esm/indexes/auto-index.js +64 -0
- package/dist/esm/indexes/auto-index.js.map +1 -0
- package/dist/esm/indexes/base-index.d.ts +54 -0
- package/dist/esm/indexes/base-index.js +46 -0
- package/dist/esm/indexes/base-index.js.map +1 -0
- package/dist/esm/indexes/btree-index.d.ts +74 -0
- package/dist/esm/indexes/btree-index.js +191 -0
- package/dist/esm/indexes/btree-index.js.map +1 -0
- package/dist/esm/indexes/index-options.d.ts +13 -0
- package/dist/esm/indexes/lazy-index.d.ts +96 -0
- package/dist/esm/indexes/lazy-index.js +193 -0
- package/dist/esm/indexes/lazy-index.js.map +1 -0
- package/dist/esm/local-storage.js +9 -15
- package/dist/esm/local-storage.js.map +1 -1
- package/dist/esm/query/builder/functions.d.ts +4 -0
- package/dist/esm/query/builder/functions.js +11 -0
- package/dist/esm/query/builder/functions.js.map +1 -1
- package/dist/esm/query/builder/index.js +6 -7
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/builder/ref-proxy.d.ts +12 -0
- package/dist/esm/query/builder/ref-proxy.js +37 -0
- package/dist/esm/query/builder/ref-proxy.js.map +1 -1
- package/dist/esm/query/compiler/evaluators.d.ts +8 -0
- package/dist/esm/query/compiler/evaluators.js +84 -59
- package/dist/esm/query/compiler/evaluators.js.map +1 -1
- package/dist/esm/query/compiler/expressions.d.ts +25 -0
- package/dist/esm/query/compiler/expressions.js +61 -0
- package/dist/esm/query/compiler/expressions.js.map +1 -0
- package/dist/esm/query/compiler/group-by.js +5 -10
- package/dist/esm/query/compiler/group-by.js.map +1 -1
- package/dist/esm/query/compiler/index.d.ts +12 -3
- package/dist/esm/query/compiler/index.js +23 -17
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/joins.js +61 -12
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/compiler/order-by.js +1 -31
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/compiler/types.d.ts +2 -2
- package/dist/esm/query/live-query-collection.js +54 -12
- package/dist/esm/query/live-query-collection.js.map +1 -1
- package/dist/esm/query/optimizer.d.ts +13 -3
- package/dist/esm/query/optimizer.js +40 -2
- package/dist/esm/query/optimizer.js.map +1 -1
- package/dist/esm/transactions.js +5 -4
- package/dist/esm/transactions.js.map +1 -1
- package/dist/esm/types.d.ts +31 -0
- package/dist/esm/utils/array-utils.d.ts +8 -0
- package/dist/esm/utils/btree.d.ts +197 -0
- package/dist/esm/utils/btree.js +677 -0
- package/dist/esm/utils/btree.js.map +1 -0
- package/dist/esm/utils/comparison.d.ts +11 -0
- package/dist/esm/utils/comparison.js +52 -0
- package/dist/esm/utils/comparison.js.map +1 -0
- package/dist/esm/utils/index-optimization.d.ts +29 -0
- package/dist/esm/utils/index-optimization.js +270 -0
- package/dist/esm/utils/index-optimization.js.map +1 -0
- package/package.json +1 -1
- package/src/change-events.ts +257 -0
- package/src/collection.ts +316 -105
- package/src/errors.ts +545 -1
- package/src/index.ts +7 -1
- package/src/indexes/auto-index.ts +108 -0
- package/src/indexes/base-index.ts +119 -0
- package/src/indexes/btree-index.ts +263 -0
- package/src/indexes/index-options.ts +42 -0
- package/src/indexes/lazy-index.ts +251 -0
- package/src/local-storage.ts +16 -17
- package/src/query/builder/functions.ts +14 -0
- package/src/query/builder/index.ts +12 -7
- package/src/query/builder/ref-proxy.ts +65 -0
- package/src/query/compiler/evaluators.ts +114 -62
- package/src/query/compiler/expressions.ts +92 -0
- package/src/query/compiler/group-by.ts +10 -10
- package/src/query/compiler/index.ts +52 -22
- package/src/query/compiler/joins.ts +114 -15
- package/src/query/compiler/order-by.ts +1 -45
- package/src/query/compiler/types.ts +2 -2
- package/src/query/live-query-collection.ts +95 -15
- package/src/query/optimizer.ts +94 -5
- package/src/transactions.ts +10 -4
- package/src/types.ts +38 -0
- package/src/utils/array-utils.ts +28 -0
- package/src/utils/btree.ts +1010 -0
- package/src/utils/comparison.ts +79 -0
- 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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
):
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
231
|
-
|
|
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
|
|
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
|
-
|
|
254
|
-
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
80
|
-
const
|
|
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
|
|
86
|
-
const
|
|
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 [
|
|
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
|
|
103
|
-
const
|
|
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 [
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
+
}
|