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