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