rawsql-ts
Version:
[beta]High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.
188 lines • 8.58 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.CTEBuilder = void 0;
const Clause_1 = require("../models/Clause");
const CTECollector_1 = require("./CTECollector");
const TableSourceCollector_1 = require("./TableSourceCollector");
const Formatter_1 = require("./Formatter");
/**
* CTENameConflictResolver is responsible for resolving name conflicts among Common Table Expressions (CTEs).
* It also sorts the tables in the proper order based on dependencies and recursiveness.
*/
class CTEBuilder {
constructor() {
this.sourceCollector = new TableSourceCollector_1.TableSourceCollector(true);
this.cteCollector = new CTECollector_1.CTECollector();
this.formatter = new Formatter_1.Formatter();
}
/**
* Resolves name conflicts among CommonTables.
* If there are duplicate CTE names, they must have identical definitions.
* Also sorts the tables so that:
* 1. Recursive CTEs come first (CTEs that reference themselves)
* 2. Then remaining tables are sorted so inner (deeper) CTEs come before outer CTEs
*
* @param commonTables The list of CommonTables to check for name conflicts
* @returns An object containing:
* - needRecursive: boolean indicating if any recursive CTEs are present
* - commonTables: A new list of CommonTables with resolved name conflicts and proper order
* @throws Error if there are duplicate CTE names with different definitions
*/
build(commonTables) {
// Early return for empty CTEs
// Note:
// Although it may seem reasonable to return early when there is only one element,
// the 'recursive' property is determined dynamically. Therefore, if there is at least one element,
// the CTEs must be rebuilt to ensure correct recursive detection.
if (commonTables.length === 0) {
return new Clause_1.WithClause(false, commonTables);
}
// Step 1: Resolve name conflicts
const resolvedTables = this.resolveDuplicateNames(commonTables);
// Step 2: Identify recursive CTEs and build dependency graph
const { tableMap, recursiveCTEs, dependencies } = this.buildDependencyGraph(resolvedTables);
// Step 3: Sort tables according to dependencies and recursiveness
const sortedTables = this.sortCommonTables(resolvedTables, tableMap, recursiveCTEs, dependencies);
return new Clause_1.WithClause(recursiveCTEs.size > 0, sortedTables);
}
/**
* Resolves duplicate CTE names by checking if they have identical definitions.
* If definitions differ, throws an error.
*
* @param commonTables The list of CTEs to check for duplicates
* @returns A list of CTEs with duplicates removed
* @throws Error if there are duplicate CTE names with different definitions
*/
resolveDuplicateNames(commonTables) {
// Group CTEs by their names
const ctesByName = new Map();
for (const table of commonTables) {
const tableName = table.aliasExpression.table.name;
if (!ctesByName.has(tableName)) {
ctesByName.set(tableName, []);
}
ctesByName.get(tableName).push(table);
}
// Resolve name duplications
const resolvedTables = [];
for (const [name, tables] of ctesByName.entries()) {
if (tables.length === 1) {
// No duplication
resolvedTables.push(tables[0]);
continue;
}
// For duplicate names, check if definitions are identical
const definitions = tables.map(table => this.formatter.format(table.query));
const uniqueDefinitions = new Set(definitions);
if (uniqueDefinitions.size === 1) {
// If all definitions are identical, use only the first one
resolvedTables.push(tables[0]);
}
else {
// Error if definitions differ
throw new Error(`CTE name conflict detected: '${name}' has multiple different definitions`);
}
}
return resolvedTables;
}
/**
* Builds a dependency graph of CTEs and identifies recursive CTEs.
*
* @param tables The list of CTEs to analyze
* @returns Object containing the table map, set of recursive CTEs, and dependency map
*/
buildDependencyGraph(tables) {
// Create a map of table names for quick lookup
const tableMap = new Map();
for (const table of tables) {
tableMap.set(table.aliasExpression.table.name, table);
}
// Identify recursive CTEs (those that reference themselves)
const recursiveCTEs = new Set();
// Build dependency graph: which tables reference which other tables
const dependencies = new Map();
const referencedBy = new Map();
for (const table of tables) {
const tableName = table.aliasExpression.table.name;
// Check for self-references (recursive CTEs)
const referencedTables = this.sourceCollector.collect(table.query);
// Check if this CTE references itself
for (const referencedTable of referencedTables) {
if (referencedTable.table.name === tableName) {
recursiveCTEs.add(tableName);
break;
}
}
// Setup dependencies
if (!dependencies.has(tableName)) {
dependencies.set(tableName, new Set());
}
// Find any references to other CTEs in this table's query
const referencedCTEs = this.cteCollector.collect(table.query);
for (const referencedCTE of referencedCTEs) {
const referencedName = referencedCTE.aliasExpression.table.name;
// Only consider references to tables in our collection
if (tableMap.has(referencedName)) {
dependencies.get(tableName).add(referencedName);
// Add the reverse relationship
if (!referencedBy.has(referencedName)) {
referencedBy.set(referencedName, new Set());
}
referencedBy.get(referencedName).add(tableName);
}
}
}
return { tableMap, recursiveCTEs, dependencies };
}
/**
* Sorts the CTEs using topological sort, with recursive CTEs coming first.
*
* @param tables The list of CTEs to sort
* @param tableMap Map of table names to their CommonTable objects
* @param recursiveCTEs Set of table names that are recursive (self-referential)
* @param dependencies Map of table dependencies
* @returns Sorted list of CTEs
* @throws Error if a circular reference is detected
*/
sortCommonTables(tables, tableMap, recursiveCTEs, dependencies) {
const recursiveResult = [];
const nonRecursiveResult = [];
const visited = new Set();
const visiting = new Set();
// Topological sort function
const visit = (tableName) => {
if (visited.has(tableName))
return;
if (visiting.has(tableName)) {
throw new Error(`Circular reference detected in CTE: ${tableName}`);
}
visiting.add(tableName);
// Process dependencies first (inner CTEs)
const deps = dependencies.get(tableName) || new Set();
for (const dep of deps) {
visit(dep);
}
visiting.delete(tableName);
visited.add(tableName);
// Add this table after its dependencies
// Recursive CTEs go to recursiveResult, others to nonRecursiveResult
if (recursiveCTEs.has(tableName)) {
recursiveResult.push(tableMap.get(tableName));
}
else {
nonRecursiveResult.push(tableMap.get(tableName));
}
};
// Process all tables
for (const table of tables) {
const tableName = table.aliasExpression.table.name;
if (!visited.has(tableName)) {
visit(tableName);
}
}
// Combine the results: recursive CTEs first, then non-recursive CTEs
return [...recursiveResult, ...nonRecursiveResult];
}
}
exports.CTEBuilder = CTEBuilder;
//# sourceMappingURL=CTEBuilder.js.map
;