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
JavaScript
"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