UNPKG

rawsql-ts

Version:

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

240 lines 10.1 kB
import { FromClause, ParenSource, SelectClause, SourceExpression, SubQuerySource, TableSource } from "../models/Clause"; import { SimpleSelectQuery } from "../models/SelectQuery"; import { ColumnReference } from "../models/ValueComponent"; import { CTECollector } from "./CTECollector"; /** * A visitor that collects all SelectItem instances from a SQL query structure. * This visitor scans through select clauses and collects all the SelectItem objects. * It can also resolve wildcard selectors (table.* or *) using a provided table column resolver. */ export class SelectValueCollector { constructor(tableColumnResolver = null, initialCommonTables = null) { this.selectValues = []; this.visitedNodes = new Set(); this.isRootVisit = true; this.tableColumnResolver = tableColumnResolver !== null && tableColumnResolver !== void 0 ? tableColumnResolver : null; this.commonTableCollector = new CTECollector(); this.commonTables = []; this.initialCommonTables = initialCommonTables; this.handlers = new Map(); this.handlers.set(SimpleSelectQuery.kind, (expr) => this.visitSimpleSelectQuery(expr)); this.handlers.set(SelectClause.kind, (expr) => this.visitSelectClause(expr)); this.handlers.set(SourceExpression.kind, (expr) => this.visitSourceExpression(expr)); this.handlers.set(FromClause.kind, (expr) => this.visitFromClause(expr)); } /** * Get all collected SelectItems as an array of objects with name and value properties * @returns An array of objects with name (string) and value (ValueComponent) properties */ getValues() { return this.selectValues; } /** * Reset the collection of SelectItems */ reset() { this.selectValues = []; this.visitedNodes.clear(); if (this.initialCommonTables) { this.commonTables = this.initialCommonTables; } else { this.commonTables = []; } } 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; } /** * 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 this is a root visit, we need to reset the state this.reset(); this.isRootVisit = false; 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; } } /** * Process a SimpleSelectQuery to collect data and store the current context */ visitSimpleSelectQuery(query) { if (this.commonTables.length === 0 && this.initialCommonTables === null) { this.commonTables = this.commonTableCollector.collect(query); } if (query.selectClause) { query.selectClause.accept(this); } // no wildcard const wildcards = this.selectValues.filter(item => item.name === '*'); if (wildcards.length === 0) { return; } // full wildcard if (this.selectValues.some(item => item.value instanceof ColumnReference && item.value.namespaces === null)) { if (query.fromClause) { this.processFromClause(query.fromClause, true); } // remove wildcard this.selectValues = this.selectValues.filter(item => item.name !== '*'); return; } ; // table wildcard const wildSourceNames = wildcards.filter(item => item.value instanceof ColumnReference && item.value.namespaces) .map(item => item.value.getNamespace()); if (query.fromClause) { const fromSourceName = query.fromClause.getSourceAliasName(); if (fromSourceName && wildSourceNames.includes(fromSourceName)) { this.processFromClause(query.fromClause, false); } if (query.fromClause.joins) { for (const join of query.fromClause.joins) { const joinSourceName = join.getSourceAliasName(); if (joinSourceName && wildSourceNames.includes(joinSourceName)) { this.processJoinClause(join); } } } } // remove wildcard this.selectValues = this.selectValues.filter(item => item.name !== '*'); return; } processFromClause(clause, joinCascade) { if (clause) { const fromSourceName = clause.getSourceAliasName(); this.processSourceExpression(fromSourceName, clause.source); if (clause.joins && joinCascade) { for (const join of clause.joins) { this.processJoinClause(join); } } } return; } processJoinClause(clause) { const sourceName = clause.getSourceAliasName(); this.processSourceExpression(sourceName, clause.source); } processSourceExpression(sourceName, source) { // check common table const commonTable = this.commonTables.find(item => item.aliasExpression.table.name === sourceName); if (commonTable) { // Exclude this CTE from consideration to prevent self-reference const innerCommonTables = this.commonTables.filter(item => item.aliasExpression.table.name !== sourceName); const innerCollector = new SelectValueCollector(this.tableColumnResolver, innerCommonTables); const innerSelected = innerCollector.collect(commonTable.query); innerSelected.forEach(item => { this.addSelectValueAsUnique(item.name, new ColumnReference(sourceName ? [sourceName] : null, item.name)); }); } else { const innerCollector = new SelectValueCollector(this.tableColumnResolver, this.commonTables); const innerSelected = innerCollector.collect(source); innerSelected.forEach(item => { this.addSelectValueAsUnique(item.name, new ColumnReference(sourceName ? [sourceName] : null, item.name)); }); } } visitSelectClause(clause) { for (const item of clause.items) { this.processSelectItem(item); } } processSelectItem(item) { if (item.identifier) { this.addSelectValueAsUnique(item.identifier.name, item.value); } else if (item.value instanceof ColumnReference) { // Handle column reference // columnName can be '*' const columnName = item.value.column.name; if (columnName === '*') { // Force add without checking duplicates this.selectValues.push({ name: columnName, value: item.value }); } else { // Add with duplicate checking this.addSelectValueAsUnique(columnName, item.value); } } } visitSourceExpression(source) { // Column aliases have the highest priority if present // For physical tables, use external function to get column names // For subqueries, instantiate a new collector and get column names from the subquery // For parenthesized expressions, treat them the same as subqueries if (source.aliasExpression && source.aliasExpression.columns) { const sourceName = source.getAliasName(); source.aliasExpression.columns.forEach(column => { this.addSelectValueAsUnique(column.name, new ColumnReference(sourceName ? [sourceName] : null, column.name)); }); return; } else if (source.datasource instanceof TableSource) { if (this.tableColumnResolver) { const sourceName = source.datasource.getSourceName(); this.tableColumnResolver(sourceName).forEach(column => { this.addSelectValueAsUnique(column, new ColumnReference([sourceName], column)); }); } return; } else if (source.datasource instanceof SubQuerySource) { const sourceName = source.getAliasName(); const innerCollector = new SelectValueCollector(this.tableColumnResolver, this.commonTables); const innerSelected = innerCollector.collect(source.datasource.query); innerSelected.forEach(item => { this.addSelectValueAsUnique(item.name, new ColumnReference(sourceName ? [sourceName] : null, item.name)); }); return; } else if (source.datasource instanceof ParenSource) { return this.visit(source.datasource.source); } } visitFromClause(clause) { if (clause) { this.processFromClause(clause, true); } } addSelectValueAsUnique(name, value) { // Check if a select value with the same name already exists before adding if (!this.selectValues.some(item => item.name === name)) { this.selectValues.push({ name, value }); } } } //# sourceMappingURL=SelectValueCollector.js.map