@tanstack/db 0.1.3 → 0.1.4
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.cjs +112 -6
- package/dist/cjs/collection.cjs.map +1 -1
- package/dist/cjs/collection.d.cts +3 -2
- package/dist/cjs/errors.cjs +6 -0
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +3 -0
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/indexes/auto-index.cjs +30 -19
- package/dist/cjs/indexes/auto-index.cjs.map +1 -1
- package/dist/cjs/indexes/auto-index.d.cts +1 -0
- package/dist/cjs/indexes/base-index.cjs.map +1 -1
- package/dist/cjs/indexes/base-index.d.cts +2 -1
- package/dist/cjs/indexes/btree-index.cjs +26 -0
- package/dist/cjs/indexes/btree-index.cjs.map +1 -1
- package/dist/cjs/indexes/btree-index.d.cts +7 -0
- package/dist/cjs/indexes/index-options.d.cts +1 -1
- package/dist/cjs/query/compiler/evaluators.cjs +2 -2
- package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
- package/dist/cjs/query/compiler/evaluators.d.cts +1 -1
- package/dist/cjs/query/compiler/group-by.cjs +3 -1
- package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.cjs +72 -6
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.d.cts +16 -2
- package/dist/cjs/query/compiler/joins.cjs +111 -12
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/compiler/joins.d.cts +9 -2
- package/dist/cjs/query/compiler/order-by.cjs +62 -3
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.d.cts +12 -2
- package/dist/cjs/query/live-query-collection.cjs +196 -23
- package/dist/cjs/query/live-query-collection.cjs.map +1 -1
- package/dist/cjs/types.d.cts +1 -0
- package/dist/cjs/utils/btree.cjs +15 -0
- package/dist/cjs/utils/btree.cjs.map +1 -1
- package/dist/cjs/utils/btree.d.cts +8 -0
- package/dist/esm/collection.d.ts +3 -2
- package/dist/esm/collection.js +113 -7
- package/dist/esm/collection.js.map +1 -1
- package/dist/esm/errors.d.ts +3 -0
- package/dist/esm/errors.js +6 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/indexes/auto-index.d.ts +1 -0
- package/dist/esm/indexes/auto-index.js +31 -20
- package/dist/esm/indexes/auto-index.js.map +1 -1
- package/dist/esm/indexes/base-index.d.ts +2 -1
- package/dist/esm/indexes/base-index.js.map +1 -1
- package/dist/esm/indexes/btree-index.d.ts +7 -0
- package/dist/esm/indexes/btree-index.js +26 -0
- package/dist/esm/indexes/btree-index.js.map +1 -1
- package/dist/esm/indexes/index-options.d.ts +1 -1
- package/dist/esm/query/compiler/evaluators.d.ts +1 -1
- package/dist/esm/query/compiler/evaluators.js +2 -2
- package/dist/esm/query/compiler/evaluators.js.map +1 -1
- package/dist/esm/query/compiler/group-by.js +3 -1
- package/dist/esm/query/compiler/group-by.js.map +1 -1
- package/dist/esm/query/compiler/index.d.ts +16 -2
- package/dist/esm/query/compiler/index.js +73 -7
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/joins.d.ts +9 -2
- package/dist/esm/query/compiler/joins.js +114 -15
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/compiler/order-by.d.ts +12 -2
- package/dist/esm/query/compiler/order-by.js +62 -3
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/live-query-collection.js +196 -23
- package/dist/esm/query/live-query-collection.js.map +1 -1
- package/dist/esm/types.d.ts +1 -0
- package/dist/esm/utils/btree.d.ts +8 -0
- package/dist/esm/utils/btree.js +15 -0
- package/dist/esm/utils/btree.js.map +1 -1
- package/package.json +2 -2
- package/src/collection.ts +163 -10
- package/src/errors.ts +6 -0
- package/src/indexes/auto-index.ts +53 -31
- package/src/indexes/base-index.ts +6 -1
- package/src/indexes/btree-index.ts +29 -0
- package/src/indexes/index-options.ts +2 -2
- package/src/query/compiler/evaluators.ts +6 -3
- package/src/query/compiler/group-by.ts +3 -1
- package/src/query/compiler/index.ts +112 -5
- package/src/query/compiler/joins.ts +216 -20
- package/src/query/compiler/order-by.ts +98 -3
- package/src/query/live-query-collection.ts +352 -24
- package/src/types.ts +1 -0
- package/src/utils/btree.ts +17 -0
|
@@ -3,30 +3,45 @@ import {
|
|
|
3
3
|
filter,
|
|
4
4
|
join as joinOperator,
|
|
5
5
|
map,
|
|
6
|
+
tap,
|
|
6
7
|
} from "@tanstack/db-ivm"
|
|
7
8
|
import {
|
|
8
9
|
CollectionInputNotFoundError,
|
|
9
10
|
InvalidJoinConditionSameTableError,
|
|
10
11
|
InvalidJoinConditionTableMismatchError,
|
|
11
12
|
InvalidJoinConditionWrongTablesError,
|
|
13
|
+
JoinCollectionNotFoundError,
|
|
12
14
|
UnsupportedJoinSourceTypeError,
|
|
13
15
|
UnsupportedJoinTypeError,
|
|
14
16
|
} from "../../errors.js"
|
|
17
|
+
import { findIndexForField } from "../../utils/index-optimization.js"
|
|
18
|
+
import { ensureIndexForField } from "../../indexes/auto-index.js"
|
|
15
19
|
import { compileExpression } from "./evaluators.js"
|
|
16
|
-
import { compileQuery } from "./index.js"
|
|
17
|
-
import type {
|
|
20
|
+
import { compileQuery, followRef } from "./index.js"
|
|
21
|
+
import type { OrderByOptimizationInfo } from "./order-by.js"
|
|
18
22
|
import type {
|
|
19
23
|
BasicExpression,
|
|
20
24
|
CollectionRef,
|
|
21
25
|
JoinClause,
|
|
26
|
+
PropRef,
|
|
27
|
+
QueryIR,
|
|
22
28
|
QueryRef,
|
|
23
29
|
} from "../ir.js"
|
|
30
|
+
import type { IStreamBuilder, JoinType } from "@tanstack/db-ivm"
|
|
31
|
+
import type { Collection } from "../../collection.js"
|
|
24
32
|
import type {
|
|
25
33
|
KeyedStream,
|
|
26
34
|
NamespacedAndKeyedStream,
|
|
27
35
|
NamespacedRow,
|
|
28
36
|
} from "../../types.js"
|
|
29
37
|
import type { QueryCache, QueryMapping } from "./types.js"
|
|
38
|
+
import type { BaseIndex } from "../../indexes/base-index.js"
|
|
39
|
+
|
|
40
|
+
export type LoadKeysFn = (key: Set<string | number>) => void
|
|
41
|
+
export type LazyCollectionCallbacks = {
|
|
42
|
+
loadKeys: LoadKeysFn
|
|
43
|
+
loadInitialState: () => void
|
|
44
|
+
}
|
|
30
45
|
|
|
31
46
|
/**
|
|
32
47
|
* Processes all join clauses in a query
|
|
@@ -35,10 +50,16 @@ export function processJoins(
|
|
|
35
50
|
pipeline: NamespacedAndKeyedStream,
|
|
36
51
|
joinClauses: Array<JoinClause>,
|
|
37
52
|
tables: Record<string, KeyedStream>,
|
|
53
|
+
mainTableId: string,
|
|
38
54
|
mainTableAlias: string,
|
|
39
55
|
allInputs: Record<string, KeyedStream>,
|
|
40
56
|
cache: QueryCache,
|
|
41
|
-
queryMapping: QueryMapping
|
|
57
|
+
queryMapping: QueryMapping,
|
|
58
|
+
collections: Record<string, Collection>,
|
|
59
|
+
callbacks: Record<string, LazyCollectionCallbacks>,
|
|
60
|
+
lazyCollections: Set<string>,
|
|
61
|
+
optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
|
|
62
|
+
rawQuery: QueryIR
|
|
42
63
|
): NamespacedAndKeyedStream {
|
|
43
64
|
let resultPipeline = pipeline
|
|
44
65
|
|
|
@@ -47,10 +68,16 @@ export function processJoins(
|
|
|
47
68
|
resultPipeline,
|
|
48
69
|
joinClause,
|
|
49
70
|
tables,
|
|
71
|
+
mainTableId,
|
|
50
72
|
mainTableAlias,
|
|
51
73
|
allInputs,
|
|
52
74
|
cache,
|
|
53
|
-
queryMapping
|
|
75
|
+
queryMapping,
|
|
76
|
+
collections,
|
|
77
|
+
callbacks,
|
|
78
|
+
lazyCollections,
|
|
79
|
+
optimizableOrderByCollections,
|
|
80
|
+
rawQuery
|
|
54
81
|
)
|
|
55
82
|
}
|
|
56
83
|
|
|
@@ -64,15 +91,29 @@ function processJoin(
|
|
|
64
91
|
pipeline: NamespacedAndKeyedStream,
|
|
65
92
|
joinClause: JoinClause,
|
|
66
93
|
tables: Record<string, KeyedStream>,
|
|
94
|
+
mainTableId: string,
|
|
67
95
|
mainTableAlias: string,
|
|
68
96
|
allInputs: Record<string, KeyedStream>,
|
|
69
97
|
cache: QueryCache,
|
|
70
|
-
queryMapping: QueryMapping
|
|
98
|
+
queryMapping: QueryMapping,
|
|
99
|
+
collections: Record<string, Collection>,
|
|
100
|
+
callbacks: Record<string, LazyCollectionCallbacks>,
|
|
101
|
+
lazyCollections: Set<string>,
|
|
102
|
+
optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
|
|
103
|
+
rawQuery: QueryIR
|
|
71
104
|
): NamespacedAndKeyedStream {
|
|
72
105
|
// Get the joined table alias and input stream
|
|
73
|
-
const {
|
|
106
|
+
const {
|
|
107
|
+
alias: joinedTableAlias,
|
|
108
|
+
input: joinedInput,
|
|
109
|
+
collectionId: joinedCollectionId,
|
|
110
|
+
} = processJoinSource(
|
|
74
111
|
joinClause.from,
|
|
75
112
|
allInputs,
|
|
113
|
+
collections,
|
|
114
|
+
callbacks,
|
|
115
|
+
lazyCollections,
|
|
116
|
+
optimizableOrderByCollections,
|
|
76
117
|
cache,
|
|
77
118
|
queryMapping
|
|
78
119
|
)
|
|
@@ -80,13 +121,22 @@ function processJoin(
|
|
|
80
121
|
// Add the joined table to the tables map
|
|
81
122
|
tables[joinedTableAlias] = joinedInput
|
|
82
123
|
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
124
|
+
const mainCollection = collections[mainTableId]
|
|
125
|
+
const joinedCollection = collections[joinedCollectionId]
|
|
126
|
+
|
|
127
|
+
if (!mainCollection) {
|
|
128
|
+
throw new JoinCollectionNotFoundError(mainTableId)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!joinedCollection) {
|
|
132
|
+
throw new JoinCollectionNotFoundError(joinedCollectionId)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const { activeCollection, lazyCollection } = getActiveAndLazyCollections(
|
|
136
|
+
joinClause.type,
|
|
137
|
+
mainCollection,
|
|
138
|
+
joinedCollection
|
|
139
|
+
)
|
|
90
140
|
|
|
91
141
|
// Analyze which table each expression refers to and swap if necessary
|
|
92
142
|
const { mainExpr, joinedExpr } = analyzeJoinExpressions(
|
|
@@ -101,7 +151,7 @@ function processJoin(
|
|
|
101
151
|
const compiledJoinedExpr = compileExpression(joinedExpr)
|
|
102
152
|
|
|
103
153
|
// Prepare the main pipeline for joining
|
|
104
|
-
|
|
154
|
+
let mainPipeline = pipeline.pipe(
|
|
105
155
|
map(([currentKey, namespacedRow]) => {
|
|
106
156
|
// Extract the join key from the main table expression
|
|
107
157
|
const mainKey = compiledMainExpr(namespacedRow)
|
|
@@ -115,7 +165,7 @@ function processJoin(
|
|
|
115
165
|
)
|
|
116
166
|
|
|
117
167
|
// Prepare the joined pipeline
|
|
118
|
-
|
|
168
|
+
let joinedPipeline = joinedInput.pipe(
|
|
119
169
|
map(([currentKey, row]) => {
|
|
120
170
|
// Wrap the row in a namespaced structure
|
|
121
171
|
const namespacedRow: NamespacedRow = { [joinedTableAlias]: row }
|
|
@@ -132,11 +182,103 @@ function processJoin(
|
|
|
132
182
|
)
|
|
133
183
|
|
|
134
184
|
// Apply the join operation
|
|
135
|
-
if (![`inner`, `left`, `right`, `full`].includes(
|
|
185
|
+
if (![`inner`, `left`, `right`, `full`].includes(joinClause.type)) {
|
|
136
186
|
throw new UnsupportedJoinTypeError(joinClause.type)
|
|
137
187
|
}
|
|
188
|
+
|
|
189
|
+
if (activeCollection) {
|
|
190
|
+
// This join can be optimized by having the active collection
|
|
191
|
+
// dynamically load keys into the lazy collection
|
|
192
|
+
// based on the value of the joinKey and by looking up
|
|
193
|
+
// matching rows in the index of the lazy collection
|
|
194
|
+
|
|
195
|
+
// Mark the lazy collection as lazy
|
|
196
|
+
// this Set is passed by the liveQueryCollection to the compiler
|
|
197
|
+
// such that the liveQueryCollection can check it after compilation
|
|
198
|
+
// to know which collections are lazy collections
|
|
199
|
+
lazyCollections.add(lazyCollection.id)
|
|
200
|
+
|
|
201
|
+
const activePipeline =
|
|
202
|
+
activeCollection === `main` ? mainPipeline : joinedPipeline
|
|
203
|
+
|
|
204
|
+
let index: BaseIndex<string | number> | undefined
|
|
205
|
+
|
|
206
|
+
const lazyCollectionJoinExpr =
|
|
207
|
+
activeCollection === `main`
|
|
208
|
+
? (joinedExpr as PropRef)
|
|
209
|
+
: (mainExpr as PropRef)
|
|
210
|
+
|
|
211
|
+
const activeColl =
|
|
212
|
+
activeCollection === `main` ? collections[mainTableId]! : lazyCollection
|
|
213
|
+
|
|
214
|
+
const followRefResult = followRef(
|
|
215
|
+
rawQuery,
|
|
216
|
+
lazyCollectionJoinExpr,
|
|
217
|
+
activeColl
|
|
218
|
+
)!
|
|
219
|
+
const followRefCollection = followRefResult.collection
|
|
220
|
+
|
|
221
|
+
const fieldName = followRefResult.path[0]
|
|
222
|
+
if (fieldName) {
|
|
223
|
+
ensureIndexForField(fieldName, followRefResult.path, followRefCollection)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let deoptimized = false
|
|
227
|
+
|
|
228
|
+
const activePipelineWithLoading: IStreamBuilder<
|
|
229
|
+
[key: unknown, [originalKey: string, namespacedRow: NamespacedRow]]
|
|
230
|
+
> = activePipeline.pipe(
|
|
231
|
+
tap(([joinKey, _]) => {
|
|
232
|
+
if (deoptimized) {
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Find the index for the path we join on
|
|
237
|
+
// we need to find the index inside the map operator
|
|
238
|
+
// because the indexes are only available after the initial sync
|
|
239
|
+
// so we can't fetch it during compilation
|
|
240
|
+
index ??= findIndexForField(
|
|
241
|
+
followRefCollection.indexes,
|
|
242
|
+
followRefResult.path
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
// The `callbacks` object is passed by the liveQueryCollection to the compiler.
|
|
246
|
+
// It contains a function to lazy load keys for each lazy collection
|
|
247
|
+
// as well as a function to switch back to a regular collection
|
|
248
|
+
// (useful when there's no index for available for lazily loading the collection)
|
|
249
|
+
const collectionCallbacks = callbacks[lazyCollection.id]
|
|
250
|
+
if (!collectionCallbacks) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
`Internal error: callbacks for collection are missing in join pipeline. Make sure the live query collection sets them before running the pipeline.`
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const { loadKeys, loadInitialState } = collectionCallbacks
|
|
257
|
+
|
|
258
|
+
if (index && index.supports(`eq`)) {
|
|
259
|
+
// Use the index to fetch the PKs of the rows in the lazy collection
|
|
260
|
+
// that match this row from the active collection based on the value of the joinKey
|
|
261
|
+
const matchingKeys = index.lookup(`eq`, joinKey)
|
|
262
|
+
// Inform the lazy collection that those keys need to be loaded
|
|
263
|
+
loadKeys(matchingKeys)
|
|
264
|
+
} else {
|
|
265
|
+
// We can't optimize the join because there is no index for the join key
|
|
266
|
+
// on the lazy collection, so we load the initial state
|
|
267
|
+
deoptimized = true
|
|
268
|
+
loadInitialState()
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
if (activeCollection === `main`) {
|
|
274
|
+
mainPipeline = activePipelineWithLoading
|
|
275
|
+
} else {
|
|
276
|
+
joinedPipeline = activePipelineWithLoading
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
138
280
|
return mainPipeline.pipe(
|
|
139
|
-
joinOperator(joinedPipeline,
|
|
281
|
+
joinOperator(joinedPipeline, joinClause.type as JoinType),
|
|
140
282
|
consolidate(),
|
|
141
283
|
processJoinResults(joinClause.type)
|
|
142
284
|
)
|
|
@@ -225,16 +367,20 @@ function getTableAliasFromExpression(expr: BasicExpression): string | null {
|
|
|
225
367
|
function processJoinSource(
|
|
226
368
|
from: CollectionRef | QueryRef,
|
|
227
369
|
allInputs: Record<string, KeyedStream>,
|
|
370
|
+
collections: Record<string, Collection>,
|
|
371
|
+
callbacks: Record<string, LazyCollectionCallbacks>,
|
|
372
|
+
lazyCollections: Set<string>,
|
|
373
|
+
optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
|
|
228
374
|
cache: QueryCache,
|
|
229
375
|
queryMapping: QueryMapping
|
|
230
|
-
): { alias: string; input: KeyedStream } {
|
|
376
|
+
): { alias: string; input: KeyedStream; collectionId: string } {
|
|
231
377
|
switch (from.type) {
|
|
232
378
|
case `collectionRef`: {
|
|
233
379
|
const input = allInputs[from.collection.id]
|
|
234
380
|
if (!input) {
|
|
235
381
|
throw new CollectionInputNotFoundError(from.collection.id)
|
|
236
382
|
}
|
|
237
|
-
return { alias: from.alias, input }
|
|
383
|
+
return { alias: from.alias, input, collectionId: from.collection.id }
|
|
238
384
|
}
|
|
239
385
|
case `queryRef`: {
|
|
240
386
|
// Find the original query for caching purposes
|
|
@@ -244,6 +390,10 @@ function processJoinSource(
|
|
|
244
390
|
const subQueryResult = compileQuery(
|
|
245
391
|
originalQuery,
|
|
246
392
|
allInputs,
|
|
393
|
+
collections,
|
|
394
|
+
callbacks,
|
|
395
|
+
lazyCollections,
|
|
396
|
+
optimizableOrderByCollections,
|
|
247
397
|
cache,
|
|
248
398
|
queryMapping
|
|
249
399
|
)
|
|
@@ -260,7 +410,11 @@ function processJoinSource(
|
|
|
260
410
|
})
|
|
261
411
|
)
|
|
262
412
|
|
|
263
|
-
return {
|
|
413
|
+
return {
|
|
414
|
+
alias: from.alias,
|
|
415
|
+
input: extractedInput as KeyedStream,
|
|
416
|
+
collectionId: subQueryResult.collectionId,
|
|
417
|
+
}
|
|
264
418
|
}
|
|
265
419
|
default:
|
|
266
420
|
throw new UnsupportedJoinSourceTypeError((from as any).type)
|
|
@@ -333,3 +487,45 @@ function processJoinResults(joinType: string) {
|
|
|
333
487
|
)
|
|
334
488
|
}
|
|
335
489
|
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Returns the active and lazy collections for a join clause.
|
|
493
|
+
* The active collection is the one that we need to fully iterate over
|
|
494
|
+
* and it can be the main table (i.e. left collection) or the joined table (i.e. right collection).
|
|
495
|
+
* The lazy collection is the one that we should join-in lazily based on matches in the active collection.
|
|
496
|
+
* @param joinClause - The join clause to analyze
|
|
497
|
+
* @param leftCollection - The left collection
|
|
498
|
+
* @param rightCollection - The right collection
|
|
499
|
+
* @returns The active and lazy collections. They are undefined if we need to loop over both collections (i.e. both are active)
|
|
500
|
+
*/
|
|
501
|
+
function getActiveAndLazyCollections(
|
|
502
|
+
joinType: JoinClause[`type`],
|
|
503
|
+
leftCollection: Collection,
|
|
504
|
+
rightCollection: Collection
|
|
505
|
+
):
|
|
506
|
+
| { activeCollection: `main` | `joined`; lazyCollection: Collection }
|
|
507
|
+
| { activeCollection: undefined; lazyCollection: undefined } {
|
|
508
|
+
if (leftCollection.id === rightCollection.id) {
|
|
509
|
+
// We can't apply this optimization if there's only one collection
|
|
510
|
+
// because `liveQueryCollection` will detect that the collection is lazy
|
|
511
|
+
// and treat it lazily (because the collection is shared)
|
|
512
|
+
// and thus it will not load any keys because both sides of the join
|
|
513
|
+
// will be handled lazily
|
|
514
|
+
return { activeCollection: undefined, lazyCollection: undefined }
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
switch (joinType) {
|
|
518
|
+
case `left`:
|
|
519
|
+
return { activeCollection: `main`, lazyCollection: rightCollection }
|
|
520
|
+
case `right`:
|
|
521
|
+
return { activeCollection: `joined`, lazyCollection: leftCollection }
|
|
522
|
+
case `inner`:
|
|
523
|
+
// The smallest collection should be the active collection
|
|
524
|
+
// and the biggest collection should be lazy
|
|
525
|
+
return leftCollection.size < rightCollection.size
|
|
526
|
+
? { activeCollection: `main`, lazyCollection: rightCollection }
|
|
527
|
+
: { activeCollection: `joined`, lazyCollection: leftCollection }
|
|
528
|
+
default:
|
|
529
|
+
return { activeCollection: undefined, lazyCollection: undefined }
|
|
530
|
+
}
|
|
531
|
+
}
|
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
import { orderByWithFractionalIndex } from "@tanstack/db-ivm"
|
|
2
2
|
import { defaultComparator, makeComparator } from "../../utils/comparison.js"
|
|
3
|
+
import { PropRef } from "../ir.js"
|
|
4
|
+
import { ensureIndexForField } from "../../indexes/auto-index.js"
|
|
5
|
+
import { findIndexForField } from "../../utils/index-optimization.js"
|
|
3
6
|
import { compileExpression } from "./evaluators.js"
|
|
4
|
-
import
|
|
7
|
+
import { followRef } from "./index.js"
|
|
8
|
+
import type { CompiledSingleRowExpression } from "./evaluators.js"
|
|
9
|
+
import type { OrderByClause, QueryIR } from "../ir.js"
|
|
5
10
|
import type { NamespacedAndKeyedStream, NamespacedRow } from "../../types.js"
|
|
6
11
|
import type { IStreamBuilder, KeyValue } from "@tanstack/db-ivm"
|
|
12
|
+
import type { BaseIndex } from "../../indexes/base-index.js"
|
|
13
|
+
import type { Collection } from "../../collection.js"
|
|
14
|
+
|
|
15
|
+
export type OrderByOptimizationInfo = {
|
|
16
|
+
offset: number
|
|
17
|
+
limit: number
|
|
18
|
+
comparator: (
|
|
19
|
+
a: Record<string, unknown> | null | undefined,
|
|
20
|
+
b: Record<string, unknown> | null | undefined
|
|
21
|
+
) => number
|
|
22
|
+
valueExtractorForRawRow: (row: Record<string, unknown>) => any
|
|
23
|
+
index: BaseIndex<string | number>
|
|
24
|
+
dataNeeded?: () => number
|
|
25
|
+
}
|
|
7
26
|
|
|
8
27
|
/**
|
|
9
28
|
* Processes the ORDER BY clause
|
|
@@ -11,8 +30,11 @@ import type { IStreamBuilder, KeyValue } from "@tanstack/db-ivm"
|
|
|
11
30
|
* Always uses fractional indexing and adds the index as __ordering_index to the result
|
|
12
31
|
*/
|
|
13
32
|
export function processOrderBy(
|
|
33
|
+
rawQuery: QueryIR,
|
|
14
34
|
pipeline: NamespacedAndKeyedStream,
|
|
15
35
|
orderByClause: Array<OrderByClause>,
|
|
36
|
+
collection: Collection,
|
|
37
|
+
optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
|
|
16
38
|
limit?: number,
|
|
17
39
|
offset?: number
|
|
18
40
|
): IStreamBuilder<KeyValue<unknown, [NamespacedRow, string]>> {
|
|
@@ -53,7 +75,7 @@ export function processOrderBy(
|
|
|
53
75
|
}
|
|
54
76
|
|
|
55
77
|
// Create a multi-property comparator that respects the order and direction of each property
|
|
56
|
-
const
|
|
78
|
+
const compare = (a: unknown, b: unknown) => {
|
|
57
79
|
// If we're comparing arrays (multiple properties), compare each property in order
|
|
58
80
|
if (orderByClause.length > 1) {
|
|
59
81
|
const arrayA = a as Array<unknown>
|
|
@@ -79,12 +101,85 @@ export function processOrderBy(
|
|
|
79
101
|
return defaultComparator(a, b)
|
|
80
102
|
}
|
|
81
103
|
|
|
104
|
+
let setSizeCallback: ((getSize: () => number) => void) | undefined
|
|
105
|
+
|
|
106
|
+
// Optimize the orderBy operator to lazily load elements
|
|
107
|
+
// by using the range index of the collection.
|
|
108
|
+
// Only for orderBy clause on a single column for now (no composite ordering)
|
|
109
|
+
if (limit && orderByClause.length === 1) {
|
|
110
|
+
const clause = orderByClause[0]!
|
|
111
|
+
const orderByExpression = clause.expression
|
|
112
|
+
|
|
113
|
+
if (orderByExpression.type === `ref`) {
|
|
114
|
+
const followRefResult = followRef(
|
|
115
|
+
rawQuery,
|
|
116
|
+
orderByExpression,
|
|
117
|
+
collection
|
|
118
|
+
)!
|
|
119
|
+
|
|
120
|
+
const followRefCollection = followRefResult.collection
|
|
121
|
+
const fieldName = followRefResult.path[0]
|
|
122
|
+
if (fieldName) {
|
|
123
|
+
ensureIndexForField(
|
|
124
|
+
fieldName,
|
|
125
|
+
followRefResult.path,
|
|
126
|
+
followRefCollection,
|
|
127
|
+
compare
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const valueExtractorForRawRow = compileExpression(
|
|
132
|
+
new PropRef(followRefResult.path),
|
|
133
|
+
true
|
|
134
|
+
) as CompiledSingleRowExpression
|
|
135
|
+
|
|
136
|
+
const comparator = (
|
|
137
|
+
a: Record<string, unknown> | null | undefined,
|
|
138
|
+
b: Record<string, unknown> | null | undefined
|
|
139
|
+
) => {
|
|
140
|
+
const extractedA = a ? valueExtractorForRawRow(a) : a
|
|
141
|
+
const extractedB = b ? valueExtractorForRawRow(b) : b
|
|
142
|
+
return compare(extractedA, extractedB)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const index: BaseIndex<string | number> | undefined = findIndexForField(
|
|
146
|
+
followRefCollection.indexes,
|
|
147
|
+
followRefResult.path
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if (index && index.supports(`gt`)) {
|
|
151
|
+
// We found an index that we can use to lazily load ordered data
|
|
152
|
+
const orderByOptimizationInfo = {
|
|
153
|
+
offset: offset ?? 0,
|
|
154
|
+
limit,
|
|
155
|
+
comparator,
|
|
156
|
+
valueExtractorForRawRow,
|
|
157
|
+
index,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
optimizableOrderByCollections[followRefCollection.id] =
|
|
161
|
+
orderByOptimizationInfo
|
|
162
|
+
|
|
163
|
+
setSizeCallback = (getSize: () => number) => {
|
|
164
|
+
optimizableOrderByCollections[followRefCollection.id] = {
|
|
165
|
+
...optimizableOrderByCollections[followRefCollection.id]!,
|
|
166
|
+
dataNeeded: () => {
|
|
167
|
+
const size = getSize()
|
|
168
|
+
return Math.max(0, limit - size)
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
82
176
|
// Use fractional indexing and return the tuple [value, index]
|
|
83
177
|
return pipeline.pipe(
|
|
84
178
|
orderByWithFractionalIndex(valueExtractor, {
|
|
85
179
|
limit,
|
|
86
180
|
offset,
|
|
87
|
-
comparator,
|
|
181
|
+
comparator: compare,
|
|
182
|
+
setSizeCallback,
|
|
88
183
|
})
|
|
89
184
|
// orderByWithFractionalIndex returns [key, [value, index]] - we keep this format
|
|
90
185
|
)
|