UNPKG

rawsql-ts

Version:

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

674 lines 27.6 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 SQL query structures. * This visitor scans through all clauses and collects all unique ColumnReference objects. * It supports both regular column collection and upstream column collection for maximum * search conditions in DynamicQuery scenarios. * * Supported query types: * - SimpleSelectQuery: Basic SELECT queries with all standard clauses * - BinarySelectQuery: UNION, INTERSECT, EXCEPT queries (collects from both sides) * - Common Table Expressions (CTEs) within queries * - Subqueries and nested queries * * Behavioral notes: * - Collects column references to tables defined in the root FROM/JOIN clauses * - For aliased columns (e.g., 'title as name'), collects both the original column * reference ('title') AND the alias ('name') to enable complete dependency tracking * - When upstream option is enabled, collects all available columns from upstream sources * (CTEs, subqueries, and tables) for maximum search conditions in DynamicQuery * - Automatically removes duplicates based on the specified duplicate detection mode * * Use cases: * - Dependency analysis and schema migration tools * - Column usage tracking across complex queries including unions and CTEs * - Security analysis for column-level access control * - DynamicQuery maximum search condition column discovery * * @example * ```typescript * // Basic usage - collect only referenced columns * const collector = new SelectableColumnCollector(); * const columns = collector.collect(query); * * // With upstream collection for DynamicQuery * const upstreamCollector = new SelectableColumnCollector( * null, false, DuplicateDetectionMode.ColumnNameOnly, * { upstream: true } * ); * const allColumns = upstreamCollector.collect(query); * * // Works with union queries and CTEs * const unionQuery = SelectQueryParser.parse(` * SELECT name, email FROM users * UNION * SELECT name, email FROM customers * `); * const unionColumns = collector.collect(unionQuery); * ``` * Related tests: packages/core/tests/transformers/SelectableColumnCollector.test.ts */ 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. * @param {boolean} [options.upstream=false] - If true, collect all columns available from upstream sources for maximum search conditions in DynamicQuery. */ constructor(tableColumnResolver, includeWildCard = false, duplicateDetection = DuplicateDetectionMode.ColumnNameOnly, options) { this.selectValues = []; this.visitedNodes = new Set(); this.uniqueKeys = new Set(); this.isRootVisit = true; this.tableColumnResolver = null; this.commonTables = []; this.initializeProperties(tableColumnResolver, includeWildCard, duplicateDetection, options); this.initializeHandlers(); } /** * Initialize instance properties. */ initializeProperties(tableColumnResolver, includeWildCard, duplicateDetection, options) { 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 || {}; } /** * Initialize the handler map for different SQL component types. */ initializeHandlers() { this.handlers = new Map(); // Main entry point handlers this.handlers.set(SelectQuery_1.SimpleSelectQuery.kind, (expr) => this.visitSimpleSelectQuery(expr)); this.handlers.set(SelectQuery_1.BinarySelectQuery.kind, (expr) => this.visitBinarySelectQuery(expr)); // Clause handlers this.initializeClauseHandlers(); // Value component handlers this.initializeValueComponentHandlers(); } /** * Initialize handlers for SQL clause types. */ initializeClauseHandlers() { 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)); // JOIN condition handlers this.handlers.set(Clause_1.JoinOnClause.kind, (expr) => this.visitJoinOnClause(expr)); this.handlers.set(Clause_1.JoinUsingClause.kind, (expr) => this.visitJoinUsingClause(expr)); } /** * Initialize handlers for value component types. */ initializeValueComponentHandlers() { 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.InlineQuery.kind, (expr) => this.visitInlineQuery(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.ArrayQueryExpression.kind, (expr) => this.visitArrayQueryExpression(expr)); this.handlers.set(ValueComponent_1.ArraySliceExpression.kind, (expr) => this.visitArraySliceExpression(expr)); this.handlers.set(ValueComponent_1.ArrayIndexExpression.kind, (expr) => this.visitArrayIndexExpression(expr)); this.handlers.set(ValueComponent_1.ValueList.kind, (expr) => this.visitValueList(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) { // Input validation if (!arg) { throw new Error("Input argument cannot be null or undefined"); } // 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.uniqueKeys.clear(); this.commonTables = []; } /** * Add a select value as unique, according to the duplicate detection option. * Uses efficient Set-based duplicate detection for better performance. */ addSelectValueAsUnique(name, value) { const key = this.generateUniqueKey(name, value); if (!this.uniqueKeys.has(key)) { this.uniqueKeys.add(key); this.selectValues.push({ name, value }); } } /** * Generate a unique key based on the duplicate detection mode. */ generateUniqueKey(name, value) { if (this.duplicateDetection === DuplicateDetectionMode.ColumnNameOnly) { // Apply case and underscore normalization if specified return this.normalizeColumnName(name); } else { // FullName mode: include table name let tableName = ''; if (value && typeof value.getNamespace === 'function') { tableName = value.getNamespace() || ''; } const fullName = tableName ? tableName + '.' + name : name; return this.normalizeColumnName(fullName); } } /** * Normalize column name based on options. * Ensures safe string handling to prevent injection attacks. */ normalizeColumnName(name) { if (typeof name !== 'string') { throw new Error("Column name must be a string"); } if (this.options.ignoreCaseAndUnderscore) { return name.toLowerCase().replace(/_/g, ''); } return name; } /** * 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 || arg instanceof SelectQuery_1.BinarySelectQuery)) { throw new Error("Root visit requires a SimpleSelectQuery or BinarySelectQuery."); } // 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); try { const handler = this.handlers.get(arg.getKind()); if (handler) { handler(arg); } // For any other component types, we don't need to do anything } catch (error) { // Re-throw with additional context const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Error processing SQL component of type ${arg.getKind().toString()}: ${errorMessage}`); } } /** * 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 } /** * Process a BinarySelectQuery (UNION, INTERSECT, EXCEPT) to collect ColumnReferences from both sides */ visitBinarySelectQuery(query) { // Collect from the left side if (query.left instanceof SelectQuery_1.SimpleSelectQuery) { this.visitSimpleSelectQuery(query.left); } else if (query.left instanceof SelectQuery_1.BinarySelectQuery) { this.visitBinarySelectQuery(query.left); } // Collect from the right side if (query.right instanceof SelectQuery_1.SimpleSelectQuery) { this.visitSimpleSelectQuery(query.right); } else if (query.right instanceof SelectQuery_1.BinarySelectQuery) { this.visitBinarySelectQuery(query.right); } } // Clause handlers visitSelectClause(clause) { for (const item of clause.items) { if (item.identifier) { // For aliased items, add the alias name this.addSelectValueAsUnique(item.identifier.name, item.value); } else if (item.value instanceof ValueComponent_1.ColumnReference) { // For non-aliased column references, preserve namespace information // This ensures u.id and p.id are treated as separate columns in FullName mode const columnName = item.value.column.name; if (columnName !== "*") { this.addSelectValueAsUnique(columnName, item.value); } else if (this.includeWildCard) { this.addSelectValueAsUnique(columnName, item.value); } } else { // For other value types (functions, expressions, etc.), process normally 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 upstream option is enabled, collect all available columns from upstream sources if (this.options.upstream) { this.collectUpstreamColumns(clause); } 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) { // Visit function arguments - this handles both single arguments and ValueList arguments if (func.argument) { func.argument.accept(this); } // Visit OVER clause for window functions if (func.over) { func.over.accept(this); } // Visit WITHIN GROUP clause for ordered aggregate functions if (func.withinGroup) { func.withinGroup.accept(this); } // Visit internal ORDER BY clause (for array_agg, json_agg, etc.) if (func.internalOrderBy) { func.internalOrderBy.accept(this); } } visitInlineQuery(inlineQuery) { // Visit the nested SELECT query within the inline query expression if (inlineQuery.selectQuery) { this.visitNode(inlineQuery.selectQuery); } } 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); } } visitArrayQueryExpression(expr) { expr.query.accept(this); } visitArraySliceExpression(expr) { // Visit the array and slice indices for column references if (expr.array) { expr.array.accept(this); } if (expr.startIndex) { expr.startIndex.accept(this); } if (expr.endIndex) { expr.endIndex.accept(this); } } visitArrayIndexExpression(expr) { // Visit the array and index for column references if (expr.array) { expr.array.accept(this); } if (expr.index) { expr.index.accept(this); } } visitValueList(expr) { // Visit all values in the list to collect column references from function arguments if (expr.values && Array.isArray(expr.values)) { for (const value of expr.values) { if (value) { value.accept(this); } } } } visitPartitionByClause(clause) { clause.value.accept(this); } /** * Collect all upstream columns available for DynamicQuery maximum search conditions. * This includes columns from CTEs, subqueries, and tables that can be used for filtering. */ collectUpstreamColumns(clause) { // For upstream collection, collect columns from ALL available CTEs // not just the ones directly referenced in the FROM clause // This ensures complex multi-CTE queries have all available columns for filtering this.collectAllAvailableCTEColumns(); // Collect columns from primary source this.collectUpstreamColumnsFromSource(clause.source); // Collect columns from JOIN sources if (clause.joins) { for (const join of clause.joins) { this.collectUpstreamColumnsFromSource(join.source); } } } /** * Collect upstream columns from a specific source (table, subquery, or CTE). */ collectUpstreamColumnsFromSource(source) { if (source.datasource instanceof Clause_1.TableSource) { // Check if this is a CTE reference first const cteTable = this.findCTEByName(source.datasource.table.name); if (cteTable) { this.collectUpstreamColumnsFromCTE(cteTable); } else { // For regular table sources, use table column resolver if available this.collectUpstreamColumnsFromTable(source.datasource); } } else if (source.datasource instanceof Clause_1.SubQuerySource) { // For subquery sources, collect columns from the subquery this.collectUpstreamColumnsFromSubquery(source.datasource); } else if (source.datasource instanceof Clause_1.ParenSource) { // For parenthesized sources, recursively collect this.collectUpstreamColumnsFromSource(new Clause_1.SourceExpression(source.datasource.source, null)); } } /** * Collect upstream columns from a table source using table column resolver. */ collectUpstreamColumnsFromTable(tableSource) { if (this.tableColumnResolver) { const tableName = tableSource.table.name; const columns = this.tableColumnResolver(tableName); for (const columnName of columns) { // Create a column reference for each available column const columnRef = new ValueComponent_1.ColumnReference(tableSource.table.name, columnName); this.addSelectValueAsUnique(columnName, columnRef); } } } /** * Collect upstream columns from a subquery source. */ collectUpstreamColumnsFromSubquery(subquerySource) { if (subquerySource.query instanceof SelectQuery_1.SimpleSelectQuery) { // Create a new collector for the subquery const subqueryCollector = new SelectableColumnCollector(this.tableColumnResolver, this.includeWildCard, this.duplicateDetection, { ...this.options, upstream: true }); // Collect columns from the subquery const subqueryColumns = subqueryCollector.collect(subquerySource.query); // Add all columns from the subquery for (const item of subqueryColumns) { this.addSelectValueAsUnique(item.name, item.value); } } } /** * Collect upstream columns from a CTE. */ collectUpstreamColumnsFromCTE(cteTable) { if (cteTable.query instanceof SelectQuery_1.SimpleSelectQuery) { // Create a recursive SelectableColumnCollector to collect ALL column references from the CTE // This includes columns from SELECT, FROM, WHERE, JOIN, and other clauses within the CTE // NOTE: Set upstream to false to prevent infinite recursion through collectAllAvailableCTEColumns const cteCollector = new SelectableColumnCollector(this.tableColumnResolver, this.includeWildCard, this.duplicateDetection, { ...this.options, upstream: false }); // Collect all columns from the CTE query (including WHERE clause columns) const cteColumns = cteCollector.collect(cteTable.query); // Add all columns from the CTE, excluding wildcards for (const item of cteColumns) { // Skip wildcard columns as they are not valid selectable column names if (item.name !== '*') { this.addSelectValueAsUnique(item.name, item.value); } } } } /** * Collect columns from ALL available CTEs for upstream functionality. * This ensures that complex multi-CTE queries have all available columns for filtering. */ collectAllAvailableCTEColumns() { for (const cte of this.commonTables) { this.collectUpstreamColumnsFromCTE(cte); } } /** * Find a CTE by name in the common tables. */ findCTEByName(name) { return this.commonTables.find(cte => cte.getSourceAliasName() === name) || null; } } exports.SelectableColumnCollector = SelectableColumnCollector; //# sourceMappingURL=SelectableColumnCollector.js.map