@tanstack/db 0.1.6 → 0.1.7

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 (58) hide show
  1. package/dist/cjs/collection.cjs.map +1 -1
  2. package/dist/cjs/collection.d.cts +5 -5
  3. package/dist/cjs/query/compiler/group-by.cjs +4 -2
  4. package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
  5. package/dist/cjs/query/compiler/index.cjs +2 -1
  6. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  7. package/dist/cjs/query/index.d.cts +2 -1
  8. package/dist/cjs/query/ir.cjs +16 -0
  9. package/dist/cjs/query/ir.cjs.map +1 -1
  10. package/dist/cjs/query/ir.d.cts +24 -1
  11. package/dist/cjs/query/live/collection-config-builder.cjs +267 -0
  12. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -0
  13. package/dist/cjs/query/live/collection-config-builder.d.cts +36 -0
  14. package/dist/cjs/query/live/collection-subscriber.cjs +263 -0
  15. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -0
  16. package/dist/cjs/query/live/collection-subscriber.d.cts +28 -0
  17. package/dist/cjs/query/live/types.d.cts +77 -0
  18. package/dist/cjs/query/live-query-collection.cjs +3 -417
  19. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  20. package/dist/cjs/query/live-query-collection.d.cts +1 -58
  21. package/dist/cjs/query/optimizer.cjs +34 -11
  22. package/dist/cjs/query/optimizer.cjs.map +1 -1
  23. package/dist/cjs/types.d.cts +12 -0
  24. package/dist/esm/collection.d.ts +5 -5
  25. package/dist/esm/collection.js.map +1 -1
  26. package/dist/esm/query/compiler/group-by.js +5 -3
  27. package/dist/esm/query/compiler/group-by.js.map +1 -1
  28. package/dist/esm/query/compiler/index.js +3 -2
  29. package/dist/esm/query/compiler/index.js.map +1 -1
  30. package/dist/esm/query/index.d.ts +2 -1
  31. package/dist/esm/query/ir.d.ts +24 -1
  32. package/dist/esm/query/ir.js +17 -1
  33. package/dist/esm/query/ir.js.map +1 -1
  34. package/dist/esm/query/live/collection-config-builder.d.ts +36 -0
  35. package/dist/esm/query/live/collection-config-builder.js +267 -0
  36. package/dist/esm/query/live/collection-config-builder.js.map +1 -0
  37. package/dist/esm/query/live/collection-subscriber.d.ts +28 -0
  38. package/dist/esm/query/live/collection-subscriber.js +263 -0
  39. package/dist/esm/query/live/collection-subscriber.js.map +1 -0
  40. package/dist/esm/query/live/types.d.ts +77 -0
  41. package/dist/esm/query/live-query-collection.d.ts +1 -58
  42. package/dist/esm/query/live-query-collection.js +3 -417
  43. package/dist/esm/query/live-query-collection.js.map +1 -1
  44. package/dist/esm/query/optimizer.js +35 -12
  45. package/dist/esm/query/optimizer.js.map +1 -1
  46. package/dist/esm/types.d.ts +12 -0
  47. package/package.json +1 -1
  48. package/src/collection.ts +8 -5
  49. package/src/query/compiler/group-by.ts +5 -3
  50. package/src/query/compiler/index.ts +3 -2
  51. package/src/query/index.ts +2 -1
  52. package/src/query/ir.ts +48 -1
  53. package/src/query/live/collection-config-builder.ts +437 -0
  54. package/src/query/live/collection-subscriber.ts +460 -0
  55. package/src/query/live/types.ts +93 -0
  56. package/src/query/live-query-collection.ts +8 -791
  57. package/src/query/optimizer.ts +66 -18
  58. package/src/types.ts +74 -0
@@ -45,6 +45,11 @@
45
45
  * - **Ordering + Limits**: ORDER BY combined with LIMIT/OFFSET (would change result set)
46
46
  * - **Functional Operations**: fnSelect, fnWhere, fnHaving (potential side effects)
47
47
  *
48
+ * ### Residual WHERE Clauses
49
+ * For outer joins (LEFT, RIGHT, FULL), WHERE clauses are copied to subqueries for optimization
50
+ * but also kept as "residual" clauses in the main query to preserve semantics. This ensures
51
+ * that NULL values from outer joins are properly filtered according to SQL standards.
52
+ *
48
53
  * The optimizer tracks which clauses were actually optimized and only removes those from the
49
54
  * main query. Subquery reuse is handled safely through immutable query copies.
50
55
  *
