UNPKG

rawsql-ts

Version:

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

589 lines 25.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CTEQueryDecomposer = void 0; const SimpleSelectQuery_1 = require("../models/SimpleSelectQuery"); const CTEDependencyAnalyzer_1 = require("./CTEDependencyAnalyzer"); const CTECollector_1 = require("./CTECollector"); const SqlFormatter_1 = require("./SqlFormatter"); const CommentEditor_1 = require("../utils/CommentEditor"); const SelectQueryParser_1 = require("../parsers/SelectQueryParser"); const CTEComposer_1 = require("./CTEComposer"); /** * Decomposes complex CTEs into executable standalone queries * * This class analyzes Common Table Expressions and generates executable standalone queries * for each CTE, making complex CTE debugging easier. It supports: * - Recursive CTE detection and handling * - Dependency analysis (dependencies and dependents for each CTE) * - CTE SQL Restoration: Generate executable SQL for a specific CTE with its dependencies * - Configurable SQL formatter options (MySQL, PostgreSQL, custom styles) * - Optional comment generation showing CTE metadata and relationships * - Comprehensive error handling for circular dependencies * * @example * ```typescript * const decomposer = new CTEQueryDecomposer({ * preset: 'postgres', * addComments: true, * keywordCase: 'upper' * }); * * const query = ` * with users_data as (select * from users), * active_users as (select * from users_data where active = true) * select * from active_users * `; * * const decomposed = decomposer.decompose(SelectQueryParser.parse(query)); * // Returns array of DecomposedCTE objects with executable queries * * // Or restore a specific CTE for debugging: * const restored = decomposer.extractCTE(SelectQueryParser.parse(query), 'active_users'); * console.log(restored.executableSql); // Standalone executable SQL with dependencies * ``` * * @public */ class CTEQueryDecomposer { /** * Creates a new CTEQueryDecomposer instance * @param options - Configuration options extending SqlFormatterOptions */ constructor(options = {}) { this.options = options; this.dependencyAnalyzer = new CTEDependencyAnalyzer_1.CTEDependencyAnalyzer(); this.cteCollector = new CTECollector_1.CTECollector(); this.formatter = new SqlFormatter_1.SqlFormatter(options); } /** * Decomposes CTEs in a query into executable standalone queries * * This method analyzes the query structure to: * 1. Collect all CTEs and analyze their dependencies * 2. Detect recursive CTEs and handle them separately * 3. Generate executable queries for each CTE including required dependencies * 4. Add optional comments with metadata (if addComments option is enabled) * 5. Format output according to specified formatter options * * @param query - The SimpleSelectQuery containing CTEs to decompose * @returns Array of decomposed CTEs with executable queries, dependencies, and metadata * @throws Error if circular dependencies are detected in non-recursive CTEs * * @example * ```typescript * const query = SelectQueryParser.parse(` * with base as (select * from users), * filtered as (select * from base where active = true) * select * from filtered * `); * * const result = decomposer.decompose(query); * // Returns: * // [ * // { name: 'base', query: 'select * from users', dependencies: [], ... }, * // { name: 'filtered', query: 'with base as (...) select * from base where active = true', dependencies: ['base'], ... } * // ] * ``` */ decompose(query) { const ctes = this.cteCollector.collect(query); if (ctes.length === 0) { return []; } const recursiveCTEs = this.findRecursiveCTEs(query, ctes); const dependencyGraph = this.dependencyAnalyzer.analyzeDependencies(query); this.validateCircularDependencies(recursiveCTEs.length > 0); return this.processCTENodes(query, dependencyGraph.nodes, recursiveCTEs); } /** * Synchronizes edited CTEs back into a unified query and re-decomposes them * * This method resolves inconsistencies between edited CTEs by: * 1. Composing the edited CTEs into a unified query * 2. Parsing the unified query to ensure consistency * 3. Re-decomposing the synchronized query * * This is useful when CTEs have been edited independently and may have * inconsistencies that need to be resolved through a unified composition. * * @param editedCTEs - Array of edited CTEs that may have inconsistencies * @param rootQuery - The main query that uses the CTEs * @returns Array of re-decomposed CTEs with resolved inconsistencies * @throws Error if the composed query cannot be parsed or contains errors * * @example * ```typescript * // After editing CTEs independently, synchronize them * const editedCTEs = [ * { name: 'users_data', query: 'select * from users where active = true' }, * { name: 'active_users', query: 'select * from users_data where id >= 1000' } * ]; * * const synchronized = decomposer.synchronize(editedCTEs, 'select count(*) from active_users'); * // Returns re-decomposed CTEs with resolved dependencies * ``` */ synchronize(editedCTEs, rootQuery) { if (editedCTEs.length === 0) { return []; } // Flatten nested WITH clauses by extracting sub-CTEs from edited CTEs const flattenedCTEs = this.flattenNestedWithClauses(editedCTEs); // Use CTEComposer to create a unified query, sharing formatter options const composerOptions = { ...this.options, // Remove decomposer-specific options addComments: undefined }; const composer = new CTEComposer_1.CTEComposer(composerOptions); const unifiedQuery = composer.compose(flattenedCTEs, rootQuery); // Parse the unified query const parsedQuery = SelectQueryParser_1.SelectQueryParser.parse(unifiedQuery); // Re-decompose the synchronized query return this.decompose(parsedQuery); } /** * Restores executable SQL for a specific CTE by including all its dependencies * * This method provides a focused API for generating standalone, executable SQL * for a specific Common Table Expression. It analyzes dependencies and includes * all required CTEs in the correct execution order. * * Key features: * - Automatic dependency resolution and ordering * - Recursive CTE detection and handling * - Error handling for circular dependencies * - Optional dependency comments for debugging * * @param query - The query containing CTEs * @param cteName - The name of the CTE to restore * @returns CTERestorationResult with executable SQL and metadata * @throws Error if CTE is not found or circular dependencies exist * * @example * ```typescript * const query = SelectQueryParser.parse(` * with users_data as (select * from users), * active_users as (select * from users_data where active = true), * premium_users as (select * from active_users where premium = true) * select * from premium_users * `); * * // Get executable SQL for 'premium_users' CTE * const result = decomposer.extractCTE(query, 'premium_users'); * // result.executableSql will contain: * // with users_data as (select * from users), * // active_users as (select * from users_data where active = true) * // select * from active_users where premium = true * ``` */ extractCTE(query, cteName) { const warnings = []; // Validate query contains CTEs const allCTEs = this.cteCollector.collect(query); if (allCTEs.length === 0) { throw new Error("Query does not contain any CTEs"); } // Find target CTE const targetCTE = allCTEs.find(cte => this.getCTEName(cte) === cteName); if (!targetCTE) { throw new Error(`CTE not found in query: ${cteName}`); } // Check if this is a recursive CTE const recursiveCTEs = this.findRecursiveCTEs(query, allCTEs); const isRecursive = recursiveCTEs.includes(cteName); if (isRecursive) { warnings.push("Recursive CTE restoration requires the full query context"); // For recursive CTEs, return the entire query as dependencies are complex return { name: cteName, executableSql: this.formatter.format(query).formattedSql, dependencies: this.getAllCTENames(allCTEs).filter(name => name !== cteName), warnings }; } // Analyze dependencies const dependencyGraph = this.dependencyAnalyzer.analyzeDependencies(query); // Check for circular dependencies if (this.dependencyAnalyzer.hasCircularDependency()) { throw new Error("Circular dependency detected in CTEs"); } // Find target node in dependency graph const targetNode = dependencyGraph.nodes.find(node => node.name === cteName); if (!targetNode || !targetNode.cte) { throw new Error(`CTE not found in dependency graph: ${cteName}`); } // Build executable query using existing private method const executableSql = this.buildExecutableQuery(targetNode, dependencyGraph.nodes); // Collect all required dependencies in correct order const requiredCTEs = this.collectRequiredCTEs(targetNode, dependencyGraph.nodes); const allDependencies = requiredCTEs.map(node => node.name); // Add comments if requested const finalSql = this.options.addComments ? this.addRestorationComments(executableSql, targetNode, warnings) : executableSql; return { name: cteName, executableSql: finalSql, dependencies: allDependencies, warnings }; } /** * Flattens nested WITH clauses by extracting sub-CTEs from edited CTEs * @param editedCTEs Array of edited CTEs that may contain nested WITH clauses * @returns Flattened array of CTEs with sub-CTEs extracted to top level */ flattenNestedWithClauses(editedCTEs) { const flattened = []; const extractedCTEs = new Map(); // name -> query mapping for extracted CTEs for (const editedCTE of editedCTEs) { try { // Check if this CTE's query contains a WITH clause const withPattern = /^\s*with\s+/i; if (withPattern.test(editedCTE.query)) { // Parse the query to extract nested CTEs const parsed = SelectQueryParser_1.SelectQueryParser.parse(editedCTE.query); if (parsed.withClause && parsed.withClause.tables) { // Extract each nested CTE for (const nestedCTE of parsed.withClause.tables) { const cteName = this.getCTEName(nestedCTE); if (!extractedCTEs.has(cteName)) { // Format the nested CTE query const cteFormatter = new SqlFormatter_1.SqlFormatter({ identifierEscape: { start: "", end: "" } }); const nestedQuery = cteFormatter.format(nestedCTE.query).formattedSql; extractedCTEs.set(cteName, nestedQuery); } } // Create the main query without the WITH clause const mainQueryWithoutWith = new SimpleSelectQuery_1.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 // Remove WITH clause }); const mainFormatter = new SqlFormatter_1.SqlFormatter({ identifierEscape: { start: "", end: "" } }); const mainQuery = mainFormatter.format(mainQueryWithoutWith).formattedSql; flattened.push({ name: editedCTE.name, query: mainQuery }); } else { // WITH clause exists but no tables found, preserve as-is flattened.push(editedCTE); } } else { // No WITH clause, preserve as-is flattened.push(editedCTE); } } catch (error) { // If parsing fails, preserve the original CTE flattened.push(editedCTE); } } // Add extracted CTEs at the beginning (they need to be defined first) const result = []; for (const [name, query] of extractedCTEs) { result.push({ name, query }); } result.push(...flattened); return result; } /** * Validates circular dependencies for non-recursive CTEs * @param hasRecursiveCTEs Whether the query contains recursive CTEs * @throws Error if circular dependencies exist in non-recursive CTEs */ validateCircularDependencies(hasRecursiveCTEs) { if (this.dependencyAnalyzer.hasCircularDependency() && !hasRecursiveCTEs) { throw new Error(CTEQueryDecomposer.ERROR_MESSAGES.CIRCULAR_REFERENCE); } } /** * Processes CTE nodes and generates decomposed CTEs * @param query Original query * @param nodes CTE dependency nodes * @param recursiveCTEs List of recursive CTE names * @returns Array of decomposed CTEs */ processCTENodes(query, nodes, recursiveCTEs) { const result = []; for (const node of nodes) { // Skip ROOT nodes (main query) as they are not CTEs if (node.type === 'ROOT') { continue; } const isRecursive = recursiveCTEs.includes(node.name); if (isRecursive) { result.push(this.createRecursiveCTE(node, query)); } else { result.push(this.createStandardCTE(node, nodes)); } } return result; } /** * Creates a decomposed CTE for recursive CTEs */ createRecursiveCTE(node, query) { const formattedQuery = this.formatter.format(query).formattedSql; // Filter out MAIN_QUERY from dependents for DecomposedCTE result const cteDependents = node.dependents.filter(dep => dep !== 'MAIN_QUERY'); const finalQuery = this.addCommentsToQuery(formattedQuery, node.name, node.dependencies, node.dependents, true); return { name: node.name, query: finalQuery, dependencies: [...node.dependencies], dependents: cteDependents, isRecursive: true }; } /** * Creates a decomposed CTE for standard (non-recursive) CTEs */ createStandardCTE(node, allNodes) { const query = this.buildExecutableQuery(node, allNodes); const finalQuery = this.addCommentsToQuery(query, node.name, node.dependencies, node.dependents, false); // Filter out MAIN_QUERY from dependents for DecomposedCTE result const cteDependents = node.dependents.filter(dep => dep !== 'MAIN_QUERY'); return { name: node.name, query: finalQuery, dependencies: [...node.dependencies], dependents: cteDependents, isRecursive: false }; } /** * Builds an executable query for a CTE by including its dependencies */ buildExecutableQuery(targetNode, allNodes) { // ROOT nodes don't have a cte property if (targetNode.type === 'ROOT' || !targetNode.cte) { throw new Error(`Cannot build executable query for ROOT node: ${targetNode.name}`); } const requiredCTEs = this.collectRequiredCTEs(targetNode, allNodes); if (requiredCTEs.length === 0) { // No dependencies, just return the CTE's query return this.formatter.format(targetNode.cte.query).formattedSql; } // Build WITH clause with required CTEs const withClause = this.buildWithClause(requiredCTEs); const mainQuery = this.formatter.format(targetNode.cte.query).formattedSql; return `${withClause} ${mainQuery}`; } /** * Collects all required CTEs for a target CTE in dependency order */ collectRequiredCTEs(targetNode, allNodes) { const visited = new Set(); const result = []; const nodeMap = new Map(); // Build node lookup map for (const node of allNodes) { nodeMap.set(node.name, node); } const collectDependencies = (nodeName) => { if (visited.has(nodeName)) return; visited.add(nodeName); const node = nodeMap.get(nodeName); if (!node) return; // First collect all dependencies for (const depName of node.dependencies) { collectDependencies(depName); } // Then add this node (ensuring dependencies come first) // Skip ROOT nodes and the target node itself if (nodeName !== targetNode.name && node.type !== 'ROOT') { result.push(node); } }; // Collect dependencies for the target node for (const depName of targetNode.dependencies) { collectDependencies(depName); } return result; } /** * Builds WITH clause from required CTEs */ buildWithClause(requiredCTEs) { if (requiredCTEs.length === 0) return ""; const cteDefinitions = requiredCTEs.map(node => { // Skip ROOT nodes as they don't have CTE definitions if (node.type === 'ROOT' || !node.cte) { throw new Error(`Cannot include ROOT node in WITH clause: ${node.name}`); } const cteName = node.name; const cteQuery = this.formatter.format(node.cte.query).formattedSql; return `${cteName} as (${cteQuery})`; }); return `with ${cteDefinitions.join(", ")}`; } /** * Finds recursive CTEs in the query * @param query The query to analyze * @param ctes All CTEs in the query * @returns Array of recursive CTE names */ findRecursiveCTEs(query, ctes) { if (!query.withClause || !this.isRecursiveWithClause(query)) { return []; } // For now, assume all CTEs in a RECURSIVE WITH are recursive // This is a simplification - in reality, only some CTEs might be recursive return ctes.map(cte => this.getCTEName(cte)); } /** * Checks if a WITH clause is recursive * @param query The query to check * @returns true if the query contains WITH RECURSIVE */ isRecursiveWithClause(query) { const queryText = this.formatter.format(query).formattedSql.toLowerCase(); return queryText.includes('with recursive'); } /** * Adds comments to the query if addComments option is enabled * @param query The query string to add comments to * @param cteName The name of the CTE * @param dependencies Array of dependency names * @param dependents Array of dependent names * @param isRecursive Whether this is a recursive CTE * @returns Query with comments added if enabled, otherwise original query */ addCommentsToQuery(query, cteName, dependencies, dependents, isRecursive) { if (this.options.addComments !== true) { return query; } try { // Parse the query to add comments to its AST const parsedQuery = SelectQueryParser_1.SelectQueryParser.parse(query); // Generate comment lines const comments = this.generateComments(cteName, dependencies, dependents, isRecursive); // Add comments to the root query (this should place them at the beginning) comments.forEach(comment => { CommentEditor_1.CommentEditor.addComment(parsedQuery, comment); }); // Format with comment export enabled const formatterWithComments = new SqlFormatter_1.SqlFormatter({ ...this.options, exportComment: true }); return formatterWithComments.format(parsedQuery).formattedSql; } catch (error) { // If parsing fails, return original query with simple text comments console.warn(`${CTEQueryDecomposer.ERROR_MESSAGES.PARSING_FAILED}: ${error}`); return this.addTextCommentsToQuery(query, cteName, dependencies, dependents, isRecursive); } } /** * Generates comment lines for a CTE * @param cteName The name of the CTE * @param dependencies Array of dependency names * @param dependents Array of dependent names * @param isRecursive Whether this is a recursive CTE * @returns Array of comment strings */ generateComments(cteName, dependencies, dependents, isRecursive) { const { AUTO_GENERATED, ORIGINAL_CTE, DEPENDENCIES, DEPENDENTS, RECURSIVE_TYPE, NONE } = CTEQueryDecomposer.COMMENT_TEXTS; const comments = []; comments.push(AUTO_GENERATED); comments.push(`${ORIGINAL_CTE} ${cteName}`); if (isRecursive) { comments.push(RECURSIVE_TYPE); } const depsText = dependencies.length > 0 ? dependencies.join(", ") : NONE; comments.push(`${DEPENDENCIES} ${depsText}`); // Filter out MAIN_QUERY from dependents for display purposes const cteDependents = dependents.filter(dep => dep !== 'MAIN_QUERY'); const dependentsText = cteDependents.length > 0 ? cteDependents.join(", ") : NONE; comments.push(`${DEPENDENTS} ${dependentsText}`); return comments; } /** * Adds text comments as SQL comments when AST parsing fails * @param query Original query * @param cteName The name of the CTE * @param dependencies Array of dependency names * @param dependents Array of dependent names * @param isRecursive Whether this is a recursive CTE * @returns Query with text comments prepended */ addTextCommentsToQuery(query, cteName, dependencies, dependents, isRecursive) { const comments = this.generateComments(cteName, dependencies, dependents, isRecursive); const commentLines = comments.map(comment => `-- ${comment}`).join('\n'); return `${commentLines}\n${query}`; } /** * Adds restoration comments to the executable SQL if enabled * @param sql The executable SQL * @param targetNode The target CTE node * @param warnings Any warnings to include * @returns SQL with comments added */ addRestorationComments(sql, targetNode, warnings) { const comments = []; comments.push("-- CTE Restoration: " + targetNode.name); if (targetNode.dependencies.length > 0) { comments.push("-- Dependencies: " + targetNode.dependencies.join(", ")); } else { comments.push("-- Dependencies: none"); } if (warnings.length > 0) { comments.push("-- Warnings: " + warnings.join(", ")); } comments.push("-- Generated by CTEQueryDecomposer.extractCTE()"); comments.push(""); return comments.join("\n") + sql; } /** * Gets all CTE names from an array of CTEs * @param ctes Array of CommonTable objects * @returns Array of CTE names */ getAllCTENames(ctes) { return ctes.map(cte => this.getCTEName(cte)); } /** * Extracts the name from a CommonTable */ getCTEName(cte) { return cte.aliasExpression.table.name; } } exports.CTEQueryDecomposer = CTEQueryDecomposer; CTEQueryDecomposer.ERROR_MESSAGES = { CIRCULAR_REFERENCE: "Circular reference detected in non-recursive CTEs", PARSING_FAILED: "Failed to parse query for comment injection" }; CTEQueryDecomposer.COMMENT_TEXTS = { AUTO_GENERATED: "Auto-generated by CTE decomposer", ORIGINAL_CTE: "Original CTE:", DEPENDENCIES: "Dependencies:", DEPENDENTS: "Dependents:", RECURSIVE_TYPE: "Type: Recursive CTE", NONE: "none" }; //# sourceMappingURL=CTEQueryDecomposer.js.map