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