UNPKG

rawsql-ts

Version:

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

213 lines 9.75 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SchemaCollector = exports.TableSchema = void 0; const Clause_1 = require("../models/Clause"); const SimpleSelectQuery_1 = require("../models/SimpleSelectQuery"); const CTECollector_1 = require("./CTECollector"); const SelectableColumnCollector_1 = require("./SelectableColumnCollector"); const ValueComponent_1 = require("../models/ValueComponent"); const SelectQuery_1 = require("../models/SelectQuery"); class TableSchema { constructor(name, columns) { this.name = name; this.columns = columns; } } exports.TableSchema = TableSchema; /** * A visitor that collects schema information (table names and column names) from a SQL query structure. */ class SchemaCollector { constructor(tableColumnResolver = null) { this.tableColumnResolver = tableColumnResolver; this.tableSchemas = []; this.visitedNodes = new Set(); this.commonTables = []; this.running = false; this.handlers = new Map(); // Setup handlers for query components this.handlers.set(SimpleSelectQuery_1.SimpleSelectQuery.kind, (expr) => this.visitSimpleSelectQuery(expr)); this.handlers.set(SelectQuery_1.BinarySelectQuery.kind, (expr) => this.visitBinarySelectQuery(expr)); } /** * Collects schema information (table names and column names) from a SQL query structure. * This method ensures that the collected schema information is unique and sorted. * The resulting schemas and columns are sorted alphabetically to ensure deterministic ordering. * * @param arg The SQL query structure to analyze. */ collect(arg) { this.visit(arg); return this.tableSchemas; } /** * Main entry point for the visitor pattern. * Implements the shallow visit pattern to distinguish between root and recursive visits. * * This method ensures that schema information is collected uniquely and sorted. * The resulting schemas and columns are sorted alphabetically to ensure deterministic ordering. * * @param arg The SQL component to visit. */ visit(arg) { // If not a root visit, just visit the node and return if (this.running) { this.visitNode(arg); return; } // If this is a root visit, we need to reset the state this.reset(); this.running = true; try { // Ensure the argument is a SelectQuery if (!(arg instanceof SimpleSelectQuery_1.SimpleSelectQuery || arg instanceof SelectQuery_1.BinarySelectQuery)) { throw new Error(`Unsupported SQL component type for schema collection. Received: ${arg.constructor.name}. Expected: SimpleSelectQuery or BinarySelectQuery.`); } // Collects Common Table Expressions (CTEs) using CTECollector const cteCollector = new CTECollector_1.CTECollector(); this.commonTables = cteCollector.collect(arg); this.visitNode(arg); // Consolidate tableSchemas this.consolidateTableSchemas(); } finally { // Regardless of success or failure, reset the root visit flag this.running = false; } } /** * 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; } // If no handler found, that's ok - we only care about specific components } /** * Resets the state of the collector for a new root visit. */ reset() { this.tableSchemas = []; this.visitedNodes = new Set(); this.commonTables = []; } /** * Consolidates table schemas by merging columns for tables with the same name. * This ensures that each table name appears only once in the final schema list, * with all its columns combined while removing duplicates. * * Note: The resulting schemas and columns are sorted alphabetically to ensure deterministic ordering. */ consolidateTableSchemas() { const consolidatedSchemas = new Map(); for (const schema of this.tableSchemas) { if (!consolidatedSchemas.has(schema.name)) { consolidatedSchemas.set(schema.name, new Set(schema.columns)); } else { const existingColumns = consolidatedSchemas.get(schema.name); schema.columns.forEach(column => existingColumns === null || existingColumns === void 0 ? void 0 : existingColumns.add(column)); } } this.tableSchemas = Array.from(consolidatedSchemas.entries()) .sort(([nameA], [nameB]) => nameA.localeCompare(nameB)) // Sort by table name .map(([name, columns]) => { return new TableSchema(name, Array.from(columns).sort()); // Sort columns alphabetically }); } handleTableSource(source, queryColumns, includeUnnamed) { var _a; if (source.datasource instanceof Clause_1.TableSource) { const tableName = source.datasource.getSourceName(); const cte = this.commonTables.filter((table) => table.getSourceAliasName() === tableName); if (cte.length > 0) { cte[0].query.accept(this); } else { const tableAlias = (_a = source.getAliasName()) !== null && _a !== void 0 ? _a : tableName; this.processCollectTableSchema(tableName, tableAlias, queryColumns, includeUnnamed); } } else { throw new Error("Datasource is not an instance of TableSource"); } } visitSimpleSelectQuery(query) { var _a; if (query.fromClause === null) { return; } // Collect columns used in the query const columnCollector = new SelectableColumnCollector_1.SelectableColumnCollector(this.tableColumnResolver, true, SelectableColumnCollector_1.DuplicateDetectionMode.FullName); const columns = columnCollector.collect(query); const queryColumns = columns.filter((column) => column.value instanceof ValueComponent_1.ColumnReference) .map(column => column.value) .map(columnRef => ({ table: columnRef.getNamespace(), column: columnRef.column.name })); // Throw an error if there are columns without table names in queries with joins if (query.fromClause.joins !== null && query.fromClause.joins.length > 0) { const columnsWithoutTable = queryColumns.filter((columnRef) => columnRef.table === "").map((columnRef) => columnRef.column); if (columnsWithoutTable.length > 0) { throw new Error(`Column reference(s) without table name found in query: ${columnsWithoutTable.join(', ')}`); } } // Handle the main FROM clause table if (query.fromClause.source.datasource instanceof Clause_1.TableSource) { this.handleTableSource(query.fromClause.source, queryColumns, true); } else if (query.fromClause.source.datasource instanceof Clause_1.SubQuerySource) { query.fromClause.source.datasource.query.accept(this); } // Handle JOIN clause tables if ((_a = query.fromClause) === null || _a === void 0 ? void 0 : _a.joins) { for (const join of query.fromClause.joins) { if (join.source.datasource instanceof Clause_1.TableSource) { this.handleTableSource(join.source, queryColumns, false); } else if (join.source.datasource instanceof Clause_1.SubQuerySource) { join.source.datasource.query.accept(this); } } } } visitBinarySelectQuery(query) { // Visit the left and right queries this.visitNode(query.left); this.visitNode(query.right); } processCollectTableSchema(tableName, tableAlias, queryColumns, includeUnnamed = false) { // If a wildcard is present and no resolver is provided, throw an error if (this.tableColumnResolver === null) { const hasWildcard = queryColumns .filter((columnRef) => columnRef.table === tableAlias || (includeUnnamed && columnRef.table === "")) .filter((columnRef) => columnRef.column === "*") .length > 0; if (hasWildcard) { const errorMessage = tableName ? `Wildcard (*) is used. A TableColumnResolver is required to resolve wildcards. Target table: ${tableName}` : "Wildcard (*) is used. A TableColumnResolver is required to resolve wildcards."; throw new Error(errorMessage); } } let tableColumns = queryColumns .filter((columnRef) => columnRef.column !== "*") .filter((columnRef) => columnRef.table === tableAlias || (includeUnnamed && columnRef.table === "")) .map((columnRef) => columnRef.column); const tableSchema = new TableSchema(tableName, tableColumns); this.tableSchemas.push(tableSchema); } } exports.SchemaCollector = SchemaCollector; //# sourceMappingURL=SchemaCollector.js.map