UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

438 lines (437 loc) 14.7 kB
"use strict"; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const utils = require("../utils.cjs"); const errors = require("../errors.cjs"); const ir = require("./ir.cjs"); function optimizeQuery(query) { const sourceWhereClauses = extractSourceWhereClauses(query); let optimized = query; let previousOptimized; let iterations = 0; const maxIterations = 10; while (iterations < maxIterations && !utils.deepEquals(optimized, previousOptimized)) { previousOptimized = optimized; optimized = applyRecursiveOptimization(optimized); iterations++; } const cleaned = removeRedundantSubqueries(optimized); return { optimizedQuery: cleaned, sourceWhereClauses }; } function extractSourceWhereClauses(query) { const sourceWhereClauses = /* @__PURE__ */ new Map(); if (!query.where || query.where.length === 0) { return sourceWhereClauses; } 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)) { sourceWhereClauses.set(sourceAlias, whereClause); } } return sourceWhereClauses; } 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) { const subqueriesOptimized = { ...query, from: query.from.type === `queryRef` ? new ir.QueryRef( applyRecursiveOptimization(query.from.query), query.from.alias ) : query.from, join: query.join?.map((joinClause) => ({ ...joinClause, from: joinClause.from.type === `queryRef` ? new ir.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) { if (query.where.length > 1) { const splitWhereClauses2 = splitAndClauses(query.where); const combinedWhere = combineWithAnd(splitWhereClauses2); return { ...query, where: [combinedWhere] }; } return query; } const nonResidualWhereClauses = query.where.filter( (where) => !ir.isResidualWhere(where) ); const splitWhereClauses = splitAndClauses(nonResidualWhereClauses); const analyzedClauses = splitWhereClauses.map( (clause) => analyzeWhereClause(clause) ); const groupedClauses = groupWhereClauses(analyzedClauses); const optimizedQuery = applyOptimizations(query, groupedClauses); const residualWhereClauses = query.where.filter( (where) => ir.isResidualWhere(where) ); if (residualWhereClauses.length > 0) { optimizedQuery.where = [ ...optimizedQuery.where || [], ...residualWhereClauses ]; } return optimizedQuery; } function removeRedundantSubqueries(query) { return { ...query, from: removeRedundantFromClause(query.from), join: query.join?.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 ir.CollectionRef(innerFrom.collection, from.alias); } else { return new ir.QueryRef(innerFrom.query, from.alias); } } return new ir.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 whereClause of whereClauses) { const clause = ir.getWhereExpression(whereClause); result.push(...splitAndClausesRecursive(clause)); } return result; } function splitAndClausesRecursive(clause) { if (clause.type === `func` && clause.name === `and`) { const result = []; for (const arg of clause.args) { result.push(...splitAndClausesRecursive(arg)); } return result; } else { return [clause]; } } function analyzeWhereClause(clause) { const touchedSources = /* @__PURE__ */ new Set(); let hasNamespaceOnlyRef = false; 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); if (expr.path.length === 1) { hasNamespaceOnlyRef = true; } } } 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, hasNamespaceOnlyRef }; } function groupWhereClauses(analyzedClauses) { const singleSource = /* @__PURE__ */ new Map(); const multiSource = []; for (const clause of analyzedClauses) { if (clause.touchedSources.size === 1 && !clause.hasNamespaceOnlyRef) { 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 || clause.hasNamespaceOnlyRef) { 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); } const hasOuterJoins = query.join && query.join.some( (join) => join.type === `left` || join.type === `right` || join.type === `full` ); for (const [source, clause] of groupedClauses.singleSource) { if (!actuallyOptimized.has(source)) { remainingWhereClauses.push(clause); } else if (hasOuterJoins) { remainingWhereClauses.push(ir.createResidualWhere(clause)); } } const finalWhere = remainingWhereClauses.length > 1 ? [ combineWithAnd( remainingWhereClauses.flatMap( (clause) => splitAndClausesRecursive(ir.getWhereExpression(clause)) ) ) ] : remainingWhereClauses; 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, distinct: query.distinct, 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, // Include combined WHERE clauses where: finalWhere.length > 0 ? finalWhere : [] }; return optimizedQuery; } function deepCopyQuery(query) { return { // Recursively copy the FROM clause from: query.from.type === `collectionRef` ? new ir.CollectionRef(query.from.collection, query.from.alias) : new ir.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 ir.CollectionRef( joinClause.from.collection, joinClause.from.alias ) : new ir.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 ir.CollectionRef(from.collection, from.alias); } return new ir.QueryRef(deepCopyQuery(from.query), from.alias); } if (from.type === `collectionRef`) { const subQuery = { from: new ir.CollectionRef(from.collection, from.alias), where: [whereClause] }; actuallyOptimized.add(from.alias); return new ir.QueryRef(subQuery, from.alias); } if (!isSafeToPushIntoExistingSubquery(from.query, whereClause, from.alias)) { return new ir.QueryRef(deepCopyQuery(from.query), from.alias); } if (referencesAliasWithRemappedSelect(from.query, whereClause, from.alias)) { return new ir.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 ir.QueryRef(optimizedSubQuery, from.alias); } function unsafeSelect(query, whereClause, outerAlias) { if (!query.select) return false; return selectHasAggregates(query.select) || whereReferencesComputedSelectFields(query.select, whereClause, outerAlias); } function unsafeGroupBy(query) { return query.groupBy && query.groupBy.length > 0; } function unsafeHaving(query) { return query.having && query.having.length > 0; } function unsafeOrderBy(query) { return query.orderBy && query.orderBy.length > 0 && (query.limit !== void 0 || query.offset !== void 0); } function unsafeFnSelect(query) { return query.fnSelect || query.fnWhere && query.fnWhere.length > 0 || query.fnHaving && query.fnHaving.length > 0; } function isSafeToPushIntoExistingSubquery(query, whereClause, outerAlias) { return !(unsafeSelect(query, whereClause, outerAlias) || unsafeGroupBy(query) || unsafeHaving(query) || unsafeOrderBy(query) || unsafeFnSelect(query)); } function selectHasAggregates(select) { for (const value of Object.values(select)) { if (typeof value === `object`) { const v = value; if (v.type === `agg`) return true; if (!(`type` in v)) { if (selectHasAggregates(v)) return true; } } } return false; } function collectRefs(expr) { const refs = []; if (expr == null || typeof expr !== `object`) return refs; switch (expr.type) { case `ref`: refs.push(expr); break; case `func`: case `agg`: for (const arg of expr.args ?? []) { refs.push(...collectRefs(arg)); } break; } return refs; } function whereReferencesComputedSelectFields(select, whereClause, outerAlias) { const computed = /* @__PURE__ */ new Set(); for (const [key, value] of Object.entries(select)) { if (key.startsWith(`__SPREAD_SENTINEL__`)) continue; if (value instanceof ir.PropRef) continue; computed.add(key); } const refs = collectRefs(whereClause); for (const ref of refs) { const path = ref.path; if (!Array.isArray(path) || path.length < 2) continue; const alias = path[0]; const field = path[1]; if (alias !== outerAlias) continue; if (computed.has(field)) return true; } return false; } function referencesAliasWithRemappedSelect(subquery, whereClause, outerAlias) { const refs = collectRefs(whereClause); if (refs.every((ref) => ref.path[0] !== outerAlias)) { return false; } if (subquery.fnSelect) { return true; } const select = subquery.select; if (!select) { return false; } for (const ref of refs) { const path = ref.path; if (path.length < 2) continue; if (path[0] !== outerAlias) continue; const projected = select[path[1]]; if (!projected) continue; if (!(projected instanceof ir.PropRef)) { return true; } if (projected.path.length < 2) { return true; } const [innerAlias, innerField] = projected.path; if (innerAlias !== outerAlias && innerAlias !== subquery.from.alias) { return true; } if (innerField !== path[1]) { return true; } } return false; } function combineWithAnd(expressions) { if (expressions.length === 0) { throw new errors.CannotCombineEmptyExpressionListError(); } if (expressions.length === 1) { return expressions[0]; } return new ir.Func(`and`, expressions); } exports.optimizeQuery = optimizeQuery; //# sourceMappingURL=optimizer.cjs.map