@tanstack/db 0.1.12 → 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 -60
  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 -61
  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 -101
  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,90 +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 followRefResult = followRef(
212
- rawQuery,
213
- lazyCollectionJoinExpr,
214
- lazyCollection
215
- )!
216
- const followRefCollection = followRefResult.collection
217
-
218
- const fieldName = followRefResult.path[0]
219
- if (fieldName) {
220
- ensureIndexForField(fieldName, followRefResult.path, followRefCollection)
221
- }
222
-
223
- let deoptimized = false
224
-
225
- const activePipelineWithLoading: IStreamBuilder<
226
- [key: unknown, [originalKey: string, namespacedRow: NamespacedRow]]
227
- > = activePipeline.pipe(
228
- tap(([joinKey, _]) => {
229
- if (deoptimized) {
230
- return
231
- }
232
-
233
- // Find the index for the path we join on
234
- // we need to find the index inside the map operator
235
- // because the indexes are only available after the initial sync
236
- // so we can't fetch it during compilation
237
- index ??= findIndexForField(
238
- followRefCollection.indexes,
239
- 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
240
238
  )
239
+ }
241
240
 
242
- // The `callbacks` object is passed by the liveQueryCollection to the compiler.
243
- // It contains a function to lazy load keys for each lazy collection
244
- // as well as a function to switch back to a regular collection
245
- // (useful when there's no index for available for lazily loading the collection)
246
- const collectionCallbacks = callbacks[lazyCollection.id]
247
- if (!collectionCallbacks) {
248
- throw new Error(
249
- `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
250
258
  )
251
- }
252
259
 
253
- const { loadKeys, loadInitialState } = collectionCallbacks
254
-
255
- if (index && index.supports(`eq`)) {
256
- // Use the index to fetch the PKs of the rows in the lazy collection
257
- // that match this row from the active collection based on the value of the joinKey
258
- const matchingKeys = index.lookup(`eq`, joinKey)
259
- // Inform the lazy collection that those keys need to be loaded
260
- loadKeys(matchingKeys)
261
- } else {
262
- // We can't optimize the join because there is no index for the join key
263
- // on the lazy collection, so we load the initial state
264
- deoptimized = true
265
- loadInitialState()
266
- }
267
- })
268
- )
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
+ )
269
287
 
270
- if (activeCollection === `main`) {
271
- mainPipeline = activePipelineWithLoading
272
- } else {
273
- joinedPipeline = activePipelineWithLoading
288
+ if (activeCollection === `main`) {
289
+ mainPipeline = activePipelineWithLoading
290
+ } else {
291
+ joinedPipeline = activePipelineWithLoading
292
+ }
274
293
  }
275
294
  }
276
295
 
@@ -283,53 +302,65 @@ function processJoin(
283
302
 
284
303
  /**
285
304
  * Analyzes join expressions to determine which refers to which table
286
- * 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)
287
306
  */
288
307
  function analyzeJoinExpressions(
289
308
  left: BasicExpression,
290
309
  right: BasicExpression,
291
- mainTableAlias: string,
310
+ allAvailableTableAliases: Array<string>,
292
311
  joinedTableAlias: string
293
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
+
294
318
  const leftTableAlias = getTableAliasFromExpression(left)
295
319
  const rightTableAlias = getTableAliasFromExpression(right)
296
320
 
297
- // 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
298
322
  if (
299
- leftTableAlias === mainTableAlias &&
323
+ leftTableAlias &&
324
+ availableTableAliases.includes(leftTableAlias) &&
300
325
  rightTableAlias === joinedTableAlias
301
326
  ) {
302
327
  return { mainExpr: left, joinedExpr: right }
303
328
  }
304
329
 
305
- // 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
306
331
  if (
307
332
  leftTableAlias === joinedTableAlias &&
308
- rightTableAlias === mainTableAlias
333
+ rightTableAlias &&
334
+ availableTableAliases.includes(rightTableAlias)
309
335
  ) {
310
336
  return { mainExpr: right, joinedExpr: left }
311
337
  }
312
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
+
313
345
  // If both expressions refer to the same alias, this is an invalid join
314
346
  if (leftTableAlias === rightTableAlias) {
315
- throw new InvalidJoinConditionSameTableError(leftTableAlias || `unknown`)
347
+ throw new InvalidJoinConditionSameTableError(leftTableAlias)
316
348
  }
317
349
 
318
- // If one expression doesn't refer to either table, this is an invalid join
319
- if (!leftTableAlias || !rightTableAlias) {
320
- throw new InvalidJoinConditionTableMismatchError(
321
- mainTableAlias,
322
- joinedTableAlias
323
- )
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)
324
355
  }
325
356
 
326
- // If expressions refer to tables not involved in this join, this is an invalid join
327
- throw new InvalidJoinConditionWrongTablesError(
328
- leftTableAlias,
329
- rightTableAlias,
330
- mainTableAlias,
331
- joinedTableAlias
332
- )
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()
333
364
  }
334
365
 
335
366
  /**