UNPKG

rawsql-ts

Version:

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

393 lines 15.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SelectableColumnCollector = exports.DuplicateDetectionMode = void 0; /** * Enum for duplicate detection modes in SelectableColumnCollector. * Determines how duplicates are identified during column collection. */ var DuplicateDetectionMode; (function (DuplicateDetectionMode) { /** * Detect duplicates based only on column names. * This mode ignores the table name, so columns with the same name * from different tables are considered duplicates. */ DuplicateDetectionMode["ColumnNameOnly"] = "columnNameOnly"; /** * Detect duplicates based on both table and column names. * This mode ensures that columns with the same name from different * tables are treated as distinct. */ DuplicateDetectionMode["FullName"] = "fullName"; })(DuplicateDetectionMode || (exports.DuplicateDetectionMode = DuplicateDetectionMode = {})); const Clause_1 = require("../models/Clause"); const SelectQuery_1 = require("../models/SelectQuery"); const ValueComponent_1 = require("../models/ValueComponent"); const CTECollector_1 = require("./CTECollector"); const SelectValueCollector_1 = require("./SelectValueCollector"); /** * A visitor that collects all ColumnReference instances from a SQL query structure. * This visitor scans through all clauses and collects all unique ColumnReference objects. * It does not scan Common Table Expressions (CTEs) or subqueries. * * Important: Only collects column references to tables defined in the root FROM/JOIN clauses, * as these are the only columns that can be directly referenced in the query. */ class SelectableColumnCollector { /** * Creates a new instance of SelectableColumnCollector. * * @param {TableColumnResolver | null} [tableColumnResolver=null] - The resolver used to resolve column references to their respective tables. * @param {boolean} [includeWildCard=false] - If true, wildcard columns (e.g., `*`) are included in the collection. * @param {DuplicateDetectionMode} [duplicateDetection=DuplicateDetectionMode.ColumnNameOnly] - Specifies the duplicate detection mode: 'columnNameOnly' (default, only column name is used), or 'fullName' (table name + column name). * @param {Object} [options={}] - Additional options for the collector. * @param {boolean} [options.ignoreCaseAndUnderscore=false] - If true, column names are compared without considering case and underscores. */ constructor(tableColumnResolver, includeWildCard = false, duplicateDetection = DuplicateDetectionMode.ColumnNameOnly, options) { this.selectValues = []; this.visitedNodes = new Set(); this.isRootVisit = true; this.tableColumnResolver = null; this.commonTables = []; this.tableColumnResolver = tableColumnResolver !== null && tableColumnResolver !== void 0 ? tableColumnResolver : null; this.includeWildCard = includeWildCard; this.commonTableCollector = new CTECollector_1.CTECollector(); this.commonTables = []; this.duplicateDetection = duplicateDetection; this.options = options || {}; this.handlers = new Map(); // Main entry point is the SimpleSelectQuery this.handlers.set(SelectQuery_1.SimpleSelectQuery.kind, (expr) => this.visitSimpleSelectQuery(expr)); // Handlers for each clause type that might contain column references this.handlers.set(Clause_1.SelectClause.kind, (expr) => this.visitSelectClause(expr)); this.handlers.set(Clause_1.FromClause.kind, (expr) => this.visitFromClause(expr)); this.handlers.set(Clause_1.WhereClause.kind, (expr) => this.visitWhereClause(expr)); this.handlers.set(Clause_1.GroupByClause.kind, (expr) => this.visitGroupByClause(expr)); this.handlers.set(Clause_1.HavingClause.kind, (expr) => this.visitHavingClause(expr)); this.handlers.set(Clause_1.OrderByClause.kind, (expr) => this.visitOrderByClause(expr)); this.handlers.set(Clause_1.WindowFrameClause.kind, (expr) => this.visitWindowFrameClause(expr)); this.handlers.set(Clause_1.LimitClause.kind, (expr) => this.visitLimitClause(expr)); this.handlers.set(Clause_1.OffsetClause.kind, (expr) => this.offsetClause(expr)); this.handlers.set(Clause_1.FetchClause.kind, (expr) => this.visitFetchClause(expr)); // Add handlers for JOIN conditions this.handlers.set(Clause_1.JoinOnClause.kind, (expr) => this.visitJoinOnClause(expr)); this.handlers.set(Clause_1.JoinUsingClause.kind, (expr) => this.visitJoinUsingClause(expr)); // For value components that might contain column references this.handlers.set(ValueComponent_1.ColumnReference.kind, (expr) => this.visitColumnReference(expr)); 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, (expr) => this.visitFunctionCall(expr)); this.handlers.set(ValueComponent_1.ParenExpression.kind, (expr) => this.visitParenExpression(expr)); 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.ArrayExpression.kind, (expr) => this.visitArrayExpression(expr)); this.handlers.set(ValueComponent_1.ValueList.kind, (expr) => this.visitValueList(expr)); this.handlers.set(Clause_1.WindowFrameClause.kind, (expr) => this.visitWindowFrameClause(expr)); this.handlers.set(ValueComponent_1.WindowFrameExpression.kind, (expr) => this.visitWindowFrameExpression(expr)); this.handlers.set(Clause_1.PartitionByClause.kind, (expr) => this.visitPartitionByClause(expr)); } getValues() { return this.selectValues; } collect(arg) { // Visit the component and return the collected select items this.visit(arg); const items = this.getValues(); this.reset(); // Reset after collection return items; } /** * Reset the collection of ColumnReferences */ reset() { this.selectValues = []; this.visitedNodes.clear(); this.commonTables = []; } /** * Add a select value as unique, according to the duplicate detection option. * If duplicateDetection is 'columnNameOnly', only column name is checked. * If duplicateDetection is 'fullName', both table and column name are checked. */ addSelectValueAsUnique(name, value) { if (this.duplicateDetection === DuplicateDetectionMode.ColumnNameOnly) { if (!this.selectValues.some(item => item.name === name)) { this.selectValues.push({ name, value }); } } else if (this.duplicateDetection === DuplicateDetectionMode.FullName) { // Try to get table name from ValueComponent if possible let tableName = ''; if (value && typeof value.getNamespace === 'function') { tableName = value.getNamespace() || ''; } const key = tableName ? tableName + '.' + name : name; if (!this.selectValues.some(item => { let itemTable = ''; if (item.value && typeof item.value.getNamespace === 'function') { itemTable = item.value.getNamespace() || ''; } const itemKey = itemTable ? itemTable + '.' + item.name : item.name; return itemKey === key; })) { this.selectValues.push({ name, value }); } } } /** * Main entry point for the visitor pattern. * Implements the shallow visit pattern to distinguish between root and recursive visits. */ visit(arg) { // If not a root visit, just visit the node and return if (!this.isRootVisit) { this.visitNode(arg); return; } if (!(arg instanceof SelectQuery_1.SimpleSelectQuery)) { throw new Error("Root visit must be a SimpleSelectQuery"); } // If this is a root visit, we need to reset the state this.reset(); this.isRootVisit = false; this.commonTables = this.commonTableCollector.collect(arg); try { this.visitNode(arg); } finally { // Regardless of success or failure, reset the root visit flag this.isRootVisit = true; } } /** * Internal visit method used for all nodes. * This separates the visit flag management from the actual node visitation logic. */ visitNode(arg) { // Skip if we've already visited this node to prevent infinite recursion if (this.visitedNodes.has(arg)) { return; } // Mark as visited this.visitedNodes.add(arg); const handler = this.handlers.get(arg.getKind()); if (handler) { handler(arg); return; } // For any other component types, we don't need to do anything } /** * Process a SimpleSelectQuery to collect ColumnReferences from all its clauses */ visitSimpleSelectQuery(query) { // Visit all clauses that might contain column references if (query.selectClause) { 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.windowClause) { for (const win of query.windowClause.windows) { win.accept(this); } } if (query.orderByClause) { query.orderByClause.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); } // Explicitly NOT processing query.WithClause to avoid scanning CTEs } // Clause handlers visitSelectClause(clause) { for (const item of clause.items) { if (item.identifier) { this.addSelectValueAsUnique(item.identifier.name, item.value); } item.value.accept(this); } } visitFromClause(clause) { // import source values const collector = new SelectValueCollector_1.SelectValueCollector(this.tableColumnResolver, this.commonTables); const sourceValues = collector.collect(clause); for (const item of sourceValues) { // Add the select value as unique to avoid duplicates this.addSelectValueAsUnique(item.name, item.value); } if (clause.joins) { for (const join of clause.joins) { if (join.condition) { join.condition.accept(this); } } } } visitWhereClause(clause) { if (clause.condition) { clause.condition.accept(this); } } visitGroupByClause(clause) { if (clause.grouping) { for (const item of clause.grouping) { item.accept(this); } } } visitHavingClause(clause) { if (clause.condition) { clause.condition.accept(this); } } visitOrderByClause(clause) { if (clause.order) { for (const item of clause.order) { item.accept(this); } } } visitWindowFrameClause(clause) { clause.expression.accept(this); } visitWindowFrameExpression(expr) { if (expr.partition) { expr.partition.accept(this); } if (expr.order) { expr.order.accept(this); } if (expr.frameSpec) { expr.frameSpec.accept(this); } } visitLimitClause(clause) { if (clause.value) { clause.value.accept(this); } } offsetClause(clause) { if (clause.value) { clause.value.accept(this); } } visitFetchClause(clause) { if (clause.expression) { clause.expression.accept(this); } } visitJoinOnClause(joinOnClause) { // Visit the join condition if (joinOnClause.condition) { joinOnClause.condition.accept(this); } } visitJoinUsingClause(joinUsingClause) { // Visit the columns in the USING clause if (joinUsingClause.condition) { joinUsingClause.condition.accept(this); } } // Value component handlers visitColumnReference(columnRef) { if (columnRef.column.name !== "*") { this.addSelectValueAsUnique(columnRef.column.name, columnRef); } else if (!this.includeWildCard) { return; } else { this.addSelectValueAsUnique(columnRef.column.name, columnRef); } } visitBinaryExpression(expr) { // Visit both sides of the expression if (expr.left) { expr.left.accept(this); } if (expr.right) { expr.right.accept(this); } } visitUnaryExpression(expr) { if (expr.expression) { expr.expression.accept(this); } } visitFunctionCall(func) { if (func.argument) { func.argument.accept(this); } if (func.over) { func.over.accept(this); } } visitParenExpression(expr) { if (expr.expression) { expr.expression.accept(this); } } visitCaseExpression(expr) { if (expr.condition) { expr.condition.accept(this); } if (expr.switchCase) { expr.switchCase.accept(this); } } visitCastExpression(expr) { if (expr.input) { expr.input.accept(this); } } visitBetweenExpression(expr) { if (expr.expression) { expr.expression.accept(this); } if (expr.lower) { expr.lower.accept(this); } if (expr.upper) { expr.upper.accept(this); } } visitArrayExpression(expr) { if (expr.expression) { expr.expression.accept(this); } } visitValueList(expr) { if (expr.values) { for (const value of expr.values) { value.accept(this); } } } visitPartitionByClause(clause) { clause.value.accept(this); } } exports.SelectableColumnCollector = SelectableColumnCollector; //# sourceMappingURL=SelectableColumnCollector.js.map