@tanstack/db 0.6.2 → 0.6.4

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 (41) hide show
  1. package/dist/cjs/query/builder/functions.cjs +2 -0
  2. package/dist/cjs/query/builder/functions.cjs.map +1 -1
  3. package/dist/cjs/query/builder/functions.d.cts +2 -0
  4. package/dist/cjs/query/builder/ref-proxy.cjs +6 -0
  5. package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
  6. package/dist/cjs/query/builder/ref-proxy.d.cts +4 -2
  7. package/dist/cjs/query/compiler/joins.cjs +5 -0
  8. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  9. package/dist/cjs/query/compiler/order-by.cjs +14 -5
  10. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  11. package/dist/cjs/query/effect.cjs +1 -1
  12. package/dist/cjs/query/effect.cjs.map +1 -1
  13. package/dist/cjs/query/live/collection-config-builder.cjs +22 -1
  14. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  15. package/dist/cjs/query/live/collection-subscriber.cjs +2 -2
  16. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  17. package/dist/esm/query/builder/functions.d.ts +2 -0
  18. package/dist/esm/query/builder/functions.js +2 -0
  19. package/dist/esm/query/builder/functions.js.map +1 -1
  20. package/dist/esm/query/builder/ref-proxy.d.ts +4 -2
  21. package/dist/esm/query/builder/ref-proxy.js +6 -0
  22. package/dist/esm/query/builder/ref-proxy.js.map +1 -1
  23. package/dist/esm/query/compiler/joins.js +5 -0
  24. package/dist/esm/query/compiler/joins.js.map +1 -1
  25. package/dist/esm/query/compiler/order-by.js +14 -5
  26. package/dist/esm/query/compiler/order-by.js.map +1 -1
  27. package/dist/esm/query/effect.js +1 -1
  28. package/dist/esm/query/effect.js.map +1 -1
  29. package/dist/esm/query/live/collection-config-builder.js +22 -1
  30. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  31. package/dist/esm/query/live/collection-subscriber.js +2 -2
  32. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  33. package/package.json +1 -1
  34. package/skills/db-core/live-queries/SKILL.md +2 -1
  35. package/src/query/builder/functions.ts +2 -0
  36. package/src/query/builder/ref-proxy.ts +18 -2
  37. package/src/query/compiler/joins.ts +8 -0
  38. package/src/query/compiler/order-by.ts +21 -6
  39. package/src/query/effect.ts +1 -1
  40. package/src/query/live/collection-config-builder.ts +29 -1
  41. package/src/query/live/collection-subscriber.ts +5 -5
@@ -284,8 +284,10 @@ export function createRefProxyWithSelected<T extends Record<string, any>>(
284
284
  }
285
285
 
286
286
  /**
287
- * Converts a value to an Expression
288
- * If it's a RefProxy, creates a Ref, otherwise creates a Value
287
+ * Converts a value to an Expression.
288
+ * If it's a RefProxy, creates a PropRef. Throws if the value is a
289
+ * ToArrayWrapper or ConcatToArrayWrapper (these must be used as direct
290
+ * select fields). Otherwise wraps it as a Value.
289
291
  */
290
292
  export function toExpression<T = any>(value: T): BasicExpression<T>
291
293
  export function toExpression(value: RefProxy<any>): BasicExpression<any>
