UNPKG

rawsql-ts

Version:

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

342 lines 14 kB
import { SimpleSelectQuery } from "../models/SimpleSelectQuery"; import { BinarySelectQuery } from "../models/BinarySelectQuery"; import { CTEDependencyAnalyzer } from "./CTEDependencyAnalyzer"; import { TableSourceCollector } from "./TableSourceCollector"; import { ColumnReferenceCollector } from "./ColumnReferenceCollector"; import { ColumnReference } from "../models/ValueComponent"; /** * Error messages for CTE renaming operations. */ const ERROR_MESSAGES = { nullQuery: 'Query cannot be null or undefined', invalidOldName: 'Old CTE name must be a non-empty string', invalidNewName: 'New CTE name must be a non-empty string', sameNames: 'Old and new CTE names cannot be the same', unsupportedQuery: 'Unsupported query type for CTE renaming', cteNotExists: (name) => `CTE '${name}' does not exist`, cteAlreadyExists: (name) => `CTE '${name}' already exists`, cteNotFound: (name) => `CTE '${name}' not found`, }; /** * A utility class for renaming Common Table Expressions (CTEs) in SQL queries. * * This class provides functionality to safely rename CTEs while automatically updating * all column references and table references throughout the query, including within * nested CTE definitions and subqueries. * * @example * ```typescript * import { CTERenamer, SelectQueryParser } from 'rawsql-ts'; * * const sql = ` * WITH user_data AS ( * SELECT id, name FROM users * ), * order_summary AS ( * SELECT user_data.id, COUNT(*) as order_count * FROM user_data * JOIN orders ON user_data.id = orders.user_id * GROUP BY user_data.id * ) * SELECT * FROM order_summary * `; * * const query = SelectQueryParser.parse(sql); * const renamer = new CTERenamer(); * * // Rename 'user_data' to 'customer_data' * renamer.renameCTE(query, 'user_data', 'customer_data'); * * // All references are automatically updated: * // - CTE definition: WITH customer_data AS (...) * // - Column references: customer_data.id * // - Table references: FROM customer_data * ``` * * @example * ```typescript * // Error handling * try { * renamer.renameCTE(query, 'nonexistent_cte', 'new_name'); * } catch (error) { * console.error(error.message); // "CTE 'nonexistent_cte' does not exist" * } * * try { * renamer.renameCTE(query, 'existing_cte', 'already_exists'); * } catch (error) { * console.error(error.message); // "CTE 'already_exists' already exists" * } * ``` * * @since 0.11.16 */ export class CTERenamer { /** * Creates a new instance of CTERenamer. * * The constructor initializes internal collectors and analyzers needed for * comprehensive CTE renaming operations. */ constructor() { this.dependencyAnalyzer = new CTEDependencyAnalyzer(); this.columnReferenceCollector = new ColumnReferenceCollector(); this.tableSourceCollector = new TableSourceCollector(); // Use default selectableOnly=true to avoid infinite recursion } /** * Renames a Common Table Expression (CTE) and updates all references to it. * * This method performs a comprehensive rename operation that includes: * - Updating the CTE definition name in the WITH clause * - Updating all column references (e.g., `old_name.column` → `new_name.column`) * - Updating all table references in FROM and JOIN clauses * - Processing references within nested CTEs and subqueries * * @param query - The SQL query containing the CTE to rename. Can be either SimpleSelectQuery or BinarySelectQuery (UNION/INTERSECT/EXCEPT). * @param oldName - The current name of the CTE to rename. * @param newName - The new name for the CTE. * * @throws {Error} When the specified CTE does not exist in the query. * @throws {Error} When a CTE with the new name already exists. * @throws {Error} When the query type is not supported (not a SelectQuery). * * @example * ```typescript * const renamer = new CTERenamer(); * * // Basic usage * renamer.renameCTE(query, 'old_cte_name', 'new_cte_name'); * * // With error handling * try { * renamer.renameCTE(query, 'user_data', 'customer_data'); * } catch (error) { * if (error.message.includes('does not exist')) { * console.log('CTE not found'); * } else if (error.message.includes('already exists')) { * console.log('Name conflict'); * } * } * ``` * * @since 0.11.16 */ renameCTE(query, oldName, newName) { // Input validation this.validateInputs(query, oldName, newName); // Sanitize input names const sanitizedOldName = oldName.trim(); const sanitizedNewName = newName.trim(); if (query instanceof SimpleSelectQuery) { this.renameInSimpleQuery(query, sanitizedOldName, sanitizedNewName); } else if (query instanceof BinarySelectQuery) { this.renameInBinaryQuery(query, sanitizedOldName, sanitizedNewName); } else { throw new Error(ERROR_MESSAGES.unsupportedQuery); } } /** * Validates input parameters for CTE renaming. */ validateInputs(query, oldName, newName) { if (!query) { throw new Error(ERROR_MESSAGES.nullQuery); } if (!oldName || typeof oldName !== 'string' || oldName.trim() === '') { throw new Error(ERROR_MESSAGES.invalidOldName); } if (!newName || typeof newName !== 'string' || newName.trim() === '') { throw new Error(ERROR_MESSAGES.invalidNewName); } if (oldName.trim() === newName.trim()) { throw new Error(ERROR_MESSAGES.sameNames); } } /** * Handles CTE renaming for SimpleSelectQuery. */ renameInSimpleQuery(query, oldName, newName) { // Get available CTE names const availableCTEs = query.getCTENames(); // Check if CTE exists if (!availableCTEs.includes(oldName)) { throw new Error(ERROR_MESSAGES.cteNotExists(oldName)); } // Check for name conflicts if (availableCTEs.includes(newName)) { throw new Error(ERROR_MESSAGES.cteAlreadyExists(newName)); } // Rename CTE definition this.renameCTEDefinition(query, oldName, newName); // Update all references this.updateAllReferences(query, oldName, newName); } /** * Handles CTE renaming for BinarySelectQuery. */ renameInBinaryQuery(query, oldName, newName) { // Use toSimpleQuery() only for WITH clause inspection (not for writing back) const withClauseQuery = query.toSimpleQuery(); // Get available CTE names from the converted query let availableCTEs = []; if (withClauseQuery.withClause && withClauseQuery.withClause.tables) { availableCTEs = withClauseQuery.withClause.tables.map(cte => cte.aliasExpression.table.name); } // Check if CTE exists if (!availableCTEs.includes(oldName)) { throw new Error(ERROR_MESSAGES.cteNotExists(oldName)); } // Check for name conflicts if (availableCTEs.includes(newName)) { throw new Error(ERROR_MESSAGES.cteAlreadyExists(newName)); } // Rename CTE definition in the converted query (this affects the original BinarySelectQuery) this.renameCTEDefinition(withClauseQuery, oldName, newName); // Add withClause to original BinarySelectQuery and left query for proper formatting if (withClauseQuery.withClause) { query.withClause = withClauseQuery.withClause; // Also add to left query so formatter can display it if (query.left instanceof SimpleSelectQuery) { query.left.withClause = withClauseQuery.withClause; } } // Recursively update references in left and right branches this.renameInSelectQuery(query.left, oldName, newName); this.renameInSelectQuery(query.right, oldName, newName); } /** * Recursively handles CTE renaming for any SelectQuery type. */ renameInSelectQuery(query, oldName, newName) { if (query instanceof SimpleSelectQuery) { // For SimpleSelectQuery, only update references (not CTE definitions) this.updateAllReferences(query, oldName, newName); } else if (query instanceof BinarySelectQuery) { // Recursively process left and right branches this.renameInSelectQuery(query.left, oldName, newName); this.renameInSelectQuery(query.right, oldName, newName); } // ValuesQuery: do nothing } /** * Renames the CTE definition in the WITH clause. */ renameCTEDefinition(query, oldName, newName) { if (!query.withClause || !query.withClause.tables) { throw new Error(ERROR_MESSAGES.cteNotFound(oldName)); } const cteToRename = query.withClause.tables.find(cte => cte.aliasExpression.table.name === oldName); if (!cteToRename) { throw new Error(ERROR_MESSAGES.cteNotFound(oldName)); } cteToRename.aliasExpression.table.name = newName; } /** * Updates all references to the old CTE name (column references and table sources). */ updateAllReferences(query, oldName, newName) { // Collect all column references from the query (including CTE internals) const columnReferences = this.columnReferenceCollector.collect(query); // Update all column references that reference the old CTE name for (const columnRef of columnReferences) { // Check namespaces for the old CTE name if (columnRef.namespaces && columnRef.namespaces.length > 0) { // Check if any namespace matches the old CTE name for (const namespace of columnRef.namespaces) { if (namespace.name === oldName) { namespace.name = newName; break; } } } } // Update table sources in the main query const tableSources = this.tableSourceCollector.collect(query); for (const tableSource of tableSources) { if (tableSource.getSourceName() === oldName) { if (tableSource.qualifiedName.name instanceof ColumnReference) { // Handle ColumnReference case if needed } else if ('name' in tableSource.qualifiedName.name) { // Handle IdentifierString tableSource.qualifiedName.name.name = newName; } else { // Handle RawString tableSource.qualifiedName.name.value = newName; } } } // Update table sources that reference the old CTE name within CTEs this.updateTableSourcesInCTEs(query, oldName, newName); } /** * Updates table sources within CTE definitions that reference the old CTE name. * This method manually traverses CTE internals to avoid infinite recursion * that occurs when using TableSourceCollector with selectableOnly=false. */ updateTableSourcesInCTEs(query, oldName, newName) { if (!query.withClause || !query.withClause.tables) { return; } // Traverse each CTE and update table sources in their FROM clauses for (const cte of query.withClause.tables) { this.updateTableSourcesInQuery(cte.query, oldName, newName); } } /** * Updates table sources in a specific query (used for CTE internals). */ updateTableSourcesInQuery(query, oldName, newName) { // Update FROM clause if (query.fromClause && query.fromClause.source.datasource) { this.updateTableSource(query.fromClause.source.datasource, oldName, newName); } // Update JOIN clauses if (query.fromClause && query.fromClause.joins) { for (const join of query.fromClause.joins) { if (join.source.datasource) { this.updateTableSource(join.source.datasource, oldName, newName); } } } } /** * Updates a specific table source if it matches the old CTE name. */ updateTableSource(datasource, oldName, newName) { // Type guard and null checks for security if (!datasource || typeof datasource !== 'object') { return; } const source = datasource; // Safely check if this is a TableSource if (typeof source.getSourceName === 'function') { try { const sourceName = source.getSourceName(); if (sourceName === oldName && source.qualifiedName && typeof source.qualifiedName === 'object') { const qualifiedName = source.qualifiedName; if (qualifiedName.name && typeof qualifiedName.name === 'object') { const nameObj = qualifiedName.name; if ('name' in nameObj && typeof nameObj.name === 'string') { // Handle IdentifierString nameObj.name = newName; } else if ('value' in nameObj && typeof nameObj.value === 'string') { // Handle RawString nameObj.value = newName; } } } } catch (error) { // Safely handle any unexpected errors during table source update console.warn('Warning: Failed to update table source:', error); } } } } //# sourceMappingURL=CTERenamer.js.map