UNPKG

rawsql-ts

Version:

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

468 lines 18.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ColumnReferenceCollector = void 0; const Clause_1 = require("../models/Clause"); const SelectQuery_1 = require("../models/SelectQuery"); const ValueComponent_1 = require("../models/ValueComponent"); /** * A comprehensive collector for all ColumnReference instances in SQL query structures. * * This collector extends beyond the capabilities of SelectableColumnCollector by traversing * CTE internal queries, subqueries, and all nested SQL components to collect every column * reference instance in the query tree. It's specifically designed for transformation * scenarios where all column references need to be identified and potentially modified. * * ## Key Differences from SelectableColumnCollector * * | Feature | SelectableColumnCollector | ColumnReferenceCollector | * |---------|---------------------------|---------------------------| * | CTE Internal Scanning | ❌ Skipped | ✅ Included | * | Subquery Traversal | ❌ Limited | ✅ Comprehensive | * | Deduplication | ✅ Yes | ❌ No (preserves all instances) | * | Use Case | Column selection analysis | Column reference transformation | * * ## Supported Query Types * * - **SimpleSelectQuery**: Standard SELECT statements with all clauses * - **BinarySelectQuery**: UNION, INTERSECT, EXCEPT operations * - **Nested CTEs**: WITH clauses and their internal queries * - **Subqueries**: All subquery types in FROM, WHERE, SELECT clauses * - **Complex Expressions**: CASE, functions, binary operations, etc. * * @example * ```typescript * import { ColumnReferenceCollector, SelectQueryParser } from 'rawsql-ts'; * * const sql = ` * WITH user_data AS ( * SELECT id, name FROM users WHERE status = 'active' * ), * 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 collector = new ColumnReferenceCollector(); * const columnRefs = collector.collect(query); * * console.log(`Found ${columnRefs.length} column references:`); * columnRefs.forEach(ref => { * const tableName = ref.namespaces?.[0]?.name || 'NO_TABLE'; * console.log(`- ${tableName}.${ref.column.name}`); * }); * * // Output includes references from: * // - CTE definitions: users.id, users.name, users.status * // - Main query: user_data.id, orders.user_id, etc. * ``` * * @example * ```typescript * // Use for column reference transformation * const columnRefs = collector.collect(query); * * // Update all references to 'old_table' to 'new_table' * columnRefs.forEach(ref => { * if (ref.namespaces?.[0]?.name === 'old_table') { * ref.namespaces[0].name = 'new_table'; * } * }); * ``` * * @since 0.11.16 */ class ColumnReferenceCollector { constructor() { this.columnReferences = []; this.visitedNodes = new Set(); this.handlers = new Map(); // Note: We don't handle SimpleSelectQuery/BinarySelectQuery here as they're handled directly in collect() // Clause handlers this.handlers.set(Clause_1.WithClause.kind, (clause) => this.visitWithClause(clause)); this.handlers.set(Clause_1.CommonTable.kind, (table) => this.visitCommonTable(table)); this.handlers.set(Clause_1.SelectClause.kind, (clause) => this.visitSelectClause(clause)); this.handlers.set(Clause_1.FromClause.kind, (clause) => this.visitFromClause(clause)); this.handlers.set(Clause_1.WhereClause.kind, (clause) => this.visitWhereClause(clause)); this.handlers.set(Clause_1.GroupByClause.kind, (clause) => this.visitGroupByClause(clause)); this.handlers.set(Clause_1.HavingClause.kind, (clause) => this.visitHavingClause(clause)); this.handlers.set(Clause_1.OrderByClause.kind, (clause) => this.visitOrderByClause(clause)); this.handlers.set(Clause_1.WindowsClause.kind, (clause) => this.visitWindowsClause(clause)); this.handlers.set(Clause_1.LimitClause.kind, (clause) => this.visitLimitClause(clause)); this.handlers.set(Clause_1.OffsetClause.kind, (clause) => this.visitOffsetClause(clause)); this.handlers.set(Clause_1.FetchClause.kind, (clause) => this.visitFetchClause(clause)); this.handlers.set(Clause_1.ForClause.kind, (clause) => this.visitForClause(clause)); // JOIN handlers this.handlers.set(Clause_1.JoinClause.kind, (clause) => this.visitJoinClause(clause)); this.handlers.set(Clause_1.JoinOnClause.kind, (clause) => this.visitJoinOnClause(clause)); this.handlers.set(Clause_1.JoinUsingClause.kind, (clause) => this.visitJoinUsingClause(clause)); // Source handlers this.handlers.set(Clause_1.SourceExpression.kind, (source) => this.visitSourceExpression(source)); this.handlers.set(Clause_1.SubQuerySource.kind, (source) => this.visitSubQuerySource(source)); // Value component handlers this.handlers.set(ValueComponent_1.ColumnReference.kind, (ref) => this.visitColumnReference(ref)); this.handlers.set(ValueComponent_1.BinaryExpression.kind, (expr) => this.visitBinaryExpression(expr)); this.handlers.set(ValueComponent_1.UnaryExpression.kind, (expr) => this.visitUnaryExpression(expr)); this.handlers.set(ValueComponent_1.FunctionCall.kind, (func) => this.visitFunctionCall(func)); this.handlers.set(ValueComponent_1.CaseExpression.kind, (expr) => this.visitCaseExpression(expr)); this.handlers.set(ValueComponent_1.CastExpression.kind, (expr) => this.visitCastExpression(expr)); this.handlers.set(ValueComponent_1.BetweenExpression.kind, (expr) => this.visitBetweenExpression(expr)); this.handlers.set(ValueComponent_1.ParenExpression.kind, (expr) => this.visitParenExpression(expr)); this.handlers.set(ValueComponent_1.InlineQuery.kind, (query) => this.visitInlineQuery(query)); this.handlers.set(ValueComponent_1.ArrayExpression.kind, (expr) => this.visitArrayExpression(expr)); this.handlers.set(ValueComponent_1.ArrayQueryExpression.kind, (expr) => this.visitArrayQueryExpression(expr)); this.handlers.set(ValueComponent_1.ValueList.kind, (list) => this.visitValueList(list)); this.handlers.set(ValueComponent_1.WindowFrameExpression.kind, (expr) => this.visitWindowFrameExpression(expr)); } /** * Collects all ColumnReference instances from the given SQL query component. * * This method performs a comprehensive traversal of the entire query structure, * including CTE definitions, subqueries, and all expression types to collect * every ColumnReference instance. The returned references are actual instances * from the query tree, allowing for direct modification. * * @param query - The SQL query component to analyze. Can be SimpleSelectQuery, BinarySelectQuery, or any SqlComponent. * @returns An array of all ColumnReference instances found in the query. Each reference maintains its original object identity for modification purposes. * * @example * ```typescript * const collector = new ColumnReferenceCollector(); * const columnRefs = collector.collect(query); * * // Analyze collected references * const tableReferences = new Map<string, number>(); * columnRefs.forEach(ref => { * const tableName = ref.namespaces?.[0]?.name || 'unqualified'; * tableReferences.set(tableName, (tableReferences.get(tableName) || 0) + 1); * }); * * console.log('Table reference counts:', tableReferences); * ``` * * @example * ```typescript * // Transform references during collection * const columnRefs = collector.collect(query); * * // Replace all references to 'old_schema.table' with 'new_schema.table' * columnRefs.forEach(ref => { * if (ref.namespaces?.length === 2 && * ref.namespaces[0].name === 'old_schema' && * ref.namespaces[1].name === 'table') { * ref.namespaces[0].name = 'new_schema'; * } * }); * ``` * * @since 0.11.16 */ collect(query) { this.columnReferences = []; this.visitedNodes.clear(); // Handle queries directly - bypass visitor pattern issues if (query instanceof SelectQuery_1.SimpleSelectQuery) { this.collectFromSimpleQuery(query); } else if (query instanceof SelectQuery_1.BinarySelectQuery) { // Convert BinarySelectQuery to SimpleSelectQuery for consistent handling this.collectFromSimpleQuery(query.toSimpleQuery()); } else { query.accept(this); } return [...this.columnReferences]; } collectFromSimpleQuery(query) { // First collect from CTEs (this is the key difference from SelectableColumnCollector) if (query.withClause && query.withClause.tables) { for (const cte of query.withClause.tables) { this.collectFromSimpleQuery(cte.query); } } // Then collect from main query clauses this.collectFromSelectClause(query.selectClause); if (query.fromClause) this.collectFromFromClause(query.fromClause); if (query.whereClause) this.collectFromValueComponent(query.whereClause.condition); if (query.groupByClause && query.groupByClause.grouping) { for (const item of query.groupByClause.grouping) { this.collectFromValueComponent(item); } } if (query.havingClause) this.collectFromValueComponent(query.havingClause.condition); if (query.orderByClause && query.orderByClause.order) { for (const item of query.orderByClause.order) { if (typeof item === 'object' && 'value' in item && item.value) { this.collectFromValueComponent(item.value); } else { this.collectFromValueComponent(item); } } } } collectFromSelectClause(clause) { for (const item of clause.items) { this.collectFromValueComponent(item.value); } } collectFromFromClause(clause) { this.collectFromSourceExpression(clause.source); if (clause.joins) { for (const join of clause.joins) { this.collectFromSourceExpression(join.source); if (join.condition) { this.collectFromValueComponent(join.condition.condition); } } } } collectFromSourceExpression(source) { if (source.datasource instanceof Clause_1.SubQuerySource) { if (source.datasource.query instanceof SelectQuery_1.SimpleSelectQuery) { this.collectFromSimpleQuery(source.datasource.query); } else if (source.datasource.query instanceof SelectQuery_1.BinarySelectQuery) { this.collectFromSimpleQuery(source.datasource.query.toSimpleQuery()); } } } collectFromValueComponent(value) { if (value instanceof ValueComponent_1.ColumnReference) { this.columnReferences.push(value); } else if (value instanceof ValueComponent_1.BinaryExpression) { this.collectFromValueComponent(value.left); this.collectFromValueComponent(value.right); } else if (value instanceof ValueComponent_1.UnaryExpression) { this.collectFromValueComponent(value.expression); } else if (value instanceof ValueComponent_1.FunctionCall && value.argument) { this.collectFromValueComponent(value.argument); } else if (value instanceof ValueComponent_1.CaseExpression) { if (value.condition) this.collectFromValueComponent(value.condition); if (value.switchCase && value.switchCase.cases) { for (const pair of value.switchCase.cases) { this.collectFromValueComponent(pair.key); this.collectFromValueComponent(pair.value); } } if (value.switchCase && value.switchCase.elseValue) this.collectFromValueComponent(value.switchCase.elseValue); } else if (value instanceof ValueComponent_1.ParenExpression) { this.collectFromValueComponent(value.expression); } else if (value instanceof ValueComponent_1.InlineQuery) { if (value.selectQuery instanceof SelectQuery_1.SimpleSelectQuery) { this.collectFromSimpleQuery(value.selectQuery); } else if (value.selectQuery instanceof SelectQuery_1.BinarySelectQuery) { this.collectFromSimpleQuery(value.selectQuery.toSimpleQuery()); } } // Add more value component types as needed } visit(component) { if (this.visitedNodes.has(component)) { return; } this.visitedNodes.add(component); const handler = this.handlers.get(component.getKind()); if (handler) { handler(component); } else { // Unhandled component type - this is expected for some components } } // Query visitors visitSimpleSelectQuery(query) { if (query.withClause) query.withClause.accept(this); query.selectClause.accept(this); if (query.fromClause) query.fromClause.accept(this); if (query.whereClause) query.whereClause.accept(this); if (query.groupByClause) query.groupByClause.accept(this); if (query.havingClause) query.havingClause.accept(this); if (query.orderByClause) query.orderByClause.accept(this); if (query.windowClause) query.windowClause.accept(this); if (query.limitClause) query.limitClause.accept(this); if (query.offsetClause) query.offsetClause.accept(this); if (query.fetchClause) query.fetchClause.accept(this); if (query.forClause) query.forClause.accept(this); } // WITH clause and CTE visitors (this is the key difference from SelectableColumnCollector) visitWithClause(clause) { for (const table of clause.tables) { table.accept(this); } } visitCommonTable(table) { // Visit the CTE query to collect column references within it table.query.accept(this); } // Clause visitors visitSelectClause(clause) { for (const item of clause.items) { item.value.accept(this); } } visitFromClause(clause) { clause.source.accept(this); if (clause.joins) { for (const join of clause.joins) { join.accept(this); } } } visitWhereClause(clause) { clause.condition.accept(this); } visitGroupByClause(clause) { if (clause.grouping) { for (const item of clause.grouping) { item.accept(this); } } } visitHavingClause(clause) { clause.condition.accept(this); } visitOrderByClause(clause) { if (clause.order) { for (const item of clause.order) { if (typeof item === 'object' && 'value' in item && item.value) { if (typeof item.value === 'object' && 'accept' in item.value) { item.value.accept(this); } } else if (typeof item === 'object' && 'accept' in item) { item.accept(this); } } } } visitWindowsClause(clause) { for (const window of clause.windows) { window.expression.accept(this); } } visitLimitClause(clause) { clause.value.accept(this); } visitOffsetClause(clause) { clause.value.accept(this); } visitFetchClause(clause) { clause.expression.accept(this); } visitForClause(clause) { // ForClause typically doesn't contain column references } // JOIN visitors visitJoinClause(clause) { clause.source.accept(this); if (clause.condition) { clause.condition.accept(this); } } visitJoinOnClause(clause) { clause.condition.accept(this); } visitJoinUsingClause(clause) { clause.condition.accept(this); } // Source visitors visitSourceExpression(source) { source.datasource.accept(this); } visitSubQuerySource(source) { source.query.accept(this); } // Value component visitors visitColumnReference(ref) { this.columnReferences.push(ref); } visitBinaryExpression(expr) { expr.left.accept(this); expr.right.accept(this); } visitUnaryExpression(expr) { expr.expression.accept(this); } visitFunctionCall(func) { if (func.argument) { func.argument.accept(this); } } visitCaseExpression(expr) { if (expr.condition) expr.condition.accept(this); if (expr.switchCase && expr.switchCase.cases) { for (const pair of expr.switchCase.cases) { pair.key.accept(this); pair.value.accept(this); } } if (expr.switchCase && expr.switchCase.elseValue) expr.switchCase.elseValue.accept(this); } visitCastExpression(expr) { expr.input.accept(this); } visitBetweenExpression(expr) { expr.expression.accept(this); expr.lower.accept(this); expr.upper.accept(this); } visitParenExpression(expr) { expr.expression.accept(this); } visitInlineQuery(query) { query.selectQuery.accept(this); } visitArrayExpression(expr) { if (expr.expression) { expr.expression.accept(this); } } visitArrayQueryExpression(expr) { expr.query.accept(this); } visitValueList(list) { if (list.values) { for (const item of list.values) { item.accept(this); } } } visitWindowFrameExpression(expr) { if (expr.partition) expr.partition.accept(this); if (expr.order) expr.order.accept(this); } } exports.ColumnReferenceCollector = ColumnReferenceCollector; //# sourceMappingURL=ColumnReferenceCollector.js.map