@tanstack/db
Version:
A reactive client store for building super fast apps on sync
322 lines (321 loc) • 10.8 kB
JavaScript
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