@tanstack/db 0.1.11 → 0.2.0

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 (74) hide show
  1. package/dist/cjs/errors.cjs +18 -6
  2. package/dist/cjs/errors.cjs.map +1 -1
  3. package/dist/cjs/errors.d.cts +9 -3
  4. package/dist/cjs/index.cjs +3 -1
  5. package/dist/cjs/index.cjs.map +1 -1
  6. package/dist/cjs/query/builder/functions.cjs +4 -1
  7. package/dist/cjs/query/builder/functions.cjs.map +1 -1
  8. package/dist/cjs/query/builder/functions.d.cts +38 -21
  9. package/dist/cjs/query/builder/index.cjs +25 -16
  10. package/dist/cjs/query/builder/index.cjs.map +1 -1
  11. package/dist/cjs/query/builder/index.d.cts +8 -8
  12. package/dist/cjs/query/builder/ref-proxy.cjs +12 -8
  13. package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
  14. package/dist/cjs/query/builder/ref-proxy.d.cts +2 -1
  15. package/dist/cjs/query/builder/types.d.cts +493 -28
  16. package/dist/cjs/query/compiler/evaluators.cjs +29 -0
  17. package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
  18. package/dist/cjs/query/compiler/group-by.cjs +4 -2
  19. package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
  20. package/dist/cjs/query/compiler/index.cjs +13 -4
  21. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  22. package/dist/cjs/query/compiler/joins.cjs +70 -61
  23. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  24. package/dist/cjs/query/compiler/select.cjs +131 -42
  25. package/dist/cjs/query/compiler/select.cjs.map +1 -1
  26. package/dist/cjs/query/compiler/select.d.cts +1 -5
  27. package/dist/cjs/query/ir.cjs +4 -0
  28. package/dist/cjs/query/ir.cjs.map +1 -1
  29. package/dist/cjs/query/ir.d.cts +6 -1
  30. package/dist/cjs/query/optimizer.cjs +61 -20
  31. package/dist/cjs/query/optimizer.cjs.map +1 -1
  32. package/dist/esm/errors.d.ts +9 -3
  33. package/dist/esm/errors.js +18 -6
  34. package/dist/esm/errors.js.map +1 -1
  35. package/dist/esm/index.js +4 -2
  36. package/dist/esm/query/builder/functions.d.ts +38 -21
  37. package/dist/esm/query/builder/functions.js +4 -1
  38. package/dist/esm/query/builder/functions.js.map +1 -1
  39. package/dist/esm/query/builder/index.d.ts +8 -8
  40. package/dist/esm/query/builder/index.js +27 -18
  41. package/dist/esm/query/builder/index.js.map +1 -1
  42. package/dist/esm/query/builder/ref-proxy.d.ts +2 -1
  43. package/dist/esm/query/builder/ref-proxy.js +12 -8
  44. package/dist/esm/query/builder/ref-proxy.js.map +1 -1
  45. package/dist/esm/query/builder/types.d.ts +493 -28
  46. package/dist/esm/query/compiler/evaluators.js +29 -0
  47. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  48. package/dist/esm/query/compiler/group-by.js +4 -2
  49. package/dist/esm/query/compiler/group-by.js.map +1 -1
  50. package/dist/esm/query/compiler/index.js +15 -6
  51. package/dist/esm/query/compiler/index.js.map +1 -1
  52. package/dist/esm/query/compiler/joins.js +71 -62
  53. package/dist/esm/query/compiler/joins.js.map +1 -1
  54. package/dist/esm/query/compiler/select.d.ts +1 -5
  55. package/dist/esm/query/compiler/select.js +131 -42
  56. package/dist/esm/query/compiler/select.js.map +1 -1
  57. package/dist/esm/query/ir.d.ts +6 -1
  58. package/dist/esm/query/ir.js +4 -0
  59. package/dist/esm/query/ir.js.map +1 -1
  60. package/dist/esm/query/optimizer.js +62 -21
  61. package/dist/esm/query/optimizer.js.map +1 -1
  62. package/package.json +2 -2
  63. package/src/errors.ts +17 -10
  64. package/src/query/builder/functions.ts +176 -108
  65. package/src/query/builder/index.ts +68 -48
  66. package/src/query/builder/ref-proxy.ts +14 -20
  67. package/src/query/builder/types.ts +622 -110
  68. package/src/query/compiler/evaluators.ts +30 -0
  69. package/src/query/compiler/group-by.ts +6 -1
  70. package/src/query/compiler/index.ts +23 -6
  71. package/src/query/compiler/joins.ts +132 -104
  72. package/src/query/compiler/select.ts +206 -113
  73. package/src/query/ir.ts +14 -1
  74. package/src/query/optimizer.ts +131 -59
