UNPKG

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