@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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Collection } from "../../collection/index.js"
|
|
2
|
+
import type { CollectionConfigBuilder } from "./collection-config-builder.js"
|
|
3
|
+
|
|
4
|
+
const collectionBuilderRegistry = new WeakMap<
|
|
5
|
+
Collection<any, any, any>,
|
|
6
|
+
CollectionConfigBuilder<any, any>
|
|
7
|
+
>()
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Retrieves the builder attached to a config object via its utils.getBuilder() method.
|
|
11
|
+
*
|
|
12
|
+
* @param config - The collection config object
|
|
13
|
+
* @returns The attached builder, or `undefined` if none exists
|
|
14
|
+
*/
|
|
15
|
+
export function getBuilderFromConfig(
|
|
16
|
+
config: object
|
|
17
|
+
): CollectionConfigBuilder<any, any> | undefined {
|
|
18
|
+
return (config as any).utils?.getBuilder?.()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Registers a builder for a collection in the global registry.
|
|
23
|
+
* Used to detect when a live query depends on another live query,
|
|
24
|
+
* enabling the scheduler to ensure parent queries run first.
|
|
25
|
+
*
|
|
26
|
+
* @param collection - The collection to register the builder for
|
|
27
|
+
* @param builder - The builder that produces this collection
|
|
28
|
+
*/
|
|
29
|
+
export function registerCollectionBuilder(
|
|
30
|
+
collection: Collection<any, any, any>,
|
|
31
|
+
builder: CollectionConfigBuilder<any, any>
|
|
32
|
+
): void {
|
|
33
|
+
collectionBuilderRegistry.set(collection, builder)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Retrieves the builder registered for a collection.
|
|
38
|
+
* Used to discover dependencies when a live query subscribes to another live query.
|
|
39
|
+
*
|
|
40
|
+
* @param collection - The collection to look up
|
|
41
|
+
* @returns The registered builder, or `undefined` if none exists
|
|
42
|
+
*/
|
|
43
|
+
export function getCollectionBuilder(
|
|
44
|
+
collection: Collection<any, any, any>
|
|
45
|
+
): CollectionConfigBuilder<any, any> | undefined {
|
|
46
|
+
return collectionBuilderRegistry.get(collection)
|
|
47
|
+
}
|
|
@@ -3,15 +3,20 @@ import {
|
|
|
3
3
|
convertOrderByToBasicExpression,
|
|
4
4
|
convertToBasicExpression,
|
|
5
5
|
} from "../compiler/expressions.js"
|
|
6
|
-
import
|
|
6
|
+
import { WhereClauseConversionError } from "../../errors.js"
|
|
7
7
|
import type { MultiSetArray, RootStreamBuilder } from "@tanstack/db-ivm"
|
|
8
8
|
import type { Collection } from "../../collection/index.js"
|
|
9
|
-
import type { ChangeMessage
|
|
9
|
+
import type { ChangeMessage } from "../../types.js"
|
|
10
10
|
import type { Context, GetResult } from "../builder/types.js"
|
|
11
11
|
import type { BasicExpression } from "../ir.js"
|
|
12
|
+
import type { OrderByOptimizationInfo } from "../compiler/order-by.js"
|
|
12
13
|
import type { CollectionConfigBuilder } from "./collection-config-builder.js"
|
|
13
14
|
import type { CollectionSubscription } from "../../collection/subscription.js"
|
|
14
15
|
|
|
16
|
+
const loadMoreCallbackSymbol = Symbol.for(
|
|
17
|
+
`@tanstack/db.collection-config-builder`
|
|
18
|
+
)
|
|
19
|
+
|
|
15
20
|
export class CollectionSubscriber<
|
|
16
21
|
TContext extends Context,
|
|
17
22
|
TResult extends object = GetResult<TContext>,
|
|
@@ -19,61 +24,42 @@ export class CollectionSubscriber<
|
|
|
19
24
|
// Keep track of the biggest value we've sent so far (needed for orderBy optimization)
|
|
20
25
|
private biggest: any = undefined
|
|
21
26
|
|
|
22
|
-
private collectionAlias: string
|
|
23
|
-
|
|
24
27
|
constructor(
|
|
28
|
+
private alias: string,
|
|
25
29
|
private collectionId: string,
|
|
26
30
|
private collection: Collection,
|
|
27
|
-
private config: Parameters<SyncConfig<TResult>[`sync`]>[0],
|
|
28
|
-
private syncState: FullSyncState,
|
|
29
31
|
private collectionConfigBuilder: CollectionConfigBuilder<TContext, TResult>
|
|
30
|
-
) {
|
|
31
|
-
this.collectionAlias = findCollectionAlias(
|
|
32
|
-
this.collectionId,
|
|
33
|
-
this.collectionConfigBuilder.query
|
|
34
|
-
)!
|
|
35
|
-
}
|
|
32
|
+
) {}
|
|
36
33
|
|
|
37
34
|
subscribe(): CollectionSubscription {
|
|
38
|
-
const whereClause = this.
|
|
35
|
+
const whereClause = this.getWhereClauseForAlias()
|
|
39
36
|
|
|
40
37
|
if (whereClause) {
|
|
41
|
-
|
|
42
|
-
const whereExpression = convertToBasicExpression(
|
|
43
|
-
whereClause,
|
|
44
|
-
this.collectionAlias
|
|
45
|
-
)
|
|
38
|
+
const whereExpression = convertToBasicExpression(whereClause, this.alias)
|
|
46
39
|
|
|
47
40
|
if (whereExpression) {
|
|
48
|
-
// Use index optimization for this collection
|
|
49
41
|
return this.subscribeToChanges(whereExpression)
|
|
50
|
-
} else {
|
|
51
|
-
// This should not happen - if we have a whereClause but can't create whereExpression,
|
|
52
|
-
// it indicates a bug in our optimization logic
|
|
53
|
-
throw new Error(
|
|
54
|
-
`Failed to convert WHERE clause to collection filter for collection '${this.collectionId}'. ` +
|
|
55
|
-
`This indicates a bug in the query optimization logic.`
|
|
56
|
-
)
|
|
57
42
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return this.subscribeToChanges()
|
|
43
|
+
|
|
44
|
+
throw new WhereClauseConversionError(this.collectionId, this.alias)
|
|
61
45
|
}
|
|
46
|
+
|
|
47
|
+
return this.subscribeToChanges()
|
|
62
48
|
}
|
|
63
49
|
|
|
64
50
|
private subscribeToChanges(whereExpression?: BasicExpression<boolean>) {
|
|
65
51
|
let subscription: CollectionSubscription
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
52
|
+
const orderByInfo = this.getOrderByInfo()
|
|
53
|
+
if (orderByInfo) {
|
|
54
|
+
subscription = this.subscribeToOrderedChanges(
|
|
55
|
+
whereExpression,
|
|
56
|
+
orderByInfo
|
|
70
57
|
)
|
|
71
|
-
) {
|
|
72
|
-
subscription = this.subscribeToOrderedChanges(whereExpression)
|
|
73
58
|
} else {
|
|
74
|
-
// If the
|
|
75
|
-
const includeInitialState =
|
|
76
|
-
|
|
59
|
+
// If the source alias is lazy then we should not include the initial state
|
|
60
|
+
const includeInitialState = !this.collectionConfigBuilder.isLazyAlias(
|
|
61
|
+
this.alias
|
|
62
|
+
)
|
|
77
63
|
|
|
78
64
|
subscription = this.subscribeToMatchingChanges(
|
|
79
65
|
whereExpression,
|
|
@@ -83,7 +69,11 @@ export class CollectionSubscriber<
|
|
|
83
69
|
const unsubscribe = () => {
|
|
84
70
|
subscription.unsubscribe()
|
|
85
71
|
}
|
|
86
|
-
|
|
72
|
+
// currentSyncState is always defined when subscribe() is called
|
|
73
|
+
// (called during sync session setup)
|
|
74
|
+
this.collectionConfigBuilder.currentSyncState!.unsubscribeCallbacks.add(
|
|
75
|
+
unsubscribe
|
|
76
|
+
)
|
|
87
77
|
return subscription
|
|
88
78
|
}
|
|
89
79
|
|
|
@@ -91,7 +81,10 @@ export class CollectionSubscriber<
|
|
|
91
81
|
changes: Iterable<ChangeMessage<any, string | number>>,
|
|
92
82
|
callback?: () => boolean
|
|
93
83
|
) {
|
|
94
|
-
|
|
84
|
+
// currentSyncState and input are always defined when this method is called
|
|
85
|
+
// (only called from active subscriptions during a sync session)
|
|
86
|
+
const input =
|
|
87
|
+
this.collectionConfigBuilder.currentSyncState!.inputs[this.alias]!
|
|
95
88
|
const sentChanges = sendChangesToInput(
|
|
96
89
|
input,
|
|
97
90
|
changes,
|
|
@@ -103,14 +96,12 @@ export class CollectionSubscriber<
|
|
|
103
96
|
// otherwise we end up in an infinite loop trying to load more data
|
|
104
97
|
const dataLoader = sentChanges > 0 ? callback : undefined
|
|
105
98
|
|
|
106
|
-
// We need to
|
|
99
|
+
// We need to schedule a graph run even if there's no data to load
|
|
107
100
|
// because we need to mark the collection as ready if it's not already
|
|
108
|
-
// and that's only done in `
|
|
109
|
-
this.collectionConfigBuilder.
|
|
110
|
-
this.
|
|
111
|
-
|
|
112
|
-
dataLoader
|
|
113
|
-
)
|
|
101
|
+
// and that's only done in `scheduleGraphRun`
|
|
102
|
+
this.collectionConfigBuilder.scheduleGraphRun(dataLoader, {
|
|
103
|
+
alias: this.alias,
|
|
104
|
+
})
|
|
114
105
|
}
|
|
115
106
|
|
|
116
107
|
private subscribeToMatchingChanges(
|
|
@@ -132,12 +123,11 @@ export class CollectionSubscriber<
|
|
|
132
123
|
}
|
|
133
124
|
|
|
134
125
|
private subscribeToOrderedChanges(
|
|
135
|
-
whereExpression: BasicExpression<boolean> | undefined
|
|
126
|
+
whereExpression: BasicExpression<boolean> | undefined,
|
|
127
|
+
orderByInfo: OrderByOptimizationInfo
|
|
136
128
|
) {
|
|
137
129
|
const { orderBy, offset, limit, comparator, dataNeeded, index } =
|
|
138
|
-
|
|
139
|
-
this.collectionId
|
|
140
|
-
]!
|
|
130
|
+
orderByInfo
|
|
141
131
|
|
|
142
132
|
const sendChangesInRange = (
|
|
143
133
|
changes: Iterable<ChangeMessage<any, string | number>>
|
|
@@ -147,7 +137,7 @@ export class CollectionSubscriber<
|
|
|
147
137
|
// because they can't affect the topK (and if later we need more data, we will dynamically load more data)
|
|
148
138
|
const splittedChanges = splitUpdates(changes)
|
|
149
139
|
let filteredChanges = splittedChanges
|
|
150
|
-
if (dataNeeded
|
|
140
|
+
if (dataNeeded && dataNeeded() === 0) {
|
|
151
141
|
// If the topK is full [..., maxSentValue] then we do not need to send changes > maxSentValue
|
|
152
142
|
// because they can never make it into the topK.
|
|
153
143
|
// However, if the topK isn't full yet, we need to also send changes > maxSentValue
|
|
@@ -173,7 +163,7 @@ export class CollectionSubscriber<
|
|
|
173
163
|
// Normalize the orderBy clauses such that the references are relative to the collection
|
|
174
164
|
const normalizedOrderBy = convertOrderByToBasicExpression(
|
|
175
165
|
orderBy,
|
|
176
|
-
this.
|
|
166
|
+
this.alias
|
|
177
167
|
)
|
|
178
168
|
|
|
179
169
|
// Load the first `offset + limit` values from the index
|
|
@@ -190,10 +180,7 @@ export class CollectionSubscriber<
|
|
|
190
180
|
// after each iteration of the query pipeline
|
|
191
181
|
// to ensure that the orderBy operator has enough data to work with
|
|
192
182
|
loadMoreIfNeeded(subscription: CollectionSubscription) {
|
|
193
|
-
const orderByInfo =
|
|
194
|
-
this.collectionConfigBuilder.optimizableOrderByCollections[
|
|
195
|
-
this.collectionId
|
|
196
|
-
]
|
|
183
|
+
const orderByInfo = this.getOrderByInfo()
|
|
197
184
|
|
|
198
185
|
if (!orderByInfo) {
|
|
199
186
|
// This query has no orderBy operator
|
|
@@ -224,24 +211,40 @@ export class CollectionSubscriber<
|
|
|
224
211
|
changes: Iterable<ChangeMessage<any, string | number>>,
|
|
225
212
|
subscription: CollectionSubscription
|
|
226
213
|
) {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
214
|
+
const orderByInfo = this.getOrderByInfo()
|
|
215
|
+
if (!orderByInfo) {
|
|
216
|
+
this.sendChangesToPipeline(changes)
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const trackedChanges = this.trackSentValues(changes, orderByInfo.comparator)
|
|
221
|
+
|
|
222
|
+
// Cache the loadMoreIfNeeded callback on the subscription using a symbol property.
|
|
223
|
+
// This ensures we pass the same function instance to the scheduler each time,
|
|
224
|
+
// allowing it to deduplicate callbacks when multiple changes arrive during a transaction.
|
|
225
|
+
type SubscriptionWithLoader = CollectionSubscription & {
|
|
226
|
+
[loadMoreCallbackSymbol]?: () => boolean
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const subscriptionWithLoader = subscription as SubscriptionWithLoader
|
|
230
|
+
|
|
231
|
+
subscriptionWithLoader[loadMoreCallbackSymbol] ??=
|
|
232
|
+
this.loadMoreIfNeeded.bind(this, subscription)
|
|
233
|
+
|
|
232
234
|
this.sendChangesToPipeline(
|
|
233
235
|
trackedChanges,
|
|
234
|
-
|
|
236
|
+
subscriptionWithLoader[loadMoreCallbackSymbol]
|
|
235
237
|
)
|
|
236
238
|
}
|
|
237
239
|
|
|
238
240
|
// Loads the next `n` items from the collection
|
|
239
241
|
// starting from the biggest item it has sent
|
|
240
242
|
private loadNextItems(n: number, subscription: CollectionSubscription) {
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
243
|
+
const orderByInfo = this.getOrderByInfo()
|
|
244
|
+
if (!orderByInfo) {
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
const { orderBy, valueExtractorForRawRow } = orderByInfo
|
|
245
248
|
const biggestSentRow = this.biggest
|
|
246
249
|
const biggestSentValue = biggestSentRow
|
|
247
250
|
? valueExtractorForRawRow(biggestSentRow)
|
|
@@ -250,7 +253,7 @@ export class CollectionSubscriber<
|
|
|
250
253
|
// Normalize the orderBy clauses such that the references are relative to the collection
|
|
251
254
|
const normalizedOrderBy = convertOrderByToBasicExpression(
|
|
252
255
|
orderBy,
|
|
253
|
-
this.
|
|
256
|
+
this.alias
|
|
254
257
|
)
|
|
255
258
|
|
|
256
259
|
// Take the `n` items after the biggest sent value
|
|
@@ -261,13 +264,22 @@ export class CollectionSubscriber<
|
|
|
261
264
|
})
|
|
262
265
|
}
|
|
263
266
|
|
|
264
|
-
private
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
267
|
+
private getWhereClauseForAlias(): BasicExpression<boolean> | undefined {
|
|
268
|
+
const sourceWhereClausesCache =
|
|
269
|
+
this.collectionConfigBuilder.sourceWhereClausesCache
|
|
270
|
+
if (!sourceWhereClausesCache) {
|
|
271
|
+
return undefined
|
|
272
|
+
}
|
|
273
|
+
return sourceWhereClausesCache.get(this.alias)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private getOrderByInfo(): OrderByOptimizationInfo | undefined {
|
|
277
|
+
const info =
|
|
278
|
+
this.collectionConfigBuilder.optimizableOrderByCollections[
|
|
279
|
+
this.collectionId
|
|
280
|
+
]
|
|
281
|
+
if (info && info.alias === this.alias) {
|
|
282
|
+
return info
|
|
271
283
|
}
|
|
272
284
|
return undefined
|
|
273
285
|
}
|
|
@@ -288,36 +300,6 @@ export class CollectionSubscriber<
|
|
|
288
300
|
}
|
|
289
301
|
}
|
|
290
302
|
|
|
291
|
-
/**
|
|
292
|
-
* Finds the alias for a collection ID in the query
|
|
293
|
-
*/
|
|
294
|
-
function findCollectionAlias(
|
|
295
|
-
collectionId: string,
|
|
296
|
-
query: any
|
|
297
|
-
): string | undefined {
|
|
298
|
-
// Check FROM clause
|
|
299
|
-
if (
|
|
300
|
-
query.from?.type === `collectionRef` &&
|
|
301
|
-
query.from.collection?.id === collectionId
|
|
302
|
-
) {
|
|
303
|
-
return query.from.alias
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Check JOIN clauses
|
|
307
|
-
if (query.join) {
|
|
308
|
-
for (const joinClause of query.join) {
|
|
309
|
-
if (
|
|
310
|
-
joinClause.from?.type === `collectionRef` &&
|
|
311
|
-
joinClause.from.collection?.id === collectionId
|
|
312
|
-
) {
|
|
313
|
-
return joinClause.from.alias
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return undefined
|
|
319
|
-
}
|
|
320
|
-
|
|
321
303
|
/**
|
|
322
304
|
* Helper function to send changes to a D2 input stream
|
|
323
305
|
*/
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { createCollection } from "../collection/index.js"
|
|
2
2
|
import { CollectionConfigBuilder } from "./live/collection-config-builder.js"
|
|
3
|
+
import {
|
|
4
|
+
getBuilderFromConfig,
|
|
5
|
+
registerCollectionBuilder,
|
|
6
|
+
} from "./live/collection-registry.js"
|
|
7
|
+
import type { LiveQueryCollectionUtils } from "./live/collection-config-builder.js"
|
|
3
8
|
import type { LiveQueryCollectionConfig } from "./live/types.js"
|
|
4
9
|
import type { InitialQueryBuilder, QueryBuilder } from "./builder/index.js"
|
|
5
10
|
import type { Collection } from "../collection/index.js"
|
|
@@ -55,7 +60,9 @@ export function liveQueryCollectionOptions<
|
|
|
55
60
|
TResult extends object = GetResult<TContext>,
|
|
56
61
|
>(
|
|
57
62
|
config: LiveQueryCollectionConfig<TContext, TResult>
|
|
58
|
-
): CollectionConfigForContext<TContext, TResult> {
|
|
63
|
+
): CollectionConfigForContext<TContext, TResult> & {
|
|
64
|
+
utils: LiveQueryCollectionUtils
|
|
65
|
+
} {
|
|
59
66
|
const collectionConfigBuilder = new CollectionConfigBuilder<
|
|
60
67
|
TContext,
|
|
61
68
|
TResult
|
|
@@ -63,7 +70,7 @@ export function liveQueryCollectionOptions<
|
|
|
63
70
|
return collectionConfigBuilder.getConfig() as CollectionConfigForContext<
|
|
64
71
|
TContext,
|
|
65
72
|
TResult
|
|
66
|
-
>
|
|
73
|
+
> & { utils: LiveQueryCollectionUtils }
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
/**
|
|
@@ -106,7 +113,9 @@ export function createLiveQueryCollection<
|
|
|
106
113
|
TResult extends object = GetResult<TContext>,
|
|
107
114
|
>(
|
|
108
115
|
query: (q: InitialQueryBuilder) => QueryBuilder<TContext>
|
|
109
|
-
): CollectionForContext<TContext, TResult>
|
|
116
|
+
): CollectionForContext<TContext, TResult> & {
|
|
117
|
+
utils: LiveQueryCollectionUtils
|
|
118
|
+
}
|
|
110
119
|
|
|
111
120
|
// Overload 2: Accept full config object with optional utilities
|
|
112
121
|
export function createLiveQueryCollection<
|
|
@@ -115,7 +124,9 @@ export function createLiveQueryCollection<
|
|
|
115
124
|
TUtils extends UtilsRecord = {},
|
|
116
125
|
>(
|
|
117
126
|
config: LiveQueryCollectionConfig<TContext, TResult> & { utils?: TUtils }
|
|
118
|
-
): CollectionForContext<TContext, TResult>
|
|
127
|
+
): CollectionForContext<TContext, TResult> & {
|
|
128
|
+
utils: LiveQueryCollectionUtils & TUtils
|
|
129
|
+
}
|
|
119
130
|
|
|
120
131
|
// Implementation
|
|
121
132
|
export function createLiveQueryCollection<
|
|
@@ -126,7 +137,9 @@ export function createLiveQueryCollection<
|
|
|
126
137
|
configOrQuery:
|
|
127
138
|
| (LiveQueryCollectionConfig<TContext, TResult> & { utils?: TUtils })
|
|
128
139
|
| ((q: InitialQueryBuilder) => QueryBuilder<TContext>)
|
|
129
|
-
): CollectionForContext<TContext, TResult> {
|
|
140
|
+
): CollectionForContext<TContext, TResult> & {
|
|
141
|
+
utils: LiveQueryCollectionUtils & TUtils
|
|
142
|
+
} {
|
|
130
143
|
// Determine if the argument is a function (query) or a config object
|
|
131
144
|
if (typeof configOrQuery === `function`) {
|
|
132
145
|
// Simple query function case
|
|
@@ -139,7 +152,7 @@ export function createLiveQueryCollection<
|
|
|
139
152
|
return bridgeToCreateCollection(options) as CollectionForContext<
|
|
140
153
|
TContext,
|
|
141
154
|
TResult
|
|
142
|
-
>
|
|
155
|
+
> & { utils: LiveQueryCollectionUtils & TUtils }
|
|
143
156
|
} else {
|
|
144
157
|
// Config object case
|
|
145
158
|
const config = configOrQuery as LiveQueryCollectionConfig<
|
|
@@ -147,10 +160,16 @@ export function createLiveQueryCollection<
|
|
|
147
160
|
TResult
|
|
148
161
|
> & { utils?: TUtils }
|
|
149
162
|
const options = liveQueryCollectionOptions<TContext, TResult>(config)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
163
|
+
|
|
164
|
+
// Merge custom utils if provided, preserving the getBuilder() method for dependency tracking
|
|
165
|
+
if (config.utils) {
|
|
166
|
+
options.utils = { ...options.utils, ...config.utils }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return bridgeToCreateCollection(options) as CollectionForContext<
|
|
170
|
+
TContext,
|
|
171
|
+
TResult
|
|
172
|
+
> & { utils: LiveQueryCollectionUtils & TUtils }
|
|
154
173
|
}
|
|
155
174
|
}
|
|
156
175
|
|
|
@@ -162,12 +181,18 @@ function bridgeToCreateCollection<
|
|
|
162
181
|
TResult extends object,
|
|
163
182
|
TUtils extends UtilsRecord = {},
|
|
164
183
|
>(
|
|
165
|
-
options: CollectionConfig<TResult> & { utils
|
|
184
|
+
options: CollectionConfig<TResult> & { utils: TUtils }
|
|
166
185
|
): Collection<TResult, string | number, TUtils> {
|
|
167
|
-
|
|
168
|
-
return createCollection(options as any) as unknown as Collection<
|
|
186
|
+
const collection = createCollection(options as any) as unknown as Collection<
|
|
169
187
|
TResult,
|
|
170
188
|
string | number,
|
|
171
|
-
|
|
189
|
+
LiveQueryCollectionUtils
|
|
172
190
|
>
|
|
191
|
+
|
|
192
|
+
const builder = getBuilderFromConfig(options)
|
|
193
|
+
if (builder) {
|
|
194
|
+
registerCollectionBuilder(collection, builder)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return collection as unknown as Collection<TResult, string | number, TUtils>
|
|
173
198
|
}
|
package/src/query/optimizer.ts
CHANGED
|
@@ -162,8 +162,8 @@ export interface GroupedWhereClauses {
|
|
|
162
162
|
export interface OptimizationResult {
|
|
163
163
|
/** The optimized query with WHERE clauses potentially moved to subqueries */
|
|
164
164
|
optimizedQuery: QueryIR
|
|
165
|
-
/** Map of
|
|
166
|
-
|
|
165
|
+
/** Map of source aliases to their extracted WHERE clauses for index optimization */
|
|
166
|
+
sourceWhereClauses: Map<string, BasicExpression<boolean>>
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
/**
|
|
@@ -184,14 +184,14 @@ export interface OptimizationResult {
|
|
|
184
184
|
* where: [eq(u.dept_id, 1), gt(p.views, 100)]
|
|
185
185
|
* }
|
|
186
186
|
*
|
|
187
|
-
* const { optimizedQuery,
|
|
187
|
+
* const { optimizedQuery, sourceWhereClauses } = optimizeQuery(originalQuery)
|
|
188
188
|
* // Result: Single-source clauses moved to deepest possible subqueries
|
|
189
|
-
* //
|
|
189
|
+
* // sourceWhereClauses: Map { 'u' => eq(u.dept_id, 1), 'p' => gt(p.views, 100) }
|
|
190
190
|
* ```
|
|
191
191
|
*/
|
|
192
192
|
export function optimizeQuery(query: QueryIR): OptimizationResult {
|
|
193
|
-
// First, extract
|
|
194
|
-
const
|
|
193
|
+
// First, extract source WHERE clauses before optimization
|
|
194
|
+
const sourceWhereClauses = extractSourceWhereClauses(query)
|
|
195
195
|
|
|
196
196
|
// Apply multi-level predicate pushdown with iterative convergence
|
|
197
197
|
let optimized = query
|
|
@@ -214,7 +214,7 @@ export function optimizeQuery(query: QueryIR): OptimizationResult {
|
|
|
214
214
|
|
|
215
215
|
return {
|
|
216
216
|
optimizedQuery: cleaned,
|
|
217
|
-
|
|
217
|
+
sourceWhereClauses,
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
|
|
@@ -224,16 +224,16 @@ export function optimizeQuery(query: QueryIR): OptimizationResult {
|
|
|
224
224
|
* to specific collections, but only for simple queries without joins.
|
|
225
225
|
*
|
|
226
226
|
* @param query - The original QueryIR to analyze
|
|
227
|
-
* @returns Map of
|
|
227
|
+
* @returns Map of source aliases to their WHERE clauses
|
|
228
228
|
*/
|
|
229
|
-
function
|
|
229
|
+
function extractSourceWhereClauses(
|
|
230
230
|
query: QueryIR
|
|
231
231
|
): Map<string, BasicExpression<boolean>> {
|
|
232
|
-
const
|
|
232
|
+
const sourceWhereClauses = new Map<string, BasicExpression<boolean>>()
|
|
233
233
|
|
|
234
234
|
// Only analyze queries that have WHERE clauses
|
|
235
235
|
if (!query.where || query.where.length === 0) {
|
|
236
|
-
return
|
|
236
|
+
return sourceWhereClauses
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
// Split all AND clauses at the root level for granular analysis
|
|
@@ -254,12 +254,12 @@ function extractCollectionWhereClauses(
|
|
|
254
254
|
if (isCollectionReference(query, sourceAlias)) {
|
|
255
255
|
// Check if the WHERE clause can be converted to collection-compatible format
|
|
256
256
|
if (isConvertibleToCollectionFilter(whereClause)) {
|
|
257
|
-
|
|
257
|
+
sourceWhereClauses.set(sourceAlias, whereClause)
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
return
|
|
262
|
+
return sourceWhereClauses
|
|
263
263
|
}
|
|
264
264
|
|
|
265
265
|
/**
|
|
@@ -782,8 +782,6 @@ function optimizeFromWithTracking(
|
|
|
782
782
|
return new QueryRefClass(subQuery, from.alias)
|
|
783
783
|
}
|
|
784
784
|
|
|
785
|
-
// Must be queryRef due to type system
|
|
786
|
-
|
|
787
785
|
// SAFETY CHECK: Only check safety when pushing WHERE clauses into existing subqueries
|
|
788
786
|
// We need to be careful about pushing WHERE clauses into subqueries that already have
|
|
789
787
|
// aggregates, HAVING, or ORDER BY + LIMIT since that could change their semantics
|
|
@@ -793,6 +791,12 @@ function optimizeFromWithTracking(
|
|
|
793
791
|
return new QueryRefClass(deepCopyQuery(from.query), from.alias)
|
|
794
792
|
}
|
|
795
793
|
|
|
794
|
+
// Skip pushdown when a clause references a field that only exists via a renamed
|
|
795
|
+
// projection inside the subquery; leaving it outside preserves the alias mapping.
|
|
796
|
+
if (referencesAliasWithRemappedSelect(from.query, whereClause, from.alias)) {
|
|
797
|
+
return new QueryRefClass(deepCopyQuery(from.query), from.alias)
|
|
798
|
+
}
|
|
799
|
+
|
|
796
800
|
// Add the WHERE clause to the existing subquery
|
|
797
801
|
// Create a deep copy to ensure immutability
|
|
798
802
|
const existingWhere = from.query.where || []
|
|
@@ -943,6 +947,72 @@ function whereReferencesComputedSelectFields(
|
|
|
943
947
|
return false
|
|
944
948
|
}
|
|
945
949
|
|
|
950
|
+
/**
|
|
951
|
+
* Detects whether a WHERE clause references the subquery alias through fields that
|
|
952
|
+
* are re-exposed under different names (renamed SELECT projections or fnSelect output).
|
|
953
|
+
* In those cases we keep the clause at the outer level to avoid alias remapping bugs.
|
|
954
|
+
* TODO: in future we should handle this by rewriting the clause to use the subquery's
|
|
955
|
+
* internal field references, but it likely needs a wider refactor to do cleanly.
|
|
956
|
+
*/
|
|
957
|
+
function referencesAliasWithRemappedSelect(
|
|
958
|
+
subquery: QueryIR,
|
|
959
|
+
whereClause: BasicExpression<boolean>,
|
|
960
|
+
outerAlias: string
|
|
961
|
+
): boolean {
|
|
962
|
+
const refs = collectRefs(whereClause)
|
|
963
|
+
// Only care about clauses that actually reference the outer alias.
|
|
964
|
+
if (refs.every((ref) => ref.path[0] !== outerAlias)) {
|
|
965
|
+
return false
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// fnSelect always rewrites the row shape, so alias-safe pushdown is impossible.
|
|
969
|
+
if (subquery.fnSelect) {
|
|
970
|
+
return true
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
const select = subquery.select
|
|
974
|
+
// Without an explicit SELECT the clause still refers to the original collection.
|
|
975
|
+
if (!select) {
|
|
976
|
+
return false
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
for (const ref of refs) {
|
|
980
|
+
const path = ref.path
|
|
981
|
+
// Need at least alias + field to matter.
|
|
982
|
+
if (path.length < 2) continue
|
|
983
|
+
if (path[0] !== outerAlias) continue
|
|
984
|
+
|
|
985
|
+
const projected = select[path[1]!]
|
|
986
|
+
// Unselected fields can't be remapped, so skip - only care about fields in the SELECT.
|
|
987
|
+
if (!projected) continue
|
|
988
|
+
|
|
989
|
+
// Non-PropRef projections are computed values; cannot push down.
|
|
990
|
+
if (!(projected instanceof PropRef)) {
|
|
991
|
+
return true
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// If the projection is just the alias (whole row) without a specific field,
|
|
995
|
+
// we can't verify whether the field we're referencing is being preserved or remapped.
|
|
996
|
+
if (projected.path.length < 2) {
|
|
997
|
+
return true
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const [innerAlias, innerField] = projected.path
|
|
1001
|
+
|
|
1002
|
+
// Safe only when the projection points straight back to the same alias or the
|
|
1003
|
+
// underlying source alias and preserves the field name.
|
|
1004
|
+
if (innerAlias !== outerAlias && innerAlias !== subquery.from.alias) {
|
|
1005
|
+
return true
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if (innerField !== path[1]) {
|
|
1009
|
+
return true
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
return false
|
|
1014
|
+
}
|
|
1015
|
+
|
|
946
1016
|
/**
|
|
947
1017
|
* Helper function to combine multiple expressions with AND.
|
|
948
1018
|
*
|