@@ -337,6 +337,36 @@ function compileFunction(func: Func, isSingleRow: boolean): (data: any) => any {
337
337
  }
338
338
  }
339
339
 
340
+ // Null/undefined checking functions
341
+ case `isUndefined`: {
342
+ const arg = compiledArgs[0]!
343
+ return (data) => {
344
+ const value = arg(data)
345
+ return value === undefined
346
+ }
347
+ }
348
+ case `isNotUndefined`: {
349
+ const arg = compiledArgs[0]!
350
+ return (data) => {
351
+ const value = arg(data)
352
+ return value !== undefined
353
+ }
354
+ }
355
+ case `isNull`: {
356
+ const arg = compiledArgs[0]!
357
+ return (data) => {
358
+ const value = arg(data)
359
+ return value === null
360
+ }
361
+ }
362
+ case `isNotNull`: {
363
+ const arg = compiledArgs[0]!
364
+ return (data) => {
365
+ const value = arg(data)
366
+ return value !== null
367
+ }
368
+ }
369
+
340
370
  default:
341
371
  throw new UnknownFunctionError(func.name)
342
372
  }
@@ -349,12 +349,17 @@ function getAggregateFunction(aggExpr: Aggregate) {
349
349
  return typeof value === `number` ? value : value != null ? Number(value) : 0
350
350
  }
351
351
 
352
+ // Create a raw value extractor function for the expression to aggregate
353
+ const rawValueExtractor = ([, namespacedRow]: [string, NamespacedRow]) => {
354
+ return compiledExpr(namespacedRow)
355
+ }
356
+
352
357
  // Return the appropriate aggregate function
353
358
  switch (aggExpr.name.toLowerCase()) {
354
359
  case `sum`:
355
360
  return sum(valueExtractor)
356
361
  case `count`:
357
- return count() // count() doesn't need a value extractor
362
+ return count(rawValueExtractor)
358
363
  case `avg`:
359
364
  return avg(valueExtractor)
360
365
  case `min`:
@@ -7,12 +7,12 @@ import {
7
7
  LimitOffsetRequireOrderByError,
8
8
  UnsupportedFromTypeError,
9
9
  } from "../../errors.js"
10
- import { PropRef, getWhereExpression } from "../ir.js"
10
+ import { PropRef, Value as ValClass, getWhereExpression } from "../ir.js"
11
11
  import { compileExpression } from "./evaluators.js"
12
12
  import { processJoins } from "./joins.js"
13
13
  import { processGroupBy } from "./group-by.js"
14
14
  import { processOrderBy } from "./order-by.js"
15
- import { processSelectToResults } from "./select.js"
15
+ import { processSelect } from "./select.js"
16
16
  import type { OrderByOptimizationInfo } from "./order-by.js"
17
17
  import type {
18
18
  BasicExpression,
@@ -173,7 +173,7 @@ export function compileQuery(
173
173
  })
174
174
  )