@@ -293,6 +295,20 @@ export function toExpression(value: any): BasicExpression<any> {
293
295
  if (isRefProxy(value)) {
294
296
  return new PropRef(value.__path)
295
297
  }
298
+ // toArray() and concat(toArray()) must be used as direct select fields, not inside expressions
299
+ if (
300
+ value &&
301
+ typeof value === `object` &&
302
+ (value.__brand === `ToArrayWrapper` ||
303
+ value.__brand === `ConcatToArrayWrapper`)
304
+ ) {
305
+ const name =
306
+ value.__brand === `ToArrayWrapper` ? `toArray()` : `concat(toArray())`
307
+ throw new Error(
308
+ `${name} cannot be used inside expressions (e.g., coalesce(), eq(), not()). ` +
309
+ `Use ${name} directly as a select field value instead.`,
310
+ )
311
+ }
296
312
  // If it's already an Expression (Func, Ref, Value) or Agg, return it directly
297
313
  if (
298
314
  value &&
@@ -324,6 +324,14 @@ function processJoin(
324
324
 
325
325
  if (!loaded) {
326
326
  // Snapshot wasn't sent because it could not be loaded from the indexes
327
+ const collectionId = followRefCollection.id
328
+ const fieldPath = followRefResult.path.join(`.`)
329
+ console.warn(
330
+ `[TanStack DB]${collectionId ? ` [${collectionId}]` : ``} Join requires an index on "${fieldPath}" for efficient loading. ` +
331
+ `Falling back to loading all data. ` +
332
+ `Consider creating an index on the collection with collection.createIndex((row) => row.${fieldPath}) ` +
333
+ `or enable auto-indexing with autoIndex: 'eager' and a defaultIndexType.`,
334
+ )
327
335
  lazySourceSubscription.requestSnapshot()
328
336
  }
329
337
  }),
@@ -191,6 +191,17 @@ export function processOrderBy(
191
191
  index = undefined
192
192
  }
193
193
 
194
+ if (!index) {
195
+ const collectionId = followRefCollection.id
196
+ const fieldPath = followRefResult.path.join(`.`)
197
+ console.warn(
198
+ `[TanStack DB]${collectionId ? ` [${collectionId}]` : ``} orderBy with limit requires an index on "${fieldPath}" for efficient lazy loading. ` +
199
+ `Falling back to loading all data. ` +
200
+ `Consider creating an index on the collection with collection.createIndex((row) => row.${fieldPath}) ` +
201
+ `or enable auto-indexing with autoIndex: 'eager' and a defaultIndexType.`,
202
+ )
203
+ }
204
+
194
205
  orderByAlias =
195
206
  firstOrderByExpression.path.length > 1
196
207
  ? String(firstOrderByExpression.path[0])
@@ -292,12 +303,16 @@ export function processOrderBy(
292
303
 
293
304
  // Set up lazy loading callback to track how much more data is needed
294
305
  // This is used by loadMoreIfNeeded to determine if more data should be loaded
295
- setSizeCallback = (getSize: () => number) => {
296
- optimizableOrderByCollections[targetCollectionId]![`dataNeeded`] =
297
- () => {
298
- const size = getSize()
299
- return Math.max(0, orderByOptimizationInfo!.limit - size)
300
- }
306
+ // Only enable when an index exists — without an index, lazy loading can't work
307
+ // and all data is loaded eagerly via requestSnapshot instead.
308
+ if (index) {
309
+ setSizeCallback = (getSize: () => number) => {
310
+ optimizableOrderByCollections[targetCollectionId]![`dataNeeded`] =
311
+ () => {
312
+ const size = getSize()
313
+ return Math.max(0, orderByOptimizationInfo!.limit - size)
314
+ }
315
+ }
301
316
  }
302
317
  }
303
318
  }
@@ -883,7 +883,7 @@ class EffectPipelineRunner<TRow extends object, TKey extends string | number> {
883
883
  for (const [, orderByInfo] of Object.entries(
884
884
  this.optimizableOrderByCollections,
885
885
  )) {
886
- if (!orderByInfo.dataNeeded) continue
886
+ if (!orderByInfo.dataNeeded || !orderByInfo.index) continue
887
887
 
888
888
  if (this.pendingOrderedLoadPromise) {
889
889
  // Wait for in-flight loads to complete before requesting more
@@ -1699,6 +1699,30 @@ function flushIncludesState(
1699
1699
  )
1700
1700
  }
1701
1701
  }
1702
+ // Finally: entries with deep nested buffer changes (grandchild-or-deeper buffers
1703
+ // have pending data, but neither this level nor the immediate child level changed).
1704
+ // Without this pass, changes at depth 3+ are stranded because drainNestedBuffers
1705
+ // only drains one level and Phase 4 only flushes entries dirty from Phase 2/3.
1706
+ const deepBufferDirty = new Set<unknown>()
1707
+ if (state.nestedSetups) {
1708
+ for (const [correlationKey, entry] of state.childRegistry) {
1709
+ if (entriesWithChildChanges.has(correlationKey)) continue
1710
+ if (dirtyFromBuffers.has(correlationKey)) continue
1711
+ if (
1712
+ entry.includesStates &&
1713
+ hasPendingIncludesChanges(entry.includesStates)
1714
+ ) {
1715
+ flushIncludesState(
1716
+ entry.includesStates,
1717
+ entry.collection,
1718
+ entry.collection.id,
1719
+ null,
1720
+ entry.syncMethods,
1721
+ )
1722
+ deepBufferDirty.add(correlationKey)
1723
+ }
1724
+ }
1725
+ }
1702
1726
 
1703
1727
  // For inline materializations: re-emit affected parents with updated snapshots.
1704
1728
  // We mutate items in-place (so collection.get() reflects changes immediately)
@@ -1707,7 +1731,11 @@ function flushIncludesState(
1707
1731
  // deepEquals, but in-place mutation means both sides reference the same
1708
1732
  // object, so the comparison always returns true and suppresses the event.
1709
1733
  const inlineReEmitKeys = materializesInline(state)
1710
- ? new Set([...(affectedCorrelationKeys || []), ...dirtyFromBuffers])
1734
+ ? new Set([
1735
+ ...(affectedCorrelationKeys || []),
1736
+ ...dirtyFromBuffers,
1737
+ ...deepBufferDirty,
1738
+ ])
1711
1739
  : null
1712
1740
  if (parentSyncMethods && inlineReEmitKeys && inlineReEmitKeys.size > 0) {
1713
1741
  const events: Array<ChangeMessage<any>> = []
@@ -332,12 +332,12 @@ export class CollectionSubscriber<
332
332
  return true
333
333
  }
334
334
 
335
- const { dataNeeded } = orderByInfo
335
+ const { dataNeeded, index } = orderByInfo
336
336
 
337
- if (!dataNeeded) {
338
- // dataNeeded is not set when there's no index (e.g., non-ref expression).
339
- // In this case, we've already loaded all data via requestSnapshot
340
- // and don't need to lazily load more.
337
+ if (!dataNeeded || !index) {
338
+ // dataNeeded is not set when there's no index (e.g., non-ref expression
339
+ // or auto-indexing is disabled). Without an index, lazy loading can't work —
340
+ // all data was already loaded eagerly via requestSnapshot.
341
341
  return true
342
342
  }
343
343