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