UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

322 lines (321 loc) 10.8 kB
import { deepEquals } from "../utils.js"; import { CannotCombineEmptyExpressionListError } from "../errors.js"; import { QueryRef, CollectionRef, Func } from "./ir.js"; import { isConvertibleToCollectionFilter } from "./compiler/expressions.js"; function optimizeQuery(query) { const collectionWhereClauses = extractCollectionWhereClauses(query); let optimized = query; let previousOptimized; let iterations = 0; const maxIterations = 10; while (iterations < maxIterations && !deepEquals(optimized, previousOptimized)) { previousOptimized = optimized; optimized = applyRecursiveOptimization(optimized); iterations++; } const cleaned = removeRedundantSubqueries(optimized); return { optimizedQuery: cleaned, collectionWhereClauses }; } function extractCollectionWhereClauses(query) { const collectionWhereClauses = /* @__PURE__ */ new Map(); if (!query.where || query.where.length === 0) { return collectionWhereClauses; } const splitWhereClauses = splitAndClauses(query.where); const analyzedClauses = splitWhereClauses.map( (clause) => analyzeWhereClause(clause) ); const groupedClauses = groupWhereClauses(analyzedClauses); for (const [sourceAlias, whereClause] of groupedClauses.singleSource) { if (isCollectionReference(query, sourceAlias)) { if (isConvertibleToCollectionFilter(whereClause)) { collectionWhereClauses.set(sourceAlias, whereClause); } } } return collectionWhereClauses; } function isCollectionReference(query, sourceAlias) { if (query.from.alias === sourceAlias) { return query.from.type === `collectionRef`; } if (query.join) { for (const joinClause of query.join) { if (joinClause.from.alias === sourceAlias) { return joinClause.from.type === `collectionRef`; } } } return false; } function applyRecursiveOptimization(query) { var _a; const subqueriesOptimized = { ...query, from: query.from.type === `queryRef` ? new QueryRef( applyRecursiveOptimization(query.from.query), query.from.alias ) : query.from, join: (_a = query.join) == null ? void 0 : _a.map((joinClause) => ({ ...joinClause, from: joinClause.from.type === `queryRef` ? new QueryRef( applyRecursiveOptimization(joinClause.from.query), joinClause.from.alias ) : joinClause.from })) }; return applySingleLevelOptimization(subqueriesOptimized); } function applySingleLevelOptimization(query) { if (!query.where || query.where.length === 0) { return query; } if (!query.join || query.join.length === 0) { return query; } const splitWhereClauses = splitAndClauses(query.where); const analyzedClauses = splitWhereClauses.map( (clause) => analyzeWhereClause(clause) ); const groupedClauses = groupWhereClauses(analyzedClauses); return applyOptimizations(query, groupedClauses); } function removeRedundantSubqueries(query) { var _a; return { ...query, from: removeRedundantFromClause(query.from), join: (_a = query.join) == null ? void 0 : _a.map((joinClause) => ({ ...joinClause, from: removeRedundantFromClause(joinClause.from) })) }; } function removeRedundantFromClause(from) { if (from.type === `collectionRef`) { return from; } const processedQuery = removeRedundantSubqueries(from.query); if (isRedundantSubquery(processedQuery)) { const innerFrom = removeRedundantFromClause(processedQuery.from); if (innerFrom.type === `collectionRef`) { return new CollectionRef(innerFrom.collection, from.alias); } else { return new QueryRef(innerFrom.query, from.alias); } } return new QueryRef(processedQuery, from.alias); } function isRedundantSubquery(query) { return (!query.where || query.where.length === 0) && !query.select && (!query.groupBy || query.groupBy.length === 0) && (!query.having || query.having.length === 0) && (!query.orderBy || query.orderBy.length === 0) && (!query.join || query.join.length === 0) && query.limit === void 0 && query.offset === void 0 && !query.fnSelect && (!query.fnWhere || query.fnWhere.length === 0) && (!query.fnHaving || query.fnHaving.length === 0); } function splitAndClauses(whereClauses) { const result = []; for (const clause of whereClauses) { if (clause.type === `func` && clause.name === `and`) { const splitArgs = splitAndClauses( clause.args ); result.push(...splitArgs); } else { result.push(clause); } } return result; } function analyzeWhereClause(clause) { const touchedSources = /* @__PURE__ */ new Set(); function collectSources(expr) { switch (expr.type) { case `ref`: if (expr.path && expr.path.length > 0) { const firstElement = expr.path[0]; if (firstElement) { touchedSources.add(firstElement); } } break; case `func`: if (expr.args) { expr.args.forEach(collectSources); } break; case `val`: break; case `agg`: if (expr.args) { expr.args.forEach(collectSources); } break; } } collectSources(clause); return { expression: clause, touchedSources }; } function groupWhereClauses(analyzedClauses) { const singleSource = /* @__PURE__ */ new Map(); const multiSource = []; for (const clause of analyzedClauses) { if (clause.touchedSources.size === 1) { const source = Array.from(clause.touchedSources)[0]; if (!singleSource.has(source)) { singleSource.set(source, []); } singleSource.get(source).push(clause.expression); } else if (clause.touchedSources.size > 1) { multiSource.push(clause.expression); } } const combinedSingleSource = /* @__PURE__ */ new Map(); for (const [source, clauses] of singleSource) { combinedSingleSource.set(source, combineWithAnd(clauses)); } const combinedMultiSource = multiSource.length > 0 ? combineWithAnd(multiSource) : void 0; return { singleSource: combinedSingleSource, multiSource: combinedMultiSource }; } function applyOptimizations(query, groupedClauses) { const actuallyOptimized = /* @__PURE__ */ new Set(); const optimizedFrom = optimizeFromWithTracking( query.from, groupedClauses.singleSource, actuallyOptimized ); const optimizedJoins = query.join ? query.join.map((joinClause) => ({ ...joinClause, from: optimizeFromWithTracking( joinClause.from, groupedClauses.singleSource, actuallyOptimized ) })) : void 0; const remainingWhereClauses = []; if (groupedClauses.multiSource) { remainingWhereClauses.push(groupedClauses.multiSource); } for (const [source, clause] of groupedClauses.singleSource) { if (!actuallyOptimized.has(source)) { remainingWhereClauses.push(clause); } } const optimizedQuery = { // Copy all non-optimized fields as-is select: query.select, groupBy: query.groupBy ? [...query.groupBy] : void 0, having: query.having ? [...query.having] : void 0, orderBy: query.orderBy ? [...query.orderBy] : void 0, limit: query.limit, offset: query.offset, fnSelect: query.fnSelect, fnWhere: query.fnWhere ? [...query.fnWhere] : void 0, fnHaving: query.fnHaving ? [...query.fnHaving] : void 0, // Use the optimized FROM and JOIN clauses from: optimizedFrom, join: optimizedJoins, // Only include WHERE clauses that weren't successfully optimized where: remainingWhereClauses.length > 0 ? remainingWhereClauses : [] }; return optimizedQuery; } function deepCopyQuery(query) { return { // Recursively copy the FROM clause from: query.from.type === `collectionRef` ? new CollectionRef(query.from.collection, query.from.alias) : new QueryRef(deepCopyQuery(query.from.query), query.from.alias), // Copy all other fields, creating new arrays where necessary select: query.select, join: query.join ? query.join.map((joinClause) => ({ type: joinClause.type, left: joinClause.left, right: joinClause.right, from: joinClause.from.type === `collectionRef` ? new CollectionRef( joinClause.from.collection, joinClause.from.alias ) : new QueryRef( deepCopyQuery(joinClause.from.query), joinClause.from.alias ) })) : void 0, where: query.where ? [...query.where] : void 0, groupBy: query.groupBy ? [...query.groupBy] : void 0, having: query.having ? [...query.having] : void 0, orderBy: query.orderBy ? [...query.orderBy] : void 0, limit: query.limit, offset: query.offset, fnSelect: query.fnSelect, fnWhere: query.fnWhere ? [...query.fnWhere] : void 0, fnHaving: query.fnHaving ? [...query.fnHaving] : void 0 }; } function optimizeFromWithTracking(from, singleSourceClauses, actuallyOptimized) { const whereClause = singleSourceClauses.get(from.alias); if (!whereClause) { if (from.type === `collectionRef`) { return new CollectionRef(from.collection, from.alias); } return new QueryRef(deepCopyQuery(from.query), from.alias); } if (from.type === `collectionRef`) { const subQuery = { from: new CollectionRef(from.collection, from.alias), where: [whereClause] }; actuallyOptimized.add(from.alias); return new QueryRef(subQuery, from.alias); } if (!isSafeToPushIntoExistingSubquery(from.query)) { return new QueryRef(deepCopyQuery(from.query), from.alias); } const existingWhere = from.query.where || []; const optimizedSubQuery = { ...deepCopyQuery(from.query), where: [...existingWhere, whereClause] }; actuallyOptimized.add(from.alias); return new QueryRef(optimizedSubQuery, from.alias); } function isSafeToPushIntoExistingSubquery(query) { if (query.select) { const hasAggregates = Object.values(query.select).some( (expr) => expr.type === `agg` ); if (hasAggregates) { return false; } } if (query.groupBy && query.groupBy.length > 0) { return false; } if (query.having && query.having.length > 0) { return false; } if (query.orderBy && query.orderBy.length > 0) { if (query.limit !== void 0 || query.offset !== void 0) { return false; } } if (query.fnSelect || query.fnWhere && query.fnWhere.length > 0 || query.fnHaving && query.fnHaving.length > 0) { return false; } return true; } function combineWithAnd(expressions) { if (expressions.length === 0) { throw new CannotCombineEmptyExpressionListError(); } if (expressions.length === 1) { return expressions[0]; } return new Func(`and`, expressions); } export { optimizeQuery }; //# sourceMappingURL=optimizer.js.map