UNPKG

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