175
175
  } else if (query.select) {
176
- pipeline = processSelectToResults(pipeline, query.select, allInputs)
176
+ pipeline = processSelect(pipeline, query.select, allInputs)
177
177
  } else {
178
178
  // If no SELECT clause, create __select_results with the main table data
179
179
  pipeline = pipeline.pipe(
@@ -270,7 +270,8 @@ export function compileQuery(
270
270
  const resultPipeline = orderedPipeline.pipe(
271
271
  map(([key, [row, orderByIndex]]) => {
272
272
  // Extract the final results from __select_results and include orderBy index
273
- const finalResults = (row as any).__select_results
273
+ const raw = (row as any).__select_results
274
+ const finalResults = unwrapValue(raw)
274
275
  return [key, [finalResults, orderByIndex]] as [unknown, [any, string]]
275
276
  })
276
277
  )
@@ -294,7 +295,8 @@ export function compileQuery(
294
295
  const resultPipeline: ResultStream = pipeline.pipe(
295
296
  map(([key, row]) => {
296
297
  // Extract the final results from __select_results and return [key, [results, undefined]]
297
- const finalResults = (row as any).__select_results
298
+ const raw = (row as any).__select_results
299
+ const finalResults = unwrapValue(raw)
298
300
  return [key, [finalResults, undefined]] as [
299
301
  unknown,
300
302
  [any, string | undefined],
@@ -359,7 +361,9 @@ function processFrom(
359
361
  const extractedInput = subQueryInput.pipe(
360
362
  map((data: any) => {
361
363
  const [key, [value, _orderByIndex]] = data
362
- return [key, value] as [unknown, any]
364
+ // Unwrap Value expressions that might have leaked through as the entire row
365
+ const unwrapped = unwrapValue(value)
366
+ return [key, unwrapped] as [unknown, any]
363
367
  })
364
368
  )
365
369
 
@@ -374,6 +378,19 @@ function processFrom(
374
378
  }
375
379
  }
376
380
 
381
+ // Helper to check if a value is a Value expression
382
+ function isValue(raw: any): boolean {
383
+ return (
384
+ raw instanceof ValClass ||
385
+ (raw && typeof raw === `object` && `type` in raw && raw.type === `val`)
386
+ )
387
+ }
388
+
389
+ // Helper to unwrap a Value expression or return the value itself
390
+ function unwrapValue(value: any): any {
391
+ return isValue(value) ? value.value : value
392
+ }
393
+
377
394
  /**
378
395
  * Recursively maps optimized subqueries to their original queries for proper caching.
379
396
  * This ensures that when we encounter the same QueryRef object in different contexts,
@@ -7,9 +7,11 @@ import {
7
7
  } from "@tanstack/db-ivm"
8
8
  import {
9
9
  CollectionInputNotFoundError,
10
+ InvalidJoinCondition,
11
+ InvalidJoinConditionLeftTableError,
12
+ InvalidJoinConditionRightTableError,
10
13
  InvalidJoinConditionSameTableError,
11
14
  InvalidJoinConditionTableMismatchError,
12
- InvalidJoinConditionWrongTablesError,
13
15
  JoinCollectionNotFoundError,
14
16
  UnsupportedJoinSourceTypeError,
15
17
  UnsupportedJoinTypeError,
@@ -139,10 +141,11 @@ function processJoin(
139
141
  )
140
142
 
141
143
  // Analyze which table each expression refers to and swap if necessary
144
+ const availableTableAliases = Object.keys(tables)
142
145
  const { mainExpr, joinedExpr } = analyzeJoinExpressions(
143
146
  joinClause.left,
144
147
  joinClause.right,
145
- mainTableAlias,
148
+ availableTableAliases,
146
149
  joinedTableAlias
147
150
  )
148
151
 
@@ -187,93 +190,106 @@ function processJoin(
187
190
  }
188
191
 
189
192
  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
193
+ // If the lazy collection comes from a subquery that has a limit and/or an offset clause
194
+ // then we need to deoptimize the join because we don't know which rows are in the result set
195
+ // since we simply lookup matching keys in the index but the index contains all rows
196
+ // (not just the ones that pass the limit and offset clauses)
197
+ const lazyFrom =
198
+ activeCollection === `main` ? joinClause.from : rawQuery.from
199
+ const limitedSubquery =
200
+ lazyFrom.type === `queryRef` &&
201
+ (lazyFrom.query.limit || lazyFrom.query.offset)
202
+
203
+ if (!limitedSubquery) {
204
+ // This join can be optimized by having the active collection
205
+ // dynamically load keys into the lazy collection
206
+ // based on the value of the joinKey and by looking up
207
+ // matching rows in the index of the lazy collection
208
+
209
+ // Mark the lazy collection as lazy
210
+ // this Set is passed by the liveQueryCollection to the compiler
211
+ // such that the liveQueryCollection can check it after compilation
212
+ // to know which collections are lazy collections
213
+ lazyCollections.add(lazyCollection.id)
214
+
215
+ const activePipeline =
216
+ activeCollection === `main` ? mainPipeline : joinedPipeline
217
+
218
+ let index: BaseIndex<string | number> | undefined
219
+
220
+ const lazyCollectionJoinExpr =
221
+ activeCollection === `main`
222
+ ? (joinedExpr as PropRef)
223
+ : (mainExpr as PropRef)
224
+
225
+ const followRefResult = followRef(
226
+ rawQuery,
227
+ lazyCollectionJoinExpr,
228
+ lazyCollection
229
+ )!
230
+ const followRefCollection = followRefResult.collection
231
+
232
+ const fieldName = followRefResult.path[0]
233
+ if (fieldName) {
234
+ ensureIndexForField(
235
+ fieldName,
236
+ followRefResult.path,
237
+ followRefCollection
243
238
  )
239
+ }
244
240
 
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.`
241
+ let deoptimized = false
242
+
243
+ const activePipelineWithLoading: IStreamBuilder<
244
+ [key: unknown, [originalKey: string, namespacedRow: NamespacedRow]]
245
+ > = activePipeline.pipe(
246
+ tap(([joinKey, _]) => {
247
+ if (deoptimized) {
248
+ return
249
+ }
250
+
251
+ // Find the index for the path we join on
252
+ // we need to find the index inside the map operator
253
+ // because the indexes are only available after the initial sync
254
+ // so we can't fetch it during compilation
255
+ index ??= findIndexForField(
256
+ followRefCollection.indexes,
257
+ followRefResult.path
253
258
  )
254
- }
255
259
 
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
- )
260
+ // The `callbacks` object is passed by the liveQueryCollection to the compiler.
261
+ // It contains a function to lazy load keys for each lazy collection
262
+ // as well as a function to switch back to a regular collection
263
+ // (useful when there's no index for available for lazily loading the collection)
264
+ const collectionCallbacks = callbacks[lazyCollection.id]
265
+ if (!collectionCallbacks) {
266
+ throw new Error(
267
+ `Internal error: callbacks for collection are missing in join pipeline. Make sure the live query collection sets them before running the pipeline.`
268
+ )
269
+ }
270
+
271
+ const { loadKeys, loadInitialState } = collectionCallbacks
272
+
273
+ if (index && index.supports(`eq`)) {
274
+ // Use the index to fetch the PKs of the rows in the lazy collection
275
+ // that match this row from the active collection based on the value of the joinKey
276
+ const matchingKeys = index.lookup(`eq`, joinKey)
277
+ // Inform the lazy collection that those keys need to be loaded
278
+ loadKeys(matchingKeys)
279
+ } else {
280
+ // We can't optimize the join because there is no index for the join key
281
+ // on the lazy collection, so we load the initial state
282
+ deoptimized = true
283
+ loadInitialState()
284
+ }
285
+ })
286
+ )
272
287
 
273
- if (activeCollection === `main`) {
274
- mainPipeline = activePipelineWithLoading
275
- } else {
276
- joinedPipeline = activePipelineWithLoading
288
+ if (activeCollection === `main`) {
289
+ mainPipeline = activePipelineWithLoading
290
+ } else {
291
+ joinedPipeline = activePipelineWithLoading
292
+ }
277
293
  }
278
294
  }
279
295
 
@@ -286,53 +302,65 @@ function processJoin(
286
302
 
287
303
  /**
288
304
  * Analyzes join expressions to determine which refers to which table
289
- * and returns them in the correct order (main table expression first, joined table expression second)
305
+ * and returns them in the correct order (available table expression first, joined table expression second)
290
306
  */
291
307
  function analyzeJoinExpressions(
292
308
  left: BasicExpression,
293
309
  right: BasicExpression,
294
- mainTableAlias: string,
310
+ allAvailableTableAliases: Array<string>,
295
311
  joinedTableAlias: string
296
312
  ): { mainExpr: BasicExpression; joinedExpr: BasicExpression } {
313
+ // Filter out the joined table alias from the available table aliases
314
+ const availableTableAliases = allAvailableTableAliases.filter(
315
+ (alias) => alias !== joinedTableAlias
316
+ )
317
+
297
318
  const leftTableAlias = getTableAliasFromExpression(left)
298
319
  const rightTableAlias = getTableAliasFromExpression(right)
299
320
 
300
- // If left expression refers to main table and right refers to joined table, keep as is
321
+ // If left expression refers to an available table and right refers to joined table, keep as is
301
322
  if (
302
- leftTableAlias === mainTableAlias &&
323
+ leftTableAlias &&
324
+ availableTableAliases.includes(leftTableAlias) &&
303
325
  rightTableAlias === joinedTableAlias
304
326
  ) {
305
327
  return { mainExpr: left, joinedExpr: right }
306
328
  }
307
329
 
308
- // If left expression refers to joined table and right refers to main table, swap them
330
+ // If left expression refers to joined table and right refers to an available table, swap them
309
331
  if (
310
332
  leftTableAlias === joinedTableAlias &&
311
- rightTableAlias === mainTableAlias
333
+ rightTableAlias &&
334
+ availableTableAliases.includes(rightTableAlias)
312
335
  ) {
313
336
  return { mainExpr: right, joinedExpr: left }
314
337
  }
315
338
 
339
+ // If one expression doesn't refer to any table, this is an invalid join
340
+ if (!leftTableAlias || !rightTableAlias) {
341
+ // For backward compatibility, use the first available table alias in error message
342
+ throw new InvalidJoinConditionTableMismatchError()
343
+ }
344
+
316
345
  // If both expressions refer to the same alias, this is an invalid join
317
346
  if (leftTableAlias === rightTableAlias) {
318
- throw new InvalidJoinConditionSameTableError(leftTableAlias || `unknown`)
347
+ throw new InvalidJoinConditionSameTableError(leftTableAlias)
319
348
  }
320
349
 
321
- // If one expression doesn't refer to either table, this is an invalid join
322
- if (!leftTableAlias || !rightTableAlias) {
323
- throw new InvalidJoinConditionTableMismatchError(
324
- mainTableAlias,
325
- joinedTableAlias
326
- )
350
+ // Left side must refer to an available table
351
+ // This cannot happen with the query builder as there is no way to build a ref
352
+ // to an unavailable table, but just in case, but could happen with the IR
353
+ if (!availableTableAliases.includes(leftTableAlias)) {
354
+ throw new InvalidJoinConditionLeftTableError(leftTableAlias)
327
355
  }
328
356
 
329
- // If expressions refer to tables not involved in this join, this is an invalid join
330
- throw new InvalidJoinConditionWrongTablesError(
331
- leftTableAlias,
332
- rightTableAlias,
333
- mainTableAlias,
334
- joinedTableAlias
335
- )
357
+ // Right side must refer to the joined table
358
+ if (rightTableAlias !== joinedTableAlias) {
359
+ throw new InvalidJoinConditionRightTableError(joinedTableAlias)
360
+ }
361
+
362
+ // This should not be reachable given the logic above, but just in case
363
+ throw new InvalidJoinCondition()
336
364
  }
337
365
 
338
366
  /**