@tanstack/db 0.5.25 → 0.5.26
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/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/builder/index.d.cts +1 -1
- package/dist/cjs/query/compiler/evaluators.cjs +2 -2
- package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
- package/dist/cjs/query/compiler/joins.cjs +3 -2
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/index.d.cts +1 -1
- package/dist/cjs/query/live/collection-config-builder.cjs +5 -0
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/esm/query/builder/index.d.ts +1 -1
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/compiler/evaluators.js +2 -2
- package/dist/esm/query/compiler/evaluators.js.map +1 -1
- package/dist/esm/query/compiler/joins.js +3 -2
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/index.d.ts +1 -1
- package/dist/esm/query/live/collection-config-builder.js +5 -0
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/package.json +2 -2
- package/src/query/builder/index.ts +18 -0
- package/src/query/compiler/evaluators.ts +2 -2
- package/src/query/compiler/joins.ts +3 -2
- package/src/query/index.ts +17 -0
- package/src/query/live/collection-config-builder.ts +9 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { map, tap, join, filter } from "@tanstack/db-ivm";
|
|
2
2
|
import { JoinCollectionNotFoundError, UnsupportedJoinTypeError, SubscriptionNotFoundError, UnsupportedJoinSourceTypeError, CollectionInputNotFoundError, InvalidJoinConditionSourceMismatchError, InvalidJoinConditionSameSourceError, InvalidJoinConditionLeftSourceError, InvalidJoinConditionRightSourceError, InvalidJoinCondition } from "../../errors.js";
|
|
3
|
+
import { normalizeValue } from "../../utils/comparison.js";
|
|
3
4
|
import { ensureIndexForField } from "../../indexes/auto-index.js";
|
|
4
5
|
import { followRef, PropRef } from "../ir.js";
|
|
5
6
|
import { inArray } from "../builder/functions.js";
|
|
@@ -81,14 +82,14 @@ function processJoin(pipeline, joinClause, sources, mainCollectionId, mainSource
|
|
|
81
82
|
const compiledJoinedExpr = compileExpression(joinedExpr);
|
|
82
83
|
let mainPipeline = pipeline.pipe(
|
|
83
84
|
map(([currentKey, namespacedRow]) => {
|
|
84
|
-
const mainKey = compiledMainExpr(namespacedRow);
|
|
85
|
+
const mainKey = normalizeValue(compiledMainExpr(namespacedRow));
|
|
85
86
|
return [mainKey, [currentKey, namespacedRow]];
|
|
86
87
|
})
|
|
87
88
|
);
|
|
88
89
|
let joinedPipeline = joinedInput.pipe(
|
|
89
90
|
map(([currentKey, row]) => {
|
|
90
91
|
const namespacedRow = { [joinedSource]: row };
|
|
91
|
-
const joinedKey = compiledJoinedExpr(namespacedRow);
|
|
92
|
+
const joinedKey = normalizeValue(compiledJoinedExpr(namespacedRow));
|
|
92
93
|
return [joinedKey, [currentKey, namespacedRow]];
|
|
93
94
|
})
|
|
94
95
|
);
|
|
@@ -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 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":";;;;;;AAiDO,SAAS,aACd,UACA,aACA,SACA,kBACA,YACA,WACA,OACA,cACA,aACA,eACA,WACA,aACA,+BACA,aACA,UACA,mBACA,qBACA,gBACA,oBAC0B;AAC1B,MAAI,iBAAiB;AAErB,aAAW,cAAc,aAAa;AACpC,qBAAiB;AAAA,MACf;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,MACA;AAAA,MACA;AAAA,IAAA;AAAA,EAEJ;AAEA,SAAO;AACT;AAMA,SAAS,YACP,UACA,YACA,SACA,kBACA,YACA,WACA,OACA,cACA,aACA,eACA,WACA,aACA,+BACA,aACA,UACA,mBACA,qBACA,gBACA,oBAC0B;AAC1B,QAAM,kBAAkB,WAAW,KAAK,SAAS;AAGjD,QAAM;AAAA,IACJ,OAAO;AAAA,IACP,OAAO;AAAA,IACP,cAAc;AAAA,EAAA,IACZ;AAAA,IACF,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAIF,UAAQ,YAAY,IAAI;AACxB,MAAI,iBAAiB;AAGnB,wBAAoB,YAAY,IAAI;AAAA,EACtC;AAEA,QAAM,iBAAiB,YAAY,gBAAgB;AACnD,QAAM,mBAAmB,YAAY,kBAAkB;AAEvD,MAAI,CAAC,gBAAgB;AACnB,UAAM,IAAI,4BAA4B,gBAAgB;AAAA,EACxD;AAEA,MAAI,CAAC,kBAAkB;AACrB,UAAM,IAAI,4BAA4B,kBAAkB;AAAA,EAC1D;AAEA,QAAM,EAAE,cAAc,WAAA,IAAe;AAAA,IACnC,WAAW;AAAA,IACX;AAAA,IACA;AAAA,EAAA;AAIF,QAAM,mBAAmB,OAAO,KAAK,OAAO;AAC5C,QAAM,EAAE,UAAU,WAAA,IAAe;AAAA,IAC/B,WAAW;AAAA,IACX,WAAW;AAAA,IACX;AAAA,IACA;AAAA,EAAA;AAIF,QAAM,mBAAmB,kBAAkB,QAAQ;AACnD,QAAM,qBAAqB,kBAAkB,UAAU;AAGvD,MAAI,eAAe,SAAS;AAAA,IAC1B,IAAI,CAAC,CAAC,YAAY,aAAa,MAAM;AAEnC,YAAM,UAAU,iBAAiB,aAAa;AAG9C,aAAO,CAAC,SAAS,CAAC,YAAY,aAAa,CAAC;AAAA,IAI9C,CAAC;AAAA,EAAA;AAIH,MAAI,iBAAiB,YAAY;AAAA,IAC/B,IAAI,CAAC,CAAC,YAAY,GAAG,MAAM;AAEzB,YAAM,gBAA+B,EAAE,CAAC,YAAY,GAAG,IAAA;AAGvD,YAAM,YAAY,mBAAmB,aAAa;AAGlD,aAAO,CAAC,WAAW,CAAC,YAAY,aAAa,CAAC;AAAA,IAIhD,CAAC;AAAA,EAAA;AAIH,MAAI,CAAC,CAAC,SAAS,QAAQ,SAAS,MAAM,EAAE,SAAS,WAAW,IAAI,GAAG;AACjE,UAAM,IAAI,yBAAyB,WAAW,IAAI;AAAA,EACpD;AAEA,MAAI,cAAc;AAKhB,UAAM,WAAW,iBAAiB,SAAS,WAAW,OAAO,SAAS;AACtE,UAAM,kBACJ,SAAS,SAAS,eACjB,SAAS,MAAM,SAAS,SAAS,MAAM;AAI1C,UAAM,sBACJ,SAAS,SAAS,UAAU,WAAW,SAAS;AAElD,QAAI,CAAC,mBAAmB,CAAC,qBAAqB;AAU5C,YAAM,YAAY,iBAAiB,SAAS,eAAe;AAC3D,kBAAY,IAAI,SAAS;AAEzB,YAAM,iBACJ,iBAAiB,SAAS,eAAe;AAE3C,YAAM,qBACJ,iBAAiB,SACZ,aACA;AAEP,YAAM,kBAAkB;AAAA,QACtB;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAEF,YAAM,sBAAsB,gBAAgB;AAE5C,YAAM,YAAY,gBAAgB,KAAK,CAAC;AACxC,UAAI,WAAW;AACb;AAAA,UACE;AAAA,UACA,gBAAgB;AAAA,UAChB;AAAA,QAAA;AAAA,MAEJ;AAIA,YAAM,4BAEF,eAAe;AAAA,QACjB,IAAI,CAAC,SAAS;AAOZ,gBAAM,gBAAgB,eAAe,SAAS,KAAK;AACnD,gBAAM,yBAAyB,cAAc,aAAa;AAE1D,cAAI,CAAC,wBAAwB;AAC3B,kBAAM,IAAI;AAAA,cACR;AAAA,cACA;AAAA,cACA,WAAW;AAAA,cACX,OAAO,KAAK,aAAa;AAAA,YAAA;AAAA,UAE7B;AAEA,cAAI,uBAAuB,yBAAyB;AAElD;AAAA,UACF;AAGA,gBAAM,WAAW,KAAK,WAAW,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,OAAO;AAC7D,gBAAM,cAAc,IAAI,QAAQ,gBAAgB,IAAI;AACpD,gBAAM,SAAS,uBAAuB,gBAAgB;AAAA,YACpD,OAAO,QAAQ,aAAa,QAAQ;AAAA,YACpC,eAAe;AAAA,UAAA,CAChB;AAED,cAAI,CAAC,QAAQ;AAEX,mCAAuB,gBAAA;AAAA,UACzB;AAAA,QACF,CAAC;AAAA,MAAA;AAGH,UAAI,iBAAiB,QAAQ;AAC3B,uBAAe;AAAA,MACjB,OAAO;AACL,yBAAiB;AAAA,MACnB;AAAA,IACF;AAAA,EACF;AAEA,SAAO,aAAa;AAAA,IAClBA,KAAa,gBAAgB,WAAW,IAAgB;AAAA,IACxD,mBAAmB,WAAW,IAAI;AAAA,EAAA;AAEtC;AAMA,SAAS,uBACP,MACA,OACA,2BACA,cAC4D;AAE5D,QAAM,mBAAmB,0BAA0B;AAAA,IACjD,CAAC,UAAU,UAAU;AAAA,EAAA;AAGvB,QAAM,kBAAkB,6BAA6B,IAAI;AACzD,QAAM,mBAAmB,6BAA6B,KAAK;AAG3D,MACE,mBACA,iBAAiB,SAAS,eAAe,KACzC,qBAAqB,cACrB;AACA,WAAO,EAAE,UAAU,MAAM,YAAY,MAAA;AAAA,EACvC;AAGA,MACE,oBAAoB,gBACpB,oBACA,iBAAiB,SAAS,gBAAgB,GAC1C;AACA,WAAO,EAAE,UAAU,OAAO,YAAY,KAAA;AAAA,EACxC;AAGA,MAAI,CAAC,mBAAmB,CAAC,kBAAkB;AACzC,UAAM,IAAI,wCAAA;AAAA,EACZ;AAGA,MAAI,oBAAoB,kBAAkB;AACxC,UAAM,IAAI,oCAAoC,eAAe;AAAA,EAC/D;AAKA,MAAI,CAAC,iBAAiB,SAAS,eAAe,GAAG;AAC/C,UAAM,IAAI,oCAAoC,eAAe;AAAA,EAC/D;AAGA,MAAI,qBAAqB,cAAc;AACrC,UAAM,IAAI,qCAAqC,YAAY;AAAA,EAC7D;AAGA,QAAM,IAAI,qBAAA;AACZ;AAKA,SAAS,6BAA6B,MAAsC;AAC1E,UAAQ,KAAK,MAAA;AAAA,IACX,KAAK;AAEH,aAAO,KAAK,KAAK,CAAC,KAAK;AAAA,IACzB,KAAK,QAAQ;AAEX,YAAM,oCAAoB,IAAA;AAC1B,iBAAW,OAAO,KAAK,MAAM;AAC3B,cAAM,QAAQ,6BAA6B,GAAG;AAC9C,YAAI,OAAO;AACT,wBAAc,IAAI,KAAK;AAAA,QACzB;AAAA,MACF;AAEA,aAAO,cAAc,SAAS,IAAI,MAAM,KAAK,aAAa,EAAE,CAAC,IAAK;AAAA,IACpE;AAAA,IACA;AAEE,aAAO;AAAA,EAAA;AAEb;AAKA,SAAS,kBACP,MACA,WACA,aACA,eACA,WACA,aACA,+BACA,aACA,OACA,cACA,mBACA,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;AAW3D,YAAM,wBAAwB,aAAa,IAAI,KAAK,KAAK;AACzD,YAAM,iBAAiB,KAAK,MAAM,KAAK;AACvC,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;AAaA,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;AACtC,iBAAO,CAAC,KAAK,KAAK;AAAA,QACpB,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,+BAAgC,KAAa,IAAI;AAAA,EAAA;AAEjE;AAKA,SAAS,mBAAmB,UAAkB;AAC5C,SAAO,SACL,UAS0B;AAC1B,WAAO,SAAS;AAAA;AAAA,MAEd,OAAO,CAAC,WAAW;AACjB,cAAM,CAAC,MAAM,CAAC,MAAM,MAAM,CAAC,IAAI;AAC/B,cAAM,oBAAoB,OAAO,CAAC;AAClC,cAAM,sBAAsB,SAAS,CAAC;AAGtC,YAAI,aAAa,SAAS;AACxB,iBAAO,CAAC,EAAE,qBAAqB;AAAA,QACjC;AAEA,YAAI,aAAa,QAAQ;AACvB,iBAAO,CAAC,CAAC;AAAA,QACX;AAEA,YAAI,aAAa,SAAS;AACxB,iBAAO,CAAC,CAAC;AAAA,QACX;AAGA,eAAO;AAAA,MACT,CAAC;AAAA,MACD,IAAI,CAAC,WAAW;AACd,cAAM,CAAC,MAAM,CAAC,MAAM,MAAM,CAAC,IAAI;AAC/B,cAAM,UAAU,OAAO,CAAC;AACxB,cAAM,oBAAoB,OAAO,CAAC;AAClC,cAAM,YAAY,SAAS,CAAC;AAC5B,cAAM,sBAAsB,SAAS,CAAC;AAGtC,cAAM,sBAAqC,CAAA;AAG3C,YAAI,mBAAmB;AACrB,iBAAO,OAAO,qBAAqB,iBAAiB;AAAA,QACtD;AAGA,YAAI,qBAAqB;AACvB,iBAAO,OAAO,qBAAqB,mBAAmB;AAAA,QACxD;AAGA,cAAM,YAAY,IAAI,OAAO,IAAI,SAAS;AAE1C,eAAO,CAAC,WAAW,mBAAmB;AAAA,MACxC,CAAC;AAAA,IAAA;AAAA,EAEL;AACF;AAYA,SAAS,wBACP,UACA,gBACA,iBAGqD;AAIrD,UAAQ,UAAA;AAAA,IACN,KAAK;AACH,aAAO,EAAE,cAAc,QAAQ,YAAY,gBAAA;AAAA,IAC7C,KAAK;AACH,aAAO,EAAE,cAAc,UAAU,YAAY,eAAA;AAAA,IAC/C,KAAK;AAGH,aAAO,eAAe,OAAO,gBAAgB,OACzC,EAAE,cAAc,QAAQ,YAAY,gBAAA,IACpC,EAAE,cAAc,UAAU,YAAY,eAAA;AAAA,IAC5C;AACE,aAAO,EAAE,cAAc,QAAW,YAAY,OAAA;AAAA,EAAU;AAE9D;"}
|
|
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 { normalizeValue } from '../../utils/comparison.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 = normalizeValue(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 = normalizeValue(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":";;;;;;;AAkDO,SAAS,aACd,UACA,aACA,SACA,kBACA,YACA,WACA,OACA,cACA,aACA,eACA,WACA,aACA,+BACA,aACA,UACA,mBACA,qBACA,gBACA,oBAC0B;AAC1B,MAAI,iBAAiB;AAErB,aAAW,cAAc,aAAa;AACpC,qBAAiB;AAAA,MACf;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,MACA;AAAA,MACA;AAAA,IAAA;AAAA,EAEJ;AAEA,SAAO;AACT;AAMA,SAAS,YACP,UACA,YACA,SACA,kBACA,YACA,WACA,OACA,cACA,aACA,eACA,WACA,aACA,+BACA,aACA,UACA,mBACA,qBACA,gBACA,oBAC0B;AAC1B,QAAM,kBAAkB,WAAW,KAAK,SAAS;AAGjD,QAAM;AAAA,IACJ,OAAO;AAAA,IACP,OAAO;AAAA,IACP,cAAc;AAAA,EAAA,IACZ;AAAA,IACF,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAIF,UAAQ,YAAY,IAAI;AACxB,MAAI,iBAAiB;AAGnB,wBAAoB,YAAY,IAAI;AAAA,EACtC;AAEA,QAAM,iBAAiB,YAAY,gBAAgB;AACnD,QAAM,mBAAmB,YAAY,kBAAkB;AAEvD,MAAI,CAAC,gBAAgB;AACnB,UAAM,IAAI,4BAA4B,gBAAgB;AAAA,EACxD;AAEA,MAAI,CAAC,kBAAkB;AACrB,UAAM,IAAI,4BAA4B,kBAAkB;AAAA,EAC1D;AAEA,QAAM,EAAE,cAAc,WAAA,IAAe;AAAA,IACnC,WAAW;AAAA,IACX;AAAA,IACA;AAAA,EAAA;AAIF,QAAM,mBAAmB,OAAO,KAAK,OAAO;AAC5C,QAAM,EAAE,UAAU,WAAA,IAAe;AAAA,IAC/B,WAAW;AAAA,IACX,WAAW;AAAA,IACX;AAAA,IACA;AAAA,EAAA;AAIF,QAAM,mBAAmB,kBAAkB,QAAQ;AACnD,QAAM,qBAAqB,kBAAkB,UAAU;AAGvD,MAAI,eAAe,SAAS;AAAA,IAC1B,IAAI,CAAC,CAAC,YAAY,aAAa,MAAM;AAEnC,YAAM,UAAU,eAAe,iBAAiB,aAAa,CAAC;AAG9D,aAAO,CAAC,SAAS,CAAC,YAAY,aAAa,CAAC;AAAA,IAI9C,CAAC;AAAA,EAAA;AAIH,MAAI,iBAAiB,YAAY;AAAA,IAC/B,IAAI,CAAC,CAAC,YAAY,GAAG,MAAM;AAEzB,YAAM,gBAA+B,EAAE,CAAC,YAAY,GAAG,IAAA;AAGvD,YAAM,YAAY,eAAe,mBAAmB,aAAa,CAAC;AAGlE,aAAO,CAAC,WAAW,CAAC,YAAY,aAAa,CAAC;AAAA,IAIhD,CAAC;AAAA,EAAA;AAIH,MAAI,CAAC,CAAC,SAAS,QAAQ,SAAS,MAAM,EAAE,SAAS,WAAW,IAAI,GAAG;AACjE,UAAM,IAAI,yBAAyB,WAAW,IAAI;AAAA,EACpD;AAEA,MAAI,cAAc;AAKhB,UAAM,WAAW,iBAAiB,SAAS,WAAW,OAAO,SAAS;AACtE,UAAM,kBACJ,SAAS,SAAS,eACjB,SAAS,MAAM,SAAS,SAAS,MAAM;AAI1C,UAAM,sBACJ,SAAS,SAAS,UAAU,WAAW,SAAS;AAElD,QAAI,CAAC,mBAAmB,CAAC,qBAAqB;AAU5C,YAAM,YAAY,iBAAiB,SAAS,eAAe;AAC3D,kBAAY,IAAI,SAAS;AAEzB,YAAM,iBACJ,iBAAiB,SAAS,eAAe;AAE3C,YAAM,qBACJ,iBAAiB,SACZ,aACA;AAEP,YAAM,kBAAkB;AAAA,QACtB;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAEF,YAAM,sBAAsB,gBAAgB;AAE5C,YAAM,YAAY,gBAAgB,KAAK,CAAC;AACxC,UAAI,WAAW;AACb;AAAA,UACE;AAAA,UACA,gBAAgB;AAAA,UAChB;AAAA,QAAA;AAAA,MAEJ;AAIA,YAAM,4BAEF,eAAe;AAAA,QACjB,IAAI,CAAC,SAAS;AAOZ,gBAAM,gBAAgB,eAAe,SAAS,KAAK;AACnD,gBAAM,yBAAyB,cAAc,aAAa;AAE1D,cAAI,CAAC,wBAAwB;AAC3B,kBAAM,IAAI;AAAA,cACR;AAAA,cACA;AAAA,cACA,WAAW;AAAA,cACX,OAAO,KAAK,aAAa;AAAA,YAAA;AAAA,UAE7B;AAEA,cAAI,uBAAuB,yBAAyB;AAElD;AAAA,UACF;AAGA,gBAAM,WAAW,KAAK,WAAW,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,OAAO;AAC7D,gBAAM,cAAc,IAAI,QAAQ,gBAAgB,IAAI;AACpD,gBAAM,SAAS,uBAAuB,gBAAgB;AAAA,YACpD,OAAO,QAAQ,aAAa,QAAQ;AAAA,YACpC,eAAe;AAAA,UAAA,CAChB;AAED,cAAI,CAAC,QAAQ;AAEX,mCAAuB,gBAAA;AAAA,UACzB;AAAA,QACF,CAAC;AAAA,MAAA;AAGH,UAAI,iBAAiB,QAAQ;AAC3B,uBAAe;AAAA,MACjB,OAAO;AACL,yBAAiB;AAAA,MACnB;AAAA,IACF;AAAA,EACF;AAEA,SAAO,aAAa;AAAA,IAClBA,KAAa,gBAAgB,WAAW,IAAgB;AAAA,IACxD,mBAAmB,WAAW,IAAI;AAAA,EAAA;AAEtC;AAMA,SAAS,uBACP,MACA,OACA,2BACA,cAC4D;AAE5D,QAAM,mBAAmB,0BAA0B;AAAA,IACjD,CAAC,UAAU,UAAU;AAAA,EAAA;AAGvB,QAAM,kBAAkB,6BAA6B,IAAI;AACzD,QAAM,mBAAmB,6BAA6B,KAAK;AAG3D,MACE,mBACA,iBAAiB,SAAS,eAAe,KACzC,qBAAqB,cACrB;AACA,WAAO,EAAE,UAAU,MAAM,YAAY,MAAA;AAAA,EACvC;AAGA,MACE,oBAAoB,gBACpB,oBACA,iBAAiB,SAAS,gBAAgB,GAC1C;AACA,WAAO,EAAE,UAAU,OAAO,YAAY,KAAA;AAAA,EACxC;AAGA,MAAI,CAAC,mBAAmB,CAAC,kBAAkB;AACzC,UAAM,IAAI,wCAAA;AAAA,EACZ;AAGA,MAAI,oBAAoB,kBAAkB;AACxC,UAAM,IAAI,oCAAoC,eAAe;AAAA,EAC/D;AAKA,MAAI,CAAC,iBAAiB,SAAS,eAAe,GAAG;AAC/C,UAAM,IAAI,oCAAoC,eAAe;AAAA,EAC/D;AAGA,MAAI,qBAAqB,cAAc;AACrC,UAAM,IAAI,qCAAqC,YAAY;AAAA,EAC7D;AAGA,QAAM,IAAI,qBAAA;AACZ;AAKA,SAAS,6BAA6B,MAAsC;AAC1E,UAAQ,KAAK,MAAA;AAAA,IACX,KAAK;AAEH,aAAO,KAAK,KAAK,CAAC,KAAK;AAAA,IACzB,KAAK,QAAQ;AAEX,YAAM,oCAAoB,IAAA;AAC1B,iBAAW,OAAO,KAAK,MAAM;AAC3B,cAAM,QAAQ,6BAA6B,GAAG;AAC9C,YAAI,OAAO;AACT,wBAAc,IAAI,KAAK;AAAA,QACzB;AAAA,MACF;AAEA,aAAO,cAAc,SAAS,IAAI,MAAM,KAAK,aAAa,EAAE,CAAC,IAAK;AAAA,IACpE;AAAA,IACA;AAEE,aAAO;AAAA,EAAA;AAEb;AAKA,SAAS,kBACP,MACA,WACA,aACA,eACA,WACA,aACA,+BACA,aACA,OACA,cACA,mBACA,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;AAW3D,YAAM,wBAAwB,aAAa,IAAI,KAAK,KAAK;AACzD,YAAM,iBAAiB,KAAK,MAAM,KAAK;AACvC,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;AAaA,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;AACtC,iBAAO,CAAC,KAAK,KAAK;AAAA,QACpB,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,+BAAgC,KAAa,IAAI;AAAA,EAAA;AAEjE;AAKA,SAAS,mBAAmB,UAAkB;AAC5C,SAAO,SACL,UAS0B;AAC1B,WAAO,SAAS;AAAA;AAAA,MAEd,OAAO,CAAC,WAAW;AACjB,cAAM,CAAC,MAAM,CAAC,MAAM,MAAM,CAAC,IAAI;AAC/B,cAAM,oBAAoB,OAAO,CAAC;AAClC,cAAM,sBAAsB,SAAS,CAAC;AAGtC,YAAI,aAAa,SAAS;AACxB,iBAAO,CAAC,EAAE,qBAAqB;AAAA,QACjC;AAEA,YAAI,aAAa,QAAQ;AACvB,iBAAO,CAAC,CAAC;AAAA,QACX;AAEA,YAAI,aAAa,SAAS;AACxB,iBAAO,CAAC,CAAC;AAAA,QACX;AAGA,eAAO;AAAA,MACT,CAAC;AAAA,MACD,IAAI,CAAC,WAAW;AACd,cAAM,CAAC,MAAM,CAAC,MAAM,MAAM,CAAC,IAAI;AAC/B,cAAM,UAAU,OAAO,CAAC;AACxB,cAAM,oBAAoB,OAAO,CAAC;AAClC,cAAM,YAAY,SAAS,CAAC;AAC5B,cAAM,sBAAsB,SAAS,CAAC;AAGtC,cAAM,sBAAqC,CAAA;AAG3C,YAAI,mBAAmB;AACrB,iBAAO,OAAO,qBAAqB,iBAAiB;AAAA,QACtD;AAGA,YAAI,qBAAqB;AACvB,iBAAO,OAAO,qBAAqB,mBAAmB;AAAA,QACxD;AAGA,cAAM,YAAY,IAAI,OAAO,IAAI,SAAS;AAE1C,eAAO,CAAC,WAAW,mBAAmB;AAAA,MACxC,CAAC;AAAA,IAAA;AAAA,EAEL;AACF;AAYA,SAAS,wBACP,UACA,gBACA,iBAGqD;AAIrD,UAAQ,UAAA;AAAA,IACN,KAAK;AACH,aAAO,EAAE,cAAc,QAAQ,YAAY,gBAAA;AAAA,IAC7C,KAAK;AACH,aAAO,EAAE,cAAc,UAAU,YAAY,eAAA;AAAA,IAC/C,KAAK;AAGH,aAAO,eAAe,OAAO,gBAAgB,OACzC,EAAE,cAAc,QAAQ,YAAY,gBAAA,IACpC,EAAE,cAAc,UAAU,YAAY,eAAA;AAAA,IAC5C;AACE,aAAO,EAAE,cAAc,QAAW,YAAY,OAAA;AAAA,EAAU;AAE9D;"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { BaseQueryBuilder, Query, type InitialQueryBuilder, type QueryBuilder, type Context, type Source, type GetResult, type InferResultType, type ExtractContext, type QueryResult, } from './builder/index.js';
|
|
1
|
+
export { BaseQueryBuilder, Query, type InitialQueryBuilder, type QueryBuilder, type Context, type ContextSchema, type Source, type GetResult, type InferResultType, type ExtractContext, type QueryResult, type SchemaFromSource, type InferCollectionType, type MergeContextWithJoinType, type MergeContextForJoinCallback, type ApplyJoinOptionalityToMergedSchema, type ResultTypeFromSelect, type WithResult, type JoinOnCallback, type RefsForContext, type WhereCallback, type OrderByCallback, type GroupByCallback, type SelectObject, type FunctionalHavingRow, type Prettify, } from './builder/index.js';
|
|
2
2
|
export { eq, gt, gte, lt, lte, and, or, not, inArray, like, ilike, isUndefined, isNull, upper, lower, length, concat, coalesce, add, count, avg, sum, min, max, } from './builder/functions.js';
|
|
3
3
|
export type { Ref } from './builder/types.js';
|
|
4
4
|
export { compileQuery } from './compiler/index.js';
|
|
@@ -165,10 +165,15 @@ class CollectionConfigBuilder {
|
|
|
165
165
|
return;
|
|
166
166
|
}
|
|
167
167
|
if (syncState.subscribedToAllCollections) {
|
|
168
|
+
let callbackCalled = false;
|
|
168
169
|
while (syncState.graph.pendingWork()) {
|
|
169
170
|
syncState.graph.run();
|
|
170
171
|
syncState.flushPendingChanges?.();
|
|
171
172
|
callback?.();
|
|
173
|
+
callbackCalled = true;
|
|
174
|
+
}
|
|
175
|
+
if (!callbackCalled) {
|
|
176
|
+
callback?.();
|
|
172
177
|
}
|
|
173
178
|
if (syncState.messagesCount === 0) {
|
|
174
179
|
begin();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection-config-builder.js","sources":["../../../../src/query/live/collection-config-builder.ts"],"sourcesContent":["import { D2, output } from '@tanstack/db-ivm'\nimport { compileQuery } from '../compiler/index.js'\nimport { buildQuery, getQueryIR } from '../builder/index.js'\nimport {\n MissingAliasInputsError,\n SetWindowRequiresOrderByError,\n} from '../../errors.js'\nimport { transactionScopedScheduler } from '../../scheduler.js'\nimport { getActiveTransaction } from '../../transactions.js'\nimport { CollectionSubscriber } from './collection-subscriber.js'\nimport { getCollectionBuilder } from './collection-registry.js'\nimport { LIVE_QUERY_INTERNAL } from './internal.js'\nimport type { LiveQueryInternalUtils } from './internal.js'\nimport type { WindowOptions } from '../compiler/index.js'\nimport type { SchedulerContextId } from '../../scheduler.js'\nimport type { CollectionSubscription } from '../../collection/subscription.js'\nimport type { RootStreamBuilder } from '@tanstack/db-ivm'\nimport type { OrderByOptimizationInfo } from '../compiler/order-by.js'\nimport type { Collection } from '../../collection/index.js'\nimport type {\n CollectionConfigSingleRowOption,\n KeyedStream,\n ResultStream,\n StringCollationConfig,\n SyncConfig,\n UtilsRecord,\n} from '../../types.js'\nimport type { Context, GetResult } from '../builder/types.js'\nimport type { BasicExpression, QueryIR } from '../ir.js'\nimport type { LazyCollectionCallbacks } from '../compiler/joins.js'\nimport type {\n Changes,\n FullSyncState,\n LiveQueryCollectionConfig,\n SyncState,\n} from './types.js'\nimport type { AllCollectionEvents } from '../../collection/events.js'\n\nexport type LiveQueryCollectionUtils = UtilsRecord & {\n getRunCount: () => number\n /**\n * Sets the offset and limit of an ordered query.\n * Is a no-op if the query is not ordered.\n *\n * @returns `true` if no subset loading was triggered, or `Promise<void>` that resolves when the subset has been loaded\n */\n setWindow: (options: WindowOptions) => true | Promise<void>\n /**\n * Gets the current window (offset and limit) for an ordered query.\n *\n * @returns The current window settings, or `undefined` if the query is not windowed\n */\n getWindow: () => { offset: number; limit: number } | undefined\n [LIVE_QUERY_INTERNAL]: LiveQueryInternalUtils\n}\n\ntype PendingGraphRun = {\n loadCallbacks: Set<() => boolean>\n}\n\n// Global counter for auto-generated collection IDs\nlet liveQueryCollectionCounter = 0\n\ntype SyncMethods<TResult extends object> = Parameters<\n SyncConfig<TResult>[`sync`]\n>[0]\n\nexport class CollectionConfigBuilder<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n> {\n private readonly id: string\n readonly query: QueryIR\n private readonly collections: Record<string, Collection<any, any, any>>\n private readonly collectionByAlias: Record<string, Collection<any, any, any>>\n // Populated during compilation with all aliases (including subquery inner aliases)\n private compiledAliasToCollectionId: Record<string, string> = {}\n\n // WeakMap to store the keys of the results\n // so that we can retrieve them in the getKey function\n private readonly resultKeys = new WeakMap<object, unknown>()\n\n // WeakMap to store the orderBy index for each result\n private readonly orderByIndices = new WeakMap<object, string>()\n\n private readonly compare?: (val1: TResult, val2: TResult) => number\n private readonly compareOptions?: StringCollationConfig\n\n private isGraphRunning = false\n private runCount = 0\n\n // Current sync session state (set when sync starts, cleared when it stops)\n // Public for testing purposes (CollectionConfigBuilder is internal, not public API)\n public currentSyncConfig:\n | Parameters<SyncConfig<TResult>[`sync`]>[0]\n | undefined\n public currentSyncState: FullSyncState | undefined\n\n // Error state tracking\n private isInErrorState = false\n\n // Reference to the live query collection for error state transitions\n public liveQueryCollection?: Collection<TResult, any, any>\n\n private windowFn: ((options: WindowOptions) => void) | undefined\n private currentWindow: WindowOptions | undefined\n\n private maybeRunGraphFn: (() => void) | undefined\n\n private readonly aliasDependencies: Record<\n string,\n Array<CollectionConfigBuilder<any, any>>\n > = {}\n\n private readonly builderDependencies = new Set<\n CollectionConfigBuilder<any, any>\n >()\n\n // Pending graph runs per scheduler context (e.g., per transaction)\n // The builder manages its own state; the scheduler just orchestrates execution order\n // Only stores callbacks - if sync ends, pending jobs gracefully no-op\n private readonly pendingGraphRuns = new Map<\n SchedulerContextId,\n PendingGraphRun\n >()\n\n // Unsubscribe function for scheduler's onClear listener\n // Registered when sync starts, unregistered when sync stops\n // Prevents memory leaks by releasing the scheduler's reference to this builder\n private unsubscribeFromSchedulerClears?: () => void\n\n private graphCache: D2 | undefined\n private inputsCache: Record<string, RootStreamBuilder<unknown>> | undefined\n private pipelineCache: ResultStream | undefined\n public sourceWhereClausesCache:\n | Map<string, BasicExpression<boolean>>\n | undefined\n\n // Map of source alias to subscription\n readonly subscriptions: Record<string, CollectionSubscription> = {}\n // Map of source aliases to functions that load keys for that lazy source\n lazySourcesCallbacks: Record<string, LazyCollectionCallbacks> = {}\n // Set of source aliases that are lazy (don't load initial state)\n readonly lazySources = new Set<string>()\n // Set of collection IDs that include an optimizable ORDER BY clause\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo> = {}\n\n constructor(\n private readonly config: LiveQueryCollectionConfig<TContext, TResult>,\n ) {\n // Generate a unique ID if not provided\n this.id = config.id || `live-query-${++liveQueryCollectionCounter}`\n\n this.query = buildQueryFromConfig(config)\n this.collections = extractCollectionsFromQuery(this.query)\n const collectionAliasesById = extractCollectionAliases(this.query)\n\n // Build a reverse lookup map from alias to collection instance.\n // This enables self-join support where the same collection can be referenced\n // multiple times with different aliases (e.g., { employee: col, manager: col })\n this.collectionByAlias = {}\n for (const [collectionId, aliases] of collectionAliasesById.entries()) {\n const collection = this.collections[collectionId]\n if (!collection) continue\n for (const alias of aliases) {\n this.collectionByAlias[alias] = collection\n }\n }\n\n // Create compare function for ordering if the query has orderBy\n if (this.query.orderBy && this.query.orderBy.length > 0) {\n this.compare = createOrderByComparator<TResult>(this.orderByIndices)\n }\n\n // Use explicitly provided compareOptions if available, otherwise inherit from FROM collection\n this.compareOptions =\n this.config.defaultStringCollation ??\n extractCollectionFromSource(this.query).compareOptions\n\n // Compile the base pipeline once initially\n // This is done to ensure that any errors are thrown immediately and synchronously\n this.compileBasePipeline()\n }\n\n /**\n * Recursively checks if a query or any of its subqueries contains joins\n */\n private hasJoins(query: QueryIR): boolean {\n // Check if this query has joins\n if (query.join && query.join.length > 0) {\n return true\n }\n\n // Recursively check subqueries in the from clause\n if (query.from.type === `queryRef`) {\n if (this.hasJoins(query.from.query)) {\n return true\n }\n }\n\n return false\n }\n\n getConfig(): CollectionConfigSingleRowOption<TResult> & {\n utils: LiveQueryCollectionUtils\n } {\n return {\n id: this.id,\n getKey:\n this.config.getKey ||\n ((item) => this.resultKeys.get(item) as string | number),\n sync: this.getSyncConfig(),\n compare: this.compare,\n defaultStringCollation: this.compareOptions,\n gcTime: this.config.gcTime || 5000, // 5 seconds by default for live queries\n schema: this.config.schema,\n onInsert: this.config.onInsert,\n onUpdate: this.config.onUpdate,\n onDelete: this.config.onDelete,\n startSync: this.config.startSync,\n singleResult: this.query.singleResult,\n utils: {\n getRunCount: this.getRunCount.bind(this),\n setWindow: this.setWindow.bind(this),\n getWindow: this.getWindow.bind(this),\n [LIVE_QUERY_INTERNAL]: {\n getBuilder: () => this,\n hasCustomGetKey: !!this.config.getKey,\n hasJoins: this.hasJoins(this.query),\n hasDistinct: !!this.query.distinct,\n },\n },\n }\n }\n\n setWindow(options: WindowOptions): true | Promise<void> {\n if (!this.windowFn) {\n throw new SetWindowRequiresOrderByError()\n }\n\n this.currentWindow = options\n this.windowFn(options)\n this.maybeRunGraphFn?.()\n\n // Check if loading a subset was triggered\n if (this.liveQueryCollection?.isLoadingSubset) {\n // Loading was triggered, return a promise that resolves when it completes\n return new Promise<void>((resolve) => {\n const unsubscribe = this.liveQueryCollection!.on(\n `loadingSubset:change`,\n (event) => {\n if (!event.isLoadingSubset) {\n unsubscribe()\n resolve()\n }\n },\n )\n })\n }\n\n // No loading was triggered\n return true\n }\n\n getWindow(): { offset: number; limit: number } | undefined {\n // Only return window if this is a windowed query (has orderBy and windowFn)\n if (!this.windowFn || !this.currentWindow) {\n return undefined\n }\n return {\n offset: this.currentWindow.offset ?? 0,\n limit: this.currentWindow.limit ?? 0,\n }\n }\n\n /**\n * Resolves a collection alias to its collection ID.\n *\n * Uses a two-tier lookup strategy:\n * 1. First checks compiled aliases (includes subquery inner aliases)\n * 2. Falls back to declared aliases from the query's from/join clauses\n *\n * @param alias - The alias to resolve (e.g., \"employee\", \"manager\")\n * @returns The collection ID that the alias references\n * @throws {Error} If the alias is not found in either lookup\n */\n getCollectionIdForAlias(alias: string): string {\n const compiled = this.compiledAliasToCollectionId[alias]\n if (compiled) {\n return compiled\n }\n const collection = this.collectionByAlias[alias]\n if (collection) {\n return collection.id\n }\n throw new Error(`Unknown source alias \"${alias}\"`)\n }\n\n isLazyAlias(alias: string): boolean {\n return this.lazySources.has(alias)\n }\n\n // The callback function is called after the graph has run.\n // This gives the callback a chance to load more data if needed,\n // that's used to optimize orderBy operators that set a limit,\n // in order to load some more data if we still don't have enough rows after the pipeline has run.\n // That can happen because even though we load N rows, the pipeline might filter some of these rows out\n // causing the orderBy operator to receive less than N rows or even no rows at all.\n // So this callback would notice that it doesn't have enough rows and load some more.\n // The callback returns a boolean, when it's true it's done loading data and we can mark the collection as ready.\n maybeRunGraph(callback?: () => boolean) {\n if (this.isGraphRunning) {\n // no nested runs of the graph\n // which is possible if the `callback`\n // would call `maybeRunGraph` e.g. after it has loaded some more data\n return\n }\n\n // Should only be called when sync is active\n if (!this.currentSyncConfig || !this.currentSyncState) {\n throw new Error(\n `maybeRunGraph called without active sync session. This should not happen.`,\n )\n }\n\n this.isGraphRunning = true\n\n try {\n const { begin, commit } = this.currentSyncConfig\n const syncState = this.currentSyncState\n\n // Don't run if the live query is in an error state\n if (this.isInErrorState) {\n return\n }\n\n // Always run the graph if subscribed (eager execution)\n if (syncState.subscribedToAllCollections) {\n while (syncState.graph.pendingWork()) {\n syncState.graph.run()\n // Flush accumulated changes after each graph step to commit them as one transaction.\n // This ensures intermediate join states (like null on one side) don't cause\n // duplicate key errors when the full join result arrives in the same step.\n syncState.flushPendingChanges?.()\n callback?.()\n }\n\n // On the initial run, we may need to do an empty commit to ensure that\n // the collection is initialized\n if (syncState.messagesCount === 0) {\n begin()\n commit()\n }\n\n // After graph processing completes, check if we should mark ready.\n // This is the canonical place to transition to ready state because:\n // 1. All data has been processed through the graph\n // 2. All source collections have had a chance to send their initial data\n // This prevents marking ready before data is processed (fixes isReady=true with empty data)\n this.updateLiveQueryStatus(this.currentSyncConfig)\n }\n } finally {\n this.isGraphRunning = false\n }\n }\n\n /**\n * Schedules a graph run with the transaction-scoped scheduler.\n * Ensures each builder runs at most once per transaction, with automatic dependency tracking\n * to run parent queries before child queries. Outside a transaction, runs immediately.\n *\n * Multiple calls during a transaction are coalesced into a single execution.\n * Dependencies are auto-discovered from subscribed live queries, or can be overridden.\n * Load callbacks are combined when entries merge.\n *\n * Uses the current sync session's config and syncState from instance properties.\n *\n * @param callback - Optional callback to load more data if needed (returns true when done)\n * @param options - Optional scheduling configuration\n * @param options.contextId - Transaction ID to group work; defaults to active transaction\n * @param options.jobId - Unique identifier for this job; defaults to this builder instance\n * @param options.alias - Source alias that triggered this schedule; adds alias-specific dependencies\n * @param options.dependencies - Explicit dependency list; overrides auto-discovered dependencies\n */\n scheduleGraphRun(\n callback?: () => boolean,\n options?: {\n contextId?: SchedulerContextId\n jobId?: unknown\n alias?: string\n dependencies?: Array<CollectionConfigBuilder<any, any>>\n },\n ) {\n const contextId = options?.contextId ?? getActiveTransaction()?.id\n // Use the builder instance as the job ID for deduplication. This is memory-safe\n // because the scheduler's context Map is deleted after flushing (no long-term retention).\n const jobId = options?.jobId ?? this\n const dependentBuilders = (() => {\n if (options?.dependencies) {\n return options.dependencies\n }\n\n const deps = new Set(this.builderDependencies)\n if (options?.alias) {\n const aliasDeps = this.aliasDependencies[options.alias]\n if (aliasDeps) {\n for (const dep of aliasDeps) {\n deps.add(dep)\n }\n }\n }\n\n deps.delete(this)\n\n return Array.from(deps)\n })()\n\n // Ensure dependent builders are actually scheduled in this context so that\n // dependency edges always point to a real job (or a deduped no-op if already scheduled).\n if (contextId) {\n for (const dep of dependentBuilders) {\n if (typeof dep.scheduleGraphRun === `function`) {\n dep.scheduleGraphRun(undefined, { contextId })\n }\n }\n }\n\n // We intentionally scope deduplication to the builder instance. Each instance\n // owns caches and compiled pipelines, so sharing work across instances that\n // merely reuse the same string id would execute the wrong builder's graph.\n\n if (!this.currentSyncConfig || !this.currentSyncState) {\n throw new Error(\n `scheduleGraphRun called without active sync session. This should not happen.`,\n )\n }\n\n // Manage our own state - get or create pending callbacks for this context\n let pending = contextId ? this.pendingGraphRuns.get(contextId) : undefined\n if (!pending) {\n pending = {\n loadCallbacks: new Set(),\n }\n if (contextId) {\n this.pendingGraphRuns.set(contextId, pending)\n }\n }\n\n // Add callback if provided (this is what accumulates between schedules)\n if (callback) {\n pending.loadCallbacks.add(callback)\n }\n\n // Schedule execution (scheduler just orchestrates order, we manage state)\n // For immediate execution (no contextId), pass pending directly since it won't be in the map\n const pendingToPass = contextId ? undefined : pending\n transactionScopedScheduler.schedule({\n contextId,\n jobId,\n dependencies: dependentBuilders,\n run: () => this.executeGraphRun(contextId, pendingToPass),\n })\n }\n\n /**\n * Clears pending graph run state for a specific context.\n * Called when the scheduler clears a context (e.g., transaction rollback/abort).\n */\n clearPendingGraphRun(contextId: SchedulerContextId): void {\n this.pendingGraphRuns.delete(contextId)\n }\n\n /**\n * Returns true if this builder has a pending graph run for the given context.\n */\n hasPendingGraphRun(contextId: SchedulerContextId): boolean {\n return this.pendingGraphRuns.has(contextId)\n }\n\n /**\n * Executes a pending graph run. Called by the scheduler when dependencies are satisfied.\n * Clears the pending state BEFORE execution so that any re-schedules during the run\n * create fresh state and don't interfere with the current execution.\n * Uses instance sync state - if sync has ended, gracefully returns without executing.\n *\n * @param contextId - Optional context ID to look up pending state\n * @param pendingParam - For immediate execution (no context), pending state is passed directly\n */\n private executeGraphRun(\n contextId?: SchedulerContextId,\n pendingParam?: PendingGraphRun,\n ): void {\n // Get pending state: either from parameter (no context) or from map (with context)\n // Remove from map BEFORE checking sync state to prevent leaking entries when sync ends\n // before the transaction flushes (e.g., unsubscribe during in-flight transaction)\n const pending =\n pendingParam ??\n (contextId ? this.pendingGraphRuns.get(contextId) : undefined)\n if (contextId) {\n this.pendingGraphRuns.delete(contextId)\n }\n\n // If no pending state, nothing to execute (context was cleared)\n if (!pending) {\n return\n }\n\n // If sync session has ended, don't execute (graph is finalized, subscriptions cleared)\n if (!this.currentSyncConfig || !this.currentSyncState) {\n return\n }\n\n this.incrementRunCount()\n\n const combinedLoader = () => {\n let allDone = true\n let firstError: unknown\n pending.loadCallbacks.forEach((loader) => {\n try {\n allDone = loader() && allDone\n } catch (error) {\n allDone = false\n firstError ??= error\n }\n })\n if (firstError) {\n throw firstError\n }\n // Returning false signals that callers should schedule another pass.\n return allDone\n }\n\n this.maybeRunGraph(combinedLoader)\n }\n\n private getSyncConfig(): SyncConfig<TResult> {\n return {\n rowUpdateMode: `full`,\n sync: this.syncFn.bind(this),\n }\n }\n\n incrementRunCount() {\n this.runCount++\n }\n\n getRunCount() {\n return this.runCount\n }\n\n private syncFn(config: SyncMethods<TResult>) {\n // Store reference to the live query collection for error state transitions\n this.liveQueryCollection = config.collection\n // Store config and syncState as instance properties for the duration of this sync session\n this.currentSyncConfig = config\n\n const syncState: SyncState = {\n messagesCount: 0,\n subscribedToAllCollections: false,\n unsubscribeCallbacks: new Set<() => void>(),\n }\n\n // Extend the pipeline such that it applies the incoming changes to the collection\n const fullSyncState = this.extendPipelineWithChangeProcessing(\n config,\n syncState,\n )\n this.currentSyncState = fullSyncState\n\n // Listen for scheduler context clears to clean up our pending state\n // Re-register on each sync start so the listener is active for the sync session's lifetime\n this.unsubscribeFromSchedulerClears = transactionScopedScheduler.onClear(\n (contextId) => {\n this.clearPendingGraphRun(contextId)\n },\n )\n\n // Listen for loadingSubset changes on the live query collection BEFORE subscribing.\n // This ensures we don't miss the event if subset loading completes synchronously.\n // When isLoadingSubset becomes false, we may need to mark the collection as ready\n // (if all source collections are already ready but we were waiting for subset load to complete)\n const loadingSubsetUnsubscribe = config.collection.on(\n `loadingSubset:change`,\n (event) => {\n if (!event.isLoadingSubset) {\n // Subset loading finished, check if we can now mark ready\n this.updateLiveQueryStatus(config)\n }\n },\n )\n syncState.unsubscribeCallbacks.add(loadingSubsetUnsubscribe)\n\n const loadSubsetDataCallbacks = this.subscribeToAllCollections(\n config,\n fullSyncState,\n )\n\n this.maybeRunGraphFn = () => this.scheduleGraphRun(loadSubsetDataCallbacks)\n\n // Initial run with callback to load more data if needed\n this.scheduleGraphRun(loadSubsetDataCallbacks)\n\n // Return the unsubscribe function\n return () => {\n syncState.unsubscribeCallbacks.forEach((unsubscribe) => unsubscribe())\n\n // Clear current sync session state\n this.currentSyncConfig = undefined\n this.currentSyncState = undefined\n\n // Clear all pending graph runs to prevent memory leaks from in-flight transactions\n // that may flush after the sync session ends\n this.pendingGraphRuns.clear()\n\n // Reset caches so a fresh graph/pipeline is compiled on next start\n // This avoids reusing a finalized D2 graph across GC restarts\n this.graphCache = undefined\n this.inputsCache = undefined\n this.pipelineCache = undefined\n this.sourceWhereClausesCache = undefined\n\n // Reset lazy source alias state\n this.lazySources.clear()\n this.optimizableOrderByCollections = {}\n this.lazySourcesCallbacks = {}\n\n // Clear subscription references to prevent memory leaks\n // Note: Individual subscriptions are already unsubscribed via unsubscribeCallbacks\n Object.keys(this.subscriptions).forEach(\n (key) => delete this.subscriptions[key],\n )\n this.compiledAliasToCollectionId = {}\n\n // Unregister from scheduler's onClear listener to prevent memory leaks\n // The scheduler's listener Set would otherwise keep a strong reference to this builder\n this.unsubscribeFromSchedulerClears?.()\n this.unsubscribeFromSchedulerClears = undefined\n }\n }\n\n /**\n * Compiles the query pipeline with all declared aliases.\n */\n private compileBasePipeline() {\n this.graphCache = new D2()\n this.inputsCache = Object.fromEntries(\n Object.keys(this.collectionByAlias).map((alias) => [\n alias,\n this.graphCache!.newInput<any>(),\n ]),\n )\n\n const compilation = compileQuery(\n this.query,\n this.inputsCache as Record<string, KeyedStream>,\n this.collections,\n this.subscriptions,\n this.lazySourcesCallbacks,\n this.lazySources,\n this.optimizableOrderByCollections,\n (windowFn: (options: WindowOptions) => void) => {\n this.windowFn = windowFn\n },\n )\n\n this.pipelineCache = compilation.pipeline\n this.sourceWhereClausesCache = compilation.sourceWhereClauses\n this.compiledAliasToCollectionId = compilation.aliasToCollectionId\n\n // Defensive check: verify all compiled aliases have corresponding inputs\n // This should never happen since all aliases come from user declarations,\n // but catch it early if the assumption is violated in the future.\n const missingAliases = Object.keys(this.compiledAliasToCollectionId).filter(\n (alias) => !Object.hasOwn(this.inputsCache!, alias),\n )\n if (missingAliases.length > 0) {\n throw new MissingAliasInputsError(missingAliases)\n }\n }\n\n private maybeCompileBasePipeline() {\n if (!this.graphCache || !this.inputsCache || !this.pipelineCache) {\n this.compileBasePipeline()\n }\n return {\n graph: this.graphCache!,\n inputs: this.inputsCache!,\n pipeline: this.pipelineCache!,\n }\n }\n\n private extendPipelineWithChangeProcessing(\n config: SyncMethods<TResult>,\n syncState: SyncState,\n ): FullSyncState {\n const { begin, commit } = config\n const { graph, inputs, pipeline } = this.maybeCompileBasePipeline()\n\n // Accumulator for changes across all output callbacks within a single graph run.\n // This allows us to batch all changes from intermediate join states into a single\n // transaction, avoiding duplicate key errors when joins produce multiple outputs\n // for the same key (e.g., first output with null, then output with joined data).\n let pendingChanges: Map<unknown, Changes<TResult>> = new Map()\n\n pipeline.pipe(\n output((data) => {\n const messages = data.getInner()\n syncState.messagesCount += messages.length\n\n // Accumulate changes from this output callback into the pending changes map.\n // Changes for the same key are merged (inserts/deletes are added together).\n messages.reduce(accumulateChanges<TResult>, pendingChanges)\n }),\n )\n\n // Flush pending changes and reset the accumulator.\n // Called at the end of each graph run to commit all accumulated changes.\n syncState.flushPendingChanges = () => {\n if (pendingChanges.size === 0) {\n return\n }\n begin()\n pendingChanges.forEach(this.applyChanges.bind(this, config))\n commit()\n pendingChanges = new Map()\n }\n\n graph.finalize()\n\n // Extend the sync state with the graph, inputs, and pipeline\n syncState.graph = graph\n syncState.inputs = inputs\n syncState.pipeline = pipeline\n\n return syncState as FullSyncState\n }\n\n private applyChanges(\n config: SyncMethods<TResult>,\n changes: {\n deletes: number\n inserts: number\n value: TResult\n orderByIndex: string | undefined\n },\n key: unknown,\n ) {\n const { write, collection } = config\n const { deletes, inserts, value, orderByIndex } = changes\n\n // Store the key of the result so that we can retrieve it in the\n // getKey function\n this.resultKeys.set(value, key)\n\n // Store the orderBy index if it exists\n if (orderByIndex !== undefined) {\n this.orderByIndices.set(value, orderByIndex)\n }\n\n // Simple singular insert.\n if (inserts && deletes === 0) {\n write({\n value,\n type: `insert`,\n })\n } else if (\n // Insert & update(s) (updates are a delete & insert)\n inserts > deletes ||\n // Just update(s) but the item is already in the collection (so\n // was inserted previously).\n (inserts === deletes && collection.has(collection.getKeyFromItem(value)))\n ) {\n write({\n value,\n type: `update`,\n })\n // Only delete is left as an option\n } else if (deletes > 0) {\n write({\n value,\n type: `delete`,\n })\n } else {\n throw new Error(\n `Could not apply changes: ${JSON.stringify(changes)}. This should never happen.`,\n )\n }\n }\n\n /**\n * Handle status changes from source collections\n */\n private handleSourceStatusChange(\n config: SyncMethods<TResult>,\n collectionId: string,\n event: AllCollectionEvents[`status:change`],\n ) {\n const { status } = event\n\n // Handle error state - any source collection in error puts live query in error\n if (status === `error`) {\n this.transitionToError(\n `Source collection '${collectionId}' entered error state`,\n )\n return\n }\n\n // Handle manual cleanup - this should not happen due to GC prevention,\n // but could happen if user manually calls cleanup()\n if (status === `cleaned-up`) {\n this.transitionToError(\n `Source collection '${collectionId}' was manually cleaned up while live query '${this.id}' depends on it. ` +\n `Live queries prevent automatic GC, so this was likely a manual cleanup() call.`,\n )\n return\n }\n\n // Update ready status based on all source collections\n this.updateLiveQueryStatus(config)\n }\n\n /**\n * Update the live query status based on source collection statuses\n */\n private updateLiveQueryStatus(config: SyncMethods<TResult>) {\n const { markReady } = config\n\n // Don't update status if already in error\n if (this.isInErrorState) {\n return\n }\n\n const subscribedToAll = this.currentSyncState?.subscribedToAllCollections\n const allReady = this.allCollectionsReady()\n const isLoading = this.liveQueryCollection?.isLoadingSubset\n // Mark ready when:\n // 1. All subscriptions are set up (subscribedToAllCollections)\n // 2. All source collections are ready\n // 3. The live query collection is not loading subset data\n // This prevents marking the live query ready before its data is processed\n // (fixes issue where useLiveQuery returns isReady=true with empty data)\n if (subscribedToAll && allReady && !isLoading) {\n markReady()\n }\n }\n\n /**\n * Transition the live query to error state\n */\n private transitionToError(message: string) {\n this.isInErrorState = true\n\n // Log error to console for debugging\n console.error(`[Live Query Error] ${message}`)\n\n // Transition live query collection to error state\n this.liveQueryCollection?._lifecycle.setStatus(`error`)\n }\n\n private allCollectionsReady() {\n return Object.values(this.collections).every((collection) =>\n collection.isReady(),\n )\n }\n\n /**\n * Creates per-alias subscriptions enabling self-join support.\n * Each alias gets its own subscription with independent filters, even for the same collection.\n * Example: `{ employee: col, manager: col }` creates two separate subscriptions.\n */\n private subscribeToAllCollections(\n config: SyncMethods<TResult>,\n syncState: FullSyncState,\n ) {\n // Use compiled aliases as the source of truth - these include all aliases from the query\n // including those from subqueries, which may not be in collectionByAlias\n const compiledAliases = Object.entries(this.compiledAliasToCollectionId)\n if (compiledAliases.length === 0) {\n throw new Error(\n `Compiler returned no alias metadata for query '${this.id}'. This should not happen; please report.`,\n )\n }\n\n // Create a separate subscription for each alias, enabling self-joins where the same\n // collection can be used multiple times with different filters and subscriptions\n const loaders = compiledAliases.map(([alias, collectionId]) => {\n // Try collectionByAlias first (for declared aliases), fall back to collections (for subquery aliases)\n const collection =\n this.collectionByAlias[alias] ?? this.collections[collectionId]!\n\n const dependencyBuilder = getCollectionBuilder(collection)\n if (dependencyBuilder && dependencyBuilder !== this) {\n this.aliasDependencies[alias] = [dependencyBuilder]\n this.builderDependencies.add(dependencyBuilder)\n } else {\n this.aliasDependencies[alias] = []\n }\n\n // CollectionSubscriber handles the actual subscription to the source collection\n // and feeds data into the D2 graph inputs for this specific alias\n const collectionSubscriber = new CollectionSubscriber(\n alias,\n collectionId,\n collection,\n this,\n )\n\n // Subscribe to status changes for status flow\n const statusUnsubscribe = collection.on(`status:change`, (event) => {\n this.handleSourceStatusChange(config, collectionId, event)\n })\n syncState.unsubscribeCallbacks.add(statusUnsubscribe)\n\n const subscription = collectionSubscriber.subscribe()\n // Store subscription by alias (not collection ID) to support lazy loading\n // which needs to look up subscriptions by their query alias\n this.subscriptions[alias] = subscription\n\n // Create a callback for loading more data if needed (used by OrderBy optimization)\n const loadMore = collectionSubscriber.loadMoreIfNeeded.bind(\n collectionSubscriber,\n subscription,\n )\n\n return loadMore\n })\n\n // Combine all loaders into a single callback that initiates loading more data\n // from any source that needs it. Returns true once all loaders have been called,\n // but the actual async loading may still be in progress.\n const loadSubsetDataCallbacks = () => {\n loaders.map((loader) => loader())\n return true\n }\n\n // Mark as subscribed so the graph can start running\n // (graph only runs when all collections are subscribed)\n syncState.subscribedToAllCollections = true\n\n // Note: We intentionally don't call updateLiveQueryStatus() here.\n // The graph hasn't run yet, so marking ready would be premature.\n // The canonical place to mark ready is after the graph processes data\n // in maybeRunGraph(), which ensures data has been processed first.\n\n return loadSubsetDataCallbacks\n }\n}\n\nfunction buildQueryFromConfig<TContext extends Context>(\n config: LiveQueryCollectionConfig<any, any>,\n) {\n // Build the query using the provided query builder function or instance\n if (typeof config.query === `function`) {\n return buildQuery<TContext>(config.query)\n }\n return getQueryIR(config.query)\n}\n\nfunction createOrderByComparator<T extends object>(\n orderByIndices: WeakMap<object, string>,\n) {\n return (val1: T, val2: T): number => {\n // Use the orderBy index stored in the WeakMap\n const index1 = orderByIndices.get(val1)\n const index2 = orderByIndices.get(val2)\n\n // Compare fractional indices lexicographically\n if (index1 && index2) {\n if (index1 < index2) {\n return -1\n } else if (index1 > index2) {\n return 1\n } else {\n return 0\n }\n }\n\n // Fallback to no ordering if indices are missing\n return 0\n }\n}\n\n/**\n * Helper function to extract collections from a compiled query\n * Traverses the query IR to find all collection references\n * Maps collections by their ID (not alias) as expected by the compiler\n */\nfunction extractCollectionsFromQuery(\n query: any,\n): Record<string, Collection<any, any, any>> {\n const collections: Record<string, any> = {}\n\n // Helper function to recursively extract collections from a query or source\n function extractFromSource(source: any) {\n if (source.type === `collectionRef`) {\n collections[source.collection.id] = source.collection\n } else if (source.type === `queryRef`) {\n // Recursively extract from subquery\n extractFromQuery(source.query)\n }\n }\n\n // Helper function to recursively extract collections from a query\n function extractFromQuery(q: any) {\n // Extract from FROM clause\n if (q.from) {\n extractFromSource(q.from)\n }\n\n // Extract from JOIN clauses\n if (q.join && Array.isArray(q.join)) {\n for (const joinClause of q.join) {\n if (joinClause.from) {\n extractFromSource(joinClause.from)\n }\n }\n }\n }\n\n // Start extraction from the root query\n extractFromQuery(query)\n\n return collections\n}\n\n/**\n * Helper function to extract the collection that is referenced in the query's FROM clause.\n * The FROM clause may refer directly to a collection or indirectly to a subquery.\n */\nfunction extractCollectionFromSource(query: any): Collection<any, any, any> {\n const from = query.from\n\n if (from.type === `collectionRef`) {\n return from.collection\n } else if (from.type === `queryRef`) {\n // Recursively extract from subquery\n return extractCollectionFromSource(from.query)\n }\n\n throw new Error(\n `Failed to extract collection. Invalid FROM clause: ${JSON.stringify(query)}`,\n )\n}\n\n/**\n * Extracts all aliases used for each collection across the entire query tree.\n *\n * Traverses the QueryIR recursively to build a map from collection ID to all aliases\n * that reference that collection. This is essential for self-join support, where the\n * same collection may be referenced multiple times with different aliases.\n *\n * For example, given a query like:\n * ```ts\n * q.from({ employee: employeesCollection })\n * .join({ manager: employeesCollection }, ({ employee, manager }) =>\n * eq(employee.managerId, manager.id)\n * )\n * ```\n *\n * This function would return:\n * ```\n * Map { \"employees\" => Set { \"employee\", \"manager\" } }\n * ```\n *\n * @param query - The query IR to extract aliases from\n * @returns A map from collection ID to the set of all aliases referencing that collection\n */\nfunction extractCollectionAliases(query: QueryIR): Map<string, Set<string>> {\n const aliasesById = new Map<string, Set<string>>()\n\n function recordAlias(source: any) {\n if (!source) return\n\n if (source.type === `collectionRef`) {\n const { id } = source.collection\n const existing = aliasesById.get(id)\n if (existing) {\n existing.add(source.alias)\n } else {\n aliasesById.set(id, new Set([source.alias]))\n }\n } else if (source.type === `queryRef`) {\n traverse(source.query)\n }\n }\n\n function traverse(q?: QueryIR) {\n if (!q) return\n\n recordAlias(q.from)\n\n if (q.join) {\n for (const joinClause of q.join) {\n recordAlias(joinClause.from)\n }\n }\n }\n\n traverse(query)\n\n return aliasesById\n}\n\nfunction accumulateChanges<T>(\n acc: Map<unknown, Changes<T>>,\n [[key, tupleData], multiplicity]: [\n [unknown, [any, string | undefined]],\n number,\n ],\n) {\n // All queries now consistently return [value, orderByIndex] format\n // where orderByIndex is undefined for queries without ORDER BY\n const [value, orderByIndex] = tupleData as [T, string | undefined]\n\n const changes = acc.get(key) || {\n deletes: 0,\n inserts: 0,\n value,\n orderByIndex,\n }\n if (multiplicity < 0) {\n changes.deletes += Math.abs(multiplicity)\n } else if (multiplicity > 0) {\n changes.inserts += multiplicity\n // Update value to the latest version for this key\n changes.value = value\n if (orderByIndex !== undefined) {\n changes.orderByIndex = orderByIndex\n }\n }\n acc.set(key, changes)\n return acc\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"collection-config-builder.js","sources":["../../../../src/query/live/collection-config-builder.ts"],"sourcesContent":["import { D2, output } from '@tanstack/db-ivm'\nimport { compileQuery } from '../compiler/index.js'\nimport { buildQuery, getQueryIR } from '../builder/index.js'\nimport {\n MissingAliasInputsError,\n SetWindowRequiresOrderByError,\n} from '../../errors.js'\nimport { transactionScopedScheduler } from '../../scheduler.js'\nimport { getActiveTransaction } from '../../transactions.js'\nimport { CollectionSubscriber } from './collection-subscriber.js'\nimport { getCollectionBuilder } from './collection-registry.js'\nimport { LIVE_QUERY_INTERNAL } from './internal.js'\nimport type { LiveQueryInternalUtils } from './internal.js'\nimport type { WindowOptions } from '../compiler/index.js'\nimport type { SchedulerContextId } from '../../scheduler.js'\nimport type { CollectionSubscription } from '../../collection/subscription.js'\nimport type { RootStreamBuilder } from '@tanstack/db-ivm'\nimport type { OrderByOptimizationInfo } from '../compiler/order-by.js'\nimport type { Collection } from '../../collection/index.js'\nimport type {\n CollectionConfigSingleRowOption,\n KeyedStream,\n ResultStream,\n StringCollationConfig,\n SyncConfig,\n UtilsRecord,\n} from '../../types.js'\nimport type { Context, GetResult } from '../builder/types.js'\nimport type { BasicExpression, QueryIR } from '../ir.js'\nimport type { LazyCollectionCallbacks } from '../compiler/joins.js'\nimport type {\n Changes,\n FullSyncState,\n LiveQueryCollectionConfig,\n SyncState,\n} from './types.js'\nimport type { AllCollectionEvents } from '../../collection/events.js'\n\nexport type LiveQueryCollectionUtils = UtilsRecord & {\n getRunCount: () => number\n /**\n * Sets the offset and limit of an ordered query.\n * Is a no-op if the query is not ordered.\n *\n * @returns `true` if no subset loading was triggered, or `Promise<void>` that resolves when the subset has been loaded\n */\n setWindow: (options: WindowOptions) => true | Promise<void>\n /**\n * Gets the current window (offset and limit) for an ordered query.\n *\n * @returns The current window settings, or `undefined` if the query is not windowed\n */\n getWindow: () => { offset: number; limit: number } | undefined\n [LIVE_QUERY_INTERNAL]: LiveQueryInternalUtils\n}\n\ntype PendingGraphRun = {\n loadCallbacks: Set<() => boolean>\n}\n\n// Global counter for auto-generated collection IDs\nlet liveQueryCollectionCounter = 0\n\ntype SyncMethods<TResult extends object> = Parameters<\n SyncConfig<TResult>[`sync`]\n>[0]\n\nexport class CollectionConfigBuilder<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n> {\n private readonly id: string\n readonly query: QueryIR\n private readonly collections: Record<string, Collection<any, any, any>>\n private readonly collectionByAlias: Record<string, Collection<any, any, any>>\n // Populated during compilation with all aliases (including subquery inner aliases)\n private compiledAliasToCollectionId: Record<string, string> = {}\n\n // WeakMap to store the keys of the results\n // so that we can retrieve them in the getKey function\n private readonly resultKeys = new WeakMap<object, unknown>()\n\n // WeakMap to store the orderBy index for each result\n private readonly orderByIndices = new WeakMap<object, string>()\n\n private readonly compare?: (val1: TResult, val2: TResult) => number\n private readonly compareOptions?: StringCollationConfig\n\n private isGraphRunning = false\n private runCount = 0\n\n // Current sync session state (set when sync starts, cleared when it stops)\n // Public for testing purposes (CollectionConfigBuilder is internal, not public API)\n public currentSyncConfig:\n | Parameters<SyncConfig<TResult>[`sync`]>[0]\n | undefined\n public currentSyncState: FullSyncState | undefined\n\n // Error state tracking\n private isInErrorState = false\n\n // Reference to the live query collection for error state transitions\n public liveQueryCollection?: Collection<TResult, any, any>\n\n private windowFn: ((options: WindowOptions) => void) | undefined\n private currentWindow: WindowOptions | undefined\n\n private maybeRunGraphFn: (() => void) | undefined\n\n private readonly aliasDependencies: Record<\n string,\n Array<CollectionConfigBuilder<any, any>>\n > = {}\n\n private readonly builderDependencies = new Set<\n CollectionConfigBuilder<any, any>\n >()\n\n // Pending graph runs per scheduler context (e.g., per transaction)\n // The builder manages its own state; the scheduler just orchestrates execution order\n // Only stores callbacks - if sync ends, pending jobs gracefully no-op\n private readonly pendingGraphRuns = new Map<\n SchedulerContextId,\n PendingGraphRun\n >()\n\n // Unsubscribe function for scheduler's onClear listener\n // Registered when sync starts, unregistered when sync stops\n // Prevents memory leaks by releasing the scheduler's reference to this builder\n private unsubscribeFromSchedulerClears?: () => void\n\n private graphCache: D2 | undefined\n private inputsCache: Record<string, RootStreamBuilder<unknown>> | undefined\n private pipelineCache: ResultStream | undefined\n public sourceWhereClausesCache:\n | Map<string, BasicExpression<boolean>>\n | undefined\n\n // Map of source alias to subscription\n readonly subscriptions: Record<string, CollectionSubscription> = {}\n // Map of source aliases to functions that load keys for that lazy source\n lazySourcesCallbacks: Record<string, LazyCollectionCallbacks> = {}\n // Set of source aliases that are lazy (don't load initial state)\n readonly lazySources = new Set<string>()\n // Set of collection IDs that include an optimizable ORDER BY clause\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo> = {}\n\n constructor(\n private readonly config: LiveQueryCollectionConfig<TContext, TResult>,\n ) {\n // Generate a unique ID if not provided\n this.id = config.id || `live-query-${++liveQueryCollectionCounter}`\n\n this.query = buildQueryFromConfig(config)\n this.collections = extractCollectionsFromQuery(this.query)\n const collectionAliasesById = extractCollectionAliases(this.query)\n\n // Build a reverse lookup map from alias to collection instance.\n // This enables self-join support where the same collection can be referenced\n // multiple times with different aliases (e.g., { employee: col, manager: col })\n this.collectionByAlias = {}\n for (const [collectionId, aliases] of collectionAliasesById.entries()) {\n const collection = this.collections[collectionId]\n if (!collection) continue\n for (const alias of aliases) {\n this.collectionByAlias[alias] = collection\n }\n }\n\n // Create compare function for ordering if the query has orderBy\n if (this.query.orderBy && this.query.orderBy.length > 0) {\n this.compare = createOrderByComparator<TResult>(this.orderByIndices)\n }\n\n // Use explicitly provided compareOptions if available, otherwise inherit from FROM collection\n this.compareOptions =\n this.config.defaultStringCollation ??\n extractCollectionFromSource(this.query).compareOptions\n\n // Compile the base pipeline once initially\n // This is done to ensure that any errors are thrown immediately and synchronously\n this.compileBasePipeline()\n }\n\n /**\n * Recursively checks if a query or any of its subqueries contains joins\n */\n private hasJoins(query: QueryIR): boolean {\n // Check if this query has joins\n if (query.join && query.join.length > 0) {\n return true\n }\n\n // Recursively check subqueries in the from clause\n if (query.from.type === `queryRef`) {\n if (this.hasJoins(query.from.query)) {\n return true\n }\n }\n\n return false\n }\n\n getConfig(): CollectionConfigSingleRowOption<TResult> & {\n utils: LiveQueryCollectionUtils\n } {\n return {\n id: this.id,\n getKey:\n this.config.getKey ||\n ((item) => this.resultKeys.get(item) as string | number),\n sync: this.getSyncConfig(),\n compare: this.compare,\n defaultStringCollation: this.compareOptions,\n gcTime: this.config.gcTime || 5000, // 5 seconds by default for live queries\n schema: this.config.schema,\n onInsert: this.config.onInsert,\n onUpdate: this.config.onUpdate,\n onDelete: this.config.onDelete,\n startSync: this.config.startSync,\n singleResult: this.query.singleResult,\n utils: {\n getRunCount: this.getRunCount.bind(this),\n setWindow: this.setWindow.bind(this),\n getWindow: this.getWindow.bind(this),\n [LIVE_QUERY_INTERNAL]: {\n getBuilder: () => this,\n hasCustomGetKey: !!this.config.getKey,\n hasJoins: this.hasJoins(this.query),\n hasDistinct: !!this.query.distinct,\n },\n },\n }\n }\n\n setWindow(options: WindowOptions): true | Promise<void> {\n if (!this.windowFn) {\n throw new SetWindowRequiresOrderByError()\n }\n\n this.currentWindow = options\n this.windowFn(options)\n this.maybeRunGraphFn?.()\n\n // Check if loading a subset was triggered\n if (this.liveQueryCollection?.isLoadingSubset) {\n // Loading was triggered, return a promise that resolves when it completes\n return new Promise<void>((resolve) => {\n const unsubscribe = this.liveQueryCollection!.on(\n `loadingSubset:change`,\n (event) => {\n if (!event.isLoadingSubset) {\n unsubscribe()\n resolve()\n }\n },\n )\n })\n }\n\n // No loading was triggered\n return true\n }\n\n getWindow(): { offset: number; limit: number } | undefined {\n // Only return window if this is a windowed query (has orderBy and windowFn)\n if (!this.windowFn || !this.currentWindow) {\n return undefined\n }\n return {\n offset: this.currentWindow.offset ?? 0,\n limit: this.currentWindow.limit ?? 0,\n }\n }\n\n /**\n * Resolves a collection alias to its collection ID.\n *\n * Uses a two-tier lookup strategy:\n * 1. First checks compiled aliases (includes subquery inner aliases)\n * 2. Falls back to declared aliases from the query's from/join clauses\n *\n * @param alias - The alias to resolve (e.g., \"employee\", \"manager\")\n * @returns The collection ID that the alias references\n * @throws {Error} If the alias is not found in either lookup\n */\n getCollectionIdForAlias(alias: string): string {\n const compiled = this.compiledAliasToCollectionId[alias]\n if (compiled) {\n return compiled\n }\n const collection = this.collectionByAlias[alias]\n if (collection) {\n return collection.id\n }\n throw new Error(`Unknown source alias \"${alias}\"`)\n }\n\n isLazyAlias(alias: string): boolean {\n return this.lazySources.has(alias)\n }\n\n // The callback function is called after the graph has run.\n // This gives the callback a chance to load more data if needed,\n // that's used to optimize orderBy operators that set a limit,\n // in order to load some more data if we still don't have enough rows after the pipeline has run.\n // That can happen because even though we load N rows, the pipeline might filter some of these rows out\n // causing the orderBy operator to receive less than N rows or even no rows at all.\n // So this callback would notice that it doesn't have enough rows and load some more.\n // The callback returns a boolean, when it's true it's done loading data and we can mark the collection as ready.\n maybeRunGraph(callback?: () => boolean) {\n if (this.isGraphRunning) {\n // no nested runs of the graph\n // which is possible if the `callback`\n // would call `maybeRunGraph` e.g. after it has loaded some more data\n return\n }\n\n // Should only be called when sync is active\n if (!this.currentSyncConfig || !this.currentSyncState) {\n throw new Error(\n `maybeRunGraph called without active sync session. This should not happen.`,\n )\n }\n\n this.isGraphRunning = true\n\n try {\n const { begin, commit } = this.currentSyncConfig\n const syncState = this.currentSyncState\n\n // Don't run if the live query is in an error state\n if (this.isInErrorState) {\n return\n }\n\n // Always run the graph if subscribed (eager execution)\n if (syncState.subscribedToAllCollections) {\n let callbackCalled = false\n while (syncState.graph.pendingWork()) {\n syncState.graph.run()\n // Flush accumulated changes after each graph step to commit them as one transaction.\n // This ensures intermediate join states (like null on one side) don't cause\n // duplicate key errors when the full join result arrives in the same step.\n syncState.flushPendingChanges?.()\n callback?.()\n callbackCalled = true\n }\n\n // Ensure the callback runs at least once even when the graph has no pending work.\n // This handles lazy loading scenarios where setWindow() increases the limit or\n // an async loadSubset completes and we need to re-check if more data is needed.\n if (!callbackCalled) {\n callback?.()\n }\n\n // On the initial run, we may need to do an empty commit to ensure that\n // the collection is initialized\n if (syncState.messagesCount === 0) {\n begin()\n commit()\n }\n\n // After graph processing completes, check if we should mark ready.\n // This is the canonical place to transition to ready state because:\n // 1. All data has been processed through the graph\n // 2. All source collections have had a chance to send their initial data\n // This prevents marking ready before data is processed (fixes isReady=true with empty data)\n this.updateLiveQueryStatus(this.currentSyncConfig)\n }\n } finally {\n this.isGraphRunning = false\n }\n }\n\n /**\n * Schedules a graph run with the transaction-scoped scheduler.\n * Ensures each builder runs at most once per transaction, with automatic dependency tracking\n * to run parent queries before child queries. Outside a transaction, runs immediately.\n *\n * Multiple calls during a transaction are coalesced into a single execution.\n * Dependencies are auto-discovered from subscribed live queries, or can be overridden.\n * Load callbacks are combined when entries merge.\n *\n * Uses the current sync session's config and syncState from instance properties.\n *\n * @param callback - Optional callback to load more data if needed (returns true when done)\n * @param options - Optional scheduling configuration\n * @param options.contextId - Transaction ID to group work; defaults to active transaction\n * @param options.jobId - Unique identifier for this job; defaults to this builder instance\n * @param options.alias - Source alias that triggered this schedule; adds alias-specific dependencies\n * @param options.dependencies - Explicit dependency list; overrides auto-discovered dependencies\n */\n scheduleGraphRun(\n callback?: () => boolean,\n options?: {\n contextId?: SchedulerContextId\n jobId?: unknown\n alias?: string\n dependencies?: Array<CollectionConfigBuilder<any, any>>\n },\n ) {\n const contextId = options?.contextId ?? getActiveTransaction()?.id\n // Use the builder instance as the job ID for deduplication. This is memory-safe\n // because the scheduler's context Map is deleted after flushing (no long-term retention).\n const jobId = options?.jobId ?? this\n const dependentBuilders = (() => {\n if (options?.dependencies) {\n return options.dependencies\n }\n\n const deps = new Set(this.builderDependencies)\n if (options?.alias) {\n const aliasDeps = this.aliasDependencies[options.alias]\n if (aliasDeps) {\n for (const dep of aliasDeps) {\n deps.add(dep)\n }\n }\n }\n\n deps.delete(this)\n\n return Array.from(deps)\n })()\n\n // Ensure dependent builders are actually scheduled in this context so that\n // dependency edges always point to a real job (or a deduped no-op if already scheduled).\n if (contextId) {\n for (const dep of dependentBuilders) {\n if (typeof dep.scheduleGraphRun === `function`) {\n dep.scheduleGraphRun(undefined, { contextId })\n }\n }\n }\n\n // We intentionally scope deduplication to the builder instance. Each instance\n // owns caches and compiled pipelines, so sharing work across instances that\n // merely reuse the same string id would execute the wrong builder's graph.\n\n if (!this.currentSyncConfig || !this.currentSyncState) {\n throw new Error(\n `scheduleGraphRun called without active sync session. This should not happen.`,\n )\n }\n\n // Manage our own state - get or create pending callbacks for this context\n let pending = contextId ? this.pendingGraphRuns.get(contextId) : undefined\n if (!pending) {\n pending = {\n loadCallbacks: new Set(),\n }\n if (contextId) {\n this.pendingGraphRuns.set(contextId, pending)\n }\n }\n\n // Add callback if provided (this is what accumulates between schedules)\n if (callback) {\n pending.loadCallbacks.add(callback)\n }\n\n // Schedule execution (scheduler just orchestrates order, we manage state)\n // For immediate execution (no contextId), pass pending directly since it won't be in the map\n const pendingToPass = contextId ? undefined : pending\n transactionScopedScheduler.schedule({\n contextId,\n jobId,\n dependencies: dependentBuilders,\n run: () => this.executeGraphRun(contextId, pendingToPass),\n })\n }\n\n /**\n * Clears pending graph run state for a specific context.\n * Called when the scheduler clears a context (e.g., transaction rollback/abort).\n */\n clearPendingGraphRun(contextId: SchedulerContextId): void {\n this.pendingGraphRuns.delete(contextId)\n }\n\n /**\n * Returns true if this builder has a pending graph run for the given context.\n */\n hasPendingGraphRun(contextId: SchedulerContextId): boolean {\n return this.pendingGraphRuns.has(contextId)\n }\n\n /**\n * Executes a pending graph run. Called by the scheduler when dependencies are satisfied.\n * Clears the pending state BEFORE execution so that any re-schedules during the run\n * create fresh state and don't interfere with the current execution.\n * Uses instance sync state - if sync has ended, gracefully returns without executing.\n *\n * @param contextId - Optional context ID to look up pending state\n * @param pendingParam - For immediate execution (no context), pending state is passed directly\n */\n private executeGraphRun(\n contextId?: SchedulerContextId,\n pendingParam?: PendingGraphRun,\n ): void {\n // Get pending state: either from parameter (no context) or from map (with context)\n // Remove from map BEFORE checking sync state to prevent leaking entries when sync ends\n // before the transaction flushes (e.g., unsubscribe during in-flight transaction)\n const pending =\n pendingParam ??\n (contextId ? this.pendingGraphRuns.get(contextId) : undefined)\n if (contextId) {\n this.pendingGraphRuns.delete(contextId)\n }\n\n // If no pending state, nothing to execute (context was cleared)\n if (!pending) {\n return\n }\n\n // If sync session has ended, don't execute (graph is finalized, subscriptions cleared)\n if (!this.currentSyncConfig || !this.currentSyncState) {\n return\n }\n\n this.incrementRunCount()\n\n const combinedLoader = () => {\n let allDone = true\n let firstError: unknown\n pending.loadCallbacks.forEach((loader) => {\n try {\n allDone = loader() && allDone\n } catch (error) {\n allDone = false\n firstError ??= error\n }\n })\n if (firstError) {\n throw firstError\n }\n // Returning false signals that callers should schedule another pass.\n return allDone\n }\n\n this.maybeRunGraph(combinedLoader)\n }\n\n private getSyncConfig(): SyncConfig<TResult> {\n return {\n rowUpdateMode: `full`,\n sync: this.syncFn.bind(this),\n }\n }\n\n incrementRunCount() {\n this.runCount++\n }\n\n getRunCount() {\n return this.runCount\n }\n\n private syncFn(config: SyncMethods<TResult>) {\n // Store reference to the live query collection for error state transitions\n this.liveQueryCollection = config.collection\n // Store config and syncState as instance properties for the duration of this sync session\n this.currentSyncConfig = config\n\n const syncState: SyncState = {\n messagesCount: 0,\n subscribedToAllCollections: false,\n unsubscribeCallbacks: new Set<() => void>(),\n }\n\n // Extend the pipeline such that it applies the incoming changes to the collection\n const fullSyncState = this.extendPipelineWithChangeProcessing(\n config,\n syncState,\n )\n this.currentSyncState = fullSyncState\n\n // Listen for scheduler context clears to clean up our pending state\n // Re-register on each sync start so the listener is active for the sync session's lifetime\n this.unsubscribeFromSchedulerClears = transactionScopedScheduler.onClear(\n (contextId) => {\n this.clearPendingGraphRun(contextId)\n },\n )\n\n // Listen for loadingSubset changes on the live query collection BEFORE subscribing.\n // This ensures we don't miss the event if subset loading completes synchronously.\n // When isLoadingSubset becomes false, we may need to mark the collection as ready\n // (if all source collections are already ready but we were waiting for subset load to complete)\n const loadingSubsetUnsubscribe = config.collection.on(\n `loadingSubset:change`,\n (event) => {\n if (!event.isLoadingSubset) {\n // Subset loading finished, check if we can now mark ready\n this.updateLiveQueryStatus(config)\n }\n },\n )\n syncState.unsubscribeCallbacks.add(loadingSubsetUnsubscribe)\n\n const loadSubsetDataCallbacks = this.subscribeToAllCollections(\n config,\n fullSyncState,\n )\n\n this.maybeRunGraphFn = () => this.scheduleGraphRun(loadSubsetDataCallbacks)\n\n // Initial run with callback to load more data if needed\n this.scheduleGraphRun(loadSubsetDataCallbacks)\n\n // Return the unsubscribe function\n return () => {\n syncState.unsubscribeCallbacks.forEach((unsubscribe) => unsubscribe())\n\n // Clear current sync session state\n this.currentSyncConfig = undefined\n this.currentSyncState = undefined\n\n // Clear all pending graph runs to prevent memory leaks from in-flight transactions\n // that may flush after the sync session ends\n this.pendingGraphRuns.clear()\n\n // Reset caches so a fresh graph/pipeline is compiled on next start\n // This avoids reusing a finalized D2 graph across GC restarts\n this.graphCache = undefined\n this.inputsCache = undefined\n this.pipelineCache = undefined\n this.sourceWhereClausesCache = undefined\n\n // Reset lazy source alias state\n this.lazySources.clear()\n this.optimizableOrderByCollections = {}\n this.lazySourcesCallbacks = {}\n\n // Clear subscription references to prevent memory leaks\n // Note: Individual subscriptions are already unsubscribed via unsubscribeCallbacks\n Object.keys(this.subscriptions).forEach(\n (key) => delete this.subscriptions[key],\n )\n this.compiledAliasToCollectionId = {}\n\n // Unregister from scheduler's onClear listener to prevent memory leaks\n // The scheduler's listener Set would otherwise keep a strong reference to this builder\n this.unsubscribeFromSchedulerClears?.()\n this.unsubscribeFromSchedulerClears = undefined\n }\n }\n\n /**\n * Compiles the query pipeline with all declared aliases.\n */\n private compileBasePipeline() {\n this.graphCache = new D2()\n this.inputsCache = Object.fromEntries(\n Object.keys(this.collectionByAlias).map((alias) => [\n alias,\n this.graphCache!.newInput<any>(),\n ]),\n )\n\n const compilation = compileQuery(\n this.query,\n this.inputsCache as Record<string, KeyedStream>,\n this.collections,\n this.subscriptions,\n this.lazySourcesCallbacks,\n this.lazySources,\n this.optimizableOrderByCollections,\n (windowFn: (options: WindowOptions) => void) => {\n this.windowFn = windowFn\n },\n )\n\n this.pipelineCache = compilation.pipeline\n this.sourceWhereClausesCache = compilation.sourceWhereClauses\n this.compiledAliasToCollectionId = compilation.aliasToCollectionId\n\n // Defensive check: verify all compiled aliases have corresponding inputs\n // This should never happen since all aliases come from user declarations,\n // but catch it early if the assumption is violated in the future.\n const missingAliases = Object.keys(this.compiledAliasToCollectionId).filter(\n (alias) => !Object.hasOwn(this.inputsCache!, alias),\n )\n if (missingAliases.length > 0) {\n throw new MissingAliasInputsError(missingAliases)\n }\n }\n\n private maybeCompileBasePipeline() {\n if (!this.graphCache || !this.inputsCache || !this.pipelineCache) {\n this.compileBasePipeline()\n }\n return {\n graph: this.graphCache!,\n inputs: this.inputsCache!,\n pipeline: this.pipelineCache!,\n }\n }\n\n private extendPipelineWithChangeProcessing(\n config: SyncMethods<TResult>,\n syncState: SyncState,\n ): FullSyncState {\n const { begin, commit } = config\n const { graph, inputs, pipeline } = this.maybeCompileBasePipeline()\n\n // Accumulator for changes across all output callbacks within a single graph run.\n // This allows us to batch all changes from intermediate join states into a single\n // transaction, avoiding duplicate key errors when joins produce multiple outputs\n // for the same key (e.g., first output with null, then output with joined data).\n let pendingChanges: Map<unknown, Changes<TResult>> = new Map()\n\n pipeline.pipe(\n output((data) => {\n const messages = data.getInner()\n syncState.messagesCount += messages.length\n\n // Accumulate changes from this output callback into the pending changes map.\n // Changes for the same key are merged (inserts/deletes are added together).\n messages.reduce(accumulateChanges<TResult>, pendingChanges)\n }),\n )\n\n // Flush pending changes and reset the accumulator.\n // Called at the end of each graph run to commit all accumulated changes.\n syncState.flushPendingChanges = () => {\n if (pendingChanges.size === 0) {\n return\n }\n begin()\n pendingChanges.forEach(this.applyChanges.bind(this, config))\n commit()\n pendingChanges = new Map()\n }\n\n graph.finalize()\n\n // Extend the sync state with the graph, inputs, and pipeline\n syncState.graph = graph\n syncState.inputs = inputs\n syncState.pipeline = pipeline\n\n return syncState as FullSyncState\n }\n\n private applyChanges(\n config: SyncMethods<TResult>,\n changes: {\n deletes: number\n inserts: number\n value: TResult\n orderByIndex: string | undefined\n },\n key: unknown,\n ) {\n const { write, collection } = config\n const { deletes, inserts, value, orderByIndex } = changes\n\n // Store the key of the result so that we can retrieve it in the\n // getKey function\n this.resultKeys.set(value, key)\n\n // Store the orderBy index if it exists\n if (orderByIndex !== undefined) {\n this.orderByIndices.set(value, orderByIndex)\n }\n\n // Simple singular insert.\n if (inserts && deletes === 0) {\n write({\n value,\n type: `insert`,\n })\n } else if (\n // Insert & update(s) (updates are a delete & insert)\n inserts > deletes ||\n // Just update(s) but the item is already in the collection (so\n // was inserted previously).\n (inserts === deletes && collection.has(collection.getKeyFromItem(value)))\n ) {\n write({\n value,\n type: `update`,\n })\n // Only delete is left as an option\n } else if (deletes > 0) {\n write({\n value,\n type: `delete`,\n })\n } else {\n throw new Error(\n `Could not apply changes: ${JSON.stringify(changes)}. This should never happen.`,\n )\n }\n }\n\n /**\n * Handle status changes from source collections\n */\n private handleSourceStatusChange(\n config: SyncMethods<TResult>,\n collectionId: string,\n event: AllCollectionEvents[`status:change`],\n ) {\n const { status } = event\n\n // Handle error state - any source collection in error puts live query in error\n if (status === `error`) {\n this.transitionToError(\n `Source collection '${collectionId}' entered error state`,\n )\n return\n }\n\n // Handle manual cleanup - this should not happen due to GC prevention,\n // but could happen if user manually calls cleanup()\n if (status === `cleaned-up`) {\n this.transitionToError(\n `Source collection '${collectionId}' was manually cleaned up while live query '${this.id}' depends on it. ` +\n `Live queries prevent automatic GC, so this was likely a manual cleanup() call.`,\n )\n return\n }\n\n // Update ready status based on all source collections\n this.updateLiveQueryStatus(config)\n }\n\n /**\n * Update the live query status based on source collection statuses\n */\n private updateLiveQueryStatus(config: SyncMethods<TResult>) {\n const { markReady } = config\n\n // Don't update status if already in error\n if (this.isInErrorState) {\n return\n }\n\n const subscribedToAll = this.currentSyncState?.subscribedToAllCollections\n const allReady = this.allCollectionsReady()\n const isLoading = this.liveQueryCollection?.isLoadingSubset\n // Mark ready when:\n // 1. All subscriptions are set up (subscribedToAllCollections)\n // 2. All source collections are ready\n // 3. The live query collection is not loading subset data\n // This prevents marking the live query ready before its data is processed\n // (fixes issue where useLiveQuery returns isReady=true with empty data)\n if (subscribedToAll && allReady && !isLoading) {\n markReady()\n }\n }\n\n /**\n * Transition the live query to error state\n */\n private transitionToError(message: string) {\n this.isInErrorState = true\n\n // Log error to console for debugging\n console.error(`[Live Query Error] ${message}`)\n\n // Transition live query collection to error state\n this.liveQueryCollection?._lifecycle.setStatus(`error`)\n }\n\n private allCollectionsReady() {\n return Object.values(this.collections).every((collection) =>\n collection.isReady(),\n )\n }\n\n /**\n * Creates per-alias subscriptions enabling self-join support.\n * Each alias gets its own subscription with independent filters, even for the same collection.\n * Example: `{ employee: col, manager: col }` creates two separate subscriptions.\n */\n private subscribeToAllCollections(\n config: SyncMethods<TResult>,\n syncState: FullSyncState,\n ) {\n // Use compiled aliases as the source of truth - these include all aliases from the query\n // including those from subqueries, which may not be in collectionByAlias\n const compiledAliases = Object.entries(this.compiledAliasToCollectionId)\n if (compiledAliases.length === 0) {\n throw new Error(\n `Compiler returned no alias metadata for query '${this.id}'. This should not happen; please report.`,\n )\n }\n\n // Create a separate subscription for each alias, enabling self-joins where the same\n // collection can be used multiple times with different filters and subscriptions\n const loaders = compiledAliases.map(([alias, collectionId]) => {\n // Try collectionByAlias first (for declared aliases), fall back to collections (for subquery aliases)\n const collection =\n this.collectionByAlias[alias] ?? this.collections[collectionId]!\n\n const dependencyBuilder = getCollectionBuilder(collection)\n if (dependencyBuilder && dependencyBuilder !== this) {\n this.aliasDependencies[alias] = [dependencyBuilder]\n this.builderDependencies.add(dependencyBuilder)\n } else {\n this.aliasDependencies[alias] = []\n }\n\n // CollectionSubscriber handles the actual subscription to the source collection\n // and feeds data into the D2 graph inputs for this specific alias\n const collectionSubscriber = new CollectionSubscriber(\n alias,\n collectionId,\n collection,\n this,\n )\n\n // Subscribe to status changes for status flow\n const statusUnsubscribe = collection.on(`status:change`, (event) => {\n this.handleSourceStatusChange(config, collectionId, event)\n })\n syncState.unsubscribeCallbacks.add(statusUnsubscribe)\n\n const subscription = collectionSubscriber.subscribe()\n // Store subscription by alias (not collection ID) to support lazy loading\n // which needs to look up subscriptions by their query alias\n this.subscriptions[alias] = subscription\n\n // Create a callback for loading more data if needed (used by OrderBy optimization)\n const loadMore = collectionSubscriber.loadMoreIfNeeded.bind(\n collectionSubscriber,\n subscription,\n )\n\n return loadMore\n })\n\n // Combine all loaders into a single callback that initiates loading more data\n // from any source that needs it. Returns true once all loaders have been called,\n // but the actual async loading may still be in progress.\n const loadSubsetDataCallbacks = () => {\n loaders.map((loader) => loader())\n return true\n }\n\n // Mark as subscribed so the graph can start running\n // (graph only runs when all collections are subscribed)\n syncState.subscribedToAllCollections = true\n\n // Note: We intentionally don't call updateLiveQueryStatus() here.\n // The graph hasn't run yet, so marking ready would be premature.\n // The canonical place to mark ready is after the graph processes data\n // in maybeRunGraph(), which ensures data has been processed first.\n\n return loadSubsetDataCallbacks\n }\n}\n\nfunction buildQueryFromConfig<TContext extends Context>(\n config: LiveQueryCollectionConfig<any, any>,\n) {\n // Build the query using the provided query builder function or instance\n if (typeof config.query === `function`) {\n return buildQuery<TContext>(config.query)\n }\n return getQueryIR(config.query)\n}\n\nfunction createOrderByComparator<T extends object>(\n orderByIndices: WeakMap<object, string>,\n) {\n return (val1: T, val2: T): number => {\n // Use the orderBy index stored in the WeakMap\n const index1 = orderByIndices.get(val1)\n const index2 = orderByIndices.get(val2)\n\n // Compare fractional indices lexicographically\n if (index1 && index2) {\n if (index1 < index2) {\n return -1\n } else if (index1 > index2) {\n return 1\n } else {\n return 0\n }\n }\n\n // Fallback to no ordering if indices are missing\n return 0\n }\n}\n\n/**\n * Helper function to extract collections from a compiled query\n * Traverses the query IR to find all collection references\n * Maps collections by their ID (not alias) as expected by the compiler\n */\nfunction extractCollectionsFromQuery(\n query: any,\n): Record<string, Collection<any, any, any>> {\n const collections: Record<string, any> = {}\n\n // Helper function to recursively extract collections from a query or source\n function extractFromSource(source: any) {\n if (source.type === `collectionRef`) {\n collections[source.collection.id] = source.collection\n } else if (source.type === `queryRef`) {\n // Recursively extract from subquery\n extractFromQuery(source.query)\n }\n }\n\n // Helper function to recursively extract collections from a query\n function extractFromQuery(q: any) {\n // Extract from FROM clause\n if (q.from) {\n extractFromSource(q.from)\n }\n\n // Extract from JOIN clauses\n if (q.join && Array.isArray(q.join)) {\n for (const joinClause of q.join) {\n if (joinClause.from) {\n extractFromSource(joinClause.from)\n }\n }\n }\n }\n\n // Start extraction from the root query\n extractFromQuery(query)\n\n return collections\n}\n\n/**\n * Helper function to extract the collection that is referenced in the query's FROM clause.\n * The FROM clause may refer directly to a collection or indirectly to a subquery.\n */\nfunction extractCollectionFromSource(query: any): Collection<any, any, any> {\n const from = query.from\n\n if (from.type === `collectionRef`) {\n return from.collection\n } else if (from.type === `queryRef`) {\n // Recursively extract from subquery\n return extractCollectionFromSource(from.query)\n }\n\n throw new Error(\n `Failed to extract collection. Invalid FROM clause: ${JSON.stringify(query)}`,\n )\n}\n\n/**\n * Extracts all aliases used for each collection across the entire query tree.\n *\n * Traverses the QueryIR recursively to build a map from collection ID to all aliases\n * that reference that collection. This is essential for self-join support, where the\n * same collection may be referenced multiple times with different aliases.\n *\n * For example, given a query like:\n * ```ts\n * q.from({ employee: employeesCollection })\n * .join({ manager: employeesCollection }, ({ employee, manager }) =>\n * eq(employee.managerId, manager.id)\n * )\n * ```\n *\n * This function would return:\n * ```\n * Map { \"employees\" => Set { \"employee\", \"manager\" } }\n * ```\n *\n * @param query - The query IR to extract aliases from\n * @returns A map from collection ID to the set of all aliases referencing that collection\n */\nfunction extractCollectionAliases(query: QueryIR): Map<string, Set<string>> {\n const aliasesById = new Map<string, Set<string>>()\n\n function recordAlias(source: any) {\n if (!source) return\n\n if (source.type === `collectionRef`) {\n const { id } = source.collection\n const existing = aliasesById.get(id)\n if (existing) {\n existing.add(source.alias)\n } else {\n aliasesById.set(id, new Set([source.alias]))\n }\n } else if (source.type === `queryRef`) {\n traverse(source.query)\n }\n }\n\n function traverse(q?: QueryIR) {\n if (!q) return\n\n recordAlias(q.from)\n\n if (q.join) {\n for (const joinClause of q.join) {\n recordAlias(joinClause.from)\n }\n }\n }\n\n traverse(query)\n\n return aliasesById\n}\n\nfunction accumulateChanges<T>(\n acc: Map<unknown, Changes<T>>,\n [[key, tupleData], multiplicity]: [\n [unknown, [any, string | undefined]],\n number,\n ],\n) {\n // All queries now consistently return [value, orderByIndex] format\n // where orderByIndex is undefined for queries without ORDER BY\n const [value, orderByIndex] = tupleData as [T, string | undefined]\n\n const changes = acc.get(key) || {\n deletes: 0,\n inserts: 0,\n value,\n orderByIndex,\n }\n if (multiplicity < 0) {\n changes.deletes += Math.abs(multiplicity)\n } else if (multiplicity > 0) {\n changes.inserts += multiplicity\n // Update value to the latest version for this key\n changes.value = value\n if (orderByIndex !== undefined) {\n changes.orderByIndex = orderByIndex\n }\n }\n acc.set(key, changes)\n return acc\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/db",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.26",
|
|
4
4
|
"description": "A reactive client store for building super fast apps on sync",
|
|
5
5
|
"author": "Kyle Mathews",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@standard-schema/spec": "^1.1.0",
|
|
41
|
-
"@tanstack/pacer-lite": "^0.2.
|
|
41
|
+
"@tanstack/pacer-lite": "^0.2.1",
|
|
42
42
|
"@tanstack/db-ivm": "0.1.17"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
@@ -910,8 +910,26 @@ export type QueryResult<T> = GetResult<ExtractContext<T>>
|
|
|
910
910
|
// Export the types from types.ts for convenience
|
|
911
911
|
export type {
|
|
912
912
|
Context,
|
|
913
|
+
ContextSchema,
|
|
913
914
|
Source,
|
|
914
915
|
GetResult,
|
|
915
916
|
RefLeaf as Ref,
|
|
916
917
|
InferResultType,
|
|
918
|
+
// Types used in public method signatures that must be exported
|
|
919
|
+
// for declaration emit to work (see https://github.com/TanStack/db/issues/1012)
|
|
920
|
+
SchemaFromSource,
|
|
921
|
+
InferCollectionType,
|
|
922
|
+
MergeContextWithJoinType,
|
|
923
|
+
MergeContextForJoinCallback,
|
|
924
|
+
ApplyJoinOptionalityToMergedSchema,
|
|
925
|
+
ResultTypeFromSelect,
|
|
926
|
+
WithResult,
|
|
927
|
+
JoinOnCallback,
|
|
928
|
+
RefsForContext,
|
|
929
|
+
WhereCallback,
|
|
930
|
+
OrderByCallback,
|
|
931
|
+
GroupByCallback,
|
|
932
|
+
SelectObject,
|
|
933
|
+
FunctionalHavingRow,
|
|
934
|
+
Prettify,
|
|
917
935
|
} from './types.js'
|
|
@@ -336,7 +336,7 @@ function compileFunction(func: Func, isSingleRow: boolean): (data: any) => any {
|
|
|
336
336
|
const valueEvaluator = compiledArgs[0]!
|
|
337
337
|
const arrayEvaluator = compiledArgs[1]!
|
|
338
338
|
return (data) => {
|
|
339
|
-
const value = valueEvaluator(data)
|
|
339
|
+
const value = normalizeValue(valueEvaluator(data))
|
|
340
340
|
const array = arrayEvaluator(data)
|
|
341
341
|
// In 3-valued logic, if the value is null/undefined, return UNKNOWN
|
|
342
342
|
if (isUnknown(value)) {
|
|
@@ -345,7 +345,7 @@ function compileFunction(func: Func, isSingleRow: boolean): (data: any) => any {
|
|
|
345
345
|
if (!Array.isArray(array)) {
|
|
346
346
|
return false
|
|
347
347
|
}
|
|
348
|
-
return array.
|
|
348
|
+
return array.some((item) => normalizeValue(item) === value)
|
|
349
349
|
}
|
|
350
350
|
}
|
|
351
351
|
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
UnsupportedJoinSourceTypeError,
|
|
12
12
|
UnsupportedJoinTypeError,
|
|
13
13
|
} from '../../errors.js'
|
|
14
|
+
import { normalizeValue } from '../../utils/comparison.js'
|
|
14
15
|
import { ensureIndexForField } from '../../indexes/auto-index.js'
|
|
15
16
|
import { PropRef, followRef } from '../ir.js'
|
|
16
17
|
import { inArray } from '../builder/functions.js'
|
|
@@ -188,7 +189,7 @@ function processJoin(
|
|
|
188
189
|
let mainPipeline = pipeline.pipe(
|
|
189
190
|
map(([currentKey, namespacedRow]) => {
|
|
190
191
|
// Extract the join key from the main source expression
|
|
191
|
-
const mainKey = compiledMainExpr(namespacedRow)
|
|
192
|
+
const mainKey = normalizeValue(compiledMainExpr(namespacedRow))
|
|
192
193
|
|
|
193
194
|
// Return [joinKey, [originalKey, namespacedRow]]
|
|
194
195
|
return [mainKey, [currentKey, namespacedRow]] as [
|
|
@@ -205,7 +206,7 @@ function processJoin(
|
|
|
205
206
|
const namespacedRow: NamespacedRow = { [joinedSource]: row }
|
|
206
207
|
|
|
207
208
|
// Extract the join key from the joined source expression
|
|
208
|
-
const joinedKey = compiledJoinedExpr(namespacedRow)
|
|
209
|
+
const joinedKey = normalizeValue(compiledJoinedExpr(namespacedRow))
|
|
209
210
|
|
|
210
211
|
// Return [joinKey, [originalKey, namespacedRow]]
|
|
211
212
|
return [joinedKey, [currentKey, namespacedRow]] as [
|
package/src/query/index.ts
CHANGED
|
@@ -7,11 +7,28 @@ export {
|
|
|
7
7
|
type InitialQueryBuilder,
|
|
8
8
|
type QueryBuilder,
|
|
9
9
|
type Context,
|
|
10
|
+
type ContextSchema,
|
|
10
11
|
type Source,
|
|
11
12
|
type GetResult,
|
|
12
13
|
type InferResultType,
|
|
13
14
|
type ExtractContext,
|
|
14
15
|
type QueryResult,
|
|
16
|
+
// Types needed for declaration emit (https://github.com/TanStack/db/issues/1012)
|
|
17
|
+
type SchemaFromSource,
|
|
18
|
+
type InferCollectionType,
|
|
19
|
+
type MergeContextWithJoinType,
|
|
20
|
+
type MergeContextForJoinCallback,
|
|
21
|
+
type ApplyJoinOptionalityToMergedSchema,
|
|
22
|
+
type ResultTypeFromSelect,
|
|
23
|
+
type WithResult,
|
|
24
|
+
type JoinOnCallback,
|
|
25
|
+
type RefsForContext,
|
|
26
|
+
type WhereCallback,
|
|
27
|
+
type OrderByCallback,
|
|
28
|
+
type GroupByCallback,
|
|
29
|
+
type SelectObject,
|
|
30
|
+
type FunctionalHavingRow,
|
|
31
|
+
type Prettify,
|
|
15
32
|
} from './builder/index.js'
|
|
16
33
|
|
|
17
34
|
// Expression functions exports
|
|
@@ -336,6 +336,7 @@ export class CollectionConfigBuilder<
|
|
|
336
336
|
|
|
337
337
|
// Always run the graph if subscribed (eager execution)
|
|
338
338
|
if (syncState.subscribedToAllCollections) {
|
|
339
|
+
let callbackCalled = false
|
|
339
340
|
while (syncState.graph.pendingWork()) {
|
|
340
341
|
syncState.graph.run()
|
|
341
342
|
// Flush accumulated changes after each graph step to commit them as one transaction.
|
|
@@ -343,6 +344,14 @@ export class CollectionConfigBuilder<
|
|
|
343
344
|
// duplicate key errors when the full join result arrives in the same step.
|
|
344
345
|
syncState.flushPendingChanges?.()
|
|
345
346
|
callback?.()
|
|
347
|
+
callbackCalled = true
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Ensure the callback runs at least once even when the graph has no pending work.
|
|
351
|
+
// This handles lazy loading scenarios where setWindow() increases the limit or
|
|
352
|
+
// an async loadSubset completes and we need to re-check if more data is needed.
|
|
353
|
+
if (!callbackCalled) {
|
|
354
|
+
callback?.()
|
|
346
355
|
}
|
|
347
356
|
|
|
348
357
|
// On the initial run, we may need to do an empty commit to ensure that
|