@tanstack/db 0.4.1 → 0.4.3
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 +2 -1
- package/dist/cjs/collection/events.cjs.map +1 -1
- package/dist/cjs/collection/lifecycle.cjs +11 -5
- package/dist/cjs/collection/lifecycle.cjs.map +1 -1
- package/dist/cjs/collection/lifecycle.d.cts +1 -1
- package/dist/cjs/collection/state.cjs +1 -1
- package/dist/cjs/collection/state.cjs.map +1 -1
- package/dist/cjs/collection/subscription.cjs +1 -1
- package/dist/cjs/collection/subscription.cjs.map +1 -1
- package/dist/cjs/indexes/btree-index.cjs +19 -13
- package/dist/cjs/indexes/btree-index.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.d.cts +1 -1
- package/dist/cjs/query/compiler/evaluators.cjs +3 -2
- package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
- package/dist/cjs/query/compiler/group-by.cjs +6 -2
- package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.cjs +2 -39
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.d.cts +1 -0
- package/dist/cjs/query/compiler/joins.cjs +11 -9
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/compiler/joins.d.cts +2 -1
- package/dist/cjs/query/compiler/order-by.cjs +4 -5
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/ir.cjs +38 -0
- package/dist/cjs/query/ir.cjs.map +1 -1
- package/dist/cjs/query/ir.d.cts +10 -1
- package/dist/cjs/query/optimizer.cjs +8 -3
- package/dist/cjs/query/optimizer.cjs.map +1 -1
- package/dist/cjs/query/optimizer.d.cts +2 -0
- package/dist/cjs/types.d.cts +8 -2
- package/dist/cjs/utils/comparison.cjs +7 -0
- package/dist/cjs/utils/comparison.cjs.map +1 -1
- package/dist/cjs/utils/comparison.d.cts +4 -0
- package/dist/esm/collection/events.js +2 -1
- package/dist/esm/collection/events.js.map +1 -1
- package/dist/esm/collection/lifecycle.d.ts +1 -1
- package/dist/esm/collection/lifecycle.js +12 -6
- package/dist/esm/collection/lifecycle.js.map +1 -1
- package/dist/esm/collection/state.js +1 -1
- package/dist/esm/collection/state.js.map +1 -1
- package/dist/esm/collection/subscription.js +1 -1
- package/dist/esm/collection/subscription.js.map +1 -1
- package/dist/esm/indexes/btree-index.js +20 -14
- package/dist/esm/indexes/btree-index.js.map +1 -1
- package/dist/esm/query/builder/functions.d.ts +1 -1
- package/dist/esm/query/builder/functions.js.map +1 -1
- package/dist/esm/query/compiler/evaluators.js +3 -2
- package/dist/esm/query/compiler/evaluators.js.map +1 -1
- package/dist/esm/query/compiler/group-by.js +6 -2
- package/dist/esm/query/compiler/group-by.js.map +1 -1
- package/dist/esm/query/compiler/index.d.ts +1 -0
- package/dist/esm/query/compiler/index.js +4 -41
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/joins.d.ts +2 -1
- package/dist/esm/query/compiler/joins.js +11 -9
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/compiler/order-by.js +1 -2
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/ir.d.ts +10 -1
- package/dist/esm/query/ir.js +38 -0
- package/dist/esm/query/ir.js.map +1 -1
- package/dist/esm/query/optimizer.d.ts +2 -0
- package/dist/esm/query/optimizer.js +8 -3
- package/dist/esm/query/optimizer.js.map +1 -1
- package/dist/esm/types.d.ts +8 -2
- package/dist/esm/utils/comparison.d.ts +4 -0
- package/dist/esm/utils/comparison.js +8 -1
- package/dist/esm/utils/comparison.js.map +1 -1
- package/package.json +2 -2
- package/src/collection/events.ts +1 -1
- package/src/collection/lifecycle.ts +20 -8
- package/src/collection/state.ts +1 -1
- package/src/collection/subscription.ts +1 -1
- package/src/indexes/btree-index.ts +24 -14
- package/src/query/builder/functions.ts +3 -3
- package/src/query/compiler/evaluators.ts +3 -2
- package/src/query/compiler/group-by.ts +15 -2
- package/src/query/compiler/index.ts +4 -1
- package/src/query/compiler/joins.ts +19 -9
- package/src/query/compiler/order-by.ts +1 -2
- package/src/query/ir.ts +67 -1
- package/src/query/optimizer.ts +28 -6
- package/src/types.ts +8 -2
- package/src/utils/comparison.ts +10 -0
|
@@ -17,10 +17,10 @@ import {
|
|
|
17
17
|
UnsupportedJoinTypeError,
|
|
18
18
|
} from "../../errors.js"
|
|
19
19
|
import { ensureIndexForField } from "../../indexes/auto-index.js"
|
|
20
|
-
import { PropRef } from "../ir.js"
|
|
20
|
+
import { PropRef, followRef } from "../ir.js"
|
|
21
21
|
import { inArray } from "../builder/functions.js"
|
|
22
22
|
import { compileExpression } from "./evaluators.js"
|
|
23
|
-
import {
|
|
23
|
+
import type { CompileQueryFn } from "./index.js"
|
|
24
24
|
import type { OrderByOptimizationInfo } from "./order-by.js"
|
|
25
25
|
import type {
|
|
26
26
|
BasicExpression,
|
|
@@ -62,7 +62,8 @@ export function processJoins(
|
|
|
62
62
|
callbacks: Record<string, LazyCollectionCallbacks>,
|
|
63
63
|
lazyCollections: Set<string>,
|
|
64
64
|
optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
|
|
65
|
-
rawQuery: QueryIR
|
|
65
|
+
rawQuery: QueryIR,
|
|
66
|
+
onCompileSubquery: CompileQueryFn
|
|
66
67
|
): NamespacedAndKeyedStream {
|
|
67
68
|
let resultPipeline = pipeline
|
|
68
69
|
|
|
@@ -81,7 +82,8 @@ export function processJoins(
|
|
|
81
82
|
callbacks,
|
|
82
83
|
lazyCollections,
|
|
83
84
|
optimizableOrderByCollections,
|
|
84
|
-
rawQuery
|
|
85
|
+
rawQuery,
|
|
86
|
+
onCompileSubquery
|
|
85
87
|
)
|
|
86
88
|
}
|
|
87
89
|
|
|
@@ -105,7 +107,8 @@ function processJoin(
|
|
|
105
107
|
callbacks: Record<string, LazyCollectionCallbacks>,
|
|
106
108
|
lazyCollections: Set<string>,
|
|
107
109
|
optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
|
|
108
|
-
rawQuery: QueryIR
|
|
110
|
+
rawQuery: QueryIR,
|
|
111
|
+
onCompileSubquery: CompileQueryFn
|
|
109
112
|
): NamespacedAndKeyedStream {
|
|
110
113
|
// Get the joined table alias and input stream
|
|
111
114
|
const {
|
|
@@ -121,7 +124,8 @@ function processJoin(
|
|
|
121
124
|
lazyCollections,
|
|
122
125
|
optimizableOrderByCollections,
|
|
123
126
|
cache,
|
|
124
|
-
queryMapping
|
|
127
|
+
queryMapping,
|
|
128
|
+
onCompileSubquery
|
|
125
129
|
)
|
|
126
130
|
|
|
127
131
|
// Add the joined table to the tables map
|
|
@@ -204,7 +208,12 @@ function processJoin(
|
|
|
204
208
|
lazyFrom.type === `queryRef` &&
|
|
205
209
|
(lazyFrom.query.limit || lazyFrom.query.offset)
|
|
206
210
|
|
|
207
|
-
|
|
211
|
+
// If join expressions contain computed values (like concat functions)
|
|
212
|
+
// we don't optimize the join because we don't have an index over the computed values
|
|
213
|
+
const hasComputedJoinExpr =
|
|
214
|
+
mainExpr.type === `func` || joinedExpr.type === `func`
|
|
215
|
+
|
|
216
|
+
if (!limitedSubquery && !hasComputedJoinExpr) {
|
|
208
217
|
// This join can be optimized by having the active collection
|
|
209
218
|
// dynamically load keys into the lazy collection
|
|
210
219
|
// based on the value of the joinKey and by looking up
|
|
@@ -387,7 +396,8 @@ function processJoinSource(
|
|
|
387
396
|
lazyCollections: Set<string>,
|
|
388
397
|
optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
|
|
389
398
|
cache: QueryCache,
|
|
390
|
-
queryMapping: QueryMapping
|
|
399
|
+
queryMapping: QueryMapping,
|
|
400
|
+
onCompileSubquery: CompileQueryFn
|
|
391
401
|
): { alias: string; input: KeyedStream; collectionId: string } {
|
|
392
402
|
switch (from.type) {
|
|
393
403
|
case `collectionRef`: {
|
|
@@ -402,7 +412,7 @@ function processJoinSource(
|
|
|
402
412
|
const originalQuery = queryMapping.get(from.query) || from.query
|
|
403
413
|
|
|
404
414
|
// Recursively compile the sub-query with cache
|
|
405
|
-
const subQueryResult =
|
|
415
|
+
const subQueryResult = onCompileSubquery(
|
|
406
416
|
originalQuery,
|
|
407
417
|
allInputs,
|
|
408
418
|
collections,
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { orderByWithFractionalIndex } from "@tanstack/db-ivm"
|
|
2
2
|
import { defaultComparator, makeComparator } from "../../utils/comparison.js"
|
|
3
|
-
import { PropRef } from "../ir.js"
|
|
3
|
+
import { PropRef, followRef } from "../ir.js"
|
|
4
4
|
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 { followRef } from "./index.js"
|
|
9
8
|
import type { CompiledSingleRowExpression } from "./evaluators.js"
|
|
10
9
|
import type { OrderByClause, QueryIR, Select } from "../ir.js"
|
|
11
10
|
import type { NamespacedAndKeyedStream, NamespacedRow } from "../../types.js"
|
package/src/query/ir.ts
CHANGED
|
@@ -3,7 +3,7 @@ This is the intermediate representation of the query.
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { CompareOptions } from "./builder/types"
|
|
6
|
-
import type { CollectionImpl } from "../collection/index.js"
|
|
6
|
+
import type { Collection, CollectionImpl } from "../collection/index.js"
|
|
7
7
|
import type { NamespacedRow } from "../types"
|
|
8
8
|
|
|
9
9
|
export interface QueryIR {
|
|
@@ -188,3 +188,69 @@ export function createResidualWhere(
|
|
|
188
188
|
): Where {
|
|
189
189
|
return { expression, residual: true }
|
|
190
190
|
}
|
|
191
|
+
|
|
192
|
+
function getRefFromAlias(
|
|
193
|
+
query: QueryIR,
|
|
194
|
+
alias: string
|
|
195
|
+
): CollectionRef | QueryRef | void {
|
|
196
|
+
if (query.from.alias === alias) {
|
|
197
|
+
return query.from
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
for (const join of query.join || []) {
|
|
201
|
+
if (join.from.alias === alias) {
|
|
202
|
+
return join.from
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Follows the given reference in a query
|
|
209
|
+
* until its finds the root field the reference points to.
|
|
210
|
+
* @returns The collection, its alias, and the path to the root field in this collection
|
|
211
|
+
*/
|
|
212
|
+
export function followRef(
|
|
213
|
+
query: QueryIR,
|
|
214
|
+
ref: PropRef<any>,
|
|
215
|
+
collection: Collection
|
|
216
|
+
): { collection: Collection; path: Array<string> } | void {
|
|
217
|
+
if (ref.path.length === 0) {
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (ref.path.length === 1) {
|
|
222
|
+
// This field should be part of this collection
|
|
223
|
+
const field = ref.path[0]!
|
|
224
|
+
// is it part of the select clause?
|
|
225
|
+
if (query.select) {
|
|
226
|
+
const selectedField = query.select[field]
|
|
227
|
+
if (selectedField && selectedField.type === `ref`) {
|
|
228
|
+
return followRef(query, selectedField, collection)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Either this field is not part of the select clause
|
|
233
|
+
// and thus it must be part of the collection itself
|
|
234
|
+
// or it is part of the select but is not a reference
|
|
235
|
+
// so we can stop here and don't have to follow it
|
|
236
|
+
return { collection, path: [field] }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (ref.path.length > 1) {
|
|
240
|
+
// This is a nested field
|
|
241
|
+
const [alias, ...rest] = ref.path
|
|
242
|
+
const aliasRef = getRefFromAlias(query, alias!)
|
|
243
|
+
if (!aliasRef) {
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (aliasRef.type === `queryRef`) {
|
|
248
|
+
return followRef(aliasRef.query, new PropRef(rest), collection)
|
|
249
|
+
} else {
|
|
250
|
+
// This is a reference to a collection
|
|
251
|
+
// we can't follow it further
|
|
252
|
+
// so the field must be on the collection itself
|
|
253
|
+
return { collection: aliasRef.collection, path: rest }
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
package/src/query/optimizer.ts
CHANGED
|
@@ -142,6 +142,8 @@ export interface AnalyzedWhereClause {
|
|
|
142
142
|
expression: BasicExpression<boolean>
|
|
143
143
|
/** Set of table/source aliases that this WHERE clause touches */
|
|
144
144
|
touchedSources: Set<string>
|
|
145
|
+
/** Whether this clause contains namespace-only references that prevent pushdown */
|
|
146
|
+
hasNamespaceOnlyRef: boolean
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
/**
|
|
@@ -486,19 +488,31 @@ function splitAndClausesRecursive(
|
|
|
486
488
|
* This determines whether a clause can be pushed down to a specific table
|
|
487
489
|
* or must remain in the main query (for multi-source clauses like join conditions).
|
|
488
490
|
*
|
|
491
|
+
* Special handling for namespace-only references in outer joins:
|
|
492
|
+
* WHERE clauses that reference only a table namespace (e.g., isUndefined(special), eq(special, value))
|
|
493
|
+
* rather than specific properties (e.g., isUndefined(special.id), eq(special.id, value)) are treated as
|
|
494
|
+
* multi-source to prevent incorrect predicate pushdown that would change join semantics.
|
|
495
|
+
*
|
|
489
496
|
* @param clause - The WHERE expression to analyze
|
|
490
497
|
* @returns Analysis result with the expression and touched source aliases
|
|
491
498
|
*
|
|
492
499
|
* @example
|
|
493
500
|
* ```typescript
|
|
494
|
-
* // eq(users.department_id, 1) -> touches ['users']
|
|
495
|
-
* // eq(users.id, posts.user_id) -> touches ['users', 'posts']
|
|
501
|
+
* // eq(users.department_id, 1) -> touches ['users'], hasNamespaceOnlyRef: false
|
|
502
|
+
* // eq(users.id, posts.user_id) -> touches ['users', 'posts'], hasNamespaceOnlyRef: false
|
|
503
|
+
* // isUndefined(special) -> touches ['special'], hasNamespaceOnlyRef: true (prevents pushdown)
|
|
504
|
+
* // eq(special, someValue) -> touches ['special'], hasNamespaceOnlyRef: true (prevents pushdown)
|
|
505
|
+
* // isUndefined(special.id) -> touches ['special'], hasNamespaceOnlyRef: false (allows pushdown)
|
|
506
|
+
* // eq(special.id, 5) -> touches ['special'], hasNamespaceOnlyRef: false (allows pushdown)
|
|
496
507
|
* ```
|
|
497
508
|
*/
|
|
498
509
|
function analyzeWhereClause(
|
|
499
510
|
clause: BasicExpression<boolean>
|
|
500
511
|
): AnalyzedWhereClause {
|
|
512
|
+
// Track which table aliases this WHERE clause touches
|
|
501
513
|
const touchedSources = new Set<string>()
|
|
514
|
+
// Track whether this clause contains namespace-only references that prevent pushdown
|
|
515
|
+
let hasNamespaceOnlyRef = false
|
|
502
516
|
|
|
503
517
|
/**
|
|
504
518
|
* Recursively collect all table aliases referenced in an expression
|
|
@@ -511,6 +525,13 @@ function analyzeWhereClause(
|
|
|
511
525
|
const firstElement = expr.path[0]
|
|
512
526
|
if (firstElement) {
|
|
513
527
|
touchedSources.add(firstElement)
|
|
528
|
+
|
|
529
|
+
// If the path has only one element (just the namespace),
|
|
530
|
+
// this is a namespace-only reference that should not be pushed down
|
|
531
|
+
// This applies to ANY function, not just existence-checking functions
|
|
532
|
+
if (expr.path.length === 1) {
|
|
533
|
+
hasNamespaceOnlyRef = true
|
|
534
|
+
}
|
|
514
535
|
}
|
|
515
536
|
}
|
|
516
537
|
break
|
|
@@ -537,6 +558,7 @@ function analyzeWhereClause(
|
|
|
537
558
|
return {
|
|
538
559
|
expression: clause,
|
|
539
560
|
touchedSources,
|
|
561
|
+
hasNamespaceOnlyRef,
|
|
540
562
|
}
|
|
541
563
|
}
|
|
542
564
|
|
|
@@ -557,15 +579,15 @@ function groupWhereClauses(
|
|
|
557
579
|
|
|
558
580
|
// Categorize each clause based on how many sources it touches
|
|
559
581
|
for (const clause of analyzedClauses) {
|
|
560
|
-
if (clause.touchedSources.size === 1) {
|
|
561
|
-
// Single source clause - can be optimized
|
|
582
|
+
if (clause.touchedSources.size === 1 && !clause.hasNamespaceOnlyRef) {
|
|
583
|
+
// Single source clause without namespace-only references - can be optimized
|
|
562
584
|
const source = Array.from(clause.touchedSources)[0]!
|
|
563
585
|
if (!singleSource.has(source)) {
|
|
564
586
|
singleSource.set(source, [])
|
|
565
587
|
}
|
|
566
588
|
singleSource.get(source)!.push(clause.expression)
|
|
567
|
-
} else if (clause.touchedSources.size > 1) {
|
|
568
|
-
// Multi-source clause - must stay in main query
|
|
589
|
+
} else if (clause.touchedSources.size > 1 || clause.hasNamespaceOnlyRef) {
|
|
590
|
+
// Multi-source clause or namespace-only reference - must stay in main query
|
|
569
591
|
multiSource.push(clause.expression)
|
|
570
592
|
}
|
|
571
593
|
// Skip clauses that touch no sources (constants) - they don't need optimization
|
package/src/types.ts
CHANGED
|
@@ -334,8 +334,14 @@ export interface BaseCollectionConfig<
|
|
|
334
334
|
*/
|
|
335
335
|
gcTime?: number
|
|
336
336
|
/**
|
|
337
|
-
* Whether to start syncing
|
|
338
|
-
*
|
|
337
|
+
* Whether to eagerly start syncing on collection creation.
|
|
338
|
+
* When true, syncing begins immediately. When false, syncing starts when the first subscriber attaches.
|
|
339
|
+
*
|
|
340
|
+
* Note: Even with startSync=true, collections will pause syncing when there are no active
|
|
341
|
+
* subscribers (typically when components querying the collection unmount), resuming when new
|
|
342
|
+
* subscribers attach. This preserves normal staleTime/gcTime behavior.
|
|
343
|
+
*
|
|
344
|
+
* @default false
|
|
339
345
|
*/
|
|
340
346
|
startSync?: boolean
|
|
341
347
|
/**
|
package/src/utils/comparison.ts
CHANGED
|
@@ -110,3 +110,13 @@ export const defaultComparator = makeComparator({
|
|
|
110
110
|
nulls: `first`,
|
|
111
111
|
stringSort: `locale`,
|
|
112
112
|
})
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Normalize a value for comparison
|
|
116
|
+
*/
|
|
117
|
+
export function normalizeValue(value: any): any {
|
|
118
|
+
if (value instanceof Date) {
|
|
119
|
+
return value.getTime()
|
|
120
|
+
}
|
|
121
|
+
return value
|
|
122
|
+
}
|