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