UNPKG

rawsql-ts

Version:

[beta]High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.

497 lines 20.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.JoinAggregationDecomposer = exports.DecompositionError = void 0; exports.analyzeJoinAggregation = analyzeJoinAggregation; exports.decomposeJoinAggregation = decomposeJoinAggregation; const SimpleSelectQuery_1 = require("../models/SimpleSelectQuery"); const Clause_1 = require("../models/Clause"); const ValueComponent_1 = require("../models/ValueComponent"); const SqlFormatter_1 = require("./SqlFormatter"); /** * Error thrown when query decomposition fails */ class DecompositionError extends Error { constructor(message, originalQuery, cause) { super(message); this.originalQuery = originalQuery; this.cause = cause; this.name = 'DecompositionError'; } } exports.DecompositionError = DecompositionError; /** * Decomposes queries that combine table joins with aggregations into separate detail and aggregation queries using CTEs * * This transformer separates JOIN operations from aggregation operations to make queries easier to debug: * - Detail query: Contains JOINs and column selection * - Aggregation query: Contains GROUP BY and aggregation functions, referencing the CTE * * Provides two patterns following existing codebase conventions: * - analyze(): Safe analysis (Result pattern like SelectQueryParser.analyze) * - decompose(): Direct decomposition with exceptions (Exception pattern like SelectQueryParser.parse) * * @example * ```typescript * const decomposer = new JoinAggregationDecomposer(); * * // Safe analysis (Result pattern) * const analysis = decomposer.analyze(query); * if (analysis.success) { * console.log('Can decompose with', analysis.metadata.joinCount, 'joins'); * if (analysis.limitations) { * console.log('Known limitations:', analysis.limitations); * } * } else { * console.log('Cannot decompose:', analysis.error); * } * * // Direct decomposition (Exception pattern) * try { * const decomposed = decomposer.decompose(query); * // Success: decomposed query ready to use * } catch (error) { * if (error instanceof DecompositionError) { * console.log('Decomposition failed:', error.message); * } * } * ``` */ class JoinAggregationDecomposer { constructor(options = {}) { this.options = { detailCTEName: options.detailCTEName || "detail_data" }; this.formatter = new SqlFormatter_1.SqlFormatter({ identifierEscape: { start: "", end: "" } }); } /** * Analyzes a query for decomposition without throwing errors (safe analysis) * Follows the same pattern as SelectQueryParser.analyze() * * @param query The query to analyze * @returns Analysis result with success status, error information, and metadata */ analyze(query) { const metadata = this.extractMetadata(query); try { // Phase 1: Validate query structure const validationError = this.getValidationError(query, metadata); if (validationError) { return { success: false, error: validationError, metadata }; } // Phase 2: Attempt decomposition const decomposed = this.performDecomposition(query); // Phase 3: Check for formatting issues (critical validation) try { this.formatter.format(decomposed); } catch (formatError) { return { success: false, error: `Decomposed query cannot be formatted: ${formatError instanceof Error ? formatError.message : String(formatError)}. This usually indicates complex expressions in aggregations that are not supported.`, metadata }; } // Check for known limitations const limitations = this.detectLimitations(metadata); return { success: true, decomposedQuery: decomposed, limitations: limitations.length > 0 ? limitations : undefined, metadata }; } catch (error) { return { success: false, error: `Analysis failed: ${error instanceof Error ? error.message : String(error)}`, metadata }; } } /** * Decomposes a JOIN + aggregation query into separate detail and aggregation queries * Follows the same pattern as SelectQueryParser.parse() - throws on error * * @param query The query to decompose * @returns The decomposed query with CTE structure * @throws DecompositionError if the query cannot be decomposed or formatted */ decompose(query) { try { // Phase 1: Validate query structure const metadata = this.extractMetadata(query); const validationError = this.getValidationError(query, metadata); if (validationError) { throw new DecompositionError(validationError, query); } // Phase 2: Perform decomposition const decomposed = this.performDecomposition(query); // Phase 3: Critical validation - ensure the result can be formatted try { this.formatter.format(decomposed); } catch (formatError) { throw new DecompositionError(`Decomposed query cannot be formatted: ${formatError instanceof Error ? formatError.message : String(formatError)}. ` + `This usually indicates complex expressions in aggregations that are not supported.`, query, formatError instanceof Error ? formatError : undefined); } return decomposed; } catch (error) { if (error instanceof DecompositionError) { throw error; } throw new DecompositionError(`Decomposition failed: ${error instanceof Error ? error.message : String(error)}`, query, error instanceof Error ? error : undefined); } } /** * Gets validation error message without throwing (for analyze method) */ getValidationError(query, metadata) { if (!query.fromClause) { return "Query does not contain FROM clause"; } if (metadata.joinCount === 0) { return "Query does not contain JOINs"; } if (metadata.aggregationCount === 0 && !query.groupByClause) { return "Query does not contain GROUP BY or aggregation functions"; } if (metadata.hasWindowFunctions) { return "Window functions are not fully supported - column references in window functions are not converted to CTE references"; } return null; } /** * Performs the actual decomposition */ performDecomposition(query) { // Extract columns needed for detail CTE const detailColumns = this.extractDetailColumns(query); // Build detail query (CTE) const detailQuery = this.buildDetailQuery(query, detailColumns); // Build aggregation query const aggregationQuery = this.buildAggregationQuery(query); // Create WITH clause const withClause = new Clause_1.WithClause(false, // not recursive [ new Clause_1.CommonTable(detailQuery, this.options.detailCTEName, null // not materialized ) ]); // Combine into final query aggregationQuery.withClause = withClause; return aggregationQuery; } /** * Extracts metadata about the query */ extractMetadata(query) { const joinCount = this.countJoins(query); const aggregationCount = this.countAggregationFunctions(query); const hasHaving = !!query.havingClause; const hasOrderBy = !!query.orderByClause; const hasWindowFunctions = this.hasWindowFunctions(query); const detailColumns = this.extractDetailColumnNames(query); return { joinCount, aggregationCount, detailColumns, hasHaving, hasOrderBy, hasWindowFunctions }; } /** * Detects known limitations based on metadata */ detectLimitations(metadata) { const limitations = []; if (metadata.hasWindowFunctions && metadata.aggregationCount > 0) { limitations.push("Window functions may reference original table columns instead of CTE columns"); } if (metadata.hasHaving) { limitations.push("HAVING clause column references are not converted to CTE references"); } if (metadata.hasOrderBy) { limitations.push("ORDER BY clause column references are not converted to CTE references"); } return limitations; } /** * Counts the number of JOINs in the query */ countJoins(query) { var _a, _b; return ((_b = (_a = query.fromClause) === null || _a === void 0 ? void 0 : _a.joins) === null || _b === void 0 ? void 0 : _b.length) || 0; } /** * Counts aggregation functions in the query */ countAggregationFunctions(query) { var _a; let count = 0; if ((_a = query.selectClause) === null || _a === void 0 ? void 0 : _a.items) { for (const item of query.selectClause.items) { if (this.containsAggregationFunction(item.value)) { count++; } } } return count; } /** * Checks if query contains window functions */ hasWindowFunctions(query) { var _a; if ((_a = query.selectClause) === null || _a === void 0 ? void 0 : _a.items) { for (const item of query.selectClause.items) { if (this.containsWindowFunction(item.value)) { return true; } } } return false; } /** * Checks if an expression contains aggregation functions */ containsAggregationFunction(expression) { if (expression instanceof ValueComponent_1.FunctionCall) { const funcName = this.getFunctionName(expression).toLowerCase(); return ['count', 'sum', 'avg', 'min', 'max'].includes(funcName); } return false; } /** * Checks if an expression contains window functions */ containsWindowFunction(expression) { if (expression instanceof ValueComponent_1.FunctionCall && expression.over) { return true; } // Fallback: check for common window function names if (expression instanceof ValueComponent_1.FunctionCall) { const funcName = this.getFunctionName(expression).toLowerCase(); return ['row_number', 'rank', 'dense_rank', 'lead', 'lag'].includes(funcName); } return false; } /** * Gets function name from FunctionCall */ getFunctionName(func) { const name = func.qualifiedName.name; if (name instanceof ValueComponent_1.IdentifierString) { return name.name; } else { return name.value; } } /** * Extracts detail column names for metadata */ extractDetailColumnNames(query) { var _a, _b; const columns = []; // Add GROUP BY columns if ((_a = query.groupByClause) === null || _a === void 0 ? void 0 : _a.grouping) { for (const expr of query.groupByClause.grouping) { columns.push(expr.toString()); } } // Add columns from aggregation functions if ((_b = query.selectClause) === null || _b === void 0 ? void 0 : _b.items) { for (const item of query.selectClause.items) { if (this.containsAggregationFunction(item.value) && item.value instanceof ValueComponent_1.FunctionCall) { if (item.value.argument) { columns.push(item.value.argument.toString()); } } } } return [...new Set(columns)]; // Remove duplicates } /** * Extracts columns needed for the detail CTE */ extractDetailColumns(query) { var _a, _b; const columns = []; const columnSet = new Set(); // Add GROUP BY columns if ((_a = query.groupByClause) === null || _a === void 0 ? void 0 : _a.grouping) { for (const expr of query.groupByClause.grouping) { if (expr instanceof ValueComponent_1.ColumnReference) { const key = this.getColumnKey(expr); if (!columnSet.has(key)) { columns.push(expr); columnSet.add(key); } } } } // Add columns from aggregation function arguments if ((_b = query.selectClause) === null || _b === void 0 ? void 0 : _b.items) { for (const item of query.selectClause.items) { this.extractColumnsFromExpression(item.value, columns, columnSet); } } return columns; } /** * Extracts column references from an expression */ extractColumnsFromExpression(expression, columns, columnSet) { if (expression instanceof ValueComponent_1.FunctionCall) { if (expression.argument) { if (expression.argument instanceof ValueComponent_1.ColumnReference) { const key = this.getColumnKey(expression.argument); if (!columnSet.has(key)) { columns.push(expression.argument); columnSet.add(key); } } else if (expression.argument.toString() === '*') { // Handle COUNT(*) by adding special marker const starColumn = new ValueComponent_1.ColumnReference(null, '*'); const key = this.getColumnKey(starColumn); if (!columnSet.has(key)) { columns.push(starColumn); columnSet.add(key); } } } } else if (expression instanceof ValueComponent_1.ColumnReference) { const key = this.getColumnKey(expression); if (!columnSet.has(key)) { columns.push(expression); columnSet.add(key); } } } /** * Gets a unique key for a column reference */ getColumnKey(column) { var _a; const namespace = ((_a = column.namespaces) === null || _a === void 0 ? void 0 : _a.map(ns => ns.name).join('.')) || ''; const columnName = column.column.name; return namespace ? `${namespace}.${columnName}` : columnName; } /** * Builds the detail query (CTE content) */ buildDetailQuery(originalQuery, detailColumns) { const selectItems = detailColumns.map(col => new Clause_1.SelectItem(col)); return new SimpleSelectQuery_1.SimpleSelectQuery({ selectClause: new Clause_1.SelectClause(selectItems), fromClause: originalQuery.fromClause, whereClause: originalQuery.whereClause }); } /** * Builds the aggregation query that references the CTE */ buildAggregationQuery(originalQuery) { var _a, _b, _c, _d; // Transform SELECT items to reference CTE columns const transformedSelectItems = ((_b = (_a = originalQuery.selectClause) === null || _a === void 0 ? void 0 : _a.items) === null || _b === void 0 ? void 0 : _b.map(item => { var _a; const transformedExpression = this.transformExpressionForCTE(item.value); return new Clause_1.SelectItem(transformedExpression, ((_a = item.identifier) === null || _a === void 0 ? void 0 : _a.name) || null); })) || []; // Transform GROUP BY to reference CTE columns const transformedGroupBy = (_d = (_c = originalQuery.groupByClause) === null || _c === void 0 ? void 0 : _c.grouping) === null || _d === void 0 ? void 0 : _d.map(expr => { return this.transformExpressionForCTE(expr); }); // Create FROM clause that references the CTE const cteFromClause = new Clause_1.FromClause(new Clause_1.SourceExpression(new Clause_1.TableSource(null, new ValueComponent_1.IdentifierString(this.options.detailCTEName)), null), null); return new SimpleSelectQuery_1.SimpleSelectQuery({ selectClause: new Clause_1.SelectClause(transformedSelectItems), fromClause: cteFromClause, groupByClause: transformedGroupBy ? new Clause_1.GroupByClause(transformedGroupBy) : undefined, havingClause: originalQuery.havingClause, // TODO: Transform references if needed orderByClause: originalQuery.orderByClause // TODO: Transform references if needed }); } /** * Transforms an expression to reference CTE columns instead of original table columns */ transformExpressionForCTE(expression) { if (expression instanceof ValueComponent_1.FunctionCall) { // Transform aggregation function arguments const transformedArg = expression.argument ? (expression.argument instanceof ValueComponent_1.ColumnReference ? // Convert table.column to just column for CTE reference new ValueComponent_1.ColumnReference(null, expression.argument.column.name) : expression.argument) : null; return new ValueComponent_1.FunctionCall(expression.qualifiedName.namespaces, expression.qualifiedName.name, transformedArg, expression.over, expression.withinGroup); } else if (expression instanceof ValueComponent_1.ColumnReference) { // Convert table.column to just column for CTE reference return new ValueComponent_1.ColumnReference(null, expression.column.name); } return expression; } } exports.JoinAggregationDecomposer = JoinAggregationDecomposer; /** * Utility function to analyze a JOIN + aggregation query from SQL string (safe, no exceptions) * * @param sql The SQL string to parse and analyze * @param options Decomposer options * @returns Analysis result with success status, error information, and metadata */ function analyzeJoinAggregation(sql, options) { try { // Import using ES module syntax to avoid require issues const { SelectQueryParser } = eval('require("../parsers/SelectQueryParser")'); const query = SelectQueryParser.parse(sql); const decomposer = new JoinAggregationDecomposer(options); return decomposer.analyze(query); } catch (error) { return { success: false, error: `Failed to parse SQL: ${error instanceof Error ? error.message : String(error)}`, metadata: { joinCount: 0, aggregationCount: 0, detailColumns: [], hasHaving: false, hasOrderBy: false, hasWindowFunctions: false } }; } } /** * Utility function to decompose a JOIN + aggregation query from SQL string * * @param sql The SQL string to parse and decompose * @param options Decomposer options * @returns The decomposed query * @throws DecompositionError if parsing or decomposition fails */ function decomposeJoinAggregation(sql, options) { try { // Import using ES module syntax to avoid require issues const { SelectQueryParser } = eval('require("../parsers/SelectQueryParser")'); const query = SelectQueryParser.parse(sql); const decomposer = new JoinAggregationDecomposer(options); return decomposer.decompose(query); } catch (error) { if (error instanceof DecompositionError) { throw error; } throw new DecompositionError(`Failed to parse SQL: ${error instanceof Error ? error.message : String(error)}`, new SimpleSelectQuery_1.SimpleSelectQuery({ selectClause: new Clause_1.SelectClause([]) }), error instanceof Error ? error : undefined); } } //# sourceMappingURL=JoinAggregationDecomposer.js.map