@tanstack/db 0.5.17 → 0.5.19

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 (83) hide show
  1. package/dist/cjs/collection/changes.cjs +3 -0
  2. package/dist/cjs/collection/changes.cjs.map +1 -1
  3. package/dist/cjs/collection/sync.cjs +2 -1
  4. package/dist/cjs/collection/sync.cjs.map +1 -1
  5. package/dist/cjs/proxy.cjs +1 -1
  6. package/dist/cjs/proxy.cjs.map +1 -1
  7. package/dist/cjs/query/builder/index.cjs +5 -4
  8. package/dist/cjs/query/builder/index.cjs.map +1 -1
  9. package/dist/cjs/query/builder/index.d.cts +5 -4
  10. package/dist/cjs/query/builder/ref-proxy.cjs +63 -0
  11. package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
  12. package/dist/cjs/query/builder/ref-proxy.d.cts +13 -0
  13. package/dist/cjs/query/builder/types.d.cts +26 -1
  14. package/dist/cjs/query/compiler/evaluators.cjs +29 -2
  15. package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
  16. package/dist/cjs/query/compiler/group-by.cjs +29 -12
  17. package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
  18. package/dist/cjs/query/compiler/group-by.d.cts +18 -2
  19. package/dist/cjs/query/compiler/index.cjs +5 -5
  20. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  21. package/dist/cjs/query/compiler/order-by.cjs +1 -1
  22. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  23. package/dist/cjs/query/compiler/order-by.d.cts +1 -1
  24. package/dist/cjs/query/compiler/select.cjs +1 -1
  25. package/dist/cjs/query/compiler/select.cjs.map +1 -1
  26. package/dist/cjs/query/compiler/select.d.cts +1 -1
  27. package/dist/cjs/query/live/collection-config-builder.cjs +26 -10
  28. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  29. package/dist/cjs/query/live/collection-subscriber.cjs +42 -33
  30. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  31. package/dist/cjs/query/live/internal.cjs +1 -1
  32. package/dist/cjs/query/live/internal.cjs.map +1 -1
  33. package/dist/cjs/query/live/types.d.cts +2 -1
  34. package/dist/cjs/types.d.cts +6 -0
  35. package/dist/esm/collection/changes.js +3 -0
  36. package/dist/esm/collection/changes.js.map +1 -1
  37. package/dist/esm/collection/sync.js +2 -1
  38. package/dist/esm/collection/sync.js.map +1 -1
  39. package/dist/esm/proxy.js +1 -1
  40. package/dist/esm/proxy.js.map +1 -1
  41. package/dist/esm/query/builder/index.d.ts +5 -4
  42. package/dist/esm/query/builder/index.js +6 -5
  43. package/dist/esm/query/builder/index.js.map +1 -1
  44. package/dist/esm/query/builder/ref-proxy.d.ts +13 -0
  45. package/dist/esm/query/builder/ref-proxy.js +63 -0
  46. package/dist/esm/query/builder/ref-proxy.js.map +1 -1
  47. package/dist/esm/query/builder/types.d.ts +26 -1
  48. package/dist/esm/query/compiler/evaluators.js +29 -2
  49. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  50. package/dist/esm/query/compiler/group-by.d.ts +18 -2
  51. package/dist/esm/query/compiler/group-by.js +30 -13
  52. package/dist/esm/query/compiler/group-by.js.map +1 -1
  53. package/dist/esm/query/compiler/index.js +5 -5
  54. package/dist/esm/query/compiler/index.js.map +1 -1
  55. package/dist/esm/query/compiler/order-by.d.ts +1 -1
  56. package/dist/esm/query/compiler/order-by.js +1 -1
  57. package/dist/esm/query/compiler/order-by.js.map +1 -1
  58. package/dist/esm/query/compiler/select.d.ts +1 -1
  59. package/dist/esm/query/compiler/select.js +1 -1
  60. package/dist/esm/query/compiler/select.js.map +1 -1
  61. package/dist/esm/query/live/collection-config-builder.js +26 -10
  62. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  63. package/dist/esm/query/live/collection-subscriber.js +42 -33
  64. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  65. package/dist/esm/query/live/internal.js +1 -1
  66. package/dist/esm/query/live/internal.js.map +1 -1
  67. package/dist/esm/query/live/types.d.ts +2 -1
  68. package/dist/esm/types.d.ts +6 -0
  69. package/package.json +2 -2
  70. package/src/collection/changes.ts +7 -0
  71. package/src/collection/sync.ts +2 -2
  72. package/src/query/builder/index.ts +22 -6
  73. package/src/query/builder/ref-proxy.ts +90 -0
  74. package/src/query/builder/types.ts +26 -1
  75. package/src/query/compiler/evaluators.ts +38 -2
  76. package/src/query/compiler/group-by.ts +76 -22
  77. package/src/query/compiler/index.ts +13 -13
  78. package/src/query/compiler/order-by.ts +7 -6
  79. package/src/query/compiler/select.ts +5 -8
  80. package/src/query/live/collection-config-builder.ts +66 -16
  81. package/src/query/live/collection-subscriber.ts +60 -41
  82. package/src/query/live/types.ts +3 -1
  83. package/src/types.ts +6 -0
