UNPKG

rawsql-ts

Version:

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

600 lines 24.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AliasRenamer = void 0; const SelectQuery_1 = require("../models/SelectQuery"); const LexemeCursor_1 = require("../utils/LexemeCursor"); const Lexeme_1 = require("../models/Lexeme"); const SelectQueryParser_1 = require("../parsers/SelectQueryParser"); const CTERegionDetector_1 = require("../utils/CTERegionDetector"); const TableSourceCollector_1 = require("./TableSourceCollector"); const ColumnReferenceCollector_1 = require("./ColumnReferenceCollector"); const KeywordParser_1 = require("../parsers/KeywordParser"); const CommandTokenReader_1 = require("../tokenReaders/CommandTokenReader"); /** * Error messages for alias renaming operations */ const ERROR_MESSAGES = { invalidSql: 'Invalid SQL: unable to parse query', invalidPosition: 'Invalid position: line or column out of bounds', noLexemeAtPosition: 'No lexeme found at the specified position', notAnAlias: 'Selected lexeme is not a valid alias', invalidNewName: 'New alias name must be a non-empty string', sameNames: 'Old and new alias names cannot be the same', nameConflict: (name) => `Alias '${name}' already exists in this scope`, aliasNotFound: (name) => `Alias '${name}' not found in current scope`, }; /** * A utility class for renaming table and column aliases in SQL queries. * * This class provides functionality to rename aliases within specific scopes * (CTE, subquery, or main query) based on cursor position from GUI editors. * It automatically detects the appropriate scope and updates all references * to the alias within that scope boundary. * * @example * ```typescript * import { AliasRenamer } from 'rawsql-ts'; * * const sql = ` * SELECT u.name, o.date * FROM users u * JOIN orders o ON u.id = o.user_id * `; * * const renamer = new AliasRenamer(); * * // Rename 'u' to 'user_alias' by selecting it at line 2, column 10 * const result = renamer.renameAlias(sql, { line: 2, column: 10 }, 'user_alias'); * * if (result.success) { * console.log(result.newSql); * // SELECT user_alias.name, o.date * // FROM users user_alias * // JOIN orders o ON user_alias.id = o.user_id * } * ``` * * Related tests: packages/core/tests/transformers/AliasRenamer.functional.test.ts * @since 0.12.0 */ class AliasRenamer { /** * Creates a new instance of AliasRenamer. */ constructor() { // Initialize keyword parser for reserved word detection using shared trie this.keywordParser = new KeywordParser_1.KeywordParser(CommandTokenReader_1.commandKeywordTrie); } /** * Renames an alias based on the cursor position in GUI editor. * * This method detects the alias at the specified line and column position, * determines its scope (CTE, subquery, or main query), and renames all * references to that alias within the scope boundaries. * * @param sql - The SQL string containing the alias to rename * @param position - Line and column position (1-based) from GUI editor * @param newName - The new name for the alias * @param options - Optional configuration for the rename operation * @returns Result containing success status, modified SQL, and change details * * @example * ```typescript * const sql = "SELECT u.name FROM users u WHERE u.active = true"; * const result = renamer.renameAlias(sql, { line: 1, column: 8 }, 'user_table'); * * if (result.success) { * console.log(result.newSql); * // "SELECT user_table.name FROM users user_table WHERE user_table.active = true" * } * ``` * * @throws {Error} When the SQL cannot be parsed or position is invalid */ renameAlias(sql, position, newName, options = {}) { try { // Input validation this.validateInputs(sql, position, newName); // Find lexeme at the specified position const lexeme = LexemeCursor_1.LexemeCursor.findLexemeAtLineColumn(sql, position); if (!lexeme) { throw new Error(ERROR_MESSAGES.noLexemeAtPosition); } // Validate that the lexeme is a valid alias this.validateLexemeIsAlias(lexeme); // Parse SQL to get AST const query = SelectQueryParser_1.SelectQueryParser.parse(sql); // Detect the scope containing this alias const scope = this.detectAliasScope(sql, query, lexeme, options.scopeType); // Find all references to this alias within the scope const references = this.collectAliasReferences(scope, lexeme.value); // Check for naming conflicts const conflicts = this.checkNameConflicts(scope, newName, lexeme.value); if (conflicts.length > 0) { return { success: false, originalSql: sql, changes: [], conflicts, scope }; } // Prepare changes const changes = this.prepareChanges(references, newName); // If dry run, return without making changes if (options.dryRun) { return { success: true, originalSql: sql, changes, conflicts, scope }; } // Perform the actual renaming using lexeme-based approach for better accuracy const newSql = this.performLexemeBasedRename(sql, lexeme.value, newName, scope); return { success: true, originalSql: sql, newSql, changes, scope }; } catch (error) { return { success: false, originalSql: sql, changes: [], conflicts: [error instanceof Error ? error.message : String(error)] }; } } /** * Validates input parameters for alias renaming. */ validateInputs(sql, position, newName) { if (!sql || typeof sql !== 'string' || sql.trim() === '') { throw new Error(ERROR_MESSAGES.invalidSql); } if (!position || typeof position.line !== 'number' || typeof position.column !== 'number' || position.line < 1 || position.column < 1) { throw new Error(ERROR_MESSAGES.invalidPosition); } if (!newName || typeof newName !== 'string' || newName.trim() === '') { throw new Error(ERROR_MESSAGES.invalidNewName); } } /** * Validates that the lexeme represents a valid alias. */ validateLexemeIsAlias(lexeme) { // Check if lexeme is an identifier (potential alias) if (!(lexeme.type & Lexeme_1.TokenType.Identifier)) { throw new Error(ERROR_MESSAGES.notAnAlias); } } /** * Detects the scope (CTE, subquery, main query) containing the alias. */ detectAliasScope(sql, query, lexeme, scopeType) { if (!lexeme.position) { // Fallback to main query if no position info return { type: 'main', query, startPosition: 0, endPosition: sql.length }; } const lexemePosition = lexeme.position.startPosition; // Force specific scope type if requested if (scopeType && scopeType !== 'auto') { return this.createScopeForType(scopeType, sql, query, lexemePosition); } // Auto-detect scope based on position return this.autoDetectScope(sql, query, lexemePosition); } /** * Creates scope for a specific requested type. */ createScopeForType(scopeType, sql, query, position) { switch (scopeType) { case 'cte': return this.detectCTEScope(sql, query, position); case 'subquery': return this.detectSubqueryScope(sql, query, position); case 'main': default: return { type: 'main', query, startPosition: 0, endPosition: sql.length }; } } /** * Auto-detects the most appropriate scope based on cursor position. */ autoDetectScope(sql, query, position) { // First check if we're in a CTE const cteScope = this.detectCTEScope(sql, query, position); if (cteScope.type === 'cte') { return cteScope; } // Then check for subqueries (implementation needed) const subqueryScope = this.detectSubqueryScope(sql, query, position); if (subqueryScope.type === 'subquery') { return subqueryScope; } // Default to main query return { type: 'main', query, startPosition: 0, endPosition: sql.length }; } /** * Detects if the position is within a CTE and returns appropriate scope. */ detectCTEScope(sql, query, position) { try { // Use CTERegionDetector to analyze cursor position const analysis = CTERegionDetector_1.CTERegionDetector.analyzeCursorPosition(sql, position); if (analysis.isInCTE && analysis.cteRegion) { // Find the corresponding CTE query in the AST const cteQuery = this.findCTEQueryByName(query, analysis.cteRegion.name); return { type: 'cte', name: analysis.cteRegion.name, query: cteQuery || query, // Fallback to main query if CTE not found startPosition: analysis.cteRegion.startPosition, endPosition: analysis.cteRegion.endPosition }; } } catch (error) { // If CTE detection fails, fall back to main query console.warn('CTE scope detection failed:', error); } // Not in CTE, return main query scope return { type: 'main', query, startPosition: 0, endPosition: sql.length }; } /** * Detects if the position is within a subquery scope. */ detectSubqueryScope(sql, query, position) { // TODO: Implement subquery detection // This would involve traversing the AST to find nested SELECT queries // and determining if the position falls within their boundaries // For now, return main query scope return { type: 'main', query, startPosition: 0, endPosition: sql.length }; } /** * Finds a CTE query by name within the parsed AST. */ findCTEQueryByName(query, cteName) { var _a; if (query instanceof SelectQuery_1.SimpleSelectQuery && ((_a = query.withClause) === null || _a === void 0 ? void 0 : _a.tables)) { for (const cte of query.withClause.tables) { if (cte.aliasExpression.table.name === cteName) { return cte.query; } } } else if (query instanceof SelectQuery_1.BinarySelectQuery) { // Check left side first const leftResult = this.findCTEQueryByName(query.left, cteName); if (leftResult) return leftResult; // Then check right side const rightResult = this.findCTEQueryByName(query.right, cteName); if (rightResult) return rightResult; } return null; } /** * Collects all references to the specified alias within the given scope. */ collectAliasReferences(scope, aliasName) { const references = []; try { // Collect table source references (FROM, JOIN clauses) const tableReferences = this.collectTableAliasReferences(scope, aliasName); references.push(...tableReferences); // Collect column references (table_alias.column format) const columnReferences = this.collectColumnAliasReferences(scope, aliasName); references.push(...columnReferences); } catch (error) { console.warn(`Failed to collect alias references for '${aliasName}':`, error); } return references; } /** * Collects table alias references within the scope. */ collectTableAliasReferences(scope, aliasName) { const references = []; try { // Use TableSourceCollector to find all table sources in the scope const collector = new TableSourceCollector_1.TableSourceCollector(true); // selectableOnly=true for proper scope const tableSources = collector.collect(scope.query); for (const tableSource of tableSources) { const sourceName = tableSource.getSourceName(); // Check if this table source matches our alias if (sourceName === aliasName) { // This is likely the definition of the alias (FROM users u, JOIN orders o) const lexeme = this.createLexemeFromTableSource(tableSource, aliasName); if (lexeme) { references.push({ lexeme, scope, referenceType: 'definition', context: 'table' }); } } } } catch (error) { console.warn(`Failed to collect table alias references for '${aliasName}':`, error); } return references; } /** * Collects column alias references (table_alias.column format) within the scope. */ collectColumnAliasReferences(scope, aliasName) { const references = []; try { // Use ColumnReferenceCollector to find all column references const collector = new ColumnReferenceCollector_1.ColumnReferenceCollector(); const columnRefs = collector.collect(scope.query); for (const columnRef of columnRefs) { // Check if any namespace in this column reference matches our alias if (columnRef.namespaces && columnRef.namespaces.length > 0) { for (const namespace of columnRef.namespaces) { if (namespace.name === aliasName) { // This is a usage of the alias (u.name, u.id, etc.) const lexeme = this.createLexemeFromNamespace(namespace, aliasName); if (lexeme) { references.push({ lexeme, scope, referenceType: 'usage', context: 'column' }); } } } } } } catch (error) { console.warn(`Failed to collect column alias references for '${aliasName}':`, error); } return references; } /** * Creates a lexeme representation from a table source for reference tracking. */ createLexemeFromTableSource(tableSource, aliasName) { try { // Try to extract position information from the table source // This is a best-effort approach since TableSource might not have direct lexeme info return { type: Lexeme_1.TokenType.Identifier, value: aliasName, comments: null, position: { startPosition: 0, // TODO: Extract actual position if available endPosition: aliasName.length } }; } catch (error) { console.warn('Failed to create lexeme from table source:', error); return null; } } /** * Creates a lexeme representation from a namespace for reference tracking. */ createLexemeFromNamespace(namespace, aliasName) { try { // Try to extract position information from the namespace return { type: Lexeme_1.TokenType.Identifier, value: aliasName, comments: null, position: { startPosition: 0, // TODO: Extract actual position if available endPosition: aliasName.length } }; } catch (error) { console.warn('Failed to create lexeme from namespace:', error); return null; } } /** * Checks for naming conflicts when renaming to the new name. */ checkNameConflicts(scope, newName, currentName) { const conflicts = []; if (newName.toLowerCase() === currentName.toLowerCase()) { conflicts.push(ERROR_MESSAGES.sameNames); return conflicts; } try { // Check for conflicts with existing table aliases const tableConflicts = this.checkTableAliasConflicts(scope, newName); conflicts.push(...tableConflicts); // Check for conflicts with SQL keywords (basic check) const keywordConflicts = this.checkKeywordConflicts(newName); conflicts.push(...keywordConflicts); } catch (error) { console.warn(`Error during conflict detection for '${newName}':`, error); conflicts.push(`Unable to verify conflicts for name '${newName}'`); } return conflicts; } /** * Checks for conflicts with existing table aliases and table names in the scope. */ checkTableAliasConflicts(scope, newName) { const conflicts = []; try { // Use TableSourceCollector to get all existing table sources in scope const collector = new TableSourceCollector_1.TableSourceCollector(true); const tableSources = collector.collect(scope.query); for (const tableSource of tableSources) { // Check alias conflicts const aliasName = tableSource.getSourceName(); if (aliasName && aliasName.toLowerCase() === newName.toLowerCase()) { conflicts.push(ERROR_MESSAGES.nameConflict(newName)); continue; // Avoid duplicate messages } // Check table name conflicts const tableName = this.extractTableName(tableSource); if (tableName && tableName.toLowerCase() === newName.toLowerCase()) { conflicts.push(`'${newName}' conflicts with table name in this scope`); } } } catch (error) { console.warn(`Failed to check table alias conflicts for '${newName}':`, error); } return conflicts; } /** * Extracts the actual table name from a table source (not the alias). */ extractTableName(tableSource) { try { // Try to access the qualified name to get the actual table name if (tableSource.qualifiedName && tableSource.qualifiedName.name) { const name = tableSource.qualifiedName.name; // Handle different name types if (typeof name === 'string') { return name; } else if (name.name && typeof name.name === 'string') { // IdentifierString case return name.name; } else if (name.value && typeof name.value === 'string') { // RawString case return name.value; } } // Fallback: try to get table name from other properties if (tableSource.table && typeof tableSource.table === 'string') { return tableSource.table; } return null; } catch (error) { console.warn('Failed to extract table name from table source:', error); return null; } } /** * Checks if the new name conflicts with SQL keywords using the existing KeywordTrie. */ checkKeywordConflicts(newName) { const conflicts = []; // Use basic check as primary method for now to ensure test reliability if (this.isBasicReservedKeyword(newName)) { conflicts.push(`'${newName}' is a reserved SQL keyword and should not be used as an alias`); return 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 an alias`); } } catch (error) { console.warn(`Failed to check keyword conflicts for '${newName}':`, error); } return conflicts; } /** * Fallback method for basic reserved keyword checking. */ isBasicReservedKeyword(name) { const basicKeywords = ['select', 'from', 'where', 'join', 'table', 'null', 'and', 'or']; return basicKeywords.includes(name.toLowerCase()); } /** * Prepares change details for the rename operation. */ prepareChanges(references, newName) { return references.map(ref => { var _a; return ({ oldName: ref.lexeme.value, newName, position: LexemeCursor_1.LexemeCursor.charOffsetToLineColumn('', // TODO: Get original SQL ((_a = ref.lexeme.position) === null || _a === void 0 ? void 0 : _a.startPosition) || 0) || { line: 1, column: 1 }, context: ref.context, referenceType: ref.referenceType }); }); } /** * Enhanced SQL text replacement using lexeme-based approach. * This method re-tokenizes the SQL to get accurate position information. */ performLexemeBasedRename(sql, aliasName, newName, scope) { try { // Get all lexemes with position information const lexemes = LexemeCursor_1.LexemeCursor.getAllLexemesWithPosition(sql); // Filter lexemes within the scope and matching the alias name const targetLexemes = lexemes.filter(lexeme => lexeme.value === aliasName && lexeme.position && lexeme.position.startPosition >= scope.startPosition && lexeme.position.endPosition <= scope.endPosition && (lexeme.type & Lexeme_1.TokenType.Identifier) // Only rename identifiers ); if (targetLexemes.length === 0) { return sql; // No matches found } // Sort by position (descending) for safe replacement targetLexemes.sort((a, b) => b.position.startPosition - a.position.startPosition); let modifiedSql = sql; // Replace each occurrence for (const lexeme of targetLexemes) { const pos = lexeme.position; modifiedSql = modifiedSql.substring(0, pos.startPosition) + newName + modifiedSql.substring(pos.endPosition); } return modifiedSql; } catch (error) { console.error('Failed to perform lexeme-based rename:', error); throw new Error(`Unable to rename alias using lexeme approach: ${error instanceof Error ? error.message : String(error)}`); } } } exports.AliasRenamer = AliasRenamer; //# sourceMappingURL=AliasRenamer.js.map