@tanstack/db 0.4.7 → 0.4.9
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/collection/index.cjs.map +1 -1
- package/dist/cjs/collection/index.d.cts +2 -1
- package/dist/cjs/collection/lifecycle.cjs +2 -3
- package/dist/cjs/collection/lifecycle.cjs.map +1 -1
- package/dist/cjs/collection/state.cjs +22 -33
- package/dist/cjs/collection/state.cjs.map +1 -1
- package/dist/cjs/collection/state.d.cts +6 -2
- package/dist/cjs/collection/sync.cjs +4 -3
- package/dist/cjs/collection/sync.cjs.map +1 -1
- package/dist/cjs/errors.cjs +51 -17
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +38 -8
- package/dist/cjs/index.cjs +8 -4
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/indexes/auto-index.cjs +0 -3
- package/dist/cjs/indexes/auto-index.cjs.map +1 -1
- package/dist/cjs/query/builder/types.d.cts +1 -1
- package/dist/cjs/query/compiler/index.cjs +42 -19
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.d.cts +33 -8
- package/dist/cjs/query/compiler/joins.cjs +88 -66
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/compiler/joins.d.cts +5 -2
- package/dist/cjs/query/compiler/order-by.cjs +2 -0
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.d.cts +1 -0
- package/dist/cjs/query/compiler/select.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.cjs +322 -46
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.d.cts +98 -7
- package/dist/cjs/query/live/collection-registry.cjs +16 -0
- package/dist/cjs/query/live/collection-registry.cjs.map +1 -0
- package/dist/cjs/query/live/collection-registry.d.cts +26 -0
- package/dist/cjs/query/live/collection-subscriber.cjs +57 -58
- package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
- package/dist/cjs/query/live/collection-subscriber.d.cts +4 -7
- package/dist/cjs/query/live-query-collection.cjs +11 -5
- package/dist/cjs/query/live-query-collection.cjs.map +1 -1
- package/dist/cjs/query/live-query-collection.d.cts +10 -3
- package/dist/cjs/query/optimizer.cjs +44 -7
- package/dist/cjs/query/optimizer.cjs.map +1 -1
- package/dist/cjs/query/optimizer.d.cts +4 -4
- package/dist/cjs/scheduler.cjs +137 -0
- package/dist/cjs/scheduler.cjs.map +1 -0
- package/dist/cjs/scheduler.d.cts +56 -0
- package/dist/cjs/transactions.cjs +7 -1
- package/dist/cjs/transactions.cjs.map +1 -1
- package/dist/cjs/types.d.cts +3 -5
- package/dist/esm/collection/index.d.ts +2 -1
- package/dist/esm/collection/index.js.map +1 -1
- package/dist/esm/collection/lifecycle.js +2 -3
- package/dist/esm/collection/lifecycle.js.map +1 -1
- package/dist/esm/collection/state.d.ts +6 -2
- package/dist/esm/collection/state.js +22 -33
- package/dist/esm/collection/state.js.map +1 -1
- package/dist/esm/collection/sync.js +4 -3
- package/dist/esm/collection/sync.js.map +1 -1
- package/dist/esm/errors.d.ts +38 -8
- package/dist/esm/errors.js +52 -18
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.js +9 -5
- package/dist/esm/indexes/auto-index.js +0 -3
- package/dist/esm/indexes/auto-index.js.map +1 -1
- package/dist/esm/query/builder/types.d.ts +1 -1
- package/dist/esm/query/compiler/index.d.ts +33 -8
- package/dist/esm/query/compiler/index.js +42 -19
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/joins.d.ts +5 -2
- package/dist/esm/query/compiler/joins.js +90 -68
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/compiler/order-by.d.ts +1 -0
- package/dist/esm/query/compiler/order-by.js +2 -0
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/compiler/select.js.map +1 -1
- package/dist/esm/query/live/collection-config-builder.d.ts +98 -7
- package/dist/esm/query/live/collection-config-builder.js +322 -46
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/collection-registry.d.ts +26 -0
- package/dist/esm/query/live/collection-registry.js +16 -0
- package/dist/esm/query/live/collection-registry.js.map +1 -0
- package/dist/esm/query/live/collection-subscriber.d.ts +4 -7
- package/dist/esm/query/live/collection-subscriber.js +57 -58
- package/dist/esm/query/live/collection-subscriber.js.map +1 -1
- package/dist/esm/query/live-query-collection.d.ts +10 -3
- package/dist/esm/query/live-query-collection.js +11 -5
- package/dist/esm/query/live-query-collection.js.map +1 -1
- package/dist/esm/query/optimizer.d.ts +4 -4
- package/dist/esm/query/optimizer.js +44 -7
- package/dist/esm/query/optimizer.js.map +1 -1
- package/dist/esm/scheduler.d.ts +56 -0
- package/dist/esm/scheduler.js +137 -0
- package/dist/esm/scheduler.js.map +1 -0
- package/dist/esm/transactions.js +7 -1
- package/dist/esm/transactions.js.map +1 -1
- package/dist/esm/types.d.ts +3 -5
- package/package.json +2 -2
- package/src/collection/index.ts +1 -1
- package/src/collection/lifecycle.ts +3 -4
- package/src/collection/state.ts +52 -48
- package/src/collection/sync.ts +7 -6
- package/src/errors.ts +79 -13
- package/src/indexes/auto-index.ts +0 -8
- package/src/query/builder/types.ts +1 -1
- package/src/query/compiler/index.ts +115 -32
- package/src/query/compiler/joins.ts +180 -127
- package/src/query/compiler/order-by.ts +7 -0
- package/src/query/compiler/select.ts +2 -3
- package/src/query/live/collection-config-builder.ts +542 -71
- package/src/query/live/collection-registry.ts +47 -0
- package/src/query/live/collection-subscriber.ts +87 -105
- package/src/query/live-query-collection.ts +39 -14
- package/src/query/optimizer.ts +85 -15
- package/src/scheduler.ts +198 -0
- package/src/transactions.ts +12 -1
- package/src/types.ts +3 -5
|
@@ -1,18 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
consolidate,
|
|
3
|
-
filter,
|
|
4
|
-
join as joinOperator,
|
|
5
|
-
map,
|
|
6
|
-
tap,
|
|
7
|
-
} from "@tanstack/db-ivm"
|
|
1
|
+
import { filter, join as joinOperator, map, tap } from "@tanstack/db-ivm"
|
|
8
2
|
import {
|
|
9
3
|
CollectionInputNotFoundError,
|
|
10
4
|
InvalidJoinCondition,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
InvalidJoinConditionLeftSourceError,
|
|
6
|
+
InvalidJoinConditionRightSourceError,
|
|
7
|
+
InvalidJoinConditionSameSourceError,
|
|
8
|
+
InvalidJoinConditionSourceMismatchError,
|
|
15
9
|
JoinCollectionNotFoundError,
|
|
10
|
+
SubscriptionNotFoundError,
|
|
16
11
|
UnsupportedJoinSourceTypeError,
|
|
17
12
|
UnsupportedJoinTypeError,
|
|
18
13
|
} from "../../errors.js"
|
|
@@ -39,31 +34,37 @@ import type {
|
|
|
39
34
|
import type { QueryCache, QueryMapping } from "./types.js"
|
|
40
35
|
import type { CollectionSubscription } from "../../collection/subscription.js"
|
|
41
36
|
|
|
37
|
+
/** Function type for loading specific keys into a lazy collection */
|
|
42
38
|
export type LoadKeysFn = (key: Set<string | number>) => void
|
|
39
|
+
|
|
40
|
+
/** Callbacks for managing lazy-loaded collections in optimized joins */
|
|
43
41
|
export type LazyCollectionCallbacks = {
|
|
44
42
|
loadKeys: LoadKeysFn
|
|
45
43
|
loadInitialState: () => void
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
/**
|
|
49
|
-
* Processes all join clauses
|
|
47
|
+
* Processes all join clauses, applying lazy loading optimizations and maintaining
|
|
48
|
+
* alias tracking for per-alias subscriptions (enables self-joins).
|
|
50
49
|
*/
|
|
51
50
|
export function processJoins(
|
|
52
51
|
pipeline: NamespacedAndKeyedStream,
|
|
53
52
|
joinClauses: Array<JoinClause>,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
sources: Record<string, KeyedStream>,
|
|
54
|
+
mainCollectionId: string,
|
|
55
|
+
mainSource: string,
|
|
57
56
|
allInputs: Record<string, KeyedStream>,
|
|
58
57
|
cache: QueryCache,
|
|
59
58
|
queryMapping: QueryMapping,
|
|
60
59
|
collections: Record<string, Collection>,
|
|
61
60
|
subscriptions: Record<string, CollectionSubscription>,
|
|
62
61
|
callbacks: Record<string, LazyCollectionCallbacks>,
|
|
63
|
-
|
|
62
|
+
lazySources: Set<string>,
|
|
64
63
|
optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
|
|
65
64
|
rawQuery: QueryIR,
|
|
66
|
-
onCompileSubquery: CompileQueryFn
|
|
65
|
+
onCompileSubquery: CompileQueryFn,
|
|
66
|
+
aliasToCollectionId: Record<string, string>,
|
|
67
|
+
aliasRemapping: Record<string, string>
|
|
67
68
|
): NamespacedAndKeyedStream {
|
|
68
69
|
let resultPipeline = pipeline
|
|
69
70
|
|
|
@@ -71,19 +72,21 @@ export function processJoins(
|
|
|
71
72
|
resultPipeline = processJoin(
|
|
72
73
|
resultPipeline,
|
|
73
74
|
joinClause,
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
sources,
|
|
76
|
+
mainCollectionId,
|
|
77
|
+
mainSource,
|
|
77
78
|
allInputs,
|
|
78
79
|
cache,
|
|
79
80
|
queryMapping,
|
|
80
81
|
collections,
|
|
81
82
|
subscriptions,
|
|
82
83
|
callbacks,
|
|
83
|
-
|
|
84
|
+
lazySources,
|
|
84
85
|
optimizableOrderByCollections,
|
|
85
86
|
rawQuery,
|
|
86
|
-
onCompileSubquery
|
|
87
|
+
onCompileSubquery,
|
|
88
|
+
aliasToCollectionId,
|
|
89
|
+
aliasRemapping
|
|
87
90
|
)
|
|
88
91
|
}
|
|
89
92
|
|
|
@@ -91,28 +94,33 @@ export function processJoins(
|
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
/**
|
|
94
|
-
* Processes a single join clause
|
|
97
|
+
* Processes a single join clause with lazy loading optimization.
|
|
98
|
+
* For LEFT/RIGHT/INNER joins, marks one side as "lazy" (loads on-demand based on join keys).
|
|
95
99
|
*/
|
|
96
100
|
function processJoin(
|
|
97
101
|
pipeline: NamespacedAndKeyedStream,
|
|
98
102
|
joinClause: JoinClause,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
sources: Record<string, KeyedStream>,
|
|
104
|
+
mainCollectionId: string,
|
|
105
|
+
mainSource: string,
|
|
102
106
|
allInputs: Record<string, KeyedStream>,
|
|
103
107
|
cache: QueryCache,
|
|
104
108
|
queryMapping: QueryMapping,
|
|
105
109
|
collections: Record<string, Collection>,
|
|
106
110
|
subscriptions: Record<string, CollectionSubscription>,
|
|
107
111
|
callbacks: Record<string, LazyCollectionCallbacks>,
|
|
108
|
-
|
|
112
|
+
lazySources: Set<string>,
|
|
109
113
|
optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
|
|
110
114
|
rawQuery: QueryIR,
|
|
111
|
-
onCompileSubquery: CompileQueryFn
|
|
115
|
+
onCompileSubquery: CompileQueryFn,
|
|
116
|
+
aliasToCollectionId: Record<string, string>,
|
|
117
|
+
aliasRemapping: Record<string, string>
|
|
112
118
|
): NamespacedAndKeyedStream {
|
|
113
|
-
|
|
119
|
+
const isCollectionRef = joinClause.from.type === `collectionRef`
|
|
120
|
+
|
|
121
|
+
// Get the joined source alias and input stream
|
|
114
122
|
const {
|
|
115
|
-
alias:
|
|
123
|
+
alias: joinedSource,
|
|
116
124
|
input: joinedInput,
|
|
117
125
|
collectionId: joinedCollectionId,
|
|
118
126
|
} = processJoinSource(
|
|
@@ -121,40 +129,47 @@ function processJoin(
|
|
|
121
129
|
collections,
|
|
122
130
|
subscriptions,
|
|
123
131
|
callbacks,
|
|
124
|
-
|
|
132
|
+
lazySources,
|
|
125
133
|
optimizableOrderByCollections,
|
|
126
134
|
cache,
|
|
127
135
|
queryMapping,
|
|
128
|
-
onCompileSubquery
|
|
136
|
+
onCompileSubquery,
|
|
137
|
+
aliasToCollectionId,
|
|
138
|
+
aliasRemapping
|
|
129
139
|
)
|
|
130
140
|
|
|
131
|
-
// Add the joined
|
|
132
|
-
|
|
141
|
+
// Add the joined source to the sources map
|
|
142
|
+
sources[joinedSource] = joinedInput
|
|
143
|
+
if (isCollectionRef) {
|
|
144
|
+
// Only direct collection references form new alias bindings. Subquery
|
|
145
|
+
// aliases reuse the mapping returned from the recursive compilation above.
|
|
146
|
+
aliasToCollectionId[joinedSource] = joinedCollectionId
|
|
147
|
+
}
|
|
133
148
|
|
|
134
|
-
const mainCollection = collections[
|
|
149
|
+
const mainCollection = collections[mainCollectionId]
|
|
135
150
|
const joinedCollection = collections[joinedCollectionId]
|
|
136
151
|
|
|
137
152
|
if (!mainCollection) {
|
|
138
|
-
throw new JoinCollectionNotFoundError(
|
|
153
|
+
throw new JoinCollectionNotFoundError(mainCollectionId)
|
|
139
154
|
}
|
|
140
155
|
|
|
141
156
|
if (!joinedCollection) {
|
|
142
157
|
throw new JoinCollectionNotFoundError(joinedCollectionId)
|
|
143
158
|
}
|
|
144
159
|
|
|
145
|
-
const {
|
|
160
|
+
const { activeSource, lazySource } = getActiveAndLazySources(
|
|
146
161
|
joinClause.type,
|
|
147
162
|
mainCollection,
|
|
148
163
|
joinedCollection
|
|
149
164
|
)
|
|
150
165
|
|
|
151
|
-
// Analyze which
|
|
152
|
-
const
|
|
166
|
+
// Analyze which source each expression refers to and swap if necessary
|
|
167
|
+
const availableSources = Object.keys(sources)
|
|
153
168
|
const { mainExpr, joinedExpr } = analyzeJoinExpressions(
|
|
154
169
|
joinClause.left,
|
|
155
170
|
joinClause.right,
|
|
156
|
-
|
|
157
|
-
|
|
171
|
+
availableSources,
|
|
172
|
+
joinedSource
|
|
158
173
|
)
|
|
159
174
|
|
|
160
175
|
// Pre-compile the join expressions
|
|
@@ -164,7 +179,7 @@ function processJoin(
|
|
|
164
179
|
// Prepare the main pipeline for joining
|
|
165
180
|
let mainPipeline = pipeline.pipe(
|
|
166
181
|
map(([currentKey, namespacedRow]) => {
|
|
167
|
-
// Extract the join key from the main
|
|
182
|
+
// Extract the join key from the main source expression
|
|
168
183
|
const mainKey = compiledMainExpr(namespacedRow)
|
|
169
184
|
|
|
170
185
|
// Return [joinKey, [originalKey, namespacedRow]]
|
|
@@ -179,9 +194,9 @@ function processJoin(
|
|
|
179
194
|
let joinedPipeline = joinedInput.pipe(
|
|
180
195
|
map(([currentKey, row]) => {
|
|
181
196
|
// Wrap the row in a namespaced structure
|
|
182
|
-
const namespacedRow: NamespacedRow = { [
|
|
197
|
+
const namespacedRow: NamespacedRow = { [joinedSource]: row }
|
|
183
198
|
|
|
184
|
-
// Extract the join key from the joined
|
|
199
|
+
// Extract the join key from the joined source expression
|
|
185
200
|
const joinedKey = compiledJoinedExpr(namespacedRow)
|
|
186
201
|
|
|
187
202
|
// Return [joinKey, [originalKey, namespacedRow]]
|
|
@@ -197,13 +212,12 @@ function processJoin(
|
|
|
197
212
|
throw new UnsupportedJoinTypeError(joinClause.type)
|
|
198
213
|
}
|
|
199
214
|
|
|
200
|
-
if (
|
|
215
|
+
if (activeSource) {
|
|
201
216
|
// If the lazy collection comes from a subquery that has a limit and/or an offset clause
|
|
202
217
|
// then we need to deoptimize the join because we don't know which rows are in the result set
|
|
203
218
|
// since we simply lookup matching keys in the index but the index contains all rows
|
|
204
219
|
// (not just the ones that pass the limit and offset clauses)
|
|
205
|
-
const lazyFrom =
|
|
206
|
-
activeCollection === `main` ? joinClause.from : rawQuery.from
|
|
220
|
+
const lazyFrom = activeSource === `main` ? joinClause.from : rawQuery.from
|
|
207
221
|
const limitedSubquery =
|
|
208
222
|
lazyFrom.type === `queryRef` &&
|
|
209
223
|
(lazyFrom.query.limit || lazyFrom.query.offset)
|
|
@@ -219,24 +233,25 @@ function processJoin(
|
|
|
219
233
|
// based on the value of the joinKey and by looking up
|
|
220
234
|
// matching rows in the index of the lazy collection
|
|
221
235
|
|
|
222
|
-
// Mark the lazy
|
|
236
|
+
// Mark the lazy source alias as lazy
|
|
223
237
|
// this Set is passed by the liveQueryCollection to the compiler
|
|
224
238
|
// such that the liveQueryCollection can check it after compilation
|
|
225
|
-
// to know which
|
|
226
|
-
|
|
239
|
+
// to know which source aliases should load data lazily (not initially)
|
|
240
|
+
const lazyAlias = activeSource === `main` ? joinedSource : mainSource
|
|
241
|
+
lazySources.add(lazyAlias)
|
|
227
242
|
|
|
228
243
|
const activePipeline =
|
|
229
|
-
|
|
244
|
+
activeSource === `main` ? mainPipeline : joinedPipeline
|
|
230
245
|
|
|
231
|
-
const
|
|
232
|
-
|
|
246
|
+
const lazySourceJoinExpr =
|
|
247
|
+
activeSource === `main`
|
|
233
248
|
? (joinedExpr as PropRef)
|
|
234
249
|
: (mainExpr as PropRef)
|
|
235
250
|
|
|
236
251
|
const followRefResult = followRef(
|
|
237
252
|
rawQuery,
|
|
238
|
-
|
|
239
|
-
|
|
253
|
+
lazySourceJoinExpr,
|
|
254
|
+
lazySource
|
|
240
255
|
)!
|
|
241
256
|
const followRefCollection = followRefResult.collection
|
|
242
257
|
|
|
@@ -249,38 +264,51 @@ function processJoin(
|
|
|
249
264
|
)
|
|
250
265
|
}
|
|
251
266
|
|
|
267
|
+
// Set up lazy loading: intercept active side's stream and dynamically load
|
|
268
|
+
// matching rows from lazy side based on join keys.
|
|
252
269
|
const activePipelineWithLoading: IStreamBuilder<
|
|
253
270
|
[key: unknown, [originalKey: string, namespacedRow: NamespacedRow]]
|
|
254
271
|
> = activePipeline.pipe(
|
|
255
272
|
tap((data) => {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
273
|
+
// Find the subscription for lazy loading.
|
|
274
|
+
// Subscriptions are keyed by the innermost alias (where the collection subscription
|
|
275
|
+
// was actually created). For subqueries, the join alias may differ from the inner alias.
|
|
276
|
+
// aliasRemapping provides a flattened one-hop lookup from outer → innermost alias.
|
|
277
|
+
// Example: .join({ activeUser: subquery }) where subquery uses .from({ user: collection })
|
|
278
|
+
// → aliasRemapping['activeUser'] = 'user' (always maps directly to innermost, never recursive)
|
|
279
|
+
const resolvedAlias = aliasRemapping[lazyAlias] || lazyAlias
|
|
280
|
+
const lazySourceSubscription = subscriptions[resolvedAlias]
|
|
281
|
+
|
|
282
|
+
if (!lazySourceSubscription) {
|
|
283
|
+
throw new SubscriptionNotFoundError(
|
|
284
|
+
resolvedAlias,
|
|
285
|
+
lazyAlias,
|
|
286
|
+
lazySource.id,
|
|
287
|
+
Object.keys(subscriptions)
|
|
261
288
|
)
|
|
262
289
|
}
|
|
263
290
|
|
|
264
|
-
if (
|
|
291
|
+
if (lazySourceSubscription.hasLoadedInitialState()) {
|
|
265
292
|
// Entire state was already loaded because we deoptimized the join
|
|
266
293
|
return
|
|
267
294
|
}
|
|
268
295
|
|
|
296
|
+
// Request filtered snapshot from lazy collection for matching join keys
|
|
269
297
|
const joinKeys = data.getInner().map(([[joinKey]]) => joinKey)
|
|
270
298
|
const lazyJoinRef = new PropRef(followRefResult.path)
|
|
271
|
-
const loaded =
|
|
299
|
+
const loaded = lazySourceSubscription.requestSnapshot({
|
|
272
300
|
where: inArray(lazyJoinRef, joinKeys),
|
|
273
301
|
optimizedOnly: true,
|
|
274
302
|
})
|
|
275
303
|
|
|
276
304
|
if (!loaded) {
|
|
277
305
|
// Snapshot wasn't sent because it could not be loaded from the indexes
|
|
278
|
-
|
|
306
|
+
lazySourceSubscription.requestSnapshot()
|
|
279
307
|
}
|
|
280
308
|
})
|
|
281
309
|
)
|
|
282
310
|
|
|
283
|
-
if (
|
|
311
|
+
if (activeSource === `main`) {
|
|
284
312
|
mainPipeline = activePipelineWithLoading
|
|
285
313
|
} else {
|
|
286
314
|
joinedPipeline = activePipelineWithLoading
|
|
@@ -290,68 +318,66 @@ function processJoin(
|
|
|
290
318
|
|
|
291
319
|
return mainPipeline.pipe(
|
|
292
320
|
joinOperator(joinedPipeline, joinClause.type as JoinType),
|
|
293
|
-
consolidate(),
|
|
294
321
|
processJoinResults(joinClause.type)
|
|
295
322
|
)
|
|
296
323
|
}
|
|
297
324
|
|
|
298
325
|
/**
|
|
299
|
-
* Analyzes join expressions to determine which refers to which
|
|
300
|
-
* and returns them in the correct order (available
|
|
326
|
+
* Analyzes join expressions to determine which refers to which source
|
|
327
|
+
* and returns them in the correct order (available source expression first, joined source expression second)
|
|
301
328
|
*/
|
|
302
329
|
function analyzeJoinExpressions(
|
|
303
330
|
left: BasicExpression,
|
|
304
331
|
right: BasicExpression,
|
|
305
|
-
|
|
306
|
-
|
|
332
|
+
allAvailableSourceAliases: Array<string>,
|
|
333
|
+
joinedSource: string
|
|
307
334
|
): { mainExpr: BasicExpression; joinedExpr: BasicExpression } {
|
|
308
|
-
// Filter out the joined
|
|
309
|
-
const
|
|
310
|
-
(alias) => alias !==
|
|
335
|
+
// Filter out the joined source alias from the available source aliases
|
|
336
|
+
const availableSources = allAvailableSourceAliases.filter(
|
|
337
|
+
(alias) => alias !== joinedSource
|
|
311
338
|
)
|
|
312
339
|
|
|
313
|
-
const
|
|
314
|
-
const
|
|
340
|
+
const leftSourceAlias = getSourceAliasFromExpression(left)
|
|
341
|
+
const rightSourceAlias = getSourceAliasFromExpression(right)
|
|
315
342
|
|
|
316
|
-
// If left expression refers to an available
|
|
343
|
+
// If left expression refers to an available source and right refers to joined source, keep as is
|
|
317
344
|
if (
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
345
|
+
leftSourceAlias &&
|
|
346
|
+
availableSources.includes(leftSourceAlias) &&
|
|
347
|
+
rightSourceAlias === joinedSource
|
|
321
348
|
) {
|
|
322
349
|
return { mainExpr: left, joinedExpr: right }
|
|
323
350
|
}
|
|
324
351
|
|
|
325
|
-
// If left expression refers to joined
|
|
352
|
+
// If left expression refers to joined source and right refers to an available source, swap them
|
|
326
353
|
if (
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
354
|
+
leftSourceAlias === joinedSource &&
|
|
355
|
+
rightSourceAlias &&
|
|
356
|
+
availableSources.includes(rightSourceAlias)
|
|
330
357
|
) {
|
|
331
358
|
return { mainExpr: right, joinedExpr: left }
|
|
332
359
|
}
|
|
333
360
|
|
|
334
|
-
// If one expression doesn't refer to any
|
|
335
|
-
if (!
|
|
336
|
-
|
|
337
|
-
throw new InvalidJoinConditionTableMismatchError()
|
|
361
|
+
// If one expression doesn't refer to any source, this is an invalid join
|
|
362
|
+
if (!leftSourceAlias || !rightSourceAlias) {
|
|
363
|
+
throw new InvalidJoinConditionSourceMismatchError()
|
|
338
364
|
}
|
|
339
365
|
|
|
340
366
|
// If both expressions refer to the same alias, this is an invalid join
|
|
341
|
-
if (
|
|
342
|
-
throw new
|
|
367
|
+
if (leftSourceAlias === rightSourceAlias) {
|
|
368
|
+
throw new InvalidJoinConditionSameSourceError(leftSourceAlias)
|
|
343
369
|
}
|
|
344
370
|
|
|
345
|
-
// Left side must refer to an available
|
|
371
|
+
// Left side must refer to an available source
|
|
346
372
|
// This cannot happen with the query builder as there is no way to build a ref
|
|
347
|
-
// to an unavailable
|
|
348
|
-
if (!
|
|
349
|
-
throw new
|
|
373
|
+
// to an unavailable source, but just in case, but could happen with the IR
|
|
374
|
+
if (!availableSources.includes(leftSourceAlias)) {
|
|
375
|
+
throw new InvalidJoinConditionLeftSourceError(leftSourceAlias)
|
|
350
376
|
}
|
|
351
377
|
|
|
352
|
-
// Right side must refer to the joined
|
|
353
|
-
if (
|
|
354
|
-
throw new
|
|
378
|
+
// Right side must refer to the joined source
|
|
379
|
+
if (rightSourceAlias !== joinedSource) {
|
|
380
|
+
throw new InvalidJoinConditionRightSourceError(joinedSource)
|
|
355
381
|
}
|
|
356
382
|
|
|
357
383
|
// This should not be reachable given the logic above, but just in case
|
|
@@ -359,27 +385,27 @@ function analyzeJoinExpressions(
|
|
|
359
385
|
}
|
|
360
386
|
|
|
361
387
|
/**
|
|
362
|
-
* Extracts the
|
|
388
|
+
* Extracts the source alias from a join expression
|
|
363
389
|
*/
|
|
364
|
-
function
|
|
390
|
+
function getSourceAliasFromExpression(expr: BasicExpression): string | null {
|
|
365
391
|
switch (expr.type) {
|
|
366
392
|
case `ref`:
|
|
367
|
-
// PropRef path has the
|
|
393
|
+
// PropRef path has the source alias as the first element
|
|
368
394
|
return expr.path[0] || null
|
|
369
395
|
case `func`: {
|
|
370
|
-
// For function expressions, we need to check if all arguments refer to the same
|
|
371
|
-
const
|
|
396
|
+
// For function expressions, we need to check if all arguments refer to the same source
|
|
397
|
+
const sourceAliases = new Set<string>()
|
|
372
398
|
for (const arg of expr.args) {
|
|
373
|
-
const alias =
|
|
399
|
+
const alias = getSourceAliasFromExpression(arg)
|
|
374
400
|
if (alias) {
|
|
375
|
-
|
|
401
|
+
sourceAliases.add(alias)
|
|
376
402
|
}
|
|
377
403
|
}
|
|
378
|
-
// If all arguments refer to the same
|
|
379
|
-
return
|
|
404
|
+
// If all arguments refer to the same source, return that source alias
|
|
405
|
+
return sourceAliases.size === 1 ? Array.from(sourceAliases)[0]! : null
|
|
380
406
|
}
|
|
381
407
|
default:
|
|
382
|
-
// Values (type='val') don't reference any
|
|
408
|
+
// Values (type='val') don't reference any source
|
|
383
409
|
return null
|
|
384
410
|
}
|
|
385
411
|
}
|
|
@@ -393,18 +419,25 @@ function processJoinSource(
|
|
|
393
419
|
collections: Record<string, Collection>,
|
|
394
420
|
subscriptions: Record<string, CollectionSubscription>,
|
|
395
421
|
callbacks: Record<string, LazyCollectionCallbacks>,
|
|
396
|
-
|
|
422
|
+
lazySources: Set<string>,
|
|
397
423
|
optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
|
|
398
424
|
cache: QueryCache,
|
|
399
425
|
queryMapping: QueryMapping,
|
|
400
|
-
onCompileSubquery: CompileQueryFn
|
|
426
|
+
onCompileSubquery: CompileQueryFn,
|
|
427
|
+
aliasToCollectionId: Record<string, string>,
|
|
428
|
+
aliasRemapping: Record<string, string>
|
|
401
429
|
): { alias: string; input: KeyedStream; collectionId: string } {
|
|
402
430
|
switch (from.type) {
|
|
403
431
|
case `collectionRef`: {
|
|
404
|
-
const input = allInputs[from.
|
|
432
|
+
const input = allInputs[from.alias]
|
|
405
433
|
if (!input) {
|
|
406
|
-
throw new CollectionInputNotFoundError(
|
|
434
|
+
throw new CollectionInputNotFoundError(
|
|
435
|
+
from.alias,
|
|
436
|
+
from.collection.id,
|
|
437
|
+
Object.keys(allInputs)
|
|
438
|
+
)
|
|
407
439
|
}
|
|
440
|
+
aliasToCollectionId[from.alias] = from.collection.id
|
|
408
441
|
return { alias: from.alias, input, collectionId: from.collection.id }
|
|
409
442
|
}
|
|
410
443
|
case `queryRef`: {
|
|
@@ -418,12 +451,38 @@ function processJoinSource(
|
|
|
418
451
|
collections,
|
|
419
452
|
subscriptions,
|
|
420
453
|
callbacks,
|
|
421
|
-
|
|
454
|
+
lazySources,
|
|
422
455
|
optimizableOrderByCollections,
|
|
423
456
|
cache,
|
|
424
457
|
queryMapping
|
|
425
458
|
)
|
|
426
459
|
|
|
460
|
+
// Pull up alias mappings from subquery to parent scope.
|
|
461
|
+
// This includes both the innermost alias-to-collection mappings AND
|
|
462
|
+
// any existing remappings from nested subquery levels.
|
|
463
|
+
Object.assign(aliasToCollectionId, subQueryResult.aliasToCollectionId)
|
|
464
|
+
Object.assign(aliasRemapping, subQueryResult.aliasRemapping)
|
|
465
|
+
|
|
466
|
+
// Create a flattened remapping from outer alias to innermost alias.
|
|
467
|
+
// For nested subqueries, this ensures one-hop lookups (not recursive chains).
|
|
468
|
+
//
|
|
469
|
+
// Example with 3-level nesting:
|
|
470
|
+
// Inner: .from({ user: usersCollection })
|
|
471
|
+
// Middle: .from({ activeUser: innerSubquery }) → creates: activeUser → user
|
|
472
|
+
// Outer: .join({ author: middleSubquery }, ...) → creates: author → user (not author → activeUser)
|
|
473
|
+
//
|
|
474
|
+
// We search through the PULLED-UP aliasToCollectionId (which contains the
|
|
475
|
+
// innermost 'user' alias), so we always map directly to the deepest level.
|
|
476
|
+
// This means aliasRemapping[lazyAlias] is always a single lookup, never recursive.
|
|
477
|
+
const innerAlias = Object.keys(subQueryResult.aliasToCollectionId).find(
|
|
478
|
+
(alias) =>
|
|
479
|
+
subQueryResult.aliasToCollectionId[alias] ===
|
|
480
|
+
subQueryResult.collectionId
|
|
481
|
+
)
|
|
482
|
+
if (innerAlias && innerAlias !== from.alias) {
|
|
483
|
+
aliasRemapping[from.alias] = innerAlias
|
|
484
|
+
}
|
|
485
|
+
|
|
427
486
|
// Extract the pipeline from the compilation result
|
|
428
487
|
const subQueryInput = subQueryResult.pipeline
|
|
429
488
|
|
|
@@ -517,41 +576,35 @@ function processJoinResults(joinType: string) {
|
|
|
517
576
|
/**
|
|
518
577
|
* Returns the active and lazy collections for a join clause.
|
|
519
578
|
* The active collection is the one that we need to fully iterate over
|
|
520
|
-
* and it can be the main
|
|
579
|
+
* and it can be the main source (i.e. left collection) or the joined source (i.e. right collection).
|
|
521
580
|
* The lazy collection is the one that we should join-in lazily based on matches in the active collection.
|
|
522
581
|
* @param joinClause - The join clause to analyze
|
|
523
582
|
* @param leftCollection - The left collection
|
|
524
583
|
* @param rightCollection - The right collection
|
|
525
584
|
* @returns The active and lazy collections. They are undefined if we need to loop over both collections (i.e. both are active)
|
|
526
585
|
*/
|
|
527
|
-
function
|
|
586
|
+
function getActiveAndLazySources(
|
|
528
587
|
joinType: JoinClause[`type`],
|
|
529
588
|
leftCollection: Collection,
|
|
530
589
|
rightCollection: Collection
|
|
531
590
|
):
|
|
532
|
-
| {
|
|
533
|
-
| {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
// because `liveQueryCollection` will detect that the collection is lazy
|
|
537
|
-
// and treat it lazily (because the collection is shared)
|
|
538
|
-
// and thus it will not load any keys because both sides of the join
|
|
539
|
-
// will be handled lazily
|
|
540
|
-
return { activeCollection: undefined, lazyCollection: undefined }
|
|
541
|
-
}
|
|
591
|
+
| { activeSource: `main` | `joined`; lazySource: Collection }
|
|
592
|
+
| { activeSource: undefined; lazySource: undefined } {
|
|
593
|
+
// Self-joins can now be optimized since we track lazy loading by source alias
|
|
594
|
+
// rather than collection ID. Each alias has its own subscription and lazy state.
|
|
542
595
|
|
|
543
596
|
switch (joinType) {
|
|
544
597
|
case `left`:
|
|
545
|
-
return {
|
|
598
|
+
return { activeSource: `main`, lazySource: rightCollection }
|
|
546
599
|
case `right`:
|
|
547
|
-
return {
|
|
600
|
+
return { activeSource: `joined`, lazySource: leftCollection }
|
|
548
601
|
case `inner`:
|
|
549
602
|
// The smallest collection should be the active collection
|
|
550
603
|
// and the biggest collection should be lazy
|
|
551
604
|
return leftCollection.size < rightCollection.size
|
|
552
|
-
? {
|
|
553
|
-
: {
|
|
605
|
+
? { activeSource: `main`, lazySource: rightCollection }
|
|
606
|
+
: { activeSource: `joined`, lazySource: leftCollection }
|
|
554
607
|
default:
|
|
555
|
-
return {
|
|
608
|
+
return { activeSource: undefined, lazySource: undefined }
|
|
556
609
|
}
|
|
557
610
|
}
|
|
@@ -13,6 +13,7 @@ import type { IndexInterface } from "../../indexes/base-index.js"
|
|
|
13
13
|
import type { Collection } from "../../collection/index.js"
|
|
14
14
|
|
|
15
15
|
export type OrderByOptimizationInfo = {
|
|
16
|
+
alias: string
|
|
16
17
|
orderBy: OrderBy
|
|
17
18
|
offset: number
|
|
18
19
|
limit: number
|
|
@@ -155,7 +156,13 @@ export function processOrderBy(
|
|
|
155
156
|
|
|
156
157
|
if (index && index.supports(`gt`)) {
|
|
157
158
|
// We found an index that we can use to lazily load ordered data
|
|
159
|
+
const orderByAlias =
|
|
160
|
+
orderByExpression.path.length > 1
|
|
161
|
+
? String(orderByExpression.path[0])
|
|
162
|
+
: rawQuery.from.alias
|
|
163
|
+
|
|
158
164
|
const orderByOptimizationInfo = {
|
|
165
|
+
alias: orderByAlias,
|
|
159
166
|
offset: offset ?? 0,
|
|
160
167
|
limit,
|
|
161
168
|
comparator,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { map } from "@tanstack/db-ivm"
|
|
2
2
|
import { PropRef, Value as ValClass, isExpressionLike } from "../ir.js"
|
|
3
|
+
import { AggregateNotSupportedError } from "../../errors.js"
|
|
3
4
|
import { compileExpression } from "./evaluators.js"
|
|
4
5
|
import type { Aggregate, BasicExpression, Select } from "../ir.js"
|
|
5
6
|
import type {
|
|
@@ -157,9 +158,7 @@ export function processArgument(
|
|
|
157
158
|
namespacedRow: NamespacedRow
|
|
158
159
|
): any {
|
|
159
160
|
if (isAggregateExpression(arg)) {
|
|
160
|
-
throw new
|
|
161
|
-
`Aggregate expressions are not supported in this context. Use GROUP BY clause for aggregates.`
|
|
162
|
-
)
|
|
161
|
+
throw new AggregateNotSupportedError()
|
|
163
162
|
}
|
|
164
163
|
|
|
165
164
|
// Pre-compile the expression and evaluate immediately
|