UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

669 lines (668 loc) 23 kB
import { groupBy, map, serializeValue, filter, groupByOperators } from "@tanstack/db-ivm"; import { isExpressionLike, getHavingExpression, ConditionalSelect, PropRef, Func } from "../ir.js"; import { NonAggregateExpressionNotInGroupByError, UnsupportedAggregateFunctionError, UnknownHavingExpressionTypeError, AggregateFunctionNotInSelectError } from "../../errors.js"; import { getParentContextIdentity, getEqualityValueIdentity, getParentContextValue } from "../equality-value-identity.js"; import { compileExpression, toBooleanPredicate, isCaseWhenConditionTrue } from "./evaluators.js"; import { attachRouteMetadata, stripInternalCallbackMetadata, getNamespacedRouteMetadata, INCLUDES_PUBLIC_KEY } from "./route-metadata.js"; const RAW_REPRESENTATIVE = /* @__PURE__ */ Symbol(`raw_group_representative`); function createInternalGroupFields(groupCount, selectClause) { const aliases = Object.keys(selectClause ?? {}); let prefix = `__tanstack_group_`; while (aliases.some((alias) => alias.startsWith(prefix))) prefix += `_`; return { virtual: `${prefix}virtual`, route: `${prefix}route`, correlationIdentity: `${prefix}correlation_identity`, parentContextIdentity: `${prefix}parent_context_identity`, singleGroup: `${prefix}single_group`, aggregatePrefix: `${prefix}aggregate_`, groupKeys: Array.from( { length: groupCount }, (_, i) => `${prefix}key_${i}` ), groupValues: Array.from( { length: groupCount }, (_, i) => `${prefix}value_${i}` ), groupKeyRefs: Array.from( { length: groupCount }, (_, i) => `${prefix}key_ref_${i}` ) }; } function createPublicGroupKey(values) { const identities = values.map(getEqualityValueIdentity); if (identities.length === 1) { const identity = identities[0]; if (identity == null || typeof identity !== `object` && typeof identity !== `function` && typeof identity !== `symbol`) { return identity; } } return serializeValue(identities); } function attachPublicGroupKey(row, publicKey) { const keyedRow = row; keyedRow[INCLUDES_PUBLIC_KEY] = publicKey; } function createRepresentative(rowKey, value, identity) { const representative = { key: serializeValue([rowKey, identity]) }; Object.defineProperty(representative, RAW_REPRESENTATIVE, { value }); return representative; } function getRepresentative(values) { let selected; for (const [candidate, multiplicity] of values) { if (multiplicity <= 0) continue; if (selected === void 0 || candidate.key < selected.key) { selected = candidate; } } return selected; } function unwrapRepresentative(value) { return value?.[RAW_REPRESENTATIVE]; } function addCorrelationRouteIdentityToGroupKey(key, row, mainSource, fields, valueIdentity) { const route = getNamespacedRouteMetadata(row, mainSource); key[fields.correlationIdentity] = valueIdentity.equality( route?.correlationKey ); if (route?.parentContext != null) { key[fields.parentContextIdentity] = getParentContextIdentity( route.parentContext ); } } function addCorrelationRouteAggregate(aggregates, mainSource, fields, valueIdentity) { aggregates[fields.route] = { preMap: ([rowKey, row]) => { const route = getNamespacedRouteMetadata(row, mainSource); return createRepresentative(rowKey, route, [ valueIdentity.exact(route?.correlationKey), getParentContextIdentity(route?.parentContext) ]); }, reduce: getRepresentative, postMap: unwrapRepresentative }; } function getGroupRoute(aggregatedRow, fields) { return aggregatedRow[fields.route]; } function getCorrelationRouteIdentity(aggregatedRow, fields) { return getGroupRoute(aggregatedRow, fields)?.parentContext == null ? aggregatedRow[fields.correlationIdentity] : [ aggregatedRow[fields.correlationIdentity], aggregatedRow[fields.parentContextIdentity] ]; } function getGroupEvaluationRow(row, fields, selected = row.$selected) { return { ...getParentContextValue(getGroupRoute(row, fields)?.parentContext), $selected: selected }; } function getRowVirtualMetadata(row) { let found = false; let allSynced = true; let hasLocal = false; for (const [alias, value] of Object.entries(row)) { if (alias === `$selected`) continue; if (value === null || typeof value !== `object`) continue; const asRecord = value; const hasSyncedProp = `$synced` in asRecord; const hasOriginProp = `$origin` in asRecord; if (!hasSyncedProp && !hasOriginProp) { continue; } found = true; if (asRecord.$synced === false) { allSynced = false; } if (asRecord.$origin === `local`) { hasLocal = true; } } return { synced: found ? allSynced : true, hasLocal }; } const { sum, count, avg, min, max } = groupByOperators; function validateAndCreateMapping(groupByClause, selectClause) { const selectToGroupByIndex = /* @__PURE__ */ new Map(); if (!selectClause) { return selectToGroupByIndex; } for (const [alias, expr] of Object.entries(selectClause)) { if (expr.type === `agg` || containsAggregate(expr)) { continue; } const groupIndex = groupByClause.findIndex( (groupExpr) => expressionsEqual(expr, groupExpr) ); if (groupIndex === -1) { throw new NonAggregateExpressionNotInGroupByError(alias); } selectToGroupByIndex.set(alias, groupIndex); } return selectToGroupByIndex; } function processGroupBy(pipeline, groupByClause, valueIdentity, havingClauses, selectClause, fnHavingClauses, aggregateCollectionId, mainSource, sanitizeCallbackRows = false) { const fields = createInternalGroupFields(groupByClause.length, selectClause); const virtualAggregates = { [fields.virtual]: { preMap: ([, row]) => getRowVirtualMetadata(row), reduce: (values) => { const group = { synced: true, hasLocal: false }; for (const [metadata, multiplicity] of values) { if (multiplicity <= 0) continue; if (!metadata.synced) group.synced = false; if (metadata.hasLocal) group.hasLocal = true; } return group; } } }; if (mainSource) { addCorrelationRouteAggregate( virtualAggregates, mainSource, fields, valueIdentity ); } const singleGroup = groupByClause.length === 0; const mapping = singleGroup ? void 0 : validateAndCreateMapping(groupByClause, selectClause); const compiledGroupByExpressions = groupByClause.map( (e) => compileExpression(e) ); const keyExtractor = ([, row]) => { const namespacedRow = singleGroup ? row : { ...row }; if (!singleGroup) delete namespacedRow.$selected; const key = singleGroup ? { [fields.singleGroup]: true } : {}; for (let i = 0; i < groupByClause.length; i++) { const compiledExpr = compiledGroupByExpressions[i]; const value = compiledExpr(namespacedRow); key[fields.groupKeys[i]] = valueIdentity.equality(value); } if (mainSource) { addCorrelationRouteIdentityToGroupKey( key, row, mainSource, fields, valueIdentity ); } return key; }; const aggregates = virtualAggregates; const wrappedAggExprs = {}; const aggCounter = { value: 0 }; for (let i = 0; i < compiledGroupByExpressions.length; i++) { const compiledExpr = compiledGroupByExpressions[i]; aggregates[fields.groupValues[i]] = { preMap: ([rowKey, row]) => { const value = compiledExpr(row); return createRepresentative(rowKey, value, valueIdentity.exact(value)); }, reduce: getRepresentative, postMap: unwrapRepresentative }; } if (selectClause) { for (const [alias, expr] of Object.entries(selectClause)) { if (expr.type === `agg`) { aggregates[alias] = getAggregateFunction(expr); } else if (containsAggregate(expr)) { const { transformed, extracted } = extractAndReplaceAggregates( expr, aggCounter, fields.aggregatePrefix ); for (const [syntheticAlias, aggExpr] of Object.entries(extracted)) { aggregates[syntheticAlias] = getAggregateFunction(aggExpr); } wrappedAggExprs[alias] = compileGroupedSelectValue( singleGroup ? transformed : replaceGroupByRefsInSelectValue( transformed, groupByClause, fields.groupKeyRefs ) ); } } } pipeline = pipeline.pipe(groupBy(keyExtractor, aggregates)); pipeline = pipeline.pipe( map(([, aggregatedRow]) => { const selectResults = aggregatedRow.$selected || {}; const finalResults = singleGroup ? { ...selectResults } : {}; if (selectClause) { for (const [alias, expr] of Object.entries(selectClause)) { if (expr.type === `agg`) { finalResults[alias] = aggregatedRow[alias]; } else if (!singleGroup && !wrappedAggExprs[alias]) { const groupIndex = mapping?.get(alias); if (groupIndex !== void 0) { finalResults[alias] = aggregatedRow[fields.groupValues[groupIndex]]; } else { finalResults[alias] = selectResults[alias]; } } } evaluateWrappedAggregates( finalResults, aggregatedRow, wrappedAggExprs, fields ); } else { for (let i = 0; i < groupByClause.length; i++) { finalResults[`__key_${i}`] = aggregatedRow[fields.groupValues[i]]; } } const route = mainSource ? getGroupRoute(aggregatedRow, fields) : void 0; const correlationKey = route?.correlationKey; const correlationRoute = mainSource ? getCorrelationRouteIdentity(aggregatedRow, fields) : void 0; const keyParts = []; const publicKeyParts = []; for (let i = 0; i < groupByClause.length; i++) { keyParts.push(aggregatedRow[fields.groupKeys[i]]); publicKeyParts.push(aggregatedRow[fields.groupValues[i]]); } if (correlationRoute !== void 0) { keyParts.push(correlationRoute); } const finalKey = singleGroup ? correlationRoute !== void 0 ? `single_group_${serializeValue(correlationRoute)}` : `single_group` : keyParts.length === 1 ? keyParts[0] : serializeValue(keyParts); const publicKey = singleGroup ? `single_group` : createPublicGroupKey(publicKeyParts); const resultRow = { ...aggregatedRow, $selected: finalResults }; const virtual = aggregatedRow[fields.virtual]; resultRow.$synced = virtual?.synced ?? true; resultRow.$origin = virtual?.hasLocal ? `local` : `remote`; resultRow.$key = publicKey; resultRow.$collectionId = aggregateCollectionId ?? resultRow.$collectionId; if (mainSource && correlationKey !== void 0) { attachPublicGroupKey(resultRow, publicKey); attachRouteMetadata( resultRow, correlationKey, route?.parentContext ?? null ); } return [mainSource ? finalKey : publicKey, resultRow]; }) ); if (havingClauses && havingClauses.length > 0) { for (const havingClause of havingClauses) { const havingExpression = getHavingExpression(havingClause); const transformedHavingClause = replaceAggregatesByRefs( havingExpression, selectClause || {} ); const compiledHaving = compileExpression(transformedHavingClause); pipeline = pipeline.pipe( filter(([, row]) => { const namespacedRow = getGroupEvaluationRow(row, fields); const result = compiledHaving(namespacedRow); return singleGroup ? toBooleanPredicate(result) : result; }) ); } } if (fnHavingClauses && fnHavingClauses.length > 0) { for (const fnHaving of fnHavingClauses) { pipeline = pipeline.pipe( filter(([, row]) => { const namespacedRow = getGroupEvaluationRow(row, fields); const callbackRow = sanitizeCallbackRows ? stripInternalCallbackMetadata(namespacedRow) : namespacedRow; return toBooleanPredicate(fnHaving(callbackRow)); }) ); } } return pipeline; } function expressionsEqual(expr1, expr2) { if (!expr1 || !expr2) return false; if (expr1.type !== expr2.type) return false; switch (expr1.type) { case `ref`: if (!expr1.path || !expr2.path) return false; if (expr1.path.length !== expr2.path.length) return false; return expr1.path.every( (segment, i) => segment === expr2.path[i] ); case `val`: return expr1.value === expr2.value; case `func`: return expr1.name === expr2.name && expr1.args?.length === expr2.args?.length && (expr1.args || []).every( (arg, i) => expressionsEqual(arg, expr2.args[i]) ); case `agg`: return expr1.name === expr2.name && expr1.args?.length === expr2.args?.length && (expr1.args || []).every( (arg, i) => expressionsEqual(arg, expr2.args[i]) ); default: return false; } } function getAggregateFunction(aggExpr) { const compiledExpr = compileExpression(aggExpr.args[0]); const valueExtractor = ([, namespacedRow]) => { const value = compiledExpr(namespacedRow); if (typeof value === `number`) { return value; } return value != null ? Number(value) : 0; }; const valueExtractorForMinMax = ([, namespacedRow]) => { const value = compiledExpr(namespacedRow); if (typeof value === `number` || typeof value === `string` || typeof value === `bigint` || value instanceof Date) { return value; } return value != null ? Number(value) : 0; }; const rawValueExtractor = ([, namespacedRow]) => { return compiledExpr(namespacedRow); }; switch (aggExpr.name.toLowerCase()) { case `sum`: return sum(valueExtractor); case `count`: return count(rawValueExtractor); case `avg`: return avg(valueExtractor); case `min`: return min(valueExtractorForMinMax); case `max`: return max(valueExtractorForMinMax); default: throw new UnsupportedAggregateFunctionError(aggExpr.name); } } function replaceAggregatesByRefs(havingExpr, selectClause, resultAlias = `$selected`) { switch (havingExpr.type) { case `agg`: { const aggExpr = havingExpr; for (const [alias, selectExpr] of Object.entries(selectClause)) { if (selectExpr.type === `agg` && aggregatesEqual(aggExpr, selectExpr)) { return new PropRef([resultAlias, alias]); } } throw new AggregateFunctionNotInSelectError(aggExpr.name); } case `func`: { const funcExpr = havingExpr; const transformedArgs = funcExpr.args.map( (arg) => replaceAggregatesByRefs(arg, selectClause) ); return new Func(funcExpr.name, transformedArgs); } case `ref`: return havingExpr; case `val`: return havingExpr; default: throw new UnknownHavingExpressionTypeError(havingExpr.type); } } function evaluateWrappedAggregates(finalResults, aggregatedRow, wrappedAggExprs, fields) { for (const key of Object.keys(aggregatedRow)) { if (key.startsWith(fields.aggregatePrefix)) { finalResults[key] = aggregatedRow[key]; } } for (let i = 0; i < fields.groupKeyRefs.length; i++) { finalResults[fields.groupKeyRefs[i]] = aggregatedRow[fields.groupValues[i]]; } for (const [alias, evaluator] of Object.entries(wrappedAggExprs)) { finalResults[alias] = evaluator( getGroupEvaluationRow(aggregatedRow, fields, finalResults) ); } for (const key of Object.keys(finalResults)) { if (key.startsWith(fields.aggregatePrefix) || fields.groupKeyRefs.includes(key)) { delete finalResults[key]; } } } function containsAggregate(expr) { if (isConditionalSelect(expr)) { const branchHasAggregate = expr.branches.some( (branch) => containsAggregate(branch.condition) || containsAggregate(branch.value) ); return branchHasAggregate || expr.defaultValue !== void 0 && containsAggregate(expr.defaultValue); } if (isNestedSelectObject(expr)) { return Object.values(expr).some( (value) => containsAggregate(value) ); } if (!isExpressionLike(expr)) { return false; } if (expr.type === `agg`) { return true; } if (expr.type === `func` && `args` in expr) { return expr.args.some( (arg) => containsAggregate(arg) ); } return false; } function extractAndReplaceAggregates(expr, counter, aggregatePrefix) { if (expr.type === `includesSubquery`) { return { transformed: expr, extracted: {} }; } if (expr.type === `agg`) { const alias = `${aggregatePrefix}${counter.value++}`; return { transformed: new PropRef([`$selected`, alias]), extracted: { [alias]: expr } }; } if (expr.type === `func`) { const allExtracted = {}; const newArgs = expr.args.map((arg) => { const result = extractAndReplaceAggregates(arg, counter, aggregatePrefix); Object.assign(allExtracted, result.extracted); return result.transformed; }); return { transformed: new Func(expr.name, newArgs), extracted: allExtracted }; } if (isConditionalSelect(expr)) { const allExtracted = {}; const branches = expr.branches.map((branch) => { const condition = extractAndReplaceAggregates( branch.condition, counter, aggregatePrefix ); const value = extractAndReplaceAggregates( branch.value, counter, aggregatePrefix ); Object.assign(allExtracted, condition.extracted, value.extracted); return { condition: condition.transformed, value: value.transformed }; }); const defaultValue = expr.defaultValue === void 0 ? void 0 : extractAndReplaceAggregates( expr.defaultValue, counter, aggregatePrefix ); if (defaultValue) { Object.assign(allExtracted, defaultValue.extracted); } return { transformed: new ConditionalSelect(branches, defaultValue?.transformed), extracted: allExtracted }; } if (isNestedSelectObject(expr)) { const allExtracted = {}; const transformed = {}; for (const [key, value] of Object.entries(expr)) { const result = extractAndReplaceAggregates( value, counter, aggregatePrefix ); Object.assign(allExtracted, result.extracted); transformed[key] = result.transformed; } return { transformed, extracted: allExtracted }; } return { transformed: expr, extracted: {} }; } function replaceGroupByRefsInSelectValue(value, groupByClause, groupKeyRefs) { if (isConditionalSelect(value)) { return new ConditionalSelect( value.branches.map((branch) => ({ condition: replaceGroupByRefsInExpression( branch.condition, groupByClause, groupKeyRefs ), value: replaceGroupByRefsInSelectValue( branch.value, groupByClause, groupKeyRefs ) })), value.defaultValue === void 0 ? void 0 : replaceGroupByRefsInSelectValue( value.defaultValue, groupByClause, groupKeyRefs ) ); } if (isNestedSelectObject(value)) { const transformed = {}; for (const [key, entry] of Object.entries(value)) { transformed[key] = replaceGroupByRefsInSelectValue( entry, groupByClause, groupKeyRefs ); } return transformed; } if (!isExpressionLike(value)) { return value; } if (value.type === `includesSubquery` || value.type === `agg`) { return value; } return replaceGroupByRefsInExpression(value, groupByClause, groupKeyRefs); } function replaceGroupByRefsInExpression(expr, groupByClause, groupKeyRefs) { if (expr.type === `ref`) { const groupIndex = groupByClause.findIndex( (groupExpr) => expressionsEqual(expr, groupExpr) ); return groupIndex === -1 ? expr : new PropRef([`$selected`, groupKeyRefs[groupIndex]]); } if (expr.type === `func`) { return new Func( expr.name, expr.args.map( (arg) => replaceGroupByRefsInExpression(arg, groupByClause, groupKeyRefs) ) ); } return expr; } function compileGroupedSelectValue(value) { if (isConditionalSelect(value)) { return compileGroupedConditionalSelect(value); } if (value.type === `includesSubquery`) { return () => null; } if (isNestedSelectObject(value)) { return compileGroupedSelectObject(value); } if (!isExpressionLike(value)) { return () => value; } return compileExpression(value); } function compileGroupedSelectObject(obj) { const entries = Object.entries(obj).map(([key, value]) => { if (key.startsWith(`__SPREAD_SENTINEL__`)) { const rest = key.slice(`__SPREAD_SENTINEL__`.length); const splitIndex = rest.lastIndexOf(`__`); const pathStr = splitIndex >= 0 ? rest.slice(0, splitIndex) : rest; const isRefExpr = typeof value === `object` && `type` in value && value.type === `ref`; const expression = isRefExpr ? value : new PropRef(pathStr.split(`.`)); return { key, spread: true, value: compileExpression(expression) }; } return { key, spread: false, value: compileGroupedSelectValue(value) }; }); return (row) => { const result = {}; for (const entry of entries) { const value = entry.value(row); if (entry.spread) { if (value && typeof value === `object`) { Object.assign(result, value); } } else { result[entry.key] = value; } } return result; }; } function compileGroupedConditionalSelect(conditional) { const branches = conditional.branches.map((branch) => ({ condition: compileExpression(branch.condition), value: compileGroupedSelectValue(branch.value) })); const defaultValue = conditional.defaultValue === void 0 ? void 0 : compileGroupedSelectValue(conditional.defaultValue); return (row) => { for (const branch of branches) { if (isCaseWhenConditionTrue(branch.condition(row))) { return branch.value(row); } } return defaultValue !== void 0 ? defaultValue(row) : null; }; } function isNestedSelectObject(value) { return value != null && typeof value === `object` && !Array.isArray(value) && !value.__refProxy && !isExpressionLike(value); } function isConditionalSelect(value) { return value instanceof ConditionalSelect || value != null && typeof value === `object` && value.type === `conditionalSelect`; } function aggregatesEqual(agg1, agg2) { return agg1.name === agg2.name && agg1.args.length === agg2.args.length && agg1.args.every((arg, i) => expressionsEqual(arg, agg2.args[i])); } export { containsAggregate, processGroupBy, replaceAggregatesByRefs }; //# sourceMappingURL=group-by.js.map