UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

486 lines (485 loc) • 18.1 kB
"use strict"; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const dbIvm = require("@tanstack/db-ivm"); const errors = require("../../errors.cjs"); const equalityValueIdentity = require("../equality-value-identity.cjs"); const autoIndex = require("../../indexes/auto-index.cjs"); const joinConditions = require("../join-conditions.cjs"); const ir = require("../ir.cjs"); const evaluators = require("./evaluators.cjs"); const lazyTargets = require("./lazy-targets.cjs"); const parentRoutes = require("./parent-routes.cjs"); const queryEquivalence = require("./query-equivalence.cjs"); const routeMetadata = require("./route-metadata.cjs"); let nextLazyDemandPlanId = 0; function parameterizeJoinInputByParentRoutes(input, parentKeyStream, valueIdentity) { return parentRoutes.crossJoinParentRoutes( input, parentKeyStream, (rowKey, row, correlationKey, parentContext) => { return [ dbIvm.serializeValue([ valueIdentity.equality(rowKey), valueIdentity.equality(correlationKey), equalityValueIdentity.getParentContextIdentity(parentContext) ]), routeMetadata.attachRouteMetadata( { ...row }, correlationKey, parentContext ) ]; } ); } function wrapJoinedInputRow(alias, row) { const scalar = routeMetadata.getRoutedScalarMetadata(row); if (scalar) { const namespaced2 = routeMetadata.attachRouteMetadata( { ...equalityValueIdentity.getParentContextValue(scalar.parentContext), [alias]: scalar.value, [routeMetadata.INCLUDES_PUBLIC_KEY]: scalar.publicKey }, scalar.correlationKey, scalar.parentContext ); return namespaced2; } if (row == null || typeof row !== `object`) { return { [alias]: row }; } const route = routeMetadata.getRouteMetadata(row); const cleanRow = route ? routeMetadata.stripRouteMetadata(row) : row; const namespaced = { ...equalityValueIdentity.getParentContextValue(route?.parentContext), [alias]: cleanRow }; if (route) { routeMetadata.attachRouteMetadata(namespaced, route.correlationKey, route.parentContext); } return namespaced; } function getRouteJoinKey(row, source, equalityKey, valueIdentity) { const route = routeMetadata.getNamespacedRouteMetadata(row, source); return dbIvm.serializeValue([ valueIdentity.equality(route?.correlationKey), equalityValueIdentity.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) : dbIvm.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, cache, queryMapping, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, rawQuery, localFrom, onCompileSubquery, aliasToCollectionId, aliasRemapping, sourceWhereClauses, mainSourceIsParentFiltered, valueIdentity, parentKeyStream) { let resultPipeline = pipeline; const localBindings = new Set( ir.getFromSources(localFrom).map((source) => source.bindingId) ); 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, valueIdentity, localBindings, parentKeyStream ); localBindings.add(joinClause.from.bindingId); } return resultPipeline; } function processJoin(pipeline, joinClause, sources, mainCollectionId, mainSource, allInputs, cache, queryMapping, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, rawQuery, onCompileSubquery, aliasToCollectionId, aliasRemapping, sourceWhereClauses, mainSourceIsParentFiltered, valueIdentity, localBindings, parentKeyStream) { const isCollectionRef = joinClause.from.type === `collectionRef`; 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 = joinConditions.validateJoinConditions(joinClause.on).map( ([left, right]) => analyzeJoinExpressions(left, right, joinedSource, role) ); const { mainExpr, joinedExpr } = conditions[0]; const joinedExpressionUsesParent = conditions.some( ({ joinedExpr: expression }) => ir.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 } = processJoinSource( joinClause.from, allInputs, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, cache, queryMapping, onCompileSubquery, aliasToCollectionId, aliasRemapping, sourceWhereClauses, valueIdentity, routeJoinedSource ? parentKeyStream : void 0 ); if (processedJoinedSource !== joinedSource) { throw new errors.InvalidJoinConditionSourceMismatchError(); } sources[joinedSource] = joinedInput; if (isCollectionRef) { aliasToCollectionId[joinedSource] = joinedCollectionId; } const mainCollection = collections[mainCollectionId]; const joinedCollection = collections[joinedCollectionId]; if (!mainCollection) { throw new errors.JoinCollectionNotFoundError(mainCollectionId); } if (!joinedCollection) { throw new errors.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) => evaluators.compileExpression(condition.mainExpr) ); const compiledJoinedExpressions = conditions.map( (condition) => evaluators.compileExpression(condition.joinedExpr) ); let mainPipeline = pipeline.pipe( dbIvm.map( ([currentKey, namespacedRow]) => keyJoinInput( currentKey, namespacedRow, mainSource, `main`, compiledMainExpressions, routeJoinedSource, valueIdentity ) ) ); let joinedPipeline = joinedInput.pipe( dbIvm.map( ([currentKey, row]) => keyJoinInput( currentKey, wrapJoinedInputRow(joinedSource, row), joinedSource, `joined`, compiledJoinedExpressions, routeJoinedSource, valueIdentity ) ) ); if (![`inner`, `left`, `right`, `full`].includes(joinClause.type)) { throw new errors.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$1 = resultUnionLazySide ? [] : lazyTargets.getLazyLoadTargets( rawQuery, lazyFrom, lazyAlias, lazySourceJoinExpr, lazySource, aliasRemapping ); if (!limitedSubquery && lazyTargets$1.length > 0) { for (const target of lazyTargets$1) { lazySources.add(target.sourceId); } const demandPlans = lazyTargets$1.map( (target) => registerLazyDemandPlan(callbacks, target) ); const demandWeights = /* @__PURE__ */ new Map(); const activePipeline = activeSource === `main` ? mainPipeline : joinedPipeline; for (const target of lazyTargets$1) { const fieldName = target.path[0]; if (fieldName) { autoIndex.ensureIndexForField(fieldName, target.path, target.collection); } } const activePipelineWithLoading = activePipeline.pipe( dbIvm.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$1.length; index++) { const target = lazyTargets$1[index]; callbacks[target.sourceId]?.setDemand?.(demandPlans[index], keys); } }) ); if (activeSource === `main`) { mainPipeline = activePipelineWithLoading; } else { joinedPipeline = activePipelineWithLoading; } } } return mainPipeline.pipe( dbIvm.join(joinedPipeline, joinClause.type), processJoinResults(joinedSource, mainAliases) ); } function getJoinRefAlias(ref) { return ir.getPropRefSourceAlias(ref) ?? ref.path[0]; } function analyzeJoinExpressions(left, right, joinedSource, role) { const leftRefs = ir.collectPropRefs(left); const rightRefs = ir.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 errors.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 errors.InvalidJoinConditionSameSourceError( getJoinRefAlias(leftRefs[0]) ); } if (!leftMain) { throw new errors.InvalidJoinConditionLeftSourceError( getJoinRefAlias(leftRefs[0]) ); } if (!rightJoined) { throw new errors.InvalidJoinConditionRightSourceError(joinedSource); } throw new errors.InvalidJoinCondition(); } function processJoinSource(from, allInputs, collections, subscriptions, callbacks, lazySources, optimizableOrderByCollections, setWindowFn, cache, queryMapping, onCompileSubquery, aliasToCollectionId, aliasRemapping, sourceWhereClauses, valueIdentity, parentKeyStream) { switch (from.type) { case `descriptorRef`: return ir.requireCollectionSource(from); case `collectionRef`: { const input = allInputs[from.sourceId]; if (!input) { throw new errors.CollectionInputNotFoundError( from.alias, from.collection.id, Object.keys(allInputs) ); } aliasToCollectionId[from.alias] = from.collection.id; return { alias: from.alias, input: parentKeyStream ? parameterizeJoinInputByParentRoutes( input, parentKeyStream, valueIdentity ) : input, collectionId: from.collection.id }; } case `queryRef`: { const originalQuery = queryMapping.get(from.query); const queryToCompile = originalQuery && queryEquivalence.queriesMatchForCaching(from.query, originalQuery) ? originalQuery : from.query; const subQueryResult = onCompileSubquery( queryToCompile, 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( dbIvm.map((data) => { const [ key, [value, _orderByIndex, correlationKey, parentContext, publicKey] ] = data; if (!parentKeyStream) { return [key, value]; } return [ key, routeMetadata.attachRouteMetadataToResult( value, correlationKey, parentContext, publicKey ) ]; }) ); return { alias: from.alias, input: extractedInput, collectionId: subQueryResult.collectionId }; } default: throw new errors.UnsupportedJoinSourceTypeError(from.type); } } function encodeNonFiniteKey(_, value) { return typeof value === `number` && !Number.isFinite(value) ? { number: String(value) } : value; } function getFirstFromAlias(query) { return ir.getFromSources(query.from)[0]?.alias; } function processJoinResults(joinedSource, mainAliases) { return (pipeline) => pipeline.pipe( dbIvm.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 = Object.assign( {}, mainNamespacedRow ?? joinedNamespacedRow ); if (main == null) { for (const alias of mainAliases) delete mergedNamespacedRow[alias]; } if (joinedNamespacedRow) { mergedNamespacedRow[joinedSource] = joinedNamespacedRow[joinedSource]; } else { delete mergedNamespacedRow[joinedSource]; } const resultKey = JSON.stringify( [mainKey ?? null, 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 }; } } exports.processJoins = processJoins; exports.registerLazyDemandPlan = registerLazyDemandPlan; //# sourceMappingURL=joins.cjs.map