@@ -121,9 +126,12 @@ import {
121
126
  CollectionRef as CollectionRefClass,
122
127
  Func,
123
128
  QueryRef as QueryRefClass,
129
+ createResidualWhere,
130
+ getWhereExpression,
131
+ isResidualWhere,
124
132
  } from "./ir.js"
125
133
  import { isConvertibleToCollectionFilter } from "./compiler/expressions.js"
126
- import type { BasicExpression, From, QueryIR } from "./ir.js"
134
+ import type { BasicExpression, From, QueryIR, Where } from "./ir.js"
127
135
 
128
136
  /**
129
137
  * Represents a WHERE clause after source analysis
@@ -325,8 +333,13 @@ function applySingleLevelOptimization(query: QueryIR): QueryIR {
325
333
  return query
326
334
  }
327
335
 
336
+ // Filter out residual WHERE clauses to prevent them from being optimized again
337
+ const nonResidualWhereClauses = query.where.filter(
338
+ (where) => !isResidualWhere(where)
339
+ )
340
+
328
341
  // Step 1: Split all AND clauses at the root level for granular optimization
329
- const splitWhereClauses = splitAndClauses(query.where)
342
+ const splitWhereClauses = splitAndClauses(nonResidualWhereClauses)
330
343
 
331
344
  // Step 2: Analyze each WHERE clause to determine which sources it touches
332
345
  const analyzedClauses = splitWhereClauses.map((clause) =>
@@ -337,7 +350,20 @@ function applySingleLevelOptimization(query: QueryIR): QueryIR {
337
350
  const groupedClauses = groupWhereClauses(analyzedClauses)
338
351
 
339
352
  // Step 4: Apply optimizations by lifting single-source clauses into subqueries
340
- return applyOptimizations(query, groupedClauses)
353
+ const optimizedQuery = applyOptimizations(query, groupedClauses)
354
+
355
+ // Add back any residual WHERE clauses that were filtered out
356
+ const residualWhereClauses = query.where.filter((where) =>
357
+ isResidualWhere(where)
358
+ )
359
+ if (residualWhereClauses.length > 0) {
360
+ optimizedQuery.where = [
361
+ ...(optimizedQuery.where || []),
362
+ ...residualWhereClauses,
363
+ ]
364
+ }
365
+
366
+ return optimizedQuery
341
367
  }
342
368
 
343
369
  /**
@@ -424,26 +450,35 @@ function isRedundantSubquery(query: QueryIR): boolean {
424
450
  * ```
425
451
  */
