@tanstack/db 0.5.21 → 0.5.23
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.
- package/dist/cjs/collection/lifecycle.cjs +1 -1
- package/dist/cjs/collection/lifecycle.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.cjs +13 -3
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/joins.cjs +15 -5
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/compiler/joins.d.cts +2 -2
- package/dist/esm/collection/lifecycle.js +1 -1
- package/dist/esm/collection/lifecycle.js.map +1 -1
- package/dist/esm/query/compiler/index.js +13 -3
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/joins.d.ts +2 -2
- package/dist/esm/query/compiler/joins.js +15 -5
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/package.json +1 -1
- package/src/collection/lifecycle.ts +4 -2
- package/src/query/compiler/index.ts +25 -0
- package/src/query/compiler/joins.ts +25 -0
|
@@ -110,7 +110,7 @@ class CollectionLifecycleManager {
|
|
|
110
110
|
clearTimeout(this.gcTimeoutId);
|
|
111
111
|
}
|
|
112
112
|
const gcTime = this.config.gcTime ?? 3e5;
|
|
113
|
-
if (gcTime
|
|
113
|
+
if (gcTime <= 0 || !Number.isFinite(gcTime)) {
|
|
114
114
|
return;
|
|
115
115
|
}
|
|
116
116
|
this.gcTimeoutId = setTimeout(() => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.cjs","sources":["../../../src/collection/lifecycle.ts"],"sourcesContent":["import {\n CollectionInErrorStateError,\n CollectionStateError,\n InvalidCollectionStatusTransitionError,\n} from '../errors'\nimport {\n safeCancelIdleCallback,\n safeRequestIdleCallback,\n} from '../utils/browser-polyfills'\nimport type { IdleCallbackDeadline } from '../utils/browser-polyfills'\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type { CollectionConfig, CollectionStatus } from '../types'\nimport type { CollectionEventsManager } from './events'\nimport type { CollectionIndexesManager } from './indexes'\nimport type { CollectionChangesManager } from './changes'\nimport type { CollectionSyncManager } from './sync'\nimport type { CollectionStateManager } from './state'\n\nexport class CollectionLifecycleManager<\n TOutput extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TSchema extends StandardSchemaV1 = StandardSchemaV1,\n TInput extends object = TOutput,\n> {\n private config: CollectionConfig<TOutput, TKey, TSchema>\n private id: string\n private indexes!: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>\n private events!: CollectionEventsManager\n private changes!: CollectionChangesManager<TOutput, TKey, TSchema, TInput>\n private sync!: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n private state!: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n\n public status: CollectionStatus = `idle`\n public hasBeenReady = false\n public hasReceivedFirstCommit = false\n public onFirstReadyCallbacks: Array<() => void> = []\n public gcTimeoutId: ReturnType<typeof setTimeout> | null = null\n private idleCallbackId: number | null = null\n\n /**\n * Creates a new CollectionLifecycleManager instance\n */\n constructor(config: CollectionConfig<TOutput, TKey, TSchema>, id: string) {\n this.config = config\n this.id = id\n }\n\n setDeps(deps: {\n indexes: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>\n events: CollectionEventsManager\n changes: CollectionChangesManager<TOutput, TKey, TSchema, TInput>\n sync: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n state: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n }) {\n this.indexes = deps.indexes\n this.events = deps.events\n this.changes = deps.changes\n this.sync = deps.sync\n this.state = deps.state\n }\n\n /**\n * Validates state transitions to prevent invalid status changes\n */\n public validateStatusTransition(\n from: CollectionStatus,\n to: CollectionStatus,\n ): void {\n if (from === to) {\n // Allow same state transitions\n return\n }\n const validTransitions: Record<\n CollectionStatus,\n Array<CollectionStatus>\n > = {\n idle: [`loading`, `error`, `cleaned-up`],\n loading: [`ready`, `error`, `cleaned-up`],\n ready: [`cleaned-up`, `error`],\n error: [`cleaned-up`, `idle`],\n 'cleaned-up': [`loading`, `error`],\n }\n\n if (!validTransitions[from].includes(to)) {\n throw new InvalidCollectionStatusTransitionError(from, to, this.id)\n }\n }\n\n /**\n * Safely update the collection status with validation\n * @private\n */\n public setStatus(\n newStatus: CollectionStatus,\n allowReady: boolean = false,\n ): void {\n if (newStatus === `ready` && !allowReady) {\n // setStatus('ready') is an internal method that should not be called directly\n // Instead, use markReady to transition to ready triggering the necessary events\n // and side effects.\n throw new CollectionStateError(\n `You can't directly call \"setStatus('ready'). You must use markReady instead.`,\n )\n }\n this.validateStatusTransition(this.status, newStatus)\n const previousStatus = this.status\n this.status = newStatus\n\n // Resolve indexes when collection becomes ready\n if (newStatus === `ready` && !this.indexes.isIndexesResolved) {\n // Resolve indexes asynchronously without blocking\n this.indexes.resolveAllIndexes().catch((error) => {\n console.warn(\n `${this.config.id ? `[${this.config.id}] ` : ``}Failed to resolve indexes:`,\n error,\n )\n })\n }\n\n // Emit event\n this.events.emitStatusChange(newStatus, previousStatus)\n }\n\n /**\n * Validates that the collection is in a usable state for data operations\n * @private\n */\n public validateCollectionUsable(operation: string): void {\n switch (this.status) {\n case `error`:\n throw new CollectionInErrorStateError(operation, this.id)\n case `cleaned-up`:\n // Automatically restart the collection when operations are called on cleaned-up collections\n this.sync.startSync()\n break\n }\n }\n\n /**\n * Mark the collection as ready for use\n * This is called by sync implementations to explicitly signal that the collection is ready,\n * providing a more intuitive alternative to using commits for readiness signaling\n * @private - Should only be called by sync implementations\n */\n public markReady(): void {\n this.validateStatusTransition(this.status, `ready`)\n // Can transition to ready from loading state\n if (this.status === `loading`) {\n this.setStatus(`ready`, true)\n\n // Call any registered first ready callbacks (only on first time becoming ready)\n if (!this.hasBeenReady) {\n this.hasBeenReady = true\n\n // Also mark as having received first commit for backwards compatibility\n if (!this.hasReceivedFirstCommit) {\n this.hasReceivedFirstCommit = true\n }\n\n const callbacks = [...this.onFirstReadyCallbacks]\n this.onFirstReadyCallbacks = []\n callbacks.forEach((callback) => callback())\n }\n // Notify dependents when markReady is called, after status is set\n // This ensures live queries get notified when their dependencies become ready\n if (this.changes.changeSubscriptions.size > 0) {\n this.changes.emitEmptyReadyEvent()\n }\n }\n }\n\n /**\n * Start the garbage collection timer\n * Called when the collection becomes inactive (no subscribers)\n */\n public startGCTimer(): void {\n if (this.gcTimeoutId) {\n clearTimeout(this.gcTimeoutId)\n }\n\n const gcTime = this.config.gcTime ?? 300000 // 5 minutes default\n\n // If gcTime is 0, GC is disabled\n if (gcTime === 0) {\n return\n }\n\n this.gcTimeoutId = setTimeout(() => {\n if (this.changes.activeSubscribersCount === 0) {\n // Schedule cleanup during idle time to avoid blocking the UI thread\n this.scheduleIdleCleanup()\n }\n }, gcTime)\n }\n\n /**\n * Cancel the garbage collection timer\n * Called when the collection becomes active again\n */\n public cancelGCTimer(): void {\n if (this.gcTimeoutId) {\n clearTimeout(this.gcTimeoutId)\n this.gcTimeoutId = null\n }\n // Also cancel any pending idle cleanup\n if (this.idleCallbackId !== null) {\n safeCancelIdleCallback(this.idleCallbackId)\n this.idleCallbackId = null\n }\n }\n\n /**\n * Schedule cleanup to run during browser idle time\n * This prevents blocking the UI thread during cleanup operations\n */\n private scheduleIdleCleanup(): void {\n // Cancel any existing idle callback\n if (this.idleCallbackId !== null) {\n safeCancelIdleCallback(this.idleCallbackId)\n }\n\n // Schedule cleanup with a timeout of 1 second\n // This ensures cleanup happens even if the browser is busy\n this.idleCallbackId = safeRequestIdleCallback(\n (deadline) => {\n // Perform cleanup if we still have no subscribers\n if (this.changes.activeSubscribersCount === 0) {\n const cleanupCompleted = this.performCleanup(deadline)\n // Only clear the callback ID if cleanup actually completed\n if (cleanupCompleted) {\n this.idleCallbackId = null\n }\n } else {\n // No need to cleanup, clear the callback ID\n this.idleCallbackId = null\n }\n },\n { timeout: 1000 },\n )\n }\n\n /**\n * Perform cleanup operations, optionally in chunks during idle time\n * @returns true if cleanup was completed, false if it was rescheduled\n */\n private performCleanup(deadline?: IdleCallbackDeadline): boolean {\n // If we have a deadline, we can potentially split cleanup into chunks\n // For now, we'll do all cleanup at once but check if we have time\n const hasTime =\n !deadline || deadline.timeRemaining() > 0 || deadline.didTimeout\n\n if (hasTime) {\n // Perform all cleanup operations except events\n this.sync.cleanup()\n this.state.cleanup()\n this.changes.cleanup()\n this.indexes.cleanup()\n\n if (this.gcTimeoutId) {\n clearTimeout(this.gcTimeoutId)\n this.gcTimeoutId = null\n }\n\n this.hasBeenReady = false\n this.onFirstReadyCallbacks = []\n\n // Set status to cleaned-up after everything is cleaned up\n // This fires the status:change event to notify listeners\n this.setStatus(`cleaned-up`)\n\n // Finally, cleanup event handlers after the event has been fired\n this.events.cleanup()\n\n return true\n } else {\n // If we don't have time, reschedule for the next idle period\n this.scheduleIdleCleanup()\n return false\n }\n }\n\n /**\n * Register a callback to be executed when the collection first becomes ready\n * Useful for preloading collections\n * @param callback Function to call when the collection first becomes ready\n */\n public onFirstReady(callback: () => void): void {\n // If already ready, call immediately\n if (this.hasBeenReady) {\n callback()\n return\n }\n\n this.onFirstReadyCallbacks.push(callback)\n }\n\n public cleanup(): void {\n // Cancel any pending idle cleanup\n if (this.idleCallbackId !== null) {\n safeCancelIdleCallback(this.idleCallbackId)\n this.idleCallbackId = null\n }\n\n // Perform cleanup immediately (used when explicitly called)\n this.performCleanup()\n }\n}\n"],"names":["InvalidCollectionStatusTransitionError","CollectionStateError","CollectionInErrorStateError","safeCancelIdleCallback","safeRequestIdleCallback"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecycle.cjs","sources":["../../../src/collection/lifecycle.ts"],"sourcesContent":["import {\n CollectionInErrorStateError,\n CollectionStateError,\n InvalidCollectionStatusTransitionError,\n} from '../errors'\nimport {\n safeCancelIdleCallback,\n safeRequestIdleCallback,\n} from '../utils/browser-polyfills'\nimport type { IdleCallbackDeadline } from '../utils/browser-polyfills'\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type { CollectionConfig, CollectionStatus } from '../types'\nimport type { CollectionEventsManager } from './events'\nimport type { CollectionIndexesManager } from './indexes'\nimport type { CollectionChangesManager } from './changes'\nimport type { CollectionSyncManager } from './sync'\nimport type { CollectionStateManager } from './state'\n\nexport class CollectionLifecycleManager<\n TOutput extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TSchema extends StandardSchemaV1 = StandardSchemaV1,\n TInput extends object = TOutput,\n> {\n private config: CollectionConfig<TOutput, TKey, TSchema>\n private id: string\n private indexes!: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>\n private events!: CollectionEventsManager\n private changes!: CollectionChangesManager<TOutput, TKey, TSchema, TInput>\n private sync!: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n private state!: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n\n public status: CollectionStatus = `idle`\n public hasBeenReady = false\n public hasReceivedFirstCommit = false\n public onFirstReadyCallbacks: Array<() => void> = []\n public gcTimeoutId: ReturnType<typeof setTimeout> | null = null\n private idleCallbackId: number | null = null\n\n /**\n * Creates a new CollectionLifecycleManager instance\n */\n constructor(config: CollectionConfig<TOutput, TKey, TSchema>, id: string) {\n this.config = config\n this.id = id\n }\n\n setDeps(deps: {\n indexes: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>\n events: CollectionEventsManager\n changes: CollectionChangesManager<TOutput, TKey, TSchema, TInput>\n sync: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n state: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n }) {\n this.indexes = deps.indexes\n this.events = deps.events\n this.changes = deps.changes\n this.sync = deps.sync\n this.state = deps.state\n }\n\n /**\n * Validates state transitions to prevent invalid status changes\n */\n public validateStatusTransition(\n from: CollectionStatus,\n to: CollectionStatus,\n ): void {\n if (from === to) {\n // Allow same state transitions\n return\n }\n const validTransitions: Record<\n CollectionStatus,\n Array<CollectionStatus>\n > = {\n idle: [`loading`, `error`, `cleaned-up`],\n loading: [`ready`, `error`, `cleaned-up`],\n ready: [`cleaned-up`, `error`],\n error: [`cleaned-up`, `idle`],\n 'cleaned-up': [`loading`, `error`],\n }\n\n if (!validTransitions[from].includes(to)) {\n throw new InvalidCollectionStatusTransitionError(from, to, this.id)\n }\n }\n\n /**\n * Safely update the collection status with validation\n * @private\n */\n public setStatus(\n newStatus: CollectionStatus,\n allowReady: boolean = false,\n ): void {\n if (newStatus === `ready` && !allowReady) {\n // setStatus('ready') is an internal method that should not be called directly\n // Instead, use markReady to transition to ready triggering the necessary events\n // and side effects.\n throw new CollectionStateError(\n `You can't directly call \"setStatus('ready'). You must use markReady instead.`,\n )\n }\n this.validateStatusTransition(this.status, newStatus)\n const previousStatus = this.status\n this.status = newStatus\n\n // Resolve indexes when collection becomes ready\n if (newStatus === `ready` && !this.indexes.isIndexesResolved) {\n // Resolve indexes asynchronously without blocking\n this.indexes.resolveAllIndexes().catch((error) => {\n console.warn(\n `${this.config.id ? `[${this.config.id}] ` : ``}Failed to resolve indexes:`,\n error,\n )\n })\n }\n\n // Emit event\n this.events.emitStatusChange(newStatus, previousStatus)\n }\n\n /**\n * Validates that the collection is in a usable state for data operations\n * @private\n */\n public validateCollectionUsable(operation: string): void {\n switch (this.status) {\n case `error`:\n throw new CollectionInErrorStateError(operation, this.id)\n case `cleaned-up`:\n // Automatically restart the collection when operations are called on cleaned-up collections\n this.sync.startSync()\n break\n }\n }\n\n /**\n * Mark the collection as ready for use\n * This is called by sync implementations to explicitly signal that the collection is ready,\n * providing a more intuitive alternative to using commits for readiness signaling\n * @private - Should only be called by sync implementations\n */\n public markReady(): void {\n this.validateStatusTransition(this.status, `ready`)\n // Can transition to ready from loading state\n if (this.status === `loading`) {\n this.setStatus(`ready`, true)\n\n // Call any registered first ready callbacks (only on first time becoming ready)\n if (!this.hasBeenReady) {\n this.hasBeenReady = true\n\n // Also mark as having received first commit for backwards compatibility\n if (!this.hasReceivedFirstCommit) {\n this.hasReceivedFirstCommit = true\n }\n\n const callbacks = [...this.onFirstReadyCallbacks]\n this.onFirstReadyCallbacks = []\n callbacks.forEach((callback) => callback())\n }\n // Notify dependents when markReady is called, after status is set\n // This ensures live queries get notified when their dependencies become ready\n if (this.changes.changeSubscriptions.size > 0) {\n this.changes.emitEmptyReadyEvent()\n }\n }\n }\n\n /**\n * Start the garbage collection timer\n * Called when the collection becomes inactive (no subscribers)\n */\n public startGCTimer(): void {\n if (this.gcTimeoutId) {\n clearTimeout(this.gcTimeoutId)\n }\n\n const gcTime = this.config.gcTime ?? 300000 // 5 minutes default\n\n // If gcTime is 0, negative, or non-finite (Infinity, -Infinity, NaN), GC is disabled.\n // Note: setTimeout with Infinity coerces to 0 via ToInt32, causing immediate GC,\n // so we must explicitly check for non-finite values here.\n if (gcTime <= 0 || !Number.isFinite(gcTime)) {\n return\n }\n\n this.gcTimeoutId = setTimeout(() => {\n if (this.changes.activeSubscribersCount === 0) {\n // Schedule cleanup during idle time to avoid blocking the UI thread\n this.scheduleIdleCleanup()\n }\n }, gcTime)\n }\n\n /**\n * Cancel the garbage collection timer\n * Called when the collection becomes active again\n */\n public cancelGCTimer(): void {\n if (this.gcTimeoutId) {\n clearTimeout(this.gcTimeoutId)\n this.gcTimeoutId = null\n }\n // Also cancel any pending idle cleanup\n if (this.idleCallbackId !== null) {\n safeCancelIdleCallback(this.idleCallbackId)\n this.idleCallbackId = null\n }\n }\n\n /**\n * Schedule cleanup to run during browser idle time\n * This prevents blocking the UI thread during cleanup operations\n */\n private scheduleIdleCleanup(): void {\n // Cancel any existing idle callback\n if (this.idleCallbackId !== null) {\n safeCancelIdleCallback(this.idleCallbackId)\n }\n\n // Schedule cleanup with a timeout of 1 second\n // This ensures cleanup happens even if the browser is busy\n this.idleCallbackId = safeRequestIdleCallback(\n (deadline) => {\n // Perform cleanup if we still have no subscribers\n if (this.changes.activeSubscribersCount === 0) {\n const cleanupCompleted = this.performCleanup(deadline)\n // Only clear the callback ID if cleanup actually completed\n if (cleanupCompleted) {\n this.idleCallbackId = null\n }\n } else {\n // No need to cleanup, clear the callback ID\n this.idleCallbackId = null\n }\n },\n { timeout: 1000 },\n )\n }\n\n /**\n * Perform cleanup operations, optionally in chunks during idle time\n * @returns true if cleanup was completed, false if it was rescheduled\n */\n private performCleanup(deadline?: IdleCallbackDeadline): boolean {\n // If we have a deadline, we can potentially split cleanup into chunks\n // For now, we'll do all cleanup at once but check if we have time\n const hasTime =\n !deadline || deadline.timeRemaining() > 0 || deadline.didTimeout\n\n if (hasTime) {\n // Perform all cleanup operations except events\n this.sync.cleanup()\n this.state.cleanup()\n this.changes.cleanup()\n this.indexes.cleanup()\n\n if (this.gcTimeoutId) {\n clearTimeout(this.gcTimeoutId)\n this.gcTimeoutId = null\n }\n\n this.hasBeenReady = false\n this.onFirstReadyCallbacks = []\n\n // Set status to cleaned-up after everything is cleaned up\n // This fires the status:change event to notify listeners\n this.setStatus(`cleaned-up`)\n\n // Finally, cleanup event handlers after the event has been fired\n this.events.cleanup()\n\n return true\n } else {\n // If we don't have time, reschedule for the next idle period\n this.scheduleIdleCleanup()\n return false\n }\n }\n\n /**\n * Register a callback to be executed when the collection first becomes ready\n * Useful for preloading collections\n * @param callback Function to call when the collection first becomes ready\n */\n public onFirstReady(callback: () => void): void {\n // If already ready, call immediately\n if (this.hasBeenReady) {\n callback()\n return\n }\n\n this.onFirstReadyCallbacks.push(callback)\n }\n\n public cleanup(): void {\n // Cancel any pending idle cleanup\n if (this.idleCallbackId !== null) {\n safeCancelIdleCallback(this.idleCallbackId)\n this.idleCallbackId = null\n }\n\n // Perform cleanup immediately (used when explicitly called)\n this.performCleanup()\n }\n}\n"],"names":["InvalidCollectionStatusTransitionError","CollectionStateError","CollectionInErrorStateError","safeCancelIdleCallback","safeRequestIdleCallback"],"mappings":"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"}
|
|
@@ -38,7 +38,8 @@ function compileQuery(rawQuery, inputs, collections, subscriptions, callbacks, l
|
|
|
38
38
|
cache,
|
|
39
39
|
queryMapping,
|
|
40
40
|
aliasToCollectionId,
|
|
41
|
-
aliasRemapping
|
|
41
|
+
aliasRemapping,
|
|
42
|
+
sourceWhereClauses
|
|
42
43
|
);
|
|
43
44
|
sources[mainSource] = mainInput;
|
|
44
45
|
let pipeline = mainInput.pipe(
|
|
@@ -66,7 +67,8 @@ function compileQuery(rawQuery, inputs, collections, subscriptions, callbacks, l
|
|
|
66
67
|
rawQuery,
|
|
67
68
|
compileQuery,
|
|
68
69
|
aliasToCollectionId,
|
|
69
|
-
aliasRemapping
|
|
70
|
+
aliasRemapping,
|
|
71
|
+
sourceWhereClauses
|
|
70
72
|
);
|
|
71
73
|
}
|
|
72
74
|
if (query.where && query.where.length > 0) {
|
|
@@ -251,7 +253,7 @@ function validateQueryStructure(query, parentCollectionAliases = /* @__PURE__ */
|
|
|
251
253
|
}
|
|
252
254
|
}
|
|
253
255
|
}
|
|
254
|
-
function processFrom(from, allInputs, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, cache, queryMapping, aliasToCollectionId, aliasRemapping) {
|
|
256
|
+
function processFrom(from, allInputs, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, cache, queryMapping, aliasToCollectionId, aliasRemapping, sourceWhereClauses) {
|
|
255
257
|
switch (from.type) {
|
|
256
258
|
case `collectionRef`: {
|
|
257
259
|
const input = allInputs[from.alias];
|
|
@@ -281,6 +283,14 @@ function processFrom(from, allInputs, collections, subscriptions, callbacks, laz
|
|
|
281
283
|
);
|
|
282
284
|
Object.assign(aliasToCollectionId, subQueryResult.aliasToCollectionId);
|
|
283
285
|
Object.assign(aliasRemapping, subQueryResult.aliasRemapping);
|
|
286
|
+
const isUserDefinedSubquery = queryMapping.has(from.query);
|
|
287
|
+
const subqueryFromAlias = from.query.from.alias;
|
|
288
|
+
const isOptimizerCreated = !isUserDefinedSubquery && from.alias === subqueryFromAlias;
|
|
289
|
+
if (!isOptimizerCreated) {
|
|
290
|
+
for (const [alias, whereClause] of subQueryResult.sourceWhereClauses) {
|
|
291
|
+
sourceWhereClauses.set(alias, whereClause);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
284
294
|
const innerAlias = Object.keys(subQueryResult.aliasToCollectionId).find(
|
|
285
295
|
(alias) => subQueryResult.aliasToCollectionId[alias] === subQueryResult.collectionId
|
|
286
296
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":["../../../../src/query/compiler/index.ts"],"sourcesContent":["import { distinct, filter, map } from '@tanstack/db-ivm'\nimport { optimizeQuery } from '../optimizer.js'\nimport {\n CollectionInputNotFoundError,\n DistinctRequiresSelectError,\n DuplicateAliasInSubqueryError,\n HavingRequiresGroupByError,\n LimitOffsetRequireOrderByError,\n UnsupportedFromTypeError,\n} from '../../errors.js'\nimport { PropRef, Value as ValClass, getWhereExpression } from '../ir.js'\nimport { compileExpression, toBooleanPredicate } from './evaluators.js'\nimport { processJoins } from './joins.js'\nimport { processGroupBy } from './group-by.js'\nimport { processOrderBy } from './order-by.js'\nimport { processSelect } from './select.js'\nimport type { CollectionSubscription } from '../../collection/subscription.js'\nimport type { OrderByOptimizationInfo } from './order-by.js'\nimport type {\n BasicExpression,\n CollectionRef,\n QueryIR,\n QueryRef,\n} from '../ir.js'\nimport type { LazyCollectionCallbacks } from './joins.js'\nimport type { Collection } from '../../collection/index.js'\nimport type {\n KeyedStream,\n NamespacedAndKeyedStream,\n ResultStream,\n} from '../../types.js'\nimport type { QueryCache, QueryMapping, WindowOptions } from './types.js'\n\nexport type { WindowOptions } from './types.js'\n\n/**\n * Result of query compilation including both the pipeline and source-specific WHERE clauses\n */\nexport interface CompilationResult {\n /** The ID of the main collection */\n collectionId: string\n\n /** The compiled query pipeline (D2 stream) */\n pipeline: ResultStream\n\n /** Map of source aliases to their WHERE clauses for index optimization */\n sourceWhereClauses: Map<string, BasicExpression<boolean>>\n\n /**\n * Maps each source alias to its collection ID. Enables per-alias subscriptions for self-joins.\n * Example: `{ employee: 'employees-col-id', manager: 'employees-col-id' }`\n */\n aliasToCollectionId: Record<string, string>\n\n /**\n * Flattened mapping from outer alias to innermost alias for subqueries.\n * Always provides one-hop lookups, never recursive chains.\n *\n * Example: `{ activeUser: 'user' }` when `.from({ activeUser: subquery })`\n * where the subquery uses `.from({ user: collection })`.\n *\n * For deeply nested subqueries, the mapping goes directly to the innermost alias:\n * `{ author: 'user' }` (not `{ author: 'activeUser' }`), so `aliasRemapping[alias]`\n * always resolves in a single lookup.\n *\n * Used to resolve subscriptions during lazy loading when join aliases differ from\n * the inner aliases where collection subscriptions were created.\n */\n aliasRemapping: Record<string, string>\n}\n\n/**\n * Compiles a query IR into a D2 pipeline\n * @param rawQuery The query IR to compile\n * @param inputs Mapping of source aliases to input streams (e.g., `{ employee: input1, manager: input2 }`)\n * @param collections Mapping of collection IDs to Collection instances\n * @param subscriptions Mapping of source aliases to CollectionSubscription instances\n * @param callbacks Mapping of source aliases to lazy loading callbacks\n * @param lazySources Set of source aliases that should load data lazily\n * @param optimizableOrderByCollections Map of collection IDs to order-by optimization info\n * @param cache Optional cache for compiled subqueries (used internally for recursion)\n * @param queryMapping Optional mapping from optimized queries to original queries\n * @returns A CompilationResult with the pipeline, source WHERE clauses, and alias metadata\n */\nexport function compileQuery(\n rawQuery: QueryIR,\n inputs: Record<string, KeyedStream>,\n collections: Record<string, Collection<any, any, any, any, any>>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazySources: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n cache: QueryCache = new WeakMap(),\n queryMapping: QueryMapping = new WeakMap(),\n): CompilationResult {\n // Check if the original raw query has already been compiled\n const cachedResult = cache.get(rawQuery)\n if (cachedResult) {\n return cachedResult\n }\n\n // Validate the raw query BEFORE optimization to check user's original structure.\n // This must happen before optimization because the optimizer may create internal\n // subqueries (e.g., for predicate pushdown) that reuse aliases, which is fine.\n validateQueryStructure(rawQuery)\n\n // Optimize the query before compilation\n const { optimizedQuery: query, sourceWhereClauses } = optimizeQuery(rawQuery)\n\n // Create mapping from optimized query to original for caching\n queryMapping.set(query, rawQuery)\n mapNestedQueries(query, rawQuery, queryMapping)\n\n // Create a copy of the inputs map to avoid modifying the original\n const allInputs = { ...inputs }\n\n // Track alias to collection id relationships discovered during compilation.\n // This includes all user-declared aliases plus inner aliases from subqueries.\n const aliasToCollectionId: Record<string, string> = {}\n\n // Track alias remapping for subqueries (outer alias → inner alias)\n // e.g., when .join({ activeUser: subquery }) where subquery uses .from({ user: collection })\n // we store: aliasRemapping['activeUser'] = 'user'\n const aliasRemapping: Record<string, string> = {}\n\n // Create a map of source aliases to input streams.\n // Inputs MUST be keyed by alias (e.g., `{ employee: input1, manager: input2 }`),\n // not by collection ID. This enables per-alias subscriptions where different aliases\n // of the same collection (e.g., self-joins) maintain independent filtered streams.\n const sources: Record<string, KeyedStream> = {}\n\n // Process the FROM clause to get the main source\n const {\n alias: mainSource,\n input: mainInput,\n collectionId: mainCollectionId,\n } = processFrom(\n query.from,\n allInputs,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n cache,\n queryMapping,\n aliasToCollectionId,\n aliasRemapping,\n )\n sources[mainSource] = mainInput\n\n // Prepare the initial pipeline with the main source wrapped in its alias\n let pipeline: NamespacedAndKeyedStream = mainInput.pipe(\n map(([key, row]) => {\n // Initialize the record with a nested structure\n const ret = [key, { [mainSource]: row }] as [\n string,\n Record<string, typeof row>,\n ]\n return ret\n }),\n )\n\n // Process JOIN clauses if they exist\n if (query.join && query.join.length > 0) {\n pipeline = processJoins(\n pipeline,\n query.join,\n sources,\n mainCollectionId,\n mainSource,\n allInputs,\n cache,\n queryMapping,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n rawQuery,\n compileQuery,\n aliasToCollectionId,\n aliasRemapping,\n )\n }\n\n // Process the WHERE clause if it exists\n if (query.where && query.where.length > 0) {\n // Apply each WHERE condition as a filter (they are ANDed together)\n for (const where of query.where) {\n const whereExpression = getWhereExpression(where)\n const compiledWhere = compileExpression(whereExpression)\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return toBooleanPredicate(compiledWhere(namespacedRow))\n }),\n )\n }\n }\n\n // Process functional WHERE clauses if they exist\n if (query.fnWhere && query.fnWhere.length > 0) {\n for (const fnWhere of query.fnWhere) {\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return toBooleanPredicate(fnWhere(namespacedRow))\n }),\n )\n }\n }\n\n if (query.distinct && !query.fnSelect && !query.select) {\n throw new DistinctRequiresSelectError()\n }\n\n // Process the SELECT clause early - always create $selected\n // This eliminates duplication and allows for DISTINCT implementation\n if (query.fnSelect) {\n // Handle functional select - apply the function to transform the row\n pipeline = pipeline.pipe(\n map(([key, namespacedRow]) => {\n const selectResults = query.fnSelect!(namespacedRow)\n return [\n key,\n {\n ...namespacedRow,\n $selected: selectResults,\n },\n ] as [string, typeof namespacedRow & { $selected: any }]\n }),\n )\n } else if (query.select) {\n pipeline = processSelect(pipeline, query.select, allInputs)\n } else {\n // If no SELECT clause, create $selected with the main table data\n pipeline = pipeline.pipe(\n map(([key, namespacedRow]) => {\n const selectResults =\n !query.join && !query.groupBy\n ? namespacedRow[mainSource]\n : namespacedRow\n\n return [\n key,\n {\n ...namespacedRow,\n $selected: selectResults,\n },\n ] as [string, typeof namespacedRow & { $selected: any }]\n }),\n )\n }\n\n // Process the GROUP BY clause if it exists\n if (query.groupBy && query.groupBy.length > 0) {\n pipeline = processGroupBy(\n pipeline,\n query.groupBy,\n query.having,\n query.select,\n query.fnHaving,\n )\n } else if (query.select) {\n // Check if SELECT contains aggregates but no GROUP BY (implicit single-group aggregation)\n const hasAggregates = Object.values(query.select).some(\n (expr) => expr.type === `agg`,\n )\n if (hasAggregates) {\n // Handle implicit single-group aggregation\n pipeline = processGroupBy(\n pipeline,\n [], // Empty group by means single group\n query.having,\n query.select,\n query.fnHaving,\n )\n }\n }\n\n // Process the HAVING clause if it exists (only applies after GROUP BY)\n if (query.having && (!query.groupBy || query.groupBy.length === 0)) {\n // Check if we have aggregates in SELECT that would trigger implicit grouping\n const hasAggregates = query.select\n ? Object.values(query.select).some((expr) => expr.type === `agg`)\n : false\n\n if (!hasAggregates) {\n throw new HavingRequiresGroupByError()\n }\n }\n\n // Process functional HAVING clauses outside of GROUP BY (treat as additional WHERE filters)\n if (\n query.fnHaving &&\n query.fnHaving.length > 0 &&\n (!query.groupBy || query.groupBy.length === 0)\n ) {\n // If there's no GROUP BY but there are fnHaving clauses, apply them as filters\n for (const fnHaving of query.fnHaving) {\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return fnHaving(namespacedRow)\n }),\n )\n }\n }\n\n // Process the DISTINCT clause if it exists\n if (query.distinct) {\n pipeline = pipeline.pipe(distinct(([_key, row]) => row.$selected))\n }\n\n // Process orderBy parameter if it exists\n if (query.orderBy && query.orderBy.length > 0) {\n const orderedPipeline = processOrderBy(\n rawQuery,\n pipeline,\n query.orderBy,\n query.select || {},\n collections[mainCollectionId]!,\n optimizableOrderByCollections,\n setWindowFn,\n query.limit,\n query.offset,\n )\n\n // Final step: extract the $selected and include orderBy index\n const resultPipeline = orderedPipeline.pipe(\n map(([key, [row, orderByIndex]]) => {\n // Extract the final results from $selected and include orderBy index\n const raw = (row as any).$selected\n const finalResults = unwrapValue(raw)\n return [key, [finalResults, orderByIndex]] as [unknown, [any, string]]\n }),\n )\n\n const result = resultPipeline\n // Cache the result before returning (use original query as key)\n const compilationResult = {\n collectionId: mainCollectionId,\n pipeline: result,\n sourceWhereClauses,\n aliasToCollectionId,\n aliasRemapping,\n }\n cache.set(rawQuery, compilationResult)\n\n return compilationResult\n } else if (query.limit !== undefined || query.offset !== undefined) {\n // If there's a limit or offset without orderBy, throw an error\n throw new LimitOffsetRequireOrderByError()\n }\n\n // Final step: extract the $selected and return tuple format (no orderBy)\n const resultPipeline: ResultStream = pipeline.pipe(\n map(([key, row]) => {\n // Extract the final results from $selected and return [key, [results, undefined]]\n const raw = (row as any).$selected\n const finalResults = unwrapValue(raw)\n return [key, [finalResults, undefined]] as [\n unknown,\n [any, string | undefined],\n ]\n }),\n )\n\n const result = resultPipeline\n // Cache the result before returning (use original query as key)\n const compilationResult = {\n collectionId: mainCollectionId,\n pipeline: result,\n sourceWhereClauses,\n aliasToCollectionId,\n aliasRemapping,\n }\n cache.set(rawQuery, compilationResult)\n\n return compilationResult\n}\n\n/**\n * Collects aliases used for DIRECT collection references (not subqueries).\n * Used to validate that subqueries don't reuse parent query collection aliases.\n * Only direct CollectionRef aliases matter - QueryRef aliases don't cause conflicts.\n */\nfunction collectDirectCollectionAliases(query: QueryIR): Set<string> {\n const aliases = new Set<string>()\n\n // Collect FROM alias only if it's a direct collection reference\n if (query.from.type === `collectionRef`) {\n aliases.add(query.from.alias)\n }\n\n // Collect JOIN aliases only for direct collection references\n if (query.join) {\n for (const joinClause of query.join) {\n if (joinClause.from.type === `collectionRef`) {\n aliases.add(joinClause.from.alias)\n }\n }\n }\n\n return aliases\n}\n\n/**\n * Validates the structure of a query and its subqueries.\n * Checks that subqueries don't reuse collection aliases from parent queries.\n * This must be called on the RAW query before optimization.\n */\nfunction validateQueryStructure(\n query: QueryIR,\n parentCollectionAliases: Set<string> = new Set(),\n): void {\n // Collect direct collection aliases from this query level\n const currentLevelAliases = collectDirectCollectionAliases(query)\n\n // Check if any current alias conflicts with parent aliases\n for (const alias of currentLevelAliases) {\n if (parentCollectionAliases.has(alias)) {\n throw new DuplicateAliasInSubqueryError(\n alias,\n Array.from(parentCollectionAliases),\n )\n }\n }\n\n // Combine parent and current aliases for checking nested subqueries\n const combinedAliases = new Set([\n ...parentCollectionAliases,\n ...currentLevelAliases,\n ])\n\n // Recursively validate FROM subquery\n if (query.from.type === `queryRef`) {\n validateQueryStructure(query.from.query, combinedAliases)\n }\n\n // Recursively validate JOIN subqueries\n if (query.join) {\n for (const joinClause of query.join) {\n if (joinClause.from.type === `queryRef`) {\n validateQueryStructure(joinClause.from.query, combinedAliases)\n }\n }\n }\n}\n\n/**\n * Processes the FROM clause, handling direct collection references and subqueries.\n * Populates `aliasToCollectionId` and `aliasRemapping` for per-alias subscription tracking.\n */\nfunction processFrom(\n from: CollectionRef | QueryRef,\n allInputs: Record<string, KeyedStream>,\n collections: Record<string, Collection>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazySources: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n cache: QueryCache,\n queryMapping: QueryMapping,\n aliasToCollectionId: Record<string, string>,\n aliasRemapping: Record<string, string>,\n): { alias: string; input: KeyedStream; collectionId: string } {\n switch (from.type) {\n case `collectionRef`: {\n const input = allInputs[from.alias]\n if (!input) {\n throw new CollectionInputNotFoundError(\n from.alias,\n from.collection.id,\n Object.keys(allInputs),\n )\n }\n aliasToCollectionId[from.alias] = from.collection.id\n return { alias: from.alias, input, collectionId: from.collection.id }\n }\n case `queryRef`: {\n // Find the original query for caching purposes\n const originalQuery = queryMapping.get(from.query) || from.query\n\n // Recursively compile the sub-query with cache\n const subQueryResult = compileQuery(\n originalQuery,\n allInputs,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n cache,\n queryMapping,\n )\n\n // Pull up alias mappings from subquery to parent scope.\n // This includes both the innermost alias-to-collection mappings AND\n // any existing remappings from nested subquery levels.\n Object.assign(aliasToCollectionId, subQueryResult.aliasToCollectionId)\n Object.assign(aliasRemapping, subQueryResult.aliasRemapping)\n\n // Create a FLATTENED remapping from outer alias to innermost alias.\n // For nested subqueries, this ensures one-hop lookups (not recursive chains).\n //\n // Example with 3-level nesting:\n // Inner: .from({ user: usersCollection })\n // Middle: .from({ activeUser: innerSubquery }) → creates: activeUser → user\n // Outer: .from({ author: middleSubquery }) → creates: author → user (not author → activeUser)\n //\n // The key insight: We search through the PULLED-UP aliasToCollectionId (which contains\n // the innermost 'user' alias), so we always map directly to the deepest level.\n // This means aliasRemapping[alias] is always a single lookup, never recursive.\n // Needed for subscription resolution during lazy loading.\n const innerAlias = Object.keys(subQueryResult.aliasToCollectionId).find(\n (alias) =>\n subQueryResult.aliasToCollectionId[alias] ===\n subQueryResult.collectionId,\n )\n if (innerAlias && innerAlias !== from.alias) {\n aliasRemapping[from.alias] = innerAlias\n }\n\n // Extract the pipeline from the compilation result\n const subQueryInput = subQueryResult.pipeline\n\n // Subqueries may return [key, [value, orderByIndex]] (with ORDER BY) or [key, value] (without ORDER BY)\n // We need to extract just the value for use in parent queries\n const extractedInput = subQueryInput.pipe(\n map((data: any) => {\n const [key, [value, _orderByIndex]] = data\n // Unwrap Value expressions that might have leaked through as the entire row\n const unwrapped = unwrapValue(value)\n return [key, unwrapped] as [unknown, any]\n }),\n )\n\n return {\n alias: from.alias,\n input: extractedInput,\n collectionId: subQueryResult.collectionId,\n }\n }\n default:\n throw new UnsupportedFromTypeError((from as any).type)\n }\n}\n\n// Helper to check if a value is a Value expression\nfunction isValue(raw: any): boolean {\n return (\n raw instanceof ValClass ||\n (raw && typeof raw === `object` && `type` in raw && raw.type === `val`)\n )\n}\n\n// Helper to unwrap a Value expression or return the value itself\nfunction unwrapValue(value: any): any {\n return isValue(value) ? value.value : value\n}\n\n/**\n * Recursively maps optimized subqueries to their original queries for proper caching.\n * This ensures that when we encounter the same QueryRef object in different contexts,\n * we can find the original query to check the cache.\n */\nfunction mapNestedQueries(\n optimizedQuery: QueryIR,\n originalQuery: QueryIR,\n queryMapping: QueryMapping,\n): void {\n // Map the FROM clause if it's a QueryRef\n if (\n optimizedQuery.from.type === `queryRef` &&\n originalQuery.from.type === `queryRef`\n ) {\n queryMapping.set(optimizedQuery.from.query, originalQuery.from.query)\n // Recursively map nested queries\n mapNestedQueries(\n optimizedQuery.from.query,\n originalQuery.from.query,\n queryMapping,\n )\n }\n\n // Map JOIN clauses if they exist\n if (optimizedQuery.join && originalQuery.join) {\n for (\n let i = 0;\n i < optimizedQuery.join.length && i < originalQuery.join.length;\n i++\n ) {\n const optimizedJoin = optimizedQuery.join[i]!\n const originalJoin = originalQuery.join[i]!\n\n if (\n optimizedJoin.from.type === `queryRef` &&\n originalJoin.from.type === `queryRef`\n ) {\n queryMapping.set(optimizedJoin.from.query, originalJoin.from.query)\n // Recursively map nested queries in joins\n mapNestedQueries(\n optimizedJoin.from.query,\n originalJoin.from.query,\n queryMapping,\n )\n }\n }\n }\n}\n\nfunction getRefFromAlias(\n query: QueryIR,\n alias: string,\n): CollectionRef | QueryRef | void {\n if (query.from.alias === alias) {\n return query.from\n }\n\n for (const join of query.join || []) {\n if (join.from.alias === alias) {\n return join.from\n }\n }\n}\n\n/**\n * Follows the given reference in a query\n * until its finds the root field the reference points to.\n * @returns The collection, its alias, and the path to the root field in this collection\n */\nexport function followRef(\n query: QueryIR,\n ref: PropRef<any>,\n collection: Collection,\n): { collection: Collection; path: Array<string> } | void {\n if (ref.path.length === 0) {\n return\n }\n\n if (ref.path.length === 1) {\n // This field should be part of this collection\n const field = ref.path[0]!\n // is it part of the select clause?\n if (query.select) {\n const selectedField = query.select[field]\n if (selectedField && selectedField.type === `ref`) {\n return followRef(query, selectedField, collection)\n }\n }\n\n // Either this field is not part of the select clause\n // and thus it must be part of the collection itself\n // or it is part of the select but is not a reference\n // so we can stop here and don't have to follow it\n return { collection, path: [field] }\n }\n\n if (ref.path.length > 1) {\n // This is a nested field\n const [alias, ...rest] = ref.path\n const aliasRef = getRefFromAlias(query, alias!)\n if (!aliasRef) {\n return\n }\n\n if (aliasRef.type === `queryRef`) {\n return followRef(aliasRef.query, new PropRef(rest), collection)\n } else {\n // This is a reference to a collection\n // we can't follow it further\n // so the field must be on the collection itself\n return { collection: aliasRef.collection, path: rest }\n }\n }\n}\n\nexport type CompileQueryFn = typeof compileQuery\n"],"names":["optimizeQuery","map","processJoins","getWhereExpression","compileExpression","filter","toBooleanPredicate","DistinctRequiresSelectError","processSelect","processGroupBy","HavingRequiresGroupByError","distinct","processOrderBy","resultPipeline","result","compilationResult","LimitOffsetRequireOrderByError","DuplicateAliasInSubqueryError","CollectionInputNotFoundError","UnsupportedFromTypeError","ValClass"],"mappings":";;;;;;;;;;;AAoFO,SAAS,aACd,UACA,QACA,aACA,eACA,WACA,aACA,+BACA,aACA,4BAAwB,QAAA,GACxB,eAA6B,oBAAI,WACd;AAEnB,QAAM,eAAe,MAAM,IAAI,QAAQ;AACvC,MAAI,cAAc;AAChB,WAAO;AAAA,EACT;AAKA,yBAAuB,QAAQ;AAG/B,QAAM,EAAE,gBAAgB,OAAO,mBAAA,IAAuBA,UAAAA,cAAc,QAAQ;AAG5E,eAAa,IAAI,OAAO,QAAQ;AAChC,mBAAiB,OAAO,UAAU,YAAY;AAG9C,QAAM,YAAY,EAAE,GAAG,OAAA;AAIvB,QAAM,sBAA8C,CAAA;AAKpD,QAAM,iBAAyC,CAAA;AAM/C,QAAM,UAAuC,CAAA;AAG7C,QAAM;AAAA,IACJ,OAAO;AAAA,IACP,OAAO;AAAA,IACP,cAAc;AAAA,EAAA,IACZ;AAAA,IACF,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEF,UAAQ,UAAU,IAAI;AAGtB,MAAI,WAAqC,UAAU;AAAA,IACjDC,MAAAA,IAAI,CAAC,CAAC,KAAK,GAAG,MAAM;AAElB,YAAM,MAAM,CAAC,KAAK,EAAE,CAAC,UAAU,GAAG,KAAK;AAIvC,aAAO;AAAA,IACT,CAAC;AAAA,EAAA;AAIH,MAAI,MAAM,QAAQ,MAAM,KAAK,SAAS,GAAG;AACvC,eAAWC,MAAAA;AAAAA,MACT;AAAA,MACA,MAAM;AAAA,MACN;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAAA,EAEJ;AAGA,MAAI,MAAM,SAAS,MAAM,MAAM,SAAS,GAAG;AAEzC,eAAW,SAAS,MAAM,OAAO;AAC/B,YAAM,kBAAkBC,GAAAA,mBAAmB,KAAK;AAChD,YAAM,gBAAgBC,WAAAA,kBAAkB,eAAe;AACvD,iBAAW,SAAS;AAAA,QAClBC,MAAAA,OAAO,CAAC,CAAC,MAAM,aAAa,MAAM;AAChC,iBAAOC,WAAAA,mBAAmB,cAAc,aAAa,CAAC;AAAA,QACxD,CAAC;AAAA,MAAA;AAAA,IAEL;AAAA,EACF;AAGA,MAAI,MAAM,WAAW,MAAM,QAAQ,SAAS,GAAG;AAC7C,eAAW,WAAW,MAAM,SAAS;AACnC,iBAAW,SAAS;AAAA,QAClBD,MAAAA,OAAO,CAAC,CAAC,MAAM,aAAa,MAAM;AAChC,iBAAOC,WAAAA,mBAAmB,QAAQ,aAAa,CAAC;AAAA,QAClD,CAAC;AAAA,MAAA;AAAA,IAEL;AAAA,EACF;AAEA,MAAI,MAAM,YAAY,CAAC,MAAM,YAAY,CAAC,MAAM,QAAQ;AACtD,UAAM,IAAIC,OAAAA,4BAAA;AAAA,EACZ;AAIA,MAAI,MAAM,UAAU;AAElB,eAAW,SAAS;AAAA,MAClBN,MAAAA,IAAI,CAAC,CAAC,KAAK,aAAa,MAAM;AAC5B,cAAM,gBAAgB,MAAM,SAAU,aAAa;AACnD,eAAO;AAAA,UACL;AAAA,UACA;AAAA,YACE,GAAG;AAAA,YACH,WAAW;AAAA,UAAA;AAAA,QACb;AAAA,MAEJ,CAAC;AAAA,IAAA;AAAA,EAEL,WAAW,MAAM,QAAQ;AACvB,eAAWO,OAAAA,cAAc,UAAU,MAAM,MAAiB;AAAA,EAC5D,OAAO;AAEL,eAAW,SAAS;AAAA,MAClBP,MAAAA,IAAI,CAAC,CAAC,KAAK,aAAa,MAAM;AAC5B,cAAM,gBACJ,CAAC,MAAM,QAAQ,CAAC,MAAM,UAClB,cAAc,UAAU,IACxB;AAEN,eAAO;AAAA,UACL;AAAA,UACA;AAAA,YACE,GAAG;AAAA,YACH,WAAW;AAAA,UAAA;AAAA,QACb;AAAA,MAEJ,CAAC;AAAA,IAAA;AAAA,EAEL;AAGA,MAAI,MAAM,WAAW,MAAM,QAAQ,SAAS,GAAG;AAC7C,eAAWQ,QAAAA;AAAAA,MACT;AAAA,MACA,MAAM;AAAA,MACN,MAAM;AAAA,MACN,MAAM;AAAA,MACN,MAAM;AAAA,IAAA;AAAA,EAEV,WAAW,MAAM,QAAQ;AAEvB,UAAM,gBAAgB,OAAO,OAAO,MAAM,MAAM,EAAE;AAAA,MAChD,CAAC,SAAS,KAAK,SAAS;AAAA,IAAA;AAE1B,QAAI,eAAe;AAEjB,iBAAWA,QAAAA;AAAAA,QACT;AAAA,QACA,CAAA;AAAA;AAAA,QACA,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,MAAA;AAAA,IAEV;AAAA,EACF;AAGA,MAAI,MAAM,WAAW,CAAC,MAAM,WAAW,MAAM,QAAQ,WAAW,IAAI;AAElE,UAAM,gBAAgB,MAAM,SACxB,OAAO,OAAO,MAAM,MAAM,EAAE,KAAK,CAAC,SAAS,KAAK,SAAS,KAAK,IAC9D;AAEJ,QAAI,CAAC,eAAe;AAClB,YAAM,IAAIC,OAAAA,2BAAA;AAAA,IACZ;AAAA,EACF;AAGA,MACE,MAAM,YACN,MAAM,SAAS,SAAS,MACvB,CAAC,MAAM,WAAW,MAAM,QAAQ,WAAW,IAC5C;AAEA,eAAW,YAAY,MAAM,UAAU;AACrC,iBAAW,SAAS;AAAA,QAClBL,MAAAA,OAAO,CAAC,CAAC,MAAM,aAAa,MAAM;AAChC,iBAAO,SAAS,aAAa;AAAA,QAC/B,CAAC;AAAA,MAAA;AAAA,IAEL;AAAA,EACF;AAGA,MAAI,MAAM,UAAU;AAClB,eAAW,SAAS,KAAKM,eAAS,CAAC,CAAC,MAAM,GAAG,MAAM,IAAI,SAAS,CAAC;AAAA,EACnE;AAGA,MAAI,MAAM,WAAW,MAAM,QAAQ,SAAS,GAAG;AAC7C,UAAM,kBAAkBC,QAAAA;AAAAA,MACtB;AAAA,MACA;AAAA,MACA,MAAM;AAAA,MACN,MAAM,UAAU,CAAA;AAAA,MAChB,YAAY,gBAAgB;AAAA,MAC5B;AAAA,MACA;AAAA,MACA,MAAM;AAAA,MACN,MAAM;AAAA,IAAA;AAIR,UAAMC,kBAAiB,gBAAgB;AAAA,MACrCZ,MAAAA,IAAI,CAAC,CAAC,KAAK,CAAC,KAAK,YAAY,CAAC,MAAM;AAElC,cAAM,MAAO,IAAY;AACzB,cAAM,eAAe,YAAY,GAAG;AACpC,eAAO,CAAC,KAAK,CAAC,cAAc,YAAY,CAAC;AAAA,MAC3C,CAAC;AAAA,IAAA;AAGH,UAAMa,UAASD;AAEf,UAAME,qBAAoB;AAAA,MACxB,cAAc;AAAA,MACd,UAAUD;AAAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAEF,UAAM,IAAI,UAAUC,kBAAiB;AAErC,WAAOA;AAAAA,EACT,WAAW,MAAM,UAAU,UAAa,MAAM,WAAW,QAAW;AAElE,UAAM,IAAIC,OAAAA,+BAAA;AAAA,EACZ;AAGA,QAAM,iBAA+B,SAAS;AAAA,IAC5Cf,MAAAA,IAAI,CAAC,CAAC,KAAK,GAAG,MAAM;AAElB,YAAM,MAAO,IAAY;AACzB,YAAM,eAAe,YAAY,GAAG;AACpC,aAAO,CAAC,KAAK,CAAC,cAAc,MAAS,CAAC;AAAA,IAIxC,CAAC;AAAA,EAAA;AAGH,QAAM,SAAS;AAEf,QAAM,oBAAoB;AAAA,IACxB,cAAc;AAAA,IACd,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEF,QAAM,IAAI,UAAU,iBAAiB;AAErC,SAAO;AACT;AAOA,SAAS,+BAA+B,OAA6B;AACnE,QAAM,8BAAc,IAAA;AAGpB,MAAI,MAAM,KAAK,SAAS,iBAAiB;AACvC,YAAQ,IAAI,MAAM,KAAK,KAAK;AAAA,EAC9B;AAGA,MAAI,MAAM,MAAM;AACd,eAAW,cAAc,MAAM,MAAM;AACnC,UAAI,WAAW,KAAK,SAAS,iBAAiB;AAC5C,gBAAQ,IAAI,WAAW,KAAK,KAAK;AAAA,MACnC;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAOA,SAAS,uBACP,OACA,0BAAuC,oBAAI,OACrC;AAEN,QAAM,sBAAsB,+BAA+B,KAAK;AAGhE,aAAW,SAAS,qBAAqB;AACvC,QAAI,wBAAwB,IAAI,KAAK,GAAG;AACtC,YAAM,IAAIgB,OAAAA;AAAAA,QACR;AAAA,QACA,MAAM,KAAK,uBAAuB;AAAA,MAAA;AAAA,IAEtC;AAAA,EACF;AAGA,QAAM,sCAAsB,IAAI;AAAA,IAC9B,GAAG;AAAA,IACH,GAAG;AAAA,EAAA,CACJ;AAGD,MAAI,MAAM,KAAK,SAAS,YAAY;AAClC,2BAAuB,MAAM,KAAK,OAAO,eAAe;AAAA,EAC1D;AAGA,MAAI,MAAM,MAAM;AACd,eAAW,cAAc,MAAM,MAAM;AACnC,UAAI,WAAW,KAAK,SAAS,YAAY;AACvC,+BAAuB,WAAW,KAAK,OAAO,eAAe;AAAA,MAC/D;AAAA,IACF;AAAA,EACF;AACF;AAMA,SAAS,YACP,MACA,WACA,aACA,eACA,WACA,aACA,+BACA,aACA,OACA,cACA,qBACA,gBAC6D;AAC7D,UAAQ,KAAK,MAAA;AAAA,IACX,KAAK,iBAAiB;AACpB,YAAM,QAAQ,UAAU,KAAK,KAAK;AAClC,UAAI,CAAC,OAAO;AACV,cAAM,IAAIC,OAAAA;AAAAA,UACR,KAAK;AAAA,UACL,KAAK,WAAW;AAAA,UAChB,OAAO,KAAK,SAAS;AAAA,QAAA;AAAA,MAEzB;AACA,0BAAoB,KAAK,KAAK,IAAI,KAAK,WAAW;AAClD,aAAO,EAAE,OAAO,KAAK,OAAO,OAAO,cAAc,KAAK,WAAW,GAAA;AAAA,IACnE;AAAA,IACA,KAAK,YAAY;AAEf,YAAM,gBAAgB,aAAa,IAAI,KAAK,KAAK,KAAK,KAAK;AAG3D,YAAM,iBAAiB;AAAA,QACrB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAMF,aAAO,OAAO,qBAAqB,eAAe,mBAAmB;AACrE,aAAO,OAAO,gBAAgB,eAAe,cAAc;AAc3D,YAAM,aAAa,OAAO,KAAK,eAAe,mBAAmB,EAAE;AAAA,QACjE,CAAC,UACC,eAAe,oBAAoB,KAAK,MACxC,eAAe;AAAA,MAAA;AAEnB,UAAI,cAAc,eAAe,KAAK,OAAO;AAC3C,uBAAe,KAAK,KAAK,IAAI;AAAA,MAC/B;AAGA,YAAM,gBAAgB,eAAe;AAIrC,YAAM,iBAAiB,cAAc;AAAA,QACnCjB,MAAAA,IAAI,CAAC,SAAc;AACjB,gBAAM,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,IAAI;AAEtC,gBAAM,YAAY,YAAY,KAAK;AACnC,iBAAO,CAAC,KAAK,SAAS;AAAA,QACxB,CAAC;AAAA,MAAA;AAGH,aAAO;AAAA,QACL,OAAO,KAAK;AAAA,QACZ,OAAO;AAAA,QACP,cAAc,eAAe;AAAA,MAAA;AAAA,IAEjC;AAAA,IACA;AACE,YAAM,IAAIkB,OAAAA,yBAA0B,KAAa,IAAI;AAAA,EAAA;AAE3D;AAGA,SAAS,QAAQ,KAAmB;AAClC,SACE,eAAeC,GAAAA,SACd,OAAO,OAAO,QAAQ,YAAY,UAAU,OAAO,IAAI,SAAS;AAErE;AAGA,SAAS,YAAY,OAAiB;AACpC,SAAO,QAAQ,KAAK,IAAI,MAAM,QAAQ;AACxC;AAOA,SAAS,iBACP,gBACA,eACA,cACM;AAEN,MACE,eAAe,KAAK,SAAS,cAC7B,cAAc,KAAK,SAAS,YAC5B;AACA,iBAAa,IAAI,eAAe,KAAK,OAAO,cAAc,KAAK,KAAK;AAEpE;AAAA,MACE,eAAe,KAAK;AAAA,MACpB,cAAc,KAAK;AAAA,MACnB;AAAA,IAAA;AAAA,EAEJ;AAGA,MAAI,eAAe,QAAQ,cAAc,MAAM;AAC7C,aACM,IAAI,GACR,IAAI,eAAe,KAAK,UAAU,IAAI,cAAc,KAAK,QACzD,KACA;AACA,YAAM,gBAAgB,eAAe,KAAK,CAAC;AAC3C,YAAM,eAAe,cAAc,KAAK,CAAC;AAEzC,UACE,cAAc,KAAK,SAAS,cAC5B,aAAa,KAAK,SAAS,YAC3B;AACA,qBAAa,IAAI,cAAc,KAAK,OAAO,aAAa,KAAK,KAAK;AAElE;AAAA,UACE,cAAc,KAAK;AAAA,UACnB,aAAa,KAAK;AAAA,UAClB;AAAA,QAAA;AAAA,MAEJ;AAAA,IACF;AAAA,EACF;AACF;;"}
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../../../../src/query/compiler/index.ts"],"sourcesContent":["import { distinct, filter, map } from '@tanstack/db-ivm'\nimport { optimizeQuery } from '../optimizer.js'\nimport {\n CollectionInputNotFoundError,\n DistinctRequiresSelectError,\n DuplicateAliasInSubqueryError,\n HavingRequiresGroupByError,\n LimitOffsetRequireOrderByError,\n UnsupportedFromTypeError,\n} from '../../errors.js'\nimport { PropRef, Value as ValClass, getWhereExpression } from '../ir.js'\nimport { compileExpression, toBooleanPredicate } from './evaluators.js'\nimport { processJoins } from './joins.js'\nimport { processGroupBy } from './group-by.js'\nimport { processOrderBy } from './order-by.js'\nimport { processSelect } from './select.js'\nimport type { CollectionSubscription } from '../../collection/subscription.js'\nimport type { OrderByOptimizationInfo } from './order-by.js'\nimport type {\n BasicExpression,\n CollectionRef,\n QueryIR,\n QueryRef,\n} from '../ir.js'\nimport type { LazyCollectionCallbacks } from './joins.js'\nimport type { Collection } from '../../collection/index.js'\nimport type {\n KeyedStream,\n NamespacedAndKeyedStream,\n ResultStream,\n} from '../../types.js'\nimport type { QueryCache, QueryMapping, WindowOptions } from './types.js'\n\nexport type { WindowOptions } from './types.js'\n\n/**\n * Result of query compilation including both the pipeline and source-specific WHERE clauses\n */\nexport interface CompilationResult {\n /** The ID of the main collection */\n collectionId: string\n\n /** The compiled query pipeline (D2 stream) */\n pipeline: ResultStream\n\n /** Map of source aliases to their WHERE clauses for index optimization */\n sourceWhereClauses: Map<string, BasicExpression<boolean>>\n\n /**\n * Maps each source alias to its collection ID. Enables per-alias subscriptions for self-joins.\n * Example: `{ employee: 'employees-col-id', manager: 'employees-col-id' }`\n */\n aliasToCollectionId: Record<string, string>\n\n /**\n * Flattened mapping from outer alias to innermost alias for subqueries.\n * Always provides one-hop lookups, never recursive chains.\n *\n * Example: `{ activeUser: 'user' }` when `.from({ activeUser: subquery })`\n * where the subquery uses `.from({ user: collection })`.\n *\n * For deeply nested subqueries, the mapping goes directly to the innermost alias:\n * `{ author: 'user' }` (not `{ author: 'activeUser' }`), so `aliasRemapping[alias]`\n * always resolves in a single lookup.\n *\n * Used to resolve subscriptions during lazy loading when join aliases differ from\n * the inner aliases where collection subscriptions were created.\n */\n aliasRemapping: Record<string, string>\n}\n\n/**\n * Compiles a query IR into a D2 pipeline\n * @param rawQuery The query IR to compile\n * @param inputs Mapping of source aliases to input streams (e.g., `{ employee: input1, manager: input2 }`)\n * @param collections Mapping of collection IDs to Collection instances\n * @param subscriptions Mapping of source aliases to CollectionSubscription instances\n * @param callbacks Mapping of source aliases to lazy loading callbacks\n * @param lazySources Set of source aliases that should load data lazily\n * @param optimizableOrderByCollections Map of collection IDs to order-by optimization info\n * @param cache Optional cache for compiled subqueries (used internally for recursion)\n * @param queryMapping Optional mapping from optimized queries to original queries\n * @returns A CompilationResult with the pipeline, source WHERE clauses, and alias metadata\n */\nexport function compileQuery(\n rawQuery: QueryIR,\n inputs: Record<string, KeyedStream>,\n collections: Record<string, Collection<any, any, any, any, any>>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazySources: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n cache: QueryCache = new WeakMap(),\n queryMapping: QueryMapping = new WeakMap(),\n): CompilationResult {\n // Check if the original raw query has already been compiled\n const cachedResult = cache.get(rawQuery)\n if (cachedResult) {\n return cachedResult\n }\n\n // Validate the raw query BEFORE optimization to check user's original structure.\n // This must happen before optimization because the optimizer may create internal\n // subqueries (e.g., for predicate pushdown) that reuse aliases, which is fine.\n validateQueryStructure(rawQuery)\n\n // Optimize the query before compilation\n const { optimizedQuery: query, sourceWhereClauses } = optimizeQuery(rawQuery)\n\n // Create mapping from optimized query to original for caching\n queryMapping.set(query, rawQuery)\n mapNestedQueries(query, rawQuery, queryMapping)\n\n // Create a copy of the inputs map to avoid modifying the original\n const allInputs = { ...inputs }\n\n // Track alias to collection id relationships discovered during compilation.\n // This includes all user-declared aliases plus inner aliases from subqueries.\n const aliasToCollectionId: Record<string, string> = {}\n\n // Track alias remapping for subqueries (outer alias → inner alias)\n // e.g., when .join({ activeUser: subquery }) where subquery uses .from({ user: collection })\n // we store: aliasRemapping['activeUser'] = 'user'\n const aliasRemapping: Record<string, string> = {}\n\n // Create a map of source aliases to input streams.\n // Inputs MUST be keyed by alias (e.g., `{ employee: input1, manager: input2 }`),\n // not by collection ID. This enables per-alias subscriptions where different aliases\n // of the same collection (e.g., self-joins) maintain independent filtered streams.\n const sources: Record<string, KeyedStream> = {}\n\n // Process the FROM clause to get the main source\n const {\n alias: mainSource,\n input: mainInput,\n collectionId: mainCollectionId,\n } = processFrom(\n query.from,\n allInputs,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n cache,\n queryMapping,\n aliasToCollectionId,\n aliasRemapping,\n sourceWhereClauses,\n )\n sources[mainSource] = mainInput\n\n // Prepare the initial pipeline with the main source wrapped in its alias\n let pipeline: NamespacedAndKeyedStream = mainInput.pipe(\n map(([key, row]) => {\n // Initialize the record with a nested structure\n const ret = [key, { [mainSource]: row }] as [\n string,\n Record<string, typeof row>,\n ]\n return ret\n }),\n )\n\n // Process JOIN clauses if they exist\n if (query.join && query.join.length > 0) {\n pipeline = processJoins(\n pipeline,\n query.join,\n sources,\n mainCollectionId,\n mainSource,\n allInputs,\n cache,\n queryMapping,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n rawQuery,\n compileQuery,\n aliasToCollectionId,\n aliasRemapping,\n sourceWhereClauses,\n )\n }\n\n // Process the WHERE clause if it exists\n if (query.where && query.where.length > 0) {\n // Apply each WHERE condition as a filter (they are ANDed together)\n for (const where of query.where) {\n const whereExpression = getWhereExpression(where)\n const compiledWhere = compileExpression(whereExpression)\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return toBooleanPredicate(compiledWhere(namespacedRow))\n }),\n )\n }\n }\n\n // Process functional WHERE clauses if they exist\n if (query.fnWhere && query.fnWhere.length > 0) {\n for (const fnWhere of query.fnWhere) {\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return toBooleanPredicate(fnWhere(namespacedRow))\n }),\n )\n }\n }\n\n if (query.distinct && !query.fnSelect && !query.select) {\n throw new DistinctRequiresSelectError()\n }\n\n // Process the SELECT clause early - always create $selected\n // This eliminates duplication and allows for DISTINCT implementation\n if (query.fnSelect) {\n // Handle functional select - apply the function to transform the row\n pipeline = pipeline.pipe(\n map(([key, namespacedRow]) => {\n const selectResults = query.fnSelect!(namespacedRow)\n return [\n key,\n {\n ...namespacedRow,\n $selected: selectResults,\n },\n ] as [string, typeof namespacedRow & { $selected: any }]\n }),\n )\n } else if (query.select) {\n pipeline = processSelect(pipeline, query.select, allInputs)\n } else {\n // If no SELECT clause, create $selected with the main table data\n pipeline = pipeline.pipe(\n map(([key, namespacedRow]) => {\n const selectResults =\n !query.join && !query.groupBy\n ? namespacedRow[mainSource]\n : namespacedRow\n\n return [\n key,\n {\n ...namespacedRow,\n $selected: selectResults,\n },\n ] as [string, typeof namespacedRow & { $selected: any }]\n }),\n )\n }\n\n // Process the GROUP BY clause if it exists\n if (query.groupBy && query.groupBy.length > 0) {\n pipeline = processGroupBy(\n pipeline,\n query.groupBy,\n query.having,\n query.select,\n query.fnHaving,\n )\n } else if (query.select) {\n // Check if SELECT contains aggregates but no GROUP BY (implicit single-group aggregation)\n const hasAggregates = Object.values(query.select).some(\n (expr) => expr.type === `agg`,\n )\n if (hasAggregates) {\n // Handle implicit single-group aggregation\n pipeline = processGroupBy(\n pipeline,\n [], // Empty group by means single group\n query.having,\n query.select,\n query.fnHaving,\n )\n }\n }\n\n // Process the HAVING clause if it exists (only applies after GROUP BY)\n if (query.having && (!query.groupBy || query.groupBy.length === 0)) {\n // Check if we have aggregates in SELECT that would trigger implicit grouping\n const hasAggregates = query.select\n ? Object.values(query.select).some((expr) => expr.type === `agg`)\n : false\n\n if (!hasAggregates) {\n throw new HavingRequiresGroupByError()\n }\n }\n\n // Process functional HAVING clauses outside of GROUP BY (treat as additional WHERE filters)\n if (\n query.fnHaving &&\n query.fnHaving.length > 0 &&\n (!query.groupBy || query.groupBy.length === 0)\n ) {\n // If there's no GROUP BY but there are fnHaving clauses, apply them as filters\n for (const fnHaving of query.fnHaving) {\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return fnHaving(namespacedRow)\n }),\n )\n }\n }\n\n // Process the DISTINCT clause if it exists\n if (query.distinct) {\n pipeline = pipeline.pipe(distinct(([_key, row]) => row.$selected))\n }\n\n // Process orderBy parameter if it exists\n if (query.orderBy && query.orderBy.length > 0) {\n const orderedPipeline = processOrderBy(\n rawQuery,\n pipeline,\n query.orderBy,\n query.select || {},\n collections[mainCollectionId]!,\n optimizableOrderByCollections,\n setWindowFn,\n query.limit,\n query.offset,\n )\n\n // Final step: extract the $selected and include orderBy index\n const resultPipeline = orderedPipeline.pipe(\n map(([key, [row, orderByIndex]]) => {\n // Extract the final results from $selected and include orderBy index\n const raw = (row as any).$selected\n const finalResults = unwrapValue(raw)\n return [key, [finalResults, orderByIndex]] as [unknown, [any, string]]\n }),\n )\n\n const result = resultPipeline\n // Cache the result before returning (use original query as key)\n const compilationResult = {\n collectionId: mainCollectionId,\n pipeline: result,\n sourceWhereClauses,\n aliasToCollectionId,\n aliasRemapping,\n }\n cache.set(rawQuery, compilationResult)\n\n return compilationResult\n } else if (query.limit !== undefined || query.offset !== undefined) {\n // If there's a limit or offset without orderBy, throw an error\n throw new LimitOffsetRequireOrderByError()\n }\n\n // Final step: extract the $selected and return tuple format (no orderBy)\n const resultPipeline: ResultStream = pipeline.pipe(\n map(([key, row]) => {\n // Extract the final results from $selected and return [key, [results, undefined]]\n const raw = (row as any).$selected\n const finalResults = unwrapValue(raw)\n return [key, [finalResults, undefined]] as [\n unknown,\n [any, string | undefined],\n ]\n }),\n )\n\n const result = resultPipeline\n // Cache the result before returning (use original query as key)\n const compilationResult = {\n collectionId: mainCollectionId,\n pipeline: result,\n sourceWhereClauses,\n aliasToCollectionId,\n aliasRemapping,\n }\n cache.set(rawQuery, compilationResult)\n\n return compilationResult\n}\n\n/**\n * Collects aliases used for DIRECT collection references (not subqueries).\n * Used to validate that subqueries don't reuse parent query collection aliases.\n * Only direct CollectionRef aliases matter - QueryRef aliases don't cause conflicts.\n */\nfunction collectDirectCollectionAliases(query: QueryIR): Set<string> {\n const aliases = new Set<string>()\n\n // Collect FROM alias only if it's a direct collection reference\n if (query.from.type === `collectionRef`) {\n aliases.add(query.from.alias)\n }\n\n // Collect JOIN aliases only for direct collection references\n if (query.join) {\n for (const joinClause of query.join) {\n if (joinClause.from.type === `collectionRef`) {\n aliases.add(joinClause.from.alias)\n }\n }\n }\n\n return aliases\n}\n\n/**\n * Validates the structure of a query and its subqueries.\n * Checks that subqueries don't reuse collection aliases from parent queries.\n * This must be called on the RAW query before optimization.\n */\nfunction validateQueryStructure(\n query: QueryIR,\n parentCollectionAliases: Set<string> = new Set(),\n): void {\n // Collect direct collection aliases from this query level\n const currentLevelAliases = collectDirectCollectionAliases(query)\n\n // Check if any current alias conflicts with parent aliases\n for (const alias of currentLevelAliases) {\n if (parentCollectionAliases.has(alias)) {\n throw new DuplicateAliasInSubqueryError(\n alias,\n Array.from(parentCollectionAliases),\n )\n }\n }\n\n // Combine parent and current aliases for checking nested subqueries\n const combinedAliases = new Set([\n ...parentCollectionAliases,\n ...currentLevelAliases,\n ])\n\n // Recursively validate FROM subquery\n if (query.from.type === `queryRef`) {\n validateQueryStructure(query.from.query, combinedAliases)\n }\n\n // Recursively validate JOIN subqueries\n if (query.join) {\n for (const joinClause of query.join) {\n if (joinClause.from.type === `queryRef`) {\n validateQueryStructure(joinClause.from.query, combinedAliases)\n }\n }\n }\n}\n\n/**\n * Processes the FROM clause, handling direct collection references and subqueries.\n * Populates `aliasToCollectionId` and `aliasRemapping` for per-alias subscription tracking.\n */\nfunction processFrom(\n from: CollectionRef | QueryRef,\n allInputs: Record<string, KeyedStream>,\n collections: Record<string, Collection>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazySources: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n cache: QueryCache,\n queryMapping: QueryMapping,\n aliasToCollectionId: Record<string, string>,\n aliasRemapping: Record<string, string>,\n sourceWhereClauses: Map<string, BasicExpression<boolean>>,\n): { alias: string; input: KeyedStream; collectionId: string } {\n switch (from.type) {\n case `collectionRef`: {\n const input = allInputs[from.alias]\n if (!input) {\n throw new CollectionInputNotFoundError(\n from.alias,\n from.collection.id,\n Object.keys(allInputs),\n )\n }\n aliasToCollectionId[from.alias] = from.collection.id\n return { alias: from.alias, input, collectionId: from.collection.id }\n }\n case `queryRef`: {\n // Find the original query for caching purposes\n const originalQuery = queryMapping.get(from.query) || from.query\n\n // Recursively compile the sub-query with cache\n const subQueryResult = compileQuery(\n originalQuery,\n allInputs,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n cache,\n queryMapping,\n )\n\n // Pull up alias mappings from subquery to parent scope.\n // This includes both the innermost alias-to-collection mappings AND\n // any existing remappings from nested subquery levels.\n Object.assign(aliasToCollectionId, subQueryResult.aliasToCollectionId)\n Object.assign(aliasRemapping, subQueryResult.aliasRemapping)\n\n // Pull up source WHERE clauses from subquery to parent scope.\n // This enables loadSubset to receive the correct where clauses for subquery collections.\n //\n // IMPORTANT: Skip pull-up for optimizer-created subqueries. These are detected when:\n // 1. The outer alias (from.alias) matches the inner alias (from.query.from.alias)\n // 2. The subquery was found in queryMapping (it's a user-defined subquery, not optimizer-created)\n //\n // For optimizer-created subqueries, the parent already has the sourceWhereClauses\n // extracted from the original raw query, so pulling up would be redundant.\n // More importantly, pulling up for optimizer-created subqueries can cause issues\n // when the optimizer has restructured the query.\n const isUserDefinedSubquery = queryMapping.has(from.query)\n const subqueryFromAlias = from.query.from.alias\n const isOptimizerCreated =\n !isUserDefinedSubquery && from.alias === subqueryFromAlias\n\n if (!isOptimizerCreated) {\n for (const [alias, whereClause] of subQueryResult.sourceWhereClauses) {\n sourceWhereClauses.set(alias, whereClause)\n }\n }\n\n // Create a FLATTENED remapping from outer alias to innermost alias.\n // For nested subqueries, this ensures one-hop lookups (not recursive chains).\n //\n // Example with 3-level nesting:\n // Inner: .from({ user: usersCollection })\n // Middle: .from({ activeUser: innerSubquery }) → creates: activeUser → user\n // Outer: .from({ author: middleSubquery }) → creates: author → user (not author → activeUser)\n //\n // The key insight: We search through the PULLED-UP aliasToCollectionId (which contains\n // the innermost 'user' alias), so we always map directly to the deepest level.\n // This means aliasRemapping[alias] is always a single lookup, never recursive.\n // Needed for subscription resolution during lazy loading.\n const innerAlias = Object.keys(subQueryResult.aliasToCollectionId).find(\n (alias) =>\n subQueryResult.aliasToCollectionId[alias] ===\n subQueryResult.collectionId,\n )\n if (innerAlias && innerAlias !== from.alias) {\n aliasRemapping[from.alias] = innerAlias\n }\n\n // Extract the pipeline from the compilation result\n const subQueryInput = subQueryResult.pipeline\n\n // Subqueries may return [key, [value, orderByIndex]] (with ORDER BY) or [key, value] (without ORDER BY)\n // We need to extract just the value for use in parent queries\n const extractedInput = subQueryInput.pipe(\n map((data: any) => {\n const [key, [value, _orderByIndex]] = data\n // Unwrap Value expressions that might have leaked through as the entire row\n const unwrapped = unwrapValue(value)\n return [key, unwrapped] as [unknown, any]\n }),\n )\n\n return {\n alias: from.alias,\n input: extractedInput,\n collectionId: subQueryResult.collectionId,\n }\n }\n default:\n throw new UnsupportedFromTypeError((from as any).type)\n }\n}\n\n// Helper to check if a value is a Value expression\nfunction isValue(raw: any): boolean {\n return (\n raw instanceof ValClass ||\n (raw && typeof raw === `object` && `type` in raw && raw.type === `val`)\n )\n}\n\n// Helper to unwrap a Value expression or return the value itself\nfunction unwrapValue(value: any): any {\n return isValue(value) ? value.value : value\n}\n\n/**\n * Recursively maps optimized subqueries to their original queries for proper caching.\n * This ensures that when we encounter the same QueryRef object in different contexts,\n * we can find the original query to check the cache.\n */\nfunction mapNestedQueries(\n optimizedQuery: QueryIR,\n originalQuery: QueryIR,\n queryMapping: QueryMapping,\n): void {\n // Map the FROM clause if it's a QueryRef\n if (\n optimizedQuery.from.type === `queryRef` &&\n originalQuery.from.type === `queryRef`\n ) {\n queryMapping.set(optimizedQuery.from.query, originalQuery.from.query)\n // Recursively map nested queries\n mapNestedQueries(\n optimizedQuery.from.query,\n originalQuery.from.query,\n queryMapping,\n )\n }\n\n // Map JOIN clauses if they exist\n if (optimizedQuery.join && originalQuery.join) {\n for (\n let i = 0;\n i < optimizedQuery.join.length && i < originalQuery.join.length;\n i++\n ) {\n const optimizedJoin = optimizedQuery.join[i]!\n const originalJoin = originalQuery.join[i]!\n\n if (\n optimizedJoin.from.type === `queryRef` &&\n originalJoin.from.type === `queryRef`\n ) {\n queryMapping.set(optimizedJoin.from.query, originalJoin.from.query)\n // Recursively map nested queries in joins\n mapNestedQueries(\n optimizedJoin.from.query,\n originalJoin.from.query,\n queryMapping,\n )\n }\n }\n }\n}\n\nfunction getRefFromAlias(\n query: QueryIR,\n alias: string,\n): CollectionRef | QueryRef | void {\n if (query.from.alias === alias) {\n return query.from\n }\n\n for (const join of query.join || []) {\n if (join.from.alias === alias) {\n return join.from\n }\n }\n}\n\n/**\n * Follows the given reference in a query\n * until its finds the root field the reference points to.\n * @returns The collection, its alias, and the path to the root field in this collection\n */\nexport function followRef(\n query: QueryIR,\n ref: PropRef<any>,\n collection: Collection,\n): { collection: Collection; path: Array<string> } | void {\n if (ref.path.length === 0) {\n return\n }\n\n if (ref.path.length === 1) {\n // This field should be part of this collection\n const field = ref.path[0]!\n // is it part of the select clause?\n if (query.select) {\n const selectedField = query.select[field]\n if (selectedField && selectedField.type === `ref`) {\n return followRef(query, selectedField, collection)\n }\n }\n\n // Either this field is not part of the select clause\n // and thus it must be part of the collection itself\n // or it is part of the select but is not a reference\n // so we can stop here and don't have to follow it\n return { collection, path: [field] }\n }\n\n if (ref.path.length > 1) {\n // This is a nested field\n const [alias, ...rest] = ref.path\n const aliasRef = getRefFromAlias(query, alias!)\n if (!aliasRef) {\n return\n }\n\n if (aliasRef.type === `queryRef`) {\n return followRef(aliasRef.query, new PropRef(rest), collection)\n } else {\n // This is a reference to a collection\n // we can't follow it further\n // so the field must be on the collection itself\n return { collection: aliasRef.collection, path: rest }\n }\n }\n}\n\nexport type CompileQueryFn = typeof compileQuery\n"],"names":["optimizeQuery","map","processJoins","getWhereExpression","compileExpression","filter","toBooleanPredicate","DistinctRequiresSelectError","processSelect","processGroupBy","HavingRequiresGroupByError","distinct","processOrderBy","resultPipeline","result","compilationResult","LimitOffsetRequireOrderByError","DuplicateAliasInSubqueryError","CollectionInputNotFoundError","UnsupportedFromTypeError","ValClass"],"mappings":"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"}
|
|
@@ -6,7 +6,7 @@ const autoIndex = require("../../indexes/auto-index.cjs");
|
|
|
6
6
|
const ir = require("../ir.cjs");
|
|
7
7
|
const functions = require("../builder/functions.cjs");
|
|
8
8
|
const evaluators = require("./evaluators.cjs");
|
|
9
|
-
function processJoins(pipeline, joinClauses, sources, mainCollectionId, mainSource, allInputs, cache, queryMapping, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, rawQuery, onCompileSubquery, aliasToCollectionId, aliasRemapping) {
|
|
9
|
+
function processJoins(pipeline, joinClauses, sources, mainCollectionId, mainSource, allInputs, cache, queryMapping, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, rawQuery, onCompileSubquery, aliasToCollectionId, aliasRemapping, sourceWhereClauses) {
|
|
10
10
|
let resultPipeline = pipeline;
|
|
11
11
|
for (const joinClause of joinClauses) {
|
|
12
12
|
resultPipeline = processJoin(
|
|
@@ -27,12 +27,13 @@ function processJoins(pipeline, joinClauses, sources, mainCollectionId, mainSour
|
|
|
27
27
|
rawQuery,
|
|
28
28
|
onCompileSubquery,
|
|
29
29
|
aliasToCollectionId,
|
|
30
|
-
aliasRemapping
|
|
30
|
+
aliasRemapping,
|
|
31
|
+
sourceWhereClauses
|
|
31
32
|
);
|
|
32
33
|
}
|
|
33
34
|
return resultPipeline;
|
|
34
35
|
}
|
|
35
|
-
function processJoin(pipeline, joinClause, sources, mainCollectionId, mainSource, allInputs, cache, queryMapping, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, rawQuery, onCompileSubquery, aliasToCollectionId, aliasRemapping) {
|
|
36
|
+
function processJoin(pipeline, joinClause, sources, mainCollectionId, mainSource, allInputs, cache, queryMapping, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, rawQuery, onCompileSubquery, aliasToCollectionId, aliasRemapping, sourceWhereClauses) {
|
|
36
37
|
const isCollectionRef = joinClause.from.type === `collectionRef`;
|
|
37
38
|
const {
|
|
38
39
|
alias: joinedSource,
|
|
@@ -51,7 +52,8 @@ function processJoin(pipeline, joinClause, sources, mainCollectionId, mainSource
|
|
|
51
52
|
queryMapping,
|
|
52
53
|
onCompileSubquery,
|
|
53
54
|
aliasToCollectionId,
|
|
54
|
-
aliasRemapping
|
|
55
|
+
aliasRemapping,
|
|
56
|
+
sourceWhereClauses
|
|
55
57
|
);
|
|
56
58
|
sources[joinedSource] = joinedInput;
|
|
57
59
|
if (isCollectionRef) {
|
|
@@ -200,7 +202,7 @@ function getSourceAliasFromExpression(expr) {
|
|
|
200
202
|
return null;
|
|
201
203
|
}
|
|
202
204
|
}
|
|
203
|
-
function processJoinSource(from, allInputs, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, cache, queryMapping, onCompileSubquery, aliasToCollectionId, aliasRemapping) {
|
|
205
|
+
function processJoinSource(from, allInputs, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, cache, queryMapping, onCompileSubquery, aliasToCollectionId, aliasRemapping, sourceWhereClauses) {
|
|
204
206
|
switch (from.type) {
|
|
205
207
|
case `collectionRef`: {
|
|
206
208
|
const input = allInputs[from.alias];
|
|
@@ -230,6 +232,14 @@ function processJoinSource(from, allInputs, collections, subscriptions, callback
|
|
|
230
232
|
);
|
|
231
233
|
Object.assign(aliasToCollectionId, subQueryResult.aliasToCollectionId);
|
|
232
234
|
Object.assign(aliasRemapping, subQueryResult.aliasRemapping);
|
|
235
|
+
const isUserDefinedSubquery = queryMapping.has(from.query);
|
|
236
|
+
const fromInnerAlias = from.query.from.alias;
|
|
237
|
+
const isOptimizerCreated = !isUserDefinedSubquery && from.alias === fromInnerAlias;
|
|
238
|
+
if (!isOptimizerCreated) {
|
|
239
|
+
for (const [alias, whereClause] of subQueryResult.sourceWhereClauses) {
|
|
240
|
+
sourceWhereClauses.set(alias, whereClause);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
233
243
|
const innerAlias = Object.keys(subQueryResult.aliasToCollectionId).find(
|
|
234
244
|
(alias) => subQueryResult.aliasToCollectionId[alias] === subQueryResult.collectionId
|
|
235
245
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"joins.cjs","sources":["../../../../src/query/compiler/joins.ts"],"sourcesContent":["import { filter, join as joinOperator, map, tap } from '@tanstack/db-ivm'\nimport {\n CollectionInputNotFoundError,\n InvalidJoinCondition,\n InvalidJoinConditionLeftSourceError,\n InvalidJoinConditionRightSourceError,\n InvalidJoinConditionSameSourceError,\n InvalidJoinConditionSourceMismatchError,\n JoinCollectionNotFoundError,\n SubscriptionNotFoundError,\n UnsupportedJoinSourceTypeError,\n UnsupportedJoinTypeError,\n} from '../../errors.js'\nimport { ensureIndexForField } from '../../indexes/auto-index.js'\nimport { PropRef, followRef } from '../ir.js'\nimport { inArray } from '../builder/functions.js'\nimport { compileExpression } from './evaluators.js'\nimport type { CompileQueryFn } from './index.js'\nimport type { OrderByOptimizationInfo } from './order-by.js'\nimport type {\n BasicExpression,\n CollectionRef,\n JoinClause,\n QueryIR,\n QueryRef,\n} from '../ir.js'\nimport type { IStreamBuilder, JoinType } from '@tanstack/db-ivm'\nimport type { Collection } from '../../collection/index.js'\nimport type {\n KeyedStream,\n NamespacedAndKeyedStream,\n NamespacedRow,\n} from '../../types.js'\nimport type { QueryCache, QueryMapping, WindowOptions } from './types.js'\nimport type { CollectionSubscription } from '../../collection/subscription.js'\n\n/** Function type for loading specific keys into a lazy collection */\nexport type LoadKeysFn = (key: Set<string | number>) => void\n\n/** Callbacks for managing lazy-loaded collections in optimized joins */\nexport type LazyCollectionCallbacks = {\n loadKeys: LoadKeysFn\n loadInitialState: () => void\n}\n\n/**\n * Processes all join clauses, applying lazy loading optimizations and maintaining\n * alias tracking for per-alias subscriptions (enables self-joins).\n */\nexport function processJoins(\n pipeline: NamespacedAndKeyedStream,\n joinClauses: Array<JoinClause>,\n sources: Record<string, KeyedStream>,\n mainCollectionId: string,\n mainSource: string,\n allInputs: Record<string, KeyedStream>,\n cache: QueryCache,\n queryMapping: QueryMapping,\n collections: Record<string, Collection>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazySources: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n rawQuery: QueryIR,\n onCompileSubquery: CompileQueryFn,\n aliasToCollectionId: Record<string, string>,\n aliasRemapping: Record<string, string>,\n): NamespacedAndKeyedStream {\n let resultPipeline = pipeline\n\n for (const joinClause of joinClauses) {\n resultPipeline = processJoin(\n resultPipeline,\n joinClause,\n sources,\n mainCollectionId,\n mainSource,\n allInputs,\n cache,\n queryMapping,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n rawQuery,\n onCompileSubquery,\n aliasToCollectionId,\n aliasRemapping,\n )\n }\n\n return resultPipeline\n}\n\n/**\n * Processes a single join clause with lazy loading optimization.\n * For LEFT/RIGHT/INNER joins, marks one side as \"lazy\" (loads on-demand based on join keys).\n */\nfunction processJoin(\n pipeline: NamespacedAndKeyedStream,\n joinClause: JoinClause,\n sources: Record<string, KeyedStream>,\n mainCollectionId: string,\n mainSource: string,\n allInputs: Record<string, KeyedStream>,\n cache: QueryCache,\n queryMapping: QueryMapping,\n collections: Record<string, Collection>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazySources: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n rawQuery: QueryIR,\n onCompileSubquery: CompileQueryFn,\n aliasToCollectionId: Record<string, string>,\n aliasRemapping: Record<string, string>,\n): NamespacedAndKeyedStream {\n const isCollectionRef = joinClause.from.type === `collectionRef`\n\n // Get the joined source alias and input stream\n const {\n alias: joinedSource,\n input: joinedInput,\n collectionId: joinedCollectionId,\n } = processJoinSource(\n joinClause.from,\n allInputs,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n cache,\n queryMapping,\n onCompileSubquery,\n aliasToCollectionId,\n aliasRemapping,\n )\n\n // Add the joined source to the sources map\n sources[joinedSource] = joinedInput\n if (isCollectionRef) {\n // Only direct collection references form new alias bindings. Subquery\n // aliases reuse the mapping returned from the recursive compilation above.\n aliasToCollectionId[joinedSource] = joinedCollectionId\n }\n\n const mainCollection = collections[mainCollectionId]\n const joinedCollection = collections[joinedCollectionId]\n\n if (!mainCollection) {\n throw new JoinCollectionNotFoundError(mainCollectionId)\n }\n\n if (!joinedCollection) {\n throw new JoinCollectionNotFoundError(joinedCollectionId)\n }\n\n const { activeSource, lazySource } = getActiveAndLazySources(\n joinClause.type,\n mainCollection,\n joinedCollection,\n )\n\n // Analyze which source each expression refers to and swap if necessary\n const availableSources = Object.keys(sources)\n const { mainExpr, joinedExpr } = analyzeJoinExpressions(\n joinClause.left,\n joinClause.right,\n availableSources,\n joinedSource,\n )\n\n // Pre-compile the join expressions\n const compiledMainExpr = compileExpression(mainExpr)\n const compiledJoinedExpr = compileExpression(joinedExpr)\n\n // Prepare the main pipeline for joining\n let mainPipeline = pipeline.pipe(\n map(([currentKey, namespacedRow]) => {\n // Extract the join key from the main source expression\n const mainKey = compiledMainExpr(namespacedRow)\n\n // Return [joinKey, [originalKey, namespacedRow]]\n return [mainKey, [currentKey, namespacedRow]] as [\n unknown,\n [string, typeof namespacedRow],\n ]\n }),\n )\n\n // Prepare the joined pipeline\n let joinedPipeline = joinedInput.pipe(\n map(([currentKey, row]) => {\n // Wrap the row in a namespaced structure\n const namespacedRow: NamespacedRow = { [joinedSource]: row }\n\n // Extract the join key from the joined source expression\n const joinedKey = compiledJoinedExpr(namespacedRow)\n\n // Return [joinKey, [originalKey, namespacedRow]]\n return [joinedKey, [currentKey, namespacedRow]] as [\n unknown,\n [string, typeof namespacedRow],\n ]\n }),\n )\n\n // Apply the join operation\n if (![`inner`, `left`, `right`, `full`].includes(joinClause.type)) {\n throw new UnsupportedJoinTypeError(joinClause.type)\n }\n\n if (activeSource) {\n // If the lazy collection comes from a subquery that has a limit and/or an offset clause\n // then we need to deoptimize the join because we don't know which rows are in the result set\n // since we simply lookup matching keys in the index but the index contains all rows\n // (not just the ones that pass the limit and offset clauses)\n const lazyFrom = activeSource === `main` ? joinClause.from : rawQuery.from\n const limitedSubquery =\n lazyFrom.type === `queryRef` &&\n (lazyFrom.query.limit || lazyFrom.query.offset)\n\n // If join expressions contain computed values (like concat functions)\n // we don't optimize the join because we don't have an index over the computed values\n const hasComputedJoinExpr =\n mainExpr.type === `func` || joinedExpr.type === `func`\n\n if (!limitedSubquery && !hasComputedJoinExpr) {\n // This join can be optimized by having the active collection\n // dynamically load keys into the lazy collection\n // based on the value of the joinKey and by looking up\n // matching rows in the index of the lazy collection\n\n // Mark the lazy source alias as lazy\n // this Set is passed by the liveQueryCollection to the compiler\n // such that the liveQueryCollection can check it after compilation\n // to know which source aliases should load data lazily (not initially)\n const lazyAlias = activeSource === `main` ? joinedSource : mainSource\n lazySources.add(lazyAlias)\n\n const activePipeline =\n activeSource === `main` ? mainPipeline : joinedPipeline\n\n const lazySourceJoinExpr =\n activeSource === `main`\n ? (joinedExpr as PropRef)\n : (mainExpr as PropRef)\n\n const followRefResult = followRef(\n rawQuery,\n lazySourceJoinExpr,\n lazySource,\n )!\n const followRefCollection = followRefResult.collection\n\n const fieldName = followRefResult.path[0]\n if (fieldName) {\n ensureIndexForField(\n fieldName,\n followRefResult.path,\n followRefCollection,\n )\n }\n\n // Set up lazy loading: intercept active side's stream and dynamically load\n // matching rows from lazy side based on join keys.\n const activePipelineWithLoading: IStreamBuilder<\n [key: unknown, [originalKey: string, namespacedRow: NamespacedRow]]\n > = activePipeline.pipe(\n tap((data) => {\n // Find the subscription for lazy loading.\n // Subscriptions are keyed by the innermost alias (where the collection subscription\n // was actually created). For subqueries, the join alias may differ from the inner alias.\n // aliasRemapping provides a flattened one-hop lookup from outer → innermost alias.\n // Example: .join({ activeUser: subquery }) where subquery uses .from({ user: collection })\n // → aliasRemapping['activeUser'] = 'user' (always maps directly to innermost, never recursive)\n const resolvedAlias = aliasRemapping[lazyAlias] || lazyAlias\n const lazySourceSubscription = subscriptions[resolvedAlias]\n\n if (!lazySourceSubscription) {\n throw new SubscriptionNotFoundError(\n resolvedAlias,\n lazyAlias,\n lazySource.id,\n Object.keys(subscriptions),\n )\n }\n\n if (lazySourceSubscription.hasLoadedInitialState()) {\n // Entire state was already loaded because we deoptimized the join\n return\n }\n\n // Request filtered snapshot from lazy collection for matching join keys\n const joinKeys = data.getInner().map(([[joinKey]]) => joinKey)\n const lazyJoinRef = new PropRef(followRefResult.path)\n const loaded = lazySourceSubscription.requestSnapshot({\n where: inArray(lazyJoinRef, joinKeys),\n optimizedOnly: true,\n })\n\n if (!loaded) {\n // Snapshot wasn't sent because it could not be loaded from the indexes\n lazySourceSubscription.requestSnapshot()\n }\n }),\n )\n\n if (activeSource === `main`) {\n mainPipeline = activePipelineWithLoading\n } else {\n joinedPipeline = activePipelineWithLoading\n }\n }\n }\n\n return mainPipeline.pipe(\n joinOperator(joinedPipeline, joinClause.type as JoinType),\n processJoinResults(joinClause.type),\n )\n}\n\n/**\n * Analyzes join expressions to determine which refers to which source\n * and returns them in the correct order (available source expression first, joined source expression second)\n */\nfunction analyzeJoinExpressions(\n left: BasicExpression,\n right: BasicExpression,\n allAvailableSourceAliases: Array<string>,\n joinedSource: string,\n): { mainExpr: BasicExpression; joinedExpr: BasicExpression } {\n // Filter out the joined source alias from the available source aliases\n const availableSources = allAvailableSourceAliases.filter(\n (alias) => alias !== joinedSource,\n )\n\n const leftSourceAlias = getSourceAliasFromExpression(left)\n const rightSourceAlias = getSourceAliasFromExpression(right)\n\n // If left expression refers to an available source and right refers to joined source, keep as is\n if (\n leftSourceAlias &&\n availableSources.includes(leftSourceAlias) &&\n rightSourceAlias === joinedSource\n ) {\n return { mainExpr: left, joinedExpr: right }\n }\n\n // If left expression refers to joined source and right refers to an available source, swap them\n if (\n leftSourceAlias === joinedSource &&\n rightSourceAlias &&\n availableSources.includes(rightSourceAlias)\n ) {\n return { mainExpr: right, joinedExpr: left }\n }\n\n // If one expression doesn't refer to any source, this is an invalid join\n if (!leftSourceAlias || !rightSourceAlias) {\n throw new InvalidJoinConditionSourceMismatchError()\n }\n\n // If both expressions refer to the same alias, this is an invalid join\n if (leftSourceAlias === rightSourceAlias) {\n throw new InvalidJoinConditionSameSourceError(leftSourceAlias)\n }\n\n // Left side must refer to an available source\n // This cannot happen with the query builder as there is no way to build a ref\n // to an unavailable source, but just in case, but could happen with the IR\n if (!availableSources.includes(leftSourceAlias)) {\n throw new InvalidJoinConditionLeftSourceError(leftSourceAlias)\n }\n\n // Right side must refer to the joined source\n if (rightSourceAlias !== joinedSource) {\n throw new InvalidJoinConditionRightSourceError(joinedSource)\n }\n\n // This should not be reachable given the logic above, but just in case\n throw new InvalidJoinCondition()\n}\n\n/**\n * Extracts the source alias from a join expression\n */\nfunction getSourceAliasFromExpression(expr: BasicExpression): string | null {\n switch (expr.type) {\n case `ref`:\n // PropRef path has the source alias as the first element\n return expr.path[0] || null\n case `func`: {\n // For function expressions, we need to check if all arguments refer to the same source\n const sourceAliases = new Set<string>()\n for (const arg of expr.args) {\n const alias = getSourceAliasFromExpression(arg)\n if (alias) {\n sourceAliases.add(alias)\n }\n }\n // If all arguments refer to the same source, return that source alias\n return sourceAliases.size === 1 ? Array.from(sourceAliases)[0]! : null\n }\n default:\n // Values (type='val') don't reference any source\n return null\n }\n}\n\n/**\n * Processes the join source (collection or sub-query)\n */\nfunction processJoinSource(\n from: CollectionRef | QueryRef,\n allInputs: Record<string, KeyedStream>,\n collections: Record<string, Collection>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazySources: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n cache: QueryCache,\n queryMapping: QueryMapping,\n onCompileSubquery: CompileQueryFn,\n aliasToCollectionId: Record<string, string>,\n aliasRemapping: Record<string, string>,\n): { alias: string; input: KeyedStream; collectionId: string } {\n switch (from.type) {\n case `collectionRef`: {\n const input = allInputs[from.alias]\n if (!input) {\n throw new CollectionInputNotFoundError(\n from.alias,\n from.collection.id,\n Object.keys(allInputs),\n )\n }\n aliasToCollectionId[from.alias] = from.collection.id\n return { alias: from.alias, input, collectionId: from.collection.id }\n }\n case `queryRef`: {\n // Find the original query for caching purposes\n const originalQuery = queryMapping.get(from.query) || from.query\n\n // Recursively compile the sub-query with cache\n const subQueryResult = onCompileSubquery(\n originalQuery,\n allInputs,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n cache,\n queryMapping,\n )\n\n // Pull up alias mappings from subquery to parent scope.\n // This includes both the innermost alias-to-collection mappings AND\n // any existing remappings from nested subquery levels.\n Object.assign(aliasToCollectionId, subQueryResult.aliasToCollectionId)\n Object.assign(aliasRemapping, subQueryResult.aliasRemapping)\n\n // Create a flattened remapping from outer alias to innermost alias.\n // For nested subqueries, this ensures one-hop lookups (not recursive chains).\n //\n // Example with 3-level nesting:\n // Inner: .from({ user: usersCollection })\n // Middle: .from({ activeUser: innerSubquery }) → creates: activeUser → user\n // Outer: .join({ author: middleSubquery }, ...) → creates: author → user (not author → activeUser)\n //\n // We search through the PULLED-UP aliasToCollectionId (which contains the\n // innermost 'user' alias), so we always map directly to the deepest level.\n // This means aliasRemapping[lazyAlias] is always a single lookup, never recursive.\n const innerAlias = Object.keys(subQueryResult.aliasToCollectionId).find(\n (alias) =>\n subQueryResult.aliasToCollectionId[alias] ===\n subQueryResult.collectionId,\n )\n if (innerAlias && innerAlias !== from.alias) {\n aliasRemapping[from.alias] = innerAlias\n }\n\n // Extract the pipeline from the compilation result\n const subQueryInput = subQueryResult.pipeline\n\n // Subqueries may return [key, [value, orderByIndex]] (with ORDER BY) or [key, value] (without ORDER BY)\n // We need to extract just the value for use in parent queries\n const extractedInput = subQueryInput.pipe(\n map((data: any) => {\n const [key, [value, _orderByIndex]] = data\n return [key, value] as [unknown, any]\n }),\n )\n\n return {\n alias: from.alias,\n input: extractedInput as KeyedStream,\n collectionId: subQueryResult.collectionId,\n }\n }\n default:\n throw new UnsupportedJoinSourceTypeError((from as any).type)\n }\n}\n\n/**\n * Processes the results of a join operation\n */\nfunction processJoinResults(joinType: string) {\n return function (\n pipeline: IStreamBuilder<\n [\n key: string,\n [\n [string, NamespacedRow] | undefined,\n [string, NamespacedRow] | undefined,\n ],\n ]\n >,\n ): NamespacedAndKeyedStream {\n return pipeline.pipe(\n // Process the join result and handle nulls\n filter((result) => {\n const [_key, [main, joined]] = result\n const mainNamespacedRow = main?.[1]\n const joinedNamespacedRow = joined?.[1]\n\n // Handle different join types\n if (joinType === `inner`) {\n return !!(mainNamespacedRow && joinedNamespacedRow)\n }\n\n if (joinType === `left`) {\n return !!mainNamespacedRow\n }\n\n if (joinType === `right`) {\n return !!joinedNamespacedRow\n }\n\n // For full joins, always include\n return true\n }),\n map((result) => {\n const [_key, [main, joined]] = result\n const mainKey = main?.[0]\n const mainNamespacedRow = main?.[1]\n const joinedKey = joined?.[0]\n const joinedNamespacedRow = joined?.[1]\n\n // Merge the namespaced rows\n const mergedNamespacedRow: NamespacedRow = {}\n\n // Add main row data if it exists\n if (mainNamespacedRow) {\n Object.assign(mergedNamespacedRow, mainNamespacedRow)\n }\n\n // Add joined row data if it exists\n if (joinedNamespacedRow) {\n Object.assign(mergedNamespacedRow, joinedNamespacedRow)\n }\n\n // We create a composite key that combines the main and joined keys\n const resultKey = `[${mainKey},${joinedKey}]`\n\n return [resultKey, mergedNamespacedRow] as [string, NamespacedRow]\n }),\n )\n }\n}\n\n/**\n * Returns the active and lazy collections for a join clause.\n * The active collection is the one that we need to fully iterate over\n * and it can be the main source (i.e. left collection) or the joined source (i.e. right collection).\n * The lazy collection is the one that we should join-in lazily based on matches in the active collection.\n * @param joinClause - The join clause to analyze\n * @param leftCollection - The left collection\n * @param rightCollection - The right collection\n * @returns The active and lazy collections. They are undefined if we need to loop over both collections (i.e. both are active)\n */\nfunction getActiveAndLazySources(\n joinType: JoinClause[`type`],\n leftCollection: Collection,\n rightCollection: Collection,\n):\n | { activeSource: `main` | `joined`; lazySource: Collection }\n | { activeSource: undefined; lazySource: undefined } {\n // Self-joins can now be optimized since we track lazy loading by source alias\n // rather than collection ID. Each alias has its own subscription and lazy state.\n\n switch (joinType) {\n case `left`:\n return { activeSource: `main`, lazySource: rightCollection }\n case `right`:\n return { activeSource: `joined`, lazySource: leftCollection }\n case `inner`:\n // The smallest collection should be the active collection\n // and the biggest collection should be lazy\n return leftCollection.size < rightCollection.size\n ? { activeSource: `main`, lazySource: rightCollection }\n : { activeSource: `joined`, lazySource: leftCollection }\n default:\n return { activeSource: undefined, lazySource: undefined }\n }\n}\n"],"names":["JoinCollectionNotFoundError","compileExpression","map","UnsupportedJoinTypeError","followRef","ensureIndexForField","tap","SubscriptionNotFoundError","PropRef","inArray","joinOperator","InvalidJoinConditionSourceMismatchError","InvalidJoinConditionSameSourceError","InvalidJoinConditionLeftSourceError","InvalidJoinConditionRightSourceError","InvalidJoinCondition","CollectionInputNotFoundError","UnsupportedJoinSourceTypeError","filter"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"joins.cjs","sources":["../../../../src/query/compiler/joins.ts"],"sourcesContent":["import { filter, join as joinOperator, map, tap } from '@tanstack/db-ivm'\nimport {\n CollectionInputNotFoundError,\n InvalidJoinCondition,\n InvalidJoinConditionLeftSourceError,\n InvalidJoinConditionRightSourceError,\n InvalidJoinConditionSameSourceError,\n InvalidJoinConditionSourceMismatchError,\n JoinCollectionNotFoundError,\n SubscriptionNotFoundError,\n UnsupportedJoinSourceTypeError,\n UnsupportedJoinTypeError,\n} from '../../errors.js'\nimport { ensureIndexForField } from '../../indexes/auto-index.js'\nimport { PropRef, followRef } from '../ir.js'\nimport { inArray } from '../builder/functions.js'\nimport { compileExpression } from './evaluators.js'\nimport type { CompileQueryFn } from './index.js'\nimport type { OrderByOptimizationInfo } from './order-by.js'\nimport type {\n BasicExpression,\n CollectionRef,\n JoinClause,\n QueryIR,\n QueryRef,\n} from '../ir.js'\nimport type { IStreamBuilder, JoinType } from '@tanstack/db-ivm'\nimport type { Collection } from '../../collection/index.js'\nimport type {\n KeyedStream,\n NamespacedAndKeyedStream,\n NamespacedRow,\n} from '../../types.js'\nimport type { QueryCache, QueryMapping, WindowOptions } from './types.js'\nimport type { CollectionSubscription } from '../../collection/subscription.js'\n\n/** Function type for loading specific keys into a lazy collection */\nexport type LoadKeysFn = (key: Set<string | number>) => void\n\n/** Callbacks for managing lazy-loaded collections in optimized joins */\nexport type LazyCollectionCallbacks = {\n loadKeys: LoadKeysFn\n loadInitialState: () => void\n}\n\n/**\n * Processes all join clauses, applying lazy loading optimizations and maintaining\n * alias tracking for per-alias subscriptions (enables self-joins).\n */\nexport function processJoins(\n pipeline: NamespacedAndKeyedStream,\n joinClauses: Array<JoinClause>,\n sources: Record<string, KeyedStream>,\n mainCollectionId: string,\n mainSource: string,\n allInputs: Record<string, KeyedStream>,\n cache: QueryCache,\n queryMapping: QueryMapping,\n collections: Record<string, Collection>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazySources: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n rawQuery: QueryIR,\n onCompileSubquery: CompileQueryFn,\n aliasToCollectionId: Record<string, string>,\n aliasRemapping: Record<string, string>,\n sourceWhereClauses: Map<string, BasicExpression<boolean>>,\n): NamespacedAndKeyedStream {\n let resultPipeline = pipeline\n\n for (const joinClause of joinClauses) {\n resultPipeline = processJoin(\n resultPipeline,\n joinClause,\n sources,\n mainCollectionId,\n mainSource,\n allInputs,\n cache,\n queryMapping,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n rawQuery,\n onCompileSubquery,\n aliasToCollectionId,\n aliasRemapping,\n sourceWhereClauses,\n )\n }\n\n return resultPipeline\n}\n\n/**\n * Processes a single join clause with lazy loading optimization.\n * For LEFT/RIGHT/INNER joins, marks one side as \"lazy\" (loads on-demand based on join keys).\n */\nfunction processJoin(\n pipeline: NamespacedAndKeyedStream,\n joinClause: JoinClause,\n sources: Record<string, KeyedStream>,\n mainCollectionId: string,\n mainSource: string,\n allInputs: Record<string, KeyedStream>,\n cache: QueryCache,\n queryMapping: QueryMapping,\n collections: Record<string, Collection>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazySources: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n rawQuery: QueryIR,\n onCompileSubquery: CompileQueryFn,\n aliasToCollectionId: Record<string, string>,\n aliasRemapping: Record<string, string>,\n sourceWhereClauses: Map<string, BasicExpression<boolean>>,\n): NamespacedAndKeyedStream {\n const isCollectionRef = joinClause.from.type === `collectionRef`\n\n // Get the joined source alias and input stream\n const {\n alias: joinedSource,\n input: joinedInput,\n collectionId: joinedCollectionId,\n } = processJoinSource(\n joinClause.from,\n allInputs,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n cache,\n queryMapping,\n onCompileSubquery,\n aliasToCollectionId,\n aliasRemapping,\n sourceWhereClauses,\n )\n\n // Add the joined source to the sources map\n sources[joinedSource] = joinedInput\n if (isCollectionRef) {\n // Only direct collection references form new alias bindings. Subquery\n // aliases reuse the mapping returned from the recursive compilation above.\n aliasToCollectionId[joinedSource] = joinedCollectionId\n }\n\n const mainCollection = collections[mainCollectionId]\n const joinedCollection = collections[joinedCollectionId]\n\n if (!mainCollection) {\n throw new JoinCollectionNotFoundError(mainCollectionId)\n }\n\n if (!joinedCollection) {\n throw new JoinCollectionNotFoundError(joinedCollectionId)\n }\n\n const { activeSource, lazySource } = getActiveAndLazySources(\n joinClause.type,\n mainCollection,\n joinedCollection,\n )\n\n // Analyze which source each expression refers to and swap if necessary\n const availableSources = Object.keys(sources)\n const { mainExpr, joinedExpr } = analyzeJoinExpressions(\n joinClause.left,\n joinClause.right,\n availableSources,\n joinedSource,\n )\n\n // Pre-compile the join expressions\n const compiledMainExpr = compileExpression(mainExpr)\n const compiledJoinedExpr = compileExpression(joinedExpr)\n\n // Prepare the main pipeline for joining\n let mainPipeline = pipeline.pipe(\n map(([currentKey, namespacedRow]) => {\n // Extract the join key from the main source expression\n const mainKey = compiledMainExpr(namespacedRow)\n\n // Return [joinKey, [originalKey, namespacedRow]]\n return [mainKey, [currentKey, namespacedRow]] as [\n unknown,\n [string, typeof namespacedRow],\n ]\n }),\n )\n\n // Prepare the joined pipeline\n let joinedPipeline = joinedInput.pipe(\n map(([currentKey, row]) => {\n // Wrap the row in a namespaced structure\n const namespacedRow: NamespacedRow = { [joinedSource]: row }\n\n // Extract the join key from the joined source expression\n const joinedKey = compiledJoinedExpr(namespacedRow)\n\n // Return [joinKey, [originalKey, namespacedRow]]\n return [joinedKey, [currentKey, namespacedRow]] as [\n unknown,\n [string, typeof namespacedRow],\n ]\n }),\n )\n\n // Apply the join operation\n if (![`inner`, `left`, `right`, `full`].includes(joinClause.type)) {\n throw new UnsupportedJoinTypeError(joinClause.type)\n }\n\n if (activeSource) {\n // If the lazy collection comes from a subquery that has a limit and/or an offset clause\n // then we need to deoptimize the join because we don't know which rows are in the result set\n // since we simply lookup matching keys in the index but the index contains all rows\n // (not just the ones that pass the limit and offset clauses)\n const lazyFrom = activeSource === `main` ? joinClause.from : rawQuery.from\n const limitedSubquery =\n lazyFrom.type === `queryRef` &&\n (lazyFrom.query.limit || lazyFrom.query.offset)\n\n // If join expressions contain computed values (like concat functions)\n // we don't optimize the join because we don't have an index over the computed values\n const hasComputedJoinExpr =\n mainExpr.type === `func` || joinedExpr.type === `func`\n\n if (!limitedSubquery && !hasComputedJoinExpr) {\n // This join can be optimized by having the active collection\n // dynamically load keys into the lazy collection\n // based on the value of the joinKey and by looking up\n // matching rows in the index of the lazy collection\n\n // Mark the lazy source alias as lazy\n // this Set is passed by the liveQueryCollection to the compiler\n // such that the liveQueryCollection can check it after compilation\n // to know which source aliases should load data lazily (not initially)\n const lazyAlias = activeSource === `main` ? joinedSource : mainSource\n lazySources.add(lazyAlias)\n\n const activePipeline =\n activeSource === `main` ? mainPipeline : joinedPipeline\n\n const lazySourceJoinExpr =\n activeSource === `main`\n ? (joinedExpr as PropRef)\n : (mainExpr as PropRef)\n\n const followRefResult = followRef(\n rawQuery,\n lazySourceJoinExpr,\n lazySource,\n )!\n const followRefCollection = followRefResult.collection\n\n const fieldName = followRefResult.path[0]\n if (fieldName) {\n ensureIndexForField(\n fieldName,\n followRefResult.path,\n followRefCollection,\n )\n }\n\n // Set up lazy loading: intercept active side's stream and dynamically load\n // matching rows from lazy side based on join keys.\n const activePipelineWithLoading: IStreamBuilder<\n [key: unknown, [originalKey: string, namespacedRow: NamespacedRow]]\n > = activePipeline.pipe(\n tap((data) => {\n // Find the subscription for lazy loading.\n // Subscriptions are keyed by the innermost alias (where the collection subscription\n // was actually created). For subqueries, the join alias may differ from the inner alias.\n // aliasRemapping provides a flattened one-hop lookup from outer → innermost alias.\n // Example: .join({ activeUser: subquery }) where subquery uses .from({ user: collection })\n // → aliasRemapping['activeUser'] = 'user' (always maps directly to innermost, never recursive)\n const resolvedAlias = aliasRemapping[lazyAlias] || lazyAlias\n const lazySourceSubscription = subscriptions[resolvedAlias]\n\n if (!lazySourceSubscription) {\n throw new SubscriptionNotFoundError(\n resolvedAlias,\n lazyAlias,\n lazySource.id,\n Object.keys(subscriptions),\n )\n }\n\n if (lazySourceSubscription.hasLoadedInitialState()) {\n // Entire state was already loaded because we deoptimized the join\n return\n }\n\n // Request filtered snapshot from lazy collection for matching join keys\n const joinKeys = data.getInner().map(([[joinKey]]) => joinKey)\n const lazyJoinRef = new PropRef(followRefResult.path)\n const loaded = lazySourceSubscription.requestSnapshot({\n where: inArray(lazyJoinRef, joinKeys),\n optimizedOnly: true,\n })\n\n if (!loaded) {\n // Snapshot wasn't sent because it could not be loaded from the indexes\n lazySourceSubscription.requestSnapshot()\n }\n }),\n )\n\n if (activeSource === `main`) {\n mainPipeline = activePipelineWithLoading\n } else {\n joinedPipeline = activePipelineWithLoading\n }\n }\n }\n\n return mainPipeline.pipe(\n joinOperator(joinedPipeline, joinClause.type as JoinType),\n processJoinResults(joinClause.type),\n )\n}\n\n/**\n * Analyzes join expressions to determine which refers to which source\n * and returns them in the correct order (available source expression first, joined source expression second)\n */\nfunction analyzeJoinExpressions(\n left: BasicExpression,\n right: BasicExpression,\n allAvailableSourceAliases: Array<string>,\n joinedSource: string,\n): { mainExpr: BasicExpression; joinedExpr: BasicExpression } {\n // Filter out the joined source alias from the available source aliases\n const availableSources = allAvailableSourceAliases.filter(\n (alias) => alias !== joinedSource,\n )\n\n const leftSourceAlias = getSourceAliasFromExpression(left)\n const rightSourceAlias = getSourceAliasFromExpression(right)\n\n // If left expression refers to an available source and right refers to joined source, keep as is\n if (\n leftSourceAlias &&\n availableSources.includes(leftSourceAlias) &&\n rightSourceAlias === joinedSource\n ) {\n return { mainExpr: left, joinedExpr: right }\n }\n\n // If left expression refers to joined source and right refers to an available source, swap them\n if (\n leftSourceAlias === joinedSource &&\n rightSourceAlias &&\n availableSources.includes(rightSourceAlias)\n ) {\n return { mainExpr: right, joinedExpr: left }\n }\n\n // If one expression doesn't refer to any source, this is an invalid join\n if (!leftSourceAlias || !rightSourceAlias) {\n throw new InvalidJoinConditionSourceMismatchError()\n }\n\n // If both expressions refer to the same alias, this is an invalid join\n if (leftSourceAlias === rightSourceAlias) {\n throw new InvalidJoinConditionSameSourceError(leftSourceAlias)\n }\n\n // Left side must refer to an available source\n // This cannot happen with the query builder as there is no way to build a ref\n // to an unavailable source, but just in case, but could happen with the IR\n if (!availableSources.includes(leftSourceAlias)) {\n throw new InvalidJoinConditionLeftSourceError(leftSourceAlias)\n }\n\n // Right side must refer to the joined source\n if (rightSourceAlias !== joinedSource) {\n throw new InvalidJoinConditionRightSourceError(joinedSource)\n }\n\n // This should not be reachable given the logic above, but just in case\n throw new InvalidJoinCondition()\n}\n\n/**\n * Extracts the source alias from a join expression\n */\nfunction getSourceAliasFromExpression(expr: BasicExpression): string | null {\n switch (expr.type) {\n case `ref`:\n // PropRef path has the source alias as the first element\n return expr.path[0] || null\n case `func`: {\n // For function expressions, we need to check if all arguments refer to the same source\n const sourceAliases = new Set<string>()\n for (const arg of expr.args) {\n const alias = getSourceAliasFromExpression(arg)\n if (alias) {\n sourceAliases.add(alias)\n }\n }\n // If all arguments refer to the same source, return that source alias\n return sourceAliases.size === 1 ? Array.from(sourceAliases)[0]! : null\n }\n default:\n // Values (type='val') don't reference any source\n return null\n }\n}\n\n/**\n * Processes the join source (collection or sub-query)\n */\nfunction processJoinSource(\n from: CollectionRef | QueryRef,\n allInputs: Record<string, KeyedStream>,\n collections: Record<string, Collection>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazySources: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n cache: QueryCache,\n queryMapping: QueryMapping,\n onCompileSubquery: CompileQueryFn,\n aliasToCollectionId: Record<string, string>,\n aliasRemapping: Record<string, string>,\n sourceWhereClauses: Map<string, BasicExpression<boolean>>,\n): { alias: string; input: KeyedStream; collectionId: string } {\n switch (from.type) {\n case `collectionRef`: {\n const input = allInputs[from.alias]\n if (!input) {\n throw new CollectionInputNotFoundError(\n from.alias,\n from.collection.id,\n Object.keys(allInputs),\n )\n }\n aliasToCollectionId[from.alias] = from.collection.id\n return { alias: from.alias, input, collectionId: from.collection.id }\n }\n case `queryRef`: {\n // Find the original query for caching purposes\n const originalQuery = queryMapping.get(from.query) || from.query\n\n // Recursively compile the sub-query with cache\n const subQueryResult = onCompileSubquery(\n originalQuery,\n allInputs,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n cache,\n queryMapping,\n )\n\n // Pull up alias mappings from subquery to parent scope.\n // This includes both the innermost alias-to-collection mappings AND\n // any existing remappings from nested subquery levels.\n Object.assign(aliasToCollectionId, subQueryResult.aliasToCollectionId)\n Object.assign(aliasRemapping, subQueryResult.aliasRemapping)\n\n // Pull up source WHERE clauses from subquery to parent scope.\n // This enables loadSubset to receive the correct where clauses for subquery collections.\n //\n // IMPORTANT: Skip pull-up for optimizer-created subqueries. These are detected when:\n // 1. The outer alias (from.alias) matches the inner alias (from.query.from.alias)\n // 2. The subquery was found in queryMapping (it's a user-defined subquery, not optimizer-created)\n //\n // For optimizer-created subqueries, the parent already has the sourceWhereClauses\n // extracted from the original raw query, so pulling up would be redundant.\n const isUserDefinedSubquery = queryMapping.has(from.query)\n const fromInnerAlias = from.query.from.alias\n const isOptimizerCreated =\n !isUserDefinedSubquery && from.alias === fromInnerAlias\n\n if (!isOptimizerCreated) {\n for (const [alias, whereClause] of subQueryResult.sourceWhereClauses) {\n sourceWhereClauses.set(alias, whereClause)\n }\n }\n\n // Create a flattened remapping from outer alias to innermost alias.\n // For nested subqueries, this ensures one-hop lookups (not recursive chains).\n //\n // Example with 3-level nesting:\n // Inner: .from({ user: usersCollection })\n // Middle: .from({ activeUser: innerSubquery }) → creates: activeUser → user\n // Outer: .join({ author: middleSubquery }, ...) → creates: author → user (not author → activeUser)\n //\n // We search through the PULLED-UP aliasToCollectionId (which contains the\n // innermost 'user' alias), so we always map directly to the deepest level.\n // This means aliasRemapping[lazyAlias] is always a single lookup, never recursive.\n const innerAlias = Object.keys(subQueryResult.aliasToCollectionId).find(\n (alias) =>\n subQueryResult.aliasToCollectionId[alias] ===\n subQueryResult.collectionId,\n )\n if (innerAlias && innerAlias !== from.alias) {\n aliasRemapping[from.alias] = innerAlias\n }\n\n // Extract the pipeline from the compilation result\n const subQueryInput = subQueryResult.pipeline\n\n // Subqueries may return [key, [value, orderByIndex]] (with ORDER BY) or [key, value] (without ORDER BY)\n // We need to extract just the value for use in parent queries\n const extractedInput = subQueryInput.pipe(\n map((data: any) => {\n const [key, [value, _orderByIndex]] = data\n return [key, value] as [unknown, any]\n }),\n )\n\n return {\n alias: from.alias,\n input: extractedInput as KeyedStream,\n collectionId: subQueryResult.collectionId,\n }\n }\n default:\n throw new UnsupportedJoinSourceTypeError((from as any).type)\n }\n}\n\n/**\n * Processes the results of a join operation\n */\nfunction processJoinResults(joinType: string) {\n return function (\n pipeline: IStreamBuilder<\n [\n key: string,\n [\n [string, NamespacedRow] | undefined,\n [string, NamespacedRow] | undefined,\n ],\n ]\n >,\n ): NamespacedAndKeyedStream {\n return pipeline.pipe(\n // Process the join result and handle nulls\n filter((result) => {\n const [_key, [main, joined]] = result\n const mainNamespacedRow = main?.[1]\n const joinedNamespacedRow = joined?.[1]\n\n // Handle different join types\n if (joinType === `inner`) {\n return !!(mainNamespacedRow && joinedNamespacedRow)\n }\n\n if (joinType === `left`) {\n return !!mainNamespacedRow\n }\n\n if (joinType === `right`) {\n return !!joinedNamespacedRow\n }\n\n // For full joins, always include\n return true\n }),\n map((result) => {\n const [_key, [main, joined]] = result\n const mainKey = main?.[0]\n const mainNamespacedRow = main?.[1]\n const joinedKey = joined?.[0]\n const joinedNamespacedRow = joined?.[1]\n\n // Merge the namespaced rows\n const mergedNamespacedRow: NamespacedRow = {}\n\n // Add main row data if it exists\n if (mainNamespacedRow) {\n Object.assign(mergedNamespacedRow, mainNamespacedRow)\n }\n\n // Add joined row data if it exists\n if (joinedNamespacedRow) {\n Object.assign(mergedNamespacedRow, joinedNamespacedRow)\n }\n\n // We create a composite key that combines the main and joined keys\n const resultKey = `[${mainKey},${joinedKey}]`\n\n return [resultKey, mergedNamespacedRow] as [string, NamespacedRow]\n }),\n )\n }\n}\n\n/**\n * Returns the active and lazy collections for a join clause.\n * The active collection is the one that we need to fully iterate over\n * and it can be the main source (i.e. left collection) or the joined source (i.e. right collection).\n * The lazy collection is the one that we should join-in lazily based on matches in the active collection.\n * @param joinClause - The join clause to analyze\n * @param leftCollection - The left collection\n * @param rightCollection - The right collection\n * @returns The active and lazy collections. They are undefined if we need to loop over both collections (i.e. both are active)\n */\nfunction getActiveAndLazySources(\n joinType: JoinClause[`type`],\n leftCollection: Collection,\n rightCollection: Collection,\n):\n | { activeSource: `main` | `joined`; lazySource: Collection }\n | { activeSource: undefined; lazySource: undefined } {\n // Self-joins can now be optimized since we track lazy loading by source alias\n // rather than collection ID. Each alias has its own subscription and lazy state.\n\n switch (joinType) {\n case `left`:\n return { activeSource: `main`, lazySource: rightCollection }\n case `right`:\n return { activeSource: `joined`, lazySource: leftCollection }\n case `inner`:\n // The smallest collection should be the active collection\n // and the biggest collection should be lazy\n return leftCollection.size < rightCollection.size\n ? { activeSource: `main`, lazySource: rightCollection }\n : { activeSource: `joined`, lazySource: leftCollection }\n default:\n return { activeSource: undefined, lazySource: undefined }\n }\n}\n"],"names":["JoinCollectionNotFoundError","compileExpression","map","UnsupportedJoinTypeError","followRef","ensureIndexForField","tap","SubscriptionNotFoundError","PropRef","inArray","joinOperator","InvalidJoinConditionSourceMismatchError","InvalidJoinConditionSameSourceError","InvalidJoinConditionLeftSourceError","InvalidJoinConditionRightSourceError","InvalidJoinCondition","CollectionInputNotFoundError","UnsupportedJoinSourceTypeError","filter"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CompileQueryFn } from './index.js';
|
|
2
2
|
import { OrderByOptimizationInfo } from './order-by.js';
|
|
3
|
-
import { JoinClause, QueryIR } from '../ir.js';
|
|
3
|
+
import { BasicExpression, JoinClause, QueryIR } from '../ir.js';
|
|
4
4
|
import { Collection } from '../../collection/index.js';
|
|
5
5
|
import { KeyedStream, NamespacedAndKeyedStream } from '../../types.js';
|
|
6
6
|
import { QueryCache, QueryMapping, WindowOptions } from './types.js';
|
|
@@ -16,4 +16,4 @@ export type LazyCollectionCallbacks = {
|
|
|
16
16
|
* Processes all join clauses, applying lazy loading optimizations and maintaining
|
|
17
17
|
* alias tracking for per-alias subscriptions (enables self-joins).
|
|
18
18
|
*/
|
|
19
|
-
export declare function processJoins(pipeline: NamespacedAndKeyedStream, joinClauses: Array<JoinClause>, sources: Record<string, KeyedStream>, mainCollectionId: string, mainSource: string, allInputs: Record<string, KeyedStream>, cache: QueryCache, queryMapping: QueryMapping, collections: Record<string, Collection>, subscriptions: Record<string, CollectionSubscription>, callbacks: Record<string, LazyCollectionCallbacks>, lazySources: Set<string>, optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>, setWindowFn: (windowFn: (options: WindowOptions) => void) => void, rawQuery: QueryIR, onCompileSubquery: CompileQueryFn, aliasToCollectionId: Record<string, string>, aliasRemapping: Record<string, string
|
|
19
|
+
export declare function processJoins(pipeline: NamespacedAndKeyedStream, joinClauses: Array<JoinClause>, sources: Record<string, KeyedStream>, mainCollectionId: string, mainSource: string, allInputs: Record<string, KeyedStream>, cache: QueryCache, queryMapping: QueryMapping, collections: Record<string, Collection>, subscriptions: Record<string, CollectionSubscription>, callbacks: Record<string, LazyCollectionCallbacks>, lazySources: Set<string>, optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>, setWindowFn: (windowFn: (options: WindowOptions) => void) => void, rawQuery: QueryIR, onCompileSubquery: CompileQueryFn, aliasToCollectionId: Record<string, string>, aliasRemapping: Record<string, string>, sourceWhereClauses: Map<string, BasicExpression<boolean>>): NamespacedAndKeyedStream;
|
|
@@ -108,7 +108,7 @@ class CollectionLifecycleManager {
|
|
|
108
108
|
clearTimeout(this.gcTimeoutId);
|
|
109
109
|
}
|
|
110
110
|
const gcTime = this.config.gcTime ?? 3e5;
|
|
111
|
-
if (gcTime
|
|
111
|
+
if (gcTime <= 0 || !Number.isFinite(gcTime)) {
|
|
112
112
|
return;
|
|
113
113
|
}
|
|
114
114
|
this.gcTimeoutId = setTimeout(() => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.js","sources":["../../../src/collection/lifecycle.ts"],"sourcesContent":["import {\n CollectionInErrorStateError,\n CollectionStateError,\n InvalidCollectionStatusTransitionError,\n} from '../errors'\nimport {\n safeCancelIdleCallback,\n safeRequestIdleCallback,\n} from '../utils/browser-polyfills'\nimport type { IdleCallbackDeadline } from '../utils/browser-polyfills'\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type { CollectionConfig, CollectionStatus } from '../types'\nimport type { CollectionEventsManager } from './events'\nimport type { CollectionIndexesManager } from './indexes'\nimport type { CollectionChangesManager } from './changes'\nimport type { CollectionSyncManager } from './sync'\nimport type { CollectionStateManager } from './state'\n\nexport class CollectionLifecycleManager<\n TOutput extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TSchema extends StandardSchemaV1 = StandardSchemaV1,\n TInput extends object = TOutput,\n> {\n private config: CollectionConfig<TOutput, TKey, TSchema>\n private id: string\n private indexes!: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>\n private events!: CollectionEventsManager\n private changes!: CollectionChangesManager<TOutput, TKey, TSchema, TInput>\n private sync!: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n private state!: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n\n public status: CollectionStatus = `idle`\n public hasBeenReady = false\n public hasReceivedFirstCommit = false\n public onFirstReadyCallbacks: Array<() => void> = []\n public gcTimeoutId: ReturnType<typeof setTimeout> | null = null\n private idleCallbackId: number | null = null\n\n /**\n * Creates a new CollectionLifecycleManager instance\n */\n constructor(config: CollectionConfig<TOutput, TKey, TSchema>, id: string) {\n this.config = config\n this.id = id\n }\n\n setDeps(deps: {\n indexes: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>\n events: CollectionEventsManager\n changes: CollectionChangesManager<TOutput, TKey, TSchema, TInput>\n sync: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n state: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n }) {\n this.indexes = deps.indexes\n this.events = deps.events\n this.changes = deps.changes\n this.sync = deps.sync\n this.state = deps.state\n }\n\n /**\n * Validates state transitions to prevent invalid status changes\n */\n public validateStatusTransition(\n from: CollectionStatus,\n to: CollectionStatus,\n ): void {\n if (from === to) {\n // Allow same state transitions\n return\n }\n const validTransitions: Record<\n CollectionStatus,\n Array<CollectionStatus>\n > = {\n idle: [`loading`, `error`, `cleaned-up`],\n loading: [`ready`, `error`, `cleaned-up`],\n ready: [`cleaned-up`, `error`],\n error: [`cleaned-up`, `idle`],\n 'cleaned-up': [`loading`, `error`],\n }\n\n if (!validTransitions[from].includes(to)) {\n throw new InvalidCollectionStatusTransitionError(from, to, this.id)\n }\n }\n\n /**\n * Safely update the collection status with validation\n * @private\n */\n public setStatus(\n newStatus: CollectionStatus,\n allowReady: boolean = false,\n ): void {\n if (newStatus === `ready` && !allowReady) {\n // setStatus('ready') is an internal method that should not be called directly\n // Instead, use markReady to transition to ready triggering the necessary events\n // and side effects.\n throw new CollectionStateError(\n `You can't directly call \"setStatus('ready'). You must use markReady instead.`,\n )\n }\n this.validateStatusTransition(this.status, newStatus)\n const previousStatus = this.status\n this.status = newStatus\n\n // Resolve indexes when collection becomes ready\n if (newStatus === `ready` && !this.indexes.isIndexesResolved) {\n // Resolve indexes asynchronously without blocking\n this.indexes.resolveAllIndexes().catch((error) => {\n console.warn(\n `${this.config.id ? `[${this.config.id}] ` : ``}Failed to resolve indexes:`,\n error,\n )\n })\n }\n\n // Emit event\n this.events.emitStatusChange(newStatus, previousStatus)\n }\n\n /**\n * Validates that the collection is in a usable state for data operations\n * @private\n */\n public validateCollectionUsable(operation: string): void {\n switch (this.status) {\n case `error`:\n throw new CollectionInErrorStateError(operation, this.id)\n case `cleaned-up`:\n // Automatically restart the collection when operations are called on cleaned-up collections\n this.sync.startSync()\n break\n }\n }\n\n /**\n * Mark the collection as ready for use\n * This is called by sync implementations to explicitly signal that the collection is ready,\n * providing a more intuitive alternative to using commits for readiness signaling\n * @private - Should only be called by sync implementations\n */\n public markReady(): void {\n this.validateStatusTransition(this.status, `ready`)\n // Can transition to ready from loading state\n if (this.status === `loading`) {\n this.setStatus(`ready`, true)\n\n // Call any registered first ready callbacks (only on first time becoming ready)\n if (!this.hasBeenReady) {\n this.hasBeenReady = true\n\n // Also mark as having received first commit for backwards compatibility\n if (!this.hasReceivedFirstCommit) {\n this.hasReceivedFirstCommit = true\n }\n\n const callbacks = [...this.onFirstReadyCallbacks]\n this.onFirstReadyCallbacks = []\n callbacks.forEach((callback) => callback())\n }\n // Notify dependents when markReady is called, after status is set\n // This ensures live queries get notified when their dependencies become ready\n if (this.changes.changeSubscriptions.size > 0) {\n this.changes.emitEmptyReadyEvent()\n }\n }\n }\n\n /**\n * Start the garbage collection timer\n * Called when the collection becomes inactive (no subscribers)\n */\n public startGCTimer(): void {\n if (this.gcTimeoutId) {\n clearTimeout(this.gcTimeoutId)\n }\n\n const gcTime = this.config.gcTime ?? 300000 // 5 minutes default\n\n // If gcTime is 0, GC is disabled\n if (gcTime === 0) {\n return\n }\n\n this.gcTimeoutId = setTimeout(() => {\n if (this.changes.activeSubscribersCount === 0) {\n // Schedule cleanup during idle time to avoid blocking the UI thread\n this.scheduleIdleCleanup()\n }\n }, gcTime)\n }\n\n /**\n * Cancel the garbage collection timer\n * Called when the collection becomes active again\n */\n public cancelGCTimer(): void {\n if (this.gcTimeoutId) {\n clearTimeout(this.gcTimeoutId)\n this.gcTimeoutId = null\n }\n // Also cancel any pending idle cleanup\n if (this.idleCallbackId !== null) {\n safeCancelIdleCallback(this.idleCallbackId)\n this.idleCallbackId = null\n }\n }\n\n /**\n * Schedule cleanup to run during browser idle time\n * This prevents blocking the UI thread during cleanup operations\n */\n private scheduleIdleCleanup(): void {\n // Cancel any existing idle callback\n if (this.idleCallbackId !== null) {\n safeCancelIdleCallback(this.idleCallbackId)\n }\n\n // Schedule cleanup with a timeout of 1 second\n // This ensures cleanup happens even if the browser is busy\n this.idleCallbackId = safeRequestIdleCallback(\n (deadline) => {\n // Perform cleanup if we still have no subscribers\n if (this.changes.activeSubscribersCount === 0) {\n const cleanupCompleted = this.performCleanup(deadline)\n // Only clear the callback ID if cleanup actually completed\n if (cleanupCompleted) {\n this.idleCallbackId = null\n }\n } else {\n // No need to cleanup, clear the callback ID\n this.idleCallbackId = null\n }\n },\n { timeout: 1000 },\n )\n }\n\n /**\n * Perform cleanup operations, optionally in chunks during idle time\n * @returns true if cleanup was completed, false if it was rescheduled\n */\n private performCleanup(deadline?: IdleCallbackDeadline): boolean {\n // If we have a deadline, we can potentially split cleanup into chunks\n // For now, we'll do all cleanup at once but check if we have time\n const hasTime =\n !deadline || deadline.timeRemaining() > 0 || deadline.didTimeout\n\n if (hasTime) {\n // Perform all cleanup operations except events\n this.sync.cleanup()\n this.state.cleanup()\n this.changes.cleanup()\n this.indexes.cleanup()\n\n if (this.gcTimeoutId) {\n clearTimeout(this.gcTimeoutId)\n this.gcTimeoutId = null\n }\n\n this.hasBeenReady = false\n this.onFirstReadyCallbacks = []\n\n // Set status to cleaned-up after everything is cleaned up\n // This fires the status:change event to notify listeners\n this.setStatus(`cleaned-up`)\n\n // Finally, cleanup event handlers after the event has been fired\n this.events.cleanup()\n\n return true\n } else {\n // If we don't have time, reschedule for the next idle period\n this.scheduleIdleCleanup()\n return false\n }\n }\n\n /**\n * Register a callback to be executed when the collection first becomes ready\n * Useful for preloading collections\n * @param callback Function to call when the collection first becomes ready\n */\n public onFirstReady(callback: () => void): void {\n // If already ready, call immediately\n if (this.hasBeenReady) {\n callback()\n return\n }\n\n this.onFirstReadyCallbacks.push(callback)\n }\n\n public cleanup(): void {\n // Cancel any pending idle cleanup\n if (this.idleCallbackId !== null) {\n safeCancelIdleCallback(this.idleCallbackId)\n this.idleCallbackId = null\n }\n\n // Perform cleanup immediately (used when explicitly called)\n this.performCleanup()\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"lifecycle.js","sources":["../../../src/collection/lifecycle.ts"],"sourcesContent":["import {\n CollectionInErrorStateError,\n CollectionStateError,\n InvalidCollectionStatusTransitionError,\n} from '../errors'\nimport {\n safeCancelIdleCallback,\n safeRequestIdleCallback,\n} from '../utils/browser-polyfills'\nimport type { IdleCallbackDeadline } from '../utils/browser-polyfills'\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type { CollectionConfig, CollectionStatus } from '../types'\nimport type { CollectionEventsManager } from './events'\nimport type { CollectionIndexesManager } from './indexes'\nimport type { CollectionChangesManager } from './changes'\nimport type { CollectionSyncManager } from './sync'\nimport type { CollectionStateManager } from './state'\n\nexport class CollectionLifecycleManager<\n TOutput extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TSchema extends StandardSchemaV1 = StandardSchemaV1,\n TInput extends object = TOutput,\n> {\n private config: CollectionConfig<TOutput, TKey, TSchema>\n private id: string\n private indexes!: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>\n private events!: CollectionEventsManager\n private changes!: CollectionChangesManager<TOutput, TKey, TSchema, TInput>\n private sync!: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n private state!: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n\n public status: CollectionStatus = `idle`\n public hasBeenReady = false\n public hasReceivedFirstCommit = false\n public onFirstReadyCallbacks: Array<() => void> = []\n public gcTimeoutId: ReturnType<typeof setTimeout> | null = null\n private idleCallbackId: number | null = null\n\n /**\n * Creates a new CollectionLifecycleManager instance\n */\n constructor(config: CollectionConfig<TOutput, TKey, TSchema>, id: string) {\n this.config = config\n this.id = id\n }\n\n setDeps(deps: {\n indexes: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>\n events: CollectionEventsManager\n changes: CollectionChangesManager<TOutput, TKey, TSchema, TInput>\n sync: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n state: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n }) {\n this.indexes = deps.indexes\n this.events = deps.events\n this.changes = deps.changes\n this.sync = deps.sync\n this.state = deps.state\n }\n\n /**\n * Validates state transitions to prevent invalid status changes\n */\n public validateStatusTransition(\n from: CollectionStatus,\n to: CollectionStatus,\n ): void {\n if (from === to) {\n // Allow same state transitions\n return\n }\n const validTransitions: Record<\n CollectionStatus,\n Array<CollectionStatus>\n > = {\n idle: [`loading`, `error`, `cleaned-up`],\n loading: [`ready`, `error`, `cleaned-up`],\n ready: [`cleaned-up`, `error`],\n error: [`cleaned-up`, `idle`],\n 'cleaned-up': [`loading`, `error`],\n }\n\n if (!validTransitions[from].includes(to)) {\n throw new InvalidCollectionStatusTransitionError(from, to, this.id)\n }\n }\n\n /**\n * Safely update the collection status with validation\n * @private\n */\n public setStatus(\n newStatus: CollectionStatus,\n allowReady: boolean = false,\n ): void {\n if (newStatus === `ready` && !allowReady) {\n // setStatus('ready') is an internal method that should not be called directly\n // Instead, use markReady to transition to ready triggering the necessary events\n // and side effects.\n throw new CollectionStateError(\n `You can't directly call \"setStatus('ready'). You must use markReady instead.`,\n )\n }\n this.validateStatusTransition(this.status, newStatus)\n const previousStatus = this.status\n this.status = newStatus\n\n // Resolve indexes when collection becomes ready\n if (newStatus === `ready` && !this.indexes.isIndexesResolved) {\n // Resolve indexes asynchronously without blocking\n this.indexes.resolveAllIndexes().catch((error) => {\n console.warn(\n `${this.config.id ? `[${this.config.id}] ` : ``}Failed to resolve indexes:`,\n error,\n )\n })\n }\n\n // Emit event\n this.events.emitStatusChange(newStatus, previousStatus)\n }\n\n /**\n * Validates that the collection is in a usable state for data operations\n * @private\n */\n public validateCollectionUsable(operation: string): void {\n switch (this.status) {\n case `error`:\n throw new CollectionInErrorStateError(operation, this.id)\n case `cleaned-up`:\n // Automatically restart the collection when operations are called on cleaned-up collections\n this.sync.startSync()\n break\n }\n }\n\n /**\n * Mark the collection as ready for use\n * This is called by sync implementations to explicitly signal that the collection is ready,\n * providing a more intuitive alternative to using commits for readiness signaling\n * @private - Should only be called by sync implementations\n */\n public markReady(): void {\n this.validateStatusTransition(this.status, `ready`)\n // Can transition to ready from loading state\n if (this.status === `loading`) {\n this.setStatus(`ready`, true)\n\n // Call any registered first ready callbacks (only on first time becoming ready)\n if (!this.hasBeenReady) {\n this.hasBeenReady = true\n\n // Also mark as having received first commit for backwards compatibility\n if (!this.hasReceivedFirstCommit) {\n this.hasReceivedFirstCommit = true\n }\n\n const callbacks = [...this.onFirstReadyCallbacks]\n this.onFirstReadyCallbacks = []\n callbacks.forEach((callback) => callback())\n }\n // Notify dependents when markReady is called, after status is set\n // This ensures live queries get notified when their dependencies become ready\n if (this.changes.changeSubscriptions.size > 0) {\n this.changes.emitEmptyReadyEvent()\n }\n }\n }\n\n /**\n * Start the garbage collection timer\n * Called when the collection becomes inactive (no subscribers)\n */\n public startGCTimer(): void {\n if (this.gcTimeoutId) {\n clearTimeout(this.gcTimeoutId)\n }\n\n const gcTime = this.config.gcTime ?? 300000 // 5 minutes default\n\n // If gcTime is 0, negative, or non-finite (Infinity, -Infinity, NaN), GC is disabled.\n // Note: setTimeout with Infinity coerces to 0 via ToInt32, causing immediate GC,\n // so we must explicitly check for non-finite values here.\n if (gcTime <= 0 || !Number.isFinite(gcTime)) {\n return\n }\n\n this.gcTimeoutId = setTimeout(() => {\n if (this.changes.activeSubscribersCount === 0) {\n // Schedule cleanup during idle time to avoid blocking the UI thread\n this.scheduleIdleCleanup()\n }\n }, gcTime)\n }\n\n /**\n * Cancel the garbage collection timer\n * Called when the collection becomes active again\n */\n public cancelGCTimer(): void {\n if (this.gcTimeoutId) {\n clearTimeout(this.gcTimeoutId)\n this.gcTimeoutId = null\n }\n // Also cancel any pending idle cleanup\n if (this.idleCallbackId !== null) {\n safeCancelIdleCallback(this.idleCallbackId)\n this.idleCallbackId = null\n }\n }\n\n /**\n * Schedule cleanup to run during browser idle time\n * This prevents blocking the UI thread during cleanup operations\n */\n private scheduleIdleCleanup(): void {\n // Cancel any existing idle callback\n if (this.idleCallbackId !== null) {\n safeCancelIdleCallback(this.idleCallbackId)\n }\n\n // Schedule cleanup with a timeout of 1 second\n // This ensures cleanup happens even if the browser is busy\n this.idleCallbackId = safeRequestIdleCallback(\n (deadline) => {\n // Perform cleanup if we still have no subscribers\n if (this.changes.activeSubscribersCount === 0) {\n const cleanupCompleted = this.performCleanup(deadline)\n // Only clear the callback ID if cleanup actually completed\n if (cleanupCompleted) {\n this.idleCallbackId = null\n }\n } else {\n // No need to cleanup, clear the callback ID\n this.idleCallbackId = null\n }\n },\n { timeout: 1000 },\n )\n }\n\n /**\n * Perform cleanup operations, optionally in chunks during idle time\n * @returns true if cleanup was completed, false if it was rescheduled\n */\n private performCleanup(deadline?: IdleCallbackDeadline): boolean {\n // If we have a deadline, we can potentially split cleanup into chunks\n // For now, we'll do all cleanup at once but check if we have time\n const hasTime =\n !deadline || deadline.timeRemaining() > 0 || deadline.didTimeout\n\n if (hasTime) {\n // Perform all cleanup operations except events\n this.sync.cleanup()\n this.state.cleanup()\n this.changes.cleanup()\n this.indexes.cleanup()\n\n if (this.gcTimeoutId) {\n clearTimeout(this.gcTimeoutId)\n this.gcTimeoutId = null\n }\n\n this.hasBeenReady = false\n this.onFirstReadyCallbacks = []\n\n // Set status to cleaned-up after everything is cleaned up\n // This fires the status:change event to notify listeners\n this.setStatus(`cleaned-up`)\n\n // Finally, cleanup event handlers after the event has been fired\n this.events.cleanup()\n\n return true\n } else {\n // If we don't have time, reschedule for the next idle period\n this.scheduleIdleCleanup()\n return false\n }\n }\n\n /**\n * Register a callback to be executed when the collection first becomes ready\n * Useful for preloading collections\n * @param callback Function to call when the collection first becomes ready\n */\n public onFirstReady(callback: () => void): void {\n // If already ready, call immediately\n if (this.hasBeenReady) {\n callback()\n return\n }\n\n this.onFirstReadyCallbacks.push(callback)\n }\n\n public cleanup(): void {\n // Cancel any pending idle cleanup\n if (this.idleCallbackId !== null) {\n safeCancelIdleCallback(this.idleCallbackId)\n this.idleCallbackId = null\n }\n\n // Perform cleanup immediately (used when explicitly called)\n this.performCleanup()\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -36,7 +36,8 @@ function compileQuery(rawQuery, inputs, collections, subscriptions, callbacks, l
|
|
|
36
36
|
cache,
|
|
37
37
|
queryMapping,
|
|
38
38
|
aliasToCollectionId,
|
|
39
|
-
aliasRemapping
|
|
39
|
+
aliasRemapping,
|
|
40
|
+
sourceWhereClauses
|
|
40
41
|
);
|
|
41
42
|
sources[mainSource] = mainInput;
|
|
42
43
|
let pipeline = mainInput.pipe(
|
|
@@ -64,7 +65,8 @@ function compileQuery(rawQuery, inputs, collections, subscriptions, callbacks, l
|
|
|
64
65
|
rawQuery,
|
|
65
66
|
compileQuery,
|
|
66
67
|
aliasToCollectionId,
|
|
67
|
-
aliasRemapping
|
|
68
|
+
aliasRemapping,
|
|
69
|
+
sourceWhereClauses
|
|
68
70
|
);
|
|
69
71
|
}
|
|
70
72
|
if (query.where && query.where.length > 0) {
|
|
@@ -249,7 +251,7 @@ function validateQueryStructure(query, parentCollectionAliases = /* @__PURE__ */
|
|
|
249
251
|
}
|
|
250
252
|
}
|
|
251
253
|
}
|
|
252
|
-
function processFrom(from, allInputs, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, cache, queryMapping, aliasToCollectionId, aliasRemapping) {
|
|
254
|
+
function processFrom(from, allInputs, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, cache, queryMapping, aliasToCollectionId, aliasRemapping, sourceWhereClauses) {
|
|
253
255
|
switch (from.type) {
|
|
254
256
|
case `collectionRef`: {
|
|
255
257
|
const input = allInputs[from.alias];
|
|
@@ -279,6 +281,14 @@ function processFrom(from, allInputs, collections, subscriptions, callbacks, laz
|
|
|
279
281
|
);
|
|
280
282
|
Object.assign(aliasToCollectionId, subQueryResult.aliasToCollectionId);
|
|
281
283
|
Object.assign(aliasRemapping, subQueryResult.aliasRemapping);
|
|
284
|
+
const isUserDefinedSubquery = queryMapping.has(from.query);
|
|
285
|
+
const subqueryFromAlias = from.query.from.alias;
|
|
286
|
+
const isOptimizerCreated = !isUserDefinedSubquery && from.alias === subqueryFromAlias;
|
|
287
|
+
if (!isOptimizerCreated) {
|
|
288
|
+
for (const [alias, whereClause] of subQueryResult.sourceWhereClauses) {
|
|
289
|
+
sourceWhereClauses.set(alias, whereClause);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
282
292
|
const innerAlias = Object.keys(subQueryResult.aliasToCollectionId).find(
|
|
283
293
|
(alias) => subQueryResult.aliasToCollectionId[alias] === subQueryResult.collectionId
|
|
284
294
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../../../src/query/compiler/index.ts"],"sourcesContent":["import { distinct, filter, map } from '@tanstack/db-ivm'\nimport { optimizeQuery } from '../optimizer.js'\nimport {\n CollectionInputNotFoundError,\n DistinctRequiresSelectError,\n DuplicateAliasInSubqueryError,\n HavingRequiresGroupByError,\n LimitOffsetRequireOrderByError,\n UnsupportedFromTypeError,\n} from '../../errors.js'\nimport { PropRef, Value as ValClass, getWhereExpression } from '../ir.js'\nimport { compileExpression, toBooleanPredicate } from './evaluators.js'\nimport { processJoins } from './joins.js'\nimport { processGroupBy } from './group-by.js'\nimport { processOrderBy } from './order-by.js'\nimport { processSelect } from './select.js'\nimport type { CollectionSubscription } from '../../collection/subscription.js'\nimport type { OrderByOptimizationInfo } from './order-by.js'\nimport type {\n BasicExpression,\n CollectionRef,\n QueryIR,\n QueryRef,\n} from '../ir.js'\nimport type { LazyCollectionCallbacks } from './joins.js'\nimport type { Collection } from '../../collection/index.js'\nimport type {\n KeyedStream,\n NamespacedAndKeyedStream,\n ResultStream,\n} from '../../types.js'\nimport type { QueryCache, QueryMapping, WindowOptions } from './types.js'\n\nexport type { WindowOptions } from './types.js'\n\n/**\n * Result of query compilation including both the pipeline and source-specific WHERE clauses\n */\nexport interface CompilationResult {\n /** The ID of the main collection */\n collectionId: string\n\n /** The compiled query pipeline (D2 stream) */\n pipeline: ResultStream\n\n /** Map of source aliases to their WHERE clauses for index optimization */\n sourceWhereClauses: Map<string, BasicExpression<boolean>>\n\n /**\n * Maps each source alias to its collection ID. Enables per-alias subscriptions for self-joins.\n * Example: `{ employee: 'employees-col-id', manager: 'employees-col-id' }`\n */\n aliasToCollectionId: Record<string, string>\n\n /**\n * Flattened mapping from outer alias to innermost alias for subqueries.\n * Always provides one-hop lookups, never recursive chains.\n *\n * Example: `{ activeUser: 'user' }` when `.from({ activeUser: subquery })`\n * where the subquery uses `.from({ user: collection })`.\n *\n * For deeply nested subqueries, the mapping goes directly to the innermost alias:\n * `{ author: 'user' }` (not `{ author: 'activeUser' }`), so `aliasRemapping[alias]`\n * always resolves in a single lookup.\n *\n * Used to resolve subscriptions during lazy loading when join aliases differ from\n * the inner aliases where collection subscriptions were created.\n */\n aliasRemapping: Record<string, string>\n}\n\n/**\n * Compiles a query IR into a D2 pipeline\n * @param rawQuery The query IR to compile\n * @param inputs Mapping of source aliases to input streams (e.g., `{ employee: input1, manager: input2 }`)\n * @param collections Mapping of collection IDs to Collection instances\n * @param subscriptions Mapping of source aliases to CollectionSubscription instances\n * @param callbacks Mapping of source aliases to lazy loading callbacks\n * @param lazySources Set of source aliases that should load data lazily\n * @param optimizableOrderByCollections Map of collection IDs to order-by optimization info\n * @param cache Optional cache for compiled subqueries (used internally for recursion)\n * @param queryMapping Optional mapping from optimized queries to original queries\n * @returns A CompilationResult with the pipeline, source WHERE clauses, and alias metadata\n */\nexport function compileQuery(\n rawQuery: QueryIR,\n inputs: Record<string, KeyedStream>,\n collections: Record<string, Collection<any, any, any, any, any>>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazySources: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n cache: QueryCache = new WeakMap(),\n queryMapping: QueryMapping = new WeakMap(),\n): CompilationResult {\n // Check if the original raw query has already been compiled\n const cachedResult = cache.get(rawQuery)\n if (cachedResult) {\n return cachedResult\n }\n\n // Validate the raw query BEFORE optimization to check user's original structure.\n // This must happen before optimization because the optimizer may create internal\n // subqueries (e.g., for predicate pushdown) that reuse aliases, which is fine.\n validateQueryStructure(rawQuery)\n\n // Optimize the query before compilation\n const { optimizedQuery: query, sourceWhereClauses } = optimizeQuery(rawQuery)\n\n // Create mapping from optimized query to original for caching\n queryMapping.set(query, rawQuery)\n mapNestedQueries(query, rawQuery, queryMapping)\n\n // Create a copy of the inputs map to avoid modifying the original\n const allInputs = { ...inputs }\n\n // Track alias to collection id relationships discovered during compilation.\n // This includes all user-declared aliases plus inner aliases from subqueries.\n const aliasToCollectionId: Record<string, string> = {}\n\n // Track alias remapping for subqueries (outer alias → inner alias)\n // e.g., when .join({ activeUser: subquery }) where subquery uses .from({ user: collection })\n // we store: aliasRemapping['activeUser'] = 'user'\n const aliasRemapping: Record<string, string> = {}\n\n // Create a map of source aliases to input streams.\n // Inputs MUST be keyed by alias (e.g., `{ employee: input1, manager: input2 }`),\n // not by collection ID. This enables per-alias subscriptions where different aliases\n // of the same collection (e.g., self-joins) maintain independent filtered streams.\n const sources: Record<string, KeyedStream> = {}\n\n // Process the FROM clause to get the main source\n const {\n alias: mainSource,\n input: mainInput,\n collectionId: mainCollectionId,\n } = processFrom(\n query.from,\n allInputs,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n cache,\n queryMapping,\n aliasToCollectionId,\n aliasRemapping,\n )\n sources[mainSource] = mainInput\n\n // Prepare the initial pipeline with the main source wrapped in its alias\n let pipeline: NamespacedAndKeyedStream = mainInput.pipe(\n map(([key, row]) => {\n // Initialize the record with a nested structure\n const ret = [key, { [mainSource]: row }] as [\n string,\n Record<string, typeof row>,\n ]\n return ret\n }),\n )\n\n // Process JOIN clauses if they exist\n if (query.join && query.join.length > 0) {\n pipeline = processJoins(\n pipeline,\n query.join,\n sources,\n mainCollectionId,\n mainSource,\n allInputs,\n cache,\n queryMapping,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n rawQuery,\n compileQuery,\n aliasToCollectionId,\n aliasRemapping,\n )\n }\n\n // Process the WHERE clause if it exists\n if (query.where && query.where.length > 0) {\n // Apply each WHERE condition as a filter (they are ANDed together)\n for (const where of query.where) {\n const whereExpression = getWhereExpression(where)\n const compiledWhere = compileExpression(whereExpression)\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return toBooleanPredicate(compiledWhere(namespacedRow))\n }),\n )\n }\n }\n\n // Process functional WHERE clauses if they exist\n if (query.fnWhere && query.fnWhere.length > 0) {\n for (const fnWhere of query.fnWhere) {\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return toBooleanPredicate(fnWhere(namespacedRow))\n }),\n )\n }\n }\n\n if (query.distinct && !query.fnSelect && !query.select) {\n throw new DistinctRequiresSelectError()\n }\n\n // Process the SELECT clause early - always create $selected\n // This eliminates duplication and allows for DISTINCT implementation\n if (query.fnSelect) {\n // Handle functional select - apply the function to transform the row\n pipeline = pipeline.pipe(\n map(([key, namespacedRow]) => {\n const selectResults = query.fnSelect!(namespacedRow)\n return [\n key,\n {\n ...namespacedRow,\n $selected: selectResults,\n },\n ] as [string, typeof namespacedRow & { $selected: any }]\n }),\n )\n } else if (query.select) {\n pipeline = processSelect(pipeline, query.select, allInputs)\n } else {\n // If no SELECT clause, create $selected with the main table data\n pipeline = pipeline.pipe(\n map(([key, namespacedRow]) => {\n const selectResults =\n !query.join && !query.groupBy\n ? namespacedRow[mainSource]\n : namespacedRow\n\n return [\n key,\n {\n ...namespacedRow,\n $selected: selectResults,\n },\n ] as [string, typeof namespacedRow & { $selected: any }]\n }),\n )\n }\n\n // Process the GROUP BY clause if it exists\n if (query.groupBy && query.groupBy.length > 0) {\n pipeline = processGroupBy(\n pipeline,\n query.groupBy,\n query.having,\n query.select,\n query.fnHaving,\n )\n } else if (query.select) {\n // Check if SELECT contains aggregates but no GROUP BY (implicit single-group aggregation)\n const hasAggregates = Object.values(query.select).some(\n (expr) => expr.type === `agg`,\n )\n if (hasAggregates) {\n // Handle implicit single-group aggregation\n pipeline = processGroupBy(\n pipeline,\n [], // Empty group by means single group\n query.having,\n query.select,\n query.fnHaving,\n )\n }\n }\n\n // Process the HAVING clause if it exists (only applies after GROUP BY)\n if (query.having && (!query.groupBy || query.groupBy.length === 0)) {\n // Check if we have aggregates in SELECT that would trigger implicit grouping\n const hasAggregates = query.select\n ? Object.values(query.select).some((expr) => expr.type === `agg`)\n : false\n\n if (!hasAggregates) {\n throw new HavingRequiresGroupByError()\n }\n }\n\n // Process functional HAVING clauses outside of GROUP BY (treat as additional WHERE filters)\n if (\n query.fnHaving &&\n query.fnHaving.length > 0 &&\n (!query.groupBy || query.groupBy.length === 0)\n ) {\n // If there's no GROUP BY but there are fnHaving clauses, apply them as filters\n for (const fnHaving of query.fnHaving) {\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return fnHaving(namespacedRow)\n }),\n )\n }\n }\n\n // Process the DISTINCT clause if it exists\n if (query.distinct) {\n pipeline = pipeline.pipe(distinct(([_key, row]) => row.$selected))\n }\n\n // Process orderBy parameter if it exists\n if (query.orderBy && query.orderBy.length > 0) {\n const orderedPipeline = processOrderBy(\n rawQuery,\n pipeline,\n query.orderBy,\n query.select || {},\n collections[mainCollectionId]!,\n optimizableOrderByCollections,\n setWindowFn,\n query.limit,\n query.offset,\n )\n\n // Final step: extract the $selected and include orderBy index\n const resultPipeline = orderedPipeline.pipe(\n map(([key, [row, orderByIndex]]) => {\n // Extract the final results from $selected and include orderBy index\n const raw = (row as any).$selected\n const finalResults = unwrapValue(raw)\n return [key, [finalResults, orderByIndex]] as [unknown, [any, string]]\n }),\n )\n\n const result = resultPipeline\n // Cache the result before returning (use original query as key)\n const compilationResult = {\n collectionId: mainCollectionId,\n pipeline: result,\n sourceWhereClauses,\n aliasToCollectionId,\n aliasRemapping,\n }\n cache.set(rawQuery, compilationResult)\n\n return compilationResult\n } else if (query.limit !== undefined || query.offset !== undefined) {\n // If there's a limit or offset without orderBy, throw an error\n throw new LimitOffsetRequireOrderByError()\n }\n\n // Final step: extract the $selected and return tuple format (no orderBy)\n const resultPipeline: ResultStream = pipeline.pipe(\n map(([key, row]) => {\n // Extract the final results from $selected and return [key, [results, undefined]]\n const raw = (row as any).$selected\n const finalResults = unwrapValue(raw)\n return [key, [finalResults, undefined]] as [\n unknown,\n [any, string | undefined],\n ]\n }),\n )\n\n const result = resultPipeline\n // Cache the result before returning (use original query as key)\n const compilationResult = {\n collectionId: mainCollectionId,\n pipeline: result,\n sourceWhereClauses,\n aliasToCollectionId,\n aliasRemapping,\n }\n cache.set(rawQuery, compilationResult)\n\n return compilationResult\n}\n\n/**\n * Collects aliases used for DIRECT collection references (not subqueries).\n * Used to validate that subqueries don't reuse parent query collection aliases.\n * Only direct CollectionRef aliases matter - QueryRef aliases don't cause conflicts.\n */\nfunction collectDirectCollectionAliases(query: QueryIR): Set<string> {\n const aliases = new Set<string>()\n\n // Collect FROM alias only if it's a direct collection reference\n if (query.from.type === `collectionRef`) {\n aliases.add(query.from.alias)\n }\n\n // Collect JOIN aliases only for direct collection references\n if (query.join) {\n for (const joinClause of query.join) {\n if (joinClause.from.type === `collectionRef`) {\n aliases.add(joinClause.from.alias)\n }\n }\n }\n\n return aliases\n}\n\n/**\n * Validates the structure of a query and its subqueries.\n * Checks that subqueries don't reuse collection aliases from parent queries.\n * This must be called on the RAW query before optimization.\n */\nfunction validateQueryStructure(\n query: QueryIR,\n parentCollectionAliases: Set<string> = new Set(),\n): void {\n // Collect direct collection aliases from this query level\n const currentLevelAliases = collectDirectCollectionAliases(query)\n\n // Check if any current alias conflicts with parent aliases\n for (const alias of currentLevelAliases) {\n if (parentCollectionAliases.has(alias)) {\n throw new DuplicateAliasInSubqueryError(\n alias,\n Array.from(parentCollectionAliases),\n )\n }\n }\n\n // Combine parent and current aliases for checking nested subqueries\n const combinedAliases = new Set([\n ...parentCollectionAliases,\n ...currentLevelAliases,\n ])\n\n // Recursively validate FROM subquery\n if (query.from.type === `queryRef`) {\n validateQueryStructure(query.from.query, combinedAliases)\n }\n\n // Recursively validate JOIN subqueries\n if (query.join) {\n for (const joinClause of query.join) {\n if (joinClause.from.type === `queryRef`) {\n validateQueryStructure(joinClause.from.query, combinedAliases)\n }\n }\n }\n}\n\n/**\n * Processes the FROM clause, handling direct collection references and subqueries.\n * Populates `aliasToCollectionId` and `aliasRemapping` for per-alias subscription tracking.\n */\nfunction processFrom(\n from: CollectionRef | QueryRef,\n allInputs: Record<string, KeyedStream>,\n collections: Record<string, Collection>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazySources: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n cache: QueryCache,\n queryMapping: QueryMapping,\n aliasToCollectionId: Record<string, string>,\n aliasRemapping: Record<string, string>,\n): { alias: string; input: KeyedStream; collectionId: string } {\n switch (from.type) {\n case `collectionRef`: {\n const input = allInputs[from.alias]\n if (!input) {\n throw new CollectionInputNotFoundError(\n from.alias,\n from.collection.id,\n Object.keys(allInputs),\n )\n }\n aliasToCollectionId[from.alias] = from.collection.id\n return { alias: from.alias, input, collectionId: from.collection.id }\n }\n case `queryRef`: {\n // Find the original query for caching purposes\n const originalQuery = queryMapping.get(from.query) || from.query\n\n // Recursively compile the sub-query with cache\n const subQueryResult = compileQuery(\n originalQuery,\n allInputs,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n cache,\n queryMapping,\n )\n\n // Pull up alias mappings from subquery to parent scope.\n // This includes both the innermost alias-to-collection mappings AND\n // any existing remappings from nested subquery levels.\n Object.assign(aliasToCollectionId, subQueryResult.aliasToCollectionId)\n Object.assign(aliasRemapping, subQueryResult.aliasRemapping)\n\n // Create a FLATTENED remapping from outer alias to innermost alias.\n // For nested subqueries, this ensures one-hop lookups (not recursive chains).\n //\n // Example with 3-level nesting:\n // Inner: .from({ user: usersCollection })\n // Middle: .from({ activeUser: innerSubquery }) → creates: activeUser → user\n // Outer: .from({ author: middleSubquery }) → creates: author → user (not author → activeUser)\n //\n // The key insight: We search through the PULLED-UP aliasToCollectionId (which contains\n // the innermost 'user' alias), so we always map directly to the deepest level.\n // This means aliasRemapping[alias] is always a single lookup, never recursive.\n // Needed for subscription resolution during lazy loading.\n const innerAlias = Object.keys(subQueryResult.aliasToCollectionId).find(\n (alias) =>\n subQueryResult.aliasToCollectionId[alias] ===\n subQueryResult.collectionId,\n )\n if (innerAlias && innerAlias !== from.alias) {\n aliasRemapping[from.alias] = innerAlias\n }\n\n // Extract the pipeline from the compilation result\n const subQueryInput = subQueryResult.pipeline\n\n // Subqueries may return [key, [value, orderByIndex]] (with ORDER BY) or [key, value] (without ORDER BY)\n // We need to extract just the value for use in parent queries\n const extractedInput = subQueryInput.pipe(\n map((data: any) => {\n const [key, [value, _orderByIndex]] = data\n // Unwrap Value expressions that might have leaked through as the entire row\n const unwrapped = unwrapValue(value)\n return [key, unwrapped] as [unknown, any]\n }),\n )\n\n return {\n alias: from.alias,\n input: extractedInput,\n collectionId: subQueryResult.collectionId,\n }\n }\n default:\n throw new UnsupportedFromTypeError((from as any).type)\n }\n}\n\n// Helper to check if a value is a Value expression\nfunction isValue(raw: any): boolean {\n return (\n raw instanceof ValClass ||\n (raw && typeof raw === `object` && `type` in raw && raw.type === `val`)\n )\n}\n\n// Helper to unwrap a Value expression or return the value itself\nfunction unwrapValue(value: any): any {\n return isValue(value) ? value.value : value\n}\n\n/**\n * Recursively maps optimized subqueries to their original queries for proper caching.\n * This ensures that when we encounter the same QueryRef object in different contexts,\n * we can find the original query to check the cache.\n */\nfunction mapNestedQueries(\n optimizedQuery: QueryIR,\n originalQuery: QueryIR,\n queryMapping: QueryMapping,\n): void {\n // Map the FROM clause if it's a QueryRef\n if (\n optimizedQuery.from.type === `queryRef` &&\n originalQuery.from.type === `queryRef`\n ) {\n queryMapping.set(optimizedQuery.from.query, originalQuery.from.query)\n // Recursively map nested queries\n mapNestedQueries(\n optimizedQuery.from.query,\n originalQuery.from.query,\n queryMapping,\n )\n }\n\n // Map JOIN clauses if they exist\n if (optimizedQuery.join && originalQuery.join) {\n for (\n let i = 0;\n i < optimizedQuery.join.length && i < originalQuery.join.length;\n i++\n ) {\n const optimizedJoin = optimizedQuery.join[i]!\n const originalJoin = originalQuery.join[i]!\n\n if (\n optimizedJoin.from.type === `queryRef` &&\n originalJoin.from.type === `queryRef`\n ) {\n queryMapping.set(optimizedJoin.from.query, originalJoin.from.query)\n // Recursively map nested queries in joins\n mapNestedQueries(\n optimizedJoin.from.query,\n originalJoin.from.query,\n queryMapping,\n )\n }\n }\n }\n}\n\nfunction getRefFromAlias(\n query: QueryIR,\n alias: string,\n): CollectionRef | QueryRef | void {\n if (query.from.alias === alias) {\n return query.from\n }\n\n for (const join of query.join || []) {\n if (join.from.alias === alias) {\n return join.from\n }\n }\n}\n\n/**\n * Follows the given reference in a query\n * until its finds the root field the reference points to.\n * @returns The collection, its alias, and the path to the root field in this collection\n */\nexport function followRef(\n query: QueryIR,\n ref: PropRef<any>,\n collection: Collection,\n): { collection: Collection; path: Array<string> } | void {\n if (ref.path.length === 0) {\n return\n }\n\n if (ref.path.length === 1) {\n // This field should be part of this collection\n const field = ref.path[0]!\n // is it part of the select clause?\n if (query.select) {\n const selectedField = query.select[field]\n if (selectedField && selectedField.type === `ref`) {\n return followRef(query, selectedField, collection)\n }\n }\n\n // Either this field is not part of the select clause\n // and thus it must be part of the collection itself\n // or it is part of the select but is not a reference\n // so we can stop here and don't have to follow it\n return { collection, path: [field] }\n }\n\n if (ref.path.length > 1) {\n // This is a nested field\n const [alias, ...rest] = ref.path\n const aliasRef = getRefFromAlias(query, alias!)\n if (!aliasRef) {\n return\n }\n\n if (aliasRef.type === `queryRef`) {\n return followRef(aliasRef.query, new PropRef(rest), collection)\n } else {\n // This is a reference to a collection\n // we can't follow it further\n // so the field must be on the collection itself\n return { collection: aliasRef.collection, path: rest }\n }\n }\n}\n\nexport type CompileQueryFn = typeof compileQuery\n"],"names":["resultPipeline","result","compilationResult","ValClass"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../../src/query/compiler/index.ts"],"sourcesContent":["import { distinct, filter, map } from '@tanstack/db-ivm'\nimport { optimizeQuery } from '../optimizer.js'\nimport {\n CollectionInputNotFoundError,\n DistinctRequiresSelectError,\n DuplicateAliasInSubqueryError,\n HavingRequiresGroupByError,\n LimitOffsetRequireOrderByError,\n UnsupportedFromTypeError,\n} from '../../errors.js'\nimport { PropRef, Value as ValClass, getWhereExpression } from '../ir.js'\nimport { compileExpression, toBooleanPredicate } from './evaluators.js'\nimport { processJoins } from './joins.js'\nimport { processGroupBy } from './group-by.js'\nimport { processOrderBy } from './order-by.js'\nimport { processSelect } from './select.js'\nimport type { CollectionSubscription } from '../../collection/subscription.js'\nimport type { OrderByOptimizationInfo } from './order-by.js'\nimport type {\n BasicExpression,\n CollectionRef,\n QueryIR,\n QueryRef,\n} from '../ir.js'\nimport type { LazyCollectionCallbacks } from './joins.js'\nimport type { Collection } from '../../collection/index.js'\nimport type {\n KeyedStream,\n NamespacedAndKeyedStream,\n ResultStream,\n} from '../../types.js'\nimport type { QueryCache, QueryMapping, WindowOptions } from './types.js'\n\nexport type { WindowOptions } from './types.js'\n\n/**\n * Result of query compilation including both the pipeline and source-specific WHERE clauses\n */\nexport interface CompilationResult {\n /** The ID of the main collection */\n collectionId: string\n\n /** The compiled query pipeline (D2 stream) */\n pipeline: ResultStream\n\n /** Map of source aliases to their WHERE clauses for index optimization */\n sourceWhereClauses: Map<string, BasicExpression<boolean>>\n\n /**\n * Maps each source alias to its collection ID. Enables per-alias subscriptions for self-joins.\n * Example: `{ employee: 'employees-col-id', manager: 'employees-col-id' }`\n */\n aliasToCollectionId: Record<string, string>\n\n /**\n * Flattened mapping from outer alias to innermost alias for subqueries.\n * Always provides one-hop lookups, never recursive chains.\n *\n * Example: `{ activeUser: 'user' }` when `.from({ activeUser: subquery })`\n * where the subquery uses `.from({ user: collection })`.\n *\n * For deeply nested subqueries, the mapping goes directly to the innermost alias:\n * `{ author: 'user' }` (not `{ author: 'activeUser' }`), so `aliasRemapping[alias]`\n * always resolves in a single lookup.\n *\n * Used to resolve subscriptions during lazy loading when join aliases differ from\n * the inner aliases where collection subscriptions were created.\n */\n aliasRemapping: Record<string, string>\n}\n\n/**\n * Compiles a query IR into a D2 pipeline\n * @param rawQuery The query IR to compile\n * @param inputs Mapping of source aliases to input streams (e.g., `{ employee: input1, manager: input2 }`)\n * @param collections Mapping of collection IDs to Collection instances\n * @param subscriptions Mapping of source aliases to CollectionSubscription instances\n * @param callbacks Mapping of source aliases to lazy loading callbacks\n * @param lazySources Set of source aliases that should load data lazily\n * @param optimizableOrderByCollections Map of collection IDs to order-by optimization info\n * @param cache Optional cache for compiled subqueries (used internally for recursion)\n * @param queryMapping Optional mapping from optimized queries to original queries\n * @returns A CompilationResult with the pipeline, source WHERE clauses, and alias metadata\n */\nexport function compileQuery(\n rawQuery: QueryIR,\n inputs: Record<string, KeyedStream>,\n collections: Record<string, Collection<any, any, any, any, any>>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazySources: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n cache: QueryCache = new WeakMap(),\n queryMapping: QueryMapping = new WeakMap(),\n): CompilationResult {\n // Check if the original raw query has already been compiled\n const cachedResult = cache.get(rawQuery)\n if (cachedResult) {\n return cachedResult\n }\n\n // Validate the raw query BEFORE optimization to check user's original structure.\n // This must happen before optimization because the optimizer may create internal\n // subqueries (e.g., for predicate pushdown) that reuse aliases, which is fine.\n validateQueryStructure(rawQuery)\n\n // Optimize the query before compilation\n const { optimizedQuery: query, sourceWhereClauses } = optimizeQuery(rawQuery)\n\n // Create mapping from optimized query to original for caching\n queryMapping.set(query, rawQuery)\n mapNestedQueries(query, rawQuery, queryMapping)\n\n // Create a copy of the inputs map to avoid modifying the original\n const allInputs = { ...inputs }\n\n // Track alias to collection id relationships discovered during compilation.\n // This includes all user-declared aliases plus inner aliases from subqueries.\n const aliasToCollectionId: Record<string, string> = {}\n\n // Track alias remapping for subqueries (outer alias → inner alias)\n // e.g., when .join({ activeUser: subquery }) where subquery uses .from({ user: collection })\n // we store: aliasRemapping['activeUser'] = 'user'\n const aliasRemapping: Record<string, string> = {}\n\n // Create a map of source aliases to input streams.\n // Inputs MUST be keyed by alias (e.g., `{ employee: input1, manager: input2 }`),\n // not by collection ID. This enables per-alias subscriptions where different aliases\n // of the same collection (e.g., self-joins) maintain independent filtered streams.\n const sources: Record<string, KeyedStream> = {}\n\n // Process the FROM clause to get the main source\n const {\n alias: mainSource,\n input: mainInput,\n collectionId: mainCollectionId,\n } = processFrom(\n query.from,\n allInputs,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n cache,\n queryMapping,\n aliasToCollectionId,\n aliasRemapping,\n sourceWhereClauses,\n )\n sources[mainSource] = mainInput\n\n // Prepare the initial pipeline with the main source wrapped in its alias\n let pipeline: NamespacedAndKeyedStream = mainInput.pipe(\n map(([key, row]) => {\n // Initialize the record with a nested structure\n const ret = [key, { [mainSource]: row }] as [\n string,\n Record<string, typeof row>,\n ]\n return ret\n }),\n )\n\n // Process JOIN clauses if they exist\n if (query.join && query.join.length > 0) {\n pipeline = processJoins(\n pipeline,\n query.join,\n sources,\n mainCollectionId,\n mainSource,\n allInputs,\n cache,\n queryMapping,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n rawQuery,\n compileQuery,\n aliasToCollectionId,\n aliasRemapping,\n sourceWhereClauses,\n )\n }\n\n // Process the WHERE clause if it exists\n if (query.where && query.where.length > 0) {\n // Apply each WHERE condition as a filter (they are ANDed together)\n for (const where of query.where) {\n const whereExpression = getWhereExpression(where)\n const compiledWhere = compileExpression(whereExpression)\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return toBooleanPredicate(compiledWhere(namespacedRow))\n }),\n )\n }\n }\n\n // Process functional WHERE clauses if they exist\n if (query.fnWhere && query.fnWhere.length > 0) {\n for (const fnWhere of query.fnWhere) {\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return toBooleanPredicate(fnWhere(namespacedRow))\n }),\n )\n }\n }\n\n if (query.distinct && !query.fnSelect && !query.select) {\n throw new DistinctRequiresSelectError()\n }\n\n // Process the SELECT clause early - always create $selected\n // This eliminates duplication and allows for DISTINCT implementation\n if (query.fnSelect) {\n // Handle functional select - apply the function to transform the row\n pipeline = pipeline.pipe(\n map(([key, namespacedRow]) => {\n const selectResults = query.fnSelect!(namespacedRow)\n return [\n key,\n {\n ...namespacedRow,\n $selected: selectResults,\n },\n ] as [string, typeof namespacedRow & { $selected: any }]\n }),\n )\n } else if (query.select) {\n pipeline = processSelect(pipeline, query.select, allInputs)\n } else {\n // If no SELECT clause, create $selected with the main table data\n pipeline = pipeline.pipe(\n map(([key, namespacedRow]) => {\n const selectResults =\n !query.join && !query.groupBy\n ? namespacedRow[mainSource]\n : namespacedRow\n\n return [\n key,\n {\n ...namespacedRow,\n $selected: selectResults,\n },\n ] as [string, typeof namespacedRow & { $selected: any }]\n }),\n )\n }\n\n // Process the GROUP BY clause if it exists\n if (query.groupBy && query.groupBy.length > 0) {\n pipeline = processGroupBy(\n pipeline,\n query.groupBy,\n query.having,\n query.select,\n query.fnHaving,\n )\n } else if (query.select) {\n // Check if SELECT contains aggregates but no GROUP BY (implicit single-group aggregation)\n const hasAggregates = Object.values(query.select).some(\n (expr) => expr.type === `agg`,\n )\n if (hasAggregates) {\n // Handle implicit single-group aggregation\n pipeline = processGroupBy(\n pipeline,\n [], // Empty group by means single group\n query.having,\n query.select,\n query.fnHaving,\n )\n }\n }\n\n // Process the HAVING clause if it exists (only applies after GROUP BY)\n if (query.having && (!query.groupBy || query.groupBy.length === 0)) {\n // Check if we have aggregates in SELECT that would trigger implicit grouping\n const hasAggregates = query.select\n ? Object.values(query.select).some((expr) => expr.type === `agg`)\n : false\n\n if (!hasAggregates) {\n throw new HavingRequiresGroupByError()\n }\n }\n\n // Process functional HAVING clauses outside of GROUP BY (treat as additional WHERE filters)\n if (\n query.fnHaving &&\n query.fnHaving.length > 0 &&\n (!query.groupBy || query.groupBy.length === 0)\n ) {\n // If there's no GROUP BY but there are fnHaving clauses, apply them as filters\n for (const fnHaving of query.fnHaving) {\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return fnHaving(namespacedRow)\n }),\n )\n }\n }\n\n // Process the DISTINCT clause if it exists\n if (query.distinct) {\n pipeline = pipeline.pipe(distinct(([_key, row]) => row.$selected))\n }\n\n // Process orderBy parameter if it exists\n if (query.orderBy && query.orderBy.length > 0) {\n const orderedPipeline = processOrderBy(\n rawQuery,\n pipeline,\n query.orderBy,\n query.select || {},\n collections[mainCollectionId]!,\n optimizableOrderByCollections,\n setWindowFn,\n query.limit,\n query.offset,\n )\n\n // Final step: extract the $selected and include orderBy index\n const resultPipeline = orderedPipeline.pipe(\n map(([key, [row, orderByIndex]]) => {\n // Extract the final results from $selected and include orderBy index\n const raw = (row as any).$selected\n const finalResults = unwrapValue(raw)\n return [key, [finalResults, orderByIndex]] as [unknown, [any, string]]\n }),\n )\n\n const result = resultPipeline\n // Cache the result before returning (use original query as key)\n const compilationResult = {\n collectionId: mainCollectionId,\n pipeline: result,\n sourceWhereClauses,\n aliasToCollectionId,\n aliasRemapping,\n }\n cache.set(rawQuery, compilationResult)\n\n return compilationResult\n } else if (query.limit !== undefined || query.offset !== undefined) {\n // If there's a limit or offset without orderBy, throw an error\n throw new LimitOffsetRequireOrderByError()\n }\n\n // Final step: extract the $selected and return tuple format (no orderBy)\n const resultPipeline: ResultStream = pipeline.pipe(\n map(([key, row]) => {\n // Extract the final results from $selected and return [key, [results, undefined]]\n const raw = (row as any).$selected\n const finalResults = unwrapValue(raw)\n return [key, [finalResults, undefined]] as [\n unknown,\n [any, string | undefined],\n ]\n }),\n )\n\n const result = resultPipeline\n // Cache the result before returning (use original query as key)\n const compilationResult = {\n collectionId: mainCollectionId,\n pipeline: result,\n sourceWhereClauses,\n aliasToCollectionId,\n aliasRemapping,\n }\n cache.set(rawQuery, compilationResult)\n\n return compilationResult\n}\n\n/**\n * Collects aliases used for DIRECT collection references (not subqueries).\n * Used to validate that subqueries don't reuse parent query collection aliases.\n * Only direct CollectionRef aliases matter - QueryRef aliases don't cause conflicts.\n */\nfunction collectDirectCollectionAliases(query: QueryIR): Set<string> {\n const aliases = new Set<string>()\n\n // Collect FROM alias only if it's a direct collection reference\n if (query.from.type === `collectionRef`) {\n aliases.add(query.from.alias)\n }\n\n // Collect JOIN aliases only for direct collection references\n if (query.join) {\n for (const joinClause of query.join) {\n if (joinClause.from.type === `collectionRef`) {\n aliases.add(joinClause.from.alias)\n }\n }\n }\n\n return aliases\n}\n\n/**\n * Validates the structure of a query and its subqueries.\n * Checks that subqueries don't reuse collection aliases from parent queries.\n * This must be called on the RAW query before optimization.\n */\nfunction validateQueryStructure(\n query: QueryIR,\n parentCollectionAliases: Set<string> = new Set(),\n): void {\n // Collect direct collection aliases from this query level\n const currentLevelAliases = collectDirectCollectionAliases(query)\n\n // Check if any current alias conflicts with parent aliases\n for (const alias of currentLevelAliases) {\n if (parentCollectionAliases.has(alias)) {\n throw new DuplicateAliasInSubqueryError(\n alias,\n Array.from(parentCollectionAliases),\n )\n }\n }\n\n // Combine parent and current aliases for checking nested subqueries\n const combinedAliases = new Set([\n ...parentCollectionAliases,\n ...currentLevelAliases,\n ])\n\n // Recursively validate FROM subquery\n if (query.from.type === `queryRef`) {\n validateQueryStructure(query.from.query, combinedAliases)\n }\n\n // Recursively validate JOIN subqueries\n if (query.join) {\n for (const joinClause of query.join) {\n if (joinClause.from.type === `queryRef`) {\n validateQueryStructure(joinClause.from.query, combinedAliases)\n }\n }\n }\n}\n\n/**\n * Processes the FROM clause, handling direct collection references and subqueries.\n * Populates `aliasToCollectionId` and `aliasRemapping` for per-alias subscription tracking.\n */\nfunction processFrom(\n from: CollectionRef | QueryRef,\n allInputs: Record<string, KeyedStream>,\n collections: Record<string, Collection>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazySources: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n cache: QueryCache,\n queryMapping: QueryMapping,\n aliasToCollectionId: Record<string, string>,\n aliasRemapping: Record<string, string>,\n sourceWhereClauses: Map<string, BasicExpression<boolean>>,\n): { alias: string; input: KeyedStream; collectionId: string } {\n switch (from.type) {\n case `collectionRef`: {\n const input = allInputs[from.alias]\n if (!input) {\n throw new CollectionInputNotFoundError(\n from.alias,\n from.collection.id,\n Object.keys(allInputs),\n )\n }\n aliasToCollectionId[from.alias] = from.collection.id\n return { alias: from.alias, input, collectionId: from.collection.id }\n }\n case `queryRef`: {\n // Find the original query for caching purposes\n const originalQuery = queryMapping.get(from.query) || from.query\n\n // Recursively compile the sub-query with cache\n const subQueryResult = compileQuery(\n originalQuery,\n allInputs,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n cache,\n queryMapping,\n )\n\n // Pull up alias mappings from subquery to parent scope.\n // This includes both the innermost alias-to-collection mappings AND\n // any existing remappings from nested subquery levels.\n Object.assign(aliasToCollectionId, subQueryResult.aliasToCollectionId)\n Object.assign(aliasRemapping, subQueryResult.aliasRemapping)\n\n // Pull up source WHERE clauses from subquery to parent scope.\n // This enables loadSubset to receive the correct where clauses for subquery collections.\n //\n // IMPORTANT: Skip pull-up for optimizer-created subqueries. These are detected when:\n // 1. The outer alias (from.alias) matches the inner alias (from.query.from.alias)\n // 2. The subquery was found in queryMapping (it's a user-defined subquery, not optimizer-created)\n //\n // For optimizer-created subqueries, the parent already has the sourceWhereClauses\n // extracted from the original raw query, so pulling up would be redundant.\n // More importantly, pulling up for optimizer-created subqueries can cause issues\n // when the optimizer has restructured the query.\n const isUserDefinedSubquery = queryMapping.has(from.query)\n const subqueryFromAlias = from.query.from.alias\n const isOptimizerCreated =\n !isUserDefinedSubquery && from.alias === subqueryFromAlias\n\n if (!isOptimizerCreated) {\n for (const [alias, whereClause] of subQueryResult.sourceWhereClauses) {\n sourceWhereClauses.set(alias, whereClause)\n }\n }\n\n // Create a FLATTENED remapping from outer alias to innermost alias.\n // For nested subqueries, this ensures one-hop lookups (not recursive chains).\n //\n // Example with 3-level nesting:\n // Inner: .from({ user: usersCollection })\n // Middle: .from({ activeUser: innerSubquery }) → creates: activeUser → user\n // Outer: .from({ author: middleSubquery }) → creates: author → user (not author → activeUser)\n //\n // The key insight: We search through the PULLED-UP aliasToCollectionId (which contains\n // the innermost 'user' alias), so we always map directly to the deepest level.\n // This means aliasRemapping[alias] is always a single lookup, never recursive.\n // Needed for subscription resolution during lazy loading.\n const innerAlias = Object.keys(subQueryResult.aliasToCollectionId).find(\n (alias) =>\n subQueryResult.aliasToCollectionId[alias] ===\n subQueryResult.collectionId,\n )\n if (innerAlias && innerAlias !== from.alias) {\n aliasRemapping[from.alias] = innerAlias\n }\n\n // Extract the pipeline from the compilation result\n const subQueryInput = subQueryResult.pipeline\n\n // Subqueries may return [key, [value, orderByIndex]] (with ORDER BY) or [key, value] (without ORDER BY)\n // We need to extract just the value for use in parent queries\n const extractedInput = subQueryInput.pipe(\n map((data: any) => {\n const [key, [value, _orderByIndex]] = data\n // Unwrap Value expressions that might have leaked through as the entire row\n const unwrapped = unwrapValue(value)\n return [key, unwrapped] as [unknown, any]\n }),\n )\n\n return {\n alias: from.alias,\n input: extractedInput,\n collectionId: subQueryResult.collectionId,\n }\n }\n default:\n throw new UnsupportedFromTypeError((from as any).type)\n }\n}\n\n// Helper to check if a value is a Value expression\nfunction isValue(raw: any): boolean {\n return (\n raw instanceof ValClass ||\n (raw && typeof raw === `object` && `type` in raw && raw.type === `val`)\n )\n}\n\n// Helper to unwrap a Value expression or return the value itself\nfunction unwrapValue(value: any): any {\n return isValue(value) ? value.value : value\n}\n\n/**\n * Recursively maps optimized subqueries to their original queries for proper caching.\n * This ensures that when we encounter the same QueryRef object in different contexts,\n * we can find the original query to check the cache.\n */\nfunction mapNestedQueries(\n optimizedQuery: QueryIR,\n originalQuery: QueryIR,\n queryMapping: QueryMapping,\n): void {\n // Map the FROM clause if it's a QueryRef\n if (\n optimizedQuery.from.type === `queryRef` &&\n originalQuery.from.type === `queryRef`\n ) {\n queryMapping.set(optimizedQuery.from.query, originalQuery.from.query)\n // Recursively map nested queries\n mapNestedQueries(\n optimizedQuery.from.query,\n originalQuery.from.query,\n queryMapping,\n )\n }\n\n // Map JOIN clauses if they exist\n if (optimizedQuery.join && originalQuery.join) {\n for (\n let i = 0;\n i < optimizedQuery.join.length && i < originalQuery.join.length;\n i++\n ) {\n const optimizedJoin = optimizedQuery.join[i]!\n const originalJoin = originalQuery.join[i]!\n\n if (\n optimizedJoin.from.type === `queryRef` &&\n originalJoin.from.type === `queryRef`\n ) {\n queryMapping.set(optimizedJoin.from.query, originalJoin.from.query)\n // Recursively map nested queries in joins\n mapNestedQueries(\n optimizedJoin.from.query,\n originalJoin.from.query,\n queryMapping,\n )\n }\n }\n }\n}\n\nfunction getRefFromAlias(\n query: QueryIR,\n alias: string,\n): CollectionRef | QueryRef | void {\n if (query.from.alias === alias) {\n return query.from\n }\n\n for (const join of query.join || []) {\n if (join.from.alias === alias) {\n return join.from\n }\n }\n}\n\n/**\n * Follows the given reference in a query\n * until its finds the root field the reference points to.\n * @returns The collection, its alias, and the path to the root field in this collection\n */\nexport function followRef(\n query: QueryIR,\n ref: PropRef<any>,\n collection: Collection,\n): { collection: Collection; path: Array<string> } | void {\n if (ref.path.length === 0) {\n return\n }\n\n if (ref.path.length === 1) {\n // This field should be part of this collection\n const field = ref.path[0]!\n // is it part of the select clause?\n if (query.select) {\n const selectedField = query.select[field]\n if (selectedField && selectedField.type === `ref`) {\n return followRef(query, selectedField, collection)\n }\n }\n\n // Either this field is not part of the select clause\n // and thus it must be part of the collection itself\n // or it is part of the select but is not a reference\n // so we can stop here and don't have to follow it\n return { collection, path: [field] }\n }\n\n if (ref.path.length > 1) {\n // This is a nested field\n const [alias, ...rest] = ref.path\n const aliasRef = getRefFromAlias(query, alias!)\n if (!aliasRef) {\n return\n }\n\n if (aliasRef.type === `queryRef`) {\n return followRef(aliasRef.query, new PropRef(rest), collection)\n } else {\n // This is a reference to a collection\n // we can't follow it further\n // so the field must be on the collection itself\n return { collection: aliasRef.collection, path: rest }\n }\n }\n}\n\nexport type CompileQueryFn = typeof compileQuery\n"],"names":["resultPipeline","result","compilationResult","ValClass"],"mappings":";;;;;;;;;AAoFO,SAAS,aACd,UACA,QACA,aACA,eACA,WACA,aACA,+BACA,aACA,4BAAwB,QAAA,GACxB,eAA6B,oBAAI,WACd;AAEnB,QAAM,eAAe,MAAM,IAAI,QAAQ;AACvC,MAAI,cAAc;AAChB,WAAO;AAAA,EACT;AAKA,yBAAuB,QAAQ;AAG/B,QAAM,EAAE,gBAAgB,OAAO,mBAAA,IAAuB,cAAc,QAAQ;AAG5E,eAAa,IAAI,OAAO,QAAQ;AAChC,mBAAiB,OAAO,UAAU,YAAY;AAG9C,QAAM,YAAY,EAAE,GAAG,OAAA;AAIvB,QAAM,sBAA8C,CAAA;AAKpD,QAAM,iBAAyC,CAAA;AAM/C,QAAM,UAAuC,CAAA;AAG7C,QAAM;AAAA,IACJ,OAAO;AAAA,IACP,OAAO;AAAA,IACP,cAAc;AAAA,EAAA,IACZ;AAAA,IACF,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEF,UAAQ,UAAU,IAAI;AAGtB,MAAI,WAAqC,UAAU;AAAA,IACjD,IAAI,CAAC,CAAC,KAAK,GAAG,MAAM;AAElB,YAAM,MAAM,CAAC,KAAK,EAAE,CAAC,UAAU,GAAG,KAAK;AAIvC,aAAO;AAAA,IACT,CAAC;AAAA,EAAA;AAIH,MAAI,MAAM,QAAQ,MAAM,KAAK,SAAS,GAAG;AACvC,eAAW;AAAA,MACT;AAAA,MACA,MAAM;AAAA,MACN;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAAA,EAEJ;AAGA,MAAI,MAAM,SAAS,MAAM,MAAM,SAAS,GAAG;AAEzC,eAAW,SAAS,MAAM,OAAO;AAC/B,YAAM,kBAAkB,mBAAmB,KAAK;AAChD,YAAM,gBAAgB,kBAAkB,eAAe;AACvD,iBAAW,SAAS;AAAA,QAClB,OAAO,CAAC,CAAC,MAAM,aAAa,MAAM;AAChC,iBAAO,mBAAmB,cAAc,aAAa,CAAC;AAAA,QACxD,CAAC;AAAA,MAAA;AAAA,IAEL;AAAA,EACF;AAGA,MAAI,MAAM,WAAW,MAAM,QAAQ,SAAS,GAAG;AAC7C,eAAW,WAAW,MAAM,SAAS;AACnC,iBAAW,SAAS;AAAA,QAClB,OAAO,CAAC,CAAC,MAAM,aAAa,MAAM;AAChC,iBAAO,mBAAmB,QAAQ,aAAa,CAAC;AAAA,QAClD,CAAC;AAAA,MAAA;AAAA,IAEL;AAAA,EACF;AAEA,MAAI,MAAM,YAAY,CAAC,MAAM,YAAY,CAAC,MAAM,QAAQ;AACtD,UAAM,IAAI,4BAAA;AAAA,EACZ;AAIA,MAAI,MAAM,UAAU;AAElB,eAAW,SAAS;AAAA,MAClB,IAAI,CAAC,CAAC,KAAK,aAAa,MAAM;AAC5B,cAAM,gBAAgB,MAAM,SAAU,aAAa;AACnD,eAAO;AAAA,UACL;AAAA,UACA;AAAA,YACE,GAAG;AAAA,YACH,WAAW;AAAA,UAAA;AAAA,QACb;AAAA,MAEJ,CAAC;AAAA,IAAA;AAAA,EAEL,WAAW,MAAM,QAAQ;AACvB,eAAW,cAAc,UAAU,MAAM,MAAiB;AAAA,EAC5D,OAAO;AAEL,eAAW,SAAS;AAAA,MAClB,IAAI,CAAC,CAAC,KAAK,aAAa,MAAM;AAC5B,cAAM,gBACJ,CAAC,MAAM,QAAQ,CAAC,MAAM,UAClB,cAAc,UAAU,IACxB;AAEN,eAAO;AAAA,UACL;AAAA,UACA;AAAA,YACE,GAAG;AAAA,YACH,WAAW;AAAA,UAAA;AAAA,QACb;AAAA,MAEJ,CAAC;AAAA,IAAA;AAAA,EAEL;AAGA,MAAI,MAAM,WAAW,MAAM,QAAQ,SAAS,GAAG;AAC7C,eAAW;AAAA,MACT;AAAA,MACA,MAAM;AAAA,MACN,MAAM;AAAA,MACN,MAAM;AAAA,MACN,MAAM;AAAA,IAAA;AAAA,EAEV,WAAW,MAAM,QAAQ;AAEvB,UAAM,gBAAgB,OAAO,OAAO,MAAM,MAAM,EAAE;AAAA,MAChD,CAAC,SAAS,KAAK,SAAS;AAAA,IAAA;AAE1B,QAAI,eAAe;AAEjB,iBAAW;AAAA,QACT;AAAA,QACA,CAAA;AAAA;AAAA,QACA,MAAM;AAAA,QACN,MAAM;AAAA,QACN,MAAM;AAAA,MAAA;AAAA,IAEV;AAAA,EACF;AAGA,MAAI,MAAM,WAAW,CAAC,MAAM,WAAW,MAAM,QAAQ,WAAW,IAAI;AAElE,UAAM,gBAAgB,MAAM,SACxB,OAAO,OAAO,MAAM,MAAM,EAAE,KAAK,CAAC,SAAS,KAAK,SAAS,KAAK,IAC9D;AAEJ,QAAI,CAAC,eAAe;AAClB,YAAM,IAAI,2BAAA;AAAA,IACZ;AAAA,EACF;AAGA,MACE,MAAM,YACN,MAAM,SAAS,SAAS,MACvB,CAAC,MAAM,WAAW,MAAM,QAAQ,WAAW,IAC5C;AAEA,eAAW,YAAY,MAAM,UAAU;AACrC,iBAAW,SAAS;AAAA,QAClB,OAAO,CAAC,CAAC,MAAM,aAAa,MAAM;AAChC,iBAAO,SAAS,aAAa;AAAA,QAC/B,CAAC;AAAA,MAAA;AAAA,IAEL;AAAA,EACF;AAGA,MAAI,MAAM,UAAU;AAClB,eAAW,SAAS,KAAK,SAAS,CAAC,CAAC,MAAM,GAAG,MAAM,IAAI,SAAS,CAAC;AAAA,EACnE;AAGA,MAAI,MAAM,WAAW,MAAM,QAAQ,SAAS,GAAG;AAC7C,UAAM,kBAAkB;AAAA,MACtB;AAAA,MACA;AAAA,MACA,MAAM;AAAA,MACN,MAAM,UAAU,CAAA;AAAA,MAChB,YAAY,gBAAgB;AAAA,MAC5B;AAAA,MACA;AAAA,MACA,MAAM;AAAA,MACN,MAAM;AAAA,IAAA;AAIR,UAAMA,kBAAiB,gBAAgB;AAAA,MACrC,IAAI,CAAC,CAAC,KAAK,CAAC,KAAK,YAAY,CAAC,MAAM;AAElC,cAAM,MAAO,IAAY;AACzB,cAAM,eAAe,YAAY,GAAG;AACpC,eAAO,CAAC,KAAK,CAAC,cAAc,YAAY,CAAC;AAAA,MAC3C,CAAC;AAAA,IAAA;AAGH,UAAMC,UAASD;AAEf,UAAME,qBAAoB;AAAA,MACxB,cAAc;AAAA,MACd,UAAUD;AAAAA,MACV;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAEF,UAAM,IAAI,UAAUC,kBAAiB;AAErC,WAAOA;AAAAA,EACT,WAAW,MAAM,UAAU,UAAa,MAAM,WAAW,QAAW;AAElE,UAAM,IAAI,+BAAA;AAAA,EACZ;AAGA,QAAM,iBAA+B,SAAS;AAAA,IAC5C,IAAI,CAAC,CAAC,KAAK,GAAG,MAAM;AAElB,YAAM,MAAO,IAAY;AACzB,YAAM,eAAe,YAAY,GAAG;AACpC,aAAO,CAAC,KAAK,CAAC,cAAc,MAAS,CAAC;AAAA,IAIxC,CAAC;AAAA,EAAA;AAGH,QAAM,SAAS;AAEf,QAAM,oBAAoB;AAAA,IACxB,cAAc;AAAA,IACd,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEF,QAAM,IAAI,UAAU,iBAAiB;AAErC,SAAO;AACT;AAOA,SAAS,+BAA+B,OAA6B;AACnE,QAAM,8BAAc,IAAA;AAGpB,MAAI,MAAM,KAAK,SAAS,iBAAiB;AACvC,YAAQ,IAAI,MAAM,KAAK,KAAK;AAAA,EAC9B;AAGA,MAAI,MAAM,MAAM;AACd,eAAW,cAAc,MAAM,MAAM;AACnC,UAAI,WAAW,KAAK,SAAS,iBAAiB;AAC5C,gBAAQ,IAAI,WAAW,KAAK,KAAK;AAAA,MACnC;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAOA,SAAS,uBACP,OACA,0BAAuC,oBAAI,OACrC;AAEN,QAAM,sBAAsB,+BAA+B,KAAK;AAGhE,aAAW,SAAS,qBAAqB;AACvC,QAAI,wBAAwB,IAAI,KAAK,GAAG;AACtC,YAAM,IAAI;AAAA,QACR;AAAA,QACA,MAAM,KAAK,uBAAuB;AAAA,MAAA;AAAA,IAEtC;AAAA,EACF;AAGA,QAAM,sCAAsB,IAAI;AAAA,IAC9B,GAAG;AAAA,IACH,GAAG;AAAA,EAAA,CACJ;AAGD,MAAI,MAAM,KAAK,SAAS,YAAY;AAClC,2BAAuB,MAAM,KAAK,OAAO,eAAe;AAAA,EAC1D;AAGA,MAAI,MAAM,MAAM;AACd,eAAW,cAAc,MAAM,MAAM;AACnC,UAAI,WAAW,KAAK,SAAS,YAAY;AACvC,+BAAuB,WAAW,KAAK,OAAO,eAAe;AAAA,MAC/D;AAAA,IACF;AAAA,EACF;AACF;AAMA,SAAS,YACP,MACA,WACA,aACA,eACA,WACA,aACA,+BACA,aACA,OACA,cACA,qBACA,gBACA,oBAC6D;AAC7D,UAAQ,KAAK,MAAA;AAAA,IACX,KAAK,iBAAiB;AACpB,YAAM,QAAQ,UAAU,KAAK,KAAK;AAClC,UAAI,CAAC,OAAO;AACV,cAAM,IAAI;AAAA,UACR,KAAK;AAAA,UACL,KAAK,WAAW;AAAA,UAChB,OAAO,KAAK,SAAS;AAAA,QAAA;AAAA,MAEzB;AACA,0BAAoB,KAAK,KAAK,IAAI,KAAK,WAAW;AAClD,aAAO,EAAE,OAAO,KAAK,OAAO,OAAO,cAAc,KAAK,WAAW,GAAA;AAAA,IACnE;AAAA,IACA,KAAK,YAAY;AAEf,YAAM,gBAAgB,aAAa,IAAI,KAAK,KAAK,KAAK,KAAK;AAG3D,YAAM,iBAAiB;AAAA,QACrB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAMF,aAAO,OAAO,qBAAqB,eAAe,mBAAmB;AACrE,aAAO,OAAO,gBAAgB,eAAe,cAAc;AAa3D,YAAM,wBAAwB,aAAa,IAAI,KAAK,KAAK;AACzD,YAAM,oBAAoB,KAAK,MAAM,KAAK;AAC1C,YAAM,qBACJ,CAAC,yBAAyB,KAAK,UAAU;AAE3C,UAAI,CAAC,oBAAoB;AACvB,mBAAW,CAAC,OAAO,WAAW,KAAK,eAAe,oBAAoB;AACpE,6BAAmB,IAAI,OAAO,WAAW;AAAA,QAC3C;AAAA,MACF;AAcA,YAAM,aAAa,OAAO,KAAK,eAAe,mBAAmB,EAAE;AAAA,QACjE,CAAC,UACC,eAAe,oBAAoB,KAAK,MACxC,eAAe;AAAA,MAAA;AAEnB,UAAI,cAAc,eAAe,KAAK,OAAO;AAC3C,uBAAe,KAAK,KAAK,IAAI;AAAA,MAC/B;AAGA,YAAM,gBAAgB,eAAe;AAIrC,YAAM,iBAAiB,cAAc;AAAA,QACnC,IAAI,CAAC,SAAc;AACjB,gBAAM,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,IAAI;AAEtC,gBAAM,YAAY,YAAY,KAAK;AACnC,iBAAO,CAAC,KAAK,SAAS;AAAA,QACxB,CAAC;AAAA,MAAA;AAGH,aAAO;AAAA,QACL,OAAO,KAAK;AAAA,QACZ,OAAO;AAAA,QACP,cAAc,eAAe;AAAA,MAAA;AAAA,IAEjC;AAAA,IACA;AACE,YAAM,IAAI,yBAA0B,KAAa,IAAI;AAAA,EAAA;AAE3D;AAGA,SAAS,QAAQ,KAAmB;AAClC,SACE,eAAeC,SACd,OAAO,OAAO,QAAQ,YAAY,UAAU,OAAO,IAAI,SAAS;AAErE;AAGA,SAAS,YAAY,OAAiB;AACpC,SAAO,QAAQ,KAAK,IAAI,MAAM,QAAQ;AACxC;AAOA,SAAS,iBACP,gBACA,eACA,cACM;AAEN,MACE,eAAe,KAAK,SAAS,cAC7B,cAAc,KAAK,SAAS,YAC5B;AACA,iBAAa,IAAI,eAAe,KAAK,OAAO,cAAc,KAAK,KAAK;AAEpE;AAAA,MACE,eAAe,KAAK;AAAA,MACpB,cAAc,KAAK;AAAA,MACnB;AAAA,IAAA;AAAA,EAEJ;AAGA,MAAI,eAAe,QAAQ,cAAc,MAAM;AAC7C,aACM,IAAI,GACR,IAAI,eAAe,KAAK,UAAU,IAAI,cAAc,KAAK,QACzD,KACA;AACA,YAAM,gBAAgB,eAAe,KAAK,CAAC;AAC3C,YAAM,eAAe,cAAc,KAAK,CAAC;AAEzC,UACE,cAAc,KAAK,SAAS,cAC5B,aAAa,KAAK,SAAS,YAC3B;AACA,qBAAa,IAAI,cAAc,KAAK,OAAO,aAAa,KAAK,KAAK;AAElE;AAAA,UACE,cAAc,KAAK;AAAA,UACnB,aAAa,KAAK;AAAA,UAClB;AAAA,QAAA;AAAA,MAEJ;AAAA,IACF;AAAA,EACF;AACF;"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CompileQueryFn } from './index.js';
|
|
2
2
|
import { OrderByOptimizationInfo } from './order-by.js';
|
|
3
|
-
import { JoinClause, QueryIR } from '../ir.js';
|
|
3
|
+
import { BasicExpression, JoinClause, QueryIR } from '../ir.js';
|
|
4
4
|
import { Collection } from '../../collection/index.js';
|
|
5
5
|
import { KeyedStream, NamespacedAndKeyedStream } from '../../types.js';
|
|
6
6
|
import { QueryCache, QueryMapping, WindowOptions } from './types.js';
|
|
@@ -16,4 +16,4 @@ export type LazyCollectionCallbacks = {
|
|
|
16
16
|
* Processes all join clauses, applying lazy loading optimizations and maintaining
|
|
17
17
|
* alias tracking for per-alias subscriptions (enables self-joins).
|
|
18
18
|
*/
|
|
19
|
-
export declare function processJoins(pipeline: NamespacedAndKeyedStream, joinClauses: Array<JoinClause>, sources: Record<string, KeyedStream>, mainCollectionId: string, mainSource: string, allInputs: Record<string, KeyedStream>, cache: QueryCache, queryMapping: QueryMapping, collections: Record<string, Collection>, subscriptions: Record<string, CollectionSubscription>, callbacks: Record<string, LazyCollectionCallbacks>, lazySources: Set<string>, optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>, setWindowFn: (windowFn: (options: WindowOptions) => void) => void, rawQuery: QueryIR, onCompileSubquery: CompileQueryFn, aliasToCollectionId: Record<string, string>, aliasRemapping: Record<string, string
|
|
19
|
+
export declare function processJoins(pipeline: NamespacedAndKeyedStream, joinClauses: Array<JoinClause>, sources: Record<string, KeyedStream>, mainCollectionId: string, mainSource: string, allInputs: Record<string, KeyedStream>, cache: QueryCache, queryMapping: QueryMapping, collections: Record<string, Collection>, subscriptions: Record<string, CollectionSubscription>, callbacks: Record<string, LazyCollectionCallbacks>, lazySources: Set<string>, optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>, setWindowFn: (windowFn: (options: WindowOptions) => void) => void, rawQuery: QueryIR, onCompileSubquery: CompileQueryFn, aliasToCollectionId: Record<string, string>, aliasRemapping: Record<string, string>, sourceWhereClauses: Map<string, BasicExpression<boolean>>): NamespacedAndKeyedStream;
|
|
@@ -4,7 +4,7 @@ import { ensureIndexForField } from "../../indexes/auto-index.js";
|
|
|
4
4
|
import { followRef, PropRef } from "../ir.js";
|
|
5
5
|
import { inArray } from "../builder/functions.js";
|
|
6
6
|
import { compileExpression } from "./evaluators.js";
|
|
7
|
-
function processJoins(pipeline, joinClauses, sources, mainCollectionId, mainSource, allInputs, cache, queryMapping, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, rawQuery, onCompileSubquery, aliasToCollectionId, aliasRemapping) {
|
|
7
|
+
function processJoins(pipeline, joinClauses, sources, mainCollectionId, mainSource, allInputs, cache, queryMapping, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, rawQuery, onCompileSubquery, aliasToCollectionId, aliasRemapping, sourceWhereClauses) {
|
|
8
8
|
let resultPipeline = pipeline;
|
|
9
9
|
for (const joinClause of joinClauses) {
|
|
10
10
|
resultPipeline = processJoin(
|
|
@@ -25,12 +25,13 @@ function processJoins(pipeline, joinClauses, sources, mainCollectionId, mainSour
|
|
|
25
25
|
rawQuery,
|
|
26
26
|
onCompileSubquery,
|
|
27
27
|
aliasToCollectionId,
|
|
28
|
-
aliasRemapping
|
|
28
|
+
aliasRemapping,
|
|
29
|
+
sourceWhereClauses
|
|
29
30
|
);
|
|
30
31
|
}
|
|
31
32
|
return resultPipeline;
|
|
32
33
|
}
|
|
33
|
-
function processJoin(pipeline, joinClause, sources, mainCollectionId, mainSource, allInputs, cache, queryMapping, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, rawQuery, onCompileSubquery, aliasToCollectionId, aliasRemapping) {
|
|
34
|
+
function processJoin(pipeline, joinClause, sources, mainCollectionId, mainSource, allInputs, cache, queryMapping, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, rawQuery, onCompileSubquery, aliasToCollectionId, aliasRemapping, sourceWhereClauses) {
|
|
34
35
|
const isCollectionRef = joinClause.from.type === `collectionRef`;
|
|
35
36
|
const {
|
|
36
37
|
alias: joinedSource,
|
|
@@ -49,7 +50,8 @@ function processJoin(pipeline, joinClause, sources, mainCollectionId, mainSource
|
|
|
49
50
|
queryMapping,
|
|
50
51
|
onCompileSubquery,
|
|
51
52
|
aliasToCollectionId,
|
|
52
|
-
aliasRemapping
|
|
53
|
+
aliasRemapping,
|
|
54
|
+
sourceWhereClauses
|
|
53
55
|
);
|
|
54
56
|
sources[joinedSource] = joinedInput;
|
|
55
57
|
if (isCollectionRef) {
|
|
@@ -198,7 +200,7 @@ function getSourceAliasFromExpression(expr) {
|
|
|
198
200
|
return null;
|
|
199
201
|
}
|
|
200
202
|
}
|
|
201
|
-
function processJoinSource(from, allInputs, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, cache, queryMapping, onCompileSubquery, aliasToCollectionId, aliasRemapping) {
|
|
203
|
+
function processJoinSource(from, allInputs, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, cache, queryMapping, onCompileSubquery, aliasToCollectionId, aliasRemapping, sourceWhereClauses) {
|
|
202
204
|
switch (from.type) {
|
|
203
205
|
case `collectionRef`: {
|
|
204
206
|
const input = allInputs[from.alias];
|
|
@@ -228,6 +230,14 @@ function processJoinSource(from, allInputs, collections, subscriptions, callback
|
|
|
228
230
|
);
|
|
229
231
|
Object.assign(aliasToCollectionId, subQueryResult.aliasToCollectionId);
|
|
230
232
|
Object.assign(aliasRemapping, subQueryResult.aliasRemapping);
|
|
233
|
+
const isUserDefinedSubquery = queryMapping.has(from.query);
|
|
234
|
+
const fromInnerAlias = from.query.from.alias;
|
|
235
|
+
const isOptimizerCreated = !isUserDefinedSubquery && from.alias === fromInnerAlias;
|
|
236
|
+
if (!isOptimizerCreated) {
|
|
237
|
+
for (const [alias, whereClause] of subQueryResult.sourceWhereClauses) {
|
|
238
|
+
sourceWhereClauses.set(alias, whereClause);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
231
241
|
const innerAlias = Object.keys(subQueryResult.aliasToCollectionId).find(
|
|
232
242
|
(alias) => subQueryResult.aliasToCollectionId[alias] === subQueryResult.collectionId
|
|
233
243
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"joins.js","sources":["../../../../src/query/compiler/joins.ts"],"sourcesContent":["import { filter, join as joinOperator, map, tap } from '@tanstack/db-ivm'\nimport {\n CollectionInputNotFoundError,\n InvalidJoinCondition,\n InvalidJoinConditionLeftSourceError,\n InvalidJoinConditionRightSourceError,\n InvalidJoinConditionSameSourceError,\n InvalidJoinConditionSourceMismatchError,\n JoinCollectionNotFoundError,\n SubscriptionNotFoundError,\n UnsupportedJoinSourceTypeError,\n UnsupportedJoinTypeError,\n} from '../../errors.js'\nimport { ensureIndexForField } from '../../indexes/auto-index.js'\nimport { PropRef, followRef } from '../ir.js'\nimport { inArray } from '../builder/functions.js'\nimport { compileExpression } from './evaluators.js'\nimport type { CompileQueryFn } from './index.js'\nimport type { OrderByOptimizationInfo } from './order-by.js'\nimport type {\n BasicExpression,\n CollectionRef,\n JoinClause,\n QueryIR,\n QueryRef,\n} from '../ir.js'\nimport type { IStreamBuilder, JoinType } from '@tanstack/db-ivm'\nimport type { Collection } from '../../collection/index.js'\nimport type {\n KeyedStream,\n NamespacedAndKeyedStream,\n NamespacedRow,\n} from '../../types.js'\nimport type { QueryCache, QueryMapping, WindowOptions } from './types.js'\nimport type { CollectionSubscription } from '../../collection/subscription.js'\n\n/** Function type for loading specific keys into a lazy collection */\nexport type LoadKeysFn = (key: Set<string | number>) => void\n\n/** Callbacks for managing lazy-loaded collections in optimized joins */\nexport type LazyCollectionCallbacks = {\n loadKeys: LoadKeysFn\n loadInitialState: () => void\n}\n\n/**\n * Processes all join clauses, applying lazy loading optimizations and maintaining\n * alias tracking for per-alias subscriptions (enables self-joins).\n */\nexport function processJoins(\n pipeline: NamespacedAndKeyedStream,\n joinClauses: Array<JoinClause>,\n sources: Record<string, KeyedStream>,\n mainCollectionId: string,\n mainSource: string,\n allInputs: Record<string, KeyedStream>,\n cache: QueryCache,\n queryMapping: QueryMapping,\n collections: Record<string, Collection>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazySources: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n rawQuery: QueryIR,\n onCompileSubquery: CompileQueryFn,\n aliasToCollectionId: Record<string, string>,\n aliasRemapping: Record<string, string>,\n): NamespacedAndKeyedStream {\n let resultPipeline = pipeline\n\n for (const joinClause of joinClauses) {\n resultPipeline = processJoin(\n resultPipeline,\n joinClause,\n sources,\n mainCollectionId,\n mainSource,\n allInputs,\n cache,\n queryMapping,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n rawQuery,\n onCompileSubquery,\n aliasToCollectionId,\n aliasRemapping,\n )\n }\n\n return resultPipeline\n}\n\n/**\n * Processes a single join clause with lazy loading optimization.\n * For LEFT/RIGHT/INNER joins, marks one side as \"lazy\" (loads on-demand based on join keys).\n */\nfunction processJoin(\n pipeline: NamespacedAndKeyedStream,\n joinClause: JoinClause,\n sources: Record<string, KeyedStream>,\n mainCollectionId: string,\n mainSource: string,\n allInputs: Record<string, KeyedStream>,\n cache: QueryCache,\n queryMapping: QueryMapping,\n collections: Record<string, Collection>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazySources: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n rawQuery: QueryIR,\n onCompileSubquery: CompileQueryFn,\n aliasToCollectionId: Record<string, string>,\n aliasRemapping: Record<string, string>,\n): NamespacedAndKeyedStream {\n const isCollectionRef = joinClause.from.type === `collectionRef`\n\n // Get the joined source alias and input stream\n const {\n alias: joinedSource,\n input: joinedInput,\n collectionId: joinedCollectionId,\n } = processJoinSource(\n joinClause.from,\n allInputs,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n cache,\n queryMapping,\n onCompileSubquery,\n aliasToCollectionId,\n aliasRemapping,\n )\n\n // Add the joined source to the sources map\n sources[joinedSource] = joinedInput\n if (isCollectionRef) {\n // Only direct collection references form new alias bindings. Subquery\n // aliases reuse the mapping returned from the recursive compilation above.\n aliasToCollectionId[joinedSource] = joinedCollectionId\n }\n\n const mainCollection = collections[mainCollectionId]\n const joinedCollection = collections[joinedCollectionId]\n\n if (!mainCollection) {\n throw new JoinCollectionNotFoundError(mainCollectionId)\n }\n\n if (!joinedCollection) {\n throw new JoinCollectionNotFoundError(joinedCollectionId)\n }\n\n const { activeSource, lazySource } = getActiveAndLazySources(\n joinClause.type,\n mainCollection,\n joinedCollection,\n )\n\n // Analyze which source each expression refers to and swap if necessary\n const availableSources = Object.keys(sources)\n const { mainExpr, joinedExpr } = analyzeJoinExpressions(\n joinClause.left,\n joinClause.right,\n availableSources,\n joinedSource,\n )\n\n // Pre-compile the join expressions\n const compiledMainExpr = compileExpression(mainExpr)\n const compiledJoinedExpr = compileExpression(joinedExpr)\n\n // Prepare the main pipeline for joining\n let mainPipeline = pipeline.pipe(\n map(([currentKey, namespacedRow]) => {\n // Extract the join key from the main source expression\n const mainKey = compiledMainExpr(namespacedRow)\n\n // Return [joinKey, [originalKey, namespacedRow]]\n return [mainKey, [currentKey, namespacedRow]] as [\n unknown,\n [string, typeof namespacedRow],\n ]\n }),\n )\n\n // Prepare the joined pipeline\n let joinedPipeline = joinedInput.pipe(\n map(([currentKey, row]) => {\n // Wrap the row in a namespaced structure\n const namespacedRow: NamespacedRow = { [joinedSource]: row }\n\n // Extract the join key from the joined source expression\n const joinedKey = compiledJoinedExpr(namespacedRow)\n\n // Return [joinKey, [originalKey, namespacedRow]]\n return [joinedKey, [currentKey, namespacedRow]] as [\n unknown,\n [string, typeof namespacedRow],\n ]\n }),\n )\n\n // Apply the join operation\n if (![`inner`, `left`, `right`, `full`].includes(joinClause.type)) {\n throw new UnsupportedJoinTypeError(joinClause.type)\n }\n\n if (activeSource) {\n // If the lazy collection comes from a subquery that has a limit and/or an offset clause\n // then we need to deoptimize the join because we don't know which rows are in the result set\n // since we simply lookup matching keys in the index but the index contains all rows\n // (not just the ones that pass the limit and offset clauses)\n const lazyFrom = activeSource === `main` ? joinClause.from : rawQuery.from\n const limitedSubquery =\n lazyFrom.type === `queryRef` &&\n (lazyFrom.query.limit || lazyFrom.query.offset)\n\n // If join expressions contain computed values (like concat functions)\n // we don't optimize the join because we don't have an index over the computed values\n const hasComputedJoinExpr =\n mainExpr.type === `func` || joinedExpr.type === `func`\n\n if (!limitedSubquery && !hasComputedJoinExpr) {\n // This join can be optimized by having the active collection\n // dynamically load keys into the lazy collection\n // based on the value of the joinKey and by looking up\n // matching rows in the index of the lazy collection\n\n // Mark the lazy source alias as lazy\n // this Set is passed by the liveQueryCollection to the compiler\n // such that the liveQueryCollection can check it after compilation\n // to know which source aliases should load data lazily (not initially)\n const lazyAlias = activeSource === `main` ? joinedSource : mainSource\n lazySources.add(lazyAlias)\n\n const activePipeline =\n activeSource === `main` ? mainPipeline : joinedPipeline\n\n const lazySourceJoinExpr =\n activeSource === `main`\n ? (joinedExpr as PropRef)\n : (mainExpr as PropRef)\n\n const followRefResult = followRef(\n rawQuery,\n lazySourceJoinExpr,\n lazySource,\n )!\n const followRefCollection = followRefResult.collection\n\n const fieldName = followRefResult.path[0]\n if (fieldName) {\n ensureIndexForField(\n fieldName,\n followRefResult.path,\n followRefCollection,\n )\n }\n\n // Set up lazy loading: intercept active side's stream and dynamically load\n // matching rows from lazy side based on join keys.\n const activePipelineWithLoading: IStreamBuilder<\n [key: unknown, [originalKey: string, namespacedRow: NamespacedRow]]\n > = activePipeline.pipe(\n tap((data) => {\n // Find the subscription for lazy loading.\n // Subscriptions are keyed by the innermost alias (where the collection subscription\n // was actually created). For subqueries, the join alias may differ from the inner alias.\n // aliasRemapping provides a flattened one-hop lookup from outer → innermost alias.\n // Example: .join({ activeUser: subquery }) where subquery uses .from({ user: collection })\n // → aliasRemapping['activeUser'] = 'user' (always maps directly to innermost, never recursive)\n const resolvedAlias = aliasRemapping[lazyAlias] || lazyAlias\n const lazySourceSubscription = subscriptions[resolvedAlias]\n\n if (!lazySourceSubscription) {\n throw new SubscriptionNotFoundError(\n resolvedAlias,\n lazyAlias,\n lazySource.id,\n Object.keys(subscriptions),\n )\n }\n\n if (lazySourceSubscription.hasLoadedInitialState()) {\n // Entire state was already loaded because we deoptimized the join\n return\n }\n\n // Request filtered snapshot from lazy collection for matching join keys\n const joinKeys = data.getInner().map(([[joinKey]]) => joinKey)\n const lazyJoinRef = new PropRef(followRefResult.path)\n const loaded = lazySourceSubscription.requestSnapshot({\n where: inArray(lazyJoinRef, joinKeys),\n optimizedOnly: true,\n })\n\n if (!loaded) {\n // Snapshot wasn't sent because it could not be loaded from the indexes\n lazySourceSubscription.requestSnapshot()\n }\n }),\n )\n\n if (activeSource === `main`) {\n mainPipeline = activePipelineWithLoading\n } else {\n joinedPipeline = activePipelineWithLoading\n }\n }\n }\n\n return mainPipeline.pipe(\n joinOperator(joinedPipeline, joinClause.type as JoinType),\n processJoinResults(joinClause.type),\n )\n}\n\n/**\n * Analyzes join expressions to determine which refers to which source\n * and returns them in the correct order (available source expression first, joined source expression second)\n */\nfunction analyzeJoinExpressions(\n left: BasicExpression,\n right: BasicExpression,\n allAvailableSourceAliases: Array<string>,\n joinedSource: string,\n): { mainExpr: BasicExpression; joinedExpr: BasicExpression } {\n // Filter out the joined source alias from the available source aliases\n const availableSources = allAvailableSourceAliases.filter(\n (alias) => alias !== joinedSource,\n )\n\n const leftSourceAlias = getSourceAliasFromExpression(left)\n const rightSourceAlias = getSourceAliasFromExpression(right)\n\n // If left expression refers to an available source and right refers to joined source, keep as is\n if (\n leftSourceAlias &&\n availableSources.includes(leftSourceAlias) &&\n rightSourceAlias === joinedSource\n ) {\n return { mainExpr: left, joinedExpr: right }\n }\n\n // If left expression refers to joined source and right refers to an available source, swap them\n if (\n leftSourceAlias === joinedSource &&\n rightSourceAlias &&\n availableSources.includes(rightSourceAlias)\n ) {\n return { mainExpr: right, joinedExpr: left }\n }\n\n // If one expression doesn't refer to any source, this is an invalid join\n if (!leftSourceAlias || !rightSourceAlias) {\n throw new InvalidJoinConditionSourceMismatchError()\n }\n\n // If both expressions refer to the same alias, this is an invalid join\n if (leftSourceAlias === rightSourceAlias) {\n throw new InvalidJoinConditionSameSourceError(leftSourceAlias)\n }\n\n // Left side must refer to an available source\n // This cannot happen with the query builder as there is no way to build a ref\n // to an unavailable source, but just in case, but could happen with the IR\n if (!availableSources.includes(leftSourceAlias)) {\n throw new InvalidJoinConditionLeftSourceError(leftSourceAlias)\n }\n\n // Right side must refer to the joined source\n if (rightSourceAlias !== joinedSource) {\n throw new InvalidJoinConditionRightSourceError(joinedSource)\n }\n\n // This should not be reachable given the logic above, but just in case\n throw new InvalidJoinCondition()\n}\n\n/**\n * Extracts the source alias from a join expression\n */\nfunction getSourceAliasFromExpression(expr: BasicExpression): string | null {\n switch (expr.type) {\n case `ref`:\n // PropRef path has the source alias as the first element\n return expr.path[0] || null\n case `func`: {\n // For function expressions, we need to check if all arguments refer to the same source\n const sourceAliases = new Set<string>()\n for (const arg of expr.args) {\n const alias = getSourceAliasFromExpression(arg)\n if (alias) {\n sourceAliases.add(alias)\n }\n }\n // If all arguments refer to the same source, return that source alias\n return sourceAliases.size === 1 ? Array.from(sourceAliases)[0]! : null\n }\n default:\n // Values (type='val') don't reference any source\n return null\n }\n}\n\n/**\n * Processes the join source (collection or sub-query)\n */\nfunction processJoinSource(\n from: CollectionRef | QueryRef,\n allInputs: Record<string, KeyedStream>,\n collections: Record<string, Collection>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazySources: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n cache: QueryCache,\n queryMapping: QueryMapping,\n onCompileSubquery: CompileQueryFn,\n aliasToCollectionId: Record<string, string>,\n aliasRemapping: Record<string, string>,\n): { alias: string; input: KeyedStream; collectionId: string } {\n switch (from.type) {\n case `collectionRef`: {\n const input = allInputs[from.alias]\n if (!input) {\n throw new CollectionInputNotFoundError(\n from.alias,\n from.collection.id,\n Object.keys(allInputs),\n )\n }\n aliasToCollectionId[from.alias] = from.collection.id\n return { alias: from.alias, input, collectionId: from.collection.id }\n }\n case `queryRef`: {\n // Find the original query for caching purposes\n const originalQuery = queryMapping.get(from.query) || from.query\n\n // Recursively compile the sub-query with cache\n const subQueryResult = onCompileSubquery(\n originalQuery,\n allInputs,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n cache,\n queryMapping,\n )\n\n // Pull up alias mappings from subquery to parent scope.\n // This includes both the innermost alias-to-collection mappings AND\n // any existing remappings from nested subquery levels.\n Object.assign(aliasToCollectionId, subQueryResult.aliasToCollectionId)\n Object.assign(aliasRemapping, subQueryResult.aliasRemapping)\n\n // Create a flattened remapping from outer alias to innermost alias.\n // For nested subqueries, this ensures one-hop lookups (not recursive chains).\n //\n // Example with 3-level nesting:\n // Inner: .from({ user: usersCollection })\n // Middle: .from({ activeUser: innerSubquery }) → creates: activeUser → user\n // Outer: .join({ author: middleSubquery }, ...) → creates: author → user (not author → activeUser)\n //\n // We search through the PULLED-UP aliasToCollectionId (which contains the\n // innermost 'user' alias), so we always map directly to the deepest level.\n // This means aliasRemapping[lazyAlias] is always a single lookup, never recursive.\n const innerAlias = Object.keys(subQueryResult.aliasToCollectionId).find(\n (alias) =>\n subQueryResult.aliasToCollectionId[alias] ===\n subQueryResult.collectionId,\n )\n if (innerAlias && innerAlias !== from.alias) {\n aliasRemapping[from.alias] = innerAlias\n }\n\n // Extract the pipeline from the compilation result\n const subQueryInput = subQueryResult.pipeline\n\n // Subqueries may return [key, [value, orderByIndex]] (with ORDER BY) or [key, value] (without ORDER BY)\n // We need to extract just the value for use in parent queries\n const extractedInput = subQueryInput.pipe(\n map((data: any) => {\n const [key, [value, _orderByIndex]] = data\n return [key, value] as [unknown, any]\n }),\n )\n\n return {\n alias: from.alias,\n input: extractedInput as KeyedStream,\n collectionId: subQueryResult.collectionId,\n }\n }\n default:\n throw new UnsupportedJoinSourceTypeError((from as any).type)\n }\n}\n\n/**\n * Processes the results of a join operation\n */\nfunction processJoinResults(joinType: string) {\n return function (\n pipeline: IStreamBuilder<\n [\n key: string,\n [\n [string, NamespacedRow] | undefined,\n [string, NamespacedRow] | undefined,\n ],\n ]\n >,\n ): NamespacedAndKeyedStream {\n return pipeline.pipe(\n // Process the join result and handle nulls\n filter((result) => {\n const [_key, [main, joined]] = result\n const mainNamespacedRow = main?.[1]\n const joinedNamespacedRow = joined?.[1]\n\n // Handle different join types\n if (joinType === `inner`) {\n return !!(mainNamespacedRow && joinedNamespacedRow)\n }\n\n if (joinType === `left`) {\n return !!mainNamespacedRow\n }\n\n if (joinType === `right`) {\n return !!joinedNamespacedRow\n }\n\n // For full joins, always include\n return true\n }),\n map((result) => {\n const [_key, [main, joined]] = result\n const mainKey = main?.[0]\n const mainNamespacedRow = main?.[1]\n const joinedKey = joined?.[0]\n const joinedNamespacedRow = joined?.[1]\n\n // Merge the namespaced rows\n const mergedNamespacedRow: NamespacedRow = {}\n\n // Add main row data if it exists\n if (mainNamespacedRow) {\n Object.assign(mergedNamespacedRow, mainNamespacedRow)\n }\n\n // Add joined row data if it exists\n if (joinedNamespacedRow) {\n Object.assign(mergedNamespacedRow, joinedNamespacedRow)\n }\n\n // We create a composite key that combines the main and joined keys\n const resultKey = `[${mainKey},${joinedKey}]`\n\n return [resultKey, mergedNamespacedRow] as [string, NamespacedRow]\n }),\n )\n }\n}\n\n/**\n * Returns the active and lazy collections for a join clause.\n * The active collection is the one that we need to fully iterate over\n * and it can be the main source (i.e. left collection) or the joined source (i.e. right collection).\n * The lazy collection is the one that we should join-in lazily based on matches in the active collection.\n * @param joinClause - The join clause to analyze\n * @param leftCollection - The left collection\n * @param rightCollection - The right collection\n * @returns The active and lazy collections. They are undefined if we need to loop over both collections (i.e. both are active)\n */\nfunction getActiveAndLazySources(\n joinType: JoinClause[`type`],\n leftCollection: Collection,\n rightCollection: Collection,\n):\n | { activeSource: `main` | `joined`; lazySource: Collection }\n | { activeSource: undefined; lazySource: undefined } {\n // Self-joins can now be optimized since we track lazy loading by source alias\n // rather than collection ID. Each alias has its own subscription and lazy state.\n\n switch (joinType) {\n case `left`:\n return { activeSource: `main`, lazySource: rightCollection }\n case `right`:\n return { activeSource: `joined`, lazySource: leftCollection }\n case `inner`:\n // The smallest collection should be the active collection\n // and the biggest collection should be lazy\n return leftCollection.size < rightCollection.size\n ? { activeSource: `main`, lazySource: rightCollection }\n : { activeSource: `joined`, lazySource: leftCollection }\n default:\n return { activeSource: undefined, lazySource: undefined }\n }\n}\n"],"names":["joinOperator"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"joins.js","sources":["../../../../src/query/compiler/joins.ts"],"sourcesContent":["import { filter, join as joinOperator, map, tap } from '@tanstack/db-ivm'\nimport {\n CollectionInputNotFoundError,\n InvalidJoinCondition,\n InvalidJoinConditionLeftSourceError,\n InvalidJoinConditionRightSourceError,\n InvalidJoinConditionSameSourceError,\n InvalidJoinConditionSourceMismatchError,\n JoinCollectionNotFoundError,\n SubscriptionNotFoundError,\n UnsupportedJoinSourceTypeError,\n UnsupportedJoinTypeError,\n} from '../../errors.js'\nimport { ensureIndexForField } from '../../indexes/auto-index.js'\nimport { PropRef, followRef } from '../ir.js'\nimport { inArray } from '../builder/functions.js'\nimport { compileExpression } from './evaluators.js'\nimport type { CompileQueryFn } from './index.js'\nimport type { OrderByOptimizationInfo } from './order-by.js'\nimport type {\n BasicExpression,\n CollectionRef,\n JoinClause,\n QueryIR,\n QueryRef,\n} from '../ir.js'\nimport type { IStreamBuilder, JoinType } from '@tanstack/db-ivm'\nimport type { Collection } from '../../collection/index.js'\nimport type {\n KeyedStream,\n NamespacedAndKeyedStream,\n NamespacedRow,\n} from '../../types.js'\nimport type { QueryCache, QueryMapping, WindowOptions } from './types.js'\nimport type { CollectionSubscription } from '../../collection/subscription.js'\n\n/** Function type for loading specific keys into a lazy collection */\nexport type LoadKeysFn = (key: Set<string | number>) => void\n\n/** Callbacks for managing lazy-loaded collections in optimized joins */\nexport type LazyCollectionCallbacks = {\n loadKeys: LoadKeysFn\n loadInitialState: () => void\n}\n\n/**\n * Processes all join clauses, applying lazy loading optimizations and maintaining\n * alias tracking for per-alias subscriptions (enables self-joins).\n */\nexport function processJoins(\n pipeline: NamespacedAndKeyedStream,\n joinClauses: Array<JoinClause>,\n sources: Record<string, KeyedStream>,\n mainCollectionId: string,\n mainSource: string,\n allInputs: Record<string, KeyedStream>,\n cache: QueryCache,\n queryMapping: QueryMapping,\n collections: Record<string, Collection>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazySources: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n rawQuery: QueryIR,\n onCompileSubquery: CompileQueryFn,\n aliasToCollectionId: Record<string, string>,\n aliasRemapping: Record<string, string>,\n sourceWhereClauses: Map<string, BasicExpression<boolean>>,\n): NamespacedAndKeyedStream {\n let resultPipeline = pipeline\n\n for (const joinClause of joinClauses) {\n resultPipeline = processJoin(\n resultPipeline,\n joinClause,\n sources,\n mainCollectionId,\n mainSource,\n allInputs,\n cache,\n queryMapping,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n rawQuery,\n onCompileSubquery,\n aliasToCollectionId,\n aliasRemapping,\n sourceWhereClauses,\n )\n }\n\n return resultPipeline\n}\n\n/**\n * Processes a single join clause with lazy loading optimization.\n * For LEFT/RIGHT/INNER joins, marks one side as \"lazy\" (loads on-demand based on join keys).\n */\nfunction processJoin(\n pipeline: NamespacedAndKeyedStream,\n joinClause: JoinClause,\n sources: Record<string, KeyedStream>,\n mainCollectionId: string,\n mainSource: string,\n allInputs: Record<string, KeyedStream>,\n cache: QueryCache,\n queryMapping: QueryMapping,\n collections: Record<string, Collection>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazySources: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n rawQuery: QueryIR,\n onCompileSubquery: CompileQueryFn,\n aliasToCollectionId: Record<string, string>,\n aliasRemapping: Record<string, string>,\n sourceWhereClauses: Map<string, BasicExpression<boolean>>,\n): NamespacedAndKeyedStream {\n const isCollectionRef = joinClause.from.type === `collectionRef`\n\n // Get the joined source alias and input stream\n const {\n alias: joinedSource,\n input: joinedInput,\n collectionId: joinedCollectionId,\n } = processJoinSource(\n joinClause.from,\n allInputs,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n cache,\n queryMapping,\n onCompileSubquery,\n aliasToCollectionId,\n aliasRemapping,\n sourceWhereClauses,\n )\n\n // Add the joined source to the sources map\n sources[joinedSource] = joinedInput\n if (isCollectionRef) {\n // Only direct collection references form new alias bindings. Subquery\n // aliases reuse the mapping returned from the recursive compilation above.\n aliasToCollectionId[joinedSource] = joinedCollectionId\n }\n\n const mainCollection = collections[mainCollectionId]\n const joinedCollection = collections[joinedCollectionId]\n\n if (!mainCollection) {\n throw new JoinCollectionNotFoundError(mainCollectionId)\n }\n\n if (!joinedCollection) {\n throw new JoinCollectionNotFoundError(joinedCollectionId)\n }\n\n const { activeSource, lazySource } = getActiveAndLazySources(\n joinClause.type,\n mainCollection,\n joinedCollection,\n )\n\n // Analyze which source each expression refers to and swap if necessary\n const availableSources = Object.keys(sources)\n const { mainExpr, joinedExpr } = analyzeJoinExpressions(\n joinClause.left,\n joinClause.right,\n availableSources,\n joinedSource,\n )\n\n // Pre-compile the join expressions\n const compiledMainExpr = compileExpression(mainExpr)\n const compiledJoinedExpr = compileExpression(joinedExpr)\n\n // Prepare the main pipeline for joining\n let mainPipeline = pipeline.pipe(\n map(([currentKey, namespacedRow]) => {\n // Extract the join key from the main source expression\n const mainKey = compiledMainExpr(namespacedRow)\n\n // Return [joinKey, [originalKey, namespacedRow]]\n return [mainKey, [currentKey, namespacedRow]] as [\n unknown,\n [string, typeof namespacedRow],\n ]\n }),\n )\n\n // Prepare the joined pipeline\n let joinedPipeline = joinedInput.pipe(\n map(([currentKey, row]) => {\n // Wrap the row in a namespaced structure\n const namespacedRow: NamespacedRow = { [joinedSource]: row }\n\n // Extract the join key from the joined source expression\n const joinedKey = compiledJoinedExpr(namespacedRow)\n\n // Return [joinKey, [originalKey, namespacedRow]]\n return [joinedKey, [currentKey, namespacedRow]] as [\n unknown,\n [string, typeof namespacedRow],\n ]\n }),\n )\n\n // Apply the join operation\n if (![`inner`, `left`, `right`, `full`].includes(joinClause.type)) {\n throw new UnsupportedJoinTypeError(joinClause.type)\n }\n\n if (activeSource) {\n // If the lazy collection comes from a subquery that has a limit and/or an offset clause\n // then we need to deoptimize the join because we don't know which rows are in the result set\n // since we simply lookup matching keys in the index but the index contains all rows\n // (not just the ones that pass the limit and offset clauses)\n const lazyFrom = activeSource === `main` ? joinClause.from : rawQuery.from\n const limitedSubquery =\n lazyFrom.type === `queryRef` &&\n (lazyFrom.query.limit || lazyFrom.query.offset)\n\n // If join expressions contain computed values (like concat functions)\n // we don't optimize the join because we don't have an index over the computed values\n const hasComputedJoinExpr =\n mainExpr.type === `func` || joinedExpr.type === `func`\n\n if (!limitedSubquery && !hasComputedJoinExpr) {\n // This join can be optimized by having the active collection\n // dynamically load keys into the lazy collection\n // based on the value of the joinKey and by looking up\n // matching rows in the index of the lazy collection\n\n // Mark the lazy source alias as lazy\n // this Set is passed by the liveQueryCollection to the compiler\n // such that the liveQueryCollection can check it after compilation\n // to know which source aliases should load data lazily (not initially)\n const lazyAlias = activeSource === `main` ? joinedSource : mainSource\n lazySources.add(lazyAlias)\n\n const activePipeline =\n activeSource === `main` ? mainPipeline : joinedPipeline\n\n const lazySourceJoinExpr =\n activeSource === `main`\n ? (joinedExpr as PropRef)\n : (mainExpr as PropRef)\n\n const followRefResult = followRef(\n rawQuery,\n lazySourceJoinExpr,\n lazySource,\n )!\n const followRefCollection = followRefResult.collection\n\n const fieldName = followRefResult.path[0]\n if (fieldName) {\n ensureIndexForField(\n fieldName,\n followRefResult.path,\n followRefCollection,\n )\n }\n\n // Set up lazy loading: intercept active side's stream and dynamically load\n // matching rows from lazy side based on join keys.\n const activePipelineWithLoading: IStreamBuilder<\n [key: unknown, [originalKey: string, namespacedRow: NamespacedRow]]\n > = activePipeline.pipe(\n tap((data) => {\n // Find the subscription for lazy loading.\n // Subscriptions are keyed by the innermost alias (where the collection subscription\n // was actually created). For subqueries, the join alias may differ from the inner alias.\n // aliasRemapping provides a flattened one-hop lookup from outer → innermost alias.\n // Example: .join({ activeUser: subquery }) where subquery uses .from({ user: collection })\n // → aliasRemapping['activeUser'] = 'user' (always maps directly to innermost, never recursive)\n const resolvedAlias = aliasRemapping[lazyAlias] || lazyAlias\n const lazySourceSubscription = subscriptions[resolvedAlias]\n\n if (!lazySourceSubscription) {\n throw new SubscriptionNotFoundError(\n resolvedAlias,\n lazyAlias,\n lazySource.id,\n Object.keys(subscriptions),\n )\n }\n\n if (lazySourceSubscription.hasLoadedInitialState()) {\n // Entire state was already loaded because we deoptimized the join\n return\n }\n\n // Request filtered snapshot from lazy collection for matching join keys\n const joinKeys = data.getInner().map(([[joinKey]]) => joinKey)\n const lazyJoinRef = new PropRef(followRefResult.path)\n const loaded = lazySourceSubscription.requestSnapshot({\n where: inArray(lazyJoinRef, joinKeys),\n optimizedOnly: true,\n })\n\n if (!loaded) {\n // Snapshot wasn't sent because it could not be loaded from the indexes\n lazySourceSubscription.requestSnapshot()\n }\n }),\n )\n\n if (activeSource === `main`) {\n mainPipeline = activePipelineWithLoading\n } else {\n joinedPipeline = activePipelineWithLoading\n }\n }\n }\n\n return mainPipeline.pipe(\n joinOperator(joinedPipeline, joinClause.type as JoinType),\n processJoinResults(joinClause.type),\n )\n}\n\n/**\n * Analyzes join expressions to determine which refers to which source\n * and returns them in the correct order (available source expression first, joined source expression second)\n */\nfunction analyzeJoinExpressions(\n left: BasicExpression,\n right: BasicExpression,\n allAvailableSourceAliases: Array<string>,\n joinedSource: string,\n): { mainExpr: BasicExpression; joinedExpr: BasicExpression } {\n // Filter out the joined source alias from the available source aliases\n const availableSources = allAvailableSourceAliases.filter(\n (alias) => alias !== joinedSource,\n )\n\n const leftSourceAlias = getSourceAliasFromExpression(left)\n const rightSourceAlias = getSourceAliasFromExpression(right)\n\n // If left expression refers to an available source and right refers to joined source, keep as is\n if (\n leftSourceAlias &&\n availableSources.includes(leftSourceAlias) &&\n rightSourceAlias === joinedSource\n ) {\n return { mainExpr: left, joinedExpr: right }\n }\n\n // If left expression refers to joined source and right refers to an available source, swap them\n if (\n leftSourceAlias === joinedSource &&\n rightSourceAlias &&\n availableSources.includes(rightSourceAlias)\n ) {\n return { mainExpr: right, joinedExpr: left }\n }\n\n // If one expression doesn't refer to any source, this is an invalid join\n if (!leftSourceAlias || !rightSourceAlias) {\n throw new InvalidJoinConditionSourceMismatchError()\n }\n\n // If both expressions refer to the same alias, this is an invalid join\n if (leftSourceAlias === rightSourceAlias) {\n throw new InvalidJoinConditionSameSourceError(leftSourceAlias)\n }\n\n // Left side must refer to an available source\n // This cannot happen with the query builder as there is no way to build a ref\n // to an unavailable source, but just in case, but could happen with the IR\n if (!availableSources.includes(leftSourceAlias)) {\n throw new InvalidJoinConditionLeftSourceError(leftSourceAlias)\n }\n\n // Right side must refer to the joined source\n if (rightSourceAlias !== joinedSource) {\n throw new InvalidJoinConditionRightSourceError(joinedSource)\n }\n\n // This should not be reachable given the logic above, but just in case\n throw new InvalidJoinCondition()\n}\n\n/**\n * Extracts the source alias from a join expression\n */\nfunction getSourceAliasFromExpression(expr: BasicExpression): string | null {\n switch (expr.type) {\n case `ref`:\n // PropRef path has the source alias as the first element\n return expr.path[0] || null\n case `func`: {\n // For function expressions, we need to check if all arguments refer to the same source\n const sourceAliases = new Set<string>()\n for (const arg of expr.args) {\n const alias = getSourceAliasFromExpression(arg)\n if (alias) {\n sourceAliases.add(alias)\n }\n }\n // If all arguments refer to the same source, return that source alias\n return sourceAliases.size === 1 ? Array.from(sourceAliases)[0]! : null\n }\n default:\n // Values (type='val') don't reference any source\n return null\n }\n}\n\n/**\n * Processes the join source (collection or sub-query)\n */\nfunction processJoinSource(\n from: CollectionRef | QueryRef,\n allInputs: Record<string, KeyedStream>,\n collections: Record<string, Collection>,\n subscriptions: Record<string, CollectionSubscription>,\n callbacks: Record<string, LazyCollectionCallbacks>,\n lazySources: Set<string>,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n cache: QueryCache,\n queryMapping: QueryMapping,\n onCompileSubquery: CompileQueryFn,\n aliasToCollectionId: Record<string, string>,\n aliasRemapping: Record<string, string>,\n sourceWhereClauses: Map<string, BasicExpression<boolean>>,\n): { alias: string; input: KeyedStream; collectionId: string } {\n switch (from.type) {\n case `collectionRef`: {\n const input = allInputs[from.alias]\n if (!input) {\n throw new CollectionInputNotFoundError(\n from.alias,\n from.collection.id,\n Object.keys(allInputs),\n )\n }\n aliasToCollectionId[from.alias] = from.collection.id\n return { alias: from.alias, input, collectionId: from.collection.id }\n }\n case `queryRef`: {\n // Find the original query for caching purposes\n const originalQuery = queryMapping.get(from.query) || from.query\n\n // Recursively compile the sub-query with cache\n const subQueryResult = onCompileSubquery(\n originalQuery,\n allInputs,\n collections,\n subscriptions,\n callbacks,\n lazySources,\n optimizableOrderByCollections,\n setWindowFn,\n cache,\n queryMapping,\n )\n\n // Pull up alias mappings from subquery to parent scope.\n // This includes both the innermost alias-to-collection mappings AND\n // any existing remappings from nested subquery levels.\n Object.assign(aliasToCollectionId, subQueryResult.aliasToCollectionId)\n Object.assign(aliasRemapping, subQueryResult.aliasRemapping)\n\n // Pull up source WHERE clauses from subquery to parent scope.\n // This enables loadSubset to receive the correct where clauses for subquery collections.\n //\n // IMPORTANT: Skip pull-up for optimizer-created subqueries. These are detected when:\n // 1. The outer alias (from.alias) matches the inner alias (from.query.from.alias)\n // 2. The subquery was found in queryMapping (it's a user-defined subquery, not optimizer-created)\n //\n // For optimizer-created subqueries, the parent already has the sourceWhereClauses\n // extracted from the original raw query, so pulling up would be redundant.\n const isUserDefinedSubquery = queryMapping.has(from.query)\n const fromInnerAlias = from.query.from.alias\n const isOptimizerCreated =\n !isUserDefinedSubquery && from.alias === fromInnerAlias\n\n if (!isOptimizerCreated) {\n for (const [alias, whereClause] of subQueryResult.sourceWhereClauses) {\n sourceWhereClauses.set(alias, whereClause)\n }\n }\n\n // Create a flattened remapping from outer alias to innermost alias.\n // For nested subqueries, this ensures one-hop lookups (not recursive chains).\n //\n // Example with 3-level nesting:\n // Inner: .from({ user: usersCollection })\n // Middle: .from({ activeUser: innerSubquery }) → creates: activeUser → user\n // Outer: .join({ author: middleSubquery }, ...) → creates: author → user (not author → activeUser)\n //\n // We search through the PULLED-UP aliasToCollectionId (which contains the\n // innermost 'user' alias), so we always map directly to the deepest level.\n // This means aliasRemapping[lazyAlias] is always a single lookup, never recursive.\n const innerAlias = Object.keys(subQueryResult.aliasToCollectionId).find(\n (alias) =>\n subQueryResult.aliasToCollectionId[alias] ===\n subQueryResult.collectionId,\n )\n if (innerAlias && innerAlias !== from.alias) {\n aliasRemapping[from.alias] = innerAlias\n }\n\n // Extract the pipeline from the compilation result\n const subQueryInput = subQueryResult.pipeline\n\n // Subqueries may return [key, [value, orderByIndex]] (with ORDER BY) or [key, value] (without ORDER BY)\n // We need to extract just the value for use in parent queries\n const extractedInput = subQueryInput.pipe(\n map((data: any) => {\n const [key, [value, _orderByIndex]] = data\n return [key, value] as [unknown, any]\n }),\n )\n\n return {\n alias: from.alias,\n input: extractedInput as KeyedStream,\n collectionId: subQueryResult.collectionId,\n }\n }\n default:\n throw new UnsupportedJoinSourceTypeError((from as any).type)\n }\n}\n\n/**\n * Processes the results of a join operation\n */\nfunction processJoinResults(joinType: string) {\n return function (\n pipeline: IStreamBuilder<\n [\n key: string,\n [\n [string, NamespacedRow] | undefined,\n [string, NamespacedRow] | undefined,\n ],\n ]\n >,\n ): NamespacedAndKeyedStream {\n return pipeline.pipe(\n // Process the join result and handle nulls\n filter((result) => {\n const [_key, [main, joined]] = result\n const mainNamespacedRow = main?.[1]\n const joinedNamespacedRow = joined?.[1]\n\n // Handle different join types\n if (joinType === `inner`) {\n return !!(mainNamespacedRow && joinedNamespacedRow)\n }\n\n if (joinType === `left`) {\n return !!mainNamespacedRow\n }\n\n if (joinType === `right`) {\n return !!joinedNamespacedRow\n }\n\n // For full joins, always include\n return true\n }),\n map((result) => {\n const [_key, [main, joined]] = result\n const mainKey = main?.[0]\n const mainNamespacedRow = main?.[1]\n const joinedKey = joined?.[0]\n const joinedNamespacedRow = joined?.[1]\n\n // Merge the namespaced rows\n const mergedNamespacedRow: NamespacedRow = {}\n\n // Add main row data if it exists\n if (mainNamespacedRow) {\n Object.assign(mergedNamespacedRow, mainNamespacedRow)\n }\n\n // Add joined row data if it exists\n if (joinedNamespacedRow) {\n Object.assign(mergedNamespacedRow, joinedNamespacedRow)\n }\n\n // We create a composite key that combines the main and joined keys\n const resultKey = `[${mainKey},${joinedKey}]`\n\n return [resultKey, mergedNamespacedRow] as [string, NamespacedRow]\n }),\n )\n }\n}\n\n/**\n * Returns the active and lazy collections for a join clause.\n * The active collection is the one that we need to fully iterate over\n * and it can be the main source (i.e. left collection) or the joined source (i.e. right collection).\n * The lazy collection is the one that we should join-in lazily based on matches in the active collection.\n * @param joinClause - The join clause to analyze\n * @param leftCollection - The left collection\n * @param rightCollection - The right collection\n * @returns The active and lazy collections. They are undefined if we need to loop over both collections (i.e. both are active)\n */\nfunction getActiveAndLazySources(\n joinType: JoinClause[`type`],\n leftCollection: Collection,\n rightCollection: Collection,\n):\n | { activeSource: `main` | `joined`; lazySource: Collection }\n | { activeSource: undefined; lazySource: undefined } {\n // Self-joins can now be optimized since we track lazy loading by source alias\n // rather than collection ID. Each alias has its own subscription and lazy state.\n\n switch (joinType) {\n case `left`:\n return { activeSource: `main`, lazySource: rightCollection }\n case `right`:\n return { activeSource: `joined`, lazySource: leftCollection }\n case `inner`:\n // The smallest collection should be the active collection\n // and the biggest collection should be lazy\n return leftCollection.size < rightCollection.size\n ? { activeSource: `main`, lazySource: rightCollection }\n : { activeSource: `joined`, lazySource: leftCollection }\n default:\n return { activeSource: undefined, lazySource: undefined }\n }\n}\n"],"names":["joinOperator"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -180,8 +180,10 @@ export class CollectionLifecycleManager<
|
|
|
180
180
|
|
|
181
181
|
const gcTime = this.config.gcTime ?? 300000 // 5 minutes default
|
|
182
182
|
|
|
183
|
-
// If gcTime is 0, GC is disabled
|
|
184
|
-
|
|
183
|
+
// If gcTime is 0, negative, or non-finite (Infinity, -Infinity, NaN), GC is disabled.
|
|
184
|
+
// Note: setTimeout with Infinity coerces to 0 via ToInt32, causing immediate GC,
|
|
185
|
+
// so we must explicitly check for non-finite values here.
|
|
186
|
+
if (gcTime <= 0 || !Number.isFinite(gcTime)) {
|
|
185
187
|
return
|
|
186
188
|
}
|
|
187
189
|
|
|
@@ -148,6 +148,7 @@ export function compileQuery(
|
|
|
148
148
|
queryMapping,
|
|
149
149
|
aliasToCollectionId,
|
|
150
150
|
aliasRemapping,
|
|
151
|
+
sourceWhereClauses,
|
|
151
152
|
)
|
|
152
153
|
sources[mainSource] = mainInput
|
|
153
154
|
|
|
@@ -184,6 +185,7 @@ export function compileQuery(
|
|
|
184
185
|
compileQuery,
|
|
185
186
|
aliasToCollectionId,
|
|
186
187
|
aliasRemapping,
|
|
188
|
+
sourceWhereClauses,
|
|
187
189
|
)
|
|
188
190
|
}
|
|
189
191
|
|
|
@@ -466,6 +468,7 @@ function processFrom(
|
|
|
466
468
|
queryMapping: QueryMapping,
|
|
467
469
|
aliasToCollectionId: Record<string, string>,
|
|
468
470
|
aliasRemapping: Record<string, string>,
|
|
471
|
+
sourceWhereClauses: Map<string, BasicExpression<boolean>>,
|
|
469
472
|
): { alias: string; input: KeyedStream; collectionId: string } {
|
|
470
473
|
switch (from.type) {
|
|
471
474
|
case `collectionRef`: {
|
|
@@ -504,6 +507,28 @@ function processFrom(
|
|
|
504
507
|
Object.assign(aliasToCollectionId, subQueryResult.aliasToCollectionId)
|
|
505
508
|
Object.assign(aliasRemapping, subQueryResult.aliasRemapping)
|
|
506
509
|
|
|
510
|
+
// Pull up source WHERE clauses from subquery to parent scope.
|
|
511
|
+
// This enables loadSubset to receive the correct where clauses for subquery collections.
|
|
512
|
+
//
|
|
513
|
+
// IMPORTANT: Skip pull-up for optimizer-created subqueries. These are detected when:
|
|
514
|
+
// 1. The outer alias (from.alias) matches the inner alias (from.query.from.alias)
|
|
515
|
+
// 2. The subquery was found in queryMapping (it's a user-defined subquery, not optimizer-created)
|
|
516
|
+
//
|
|
517
|
+
// For optimizer-created subqueries, the parent already has the sourceWhereClauses
|
|
518
|
+
// extracted from the original raw query, so pulling up would be redundant.
|
|
519
|
+
// More importantly, pulling up for optimizer-created subqueries can cause issues
|
|
520
|
+
// when the optimizer has restructured the query.
|
|
521
|
+
const isUserDefinedSubquery = queryMapping.has(from.query)
|
|
522
|
+
const subqueryFromAlias = from.query.from.alias
|
|
523
|
+
const isOptimizerCreated =
|
|
524
|
+
!isUserDefinedSubquery && from.alias === subqueryFromAlias
|
|
525
|
+
|
|
526
|
+
if (!isOptimizerCreated) {
|
|
527
|
+
for (const [alias, whereClause] of subQueryResult.sourceWhereClauses) {
|
|
528
|
+
sourceWhereClauses.set(alias, whereClause)
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
507
532
|
// Create a FLATTENED remapping from outer alias to innermost alias.
|
|
508
533
|
// For nested subqueries, this ensures one-hop lookups (not recursive chains).
|
|
509
534
|
//
|
|
@@ -66,6 +66,7 @@ export function processJoins(
|
|
|
66
66
|
onCompileSubquery: CompileQueryFn,
|
|
67
67
|
aliasToCollectionId: Record<string, string>,
|
|
68
68
|
aliasRemapping: Record<string, string>,
|
|
69
|
+
sourceWhereClauses: Map<string, BasicExpression<boolean>>,
|
|
69
70
|
): NamespacedAndKeyedStream {
|
|
70
71
|
let resultPipeline = pipeline
|
|
71
72
|
|
|
@@ -89,6 +90,7 @@ export function processJoins(
|
|
|
89
90
|
onCompileSubquery,
|
|
90
91
|
aliasToCollectionId,
|
|
91
92
|
aliasRemapping,
|
|
93
|
+
sourceWhereClauses,
|
|
92
94
|
)
|
|
93
95
|
}
|
|
94
96
|
|
|
@@ -118,6 +120,7 @@ function processJoin(
|
|
|
118
120
|
onCompileSubquery: CompileQueryFn,
|
|
119
121
|
aliasToCollectionId: Record<string, string>,
|
|
120
122
|
aliasRemapping: Record<string, string>,
|
|
123
|
+
sourceWhereClauses: Map<string, BasicExpression<boolean>>,
|
|
121
124
|
): NamespacedAndKeyedStream {
|
|
122
125
|
const isCollectionRef = joinClause.from.type === `collectionRef`
|
|
123
126
|
|
|
@@ -140,6 +143,7 @@ function processJoin(
|
|
|
140
143
|
onCompileSubquery,
|
|
141
144
|
aliasToCollectionId,
|
|
142
145
|
aliasRemapping,
|
|
146
|
+
sourceWhereClauses,
|
|
143
147
|
)
|
|
144
148
|
|
|
145
149
|
// Add the joined source to the sources map
|
|
@@ -431,6 +435,7 @@ function processJoinSource(
|
|
|
431
435
|
onCompileSubquery: CompileQueryFn,
|
|
432
436
|
aliasToCollectionId: Record<string, string>,
|
|
433
437
|
aliasRemapping: Record<string, string>,
|
|
438
|
+
sourceWhereClauses: Map<string, BasicExpression<boolean>>,
|
|
434
439
|
): { alias: string; input: KeyedStream; collectionId: string } {
|
|
435
440
|
switch (from.type) {
|
|
436
441
|
case `collectionRef`: {
|
|
@@ -469,6 +474,26 @@ function processJoinSource(
|
|
|
469
474
|
Object.assign(aliasToCollectionId, subQueryResult.aliasToCollectionId)
|
|
470
475
|
Object.assign(aliasRemapping, subQueryResult.aliasRemapping)
|
|
471
476
|
|
|
477
|
+
// Pull up source WHERE clauses from subquery to parent scope.
|
|
478
|
+
// This enables loadSubset to receive the correct where clauses for subquery collections.
|
|
479
|
+
//
|
|
480
|
+
// IMPORTANT: Skip pull-up for optimizer-created subqueries. These are detected when:
|
|
481
|
+
// 1. The outer alias (from.alias) matches the inner alias (from.query.from.alias)
|
|
482
|
+
// 2. The subquery was found in queryMapping (it's a user-defined subquery, not optimizer-created)
|
|
483
|
+
//
|
|
484
|
+
// For optimizer-created subqueries, the parent already has the sourceWhereClauses
|
|
485
|
+
// extracted from the original raw query, so pulling up would be redundant.
|
|
486
|
+
const isUserDefinedSubquery = queryMapping.has(from.query)
|
|
487
|
+
const fromInnerAlias = from.query.from.alias
|
|
488
|
+
const isOptimizerCreated =
|
|
489
|
+
!isUserDefinedSubquery && from.alias === fromInnerAlias
|
|
490
|
+
|
|
491
|
+
if (!isOptimizerCreated) {
|
|
492
|
+
for (const [alias, whereClause] of subQueryResult.sourceWhereClauses) {
|
|
493
|
+
sourceWhereClauses.set(alias, whereClause)
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
472
497
|
// Create a flattened remapping from outer alias to innermost alias.
|
|
473
498
|
// For nested subqueries, this ensures one-hop lookups (not recursive chains).
|
|
474
499
|
//
|