@tanstack/db
Version:
A reactive client store for building super fast apps on sync
440 lines (439 loc) • 17 kB
JavaScript
import { map, tap, join, serializeValue } from "@tanstack/db-ivm";
import { InvalidJoinConditionSourceMismatchError, JoinCollectionNotFoundError, UnsupportedJoinTypeError, UnsupportedJoinSourceTypeError, UnsupportedFromTypeError, CollectionInputNotFoundError, InvalidJoinConditionSameSourceError, InvalidJoinConditionLeftSourceError, InvalidJoinConditionRightSourceError, InvalidJoinCondition } from "../../errors.js";
import { getParentContextIdentity } from "../equality-value-identity.js";
import { ensureIndexForField } from "../../indexes/auto-index.js";
import { validateJoinConditions } from "../join-conditions.js";
import { getFromSources, collectPropRefs, Value, requireCollectionSource, getPropRefSourceAlias } from "../ir.js";
import { compileExpression } from "./evaluators.js";
import { getLazyLoadTargets } from "./lazy-targets.js";
import { crossJoinParentRoutes } from "./parent-routes.js";
import { queriesMatchForCaching } from "./query-equivalence.js";
import { wrapInputRow, attachRouteMetadataToResult, mergeSourceContext, INCLUDES_PUBLIC_KEY, attachRouteMetadata, getNamespacedRouteMetadata } from "./route-metadata.js";
let nextLazyDemandPlanId = 0;
function parameterizeJoinInputByParentRoutes(input, parentKeyStream, valueIdentity) {
return crossJoinParentRoutes(
input,
parentKeyStream,
(rowKey, row, correlationKey, parentContext, metadata) => {
return [
serializeValue([
valueIdentity.equality(rowKey),
valueIdentity.equality(correlationKey),
getParentContextIdentity(parentContext)
]),
attachRouteMetadata(
{
...row
},
correlationKey,
parentContext
),
metadata
];
}
);
}
function getRouteJoinKey(row, source, equalityKey, valueIdentity) {
const route = getNamespacedRouteMetadata(row, source);
return serializeValue([
valueIdentity.equality(route?.correlationKey),
getParentContextIdentity(route?.parentContext ?? null),
equalityKey
]);
}
function keyJoinInput(originalKey, row, source, side, expressions, routeJoinedSource, valueIdentity) {
const value = expressions[0](row);
const input = [originalKey, row, void 0];
const unmatchedKey = side === `main` ? `\0m` : `\0j`;
if (value == null) return [unmatchedKey, input];
let key = valueIdentity.equality(value);
if (expressions.length > 1) {
const values = [key];
for (let index = 1; index < expressions.length; index++) {
const component = expressions[index](row);
if (component == null) return [unmatchedKey, input];
values.push(valueIdentity.equality(component));
}
key = values;
}
input[2] = value;
return [
routeJoinedSource ? getRouteJoinKey(row, source, key, valueIdentity) : serializeValue(key),
input
];
}
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, queryMapping, collections, callbacks, lazySources, rawQuery, localFrom, onCompileSubquery, aliasToCollectionId, aliasRemapping, sourceWhereClauses, mainSourceIsParentFiltered, valueIdentity, sourceIncludes, parentKeyStream) {
const localBindings = new Set(
getFromSources(localFrom).map((source) => source.bindingId)
);
function processJoin(inputPipeline, joinClause) {
const joinedSource = joinClause.from.alias;
const mainAliases = Object.keys(sources);
const role = (ref) => {
const alias = getJoinRefAlias(ref);
if (ref.bindingId !== void 0) {
if (ref.bindingId === joinClause.from.bindingId) return `joined`;
return localBindings.has(ref.bindingId) ? `main` : `ancestor`;
}
if (alias === joinedSource) return `joined`;
if (alias && (mainAliases.includes(alias) || rawQuery.from.type === `unionAll`))
return `main`;
return `unknown`;
};
const conditions = validateJoinConditions(joinClause.on).map(
([left, right]) => analyzeJoinExpressions(left, right, joinedSource, role)
);
const { mainExpr, joinedExpr } = conditions[0];
const joinedExpressionUsesParent = conditions.some(
({ joinedExpr: expression }) => collectPropRefs(expression).some((ref) => {
const sourceRole = role(ref);
return sourceRole === `ancestor` || sourceRole === `unknown`;
})
);
const routeJoinedSource = parentKeyStream !== void 0 && (joinClause.from.type === `queryRef` || (joinClause.type === `right` || joinClause.type === `full`) && !mainSourceIsParentFiltered || joinedExpressionUsesParent);
const {
alias: processedJoinedSource,
input: joinedInput,
collectionId: joinedCollectionId,
includes
} = processSource(
joinClause.from,
allInputs,
queryMapping,
onCompileSubquery,
aliasToCollectionId,
aliasRemapping,
sourceWhereClauses,
valueIdentity,
`join`,
routeJoinedSource ? parentKeyStream : void 0
);
if (processedJoinedSource !== joinedSource) {
throw new InvalidJoinConditionSourceMismatchError();
}
sources[joinedSource] = joinedInput;
if (includes)
sourceIncludes.push(
...includes.map((include) => ({ sourceAlias: joinedSource, include }))
);
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 compiledMainExpressions = conditions.map(
(condition) => compileExpression(condition.mainExpr)
);
const compiledJoinedExpressions = conditions.map(
(condition) => compileExpression(condition.joinedExpr)
);
let mainPipeline = inputPipeline.pipe(
map(
([currentKey, namespacedRow]) => keyJoinInput(
currentKey,
namespacedRow,
mainSource,
`main`,
compiledMainExpressions,
routeJoinedSource,
valueIdentity
)
)
);
let joinedPipeline = joinedInput.pipe(
map(
([currentKey, row, metadata]) => keyJoinInput(
currentKey,
wrapInputRow(
joinedSource,
row,
metadata,
routeJoinedSource || joinClause.from.type === `queryRef`
),
joinedSource,
`joined`,
compiledJoinedExpressions,
routeJoinedSource,
valueIdentity
)
)
);
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 !== void 0 || lazyFrom.query.offset !== void 0 || lazyFrom.query.singleResult);
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, [, , joinValue]],
weight
] of data.getInner()) {
if (joinValue == null) continue;
const previous = demandWeights.get(joinKey);
const nextWeight = (previous?.weight ?? 0) + weight;
if (nextWeight === 0) {
demandWeights.delete(joinKey);
} else {
demandWeights.set(joinKey, {
key: previous?.key ?? joinValue,
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(
joinedSource,
mainAliases,
parentKeyStream !== void 0
)
);
}
for (const joinClause of joinClauses) {
pipeline = processJoin(pipeline, joinClause);
localBindings.add(joinClause.from.bindingId);
}
return pipeline;
}
function getJoinRefAlias(ref) {
return getPropRefSourceAlias(ref) ?? ref.path[0];
}
function analyzeJoinExpressions(left, right, joinedSource, role) {
const leftRefs = collectPropRefs(left);
const rightRefs = collectPropRefs(right);
const leftRoles = new Set(leftRefs.map(role));
const rightRoles = new Set(rightRefs.map(role));
const leftMain = leftRoles.has(`main`) || leftRoles.has(`ancestor`);
const rightMain = rightRoles.has(`main`) || rightRoles.has(`ancestor`);
const leftJoined = leftRoles.has(`joined`);
const rightJoined = rightRoles.has(`joined`);
if (leftMain && !leftJoined && rightJoined && !rightRoles.has(`main`)) {
return { mainExpr: left, joinedExpr: right };
}
if (leftJoined && !leftRoles.has(`main`) && rightMain && !rightJoined) {
return { mainExpr: right, joinedExpr: left };
}
if (leftRefs.length === 0 || rightRefs.length === 0) {
throw new InvalidJoinConditionSourceMismatchError();
}
if (leftRefs.length === 1 && rightRefs.length === 1 && (leftRefs[0].bindingId !== void 0 && rightRefs[0].bindingId !== void 0 ? leftRefs[0].bindingId === rightRefs[0].bindingId : getJoinRefAlias(leftRefs[0]) === getJoinRefAlias(rightRefs[0]))) {
throw new InvalidJoinConditionSameSourceError(
getJoinRefAlias(leftRefs[0])
);
}
if (!leftMain) {
throw new InvalidJoinConditionLeftSourceError(
getJoinRefAlias(leftRefs[0])
);
}
if (!rightJoined) {
throw new InvalidJoinConditionRightSourceError(joinedSource);
}
throw new InvalidJoinCondition();
}
function processSource(from, allInputs, queryMapping, onCompileSubquery, aliasToCollectionId, aliasRemapping, sourceWhereClauses, valueIdentity, sourceKind, parentKeyStream) {
switch (from.type) {
case `descriptorRef`:
return requireCollectionSource(from);
case `collectionRef`: {
const input = allInputs[from.sourceId];
if (!input) {
throw new CollectionInputNotFoundError(
from.alias,
from.collection.id,
Object.keys(allInputs)
);
}
aliasToCollectionId[from.alias] = from.collection.id;
return {
alias: from.alias,
input: sourceKind === `join` && parentKeyStream ? parameterizeJoinInputByParentRoutes(
input,
parentKeyStream,
valueIdentity
) : input,
collectionId: from.collection.id
};
}
case `queryRef`: {
const originalQuery = queryMapping.get(from.query);
const queryToCompile = originalQuery && queriesMatchForCaching(from.query, originalQuery) ? originalQuery : from.query;
const subQueryResult = onCompileSubquery(queryToCompile, parentKeyStream);
Object.assign(aliasToCollectionId, subQueryResult.aliasToCollectionId);
Object.assign(aliasRemapping, subQueryResult.aliasRemapping);
if (queryMapping.has(from.query) || from.alias !== getFromSources(from.query.from)[0]?.alias) {
for (const [alias, where] of subQueryResult.sourceWhereClauses)
sourceWhereClauses.set(alias, where);
}
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,
metadata
]
] = data;
if (sourceKind === `join` && !parentKeyStream) {
return [key, value, metadata];
}
return [
key,
attachRouteMetadataToResult(
sourceKind === `from` && value instanceof Value ? value.value : value,
correlationKey,
parentContext,
publicKey
),
metadata
];
})
);
return {
alias: from.alias,
input: extractedInput,
collectionId: subQueryResult.collectionId,
includes: subQueryResult.includes
};
}
default:
throw sourceKind === `join` ? new UnsupportedJoinSourceTypeError(from.type) : new UnsupportedFromTypeError(from.type);
}
}
function encodeNonFiniteKey(_, value) {
return typeof value === `number` && !Number.isFinite(value) ? { number: String(value) } : value;
}
function processJoinResults(joinedSource, mainAliases, parentRouted) {
return (pipeline) => pipeline.pipe(
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 = {
...mainNamespacedRow ?? joinedNamespacedRow
};
if (main == null)
for (const alias of mainAliases) delete mergedNamespacedRow[alias];
if (joinedNamespacedRow)
mergedNamespacedRow[joinedSource] = joinedNamespacedRow[joinedSource];
else delete mergedNamespacedRow[joinedSource];
mergeSourceContext(
mergedNamespacedRow,
mainNamespacedRow,
joinedNamespacedRow
);
const resultKey = JSON.stringify(
[mainKey ?? null, joinedKey ?? null],
encodeNonFiniteKey
);
if (parentRouted)
mergedNamespacedRow[INCLUDES_PUBLIC_KEY] = JSON.stringify(
[
mainNamespacedRow?.[INCLUDES_PUBLIC_KEY] ?? mainKey ?? null,
joinedNamespacedRow?.[INCLUDES_PUBLIC_KEY] ?? joinedKey ?? null
],
encodeNonFiniteKey
);
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,
processSource,
registerLazyDemandPlan
};
//# sourceMappingURL=joins.js.map