UNPKG

rawsql-ts

Version:

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

592 lines 23.4 kB
/** * Enum for duplicate detection modes in SelectableColumnCollector. * Determines how duplicates are identified during column collection. */ export 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 || (DuplicateDetectionMode = {})); import { FromClause, GroupByClause, HavingClause, LimitClause, OrderByClause, SelectClause, WhereClause, WindowFrameClause, JoinOnClause, JoinUsingClause, TableSource, SubQuerySource, SourceExpression, PartitionByClause, FetchClause, OffsetClause, ParenSource } from "../models/Clause"; import { SimpleSelectQuery, BinarySelectQuery } from "../models/SelectQuery"; import { ArrayExpression, ArrayQueryExpression, BetweenExpression, BinaryExpression, CaseExpression, CastExpression, ColumnReference, FunctionCall, ParenExpression, UnaryExpression, ValueList, WindowFrameExpression } from "../models/ValueComponent"; import { CTECollector } from "./CTECollector"; import { SelectValueCollector } from "./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); * ``` */ export 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(); 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(SimpleSelectQuery.kind, (expr) => this.visitSimpleSelectQuery(expr)); this.handlers.set(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(SelectClause.kind, (expr) => this.visitSelectClause(expr)); this.handlers.set(FromClause.kind, (expr) => this.visitFromClause(expr)); this.handlers.set(WhereClause.kind, (expr) => this.visitWhereClause(expr)); this.handlers.set(GroupByClause.kind, (expr) => this.visitGroupByClause(expr)); this.handlers.set(HavingClause.kind, (expr) => this.visitHavingClause(expr)); this.handlers.set(OrderByClause.kind, (expr) => this.visitOrderByClause(expr)); this.handlers.set(WindowFrameClause.kind, (expr) => this.visitWindowFrameClause(expr)); this.handlers.set(LimitClause.kind, (expr) => this.visitLimitClause(expr)); this.handlers.set(OffsetClause.kind, (expr) => this.offsetClause(expr)); this.handlers.set(FetchClause.kind, (expr) => this.visitFetchClause(expr)); // JOIN condition handlers this.handlers.set(JoinOnClause.kind, (expr) => this.visitJoinOnClause(expr)); this.handlers.set(JoinUsingClause.kind, (expr) => this.visitJoinUsingClause(expr)); } /** * Initialize handlers for value component types. */ initializeValueComponentHandlers() { this.handlers.set(ColumnReference.kind, (expr) => this.visitColumnReference(expr)); this.handlers.set(BinaryExpression.kind, (expr) => this.visitBinaryExpression(expr)); this.handlers.set(UnaryExpression.kind, (expr) => this.visitUnaryExpression(expr)); this.handlers.set(FunctionCall.kind, (expr) => this.visitFunctionCall(expr)); this.handlers.set(ParenExpression.kind, (expr) => this.visitParenExpression(expr)); this.handlers.set(CaseExpression.kind, (expr) => this.visitCaseExpression(expr)); this.handlers.set(CastExpression.kind, (expr) => this.visitCastExpression(expr)); this.handlers.set(BetweenExpression.kind, (expr) => this.visitBetweenExpression(expr)); this.handlers.set(ArrayExpression.kind, (expr) => this.visitArrayExpression(expr)); this.handlers.set(ArrayQueryExpression.kind, (expr) => this.visitArrayQueryExpression(expr)); this.handlers.set(ValueList.kind, (expr) => this.visitValueList(expr)); this.handlers.set(WindowFrameExpression.kind, (expr) => this.visitWindowFrameExpression(expr)); this.handlers.set(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 SimpleSelectQuery || arg instanceof 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 SimpleSelectQuery) { this.visitSimpleSelectQuery(query.left); } else if (query.left instanceof BinarySelectQuery) { this.visitBinarySelectQuery(query.left); } // Collect from the right side if (query.right instanceof SimpleSelectQuery) { this.visitSimpleSelectQuery(query.right); } else if (query.right instanceof BinarySelectQuery) { this.visitBinarySelectQuery(query.right); } } // 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(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) { 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); } } visitArrayQueryExpression(expr) { expr.query.accept(this); } visitValueList(expr) { if (expr.values) { for (const value of expr.values) { 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) { // 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 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 SubQuerySource) { // For subquery sources, collect columns from the subquery this.collectUpstreamColumnsFromSubquery(source.datasource); } else if (source.datasource instanceof ParenSource) { // For parenthesized sources, recursively collect this.collectUpstreamColumnsFromSource(new 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 ColumnReference(tableSource.table.name, columnName); this.addSelectValueAsUnique(columnName, columnRef); } } } /** * Collect upstream columns from a subquery source. */ collectUpstreamColumnsFromSubquery(subquerySource) { if (subquerySource.query instanceof SimpleSelectQuery) { // Create a new collector for the subquery const subqueryCollector = new SelectableColumnCollector(this.tableColumnResolver, this.includeWildCard, this.duplicateDetection, Object.assign(Object.assign({}, 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 SimpleSelectQuery) { // Use SelectValueCollector to get the columns defined in the CTE's SELECT clause const cteCollector = new SelectValueCollector(this.tableColumnResolver, this.commonTables); const cteColumns = cteCollector.collect(cteTable.query.selectClause); // Add all columns from the CTE for (const item of cteColumns) { this.addSelectValueAsUnique(item.name, item.value); } } } /** * Find a CTE by name in the common tables. */ findCTEByName(name) { return this.commonTables.find(cte => cte.getSourceAliasName() === name) || null; } } //# sourceMappingURL=SelectableColumnCollector.js.map