426
452
  function splitAndClauses(
427
- whereClauses: Array<BasicExpression<boolean>>
453
+ whereClauses: Array<Where>
428
454
  ): Array<BasicExpression<boolean>> {
429
455
  const result: Array<BasicExpression<boolean>> = []
430
456
 
431
- for (const clause of whereClauses) {
432
- if (clause.type === `func` && clause.name === `and`) {
433
- // Recursively split nested AND clauses to handle complex expressions
434
- const splitArgs = splitAndClauses(
435
- clause.args as Array<BasicExpression<boolean>>
436
- )
437
- result.push(...splitArgs)
438
- } else {
439
- // Preserve non-AND clauses as-is (including OR clauses)
440
- result.push(clause)
441
- }
457
+ for (const whereClause of whereClauses) {
458
+ const clause = getWhereExpression(whereClause)
459
+ result.push(...splitAndClausesRecursive(clause))
442
460
  }
443
461
 
444
462
  return result
445
463
  }
446
464
 
465
+ // Helper function for recursive splitting of BasicExpression arrays
466
+ function splitAndClausesRecursive(
467
+ clause: BasicExpression<boolean>
468
+ ): Array<BasicExpression<boolean>> {
469
+ if (clause.type === `func` && clause.name === `and`) {
470
+ // Recursively split nested AND clauses to handle complex expressions
471
+ const result: Array<BasicExpression<boolean>> = []
472
+ for (const arg of clause.args as Array<BasicExpression<boolean>>) {
473
+ result.push(...splitAndClausesRecursive(arg))
474
+ }
475
+ return result
476
+ } else {
477
+ // Preserve non-AND clauses as-is (including OR clauses)
478
+ return [clause]
479
+ }
480
+ }
481
+
447
482
  /**
448
483
  * Step 2: Analyze which table sources a WHERE clause touches.
449
484
  *
@@ -588,19 +623,32 @@ function applyOptimizations(
588
623
  }))
589
624
  : undefined
590
625
 
591
- // Build the remaining WHERE clauses: multi-source + any single-source that weren't optimized
592
- const remainingWhereClauses: Array<BasicExpression<boolean>> = []
626
+ // Build the remaining WHERE clauses: multi-source + residual single-source clauses
627
+ const remainingWhereClauses: Array<Where> = []
593
628
 
594
629
  // Add multi-source clauses
595
630
  if (groupedClauses.multiSource) {
596
631
  remainingWhereClauses.push(groupedClauses.multiSource)
597
632
  }
598
633
 
599
- // Add single-source clauses that weren't actually optimized
634
+ // Determine if we need residual clauses (when query has outer JOINs)
635
+ const hasOuterJoins =
636
+ query.join &&
637
+ query.join.some(
638
+ (join) =>
639
+ join.type === `left` || join.type === `right` || join.type === `full`
640
+ )
641
+
642
+ // Add single-source clauses
600
643
  for (const [source, clause] of groupedClauses.singleSource) {
601
644
  if (!actuallyOptimized.has(source)) {
645
+ // Wasn't optimized at all - keep as regular WHERE clause
602
646
  remainingWhereClauses.push(clause)
647
+ } else if (hasOuterJoins) {
648
+ // Was optimized AND query has outer JOINs - keep as residual WHERE clause
649
+ remainingWhereClauses.push(createResidualWhere(clause))
603
650
  }
651
+ // If optimized and no outer JOINs - don't keep (original behavior)
604
652
  }
605
653
 
606
654
  // Create a completely new query object to ensure immutability
package/src/types.ts CHANGED
@@ -629,3 +629,77 @@ export type ChangeListener<
629
629
  T extends object = Record<string, unknown>,
630
630
  TKey extends string | number = string | number,
631
631
  > = (changes: Array<ChangeMessage<T, TKey>>) => void
632
+
633
+ // Adapted from https://github.com/sindresorhus/type-fest
634
+ // MIT License Copyright (c) Sindre Sorhus
635
+
636
+ type BuiltIns =
637
+ | null
638
+ | undefined
639
+ | string
640
+ | number
641
+ | boolean
642
+ | symbol
643
+ | bigint
644
+ | void
645
+ | Date
646
+ | RegExp
647
+
648
+ type HasMultipleCallSignatures<
649
+ T extends (...arguments_: Array<any>) => unknown,
650
+ > = T extends {
651
+ (...arguments_: infer A): unknown
652
+ (...arguments_: infer B): unknown
653
+ }
654
+ ? B extends A
655
+ ? A extends B
656
+ ? false
657
+ : true
658
+ : true
659
+ : false
660
+
661
+ type WritableMapDeep<MapType extends ReadonlyMap<unknown, unknown>> =
662
+ MapType extends ReadonlyMap<infer KeyType, infer ValueType>
663
+ ? Map<WritableDeep<KeyType>, WritableDeep<ValueType>>
664
+ : MapType
665
+
666
+ type WritableSetDeep<SetType extends ReadonlySet<unknown>> =
667
+ SetType extends ReadonlySet<infer ItemType>
668
+ ? Set<WritableDeep<ItemType>>
669
+ : SetType
670
+
671
+ type WritableObjectDeep<ObjectType extends object> = {
672
+ -readonly [KeyType in keyof ObjectType]: WritableDeep<ObjectType[KeyType]>
673
+ }
674
+
675
+ type WritableArrayDeep<ArrayType extends ReadonlyArray<unknown>> =
676
+ ArrayType extends readonly []
677
+ ? []
678
+ : ArrayType extends readonly [...infer U, infer V]
679
+ ? [...WritableArrayDeep<U>, WritableDeep<V>]
680
+ : ArrayType extends readonly [infer U, ...infer V]
681
+ ? [WritableDeep<U>, ...WritableArrayDeep<V>]
682
+ : ArrayType extends ReadonlyArray<infer U>
683
+ ? Array<WritableDeep<U>>
684
+ : ArrayType extends Array<infer U>
685
+ ? Array<WritableDeep<U>>
686
+ : ArrayType
687
+
688
+ export type WritableDeep<T> = T extends BuiltIns
689
+ ? T
690
+ : T extends (...arguments_: Array<any>) => unknown
691
+ ? {} extends WritableObjectDeep<T>
692
+ ? T
693
+ : HasMultipleCallSignatures<T> extends true
694
+ ? T
695
+ : ((...arguments_: Parameters<T>) => ReturnType<T>) &
696
+ WritableObjectDeep<T>
697
+ : T extends ReadonlyMap<unknown, unknown>
698
+ ? WritableMapDeep<T>
699
+ : T extends ReadonlySet<unknown>
700
+ ? WritableSetDeep<T>
701
+ : T extends ReadonlyArray<unknown>
702
+ ? WritableArrayDeep<T>
703
+ : T extends object
704
+ ? WritableObjectDeep<T>
705
+ : unknown