rawsql-ts
Version:
[beta]High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.
484 lines • 20.6 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.CTERenamer = void 0;
const SimpleSelectQuery_1 = require("../models/SimpleSelectQuery");
const BinarySelectQuery_1 = require("../models/BinarySelectQuery");
const CTEDependencyAnalyzer_1 = require("./CTEDependencyAnalyzer");
const TableSourceCollector_1 = require("./TableSourceCollector");
const ColumnReferenceCollector_1 = require("./ColumnReferenceCollector");
const ValueComponent_1 = require("../models/ValueComponent");
const LexemeCursor_1 = require("../utils/LexemeCursor");
const SelectQueryParser_1 = require("../parsers/SelectQueryParser");
const Lexeme_1 = require("../models/Lexeme");
const SqlFormatter_1 = require("./SqlFormatter");
const KeywordParser_1 = require("../parsers/KeywordParser");
const CommandTokenReader_1 = require("../tokenReaders/CommandTokenReader");
/**
* 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
*/
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_1.CTEDependencyAnalyzer();
this.columnReferenceCollector = new ColumnReferenceCollector_1.ColumnReferenceCollector();
this.tableSourceCollector = new TableSourceCollector_1.TableSourceCollector(); // Use default selectableOnly=true to avoid infinite recursion
this.keywordParser = new KeywordParser_1.KeywordParser(CommandTokenReader_1.commandKeywordTrie);
}
/**
* 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_1.SimpleSelectQuery) {
this.renameInSimpleQuery(query, sanitizedOldName, sanitizedNewName);
}
else if (query instanceof BinarySelectQuery_1.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_1.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_1.SimpleSelectQuery) {
// For SimpleSelectQuery, only update references (not CTE definitions)
this.updateAllReferences(query, oldName, newName);
}
else if (query instanceof BinarySelectQuery_1.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 ValueComponent_1.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);
}
}
}
/**
* GUI-integrated CTE renaming with line/column position support.
*
* Designed for editor integration where users can right-click on CTE names
* and rename them. Automatically detects the CTE name at the cursor position
* and performs the rename operation.
*
* @param sql - The complete SQL string containing CTE definitions
* @param position - Line and column position where the user clicked (1-based)
* @param newName - The new name for the CTE
* @returns The updated SQL string with the CTE renamed
*
* @throws {Error} When no CTE name is found at the specified position
* @throws {Error} When the new name conflicts with existing CTE names
*
* @example
* ```typescript
* const sql = `
* WITH user_data AS (SELECT * FROM users),
* order_data AS (SELECT * FROM orders)
* SELECT * FROM user_data JOIN order_data ON ...
* `;
*
* const renamer = new CTERenamer();
* // User right-clicks on 'user_data' at line 2, column 8
* const result = renamer.renameCTEAtPosition(sql, { line: 2, column: 8 }, 'customer_data');
* console.log(result);
* // Returns SQL with 'user_data' renamed to 'customer_data' everywhere
* ```
*/
renameCTEAtPosition(sql, position, newName) {
// Input validation
if (!(sql === null || sql === void 0 ? void 0 : sql.trim())) {
throw new Error('SQL cannot be empty');
}
if (!position || position.line < 1 || position.column < 1) {
throw new Error('Position must be a valid line/column (1-based)');
}
if (!(newName === null || newName === void 0 ? void 0 : newName.trim())) {
throw new Error('New CTE name cannot be empty');
}
// Find the lexeme at the specified position
const lexeme = LexemeCursor_1.LexemeCursor.findLexemeAtLineColumn(sql, position);
if (!lexeme) {
throw new Error(`No CTE name found at line ${position.line}, column ${position.column}`);
}
const cteName = lexeme.value;
// Parse the SQL to get the query structure
const query = SelectQueryParser_1.SelectQueryParser.parse(sql);
// First check if this is actually a CTE name in the query
if (!this.isCTENameInQuery(query, cteName)) {
throw new Error(`'${cteName}' is not a CTE name in this query`);
}
// Validate that the lexeme is a valid identifier or function (not a command/keyword)
if (!(lexeme.type & (Lexeme_1.TokenType.Identifier | Lexeme_1.TokenType.Function))) {
throw new Error(`Token at position is not a CTE name: '${lexeme.value}'`);
}
// Check for naming conflicts
const conflicts = this.checkNameConflicts(query, newName, cteName);
if (conflicts.length > 0) {
throw new Error(conflicts.join(', '));
}
// Perform the CTE rename operation
this.renameCTE(query, cteName, newName);
// Return the formatted SQL using SqlFormatter
const formatter = new SqlFormatter_1.SqlFormatter();
const result = formatter.format(query);
return result.formattedSql;
}
/**
* Check for naming conflicts with existing CTEs and reserved keywords.
* @private
*/
checkNameConflicts(query, newName, currentName) {
const conflicts = [];
// Skip checks if the name isn't actually changing
if (currentName === newName) {
return conflicts;
}
// Check for reserved keyword conflicts
conflicts.push(...this.checkKeywordConflicts(newName));
// Check for existing CTE name conflicts
if (this.isCTENameInQuery(query, newName)) {
conflicts.push(`CTE name '${newName}' already exists`);
}
return conflicts;
}
/**
* Checks if the new name conflicts with SQL keywords using the existing KeywordTrie.
* @private
*/
checkKeywordConflicts(newName) {
const conflicts = [];
try {
// Use the KeywordParser to check if the new name is a reserved keyword
const keywordResult = this.keywordParser.parse(newName, 0);
if (keywordResult !== null && keywordResult.keyword.toLowerCase() === newName.toLowerCase()) {
conflicts.push(`'${newName}' is a reserved SQL keyword and should not be used as a CTE name`);
}
}
catch (error) {
console.warn(`Failed to check keyword conflicts for '${newName}':`, error);
// Fallback to basic check if KeywordParser fails
if (this.isBasicReservedKeyword(newName)) {
conflicts.push(`'${newName}' is a reserved SQL keyword and should not be used as a CTE name`);
}
}
return conflicts;
}
/**
* Fallback method for basic reserved keyword checking.
* @private
*/
isBasicReservedKeyword(name) {
const basicKeywords = ['select', 'from', 'where', 'with', 'as', 'union', 'join', 'table', 'null'];
return basicKeywords.includes(name.toLowerCase());
}
/**
* Check if a CTE name exists in the query.
* @private
*/
isCTENameInQuery(query, cteName) {
if (query instanceof SimpleSelectQuery_1.SimpleSelectQuery && query.withClause) {
return query.withClause.tables.some(cte => cte.aliasExpression && cte.aliasExpression.table && cte.aliasExpression.table.name === cteName);
}
if (query instanceof BinarySelectQuery_1.BinarySelectQuery) {
return this.isCTENameInQuery(query.left, cteName) ||
this.isCTENameInQuery(query.right, cteName);
}
return false;
}
}
exports.CTERenamer = CTERenamer;
//# sourceMappingURL=CTERenamer.js.map
;