@tanstack/db 0.0.23 → 0.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/proxy.cjs +21 -0
- package/dist/cjs/proxy.cjs.map +1 -1
- package/dist/cjs/query/builder/index.cjs +72 -0
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/builder/index.d.cts +64 -0
- package/dist/cjs/query/compiler/index.cjs +44 -8
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.d.cts +4 -7
- package/dist/cjs/query/compiler/joins.cjs +14 -6
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/compiler/joins.d.cts +4 -8
- package/dist/cjs/query/compiler/types.d.cts +10 -0
- package/dist/cjs/query/optimizer.cjs +283 -0
- package/dist/cjs/query/optimizer.cjs.map +1 -0
- package/dist/cjs/query/optimizer.d.cts +42 -0
- package/dist/cjs/utils.cjs +42 -0
- package/dist/cjs/utils.cjs.map +1 -0
- package/dist/cjs/utils.d.cts +18 -0
- package/dist/esm/proxy.js +21 -0
- package/dist/esm/proxy.js.map +1 -1
- package/dist/esm/query/builder/index.d.ts +64 -0
- package/dist/esm/query/builder/index.js +72 -0
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/compiler/index.d.ts +4 -7
- package/dist/esm/query/compiler/index.js +44 -8
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/joins.d.ts +4 -8
- package/dist/esm/query/compiler/joins.js +14 -6
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/compiler/types.d.ts +10 -0
- package/dist/esm/query/optimizer.d.ts +42 -0
- package/dist/esm/query/optimizer.js +283 -0
- package/dist/esm/query/optimizer.js.map +1 -0
- package/dist/esm/utils.d.ts +18 -0
- package/dist/esm/utils.js +42 -0
- package/dist/esm/utils.js.map +1 -0
- package/package.json +1 -1
- package/src/proxy.ts +24 -0
- package/src/query/builder/index.ts +104 -0
- package/src/query/compiler/index.ts +85 -18
- package/src/query/compiler/joins.ts +21 -13
- package/src/query/compiler/types.ts +12 -0
- package/src/query/optimizer.ts +738 -0
- package/src/utils.ts +86 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":["../../../../src/query/compiler/index.ts"],"sourcesContent":["import { distinct, filter, map } from \"@electric-sql/d2mini\"\nimport { compileExpression } from \"./evaluators.js\"\nimport { processJoins } from \"./joins.js\"\nimport { processGroupBy } from \"./group-by.js\"\nimport { processOrderBy } from \"./order-by.js\"\nimport { processSelectToResults } from \"./select.js\"\nimport type { CollectionRef, QueryIR, QueryRef } from \"../ir.js\"\nimport type {\n KeyedStream,\n NamespacedAndKeyedStream,\n ResultStream,\n} from \"../../types.js\"\n\n/**\n * Cache for compiled subqueries to avoid duplicate compilation\n */\ntype QueryCache = WeakMap<QueryIR, ResultStream>\n\n/**\n * Compiles a query2 IR into a D2 pipeline\n * @param query The query IR to compile\n * @param inputs Mapping of collection names to input streams\n * @param cache Optional cache for compiled subqueries (used internally for recursion)\n * @returns A stream builder representing the compiled query\n */\nexport function compileQuery(\n query: QueryIR,\n inputs: Record<string, KeyedStream>,\n cache: QueryCache = new WeakMap()\n): ResultStream {\n // Check if this query has already been compiled\n const cachedResult = cache.get(query)\n if (cachedResult) {\n return cachedResult\n }\n\n // Create a copy of the inputs map to avoid modifying the original\n const allInputs = { ...inputs }\n\n // Create a map of table aliases to inputs\n const tables: Record<string, KeyedStream> = {}\n\n // Process the FROM clause to get the main table\n const { alias: mainTableAlias, input: mainInput } = processFrom(\n query.from,\n allInputs,\n cache\n )\n tables[mainTableAlias] = mainInput\n\n // Prepare the initial pipeline with the main table wrapped in its alias\n let pipeline: NamespacedAndKeyedStream = mainInput.pipe(\n map(([key, row]) => {\n // Initialize the record with a nested structure\n const ret = [key, { [mainTableAlias]: row }] as [\n string,\n Record<string, typeof row>,\n ]\n return ret\n })\n )\n\n // Process JOIN clauses if they exist\n if (query.join && query.join.length > 0) {\n pipeline = processJoins(\n pipeline,\n query.join,\n tables,\n mainTableAlias,\n allInputs,\n cache\n )\n }\n\n // Process the WHERE clause if it exists\n if (query.where && query.where.length > 0) {\n // Compile all WHERE expressions\n const compiledWheres = query.where.map((where) => compileExpression(where))\n\n // Apply each WHERE condition as a filter (they are ANDed together)\n for (const compiledWhere of compiledWheres) {\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return compiledWhere(namespacedRow)\n })\n )\n }\n }\n\n // Process functional WHERE clauses if they exist\n if (query.fnWhere && query.fnWhere.length > 0) {\n for (const fnWhere of query.fnWhere) {\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return fnWhere(namespacedRow)\n })\n )\n }\n }\n\n if (query.distinct && !query.fnSelect && !query.select) {\n throw new Error(`DISTINCT requires a SELECT clause.`)\n }\n\n // Process the SELECT clause early - always create __select_results\n // This eliminates duplication and allows for DISTINCT implementation\n if (query.fnSelect) {\n // Handle functional select - apply the function to transform the row\n pipeline = pipeline.pipe(\n map(([key, namespacedRow]) => {\n const selectResults = query.fnSelect!(namespacedRow)\n return [\n key,\n {\n ...namespacedRow,\n __select_results: selectResults,\n },\n ] as [string, typeof namespacedRow & { __select_results: any }]\n })\n )\n } else if (query.select) {\n pipeline = processSelectToResults(pipeline, query.select, allInputs)\n } else {\n // If no SELECT clause, create __select_results with the main table data\n pipeline = pipeline.pipe(\n map(([key, namespacedRow]) => {\n const selectResults =\n !query.join && !query.groupBy\n ? namespacedRow[mainTableAlias]\n : namespacedRow\n\n return [\n key,\n {\n ...namespacedRow,\n __select_results: selectResults,\n },\n ] as [string, typeof namespacedRow & { __select_results: any }]\n })\n )\n }\n\n // Process the GROUP BY clause if it exists\n if (query.groupBy && query.groupBy.length > 0) {\n pipeline = processGroupBy(\n pipeline,\n query.groupBy,\n query.having,\n query.select,\n query.fnHaving\n )\n } else if (query.select) {\n // Check if SELECT contains aggregates but no GROUP BY (implicit single-group aggregation)\n const hasAggregates = Object.values(query.select).some(\n (expr) => expr.type === `agg`\n )\n if (hasAggregates) {\n // Handle implicit single-group aggregation\n pipeline = processGroupBy(\n pipeline,\n [], // Empty group by means single group\n query.having,\n query.select,\n query.fnHaving\n )\n }\n }\n\n // Process the HAVING clause if it exists (only applies after GROUP BY)\n if (query.having && (!query.groupBy || query.groupBy.length === 0)) {\n // Check if we have aggregates in SELECT that would trigger implicit grouping\n const hasAggregates = query.select\n ? Object.values(query.select).some((expr) => expr.type === `agg`)\n : false\n\n if (!hasAggregates) {\n throw new Error(`HAVING clause requires GROUP BY clause`)\n }\n }\n\n // Process functional HAVING clauses outside of GROUP BY (treat as additional WHERE filters)\n if (\n query.fnHaving &&\n query.fnHaving.length > 0 &&\n (!query.groupBy || query.groupBy.length === 0)\n ) {\n // If there's no GROUP BY but there are fnHaving clauses, apply them as filters\n for (const fnHaving of query.fnHaving) {\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return fnHaving(namespacedRow)\n })\n )\n }\n }\n\n // Process the DISTINCT clause if it exists\n if (query.distinct) {\n pipeline = pipeline.pipe(distinct(([_key, row]) => row.__select_results))\n }\n\n // Process orderBy parameter if it exists\n if (query.orderBy && query.orderBy.length > 0) {\n const orderedPipeline = processOrderBy(\n pipeline,\n query.orderBy,\n query.limit,\n query.offset\n )\n\n // Final step: extract the __select_results and include orderBy index\n const resultPipeline = orderedPipeline.pipe(\n map(([key, [row, orderByIndex]]) => {\n // Extract the final results from __select_results and include orderBy index\n const finalResults = (row as any).__select_results\n return [key, [finalResults, orderByIndex]] as [unknown, [any, string]]\n })\n )\n\n const result = resultPipeline\n // Cache the result before returning\n cache.set(query, result)\n return result\n } else if (query.limit !== undefined || query.offset !== undefined) {\n // If there's a limit or offset without orderBy, throw an error\n throw new Error(\n `LIMIT and OFFSET require an ORDER BY clause to ensure deterministic results`\n )\n }\n\n // Final step: extract the __select_results and return tuple format (no orderBy)\n const resultPipeline: ResultStream = pipeline.pipe(\n map(([key, row]) => {\n // Extract the final results from __select_results and return [key, [results, undefined]]\n const finalResults = (row as any).__select_results\n return [key, [finalResults, undefined]] as [\n unknown,\n [any, string | undefined],\n ]\n })\n )\n\n const result = resultPipeline\n // Cache the result before returning\n cache.set(query, result)\n return result\n}\n\n/**\n * Processes the FROM clause to extract the main table alias and input stream\n */\nfunction processFrom(\n from: CollectionRef | QueryRef,\n allInputs: Record<string, KeyedStream>,\n cache: QueryCache\n): { alias: string; input: KeyedStream } {\n switch (from.type) {\n case `collectionRef`: {\n const input = allInputs[from.collection.id]\n if (!input) {\n throw new Error(\n `Input for collection \"${from.collection.id}\" not found in inputs map`\n )\n }\n return { alias: from.alias, input }\n }\n case `queryRef`: {\n // Recursively compile the sub-query with cache\n const subQueryInput = compileQuery(from.query, allInputs, cache)\n\n // Subqueries may return [key, [value, orderByIndex]] (with ORDER BY) or [key, value] (without ORDER BY)\n // We need to extract just the value for use in parent queries\n const extractedInput = subQueryInput.pipe(\n map((data: any) => {\n const [key, [value, _orderByIndex]] = data\n return [key, value] as [unknown, any]\n })\n )\n\n return { alias: from.alias, input: extractedInput }\n }\n default:\n throw new Error(`Unsupported FROM type: ${(from as any).type}`)\n }\n}\n"],"names":["map","processJoins","compileExpression","filter","processSelectToResults","processGroupBy","distinct","processOrderBy","resultPipeline","result"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../../../../src/query/compiler/index.ts"],"sourcesContent":["import { distinct, filter, map } from \"@electric-sql/d2mini\"\nimport { optimizeQuery } from \"../optimizer.js\"\nimport { compileExpression } from \"./evaluators.js\"\nimport { processJoins } from \"./joins.js\"\nimport { processGroupBy } from \"./group-by.js\"\nimport { processOrderBy } from \"./order-by.js\"\nimport { processSelectToResults } from \"./select.js\"\nimport type { CollectionRef, QueryIR, QueryRef } from \"../ir.js\"\nimport type {\n KeyedStream,\n NamespacedAndKeyedStream,\n ResultStream,\n} from \"../../types.js\"\nimport type { QueryCache, QueryMapping } from \"./types.js\"\n\n/**\n * Compiles a query2 IR into a D2 pipeline\n * @param rawQuery The query IR to compile\n * @param inputs Mapping of collection names to input streams\n * @param cache Optional cache for compiled subqueries (used internally for recursion)\n * @param queryMapping Optional mapping from optimized queries to original queries\n * @returns A stream builder representing the compiled query\n */\nexport function compileQuery(\n rawQuery: QueryIR,\n inputs: Record<string, KeyedStream>,\n cache: QueryCache = new WeakMap(),\n queryMapping: QueryMapping = new WeakMap()\n): ResultStream {\n // Check if the original raw query has already been compiled\n const cachedResult = cache.get(rawQuery)\n if (cachedResult) {\n return cachedResult\n }\n\n // Optimize the query before compilation\n const query = optimizeQuery(rawQuery)\n\n // Create mapping from optimized query to original for caching\n queryMapping.set(query, rawQuery)\n mapNestedQueries(query, rawQuery, queryMapping)\n\n // Create a copy of the inputs map to avoid modifying the original\n const allInputs = { ...inputs }\n\n // Create a map of table aliases to inputs\n const tables: Record<string, KeyedStream> = {}\n\n // Process the FROM clause to get the main table\n const { alias: mainTableAlias, input: mainInput } = processFrom(\n query.from,\n allInputs,\n cache,\n queryMapping\n )\n tables[mainTableAlias] = mainInput\n\n // Prepare the initial pipeline with the main table wrapped in its alias\n let pipeline: NamespacedAndKeyedStream = mainInput.pipe(\n map(([key, row]) => {\n // Initialize the record with a nested structure\n const ret = [key, { [mainTableAlias]: row }] as [\n string,\n Record<string, typeof row>,\n ]\n return ret\n })\n )\n\n // Process JOIN clauses if they exist\n if (query.join && query.join.length > 0) {\n pipeline = processJoins(\n pipeline,\n query.join,\n tables,\n mainTableAlias,\n allInputs,\n cache,\n queryMapping\n )\n }\n\n // Process the WHERE clause if it exists\n if (query.where && query.where.length > 0) {\n // Compile all WHERE expressions\n const compiledWheres = query.where.map((where) => compileExpression(where))\n\n // Apply each WHERE condition as a filter (they are ANDed together)\n for (const compiledWhere of compiledWheres) {\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return compiledWhere(namespacedRow)\n })\n )\n }\n }\n\n // Process functional WHERE clauses if they exist\n if (query.fnWhere && query.fnWhere.length > 0) {\n for (const fnWhere of query.fnWhere) {\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return fnWhere(namespacedRow)\n })\n )\n }\n }\n\n if (query.distinct && !query.fnSelect && !query.select) {\n throw new Error(`DISTINCT requires a SELECT clause.`)\n }\n\n // Process the SELECT clause early - always create __select_results\n // This eliminates duplication and allows for DISTINCT implementation\n if (query.fnSelect) {\n // Handle functional select - apply the function to transform the row\n pipeline = pipeline.pipe(\n map(([key, namespacedRow]) => {\n const selectResults = query.fnSelect!(namespacedRow)\n return [\n key,\n {\n ...namespacedRow,\n __select_results: selectResults,\n },\n ] as [string, typeof namespacedRow & { __select_results: any }]\n })\n )\n } else if (query.select) {\n pipeline = processSelectToResults(pipeline, query.select, allInputs)\n } else {\n // If no SELECT clause, create __select_results with the main table data\n pipeline = pipeline.pipe(\n map(([key, namespacedRow]) => {\n const selectResults =\n !query.join && !query.groupBy\n ? namespacedRow[mainTableAlias]\n : namespacedRow\n\n return [\n key,\n {\n ...namespacedRow,\n __select_results: selectResults,\n },\n ] as [string, typeof namespacedRow & { __select_results: any }]\n })\n )\n }\n\n // Process the GROUP BY clause if it exists\n if (query.groupBy && query.groupBy.length > 0) {\n pipeline = processGroupBy(\n pipeline,\n query.groupBy,\n query.having,\n query.select,\n query.fnHaving\n )\n } else if (query.select) {\n // Check if SELECT contains aggregates but no GROUP BY (implicit single-group aggregation)\n const hasAggregates = Object.values(query.select).some(\n (expr) => expr.type === `agg`\n )\n if (hasAggregates) {\n // Handle implicit single-group aggregation\n pipeline = processGroupBy(\n pipeline,\n [], // Empty group by means single group\n query.having,\n query.select,\n query.fnHaving\n )\n }\n }\n\n // Process the HAVING clause if it exists (only applies after GROUP BY)\n if (query.having && (!query.groupBy || query.groupBy.length === 0)) {\n // Check if we have aggregates in SELECT that would trigger implicit grouping\n const hasAggregates = query.select\n ? Object.values(query.select).some((expr) => expr.type === `agg`)\n : false\n\n if (!hasAggregates) {\n throw new Error(`HAVING clause requires GROUP BY clause`)\n }\n }\n\n // Process functional HAVING clauses outside of GROUP BY (treat as additional WHERE filters)\n if (\n query.fnHaving &&\n query.fnHaving.length > 0 &&\n (!query.groupBy || query.groupBy.length === 0)\n ) {\n // If there's no GROUP BY but there are fnHaving clauses, apply them as filters\n for (const fnHaving of query.fnHaving) {\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return fnHaving(namespacedRow)\n })\n )\n }\n }\n\n // Process the DISTINCT clause if it exists\n if (query.distinct) {\n pipeline = pipeline.pipe(distinct(([_key, row]) => row.__select_results))\n }\n\n // Process orderBy parameter if it exists\n if (query.orderBy && query.orderBy.length > 0) {\n const orderedPipeline = processOrderBy(\n pipeline,\n query.orderBy,\n query.limit,\n query.offset\n )\n\n // Final step: extract the __select_results and include orderBy index\n const resultPipeline = orderedPipeline.pipe(\n map(([key, [row, orderByIndex]]) => {\n // Extract the final results from __select_results and include orderBy index\n const finalResults = (row as any).__select_results\n return [key, [finalResults, orderByIndex]] as [unknown, [any, string]]\n })\n )\n\n const result = resultPipeline\n // Cache the result before returning (use original query as key)\n cache.set(rawQuery, result)\n return result\n } else if (query.limit !== undefined || query.offset !== undefined) {\n // If there's a limit or offset without orderBy, throw an error\n throw new Error(\n `LIMIT and OFFSET require an ORDER BY clause to ensure deterministic results`\n )\n }\n\n // Final step: extract the __select_results and return tuple format (no orderBy)\n const resultPipeline: ResultStream = pipeline.pipe(\n map(([key, row]) => {\n // Extract the final results from __select_results and return [key, [results, undefined]]\n const finalResults = (row as any).__select_results\n return [key, [finalResults, undefined]] as [\n unknown,\n [any, string | undefined],\n ]\n })\n )\n\n const result = resultPipeline\n // Cache the result before returning (use original query as key)\n cache.set(rawQuery, result)\n return result\n}\n\n/**\n * Processes the FROM clause to extract the main table alias and input stream\n */\nfunction processFrom(\n from: CollectionRef | QueryRef,\n allInputs: Record<string, KeyedStream>,\n cache: QueryCache,\n queryMapping: QueryMapping\n): { alias: string; input: KeyedStream } {\n switch (from.type) {\n case `collectionRef`: {\n const input = allInputs[from.collection.id]\n if (!input) {\n throw new Error(\n `Input for collection \"${from.collection.id}\" not found in inputs map`\n )\n }\n return { alias: from.alias, input }\n }\n case `queryRef`: {\n // Find the original query for caching purposes\n const originalQuery = queryMapping.get(from.query) || from.query\n\n // Recursively compile the sub-query with cache\n const subQueryInput = compileQuery(\n originalQuery,\n allInputs,\n cache,\n queryMapping\n )\n\n // Subqueries may return [key, [value, orderByIndex]] (with ORDER BY) or [key, value] (without ORDER BY)\n // We need to extract just the value for use in parent queries\n const extractedInput = subQueryInput.pipe(\n map((data: any) => {\n const [key, [value, _orderByIndex]] = data\n return [key, value] as [unknown, any]\n })\n )\n\n return { alias: from.alias, input: extractedInput }\n }\n default:\n throw new Error(`Unsupported FROM type: ${(from as any).type}`)\n }\n}\n\n/**\n * Recursively maps optimized subqueries to their original queries for proper caching.\n * This ensures that when we encounter the same QueryRef object in different contexts,\n * we can find the original query to check the cache.\n */\nfunction mapNestedQueries(\n optimizedQuery: QueryIR,\n originalQuery: QueryIR,\n queryMapping: QueryMapping\n): void {\n // Map the FROM clause if it's a QueryRef\n if (\n optimizedQuery.from.type === `queryRef` &&\n originalQuery.from.type === `queryRef`\n ) {\n queryMapping.set(optimizedQuery.from.query, originalQuery.from.query)\n // Recursively map nested queries\n mapNestedQueries(\n optimizedQuery.from.query,\n originalQuery.from.query,\n queryMapping\n )\n }\n\n // Map JOIN clauses if they exist\n if (optimizedQuery.join && originalQuery.join) {\n for (\n let i = 0;\n i < optimizedQuery.join.length && i < originalQuery.join.length;\n i++\n ) {\n const optimizedJoin = optimizedQuery.join[i]!\n const originalJoin = originalQuery.join[i]!\n\n if (\n optimizedJoin.from.type === `queryRef` &&\n originalJoin.from.type === `queryRef`\n ) {\n queryMapping.set(optimizedJoin.from.query, originalJoin.from.query)\n // Recursively map nested queries in joins\n mapNestedQueries(\n optimizedJoin.from.query,\n originalJoin.from.query,\n queryMapping\n )\n }\n }\n }\n}\n"],"names":["optimizeQuery","map","processJoins","compileExpression","filter","processSelectToResults","processGroupBy","distinct","processOrderBy","resultPipeline","result"],"mappings":"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"}
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import { QueryIR } from '../ir.js';
|
|
2
2
|
import { KeyedStream, ResultStream } from '../../types.js';
|
|
3
|
-
|
|
4
|
-
* Cache for compiled subqueries to avoid duplicate compilation
|
|
5
|
-
*/
|
|
6
|
-
type QueryCache = WeakMap<QueryIR, ResultStream>;
|
|
3
|
+
import { QueryCache, QueryMapping } from './types.js';
|
|
7
4
|
/**
|
|
8
5
|
* Compiles a query2 IR into a D2 pipeline
|
|
9
|
-
* @param
|
|
6
|
+
* @param rawQuery The query IR to compile
|
|
10
7
|
* @param inputs Mapping of collection names to input streams
|
|
11
8
|
* @param cache Optional cache for compiled subqueries (used internally for recursion)
|
|
9
|
+
* @param queryMapping Optional mapping from optimized queries to original queries
|
|
12
10
|
* @returns A stream builder representing the compiled query
|
|
13
11
|
*/
|
|
14
|
-
export declare function compileQuery(
|
|
15
|
-
export {};
|
|
12
|
+
export declare function compileQuery(rawQuery: QueryIR, inputs: Record<string, KeyedStream>, cache?: QueryCache, queryMapping?: QueryMapping): ResultStream;
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
3
3
|
const d2mini = require("@electric-sql/d2mini");
|
|
4
4
|
const evaluators = require("./evaluators.cjs");
|
|
5
5
|
const index = require("./index.cjs");
|
|
6
|
-
function processJoins(pipeline, joinClauses, tables, mainTableAlias, allInputs, cache) {
|
|
6
|
+
function processJoins(pipeline, joinClauses, tables, mainTableAlias, allInputs, cache, queryMapping) {
|
|
7
7
|
let resultPipeline = pipeline;
|
|
8
8
|
for (const joinClause of joinClauses) {
|
|
9
9
|
resultPipeline = processJoin(
|
|
@@ -12,16 +12,18 @@ function processJoins(pipeline, joinClauses, tables, mainTableAlias, allInputs,
|
|
|
12
12
|
tables,
|
|
13
13
|
mainTableAlias,
|
|
14
14
|
allInputs,
|
|
15
|
-
cache
|
|
15
|
+
cache,
|
|
16
|
+
queryMapping
|
|
16
17
|
);
|
|
17
18
|
}
|
|
18
19
|
return resultPipeline;
|
|
19
20
|
}
|
|
20
|
-
function processJoin(pipeline, joinClause, tables, mainTableAlias, allInputs, cache) {
|
|
21
|
+
function processJoin(pipeline, joinClause, tables, mainTableAlias, allInputs, cache, queryMapping) {
|
|
21
22
|
const { alias: joinedTableAlias, input: joinedInput } = processJoinSource(
|
|
22
23
|
joinClause.from,
|
|
23
24
|
allInputs,
|
|
24
|
-
cache
|
|
25
|
+
cache,
|
|
26
|
+
queryMapping
|
|
25
27
|
);
|
|
26
28
|
tables[joinedTableAlias] = joinedInput;
|
|
27
29
|
const joinType = joinClause.type === `cross` ? `inner` : joinClause.type === `outer` ? `full` : joinClause.type;
|
|
@@ -49,7 +51,7 @@ function processJoin(pipeline, joinClause, tables, mainTableAlias, allInputs, ca
|
|
|
49
51
|
processJoinResults(joinClause.type)
|
|
50
52
|
);
|
|
51
53
|
}
|
|
52
|
-
function processJoinSource(from, allInputs, cache) {
|
|
54
|
+
function processJoinSource(from, allInputs, cache, queryMapping) {
|
|
53
55
|
switch (from.type) {
|
|
54
56
|
case `collectionRef`: {
|
|
55
57
|
const input = allInputs[from.collection.id];
|
|
@@ -61,7 +63,13 @@ function processJoinSource(from, allInputs, cache) {
|
|
|
61
63
|
return { alias: from.alias, input };
|
|
62
64
|
}
|
|
63
65
|
case `queryRef`: {
|
|
64
|
-
const
|
|
66
|
+
const originalQuery = queryMapping.get(from.query) || from.query;
|
|
67
|
+
const subQueryInput = index.compileQuery(
|
|
68
|
+
originalQuery,
|
|
69
|
+
allInputs,
|
|
70
|
+
cache,
|
|
71
|
+
queryMapping
|
|
72
|
+
);
|
|
65
73
|
const extractedInput = subQueryInput.pipe(
|
|
66
74
|
d2mini.map((data) => {
|
|
67
75
|
const [key, [value, _orderByIndex]] = data;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"joins.cjs","sources":["../../../../src/query/compiler/joins.ts"],"sourcesContent":["import {\n consolidate,\n filter,\n join as joinOperator,\n map,\n} from \"@electric-sql/d2mini\"\nimport { compileExpression } from \"./evaluators.js\"\nimport { compileQuery } from \"./index.js\"\nimport type { IStreamBuilder, JoinType } from \"@electric-sql/d2mini\"\nimport type { CollectionRef, JoinClause,
|
|
1
|
+
{"version":3,"file":"joins.cjs","sources":["../../../../src/query/compiler/joins.ts"],"sourcesContent":["import {\n consolidate,\n filter,\n join as joinOperator,\n map,\n} from \"@electric-sql/d2mini\"\nimport { compileExpression } from \"./evaluators.js\"\nimport { compileQuery } from \"./index.js\"\nimport type { IStreamBuilder, JoinType } from \"@electric-sql/d2mini\"\nimport type { CollectionRef, JoinClause, QueryRef } from \"../ir.js\"\nimport type {\n KeyedStream,\n NamespacedAndKeyedStream,\n NamespacedRow,\n} from \"../../types.js\"\nimport type { QueryCache, QueryMapping } from \"./types.js\"\n\n/**\n * Processes all join clauses in a query\n */\nexport function processJoins(\n pipeline: NamespacedAndKeyedStream,\n joinClauses: Array<JoinClause>,\n tables: Record<string, KeyedStream>,\n mainTableAlias: string,\n allInputs: Record<string, KeyedStream>,\n cache: QueryCache,\n queryMapping: QueryMapping\n): NamespacedAndKeyedStream {\n let resultPipeline = pipeline\n\n for (const joinClause of joinClauses) {\n resultPipeline = processJoin(\n resultPipeline,\n joinClause,\n tables,\n mainTableAlias,\n allInputs,\n cache,\n queryMapping\n )\n }\n\n return resultPipeline\n}\n\n/**\n * Processes a single join clause\n */\nfunction processJoin(\n pipeline: NamespacedAndKeyedStream,\n joinClause: JoinClause,\n tables: Record<string, KeyedStream>,\n mainTableAlias: string,\n allInputs: Record<string, KeyedStream>,\n cache: QueryCache,\n queryMapping: QueryMapping\n): NamespacedAndKeyedStream {\n // Get the joined table alias and input stream\n const { alias: joinedTableAlias, input: joinedInput } = processJoinSource(\n joinClause.from,\n allInputs,\n cache,\n queryMapping\n )\n\n // Add the joined table to the tables map\n tables[joinedTableAlias] = joinedInput\n\n // Convert join type to D2 join type\n const joinType: JoinType =\n joinClause.type === `cross`\n ? `inner`\n : joinClause.type === `outer`\n ? `full`\n : (joinClause.type as JoinType)\n\n // Pre-compile the join expressions\n const compiledLeftExpr = compileExpression(joinClause.left)\n const compiledRightExpr = compileExpression(joinClause.right)\n\n // Prepare the main pipeline for joining\n const mainPipeline = pipeline.pipe(\n map(([currentKey, namespacedRow]) => {\n // Extract the join key from the left side of the join condition\n const leftKey = compiledLeftExpr(namespacedRow)\n\n // Return [joinKey, [originalKey, namespacedRow]]\n return [leftKey, [currentKey, namespacedRow]] as [\n unknown,\n [string, typeof namespacedRow],\n ]\n })\n )\n\n // Prepare the joined pipeline\n const joinedPipeline = joinedInput.pipe(\n map(([currentKey, row]) => {\n // Wrap the row in a namespaced structure\n const namespacedRow: NamespacedRow = { [joinedTableAlias]: row }\n\n // Extract the join key from the right side of the join condition\n const rightKey = compiledRightExpr(namespacedRow)\n\n // Return [joinKey, [originalKey, namespacedRow]]\n return [rightKey, [currentKey, namespacedRow]] as [\n unknown,\n [string, typeof namespacedRow],\n ]\n })\n )\n\n // Apply the join operation\n if (![`inner`, `left`, `right`, `full`].includes(joinType)) {\n throw new Error(`Unsupported join type: ${joinClause.type}`)\n }\n return mainPipeline.pipe(\n joinOperator(joinedPipeline, joinType),\n consolidate(),\n processJoinResults(joinClause.type)\n )\n}\n\n/**\n * Processes the join source (collection or sub-query)\n */\nfunction processJoinSource(\n from: CollectionRef | QueryRef,\n allInputs: Record<string, KeyedStream>,\n cache: QueryCache,\n queryMapping: QueryMapping\n): { alias: string; input: KeyedStream } {\n switch (from.type) {\n case `collectionRef`: {\n const input = allInputs[from.collection.id]\n if (!input) {\n throw new Error(\n `Input for collection \"${from.collection.id}\" not found in inputs map`\n )\n }\n return { alias: from.alias, input }\n }\n case `queryRef`: {\n // Find the original query for caching purposes\n const originalQuery = queryMapping.get(from.query) || from.query\n\n // Recursively compile the sub-query with cache\n const subQueryInput = compileQuery(\n originalQuery,\n allInputs,\n cache,\n queryMapping\n )\n\n // Subqueries may return [key, [value, orderByIndex]] (with ORDER BY) or [key, value] (without ORDER BY)\n // We need to extract just the value for use in parent queries\n const extractedInput = subQueryInput.pipe(\n map((data: any) => {\n const [key, [value, _orderByIndex]] = data\n return [key, value] as [unknown, any]\n })\n )\n\n return { alias: from.alias, input: extractedInput as KeyedStream }\n }\n default:\n throw new Error(`Unsupported join source type: ${(from as any).type}`)\n }\n}\n\n/**\n * Processes the results of a join operation\n */\nfunction processJoinResults(joinType: string) {\n return function (\n pipeline: IStreamBuilder<\n [\n key: string,\n [\n [string, NamespacedRow] | undefined,\n [string, NamespacedRow] | undefined,\n ],\n ]\n >\n ): NamespacedAndKeyedStream {\n return pipeline.pipe(\n // Process the join result and handle nulls\n filter((result) => {\n const [_key, [main, joined]] = result\n const mainNamespacedRow = main?.[1]\n const joinedNamespacedRow = joined?.[1]\n\n // Handle different join types\n if (joinType === `inner`) {\n return !!(mainNamespacedRow && joinedNamespacedRow)\n }\n\n if (joinType === `left`) {\n return !!mainNamespacedRow\n }\n\n if (joinType === `right`) {\n return !!joinedNamespacedRow\n }\n\n // For full joins, always include\n return true\n }),\n map((result) => {\n const [_key, [main, joined]] = result\n const mainKey = main?.[0]\n const mainNamespacedRow = main?.[1]\n const joinedKey = joined?.[0]\n const joinedNamespacedRow = joined?.[1]\n\n // Merge the namespaced rows\n const mergedNamespacedRow: NamespacedRow = {}\n\n // Add main row data if it exists\n if (mainNamespacedRow) {\n Object.assign(mergedNamespacedRow, mainNamespacedRow)\n }\n\n // Add joined row data if it exists\n if (joinedNamespacedRow) {\n Object.assign(mergedNamespacedRow, joinedNamespacedRow)\n }\n\n // We create a composite key that combines the main and joined keys\n const resultKey = `[${mainKey},${joinedKey}]`\n\n return [resultKey, mergedNamespacedRow] as [string, NamespacedRow]\n })\n )\n }\n}\n"],"names":["compileExpression","map","joinOperator","consolidate","compileQuery","filter"],"mappings":"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"}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import { JoinClause
|
|
2
|
-
import { KeyedStream, NamespacedAndKeyedStream
|
|
3
|
-
|
|
4
|
-
* Cache for compiled subqueries to avoid duplicate compilation
|
|
5
|
-
*/
|
|
6
|
-
type QueryCache = WeakMap<QueryIR, ResultStream>;
|
|
1
|
+
import { JoinClause } from '../ir.js';
|
|
2
|
+
import { KeyedStream, NamespacedAndKeyedStream } from '../../types.js';
|
|
3
|
+
import { QueryCache, QueryMapping } from './types.js';
|
|
7
4
|
/**
|
|
8
5
|
* Processes all join clauses in a query
|
|
9
6
|
*/
|
|
10
|
-
export declare function processJoins(pipeline: NamespacedAndKeyedStream, joinClauses: Array<JoinClause>, tables: Record<string, KeyedStream>, mainTableAlias: string, allInputs: Record<string, KeyedStream>, cache: QueryCache): NamespacedAndKeyedStream;
|
|
11
|
-
export {};
|
|
7
|
+
export declare function processJoins(pipeline: NamespacedAndKeyedStream, joinClauses: Array<JoinClause>, tables: Record<string, KeyedStream>, mainTableAlias: string, allInputs: Record<string, KeyedStream>, cache: QueryCache, queryMapping: QueryMapping): NamespacedAndKeyedStream;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { QueryIR } from '../ir.js';
|
|
2
|
+
import { ResultStream } from '../../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Cache for compiled subqueries to avoid duplicate compilation
|
|
5
|
+
*/
|
|
6
|
+
export type QueryCache = WeakMap<QueryIR, ResultStream>;
|
|
7
|
+
/**
|
|
8
|
+
* Mapping from optimized queries back to their original queries for caching
|
|
9
|
+
*/
|
|
10
|
+
export type QueryMapping = WeakMap<QueryIR, QueryIR>;
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const utils = require("../utils.cjs");
|
|
4
|
+
const ir = require("./ir.cjs");
|
|
5
|
+
function optimizeQuery(query) {
|
|
6
|
+
let optimized = query;
|
|
7
|
+
let previousOptimized;
|
|
8
|
+
let iterations = 0;
|
|
9
|
+
const maxIterations = 10;
|
|
10
|
+
while (iterations < maxIterations && !utils.deepEquals(optimized, previousOptimized)) {
|
|
11
|
+
previousOptimized = optimized;
|
|
12
|
+
optimized = applyRecursiveOptimization(optimized);
|
|
13
|
+
iterations++;
|
|
14
|
+
}
|
|
15
|
+
const cleaned = removeRedundantSubqueries(optimized);
|
|
16
|
+
return cleaned;
|
|
17
|
+
}
|
|
18
|
+
function applyRecursiveOptimization(query) {
|
|
19
|
+
var _a;
|
|
20
|
+
const subqueriesOptimized = {
|
|
21
|
+
...query,
|
|
22
|
+
from: query.from.type === `queryRef` ? new ir.QueryRef(
|
|
23
|
+
applyRecursiveOptimization(query.from.query),
|
|
24
|
+
query.from.alias
|
|
25
|
+
) : query.from,
|
|
26
|
+
join: (_a = query.join) == null ? void 0 : _a.map((joinClause) => ({
|
|
27
|
+
...joinClause,
|
|
28
|
+
from: joinClause.from.type === `queryRef` ? new ir.QueryRef(
|
|
29
|
+
applyRecursiveOptimization(joinClause.from.query),
|
|
30
|
+
joinClause.from.alias
|
|
31
|
+
) : joinClause.from
|
|
32
|
+
}))
|
|
33
|
+
};
|
|
34
|
+
return applySingleLevelOptimization(subqueriesOptimized);
|
|
35
|
+
}
|
|
36
|
+
function applySingleLevelOptimization(query) {
|
|
37
|
+
if (!query.where || query.where.length === 0) {
|
|
38
|
+
return query;
|
|
39
|
+
}
|
|
40
|
+
if (!query.join || query.join.length === 0) {
|
|
41
|
+
return query;
|
|
42
|
+
}
|
|
43
|
+
const splitWhereClauses = splitAndClauses(query.where);
|
|
44
|
+
const analyzedClauses = splitWhereClauses.map(
|
|
45
|
+
(clause) => analyzeWhereClause(clause)
|
|
46
|
+
);
|
|
47
|
+
const groupedClauses = groupWhereClauses(analyzedClauses);
|
|
48
|
+
return applyOptimizations(query, groupedClauses);
|
|
49
|
+
}
|
|
50
|
+
function removeRedundantSubqueries(query) {
|
|
51
|
+
var _a;
|
|
52
|
+
return {
|
|
53
|
+
...query,
|
|
54
|
+
from: removeRedundantFromClause(query.from),
|
|
55
|
+
join: (_a = query.join) == null ? void 0 : _a.map((joinClause) => ({
|
|
56
|
+
...joinClause,
|
|
57
|
+
from: removeRedundantFromClause(joinClause.from)
|
|
58
|
+
}))
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function removeRedundantFromClause(from) {
|
|
62
|
+
if (from.type === `collectionRef`) {
|
|
63
|
+
return from;
|
|
64
|
+
}
|
|
65
|
+
const processedQuery = removeRedundantSubqueries(from.query);
|
|
66
|
+
if (isRedundantSubquery(processedQuery)) {
|
|
67
|
+
const innerFrom = removeRedundantFromClause(processedQuery.from);
|
|
68
|
+
if (innerFrom.type === `collectionRef`) {
|
|
69
|
+
return new ir.CollectionRef(innerFrom.collection, from.alias);
|
|
70
|
+
} else {
|
|
71
|
+
return new ir.QueryRef(innerFrom.query, from.alias);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return new ir.QueryRef(processedQuery, from.alias);
|
|
75
|
+
}
|
|
76
|
+
function isRedundantSubquery(query) {
|
|
77
|
+
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);
|
|
78
|
+
}
|
|
79
|
+
function splitAndClauses(whereClauses) {
|
|
80
|
+
const result = [];
|
|
81
|
+
for (const clause of whereClauses) {
|
|
82
|
+
if (clause.type === `func` && clause.name === `and`) {
|
|
83
|
+
const splitArgs = splitAndClauses(
|
|
84
|
+
clause.args
|
|
85
|
+
);
|
|
86
|
+
result.push(...splitArgs);
|
|
87
|
+
} else {
|
|
88
|
+
result.push(clause);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
function analyzeWhereClause(clause) {
|
|
94
|
+
const touchedSources = /* @__PURE__ */ new Set();
|
|
95
|
+
function collectSources(expr) {
|
|
96
|
+
switch (expr.type) {
|
|
97
|
+
case `ref`:
|
|
98
|
+
if (expr.path && expr.path.length > 0) {
|
|
99
|
+
const firstElement = expr.path[0];
|
|
100
|
+
if (firstElement) {
|
|
101
|
+
touchedSources.add(firstElement);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
case `func`:
|
|
106
|
+
if (expr.args) {
|
|
107
|
+
expr.args.forEach(collectSources);
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
case `val`:
|
|
111
|
+
break;
|
|
112
|
+
case `agg`:
|
|
113
|
+
if (expr.args) {
|
|
114
|
+
expr.args.forEach(collectSources);
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
collectSources(clause);
|
|
120
|
+
return {
|
|
121
|
+
expression: clause,
|
|
122
|
+
touchedSources
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function groupWhereClauses(analyzedClauses) {
|
|
126
|
+
const singleSource = /* @__PURE__ */ new Map();
|
|
127
|
+
const multiSource = [];
|
|
128
|
+
for (const clause of analyzedClauses) {
|
|
129
|
+
if (clause.touchedSources.size === 1) {
|
|
130
|
+
const source = Array.from(clause.touchedSources)[0];
|
|
131
|
+
if (!singleSource.has(source)) {
|
|
132
|
+
singleSource.set(source, []);
|
|
133
|
+
}
|
|
134
|
+
singleSource.get(source).push(clause.expression);
|
|
135
|
+
} else if (clause.touchedSources.size > 1) {
|
|
136
|
+
multiSource.push(clause.expression);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const combinedSingleSource = /* @__PURE__ */ new Map();
|
|
140
|
+
for (const [source, clauses] of singleSource) {
|
|
141
|
+
combinedSingleSource.set(source, combineWithAnd(clauses));
|
|
142
|
+
}
|
|
143
|
+
const combinedMultiSource = multiSource.length > 0 ? combineWithAnd(multiSource) : void 0;
|
|
144
|
+
return {
|
|
145
|
+
singleSource: combinedSingleSource,
|
|
146
|
+
multiSource: combinedMultiSource
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function applyOptimizations(query, groupedClauses) {
|
|
150
|
+
const actuallyOptimized = /* @__PURE__ */ new Set();
|
|
151
|
+
const optimizedFrom = optimizeFromWithTracking(
|
|
152
|
+
query.from,
|
|
153
|
+
groupedClauses.singleSource,
|
|
154
|
+
actuallyOptimized
|
|
155
|
+
);
|
|
156
|
+
const optimizedJoins = query.join ? query.join.map((joinClause) => ({
|
|
157
|
+
...joinClause,
|
|
158
|
+
from: optimizeFromWithTracking(
|
|
159
|
+
joinClause.from,
|
|
160
|
+
groupedClauses.singleSource,
|
|
161
|
+
actuallyOptimized
|
|
162
|
+
)
|
|
163
|
+
})) : void 0;
|
|
164
|
+
const remainingWhereClauses = [];
|
|
165
|
+
if (groupedClauses.multiSource) {
|
|
166
|
+
remainingWhereClauses.push(groupedClauses.multiSource);
|
|
167
|
+
}
|
|
168
|
+
for (const [source, clause] of groupedClauses.singleSource) {
|
|
169
|
+
if (!actuallyOptimized.has(source)) {
|
|
170
|
+
remainingWhereClauses.push(clause);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const optimizedQuery = {
|
|
174
|
+
// Copy all non-optimized fields as-is
|
|
175
|
+
select: query.select,
|
|
176
|
+
groupBy: query.groupBy ? [...query.groupBy] : void 0,
|
|
177
|
+
having: query.having ? [...query.having] : void 0,
|
|
178
|
+
orderBy: query.orderBy ? [...query.orderBy] : void 0,
|
|
179
|
+
limit: query.limit,
|
|
180
|
+
offset: query.offset,
|
|
181
|
+
fnSelect: query.fnSelect,
|
|
182
|
+
fnWhere: query.fnWhere ? [...query.fnWhere] : void 0,
|
|
183
|
+
fnHaving: query.fnHaving ? [...query.fnHaving] : void 0,
|
|
184
|
+
// Use the optimized FROM and JOIN clauses
|
|
185
|
+
from: optimizedFrom,
|
|
186
|
+
join: optimizedJoins,
|
|
187
|
+
// Only include WHERE clauses that weren't successfully optimized
|
|
188
|
+
where: remainingWhereClauses.length > 0 ? remainingWhereClauses : []
|
|
189
|
+
};
|
|
190
|
+
return optimizedQuery;
|
|
191
|
+
}
|
|
192
|
+
function deepCopyQuery(query) {
|
|
193
|
+
return {
|
|
194
|
+
// Recursively copy the FROM clause
|
|
195
|
+
from: query.from.type === `collectionRef` ? new ir.CollectionRef(query.from.collection, query.from.alias) : new ir.QueryRef(deepCopyQuery(query.from.query), query.from.alias),
|
|
196
|
+
// Copy all other fields, creating new arrays where necessary
|
|
197
|
+
select: query.select,
|
|
198
|
+
join: query.join ? query.join.map((joinClause) => ({
|
|
199
|
+
type: joinClause.type,
|
|
200
|
+
left: joinClause.left,
|
|
201
|
+
right: joinClause.right,
|
|
202
|
+
from: joinClause.from.type === `collectionRef` ? new ir.CollectionRef(
|
|
203
|
+
joinClause.from.collection,
|
|
204
|
+
joinClause.from.alias
|
|
205
|
+
) : new ir.QueryRef(
|
|
206
|
+
deepCopyQuery(joinClause.from.query),
|
|
207
|
+
joinClause.from.alias
|
|
208
|
+
)
|
|
209
|
+
})) : void 0,
|
|
210
|
+
where: query.where ? [...query.where] : void 0,
|
|
211
|
+
groupBy: query.groupBy ? [...query.groupBy] : void 0,
|
|
212
|
+
having: query.having ? [...query.having] : void 0,
|
|
213
|
+
orderBy: query.orderBy ? [...query.orderBy] : void 0,
|
|
214
|
+
limit: query.limit,
|
|
215
|
+
offset: query.offset,
|
|
216
|
+
fnSelect: query.fnSelect,
|
|
217
|
+
fnWhere: query.fnWhere ? [...query.fnWhere] : void 0,
|
|
218
|
+
fnHaving: query.fnHaving ? [...query.fnHaving] : void 0
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function optimizeFromWithTracking(from, singleSourceClauses, actuallyOptimized) {
|
|
222
|
+
const whereClause = singleSourceClauses.get(from.alias);
|
|
223
|
+
if (!whereClause) {
|
|
224
|
+
if (from.type === `collectionRef`) {
|
|
225
|
+
return new ir.CollectionRef(from.collection, from.alias);
|
|
226
|
+
}
|
|
227
|
+
return new ir.QueryRef(deepCopyQuery(from.query), from.alias);
|
|
228
|
+
}
|
|
229
|
+
if (from.type === `collectionRef`) {
|
|
230
|
+
const subQuery = {
|
|
231
|
+
from: new ir.CollectionRef(from.collection, from.alias),
|
|
232
|
+
where: [whereClause]
|
|
233
|
+
};
|
|
234
|
+
actuallyOptimized.add(from.alias);
|
|
235
|
+
return new ir.QueryRef(subQuery, from.alias);
|
|
236
|
+
}
|
|
237
|
+
if (!isSafeToPushIntoExistingSubquery(from.query)) {
|
|
238
|
+
return new ir.QueryRef(deepCopyQuery(from.query), from.alias);
|
|
239
|
+
}
|
|
240
|
+
const existingWhere = from.query.where || [];
|
|
241
|
+
const optimizedSubQuery = {
|
|
242
|
+
...deepCopyQuery(from.query),
|
|
243
|
+
where: [...existingWhere, whereClause]
|
|
244
|
+
};
|
|
245
|
+
actuallyOptimized.add(from.alias);
|
|
246
|
+
return new ir.QueryRef(optimizedSubQuery, from.alias);
|
|
247
|
+
}
|
|
248
|
+
function isSafeToPushIntoExistingSubquery(query) {
|
|
249
|
+
if (query.select) {
|
|
250
|
+
const hasAggregates = Object.values(query.select).some(
|
|
251
|
+
(expr) => expr.type === `agg`
|
|
252
|
+
);
|
|
253
|
+
if (hasAggregates) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (query.groupBy && query.groupBy.length > 0) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
if (query.having && query.having.length > 0) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
if (query.orderBy && query.orderBy.length > 0) {
|
|
264
|
+
if (query.limit !== void 0 || query.offset !== void 0) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (query.fnSelect || query.fnWhere && query.fnWhere.length > 0 || query.fnHaving && query.fnHaving.length > 0) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
function combineWithAnd(expressions) {
|
|
274
|
+
if (expressions.length === 0) {
|
|
275
|
+
throw new Error(`Cannot combine empty expression list`);
|
|
276
|
+
}
|
|
277
|
+
if (expressions.length === 1) {
|
|
278
|
+
return expressions[0];
|
|
279
|
+
}
|
|
280
|
+
return new ir.Func(`and`, expressions);
|
|
281
|
+
}
|
|
282
|
+
exports.optimizeQuery = optimizeQuery;
|
|
283
|
+
//# sourceMappingURL=optimizer.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"optimizer.cjs","sources":["../../../src/query/optimizer.ts"],"sourcesContent":["/**\n * # Query Optimizer\n *\n * The query optimizer improves query performance by implementing predicate pushdown optimization.\n * It rewrites the intermediate representation (IR) to push WHERE clauses as close to the data\n * source as possible, reducing the amount of data processed during joins.\n *\n * ## How It Works\n *\n * The optimizer follows a 4-step process:\n *\n * ### 1. AND Clause Splitting\n * Splits AND clauses at the root level into separate WHERE clauses for granular optimization.\n * ```javascript\n * // Before: WHERE and(eq(users.department_id, 1), gt(users.age, 25))\n * // After: WHERE eq(users.department_id, 1) + WHERE gt(users.age, 25)\n * ```\n *\n * ### 2. Source Analysis\n * Analyzes each WHERE clause to determine which table sources it references:\n * - Single-source clauses: Touch only one table (e.g., `users.department_id = 1`)\n * - Multi-source clauses: Touch multiple tables (e.g., `users.id = posts.user_id`)\n *\n * ### 3. Clause Grouping\n * Groups WHERE clauses by the sources they touch:\n * - Single-source clauses are grouped by their respective table\n * - Multi-source clauses are combined for the main query\n *\n * ### 4. Subquery Creation\n * Lifts single-source WHERE clauses into subqueries that wrap the original table references.\n *\n * ## Safety & Edge Cases\n *\n * The optimizer includes targeted safety checks to prevent predicate pushdown when it could\n * break query semantics:\n *\n * ### Always Safe Operations\n * - **Creating new subqueries**: Wrapping collection references in subqueries with WHERE clauses\n * - **Main query optimizations**: Moving single-source WHERE clauses from main query to subqueries\n * - **Queries with aggregates/ORDER BY/HAVING**: Can still create new filtered subqueries\n *\n * ### Unsafe Operations (blocked by safety checks)\n * Pushing WHERE clauses **into existing subqueries** that have:\n * - **Aggregates**: GROUP BY, HAVING, or aggregate functions in SELECT (would change aggregation)\n * - **Ordering + Limits**: ORDER BY combined with LIMIT/OFFSET (would change result set)\n * - **Functional Operations**: fnSelect, fnWhere, fnHaving (potential side effects)\n *\n * The optimizer tracks which clauses were actually optimized and only removes those from the\n * main query. Subquery reuse is handled safely through immutable query copies.\n *\n * ## Example Optimizations\n *\n * ### Basic Query with Joins\n * **Original Query:**\n * ```javascript\n * query\n * .from({ users: usersCollection })\n * .join({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.user_id))\n * .where(({users}) => eq(users.department_id, 1))\n * .where(({posts}) => gt(posts.views, 100))\n * .where(({users, posts}) => eq(users.id, posts.author_id))\n * ```\n *\n * **Optimized Query:**\n * ```javascript\n * query\n * .from({\n * users: subquery\n * .from({ users: usersCollection })\n * .where(({users}) => eq(users.department_id, 1))\n * })\n * .join({\n * posts: subquery\n * .from({ posts: postsCollection })\n * .where(({posts}) => gt(posts.views, 100))\n * }, ({users, posts}) => eq(users.id, posts.user_id))\n * .where(({users, posts}) => eq(users.id, posts.author_id))\n * ```\n *\n * ### Query with Aggregates (Now Optimizable!)\n * **Original Query:**\n * ```javascript\n * query\n * .from({ users: usersCollection })\n * .join({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.user_id))\n * .where(({users}) => eq(users.department_id, 1))\n * .groupBy(['users.department_id'])\n * .select({ count: agg('count', '*') })\n * ```\n *\n * **Optimized Query:**\n * ```javascript\n * query\n * .from({\n * users: subquery\n * .from({ users: usersCollection })\n * .where(({users}) => eq(users.department_id, 1))\n * })\n * .join({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.user_id))\n * .groupBy(['users.department_id'])\n * .select({ count: agg('count', '*') })\n * ```\n *\n * ## Benefits\n *\n * - **Reduced Data Processing**: Filters applied before joins reduce intermediate result size\n * - **Better Performance**: Smaller datasets lead to faster query execution\n * - **Automatic Optimization**: No manual query rewriting required\n * - **Preserves Semantics**: Optimized queries return identical results\n * - **Safe by Design**: Comprehensive checks prevent semantic-breaking optimizations\n *\n * ## Integration\n *\n * The optimizer is automatically called during query compilation before the IR is\n * transformed into a D2Mini pipeline.\n */\n\nimport { deepEquals } from \"../utils.js\"\nimport {\n CollectionRef as CollectionRefClass,\n Func,\n QueryRef as QueryRefClass,\n} from \"./ir.js\"\nimport type { BasicExpression, From, QueryIR } from \"./ir.js\"\n\n/**\n * Represents a WHERE clause after source analysis\n */\nexport interface AnalyzedWhereClause {\n /** The WHERE expression */\n expression: BasicExpression<boolean>\n /** Set of table/source aliases that this WHERE clause touches */\n touchedSources: Set<string>\n}\n\n/**\n * Represents WHERE clauses grouped by the sources they touch\n */\nexport interface GroupedWhereClauses {\n /** WHERE clauses that touch only a single source, grouped by source alias */\n singleSource: Map<string, BasicExpression<boolean>>\n /** WHERE clauses that touch multiple sources, combined into one expression */\n multiSource?: BasicExpression<boolean>\n}\n\n/**\n * Main query optimizer entry point that lifts WHERE clauses into subqueries.\n *\n * This function implements multi-level predicate pushdown optimization by recursively\n * moving WHERE clauses through nested subqueries to get them as close to the data\n * sources as possible, then removing redundant subqueries.\n *\n * @param query - The QueryIR to optimize\n * @returns A new QueryIR with optimizations applied (or original if no optimization possible)\n *\n * @example\n * ```typescript\n * const originalQuery = {\n * from: new CollectionRef(users, 'u'),\n * join: [{ from: new CollectionRef(posts, 'p'), ... }],\n * where: [eq(u.dept_id, 1), gt(p.views, 100)]\n * }\n *\n * const optimized = optimizeQuery(originalQuery)\n * // Result: Single-source clauses moved to deepest possible subqueries\n * ```\n */\nexport function optimizeQuery(query: QueryIR): QueryIR {\n // Apply multi-level predicate pushdown with iterative convergence\n let optimized = query\n let previousOptimized: QueryIR | undefined\n let iterations = 0\n const maxIterations = 10 // Prevent infinite loops\n\n // Keep optimizing until no more changes occur or max iterations reached\n while (\n iterations < maxIterations &&\n !deepEquals(optimized, previousOptimized)\n ) {\n previousOptimized = optimized\n optimized = applyRecursiveOptimization(optimized)\n iterations++\n }\n\n // Remove redundant subqueries\n const cleaned = removeRedundantSubqueries(optimized)\n\n return cleaned\n}\n\n/**\n * Applies recursive predicate pushdown optimization.\n *\n * @param query - The QueryIR to optimize\n * @returns A new QueryIR with optimizations applied\n */\nfunction applyRecursiveOptimization(query: QueryIR): QueryIR {\n // First, recursively optimize any existing subqueries\n const subqueriesOptimized = {\n ...query,\n from:\n query.from.type === `queryRef`\n ? new QueryRefClass(\n applyRecursiveOptimization(query.from.query),\n query.from.alias\n )\n : query.from,\n join: query.join?.map((joinClause) => ({\n ...joinClause,\n from:\n joinClause.from.type === `queryRef`\n ? new QueryRefClass(\n applyRecursiveOptimization(joinClause.from.query),\n joinClause.from.alias\n )\n : joinClause.from,\n })),\n }\n\n // Then apply single-level optimization to this query\n return applySingleLevelOptimization(subqueriesOptimized)\n}\n\n/**\n * Applies single-level predicate pushdown optimization (existing logic)\n */\nfunction applySingleLevelOptimization(query: QueryIR): QueryIR {\n // Skip optimization if no WHERE clauses exist\n if (!query.where || query.where.length === 0) {\n return query\n }\n\n // Skip optimization if there are no joins - predicate pushdown only benefits joins\n // Single-table queries don't benefit from this optimization\n if (!query.join || query.join.length === 0) {\n return query\n }\n\n // Step 1: Split all AND clauses at the root level for granular optimization\n const splitWhereClauses = splitAndClauses(query.where)\n\n // Step 2: Analyze each WHERE clause to determine which sources it touches\n const analyzedClauses = splitWhereClauses.map((clause) =>\n analyzeWhereClause(clause)\n )\n\n // Step 3: Group clauses by single-source vs multi-source\n const groupedClauses = groupWhereClauses(analyzedClauses)\n\n // Step 4: Apply optimizations by lifting single-source clauses into subqueries\n return applyOptimizations(query, groupedClauses)\n}\n\n/**\n * Removes redundant subqueries that don't add value.\n * A subquery is redundant if it only wraps another query without adding\n * WHERE, SELECT, GROUP BY, HAVING, ORDER BY, or LIMIT/OFFSET clauses.\n *\n * @param query - The QueryIR to process\n * @returns A new QueryIR with redundant subqueries removed\n */\nfunction removeRedundantSubqueries(query: QueryIR): QueryIR {\n return {\n ...query,\n from: removeRedundantFromClause(query.from),\n join: query.join?.map((joinClause) => ({\n ...joinClause,\n from: removeRedundantFromClause(joinClause.from),\n })),\n }\n}\n\n/**\n * Removes redundant subqueries from a FROM clause.\n *\n * @param from - The FROM clause to process\n * @returns A FROM clause with redundant subqueries removed\n */\nfunction removeRedundantFromClause(from: From): From {\n if (from.type === `collectionRef`) {\n return from\n }\n\n const processedQuery = removeRedundantSubqueries(from.query)\n\n // Check if this subquery is redundant\n if (isRedundantSubquery(processedQuery)) {\n // Return the inner query's FROM clause with this alias\n const innerFrom = removeRedundantFromClause(processedQuery.from)\n if (innerFrom.type === `collectionRef`) {\n return new CollectionRefClass(innerFrom.collection, from.alias)\n } else {\n return new QueryRefClass(innerFrom.query, from.alias)\n }\n }\n\n return new QueryRefClass(processedQuery, from.alias)\n}\n\n/**\n * Determines if a subquery is redundant (adds no value).\n *\n * @param query - The query to check\n * @returns True if the query is redundant and can be removed\n */\nfunction isRedundantSubquery(query: QueryIR): boolean {\n return (\n (!query.where || query.where.length === 0) &&\n !query.select &&\n (!query.groupBy || query.groupBy.length === 0) &&\n (!query.having || query.having.length === 0) &&\n (!query.orderBy || query.orderBy.length === 0) &&\n (!query.join || query.join.length === 0) &&\n query.limit === undefined &&\n query.offset === undefined &&\n !query.fnSelect &&\n (!query.fnWhere || query.fnWhere.length === 0) &&\n (!query.fnHaving || query.fnHaving.length === 0)\n )\n}\n\n/**\n * Step 1: Split all AND clauses recursively into separate WHERE clauses.\n *\n * This enables more granular optimization by treating each condition independently.\n * OR clauses are preserved as they cannot be split without changing query semantics.\n *\n * @param whereClauses - Array of WHERE expressions to split\n * @returns Flattened array with AND clauses split into separate expressions\n *\n * @example\n * ```typescript\n * // Input: [and(eq(a, 1), gt(b, 2)), eq(c, 3)]\n * // Output: [eq(a, 1), gt(b, 2), eq(c, 3)]\n * ```\n */\nfunction splitAndClauses(\n whereClauses: Array<BasicExpression<boolean>>\n): Array<BasicExpression<boolean>> {\n const result: Array<BasicExpression<boolean>> = []\n\n for (const clause of whereClauses) {\n if (clause.type === `func` && clause.name === `and`) {\n // Recursively split nested AND clauses to handle complex expressions\n const splitArgs = splitAndClauses(\n clause.args as Array<BasicExpression<boolean>>\n )\n result.push(...splitArgs)\n } else {\n // Preserve non-AND clauses as-is (including OR clauses)\n result.push(clause)\n }\n }\n\n return result\n}\n\n/**\n * Step 2: Analyze which table sources a WHERE clause touches.\n *\n * This determines whether a clause can be pushed down to a specific table\n * or must remain in the main query (for multi-source clauses like join conditions).\n *\n * @param clause - The WHERE expression to analyze\n * @returns Analysis result with the expression and touched source aliases\n *\n * @example\n * ```typescript\n * // eq(users.department_id, 1) -> touches ['users']\n * // eq(users.id, posts.user_id) -> touches ['users', 'posts']\n * ```\n */\nfunction analyzeWhereClause(\n clause: BasicExpression<boolean>\n): AnalyzedWhereClause {\n const touchedSources = new Set<string>()\n\n /**\n * Recursively collect all table aliases referenced in an expression\n */\n function collectSources(expr: BasicExpression | any): void {\n switch (expr.type) {\n case `ref`:\n // PropRef path has the table alias as the first element\n if (expr.path && expr.path.length > 0) {\n const firstElement = expr.path[0]\n if (firstElement) {\n touchedSources.add(firstElement)\n }\n }\n break\n case `func`:\n // Recursively analyze function arguments (e.g., eq, gt, and, or)\n if (expr.args) {\n expr.args.forEach(collectSources)\n }\n break\n case `val`:\n // Values don't reference any sources\n break\n case `agg`:\n // Aggregates can reference sources in their arguments\n if (expr.args) {\n expr.args.forEach(collectSources)\n }\n break\n }\n }\n\n collectSources(clause)\n\n return {\n expression: clause,\n touchedSources,\n }\n}\n\n/**\n * Step 3: Group WHERE clauses by the sources they touch.\n *\n * Single-source clauses can be pushed down to subqueries for optimization.\n * Multi-source clauses must remain in the main query to preserve join semantics.\n *\n * @param analyzedClauses - Array of analyzed WHERE clauses\n * @returns Grouped clauses ready for optimization\n */\nfunction groupWhereClauses(\n analyzedClauses: Array<AnalyzedWhereClause>\n): GroupedWhereClauses {\n const singleSource = new Map<string, Array<BasicExpression<boolean>>>()\n const multiSource: Array<BasicExpression<boolean>> = []\n\n // Categorize each clause based on how many sources it touches\n for (const clause of analyzedClauses) {\n if (clause.touchedSources.size === 1) {\n // Single source clause - can be optimized\n const source = Array.from(clause.touchedSources)[0]!\n if (!singleSource.has(source)) {\n singleSource.set(source, [])\n }\n singleSource.get(source)!.push(clause.expression)\n } else if (clause.touchedSources.size > 1) {\n // Multi-source clause - must stay in main query\n multiSource.push(clause.expression)\n }\n // Skip clauses that touch no sources (constants) - they don't need optimization\n }\n\n // Combine multiple clauses for each source with AND\n const combinedSingleSource = new Map<string, BasicExpression<boolean>>()\n for (const [source, clauses] of singleSource) {\n combinedSingleSource.set(source, combineWithAnd(clauses))\n }\n\n // Combine multi-source clauses with AND\n const combinedMultiSource =\n multiSource.length > 0 ? combineWithAnd(multiSource) : undefined\n\n return {\n singleSource: combinedSingleSource,\n multiSource: combinedMultiSource,\n }\n}\n\n/**\n * Step 4: Apply optimizations by lifting single-source clauses into subqueries.\n *\n * Creates a new QueryIR with single-source WHERE clauses moved to subqueries\n * that wrap the original table references. This ensures immutability and prevents\n * infinite recursion issues.\n *\n * @param query - Original QueryIR to optimize\n * @param groupedClauses - WHERE clauses grouped by optimization strategy\n * @returns New QueryIR with optimizations applied\n */\nfunction applyOptimizations(\n query: QueryIR,\n groupedClauses: GroupedWhereClauses\n): QueryIR {\n // Track which single-source clauses were actually optimized\n const actuallyOptimized = new Set<string>()\n\n // Optimize the main FROM clause and track what was optimized\n const optimizedFrom = optimizeFromWithTracking(\n query.from,\n groupedClauses.singleSource,\n actuallyOptimized\n )\n\n // Optimize JOIN clauses and track what was optimized\n const optimizedJoins = query.join\n ? query.join.map((joinClause) => ({\n ...joinClause,\n from: optimizeFromWithTracking(\n joinClause.from,\n groupedClauses.singleSource,\n actuallyOptimized\n ),\n }))\n : undefined\n\n // Build the remaining WHERE clauses: multi-source + any single-source that weren't optimized\n const remainingWhereClauses: Array<BasicExpression<boolean>> = []\n\n // Add multi-source clauses\n if (groupedClauses.multiSource) {\n remainingWhereClauses.push(groupedClauses.multiSource)\n }\n\n // Add single-source clauses that weren't actually optimized\n for (const [source, clause] of groupedClauses.singleSource) {\n if (!actuallyOptimized.has(source)) {\n remainingWhereClauses.push(clause)\n }\n }\n\n // Create a completely new query object to ensure immutability\n const optimizedQuery: QueryIR = {\n // Copy all non-optimized fields as-is\n select: query.select,\n groupBy: query.groupBy ? [...query.groupBy] : undefined,\n having: query.having ? [...query.having] : undefined,\n orderBy: query.orderBy ? [...query.orderBy] : undefined,\n limit: query.limit,\n offset: query.offset,\n fnSelect: query.fnSelect,\n fnWhere: query.fnWhere ? [...query.fnWhere] : undefined,\n fnHaving: query.fnHaving ? [...query.fnHaving] : undefined,\n\n // Use the optimized FROM and JOIN clauses\n from: optimizedFrom,\n join: optimizedJoins,\n\n // Only include WHERE clauses that weren't successfully optimized\n where: remainingWhereClauses.length > 0 ? remainingWhereClauses : [],\n }\n\n return optimizedQuery\n}\n\n/**\n * Helper function to create a deep copy of a QueryIR object for immutability.\n *\n * This ensures that all optimizations create new objects rather than modifying\n * existing ones, preventing infinite recursion and shared reference issues.\n *\n * @param query - QueryIR to deep copy\n * @returns New QueryIR object with all nested objects copied\n */\nfunction deepCopyQuery(query: QueryIR): QueryIR {\n return {\n // Recursively copy the FROM clause\n from:\n query.from.type === `collectionRef`\n ? new CollectionRefClass(query.from.collection, query.from.alias)\n : new QueryRefClass(deepCopyQuery(query.from.query), query.from.alias),\n\n // Copy all other fields, creating new arrays where necessary\n select: query.select,\n join: query.join\n ? query.join.map((joinClause) => ({\n type: joinClause.type,\n left: joinClause.left,\n right: joinClause.right,\n from:\n joinClause.from.type === `collectionRef`\n ? new CollectionRefClass(\n joinClause.from.collection,\n joinClause.from.alias\n )\n : new QueryRefClass(\n deepCopyQuery(joinClause.from.query),\n joinClause.from.alias\n ),\n }))\n : undefined,\n where: query.where ? [...query.where] : undefined,\n groupBy: query.groupBy ? [...query.groupBy] : undefined,\n having: query.having ? [...query.having] : undefined,\n orderBy: query.orderBy ? [...query.orderBy] : undefined,\n limit: query.limit,\n offset: query.offset,\n fnSelect: query.fnSelect,\n fnWhere: query.fnWhere ? [...query.fnWhere] : undefined,\n fnHaving: query.fnHaving ? [...query.fnHaving] : undefined,\n }\n}\n\n/**\n * Helper function to optimize a FROM clause while tracking what was actually optimized.\n *\n * @param from - FROM clause to optimize\n * @param singleSourceClauses - Map of source aliases to their WHERE clauses\n * @param actuallyOptimized - Set to track which sources were actually optimized\n * @returns New FROM clause, potentially wrapped in a subquery\n */\nfunction optimizeFromWithTracking(\n from: From,\n singleSourceClauses: Map<string, BasicExpression<boolean>>,\n actuallyOptimized: Set<string>\n): From {\n const whereClause = singleSourceClauses.get(from.alias)\n\n if (!whereClause) {\n // No optimization needed, but return a copy to maintain immutability\n if (from.type === `collectionRef`) {\n return new CollectionRefClass(from.collection, from.alias)\n }\n // Must be queryRef due to type system\n return new QueryRefClass(deepCopyQuery(from.query), from.alias)\n }\n\n if (from.type === `collectionRef`) {\n // Create a new subquery with the WHERE clause for the collection\n // This is always safe since we're creating a new subquery\n const subQuery: QueryIR = {\n from: new CollectionRefClass(from.collection, from.alias),\n where: [whereClause],\n }\n actuallyOptimized.add(from.alias) // Mark as successfully optimized\n return new QueryRefClass(subQuery, from.alias)\n }\n\n // Must be queryRef due to type system\n\n // SAFETY CHECK: Only check safety when pushing WHERE clauses into existing subqueries\n // We need to be careful about pushing WHERE clauses into subqueries that already have\n // aggregates, HAVING, or ORDER BY + LIMIT since that could change their semantics\n if (!isSafeToPushIntoExistingSubquery(from.query)) {\n // Return a copy without optimization to maintain immutability\n // Do NOT mark as optimized since we didn't actually optimize it\n return new QueryRefClass(deepCopyQuery(from.query), from.alias)\n }\n\n // Add the WHERE clause to the existing subquery\n // Create a deep copy to ensure immutability\n const existingWhere = from.query.where || []\n const optimizedSubQuery: QueryIR = {\n ...deepCopyQuery(from.query),\n where: [...existingWhere, whereClause],\n }\n actuallyOptimized.add(from.alias) // Mark as successfully optimized\n return new QueryRefClass(optimizedSubQuery, from.alias)\n}\n\n/**\n * Determines if it's safe to push WHERE clauses into an existing subquery.\n *\n * Pushing WHERE clauses into existing subqueries can break semantics in several cases:\n *\n * 1. **Aggregates**: Pushing predicates before GROUP BY changes what gets aggregated\n * 2. **ORDER BY + LIMIT/OFFSET**: Pushing predicates before sorting+limiting changes the result set\n * 3. **HAVING clauses**: These operate on aggregated data, predicates should not be pushed past them\n * 4. **Functional operations**: fnSelect, fnWhere, fnHaving could have side effects\n *\n * Note: This safety check only applies when pushing WHERE clauses into existing subqueries.\n * Creating new subqueries from collection references is always safe.\n *\n * @param query - The existing subquery to check for safety\n * @returns True if it's safe to push WHERE clauses into this subquery, false otherwise\n *\n * @example\n * ```typescript\n * // UNSAFE: has GROUP BY - pushing WHERE could change aggregation\n * { from: users, groupBy: [dept], select: { count: agg('count', '*') } }\n *\n * // UNSAFE: has ORDER BY + LIMIT - pushing WHERE could change \"top 10\"\n * { from: users, orderBy: [salary desc], limit: 10 }\n *\n * // SAFE: plain SELECT without aggregates/limits\n * { from: users, select: { id, name } }\n * ```\n */\nfunction isSafeToPushIntoExistingSubquery(query: QueryIR): boolean {\n // Check for aggregates in SELECT clause\n if (query.select) {\n const hasAggregates = Object.values(query.select).some(\n (expr) => expr.type === `agg`\n )\n if (hasAggregates) {\n return false\n }\n }\n\n // Check for GROUP BY clause\n if (query.groupBy && query.groupBy.length > 0) {\n return false\n }\n\n // Check for HAVING clause\n if (query.having && query.having.length > 0) {\n return false\n }\n\n // Check for ORDER BY with LIMIT or OFFSET (dangerous combination)\n if (query.orderBy && query.orderBy.length > 0) {\n if (query.limit !== undefined || query.offset !== undefined) {\n return false\n }\n }\n\n // Check for functional variants that might have side effects\n if (\n query.fnSelect ||\n (query.fnWhere && query.fnWhere.length > 0) ||\n (query.fnHaving && query.fnHaving.length > 0)\n ) {\n return false\n }\n\n // If none of the unsafe conditions are present, it's safe to optimize\n return true\n}\n\n/**\n * Helper function to combine multiple expressions with AND.\n *\n * If there's only one expression, it's returned as-is.\n * If there are multiple expressions, they're combined with an AND function.\n *\n * @param expressions - Array of expressions to combine\n * @returns Single expression representing the AND combination\n * @throws Error if the expressions array is empty\n */\nfunction combineWithAnd(\n expressions: Array<BasicExpression<boolean>>\n): BasicExpression<boolean> {\n if (expressions.length === 0) {\n throw new Error(`Cannot combine empty expression list`)\n }\n\n if (expressions.length === 1) {\n return expressions[0]!\n }\n\n // Create an AND function with all expressions as arguments\n return new Func(`and`, expressions)\n}\n"],"names":["deepEquals","QueryRefClass","CollectionRefClass","Func"],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { BasicExpression, QueryIR } from './ir.js';
|
|
2
|
+
/**
|
|
3
|
+
* Represents a WHERE clause after source analysis
|
|
4
|
+
*/
|
|
5
|
+
export interface AnalyzedWhereClause {
|
|
6
|
+
/** The WHERE expression */
|
|
7
|
+
expression: BasicExpression<boolean>;
|
|
8
|
+
/** Set of table/source aliases that this WHERE clause touches */
|
|
9
|
+
touchedSources: Set<string>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Represents WHERE clauses grouped by the sources they touch
|
|
13
|
+
*/
|
|
14
|
+
export interface GroupedWhereClauses {
|
|
15
|
+
/** WHERE clauses that touch only a single source, grouped by source alias */
|
|
16
|
+
singleSource: Map<string, BasicExpression<boolean>>;
|
|
17
|
+
/** WHERE clauses that touch multiple sources, combined into one expression */
|
|
18
|
+
multiSource?: BasicExpression<boolean>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Main query optimizer entry point that lifts WHERE clauses into subqueries.
|
|
22
|
+
*
|
|
23
|
+
* This function implements multi-level predicate pushdown optimization by recursively
|
|
24
|
+
* moving WHERE clauses through nested subqueries to get them as close to the data
|
|
25
|
+
* sources as possible, then removing redundant subqueries.
|
|
26
|
+
*
|
|
27
|
+
* @param query - The QueryIR to optimize
|
|
28
|
+
* @returns A new QueryIR with optimizations applied (or original if no optimization possible)
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* const originalQuery = {
|
|
33
|
+
* from: new CollectionRef(users, 'u'),
|
|
34
|
+
* join: [{ from: new CollectionRef(posts, 'p'), ... }],
|
|
35
|
+
* where: [eq(u.dept_id, 1), gt(p.views, 100)]
|
|
36
|
+
* }
|
|
37
|
+
*
|
|
38
|
+
* const optimized = optimizeQuery(originalQuery)
|
|
39
|
+
* // Result: Single-source clauses moved to deepest possible subqueries
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare function optimizeQuery(query: QueryIR): QueryIR;
|