@@ -1 +1 @@
1
- {"version":3,"file":"collection-subscriber.js","sources":["../../../../src/query/live/collection-subscriber.ts"],"sourcesContent":["import { MultiSet } from '@tanstack/db-ivm'\nimport {\n normalizeExpressionPaths,\n normalizeOrderByPaths,\n} from '../compiler/expressions.js'\nimport type { MultiSetArray, RootStreamBuilder } from '@tanstack/db-ivm'\nimport type { Collection } from '../../collection/index.js'\nimport type { ChangeMessage } from '../../types.js'\nimport type { Context, GetResult } from '../builder/types.js'\nimport type { BasicExpression } from '../ir.js'\nimport type { OrderByOptimizationInfo } from '../compiler/order-by.js'\nimport type { CollectionConfigBuilder } from './collection-config-builder.js'\nimport type { CollectionSubscription } from '../../collection/subscription.js'\n\nconst loadMoreCallbackSymbol = Symbol.for(\n `@tanstack/db.collection-config-builder`,\n)\n\nexport class CollectionSubscriber<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n> {\n // Keep track of the biggest value we've sent so far (needed for orderBy optimization)\n private biggest: any = undefined\n\n // Track deferred promises for subscription loading states\n private subscriptionLoadingPromises = new Map<\n CollectionSubscription,\n { resolve: () => void }\n >()\n\n // Track keys that have been sent to the D2 pipeline to prevent duplicate inserts\n // This is necessary because different code paths (initial load, change events)\n // can potentially send the same item to D2 multiple times.\n private sentToD2Keys = new Set<string | number>()\n\n constructor(\n private alias: string,\n private collectionId: string,\n private collection: Collection,\n private collectionConfigBuilder: CollectionConfigBuilder<TContext, TResult>,\n ) {}\n\n subscribe(): CollectionSubscription {\n const whereClause = this.getWhereClauseForAlias()\n\n if (whereClause) {\n const whereExpression = normalizeExpressionPaths(whereClause, this.alias)\n return this.subscribeToChanges(whereExpression)\n }\n\n return this.subscribeToChanges()\n }\n\n private subscribeToChanges(whereExpression?: BasicExpression<boolean>) {\n let subscription: CollectionSubscription\n const orderByInfo = this.getOrderByInfo()\n if (orderByInfo) {\n subscription = this.subscribeToOrderedChanges(\n whereExpression,\n orderByInfo,\n )\n } else {\n // If the source alias is lazy then we should not include the initial state\n const includeInitialState = !this.collectionConfigBuilder.isLazyAlias(\n this.alias,\n )\n\n subscription = this.subscribeToMatchingChanges(\n whereExpression,\n includeInitialState,\n )\n }\n\n const trackLoadPromise = () => {\n // Guard against duplicate transitions\n if (!this.subscriptionLoadingPromises.has(subscription)) {\n let resolve: () => void\n const promise = new Promise<void>((res) => {\n resolve = res\n })\n\n this.subscriptionLoadingPromises.set(subscription, {\n resolve: resolve!,\n })\n this.collectionConfigBuilder.liveQueryCollection!._sync.trackLoadPromise(\n promise,\n )\n }\n }\n\n // It can be that we are not yet subscribed when the first `loadSubset` call happens (i.e. the initial query).\n // So we also check the status here and if it's `loadingSubset` then we track the load promise\n if (subscription.status === `loadingSubset`) {\n trackLoadPromise()\n }\n\n // Subscribe to subscription status changes to propagate loading state\n const statusUnsubscribe = subscription.on(`status:change`, (event) => {\n if (event.status === `loadingSubset`) {\n trackLoadPromise()\n } else {\n // status is 'ready'\n const deferred = this.subscriptionLoadingPromises.get(subscription)\n if (deferred) {\n // Clear the map entry FIRST (before resolving)\n this.subscriptionLoadingPromises.delete(subscription)\n deferred.resolve()\n }\n }\n })\n\n const unsubscribe = () => {\n // If subscription has a pending promise, resolve it before unsubscribing\n const deferred = this.subscriptionLoadingPromises.get(subscription)\n if (deferred) {\n // Clear the map entry FIRST (before resolving)\n this.subscriptionLoadingPromises.delete(subscription)\n deferred.resolve()\n }\n\n statusUnsubscribe()\n subscription.unsubscribe()\n }\n // currentSyncState is always defined when subscribe() is called\n // (called during sync session setup)\n this.collectionConfigBuilder.currentSyncState!.unsubscribeCallbacks.add(\n unsubscribe,\n )\n return subscription\n }\n\n private sendChangesToPipeline(\n changes: Iterable<ChangeMessage<any, string | number>>,\n callback?: () => boolean,\n ) {\n // Filter changes to prevent duplicate inserts to D2 pipeline.\n // This ensures D2 multiplicity stays at 1 for visible items, so deletes\n // properly reduce multiplicity to 0 (triggering DELETE output).\n const changesArray = Array.isArray(changes) ? changes : [...changes]\n const filteredChanges: Array<ChangeMessage<any, string | number>> = []\n for (const change of changesArray) {\n if (change.type === `insert`) {\n if (this.sentToD2Keys.has(change.key)) {\n // Skip duplicate insert - already sent to D2\n continue\n }\n this.sentToD2Keys.add(change.key)\n } else if (change.type === `delete`) {\n // Remove from tracking so future re-inserts are allowed\n this.sentToD2Keys.delete(change.key)\n }\n // Updates are handled as delete+insert by splitUpdates, so no special handling needed\n filteredChanges.push(change)\n }\n\n // currentSyncState and input are always defined when this method is called\n // (only called from active subscriptions during a sync session)\n const input =\n this.collectionConfigBuilder.currentSyncState!.inputs[this.alias]!\n const sentChanges = sendChangesToInput(\n input,\n filteredChanges,\n this.collection.config.getKey,\n )\n\n // Do not provide the callback that loads more data\n // if there's no more data to load\n // otherwise we end up in an infinite loop trying to load more data\n const dataLoader = sentChanges > 0 ? callback : undefined\n\n // We need to schedule a graph run even if there's no data to load\n // because we need to mark the collection as ready if it's not already\n // and that's only done in `scheduleGraphRun`\n this.collectionConfigBuilder.scheduleGraphRun(dataLoader, {\n alias: this.alias,\n })\n }\n\n private subscribeToMatchingChanges(\n whereExpression: BasicExpression<boolean> | undefined,\n includeInitialState: boolean = false,\n ) {\n const sendChanges = (\n changes: Array<ChangeMessage<any, string | number>>,\n ) => {\n this.sendChangesToPipeline(changes)\n }\n\n // Only pass includeInitialState when true. When it's false, we leave it\n // undefined so that user subscriptions with explicit `includeInitialState: false`\n // can be distinguished from internal lazy-loading subscriptions.\n // If we pass `false`, changes.ts would call markAllStateAsSeen() which\n // disables filtering - but internal subscriptions still need filtering.\n const subscription = this.collection.subscribeChanges(sendChanges, {\n ...(includeInitialState && { includeInitialState }),\n whereExpression,\n })\n\n return subscription\n }\n\n private subscribeToOrderedChanges(\n whereExpression: BasicExpression<boolean> | undefined,\n orderByInfo: OrderByOptimizationInfo,\n ) {\n const { orderBy, offset, limit, index } = orderByInfo\n\n const sendChangesInRange = (\n changes: Iterable<ChangeMessage<any, string | number>>,\n ) => {\n // Split live updates into a delete of the old value and an insert of the new value\n const splittedChanges = splitUpdates(changes)\n this.sendChangesToPipelineWithTracking(splittedChanges, subscription)\n }\n\n // Subscribe to changes and only send changes that are smaller than the biggest value we've sent so far\n // values that are bigger don't need to be sent because they can't affect the topK\n const subscription = this.collection.subscribeChanges(sendChangesInRange, {\n whereExpression,\n })\n\n // Listen for truncate events to reset cursor tracking state and sentToD2Keys\n // This ensures that after a must-refetch/truncate, we don't use stale cursor data\n // and allow re-inserts of previously sent keys\n const truncateUnsubscribe = this.collection.on(`truncate`, () => {\n this.biggest = undefined\n this.sentToD2Keys.clear()\n })\n\n // Clean up truncate listener when subscription is unsubscribed\n subscription.on(`unsubscribed`, () => {\n truncateUnsubscribe()\n })\n\n // Normalize the orderBy clauses such that the references are relative to the collection\n const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias)\n\n if (index) {\n // We have an index on the first orderBy column - use lazy loading optimization\n // This works for both single-column and multi-column orderBy:\n // - Single-column: index provides exact ordering\n // - Multi-column: index provides ordering on first column, secondary sort in memory\n subscription.setOrderByIndex(index)\n\n // Load the first `offset + limit` values from the index\n // i.e. the K items from the collection that fall into the requested range: [offset, offset + limit[\n subscription.requestLimitedSnapshot({\n limit: offset + limit,\n orderBy: normalizedOrderBy,\n })\n } else {\n // No index available (e.g., non-ref expression): pass orderBy/limit to loadSubset\n // so the sync layer can optimize if the backend supports it\n subscription.requestSnapshot({\n orderBy: normalizedOrderBy,\n limit: offset + limit,\n })\n }\n\n return subscription\n }\n\n // This function is called by maybeRunGraph\n // after each iteration of the query pipeline\n // to ensure that the orderBy operator has enough data to work with\n loadMoreIfNeeded(subscription: CollectionSubscription) {\n const orderByInfo = this.getOrderByInfo()\n\n if (!orderByInfo) {\n // This query has no orderBy operator\n // so there's no data to load\n return true\n }\n\n const { dataNeeded } = orderByInfo\n\n if (!dataNeeded) {\n // dataNeeded is not set when there's no index (e.g., non-ref expression).\n // In this case, we've already loaded all data via requestSnapshot\n // and don't need to lazily load more.\n return true\n }\n\n // `dataNeeded` probes the orderBy operator to see if it needs more data\n // if it needs more data, it returns the number of items it needs\n const n = dataNeeded()\n if (n > 0) {\n this.loadNextItems(n, subscription)\n }\n return true\n }\n\n private sendChangesToPipelineWithTracking(\n changes: Iterable<ChangeMessage<any, string | number>>,\n subscription: CollectionSubscription,\n ) {\n const orderByInfo = this.getOrderByInfo()\n if (!orderByInfo) {\n this.sendChangesToPipeline(changes)\n return\n }\n\n const trackedChanges = this.trackSentValues(changes, orderByInfo.comparator)\n\n // Cache the loadMoreIfNeeded callback on the subscription using a symbol property.\n // This ensures we pass the same function instance to the scheduler each time,\n // allowing it to deduplicate callbacks when multiple changes arrive during a transaction.\n type SubscriptionWithLoader = CollectionSubscription & {\n [loadMoreCallbackSymbol]?: () => boolean\n }\n\n const subscriptionWithLoader = subscription as SubscriptionWithLoader\n\n subscriptionWithLoader[loadMoreCallbackSymbol] ??=\n this.loadMoreIfNeeded.bind(this, subscription)\n\n this.sendChangesToPipeline(\n trackedChanges,\n subscriptionWithLoader[loadMoreCallbackSymbol],\n )\n }\n\n // Loads the next `n` items from the collection\n // starting from the biggest item it has sent\n private loadNextItems(n: number, subscription: CollectionSubscription) {\n const orderByInfo = this.getOrderByInfo()\n if (!orderByInfo) {\n return\n }\n const { orderBy, valueExtractorForRawRow, offset } = orderByInfo\n const biggestSentRow = this.biggest\n\n // Extract all orderBy column values from the biggest sent row\n // For single-column: returns single value, for multi-column: returns array\n const extractedValues = biggestSentRow\n ? valueExtractorForRawRow(biggestSentRow)\n : undefined\n\n // Normalize to array format for minValues\n const minValues =\n extractedValues !== undefined\n ? Array.isArray(extractedValues)\n ? extractedValues\n : [extractedValues]\n : undefined\n\n // Normalize the orderBy clauses such that the references are relative to the collection\n const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias)\n\n // Take the `n` items after the biggest sent value\n // Pass the current window offset to ensure proper deduplication\n subscription.requestLimitedSnapshot({\n orderBy: normalizedOrderBy,\n limit: n,\n minValues,\n offset,\n })\n }\n\n private getWhereClauseForAlias(): BasicExpression<boolean> | undefined {\n const sourceWhereClausesCache =\n this.collectionConfigBuilder.sourceWhereClausesCache\n if (!sourceWhereClausesCache) {\n return undefined\n }\n return sourceWhereClausesCache.get(this.alias)\n }\n\n private getOrderByInfo(): OrderByOptimizationInfo | undefined {\n const info =\n this.collectionConfigBuilder.optimizableOrderByCollections[\n this.collectionId\n ]\n if (info && info.alias === this.alias) {\n return info\n }\n return undefined\n }\n\n private *trackSentValues(\n changes: Iterable<ChangeMessage<any, string | number>>,\n comparator: (a: any, b: any) => number,\n ) {\n for (const change of changes) {\n // Only track inserts/updates for cursor positioning, not deletes\n if (change.type !== `delete`) {\n if (!this.biggest) {\n this.biggest = change.value\n } else if (comparator(this.biggest, change.value) < 0) {\n this.biggest = change.value\n }\n }\n\n yield change\n }\n }\n}\n\n/**\n * Helper function to send changes to a D2 input stream\n */\nfunction sendChangesToInput(\n input: RootStreamBuilder<unknown>,\n changes: Iterable<ChangeMessage>,\n getKey: (item: ChangeMessage[`value`]) => any,\n): number {\n const multiSetArray: MultiSetArray<unknown> = []\n for (const change of changes) {\n const key = getKey(change.value)\n if (change.type === `insert`) {\n multiSetArray.push([[key, change.value], 1])\n } else if (change.type === `update`) {\n multiSetArray.push([[key, change.previousValue], -1])\n multiSetArray.push([[key, change.value], 1])\n } else {\n // change.type === `delete`\n multiSetArray.push([[key, change.value], -1])\n }\n }\n\n if (multiSetArray.length !== 0) {\n input.sendData(new MultiSet(multiSetArray))\n }\n\n return multiSetArray.length\n}\n\n/** Splits updates into a delete of the old value and an insert of the new value */\nfunction* splitUpdates<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n>(\n changes: Iterable<ChangeMessage<T, TKey>>,\n): Generator<ChangeMessage<T, TKey>> {\n for (const change of changes) {\n if (change.type === `update`) {\n yield { type: `delete`, key: change.key, value: change.previousValue! }\n yield { type: `insert`, key: change.key, value: change.value }\n } else {\n yield change\n }\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"collection-subscriber.js","sources":["../../../../src/query/live/collection-subscriber.ts"],"sourcesContent":["import { MultiSet } from '@tanstack/db-ivm'\nimport {\n normalizeExpressionPaths,\n normalizeOrderByPaths,\n} from '../compiler/expressions.js'\nimport type { MultiSetArray, RootStreamBuilder } from '@tanstack/db-ivm'\nimport type { Collection } from '../../collection/index.js'\nimport type {\n ChangeMessage,\n SubscriptionStatusChangeEvent,\n} from '../../types.js'\nimport type { Context, GetResult } from '../builder/types.js'\nimport type { BasicExpression } from '../ir.js'\nimport type { OrderByOptimizationInfo } from '../compiler/order-by.js'\nimport type { CollectionConfigBuilder } from './collection-config-builder.js'\nimport type { CollectionSubscription } from '../../collection/subscription.js'\n\nconst loadMoreCallbackSymbol = Symbol.for(\n `@tanstack/db.collection-config-builder`,\n)\n\nexport class CollectionSubscriber<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n> {\n // Keep track of the biggest value we've sent so far (needed for orderBy optimization)\n private biggest: any = undefined\n\n // Track deferred promises for subscription loading states\n private subscriptionLoadingPromises = new Map<\n CollectionSubscription,\n { resolve: () => void }\n >()\n\n // Track keys that have been sent to the D2 pipeline to prevent duplicate inserts\n // This is necessary because different code paths (initial load, change events)\n // can potentially send the same item to D2 multiple times.\n private sentToD2Keys = new Set<string | number>()\n\n constructor(\n private alias: string,\n private collectionId: string,\n private collection: Collection,\n private collectionConfigBuilder: CollectionConfigBuilder<TContext, TResult>,\n ) {}\n\n subscribe(): CollectionSubscription {\n const whereClause = this.getWhereClauseForAlias()\n\n if (whereClause) {\n const whereExpression = normalizeExpressionPaths(whereClause, this.alias)\n return this.subscribeToChanges(whereExpression)\n }\n\n return this.subscribeToChanges()\n }\n\n private subscribeToChanges(whereExpression?: BasicExpression<boolean>) {\n const orderByInfo = this.getOrderByInfo()\n\n // Track load promises using subscription from the event (avoids circular dependency)\n const trackLoadPromise = (subscription: CollectionSubscription) => {\n // Guard against duplicate transitions\n if (!this.subscriptionLoadingPromises.has(subscription)) {\n let resolve: () => void\n const promise = new Promise<void>((res) => {\n resolve = res\n })\n\n this.subscriptionLoadingPromises.set(subscription, {\n resolve: resolve!,\n })\n this.collectionConfigBuilder.liveQueryCollection!._sync.trackLoadPromise(\n promise,\n )\n }\n }\n\n // Status change handler - passed to subscribeChanges so it's registered\n // BEFORE any snapshot is requested, preventing race conditions\n const onStatusChange = (event: SubscriptionStatusChangeEvent) => {\n const subscription = event.subscription as CollectionSubscription\n if (event.status === `loadingSubset`) {\n trackLoadPromise(subscription)\n } else {\n // status is 'ready'\n const deferred = this.subscriptionLoadingPromises.get(subscription)\n if (deferred) {\n // Clear the map entry FIRST (before resolving)\n this.subscriptionLoadingPromises.delete(subscription)\n deferred.resolve()\n }\n }\n }\n\n // Create subscription with onStatusChange - listener is registered before any async work\n let subscription: CollectionSubscription\n if (orderByInfo) {\n subscription = this.subscribeToOrderedChanges(\n whereExpression,\n orderByInfo,\n onStatusChange,\n )\n } else {\n // If the source alias is lazy then we should not include the initial state\n const includeInitialState = !this.collectionConfigBuilder.isLazyAlias(\n this.alias,\n )\n\n subscription = this.subscribeToMatchingChanges(\n whereExpression,\n includeInitialState,\n onStatusChange,\n )\n }\n\n // Check current status after subscribing - if status is 'loadingSubset', track it.\n // The onStatusChange listener will catch the transition to 'ready'.\n if (subscription.status === `loadingSubset`) {\n trackLoadPromise(subscription)\n }\n\n const unsubscribe = () => {\n // If subscription has a pending promise, resolve it before unsubscribing\n const deferred = this.subscriptionLoadingPromises.get(subscription)\n if (deferred) {\n // Clear the map entry FIRST (before resolving)\n this.subscriptionLoadingPromises.delete(subscription)\n deferred.resolve()\n }\n\n subscription.unsubscribe()\n }\n // currentSyncState is always defined when subscribe() is called\n // (called during sync session setup)\n this.collectionConfigBuilder.currentSyncState!.unsubscribeCallbacks.add(\n unsubscribe,\n )\n return subscription\n }\n\n private sendChangesToPipeline(\n changes: Iterable<ChangeMessage<any, string | number>>,\n callback?: () => boolean,\n ) {\n // Filter changes to prevent duplicate inserts to D2 pipeline.\n // This ensures D2 multiplicity stays at 1 for visible items, so deletes\n // properly reduce multiplicity to 0 (triggering DELETE output).\n const changesArray = Array.isArray(changes) ? changes : [...changes]\n const filteredChanges: Array<ChangeMessage<any, string | number>> = []\n for (const change of changesArray) {\n if (change.type === `insert`) {\n if (this.sentToD2Keys.has(change.key)) {\n // Skip duplicate insert - already sent to D2\n continue\n }\n this.sentToD2Keys.add(change.key)\n } else if (change.type === `delete`) {\n // Remove from tracking so future re-inserts are allowed\n this.sentToD2Keys.delete(change.key)\n }\n // Updates are handled as delete+insert by splitUpdates, so no special handling needed\n filteredChanges.push(change)\n }\n\n // currentSyncState and input are always defined when this method is called\n // (only called from active subscriptions during a sync session)\n const input =\n this.collectionConfigBuilder.currentSyncState!.inputs[this.alias]!\n const sentChanges = sendChangesToInput(\n input,\n filteredChanges,\n this.collection.config.getKey,\n )\n\n // Do not provide the callback that loads more data\n // if there's no more data to load\n // otherwise we end up in an infinite loop trying to load more data\n const dataLoader = sentChanges > 0 ? callback : undefined\n\n // We need to schedule a graph run even if there's no data to load\n // because we need to mark the collection as ready if it's not already\n // and that's only done in `scheduleGraphRun`\n this.collectionConfigBuilder.scheduleGraphRun(dataLoader, {\n alias: this.alias,\n })\n }\n\n private subscribeToMatchingChanges(\n whereExpression: BasicExpression<boolean> | undefined,\n includeInitialState: boolean,\n onStatusChange: (event: SubscriptionStatusChangeEvent) => void,\n ): CollectionSubscription {\n const sendChanges = (\n changes: Array<ChangeMessage<any, string | number>>,\n ) => {\n this.sendChangesToPipeline(changes)\n }\n\n // Create subscription with onStatusChange - listener is registered before snapshot\n // Note: For non-ordered queries (no limit/offset), we use trackLoadSubsetPromise: false\n // which is the default behavior in subscribeChanges\n const subscription = this.collection.subscribeChanges(sendChanges, {\n ...(includeInitialState && { includeInitialState }),\n whereExpression,\n onStatusChange,\n })\n\n return subscription\n }\n\n private subscribeToOrderedChanges(\n whereExpression: BasicExpression<boolean> | undefined,\n orderByInfo: OrderByOptimizationInfo,\n onStatusChange: (event: SubscriptionStatusChangeEvent) => void,\n ): CollectionSubscription {\n const { orderBy, offset, limit, index } = orderByInfo\n\n // Use a holder to forward-reference subscription in the callback\n const subscriptionHolder: { current?: CollectionSubscription } = {}\n\n const sendChangesInRange = (\n changes: Iterable<ChangeMessage<any, string | number>>,\n ) => {\n // Split live updates into a delete of the old value and an insert of the new value\n const splittedChanges = splitUpdates(changes)\n this.sendChangesToPipelineWithTracking(\n splittedChanges,\n subscriptionHolder.current!,\n )\n }\n\n // Subscribe to changes with onStatusChange - listener is registered before any snapshot\n // values bigger than what we've sent don't need to be sent because they can't affect the topK\n const subscription = this.collection.subscribeChanges(sendChangesInRange, {\n whereExpression,\n onStatusChange,\n })\n subscriptionHolder.current = subscription\n\n // Listen for truncate events to reset cursor tracking state and sentToD2Keys\n // This ensures that after a must-refetch/truncate, we don't use stale cursor data\n // and allow re-inserts of previously sent keys\n const truncateUnsubscribe = this.collection.on(`truncate`, () => {\n this.biggest = undefined\n this.sentToD2Keys.clear()\n })\n\n // Clean up truncate listener when subscription is unsubscribed\n subscription.on(`unsubscribed`, () => {\n truncateUnsubscribe()\n })\n\n // Normalize the orderBy clauses such that the references are relative to the collection\n const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias)\n\n // Trigger the snapshot request - onStatusChange listener is already registered\n if (index) {\n // We have an index on the first orderBy column - use lazy loading optimization\n // This works for both single-column and multi-column orderBy:\n // - Single-column: index provides exact ordering\n // - Multi-column: index provides ordering on first column, secondary sort in memory\n subscription.setOrderByIndex(index)\n\n // Load the first `offset + limit` values from the index\n // i.e. the K items from the collection that fall into the requested range: [offset, offset + limit[\n subscription.requestLimitedSnapshot({\n limit: offset + limit,\n orderBy: normalizedOrderBy,\n })\n } else {\n // No index available (e.g., non-ref expression): pass orderBy/limit to loadSubset\n // so the sync layer can optimize if the backend supports it\n subscription.requestSnapshot({\n orderBy: normalizedOrderBy,\n limit: offset + limit,\n })\n }\n\n return subscription\n }\n\n // This function is called by maybeRunGraph\n // after each iteration of the query pipeline\n // to ensure that the orderBy operator has enough data to work with\n loadMoreIfNeeded(subscription: CollectionSubscription) {\n const orderByInfo = this.getOrderByInfo()\n\n if (!orderByInfo) {\n // This query has no orderBy operator\n // so there's no data to load\n return true\n }\n\n const { dataNeeded } = orderByInfo\n\n if (!dataNeeded) {\n // dataNeeded is not set when there's no index (e.g., non-ref expression).\n // In this case, we've already loaded all data via requestSnapshot\n // and don't need to lazily load more.\n return true\n }\n\n // `dataNeeded` probes the orderBy operator to see if it needs more data\n // if it needs more data, it returns the number of items it needs\n const n = dataNeeded()\n if (n > 0) {\n this.loadNextItems(n, subscription)\n }\n return true\n }\n\n private sendChangesToPipelineWithTracking(\n changes: Iterable<ChangeMessage<any, string | number>>,\n subscription: CollectionSubscription,\n ) {\n const orderByInfo = this.getOrderByInfo()\n if (!orderByInfo) {\n this.sendChangesToPipeline(changes)\n return\n }\n\n const trackedChanges = this.trackSentValues(changes, orderByInfo.comparator)\n\n // Cache the loadMoreIfNeeded callback on the subscription using a symbol property.\n // This ensures we pass the same function instance to the scheduler each time,\n // allowing it to deduplicate callbacks when multiple changes arrive during a transaction.\n type SubscriptionWithLoader = CollectionSubscription & {\n [loadMoreCallbackSymbol]?: () => boolean\n }\n\n const subscriptionWithLoader = subscription as SubscriptionWithLoader\n\n subscriptionWithLoader[loadMoreCallbackSymbol] ??=\n this.loadMoreIfNeeded.bind(this, subscription)\n\n this.sendChangesToPipeline(\n trackedChanges,\n subscriptionWithLoader[loadMoreCallbackSymbol],\n )\n }\n\n // Loads the next `n` items from the collection\n // starting from the biggest item it has sent\n private loadNextItems(n: number, subscription: CollectionSubscription) {\n const orderByInfo = this.getOrderByInfo()\n if (!orderByInfo) {\n return\n }\n const { orderBy, valueExtractorForRawRow, offset } = orderByInfo\n const biggestSentRow = this.biggest\n\n // Extract all orderBy column values from the biggest sent row\n // For single-column: returns single value, for multi-column: returns array\n const extractedValues = biggestSentRow\n ? valueExtractorForRawRow(biggestSentRow)\n : undefined\n\n // Normalize to array format for minValues\n const minValues =\n extractedValues !== undefined\n ? Array.isArray(extractedValues)\n ? extractedValues\n : [extractedValues]\n : undefined\n\n // Normalize the orderBy clauses such that the references are relative to the collection\n const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias)\n\n // Take the `n` items after the biggest sent value\n // Pass the current window offset to ensure proper deduplication\n subscription.requestLimitedSnapshot({\n orderBy: normalizedOrderBy,\n limit: n,\n minValues,\n offset,\n })\n }\n\n private getWhereClauseForAlias(): BasicExpression<boolean> | undefined {\n const sourceWhereClausesCache =\n this.collectionConfigBuilder.sourceWhereClausesCache\n if (!sourceWhereClausesCache) {\n return undefined\n }\n return sourceWhereClausesCache.get(this.alias)\n }\n\n private getOrderByInfo(): OrderByOptimizationInfo | undefined {\n const info =\n this.collectionConfigBuilder.optimizableOrderByCollections[\n this.collectionId\n ]\n if (info && info.alias === this.alias) {\n return info\n }\n return undefined\n }\n\n private *trackSentValues(\n changes: Iterable<ChangeMessage<any, string | number>>,\n comparator: (a: any, b: any) => number,\n ) {\n for (const change of changes) {\n // Only track inserts/updates for cursor positioning, not deletes\n if (change.type !== `delete`) {\n if (!this.biggest) {\n this.biggest = change.value\n } else if (comparator(this.biggest, change.value) < 0) {\n this.biggest = change.value\n }\n }\n\n yield change\n }\n }\n}\n\n/**\n * Helper function to send changes to a D2 input stream\n */\nfunction sendChangesToInput(\n input: RootStreamBuilder<unknown>,\n changes: Iterable<ChangeMessage>,\n getKey: (item: ChangeMessage[`value`]) => any,\n): number {\n const multiSetArray: MultiSetArray<unknown> = []\n for (const change of changes) {\n const key = getKey(change.value)\n if (change.type === `insert`) {\n multiSetArray.push([[key, change.value], 1])\n } else if (change.type === `update`) {\n multiSetArray.push([[key, change.previousValue], -1])\n multiSetArray.push([[key, change.value], 1])\n } else {\n // change.type === `delete`\n multiSetArray.push([[key, change.value], -1])\n }\n }\n\n if (multiSetArray.length !== 0) {\n input.sendData(new MultiSet(multiSetArray))\n }\n\n return multiSetArray.length\n}\n\n/** Splits updates into a delete of the old value and an insert of the new value */\nfunction* splitUpdates<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n>(\n changes: Iterable<ChangeMessage<T, TKey>>,\n): Generator<ChangeMessage<T, TKey>> {\n for (const change of changes) {\n if (change.type === `update`) {\n yield { type: `delete`, key: change.key, value: change.previousValue! }\n yield { type: `insert`, key: change.key, value: change.value }\n } else {\n yield change\n }\n }\n}\n"],"names":["subscription"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- const LIVE_QUERY_INTERNAL = Symbol(`liveQueryInternal`);
1
+ const LIVE_QUERY_INTERNAL = /* @__PURE__ */ Symbol(`liveQueryInternal`);
2
2
  export {
3
3
  LIVE_QUERY_INTERNAL
4
4
  };
@@ -1 +1 @@
1
- {"version":3,"file":"internal.js","sources":["../../../../src/query/live/internal.ts"],"sourcesContent":["import type { CollectionConfigBuilder } from './collection-config-builder.js'\n\n/**\n * Symbol for accessing internal utilities that should not be part of the public API\n */\nexport const LIVE_QUERY_INTERNAL = Symbol(`liveQueryInternal`)\n\n/**\n * Internal utilities for live queries, accessible via Symbol\n */\nexport type LiveQueryInternalUtils = {\n getBuilder: () => CollectionConfigBuilder<any, any>\n hasCustomGetKey: boolean\n hasJoins: boolean\n}\n"],"names":[],"mappings":"AAKO,MAAM,sBAAsB,OAAO,mBAAmB;"}
1
+ {"version":3,"file":"internal.js","sources":["../../../../src/query/live/internal.ts"],"sourcesContent":["import type { CollectionConfigBuilder } from './collection-config-builder.js'\n\n/**\n * Symbol for accessing internal utilities that should not be part of the public API\n */\nexport const LIVE_QUERY_INTERNAL = Symbol(`liveQueryInternal`)\n\n/**\n * Internal utilities for live queries, accessible via Symbol\n */\nexport type LiveQueryInternalUtils = {\n getBuilder: () => CollectionConfigBuilder<any, any>\n hasCustomGetKey: boolean\n hasJoins: boolean\n}\n"],"names":[],"mappings":"AAKO,MAAM,6CAA6B,mBAAmB;"}
@@ -15,8 +15,9 @@ export type SyncState = {
15
15
  graph?: D2;
16
16
  inputs?: Record<string, RootStreamBuilder<unknown>>;
17
17
  pipeline?: ResultStream;
18
+ flushPendingChanges?: () => void;
18
19
  };
19
- export type FullSyncState = Required<SyncState>;
20
+ export type FullSyncState = Required<Omit<SyncState, `flushPendingChanges`>> & Pick<SyncState, `flushPendingChanges`>;
20
21
  /**
21
22
  * Configuration interface for live query collection options
22
23
  *
@@ -615,6 +615,12 @@ export interface SubscribeChangesOptions<T extends object = Record<string, unkno
615
615
  where?: (row: SingleRowRefProxy<T>) => any;
616
616
  /** Pre-compiled expression for filtering changes */
617
617
  whereExpression?: BasicExpression<boolean>;
618
+ /**
619
+ * Listener for subscription status changes.
620
+ * Registered BEFORE any snapshot is requested, ensuring no status transitions are missed.
621
+ * @internal
622
+ */
623
+ onStatusChange?: (event: SubscriptionStatusChangeEvent) => void;
618
624
  }
619
625
  export interface SubscribeChangesSnapshotOptions<T extends object = Record<string, unknown>> extends Omit<SubscribeChangesOptions<T>, `includeInitialState`> {
620
626
  orderBy?: OrderBy;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/db",
3
- "version": "0.5.17",
3
+ "version": "0.5.19",
4
4
  "description": "A reactive client store for building super fast apps on sync",
5
5
  "author": "Kyle Mathews",
6
6
  "license": "MIT",
@@ -39,7 +39,7 @@
39
39
  "dependencies": {
40
40
  "@standard-schema/spec": "^1.1.0",
41
41
  "@tanstack/pacer-lite": "^0.2.0",
42
- "@tanstack/db-ivm": "0.1.14"
42
+ "@tanstack/db-ivm": "0.1.15"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "typescript": ">=4.7"
@@ -127,6 +127,13 @@ export class CollectionChangesManager<
127
127
  },
128
128
  })
129
129
 
130
+ // Register status listener BEFORE requesting snapshot to avoid race condition.
131
+ // This ensures the listener catches all status transitions, even if the
132
+ // loadSubset promise resolves synchronously or very quickly.
133
+ if (options.onStatusChange) {
134
+ subscription.on(`status:change`, options.onStatusChange)
135
+ }
136
+
130
137
  if (options.includeInitialState) {
131
138
  subscription.requestSnapshot({ trackLoadSubsetPromise: false })
132
139
  } else if (options.includeInitialState === false) {
@@ -134,10 +134,10 @@ export class CollectionSyncManager<
134
134
  !isTruncateTransaction
135
135
  ) {
136
136
  const existingValue = this.state.syncedData.get(key)
137
- if (
137
+ const valuesEqual =
138
138
  existingValue !== undefined &&
139
139
  deepEquals(existingValue, messageWithOptionalKey.value)
140
- ) {
140
+ if (valuesEqual) {
141
141
  // The "insert" is an echo of a value we already have locally.
142
142
  // Treat it as an update so we preserve optimistic intent without
143
143
  // throwing a duplicate-key error during reconciliation.
@@ -17,7 +17,11 @@ import {
17
17
  QueryMustHaveFromClauseError,
18
18
  SubQueryMustHaveFromClauseError,
19
19
  } from '../../errors.js'
20
- import { createRefProxy, toExpression } from './ref-proxy.js'
20
+ import {
21
+ createRefProxy,
22
+ createRefProxyWithSelected,
23
+ toExpression,
24
+ } from './ref-proxy.js'
21
25
  import type { NamespacedRow, SingleResult } from '../../types.js'
22
26
  import type {
23
27
  Aggregate,
@@ -30,6 +34,7 @@ import type {
30
34
  import type {
31
35
  CompareOptions,
32
36
  Context,
37
+ FunctionalHavingRow,
33
38
  GetResult,
34
39
  GroupByCallback,
35
40
  JoinOnCallback,
@@ -408,7 +413,12 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
408
413
  */
409
414
  having(callback: WhereCallback<TContext>): QueryBuilder<TContext> {
410
415
  const aliases = this._getCurrentAliases()
411
- const refProxy = createRefProxy(aliases) as RefsForContext<TContext>
416
+ // Add $selected namespace if SELECT clause exists
417
+ const refProxy = (
418
+ this.query.select
419
+ ? createRefProxyWithSelected(aliases)
420
+ : createRefProxy(aliases)
421
+ ) as RefsForContext<TContext>
412
422
  const expression = callback(refProxy)
413
423
 
414
424
  // Validate that the callback returned a valid expression
@@ -506,7 +516,12 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
506
516
  options: OrderByDirection | OrderByOptions = `asc`,
507
517
  ): QueryBuilder<TContext> {
508
518
  const aliases = this._getCurrentAliases()
509
- const refProxy = createRefProxy(aliases) as RefsForContext<TContext>
519
+ // Add $selected namespace if SELECT clause exists
520
+ const refProxy = (
521
+ this.query.select
522
+ ? createRefProxyWithSelected(aliases)
523
+ : createRefProxy(aliases)
524
+ ) as RefsForContext<TContext>
510
525
  const result = callback(refProxy)
511
526
 
512
527
  const opts: CompareOptions =
@@ -771,7 +786,7 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
771
786
  * Filter grouped rows using a function that operates on each aggregated row
772
787
  * Warning: This cannot be optimized by the query compiler
773
788
  *
774
- * @param callback - A function that receives an aggregated row and returns a boolean
789
+ * @param callback - A function that receives an aggregated row (with $selected when select() was called) and returns a boolean
775
790
  * @returns A QueryBuilder with functional having filter applied
776
791
  *
777
792
  * @example
@@ -780,11 +795,12 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
780
795
  * query
781
796
  * .from({ posts: postsCollection })
782
797
  * .groupBy(({posts}) => posts.userId)
783
- * .fn.having(row => row.count > 5)
798
+ * .select(({posts}) => ({ userId: posts.userId, count: count(posts.id) }))
799
+ * .fn.having(({ $selected }) => $selected.count > 5)
784
800
  * ```
785
801
  */
786
802
  having(
787
- callback: (row: TContext[`schema`]) => any,
803
+ callback: (row: FunctionalHavingRow<TContext>) => any,
788
804
  ): QueryBuilder<TContext> {
789
805
  return new BaseQueryBuilder({
790
806
  ...builder.query,
@@ -175,6 +175,96 @@ export function createRefProxy<T extends Record<string, any>>(
175
175
  return rootProxy
176
176
  }
177
177
 
178
+ /**
179
+ * Creates a ref proxy with $selected namespace for SELECT fields
180
+ *
181
+ * Adds a $selected property that allows accessing SELECT fields via $selected.fieldName syntax.
182
+ * The $selected proxy creates paths like ['$selected', 'fieldName'] which directly reference
183
+ * the $selected property on the namespaced row.
184
+ *
185
+ * @param aliases - Array of table aliases to create proxies for
186
+ * @returns A ref proxy with table aliases and $selected namespace
187
+ */
188
+ export function createRefProxyWithSelected<T extends Record<string, any>>(
189
+ aliases: Array<string>,
190
+ ): RefProxy<T> & T & { $selected: SingleRowRefProxy<any> } {
191
+ const baseProxy = createRefProxy(aliases)
192
+
193
+ // Create a proxy for $selected that prefixes all paths with '$selected'
194
+ const cache = new Map<string, any>()
195
+
196
+ function createSelectedProxy(path: Array<string>): any {
197
+ const pathKey = path.join(`.`)
198
+ if (cache.has(pathKey)) {
199
+ return cache.get(pathKey)
200
+ }
201
+
202
+ const proxy = new Proxy({} as any, {
203
+ get(target, prop, receiver) {
204
+ if (prop === `__refProxy`) return true
205
+ if (prop === `__path`) return [`$selected`, ...path]
206
+ if (prop === `__type`) return undefined
207
+ if (typeof prop === `symbol`) return Reflect.get(target, prop, receiver)
208
+
209
+ const newPath = [...path, String(prop)]
210
+ return createSelectedProxy(newPath)
211
+ },
212
+
213
+ has(target, prop) {
214
+ if (prop === `__refProxy` || prop === `__path` || prop === `__type`)
215
+ return true
216
+ return Reflect.has(target, prop)
217
+ },
218
+
219
+ ownKeys(target) {
220
+ return Reflect.ownKeys(target)
221
+ },
222
+
223
+ getOwnPropertyDescriptor(target, prop) {
224
+ if (prop === `__refProxy` || prop === `__path` || prop === `__type`) {
225
+ return { enumerable: false, configurable: true }
226
+ }
227
+ return Reflect.getOwnPropertyDescriptor(target, prop)
228
+ },
229
+ })
230
+
231
+ cache.set(pathKey, proxy)
232
+ return proxy
233
+ }
234
+
235
+ const wrappedSelectedProxy = createSelectedProxy([])
236
+
237
+ // Wrap the base proxy to also handle $selected access
238
+ return new Proxy(baseProxy, {
239
+ get(target, prop, receiver) {
240
+ if (prop === `$selected`) {
241
+ return wrappedSelectedProxy
242
+ }
243
+ return Reflect.get(target, prop, receiver)
244
+ },
245
+
246
+ has(target, prop) {
247
+ if (prop === `$selected`) return true
248
+ return Reflect.has(target, prop)
249
+ },
250
+
251
+ ownKeys(target) {
252
+ return [...Reflect.ownKeys(target), `$selected`]
253
+ },
254
+
255
+ getOwnPropertyDescriptor(target, prop) {
256
+ if (prop === `$selected`) {
257
+ return {
258
+ enumerable: true,
259
+ configurable: true,
260
+ value: wrappedSelectedProxy,
261
+ }
262
+ }
263
+ return Reflect.getOwnPropertyDescriptor(target, prop)
264
+ },
265
+ }) as RefProxy<T> & T & { $selected: SingleRowRefProxy<any> }
266
+ }
267
+
178
268
  /**
179
269
  * Converts a value to an Expression
180
270
  * If it's a RefProxy, creates a Ref, otherwise creates a Value
@@ -345,6 +345,26 @@ export type JoinOnCallback<TContext extends Context> = (
345
345
  refs: RefsForContext<TContext>,
346
346
  ) => any
347
347
 
348
+ /**
349
+ * FunctionalHavingRow - Type for the row parameter in functional having callbacks
350
+ *
351
+ * Functional having callbacks receive a namespaced row that includes:
352
+ * - Table data from the schema (when available)
353
+ * - $selected: The SELECT result fields (when select() has been called)
354
+ *
355
+ * After `select()` is called, this type includes `$selected` which provides access
356
+ * to the SELECT result fields via `$selected.fieldName` syntax.
357
+ *
358
+ * Note: When used with GROUP BY, functional having receives `{ $selected: ... }` with the
359
+ * aggregated SELECT results. When used without GROUP BY, it receives the full namespaced row
360
+ * which includes both table data and `$selected`.
361
+ *
362
+ * Example: `({ $selected }) => $selected.sessionCount > 2`
363
+ * Example (no GROUP BY): `(row) => row.user.salary > 70000 && row.$selected.user_count > 2`
364
+ */
365
+ export type FunctionalHavingRow<TContext extends Context> = TContext[`schema`] &
366
+ (TContext[`result`] extends object ? { $selected: TContext[`result`] } : {})
367
+
348
368
  /**
349
369
  * RefProxyForContext - Creates ref proxies for all tables/collections in a query context
350
370
  *
@@ -364,6 +384,9 @@ export type JoinOnCallback<TContext extends Context> = (
364
384
  *
365
385
  * The logic prioritizes optional chaining by always placing `undefined` outside when
366
386
  * a type is both optional and nullable (e.g., `string | null | undefined`).
387
+ *
388
+ * After `select()` is called, this type also includes `$selected` which provides access
389
+ * to the SELECT result fields via `$selected.fieldName` syntax.
367
390
  */
368
391
  export type RefsForContext<TContext extends Context> = {
369
392
  [K in keyof TContext[`schema`]]: IsNonExactOptional<
@@ -383,7 +406,9 @@ export type RefsForContext<TContext extends Context> = {
383
406
  : // T is exactly undefined, exactly null, or neither optional nor nullable
384
407
  // Wrap in RefProxy as-is (includes exact undefined, exact null, and normal types)
385
408
  Ref<TContext[`schema`][K]>
386
- }
409
+ } & (TContext[`result`] extends object
410
+ ? { $selected: Ref<TContext[`result`]> }
411
+ : {})
387
412
 
388
413
  /**
389
414
  * Type Detection Helpers
@@ -95,12 +95,48 @@ function compileExpressionInternal(
95
95
  * Compiles a reference expression into an optimized evaluator
96
96
  */
97
97
  function compileRef(ref: PropRef): CompiledExpression {
98
- const [tableAlias, ...propertyPath] = ref.path
98
+ const [namespace, ...propertyPath] = ref.path
99
99
 
100
- if (!tableAlias) {
100
+ if (!namespace) {
101
101
  throw new EmptyReferencePathError()
102
102
  }
103
103
 
104
+ // Handle $selected namespace - references SELECT result fields
105
+ if (namespace === `$selected`) {
106
+ // Access $selected directly
107
+ if (propertyPath.length === 0) {
108
+ // Just $selected - return entire $selected object
109
+ return (namespacedRow) => (namespacedRow as any).$selected
110
+ } else if (propertyPath.length === 1) {
111
+ // Single property access - most common case
112
+ const prop = propertyPath[0]!
113
+ return (namespacedRow) => {
114
+ const selectResults = (namespacedRow as any).$selected
115
+ return selectResults?.[prop]
116
+ }
117
+ } else {
118
+ // Multiple property navigation (nested SELECT fields)
119
+ return (namespacedRow) => {
120
+ const selectResults = (namespacedRow as any).$selected
121
+ if (selectResults === undefined) {
122
+ return undefined
123
+ }
124
+
125
+ let value: any = selectResults
126
+ for (const prop of propertyPath) {
127
+ if (value == null) {
128
+ return value
129
+ }
130
+ value = value[prop]
131
+ }
132
+ return value
133
+ }
134
+ }
135
+ }
136
+
137
+ // Handle table alias namespace (existing logic)
138
+ const tableAlias = namespace
139
+
104
140
  // Pre-compile the property path navigation
105
141
  if (propertyPath.length === 0) {
106
142
  // Simple table reference