@tanstack/db
Version:
A reactive client store for building super fast apps on sync
439 lines (438 loc) • 16.7 kB
JavaScript
import { map, tap, serializeValue, join, filter } from "@tanstack/db-ivm";
import { InvalidJoinConditionSourceMismatchError, JoinCollectionNotFoundError, UnsupportedJoinTypeError, InvalidJoinConditionSameSourceError, InvalidJoinConditionLeftSourceError, InvalidJoinConditionRightSourceError, InvalidJoinCondition, UnsupportedJoinSourceTypeError, CollectionInputNotFoundError } from "../../errors.js";
import { normalizeValue } from "../../utils/comparison.js";
import { ensureIndexForField } from "../../indexes/auto-index.js";
import { compileExpression } from "./evaluators.js";
import { getLazyLoadTargets } from "./lazy-targets.js";
import { crossJoinParentRoutes } from "./parent-routes.js";
import { attachRouteMetadataToResult, getRoutedScalarMetadata, INCLUDES_PUBLIC_KEY } from "./route-metadata.js";
let nextLazyDemandPlanId = 0;
function parameterizeJoinInputByParentRoutes(input, parentKeyStream) {
return crossJoinParentRoutes(
input,
parentKeyStream,
(rowKey, row, correlationKey, parentContext) => {
return [
serializeValue([rowKey, correlationKey, parentContext]),
{
...row,
__correlationKey: correlationKey,
__parentContext: parentContext
}
];
}
);
}
function wrapJoinedInputRow(alias, row) {
const scalar = getRoutedScalarMetadata(row);
if (scalar) {
const namespaced2 = {
[alias]: scalar.value,
__correlationKey: scalar.correlationKey,
__parentContext: scalar.parentContext,
[INCLUDES_PUBLIC_KEY]: scalar.publicKey
};
if (scalar.parentContext != null && typeof scalar.parentContext === `object`) {
Object.assign(namespaced2, scalar.parentContext);
}
return namespaced2;
}
if (row == null || typeof row !== `object`) {
return { [alias]: row };
}
const { __parentContext, ...cleanRow } = row;
const namespaced = { [alias]: cleanRow };
if (__parentContext != null) {
Object.assign(namespaced, __parentContext);
namespaced.__parentContext = __parentContext;
}
return namespaced;
}
function getRouteJoinKey(row, source, value) {
return serializeValue([
row[source]?.__correlationKey ?? row.__correlationKey,
row.__parentContext ?? row[source]?.__parentContext ?? null,
value
]);
}
function registerLazyDemandPlan(callbacks, target, initialKeys = /* @__PURE__ */ new Set()) {
const plan = {
id: `lazy-demand-${++nextLazyDemandPlanId}`,
path: target.path,
collectionId: target.collection.id,
initialKeys: new Set(initialKeys)
};
const state = callbacks[target.sourceId] ??= {};
(state.plans ??= []).push(plan);
return plan;
}
function processJoins(pipeline, joinClauses, sources, mainCollectionId, mainSource, allInputs, cache, queryMapping, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, rawQuery, onCompileSubquery, aliasToCollectionId, aliasRemapping, sourceWhereClauses, mainSourceIsParentFiltered, parentKeyStream) {
let resultPipeline = pipeline;
for (const joinClause of joinClauses) {
resultPipeline = processJoin(
resultPipeline,
joinClause,
sources,
mainCollectionId,
mainSource,
allInputs,
cache,
queryMapping,
collections,
subscriptions,
callbacks,
lazySources,
optimizableOrderByCollections,
setWindowFn,
rawQuery,
onCompileSubquery,
aliasToCollectionId,
aliasRemapping,
sourceWhereClauses,
mainSourceIsParentFiltered,
parentKeyStream
);
}
return resultPipeline;
}
function processJoin(pipeline, joinClause, sources, mainCollectionId, mainSource, allInputs, cache, queryMapping, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, rawQuery, onCompileSubquery, aliasToCollectionId, aliasRemapping, sourceWhereClauses, mainSourceIsParentFiltered, parentKeyStream) {
const isCollectionRef = joinClause.from.type === `collectionRef`;
const joinedSource = joinClause.from.alias;
const availableSources = [...Object.keys(sources), joinedSource];
const { mainExpr, joinedExpr } = analyzeJoinExpressions(
joinClause.left,
joinClause.right,
availableSources,
joinedSource,
rawQuery.from.type === `unionAll`
);
const joinedExpressionAliases = getSourceAliasesFromExpression(joinedExpr);
const joinedExpressionUsesParent = [...joinedExpressionAliases].some(
(alias) => alias !== joinedSource && !sources[alias]
);
const routeJoinedSource = parentKeyStream !== void 0 && (joinClause.from.type === `queryRef` || joinedExpressionUsesParent);
const {
alias: processedJoinedSource,
input: joinedInput,
collectionId: joinedCollectionId
} = processJoinSource(
joinClause.from,
allInputs,
collections,
subscriptions,
callbacks,
lazySources,
optimizableOrderByCollections,
setWindowFn,
cache,
queryMapping,
onCompileSubquery,
aliasToCollectionId,
aliasRemapping,
sourceWhereClauses,
routeJoinedSource ? parentKeyStream : void 0
);
if (processedJoinedSource !== joinedSource) {
throw new InvalidJoinConditionSourceMismatchError();
}
sources[joinedSource] = joinedInput;
if (isCollectionRef) {
aliasToCollectionId[joinedSource] = joinedCollectionId;
}
const mainCollection = collections[mainCollectionId];
const joinedCollection = collections[joinedCollectionId];
if (!mainCollection) {
throw new JoinCollectionNotFoundError(mainCollectionId);
}
if (!joinedCollection) {
throw new JoinCollectionNotFoundError(joinedCollectionId);
}
const sourceActivity = getActiveAndLazySources(
joinClause.type,
mainCollection,
joinedCollection,
mainSourceIsParentFiltered
);
const activeSource = routeJoinedSource ? void 0 : sourceActivity.activeSource;
const lazySource = sourceActivity.lazySource;
const compiledMainExpr = compileExpression(mainExpr);
const compiledJoinedExpr = compileExpression(joinedExpr);
let mainPipeline = pipeline.pipe(
map(([currentKey, namespacedRow]) => {
const value = normalizeValue(compiledMainExpr(namespacedRow));
const mainKey = routeJoinedSource ? getRouteJoinKey(namespacedRow, mainSource, value) : value;
return [mainKey, [currentKey, namespacedRow]];
})
);
let joinedPipeline = joinedInput.pipe(
map(([currentKey, row]) => {
const namespacedRow = wrapJoinedInputRow(joinedSource, row);
const value = normalizeValue(compiledJoinedExpr(namespacedRow));
const joinedKey = routeJoinedSource ? getRouteJoinKey(namespacedRow, joinedSource, value) : value;
return [joinedKey, [currentKey, namespacedRow]];
})
);
if (![`inner`, `left`, `right`, `full`].includes(joinClause.type)) {
throw new UnsupportedJoinTypeError(joinClause.type);
}
if (activeSource) {
const lazyFrom = activeSource === `main` ? joinClause.from : rawQuery.from;
const limitedSubquery = lazyFrom.type === `queryRef` && (lazyFrom.query.limit || lazyFrom.query.offset);
const resultUnionLazySide = lazyFrom.type === `unionAll`;
const lazySourceJoinExpr = activeSource === `main` ? joinedExpr : mainExpr;
const lazyAlias = activeSource === `main` ? joinedSource : mainSource;
const lazyTargets = resultUnionLazySide ? [] : getLazyLoadTargets(
rawQuery,
lazyFrom,
lazyAlias,
lazySourceJoinExpr,
lazySource,
aliasRemapping
);
if (!limitedSubquery && lazyTargets.length > 0) {
for (const target of lazyTargets) {
lazySources.add(target.sourceId);
}
const demandPlans = lazyTargets.map(
(target) => registerLazyDemandPlan(callbacks, target)
);
const demandWeights = /* @__PURE__ */ new Map();
const activePipeline = activeSource === `main` ? mainPipeline : joinedPipeline;
for (const target of lazyTargets) {
const fieldName = target.path[0];
if (fieldName) {
ensureIndexForField(fieldName, target.path, target.collection);
}
}
const activePipelineWithLoading = activePipeline.pipe(
tap((data) => {
for (const [[joinKey], weight] of data.getInner()) {
if (joinKey == null) continue;
const encoded = serializeValue(joinKey);
const previous = demandWeights.get(encoded);
const nextWeight = (previous?.weight ?? 0) + weight;
if (nextWeight === 0) {
demandWeights.delete(encoded);
} else {
demandWeights.set(encoded, { key: joinKey, weight: nextWeight });
}
}
const keys = new Set(
[...demandWeights.values()].filter(({ weight }) => weight > 0).map(({ key }) => key)
);
for (let index = 0; index < lazyTargets.length; index++) {
const target = lazyTargets[index];
callbacks[target.sourceId]?.setDemand?.(demandPlans[index], keys);
}
})
);
if (activeSource === `main`) {
mainPipeline = activePipelineWithLoading;
} else {
joinedPipeline = activePipelineWithLoading;
}
}
}
return mainPipeline.pipe(
join(joinedPipeline, joinClause.type),
processJoinResults(joinClause.type)
);
}
function analyzeJoinExpressions(left, right, allAvailableSourceAliases, joinedSource, allowResultFields = false) {
const availableSources = allAvailableSourceAliases.filter(
(alias) => alias !== joinedSource
);
const leftSourceAliases = getSourceAliasesFromExpression(left);
const rightSourceAliases = getSourceAliasesFromExpression(right);
const leftReferencesJoined = leftSourceAliases.has(joinedSource);
const rightReferencesJoined = rightSourceAliases.has(joinedSource);
const leftAvailableAliases = [...leftSourceAliases].filter(
(alias) => availableSources.includes(alias) || allowResultFields && alias !== joinedSource
);
const rightAvailableAliases = [...rightSourceAliases].filter(
(alias) => availableSources.includes(alias) || allowResultFields && alias !== joinedSource
);
if (leftAvailableAliases.length > 0 && !leftReferencesJoined && rightReferencesJoined && rightAvailableAliases.length === 0) {
return { mainExpr: left, joinedExpr: right };
}
if (leftReferencesJoined && leftAvailableAliases.length === 0 && rightAvailableAliases.length > 0 && !rightReferencesJoined) {
return { mainExpr: right, joinedExpr: left };
}
if (leftSourceAliases.size === 0 || rightSourceAliases.size === 0) {
throw new InvalidJoinConditionSourceMismatchError();
}
if (leftSourceAliases.size === 1 && rightSourceAliases.size === 1 && [...leftSourceAliases][0] === [...rightSourceAliases][0]) {
throw new InvalidJoinConditionSameSourceError([...leftSourceAliases][0]);
}
if (leftAvailableAliases.length === 0) {
throw new InvalidJoinConditionLeftSourceError([...leftSourceAliases][0]);
}
if (!rightReferencesJoined) {
throw new InvalidJoinConditionRightSourceError(joinedSource);
}
throw new InvalidJoinCondition();
}
function getSourceAliasesFromExpression(expr) {
switch (expr.type) {
case `ref`:
return new Set(expr.path[0] ? [expr.path[0]] : []);
case `func`: {
const sourceAliases = /* @__PURE__ */ new Set();
for (const arg of expr.args) {
for (const alias of getSourceAliasesFromExpression(arg)) {
sourceAliases.add(alias);
}
}
return sourceAliases;
}
default:
return /* @__PURE__ */ new Set();
}
}
function processJoinSource(from, allInputs, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, cache, queryMapping, onCompileSubquery, aliasToCollectionId, aliasRemapping, sourceWhereClauses, parentKeyStream) {
switch (from.type) {
case `collectionRef`: {
const input = allInputs[from.sourceId] ?? allInputs[from.alias];
if (!input) {
throw new CollectionInputNotFoundError(
from.alias,
from.collection.id,
Object.keys(allInputs)
);
}
aliasToCollectionId[from.alias] = from.collection.id;
return {
alias: from.alias,
input: parentKeyStream ? parameterizeJoinInputByParentRoutes(input, parentKeyStream) : input,
collectionId: from.collection.id
};
}
case `queryRef`: {
const originalQuery = queryMapping.get(from.query) || from.query;
const subQueryResult = onCompileSubquery(
originalQuery,
allInputs,
collections,
subscriptions,
callbacks,
lazySources,
optimizableOrderByCollections,
setWindowFn,
cache,
queryMapping,
parentKeyStream
);
Object.assign(aliasToCollectionId, subQueryResult.aliasToCollectionId);
Object.assign(aliasRemapping, subQueryResult.aliasRemapping);
const isUserDefinedSubquery = queryMapping.has(from.query);
const fromInnerAlias = getFirstFromAlias(from.query);
const isOptimizerCreated = !isUserDefinedSubquery && fromInnerAlias !== void 0 && from.alias === fromInnerAlias;
if (!isOptimizerCreated) {
for (const [alias, whereClause] of subQueryResult.sourceWhereClauses) {
sourceWhereClauses.set(alias, whereClause);
}
}
const innerAlias = Object.keys(subQueryResult.aliasToCollectionId).find(
(alias) => subQueryResult.aliasToCollectionId[alias] === subQueryResult.collectionId
);
if (innerAlias && innerAlias !== from.alias) {
aliasRemapping[from.alias] = innerAlias;
}
const subQueryInput = subQueryResult.pipeline;
const extractedInput = subQueryInput.pipe(
map((data) => {
const [
key,
[value, _orderByIndex, correlationKey, parentContext, publicKey]
] = data;
if (!parentKeyStream) {
return [key, value];
}
return [
key,
attachRouteMetadataToResult(
value,
correlationKey,
parentContext,
publicKey
)
];
})
);
return {
alias: from.alias,
input: extractedInput,
collectionId: subQueryResult.collectionId
};
}
default:
throw new UnsupportedJoinSourceTypeError(from.type);
}
}
function getFirstFromAlias(query) {
if (query.from.type === `unionFrom`) {
return query.from.sources[0]?.alias;
}
if (query.from.type === `unionAll`) {
return void 0;
}
return query.from.alias;
}
function processJoinResults(joinType) {
return function(pipeline) {
return pipeline.pipe(
// Process the join result and handle nulls
filter((result) => {
const [_key, [main, joined]] = result;
const mainNamespacedRow = main?.[1];
const joinedNamespacedRow = joined?.[1];
if (joinType === `inner`) {
return !!(mainNamespacedRow && joinedNamespacedRow);
}
if (joinType === `left`) {
return !!mainNamespacedRow;
}
if (joinType === `right`) {
return !!joinedNamespacedRow;
}
return true;
}),
map((result) => {
const [_key, [main, joined]] = result;
const mainKey = main?.[0];
const mainNamespacedRow = main?.[1];
const joinedKey = joined?.[0];
const joinedNamespacedRow = joined?.[1];
const mergedNamespacedRow = {};
if (mainNamespacedRow) {
Object.assign(mergedNamespacedRow, mainNamespacedRow);
}
if (joinedNamespacedRow) {
Object.assign(mergedNamespacedRow, joinedNamespacedRow);
}
const resultKey = `[${mainKey},${joinedKey}]`;
return [resultKey, mergedNamespacedRow];
})
);
};
}
function getActiveAndLazySources(joinType, leftCollection, rightCollection, mainSourceIsParentFiltered) {
switch (joinType) {
case `left`:
return { activeSource: `main`, lazySource: rightCollection };
case `right`:
return { activeSource: `joined`, lazySource: leftCollection };
case `inner`:
if (mainSourceIsParentFiltered) {
return { activeSource: `main`, lazySource: rightCollection };
}
return leftCollection.size < rightCollection.size ? { activeSource: `main`, lazySource: rightCollection } : { activeSource: `joined`, lazySource: leftCollection };
default:
return { activeSource: void 0, lazySource: void 0 };
}
}
export {
processJoins,
registerLazyDemandPlan
};
//# sourceMappingURL=joins.js.map