rawsql-ts
Version:
[beta]High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.
442 lines • 18.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SmartRenamer = void 0;
const LexemeCursor_1 = require("../utils/LexemeCursor");
const SelectQueryParser_1 = require("../parsers/SelectQueryParser");
const SimpleSelectQuery_1 = require("../models/SimpleSelectQuery");
const BinarySelectQuery_1 = require("../models/BinarySelectQuery");
const Lexeme_1 = require("../models/Lexeme");
const CTERenamer_1 = require("./CTERenamer");
const AliasRenamer_1 = require("./AliasRenamer");
const SqlIdentifierRenamer_1 = require("./SqlIdentifierRenamer");
/**
* Smart renamer that automatically detects whether to use CTERenamer or AliasRenamer
* based on the cursor position in SQL text.
*
* This class provides unified GUI integration for SQL renaming operations:
* - If cursor is on a CTE name → uses CTERenamer
* - If cursor is on a table alias → uses AliasRenamer
* - Auto-detects the type and calls appropriate renamer
* - Supports optional formatting preservation via SqlIdentifierRenamer
*
* @example
* ```typescript
* const renamer = new SmartRenamer();
* const sql = `
* -- User analysis
* WITH user_data AS ( /* User CTE *\/
* SELECT * FROM users u
* WHERE u.active = true
* )
* SELECT * FROM user_data
* `;
*
* // Standard rename (no formatting preservation)
* const result1 = renamer.rename(sql, { line: 3, column: 8 }, 'customer_data');
*
* // Rename with formatting preservation
* const result2 = renamer.rename(sql, { line: 3, column: 8 }, 'customer_data',
* { preserveFormatting: true });
* // Preserves comments, indentation, line breaks
*
* // Batch rename with formatting preservation
* const result3 = renamer.batchRename(sql, {
* 'user_data': 'customers',
* 'u': 'users_tbl'
* }, { preserveFormatting: true });
*
* // Check if position is renameable (for GUI context menus)
* const isRenameable = renamer.isRenameable(sql, { line: 3, column: 8 });
* ```
*/
class SmartRenamer {
constructor() {
this.cteRenamer = new CTERenamer_1.CTERenamer();
this.aliasRenamer = new AliasRenamer_1.AliasRenamer();
this.identifierRenamer = new SqlIdentifierRenamer_1.SqlIdentifierRenamer();
}
/**
* Check if the token at the given position is renameable (CTE name or table alias).
* This is a lightweight check for GUI applications to determine if a rename context menu
* should be shown when right-clicking.
*
* @param sql - The complete SQL string
* @param position - Line and column position where user clicked (1-based)
* @returns Object indicating if renameable and what type of renamer would be used
*/
isRenameable(sql, position) {
try {
// Basic validation
if (!(sql === null || sql === void 0 ? void 0 : sql.trim())) {
return { renameable: false, renamerType: 'none', reason: 'Empty SQL' };
}
if (!position || position.line < 1 || position.column < 1) {
return { renameable: false, renamerType: 'none', reason: 'Invalid position' };
}
// Find lexeme at position
const lexeme = LexemeCursor_1.LexemeCursor.findLexemeAtLineColumn(sql, position);
if (!lexeme) {
return { renameable: false, renamerType: 'none', reason: 'No token found' };
}
// Must be an identifier or function
if (!(lexeme.type & (Lexeme_1.TokenType.Identifier | Lexeme_1.TokenType.Function))) {
return {
renameable: false,
renamerType: 'none',
tokenName: lexeme.value,
reason: `Token '${lexeme.value}' is not an identifier`
};
}
const tokenName = lexeme.value;
// Detect what type of identifier this is
const renamerType = this.detectRenamerType(sql, tokenName);
if (renamerType === 'unknown') {
return {
renameable: false,
renamerType: 'none',
tokenName,
reason: `Cannot determine if '${tokenName}' is renameable`
};
}
// Additional check: some identifiers might be column names or other non-renameable items
// For now, if we can detect it as CTE or potential alias, consider it renameable
return {
renameable: true,
renamerType,
tokenName
};
}
catch (error) {
return {
renameable: false,
renamerType: 'none',
reason: `Error: ${error instanceof Error ? error.message : String(error)}`
};
}
}
/**
* Automatically detect and rename CTE names or table aliases based on cursor position.
*
* @param sql - The complete SQL string
* @param position - Line and column position where user clicked (1-based)
* @param newName - The new name to assign
* @param options - Optional configuration { preserveFormatting?: boolean }
* @returns Result object with success status and details
*/
rename(sql, position, newName, options) {
var _a, _b;
try {
// Input validation
if (!(sql === null || sql === void 0 ? void 0 : sql.trim())) {
return this.createErrorResult(sql, newName, 'unknown', '', 'SQL cannot be empty');
}
if (!position || position.line < 1 || position.column < 1) {
return this.createErrorResult(sql, newName, 'unknown', '', 'Position must be valid line/column (1-based)');
}
if (!(newName === null || newName === void 0 ? void 0 : newName.trim())) {
return this.createErrorResult(sql, newName, 'unknown', '', 'New name cannot be empty');
}
// Find lexeme at position
const lexeme = LexemeCursor_1.LexemeCursor.findLexemeAtLineColumn(sql, position);
if (!lexeme) {
return this.createErrorResult(sql, newName, 'unknown', '', `No identifier found at line ${position.line}, column ${position.column}`);
}
// Must be an identifier
if (!(lexeme.type & Lexeme_1.TokenType.Identifier)) {
return this.createErrorResult(sql, newName, 'unknown', lexeme.value, `Token '${lexeme.value}' is not renameable`);
}
const originalName = lexeme.value;
const preserveFormatting = (_a = options === null || options === void 0 ? void 0 : options.preserveFormatting) !== null && _a !== void 0 ? _a : false;
// Detect the renamer type
const renamerType = this.detectRenamerType(sql, originalName);
// If formatting preservation is requested, try that approach first
if (preserveFormatting) {
try {
const formatPreservedResult = this.attemptFormattingPreservationRename(sql, position, newName, originalName, renamerType);
if (formatPreservedResult.success) {
return formatPreservedResult;
}
}
catch (error) {
// Log error but continue with fallback approach
console.warn('Formatting preservation failed, falling back to standard rename:', error);
}
}
// Standard rename approach (no formatting preservation)
try {
let newSql;
if (renamerType === 'cte') {
newSql = this.cteRenamer.renameCTEAtPosition(sql, position, newName);
}
else if (renamerType === 'alias') {
const result = this.aliasRenamer.renameAlias(sql, position, newName);
if (!result.success) {
return {
success: false,
originalSql: sql,
renamerType: 'alias',
originalName,
newName,
error: ((_b = result.conflicts) === null || _b === void 0 ? void 0 : _b.join(', ')) || 'Alias rename failed',
formattingPreserved: false,
formattingMethod: 'smart-renamer-only'
};
}
newSql = result.newSql;
}
else {
return this.createErrorResult(sql, newName, 'unknown', originalName, `Cannot determine if '${originalName}' is a CTE name or table alias`);
}
return {
success: true,
originalSql: sql,
newSql,
renamerType,
originalName,
newName,
formattingPreserved: false,
formattingMethod: 'smart-renamer-only'
};
}
catch (error) {
return this.createErrorResult(sql, newName, renamerType, originalName, `${renamerType.toUpperCase()} rename failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
catch (error) {
return this.createErrorResult(sql, newName, 'unknown', '', `Unexpected error: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Detect whether an identifier is a CTE name or table alias.
* @private
*/
detectRenamerType(sql, identifierName) {
try {
const query = SelectQueryParser_1.SelectQueryParser.parse(sql);
// Check if it's a CTE name
if (this.isCTEName(query, identifierName)) {
return 'cte';
}
// If not a CTE, assume it's a table alias
// Note: More sophisticated detection could be added here
return 'alias';
}
catch (error) {
return 'unknown';
}
}
/**
* Check if identifier is a CTE name in the query.
* @private
*/
isCTEName(query, name) {
if (query instanceof SimpleSelectQuery_1.SimpleSelectQuery && query.withClause) {
return query.withClause.tables.some((cte) => cte.aliasExpression && cte.aliasExpression.table && cte.aliasExpression.table.name === name);
}
if (query instanceof BinarySelectQuery_1.BinarySelectQuery) {
return this.isCTEName(query.left, name) || this.isCTEName(query.right, name);
}
return false;
}
/**
* Attempts to perform rename using SqlIdentifierRenamer to preserve formatting.
* @private
*/
attemptFormattingPreservationRename(sql, position, newName, originalName, renamerType) {
// First, use standard renaming to validate the operation
const standardResult = this.performStandardRename(sql, position, newName, originalName, renamerType);
if (!standardResult.success) {
return {
...standardResult,
formattingPreserved: false,
formattingMethod: 'smart-renamer-only'
};
}
// Create rename mapping for format restorer
const renameMap = new Map([[originalName, newName]]);
try {
// Use SqlIdentifierRenamer to apply the rename while preserving formatting
const formattedSql = this.identifierRenamer.renameIdentifiers(sql, renameMap);
// Validate that the rename was successful
if (this.validateRenameResult(sql, formattedSql, originalName, newName)) {
return {
success: true,
originalSql: sql,
newSql: formattedSql,
renamerType,
originalName,
newName,
formattingPreserved: true,
formattingMethod: 'sql-identifier-renamer'
};
}
else {
throw new Error('Validation failed: rename may not have been applied correctly');
}
}
catch (error) {
// Return standard result on formatting preservation failure
return {
...standardResult,
formattingPreserved: false,
formattingMethod: 'smart-renamer-only'
};
}
}
/**
* Perform standard rename without formatting preservation
* @private
*/
performStandardRename(sql, position, newName, originalName, renamerType) {
var _a;
try {
let newSql;
if (renamerType === 'cte') {
newSql = this.cteRenamer.renameCTEAtPosition(sql, position, newName);
}
else if (renamerType === 'alias') {
const result = this.aliasRenamer.renameAlias(sql, position, newName);
if (!result.success) {
return {
success: false,
originalSql: sql,
renamerType: 'alias',
originalName,
newName,
error: ((_a = result.conflicts) === null || _a === void 0 ? void 0 : _a.join(', ')) || 'Alias rename failed'
};
}
newSql = result.newSql;
}
else {
return {
success: false,
originalSql: sql,
renamerType: 'unknown',
originalName,
newName,
error: `Cannot determine if '${originalName}' is a CTE name or table alias`
};
}
return {
success: true,
originalSql: sql,
newSql,
renamerType,
originalName,
newName
};
}
catch (error) {
return {
success: false,
originalSql: sql,
renamerType,
originalName,
newName,
error: `${renamerType.toUpperCase()} rename failed: ${error instanceof Error ? error.message : String(error)}`
};
}
}
/**
* Validates that the rename operation was successful
* @private
*/
validateRenameResult(originalSql, newSql, oldName, newName) {
// Basic validation: new SQL should be different from original
if (originalSql === newSql) {
return false;
}
// The new name should appear in the result
if (!newSql.includes(newName)) {
return false;
}
// The new SQL should have fewer occurrences of the old name than the original
const originalOccurrences = this.countWordOccurrences(originalSql, oldName);
const newOccurrences = this.countWordOccurrences(newSql, oldName);
return newOccurrences < originalOccurrences;
}
/**
* Counts word boundary occurrences of a name in SQL
* @private
*/
countWordOccurrences(sql, name) {
const regex = new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'gi');
const matches = sql.match(regex);
return matches ? matches.length : 0;
}
/**
* Create error result object.
* @private
*/
createErrorResult(sql, newName, renamerType, originalName, error) {
return {
success: false,
originalSql: sql,
renamerType,
originalName,
newName,
error,
formattingPreserved: false,
formattingMethod: 'smart-renamer-only'
};
}
/**
* Batch rename multiple identifiers with optional formatting preservation.
*
* @param sql - The complete SQL string
* @param renames - Map of old names to new names
* @param options - Optional configuration { preserveFormatting?: boolean }
* @returns Result with success status and details
*/
batchRename(sql, renames, options) {
var _a;
const preserveFormatting = (_a = options === null || options === void 0 ? void 0 : options.preserveFormatting) !== null && _a !== void 0 ? _a : false;
if (preserveFormatting) {
try {
const renameMap = new Map(Object.entries(renames));
const formattedSql = this.identifierRenamer.renameIdentifiers(sql, renameMap);
const originalNames = Object.keys(renames);
const newNames = Object.values(renames);
return {
success: true,
originalSql: sql,
newSql: formattedSql,
renamerType: 'alias', // Assume alias for batch operations
originalName: originalNames.join(', '),
newName: newNames.join(', '),
formattingPreserved: true,
formattingMethod: 'sql-identifier-renamer'
};
}
catch (error) {
return {
success: false,
originalSql: sql,
renamerType: 'unknown',
originalName: Object.keys(renames).join(', '),
newName: Object.values(renames).join(', '),
error: `Batch rename failed: ${error instanceof Error ? error.message : String(error)}`,
formattingPreserved: false,
formattingMethod: 'smart-renamer-only'
};
}
}
else {
// Standard batch rename without formatting preservation would need implementation
// For now, return error suggesting individual renames
return {
success: false,
originalSql: sql,
renamerType: 'unknown',
originalName: Object.keys(renames).join(', '),
newName: Object.values(renames).join(', '),
error: 'Batch rename without formatting preservation not implemented. Use individual renames or enable formatting preservation.',
formattingPreserved: false,
formattingMethod: 'smart-renamer-only'
};
}
}
}
exports.SmartRenamer = SmartRenamer;
//# sourceMappingURL=SmartRenamer.js.map