@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.
Files changed (86) hide show
  1. package/dist/cjs/collection/events.cjs +2 -1
  2. package/dist/cjs/collection/events.cjs.map +1 -1
  3. package/dist/cjs/collection/lifecycle.cjs +11 -5
  4. package/dist/cjs/collection/lifecycle.cjs.map +1 -1
  5. package/dist/cjs/collection/lifecycle.d.cts +1 -1
  6. package/dist/cjs/collection/state.cjs +1 -1
  7. package/dist/cjs/collection/state.cjs.map +1 -1
  8. package/dist/cjs/collection/subscription.cjs +1 -1
  9. package/dist/cjs/collection/subscription.cjs.map +1 -1
  10. package/dist/cjs/indexes/btree-index.cjs +19 -13
  11. package/dist/cjs/indexes/btree-index.cjs.map +1 -1
  12. package/dist/cjs/query/builder/functions.cjs.map +1 -1
  13. package/dist/cjs/query/builder/functions.d.cts +1 -1
  14. package/dist/cjs/query/compiler/evaluators.cjs +3 -2
  15. package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
  16. package/dist/cjs/query/compiler/group-by.cjs +6 -2
  17. package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
  18. package/dist/cjs/query/compiler/index.cjs +2 -39
  19. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  20. package/dist/cjs/query/compiler/index.d.cts +1 -0
  21. package/dist/cjs/query/compiler/joins.cjs +11 -9
  22. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  23. package/dist/cjs/query/compiler/joins.d.cts +2 -1
  24. package/dist/cjs/query/compiler/order-by.cjs +4 -5
  25. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  26. package/dist/cjs/query/ir.cjs +38 -0
  27. package/dist/cjs/query/ir.cjs.map +1 -1
  28. package/dist/cjs/query/ir.d.cts +10 -1
  29. package/dist/cjs/query/optimizer.cjs +8 -3
  30. package/dist/cjs/query/optimizer.cjs.map +1 -1
  31. package/dist/cjs/query/optimizer.d.cts +2 -0
  32. package/dist/cjs/types.d.cts +8 -2
  33. package/dist/cjs/utils/comparison.cjs +7 -0
  34. package/dist/cjs/utils/comparison.cjs.map +1 -1
  35. package/dist/cjs/utils/comparison.d.cts +4 -0
  36. package/dist/esm/collection/events.js +2 -1
  37. package/dist/esm/collection/events.js.map +1 -1
  38. package/dist/esm/collection/lifecycle.d.ts +1 -1
  39. package/dist/esm/collection/lifecycle.js +12 -6
  40. package/dist/esm/collection/lifecycle.js.map +1 -1
  41. package/dist/esm/collection/state.js +1 -1
  42. package/dist/esm/collection/state.js.map +1 -1
  43. package/dist/esm/collection/subscription.js +1 -1
  44. package/dist/esm/collection/subscription.js.map +1 -1
  45. package/dist/esm/indexes/btree-index.js +20 -14
  46. package/dist/esm/indexes/btree-index.js.map +1 -1
  47. package/dist/esm/query/builder/functions.d.ts +1 -1
  48. package/dist/esm/query/builder/functions.js.map +1 -1
  49. package/dist/esm/query/compiler/evaluators.js +3 -2
  50. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  51. package/dist/esm/query/compiler/group-by.js +6 -2
  52. package/dist/esm/query/compiler/group-by.js.map +1 -1
  53. package/dist/esm/query/compiler/index.d.ts +1 -0
  54. package/dist/esm/query/compiler/index.js +4 -41
  55. package/dist/esm/query/compiler/index.js.map +1 -1
  56. package/dist/esm/query/compiler/joins.d.ts +2 -1
  57. package/dist/esm/query/compiler/joins.js +11 -9
  58. package/dist/esm/query/compiler/joins.js.map +1 -1
  59. package/dist/esm/query/compiler/order-by.js +1 -2
  60. package/dist/esm/query/compiler/order-by.js.map +1 -1
  61. package/dist/esm/query/ir.d.ts +10 -1
  62. package/dist/esm/query/ir.js +38 -0
  63. package/dist/esm/query/ir.js.map +1 -1
  64. package/dist/esm/query/optimizer.d.ts +2 -0
  65. package/dist/esm/query/optimizer.js +8 -3
  66. package/dist/esm/query/optimizer.js.map +1 -1
  67. package/dist/esm/types.d.ts +8 -2
  68. package/dist/esm/utils/comparison.d.ts +4 -0
  69. package/dist/esm/utils/comparison.js +8 -1
  70. package/dist/esm/utils/comparison.js.map +1 -1
  71. package/package.json +2 -2
  72. package/src/collection/events.ts +1 -1
  73. package/src/collection/lifecycle.ts +20 -8
  74. package/src/collection/state.ts +1 -1
  75. package/src/collection/subscription.ts +1 -1
  76. package/src/indexes/btree-index.ts +24 -14
  77. package/src/query/builder/functions.ts +3 -3
  78. package/src/query/compiler/evaluators.ts +3 -2
  79. package/src/query/compiler/group-by.ts +15 -2
  80. package/src/query/compiler/index.ts +4 -1
  81. package/src/query/compiler/joins.ts +19 -9
  82. package/src/query/compiler/order-by.ts +1 -2
  83. package/src/query/ir.ts +67 -1
  84. package/src/query/optimizer.ts +28 -6
  85. package/src/types.ts +8 -2
  86. 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 { compileQuery, followRef } from "./index.js"
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
- if (!limitedSubquery) {
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 = compileQuery(
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
+ }
@@ -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 immediately when the collection is created.
338
- * Defaults to false for lazy loading. Set to true to immediately sync.
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
  /**
@@ -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
+ }