@tanstack/db 0.5.2 → 0.5.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.
@@ -89,7 +89,26 @@ export type NonEmptyArray<T> = [T, ...Array<T>];
89
89
  * Utility type for a Transaction with at least one mutation
90
90
  * This is used internally by the Transaction.commit method
91
91
  */
92
- export type TransactionWithMutations<T extends object = Record<string, unknown>, TOperation extends OperationType = OperationType> = Transaction<T> & {
92
+ export type TransactionWithMutations<T extends object = Record<string, unknown>, TOperation extends OperationType = OperationType> = Omit<Transaction<T>, `mutations`> & {
93
+ /**
94
+ * We must omit the `mutations` property from `Transaction<T>` before intersecting
95
+ * because TypeScript intersects property types when the same property appears on
96
+ * both sides of an intersection.
97
+ *
98
+ * Without `Omit`:
99
+ * - `Transaction<T>` has `mutations: Array<PendingMutation<T>>`
100
+ * - The intersection would create: `Array<PendingMutation<T>> & NonEmptyArray<PendingMutation<T, TOperation>>`
101
+ * - When mapping over this array, TypeScript widens `TOperation` from the specific literal
102
+ * (e.g., `"delete"`) to the union `OperationType` (`"insert" | "update" | "delete"`)
103
+ * - This causes `PendingMutation<T, OperationType>` to evaluate the conditional type
104
+ * `original: TOperation extends 'insert' ? {} : T` as `{} | T` instead of just `T`
105
+ *
106
+ * With `Omit`:
107
+ * - We remove `mutations` from `Transaction<T>` first
108
+ * - Then add back `mutations: NonEmptyArray<PendingMutation<T, TOperation>>`
109
+ * - TypeScript can properly narrow `TOperation` to the specific literal type
110
+ * - This ensures `mutation.original` is correctly typed as `T` (not `{} | T`) when mapping
111
+ */
93
112
  mutations: NonEmptyArray<PendingMutation<T, TOperation>>;
94
113
  };
95
114
  export interface TransactionConfig<T extends object = Record<string, unknown>> {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tanstack/db",
3
3
  "description": "A reactive client store for building super fast apps on sync",
4
- "version": "0.5.2",
4
+ "version": "0.5.3",
5
5
  "dependencies": {
6
6
  "@standard-schema/spec": "^1.0.0",
7
7
  "@tanstack/pacer": "^0.16.3",
@@ -2,61 +2,26 @@ import { Func, PropRef, Value } from "../ir.js"
2
2
  import type { BasicExpression, OrderBy } from "../ir.js"
3
3
 
4
4
  /**
5
- * Functions supported by the collection index system.
6
- * These are the only functions that can be used in WHERE clauses
7
- * that are pushed down to collection subscriptions for index optimization.
8
- */
9
- export const SUPPORTED_COLLECTION_FUNCS = new Set([
10
- `eq`,
11
- `gt`,
12
- `lt`,
13
- `gte`,
14
- `lte`,
15
- `and`,
16
- `or`,
17
- `in`,
18
- `isNull`,
19
- `isUndefined`,
20
- `not`,
21
- ])
22
-
23
- /**
24
- * Determines if a WHERE clause can be converted to collection-compatible BasicExpression format.
25
- * This checks if the expression only uses functions supported by the collection index system.
5
+ * Normalizes a WHERE clause expression by removing table aliases from property references.
26
6
  *
27
- * @param whereClause - The WHERE clause to check
28
- * @returns True if the clause can be converted for collection index optimization
29
- */
30
- export function isConvertibleToCollectionFilter(
31
- whereClause: BasicExpression<boolean>
32
- ): boolean {
33
- const tpe = whereClause.type
34
- if (tpe === `func`) {
35
- // Check if this function is supported
36
- if (!SUPPORTED_COLLECTION_FUNCS.has(whereClause.name)) {
37
- return false
38
- }
39
- // Recursively check all arguments
40
- return whereClause.args.every((arg) =>
41
- isConvertibleToCollectionFilter(arg as BasicExpression<boolean>)
42
- )
43
- }
44
- return [`val`, `ref`].includes(tpe)
45
- }
46
-
47
- /**
48
- * Converts a WHERE clause to BasicExpression format compatible with collection indexes.
49
- * This function creates proper BasicExpression class instances that the collection
50
- * index system can understand.
7
+ * This function recursively traverses an expression tree and creates new BasicExpression
8
+ * instances with normalized paths. The main transformation is removing the collection alias
9
+ * from property reference paths (e.g., `['user', 'id']` becomes `['id']` when `collectionAlias`
10
+ * is `'user'`), which is needed when converting query-level expressions to collection-level
11
+ * expressions for subscriptions.
51
12
  *
52
- * @param whereClause - The WHERE clause to convert
53
- * @param collectionAlias - The alias of the collection being filtered
54
- * @returns The converted BasicExpression or null if conversion fails
13
+ * @param whereClause - The WHERE clause expression to normalize
14
+ * @param collectionAlias - The alias of the collection being filtered (to strip from paths)
15
+ * @returns A new BasicExpression with normalized paths
16
+ *
17
+ * @example
18
+ * // Input: ref with path ['user', 'id'] where collectionAlias is 'user'
19
+ * // Output: ref with path ['id']
55
20
  */
56
- export function convertToBasicExpression(
21
+ export function normalizeExpressionPaths(
57
22
  whereClause: BasicExpression<boolean>,
58
23
  collectionAlias: string
59
- ): BasicExpression<boolean> | null {
24
+ ): BasicExpression<boolean> {
60
25
  const tpe = whereClause.type
61
26
  if (tpe === `val`) {
62
27
  return new Value(whereClause.value)
@@ -74,42 +39,29 @@ export function convertToBasicExpression(
74
39
  // Fallback for non-array paths
75
40
  return new PropRef(Array.isArray(path) ? path : [String(path)])
76
41
  } else {
77
- // Check if this function is supported
78
- if (!SUPPORTED_COLLECTION_FUNCS.has(whereClause.name)) {
79
- return null
80
- }
81
42
  // Recursively convert all arguments
82
43
  const args: Array<BasicExpression> = []
83
44
  for (const arg of whereClause.args) {
84
- const convertedArg = convertToBasicExpression(
45
+ const convertedArg = normalizeExpressionPaths(
85
46
  arg as BasicExpression<boolean>,
86
47
  collectionAlias
87
48
  )
88
- if (convertedArg == null) {
89
- return null
90
- }
91
49
  args.push(convertedArg)
92
50
  }
93
51
  return new Func(whereClause.name, args)
94
52
  }
95
53
  }
96
54
 
97
- export function convertOrderByToBasicExpression(
55
+ export function normalizeOrderByPaths(
98
56
  orderBy: OrderBy,
99
57
  collectionAlias: string
100
58
  ): OrderBy {
101
59
  const normalizedOrderBy = orderBy.map((clause) => {
102
- const basicExp = convertToBasicExpression(
60
+ const basicExp = normalizeExpressionPaths(
103
61
  clause.expression,
104
62
  collectionAlias
105
63
  )
106
64
 
107
- if (!basicExp) {
108
- throw new Error(
109
- `Failed to convert orderBy expression to a basic expression: ${clause.expression}`
110
- )
111
- }
112
-
113
65
  return {
114
66
  ...clause,
115
67
  expression: basicExp,
@@ -1,9 +1,8 @@
1
1
  import { MultiSet } from "@tanstack/db-ivm"
2
2
  import {
3
- convertOrderByToBasicExpression,
4
- convertToBasicExpression,
3
+ normalizeExpressionPaths,
4
+ normalizeOrderByPaths,
5
5
  } from "../compiler/expressions.js"
6
- import { WhereClauseConversionError } from "../../errors.js"
7
6
  import type { MultiSetArray, RootStreamBuilder } from "@tanstack/db-ivm"
8
7
  import type { Collection } from "../../collection/index.js"
9
8
  import type { ChangeMessage } from "../../types.js"
@@ -41,13 +40,8 @@ export class CollectionSubscriber<
41
40
  const whereClause = this.getWhereClauseForAlias()
42
41
 
43
42
  if (whereClause) {
44
- const whereExpression = convertToBasicExpression(whereClause, this.alias)
45
-
46
- if (whereExpression) {
47
- return this.subscribeToChanges(whereExpression)
48
- }
49
-
50
- throw new WhereClauseConversionError(this.collectionId, this.alias)
43
+ const whereExpression = normalizeExpressionPaths(whereClause, this.alias)
44
+ return this.subscribeToChanges(whereExpression)
51
45
  }
52
46
 
53
47
  return this.subscribeToChanges()
@@ -199,10 +193,7 @@ export class CollectionSubscriber<
199
193
  subscription.setOrderByIndex(index)
200
194
 
201
195
  // Normalize the orderBy clauses such that the references are relative to the collection
202
- const normalizedOrderBy = convertOrderByToBasicExpression(
203
- orderBy,
204
- this.alias
205
- )
196
+ const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias)
206
197
 
207
198
  // Load the first `offset + limit` values from the index
208
199
  // i.e. the K items from the collection that fall into the requested range: [offset, offset + limit[
@@ -289,10 +280,7 @@ export class CollectionSubscriber<
289
280
  : biggestSentRow
290
281
 
291
282
  // Normalize the orderBy clauses such that the references are relative to the collection
292
- const normalizedOrderBy = convertOrderByToBasicExpression(
293
- orderBy,
294
- this.alias
295
- )
283
+ const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias)
296
284
 
297
285
  // Take the `n` items after the biggest sent value
298
286
  subscription.requestLimitedSnapshot({
@@ -131,7 +131,6 @@ import {
131
131
  getWhereExpression,
132
132
  isResidualWhere,
133
133
  } from "./ir.js"
134
- import { isConvertibleToCollectionFilter } from "./compiler/expressions.js"
135
134
  import type { BasicExpression, From, QueryIR, Select, Where } from "./ir.js"
136
135
 
137
136
  /**
@@ -248,14 +247,10 @@ function extractSourceWhereClauses(
248
247
  const groupedClauses = groupWhereClauses(analyzedClauses)
249
248
 
250
249
  // Only include single-source clauses that reference collections directly
251
- // and can be converted to BasicExpression format for collection indexes
252
250
  for (const [sourceAlias, whereClause] of groupedClauses.singleSource) {
253
251
  // Check if this source alias corresponds to a collection reference
254
252
  if (isCollectionReference(query, sourceAlias)) {
255
- // Check if the WHERE clause can be converted to collection-compatible format
256
- if (isConvertibleToCollectionFilter(whereClause)) {
257
- sourceWhereClauses.set(sourceAlias, whereClause)
258
- }
253
+ sourceWhereClauses.set(sourceAlias, whereClause)
259
254
  }
260
255
  }
261
256
 
package/src/types.ts CHANGED
@@ -139,7 +139,26 @@ export type NonEmptyArray<T> = [T, ...Array<T>]
139
139
  export type TransactionWithMutations<
140
140
  T extends object = Record<string, unknown>,
141
141
  TOperation extends OperationType = OperationType,
142
- > = Transaction<T> & {
142
+ > = Omit<Transaction<T>, `mutations`> & {
143
+ /**
144
+ * We must omit the `mutations` property from `Transaction<T>` before intersecting
145
+ * because TypeScript intersects property types when the same property appears on
146
+ * both sides of an intersection.
147
+ *
148
+ * Without `Omit`:
149
+ * - `Transaction<T>` has `mutations: Array<PendingMutation<T>>`
150
+ * - The intersection would create: `Array<PendingMutation<T>> & NonEmptyArray<PendingMutation<T, TOperation>>`
151
+ * - When mapping over this array, TypeScript widens `TOperation` from the specific literal
152
+ * (e.g., `"delete"`) to the union `OperationType` (`"insert" | "update" | "delete"`)
153
+ * - This causes `PendingMutation<T, OperationType>` to evaluate the conditional type
154
+ * `original: TOperation extends 'insert' ? {} : T` as `{} | T` instead of just `T`
155
+ *
156
+ * With `Omit`:
157
+ * - We remove `mutations` from `Transaction<T>` first
158
+ * - Then add back `mutations: NonEmptyArray<PendingMutation<T, TOperation>>`
159
+ * - TypeScript can properly narrow `TOperation` to the specific literal type
160
+ * - This ensures `mutation.original` is correctly typed as `T` (not `{} | T`) when mapping
161
+ */
143
162
  mutations: NonEmptyArray<PendingMutation<T, TOperation>>
144
163
  }
145
164