rawsql-ts
Version:
High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.
298 lines • 12.7 kB
JavaScript
import { SimpleSelectQuery } from "../models/SimpleSelectQuery";
import { SelectQueryParser } from "../parsers/SelectQueryParser";
import { SqlFormatter } from "./SqlFormatter";
import { SqlSchemaValidator } from "../utils/SqlSchemaValidator";
import { CTEDependencyAnalyzer } from "./CTEDependencyAnalyzer";
/**
* Composes edited CTEs back into a unified SQL query
*
* Takes CTEs that were individually edited after decomposition and reconstructs them
* into a proper WITH clause structure. This completes the CTE debugging workflow:
* 1. Use CTEQueryDecomposer to break down complex CTEs
* 2. Edit individual CTEs to fix issues
* 3. Use CTEComposer to reconstruct the unified query
*
* @example
* ```typescript
* // After decomposing and editing CTEs
* const composer = new CTEComposer({
* preset: 'postgres',
* validateSchema: true,
* schema: { users: ['id', 'name', 'active'] }
* });
*
* const editedCTEs = [
* { name: 'base_data', query: 'select * from users where active = true' },
* { name: 'filtered_data', query: 'select * from base_data where region = "US"' }
* ];
*
* const composedSQL = composer.compose(editedCTEs, 'select * from filtered_data');
* // Dependencies are automatically analyzed and sorted
* // Result: "with base_data as (...), filtered_data as (...) select * from filtered_data"
* ```
*
* @public
*/
export class CTEComposer {
/**
* Creates a new CTEComposer instance
* @param options - Configuration options extending SqlFormatterOptions
*/
constructor(options = {}) {
this.knownCTENames = [];
this.options = options;
this.formatter = new SqlFormatter(options);
this.dependencyAnalyzer = new CTEDependencyAnalyzer();
}
/**
* Compose edited CTEs and root query into a unified SQL query
*
* This method:
* 1. Extracts pure SELECT queries from edited CTEs (removes any WITH clauses)
* 2. Builds a temporary query to analyze dependencies automatically
* 3. Sorts CTEs by dependency order using topological sort
* 4. Validates schema if options.validateSchema is enabled
* 5. Applies formatter options for consistent output
* 6. Constructs the final WITH clause with proper recursive handling
*
* @param editedCTEs - Array of edited CTEs with name and query only
* @param rootQuery - The main query that uses the CTEs (without WITH clause)
* @returns Composed SQL query with properly structured WITH clause
* @throws Error if schema validation fails or circular dependencies are detected
*
* @example
* ```typescript
* const editedCTEs = [
* { name: 'base_data', query: 'select * from users where active = true' },
* { name: 'filtered_data', query: 'select * from base_data where region = "US"' }
* ];
*
* const result = composer.compose(editedCTEs, 'select count(*) from filtered_data');
* // Dependencies automatically analyzed and sorted
* // Result: "with base_data as (...), filtered_data as (...) select count(*) from filtered_data"
* ```
*/
compose(editedCTEs, rootQuery) {
if (editedCTEs.length === 0) {
return rootQuery;
}
// Set known CTE names for WITH clause filtering
this.knownCTENames = editedCTEs.map(cte => cte.name);
// Extract pure queries and detect recursion
const pureQueries = editedCTEs.map(cte => ({
name: cte.name,
query: this.extractPureQuery(cte.query, cte.name)
}));
// Build temporary query to analyze dependencies
const tempQuery = this.buildTempQueryForAnalysis(pureQueries, rootQuery);
// Analyze dependencies
const dependencyGraph = this.dependencyAnalyzer.analyzeDependencies(tempQuery);
// Sort CTEs by dependencies
const sortedCTEs = this.sortCTEsByDependencies(pureQueries, dependencyGraph);
// Check for recursive CTEs by analyzing original queries
const isRecursive = this.detectRecursiveFromOriginalQueries(editedCTEs);
// Special handling for recursive CTEs: Current implementation preserves recursive CTEs as-is
// This is expected behavior for now as recursive CTEs require complex handling
// Build WITH clause with sorted CTEs
const cteDefinitions = sortedCTEs.map(cte => {
return `${cte.name} as (${cte.query})`;
});
const withKeyword = isRecursive ? "with recursive" : "with";
const composedQuery = `${withKeyword} ${cteDefinitions.join(", ")} ${rootQuery}`;
// Validate schema if requested
if (this.options.validateSchema && this.options.schema) {
this.validateComposedQuery(composedQuery);
}
// Apply formatting if options specify it
return this.formatFinalQuery(composedQuery);
}
/**
* Extract pure SELECT query from a query that may contain WITH clause
* If query contains WITH with known CTEs, extract main SELECT and ignore old definitions
* If query contains WITH with unknown CTEs, preserve entire query
* @param query The query that may contain WITH clause
* @param cteName The name of the CTE to extract (if query contains WITH)
* @returns Pure SELECT query without WITH clause, or entire query if it contains new sub-CTEs
*/
extractPureQuery(query, cteName) {
// Simple regex to check if query starts with WITH
const withPattern = /^\s*with\s+/i;
if (!withPattern.test(query)) {
return query;
}
// Check if this is a recursive CTE by looking for "WITH RECURSIVE"
const recursivePattern = /^\s*with\s+recursive\s+/i;
if (recursivePattern.test(query)) {
// For recursive CTEs, preserve the entire query as-is
return query;
}
// Parse the query to check what CTEs are defined in the WITH clause
try {
const parsed = SelectQueryParser.parse(query);
if (parsed.withClause && parsed.withClause.tables) {
// Check if WITH clause contains only known CTEs from our composition
const knownCTENames = this.getKnownCTENames();
const withCTENames = parsed.withClause.tables.map(cte => this.getCTEName(cte));
// If all CTEs in WITH clause are known (old definitions), extract main SELECT
const hasOnlyKnownCTEs = withCTENames.every(name => knownCTENames.includes(name));
if (hasOnlyKnownCTEs) {
// Remove WITH clause and format just the main query
const queryWithoutWith = new SimpleSelectQuery({
selectClause: parsed.selectClause,
fromClause: parsed.fromClause,
whereClause: parsed.whereClause,
groupByClause: parsed.groupByClause,
havingClause: parsed.havingClause,
orderByClause: parsed.orderByClause,
windowClause: parsed.windowClause,
limitClause: parsed.limitClause,
offsetClause: parsed.offsetClause,
fetchClause: parsed.fetchClause,
forClause: parsed.forClause,
withClause: undefined // withClause removed
});
// Use a formatter without quotes for extraction to preserve original format
const extractFormatter = new SqlFormatter({
identifierEscape: { start: "", end: "" }
});
return extractFormatter.format(queryWithoutWith).formattedSql;
}
}
}
catch (error) {
// If parsing fails, fall through to preserve entire query
}
// If query contains WITH clause with unknown CTEs, preserve it like a recursive CTE
// This handles the case where user edited a CTE and added new sub-CTEs
return query;
}
/**
* Get list of known CTE names from current composition context
*/
getKnownCTENames() {
// This will be set during composition to track known CTE names
return this.knownCTENames || [];
}
/**
* Extract CTE name from CommonTable
*/
getCTEName(cte) {
return cte.aliasExpression.table.name;
}
/**
* Extract CTE definition using regex as fallback
*/
extractCTEWithRegex(query, cteName) {
// More robust regex to extract CTE definition
// Pattern: cteName as (...) accounting for nested parentheses
const ctePattern = new RegExp(`${cteName}\\s+as\\s*\\(`, 'i');
const match = query.match(ctePattern);
if (!match) {
return query;
}
// Find the start of the CTE definition (after "as (")
const startIndex = match.index + match[0].length;
// Use parentheses counting to find the end of the CTE definition
let parenCount = 1;
let endIndex = startIndex;
while (endIndex < query.length && parenCount > 0) {
const char = query[endIndex];
if (char === '(') {
parenCount++;
}
else if (char === ')') {
parenCount--;
}
endIndex++;
}
if (parenCount === 0) {
// Extract the content between parentheses
const cteDefinition = query.substring(startIndex, endIndex - 1).trim();
return cteDefinition;
}
return query;
}
/**
* Build a temporary query for dependency analysis
*/
buildTempQueryForAnalysis(pureQueries, rootQuery) {
const cteDefinitions = pureQueries.map(cte => `${cte.name} as (${cte.query})`);
const tempSql = `with ${cteDefinitions.join(", ")} ${rootQuery}`;
try {
return SelectQueryParser.parse(tempSql);
}
catch (error) {
throw new Error(`Failed to parse temporary query for dependency analysis: ${error}`);
}
}
/**
* Sort CTEs by dependencies using dependency graph
*/
sortCTEsByDependencies(pureQueries, dependencyGraph) {
// Create a map for quick lookup
const queryMap = new Map();
pureQueries.forEach(cte => queryMap.set(cte.name, cte.query));
// Return CTEs in dependency order
return dependencyGraph.nodes.map(node => ({
name: node.name,
query: queryMap.get(node.name) || ""
})).filter(cte => cte.query !== "");
}
/**
* Detect if any CTEs are recursive by analyzing original queries
*/
detectRecursiveFromOriginalQueries(editedCTEs) {
// Check if any of the original queries contain "with recursive"
return editedCTEs.some(cte => {
const queryLower = cte.query.toLowerCase();
return queryLower.includes('with recursive');
});
}
/**
* Detect if any CTEs are recursive
*/
detectRecursiveCTEs(query) {
if (!query.withClause)
return false;
// Check if the query text contains "recursive"
const queryText = this.formatter.format(query).formattedSql.toLowerCase();
return queryText.includes('with recursive');
}
/**
* Validate the composed query against schema
*/
validateComposedQuery(composedQuery) {
try {
const parsed = SelectQueryParser.parse(composedQuery);
// Convert schema to TableSchema format
const tableSchemas = Object.entries(this.options.schema).map(([name, columns]) => ({
name,
columns
}));
SqlSchemaValidator.validate(parsed, tableSchemas);
}
catch (error) {
if (error instanceof Error) {
throw new Error(`Schema validation failed: ${error.message}`);
}
throw error;
}
}
/**
* Apply formatting to the final query
*/
formatFinalQuery(composedQuery) {
if (this.options.preset || this.options.keywordCase) {
try {
const parsed = SelectQueryParser.parse(composedQuery);
return this.formatter.format(parsed).formattedSql;
}
catch (error) {
// If parsing fails, return unformatted query
return composedQuery;
}
}
return composedQuery;
}
}
//# sourceMappingURL=CTEComposer.js.map