rawsql-ts
Version:
[beta]High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.
153 lines • 7.3 kB
JavaScript
import { SelectQueryParser } from '../parsers/SelectQueryParser';
import { SimpleSelectQuery } from '../models/SimpleSelectQuery';
import { BinarySelectQuery } from '../models/BinarySelectQuery';
import { DataFlowGraph } from '../reporting/models/DataFlowGraph';
import { DataSourceHandler } from '../reporting/services/DataSourceHandler';
import { JoinHandler } from '../reporting/services/JoinHandler';
import { ProcessHandler } from '../reporting/services/ProcessHandler';
import { CTEHandler } from '../reporting/services/CTEHandler';
/**
* QueryFlowDiagramGenerator using model-based architecture
* Generates Mermaid diagrams from SQL queries following consistent principles
*/
export class QueryFlowDiagramGenerator {
constructor() {
this.graph = new DataFlowGraph();
this.dataSourceHandler = new DataSourceHandler(this.graph);
this.joinHandler = new JoinHandler(this.graph, this.dataSourceHandler);
this.processHandler = new ProcessHandler(this.graph, this.dataSourceHandler);
this.cteHandler = new CTEHandler(this.graph);
}
generateMermaidFlow(query, options) {
// Reset state for new diagram generation
this.graph = new DataFlowGraph();
this.dataSourceHandler = new DataSourceHandler(this.graph);
this.joinHandler = new JoinHandler(this.graph, this.dataSourceHandler);
this.processHandler = new ProcessHandler(this.graph, this.dataSourceHandler);
this.cteHandler = new CTEHandler(this.graph);
this.joinHandler.resetJoinCounter();
// Parse SQL if string
const parsedQuery = typeof query === 'string'
? SelectQueryParser.parse(query)
: query;
// Process the query
const cteNames = new Set();
this.processQuery(parsedQuery, 'main', cteNames);
// Generate Mermaid output
return this.graph.generateMermaid((options === null || options === void 0 ? void 0 : options.direction) || 'TD', options === null || options === void 0 ? void 0 : options.title);
}
static generate(sql) {
const generator = new QueryFlowDiagramGenerator();
return generator.generateMermaidFlow(sql);
}
processQuery(query, context, cteNames) {
if (query instanceof SimpleSelectQuery) {
return this.processSimpleQuery(query, context, cteNames);
}
else if (query instanceof BinarySelectQuery) {
return this.processBinaryQuery(query, context, cteNames);
}
throw new Error('Unsupported query type');
}
processSimpleQuery(query, context, cteNames) {
// Process CTEs first
if (query.withClause) {
this.cteHandler.processCTEs(query.withClause, cteNames, this.processQuery.bind(this));
}
let currentNodeId = '';
// 1. Process FROM clause (including JOINs)
if (query.fromClause) {
const hasJoins = query.fromClause.joins && query.fromClause.joins.length > 0;
if (hasJoins) {
// If there are JOINs, process normally (data source -> JOIN -> SELECT)
currentNodeId = this.joinHandler.processFromClause(query.fromClause, cteNames, this.processQuery.bind(this));
}
else {
// If no JOINs, connect data source directly to SELECT
currentNodeId = this.dataSourceHandler.processSource(query.fromClause.source, cteNames, this.processQuery.bind(this));
}
}
// 2-7. Process other clauses in execution order
if (currentNodeId) {
currentNodeId = this.processHandler.processQueryClauses(query, context, currentNodeId, cteNames, this.processQuery.bind(this));
}
// Handle output node creation based on context
return this.handleOutputNode(currentNodeId, context);
}
processBinaryQuery(query, context, cteNames) {
// Check if this is a chain of the same operation
const parts = this.flattenBinaryChain(query, query.operator.value);
if (parts.length > 2) {
return this.processMultiPartOperation(parts, query.operator.value, context, cteNames);
}
else {
return this.processSimpleBinaryOperation(query, context, cteNames);
}
}
processSimpleBinaryOperation(query, context, cteNames) {
const leftNodeId = this.processQuery(query.left, `${context}_left`, cteNames);
const rightNodeId = this.processQuery(query.right, `${context}_right`, cteNames);
// Create operation node with unique ID based on context
const operationId = context === 'main' ? 'main' : context.replace(/^cte_/, '');
const operationNode = this.graph.createSetOperationNode(operationId, query.operator.value);
// Connect left and right to operation
if (leftNodeId && !this.graph.hasConnection(leftNodeId, operationNode.id)) {
this.graph.addConnection(leftNodeId, operationNode.id);
}
if (rightNodeId && !this.graph.hasConnection(rightNodeId, operationNode.id)) {
this.graph.addConnection(rightNodeId, operationNode.id);
}
// Return the operation node directly without adding SELECT
return operationNode.id;
}
processMultiPartOperation(parts, operator, context, cteNames) {
const partNodes = [];
// Use context to create unique operation ID
const operationId = context === 'main' ? 'main' : context.replace(/^cte_/, '');
const operationNode = this.graph.createSetOperationNode(operationId, operator);
// Process each part with numbered naming
for (let i = 0; i < parts.length; i++) {
const partContext = `${context}_part${i + 1}`;
const partNodeId = this.processQuery(parts[i], partContext, cteNames);
partNodes.push(partNodeId);
}
// Connect all parts to operation
for (const partNodeId of partNodes) {
if (partNodeId && !this.graph.hasConnection(partNodeId, operationNode.id)) {
this.graph.addConnection(partNodeId, operationNode.id);
}
}
// Return the operation node directly without adding SELECT
return operationNode.id;
}
handleOutputNode(currentNodeId, context) {
// Simple principle: Only create output node for main query
// All other contexts return their processing result directly
if (context === 'main') {
const outputNode = this.graph.createOutputNode(context);
if (currentNodeId) {
this.graph.addConnection(currentNodeId, outputNode.id);
}
return outputNode.id;
}
return currentNodeId;
}
/**
* Flattens a binary operation chain into individual parts
*/
flattenBinaryChain(query, operator) {
const parts = [];
const collectParts = (q) => {
if (q instanceof BinarySelectQuery && q.operator.value === operator) {
collectParts(q.left);
collectParts(q.right);
}
else {
parts.push(q);
}
};
collectParts(query);
return parts;
}
}
//# sourceMappingURL=QueryFlowDiagramGenerator.js.map