rawsql-ts
Version:
[beta]High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.
389 lines • 15.2 kB
JavaScript
/**
* 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, PartitionByClause, FetchClause, OffsetClause } from "../models/Clause";
import { SimpleSelectQuery } from "../models/SelectQuery";
import { ArrayExpression, 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 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.
*/
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.
*/
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();
this.commonTables = [];
this.duplicateDetection = duplicateDetection;
this.options = options || {};
this.handlers = new Map();
// Main entry point is the SimpleSelectQuery
this.handlers.set(SimpleSelectQuery.kind, (expr) => this.visitSimpleSelectQuery(expr));
// Handlers for each clause type that might contain column references
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));
// Add handlers for JOIN conditions
this.handlers.set(JoinOnClause.kind, (expr) => this.visitJoinOnClause(expr));
this.handlers.set(JoinUsingClause.kind, (expr) => this.visitJoinUsingClause(expr));
// For value components that might contain column references
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(ValueList.kind, (expr) => this.visitValueList(expr));
this.handlers.set(WindowFrameClause.kind, (expr) => this.visitWindowFrameClause(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) {
// 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 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(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);
}
}
//# sourceMappingURL=SelectableColumnCollector.js.map