979 lines
36 KiB
C#
979 lines
36 KiB
C#
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
||
using SQLLinter.Core;
|
||
|
||
namespace SQLLinter.Infrastructure.Diagram;
|
||
|
||
/// <summary>
|
||
/// Билдер диаграммы BPMN из T-SQL AST
|
||
/// </summary>
|
||
public static class BpmnBuilder
|
||
{
|
||
/// <summary>
|
||
/// Сборка диаграммы BPMN из T-SQL AST
|
||
/// </summary>
|
||
/// <param name="fragment"></param>
|
||
/// <returns></returns>
|
||
public static BpmnDiagram Build(TSqlFragment fragment) => Build(fragment, new());
|
||
|
||
/// <summary>
|
||
/// Сборка диаграммы BPMN из T-SQL AST с использованием существующей диаграммы
|
||
/// </summary>
|
||
/// <param name="fragment"></param>
|
||
/// <param name="diagram"></param>
|
||
/// <returns></returns>
|
||
public static BpmnDiagram Build(TSqlFragment fragment, BpmnDiagram diagram)
|
||
{
|
||
var visitor = new BpmnVisitor(diagram);
|
||
fragment.Accept(visitor);
|
||
return visitor.Diagram;
|
||
}
|
||
|
||
private class BpmnVisitor : TSqlFragmentVisitor
|
||
{
|
||
public BpmnDiagram Diagram { get; } = new BpmnDiagram();
|
||
|
||
private Dictionary<string, string> _lastNodeByProcess = new();
|
||
private Stack<BpmnProcess> _processStack = new Stack<BpmnProcess>();
|
||
private int _nodeCounter = 0;
|
||
private const int MaxConditionLength = 50;
|
||
|
||
public BpmnVisitor() : this(new BpmnDiagram()) { }
|
||
|
||
public BpmnVisitor(BpmnDiagram diagram)
|
||
{
|
||
Diagram = diagram;
|
||
_nodeCounter = diagram.Processes.Sum(p => p.Nodes.Count()) + 1;
|
||
}
|
||
|
||
private string NewId() => "n" + (_nodeCounter++);
|
||
|
||
private void AddNode(BpmnProcess proc, BpmnNode node, string? edgeLabel = null)
|
||
{
|
||
if (proc == null) return;
|
||
|
||
if (string.IsNullOrEmpty(node.Id)) node.Id = NewId();
|
||
proc.Nodes.Add(node);
|
||
|
||
if (_lastNodeByProcess.TryGetValue(proc.Id, out var last) && !string.IsNullOrEmpty(last))
|
||
{
|
||
proc.Edges.Add(new BpmnEdge
|
||
{
|
||
From = last,
|
||
To = node.Id,
|
||
Label = edgeLabel ?? string.Empty
|
||
});
|
||
}
|
||
_lastNodeByProcess[proc.Id] = node.Id;
|
||
}
|
||
|
||
private void AddNodeToCurrent(string label, BpmnNodeType type, Dictionary<string, string>? properties = null)
|
||
{
|
||
if (_processStack.Count == 0) return;
|
||
var currentProcess = _processStack.Peek();
|
||
var node = new BpmnNode
|
||
{
|
||
Id = NewId(),
|
||
Label = label,
|
||
Type = type
|
||
};
|
||
|
||
if (properties != null)
|
||
{
|
||
node.Properties = new Dictionary<string, string>(properties);
|
||
}
|
||
|
||
AddNode(currentProcess, node);
|
||
}
|
||
|
||
private void ProcessProcedureOrFunction(TSqlStatement node, string objectType)
|
||
{
|
||
// Извлечение имени объекта на основе AST
|
||
var name = FindSchemaObjectName(node) ?? ($"{objectType}" + Diagram.Processes.Count);
|
||
var pid = SanitizeId(name);
|
||
var proc = new BpmnProcess { Id = pid, Name = $"{name} ({objectType})" };
|
||
|
||
// создать начальный узел
|
||
var start = new BpmnNode { Id = pid + "_start", Label = "Начало", Type = BpmnNodeType.Start };
|
||
proc.Nodes.Add(start);
|
||
_lastNodeByProcess[proc.Id] = start.Id;
|
||
|
||
Diagram.Processes.Add(proc);
|
||
|
||
// Помещаем процесс в стек
|
||
_processStack.Push(proc);
|
||
|
||
// Используем специальный visitor для поиска StatementList
|
||
var statementCollector = new StatementListCollector();
|
||
node.Accept(statementCollector);
|
||
|
||
if (statementCollector.FoundStatementList != null)
|
||
{
|
||
statementCollector.FoundStatementList.Accept(this);
|
||
}
|
||
|
||
// после посещения тела добавляем конечный узел
|
||
var end = new BpmnNode { Id = pid + "_end", Label = "Конец", Type = BpmnNodeType.End };
|
||
AddNode(proc, end);
|
||
|
||
// Убираем процесс из стека
|
||
_processStack.Pop();
|
||
}
|
||
|
||
private class StatementListCollector : TSqlFragmentVisitor
|
||
{
|
||
public StatementList? FoundStatementList { get; private set; }
|
||
|
||
public override void Visit(StatementList node)
|
||
{
|
||
if (FoundStatementList == null)
|
||
{
|
||
FoundStatementList = node;
|
||
}
|
||
}
|
||
}
|
||
|
||
public override void ExplicitVisit(CreateProcedureStatement node) => ProcessProcedureOrFunction(node, "процедура");
|
||
public override void ExplicitVisit(CreateOrAlterProcedureStatement node) => ProcessProcedureOrFunction(node, "процедура");
|
||
public override void ExplicitVisit(AlterProcedureStatement node) => ProcessProcedureOrFunction(node, "процедура");
|
||
|
||
public override void ExplicitVisit(CreateFunctionStatement node) => ProcessProcedureOrFunction(node, "функция");
|
||
public override void ExplicitVisit(CreateOrAlterFunctionStatement node) => ProcessProcedureOrFunction(node, "функция");
|
||
public override void ExplicitVisit(AlterFunctionStatement node) => ProcessProcedureOrFunction(node, "функция");
|
||
|
||
public override void ExplicitVisit(CreateTriggerStatement node) => ProcessProcedureOrFunction(node, "триггер");
|
||
public override void ExplicitVisit(CreateOrAlterTriggerStatement node) => ProcessProcedureOrFunction(node, "триггер");
|
||
public override void ExplicitVisit(AlterTriggerStatement node) => ProcessProcedureOrFunction(node, "триггер");
|
||
|
||
public override void Visit(ExecuteStatement node)
|
||
{
|
||
if (_processStack.Count == 0) return;
|
||
var currentProcess = _processStack.Peek();
|
||
|
||
// Извлечение на основе AST для вызываемого процесса
|
||
var called = FindProcedureNameFromExecute(node);
|
||
var label = called != null ? $"EXEC {called}" : "EXEC";
|
||
|
||
// задача в текущем процессе
|
||
var task = new BpmnNode { Id = NewId(), Label = label!, Type = BpmnNodeType.Subprocess, SubprocessId = called is null ? string.Empty : SanitizeId(called), };
|
||
AddNode(currentProcess, task);
|
||
|
||
// Обходим параметры вызова
|
||
if (node.ExecuteSpecification != null)
|
||
{
|
||
node.ExecuteSpecification.Accept(this);
|
||
}
|
||
}
|
||
|
||
public override void Visit(FunctionCall node)
|
||
{
|
||
if (_processStack.Count == 0) return;
|
||
var currentProcess = _processStack.Peek();
|
||
|
||
// Извлекаем имя функции
|
||
var functionName = node.FunctionName?.Value ?? "unknown";
|
||
var schemaPrefix = "";
|
||
|
||
// Проверяем, есть ли схема в имени через MultiPartIdentifier
|
||
if (node.CallTarget is MultiPartIdentifierCallTarget multiPartCallTarget)
|
||
{
|
||
schemaPrefix = GetMultiPartIdentifierName(multiPartCallTarget.MultiPartIdentifier);
|
||
}
|
||
|
||
var fullFunctionName = !string.IsNullOrEmpty(schemaPrefix)
|
||
? $"{schemaPrefix}.{functionName}"
|
||
: functionName;
|
||
|
||
// Проверяем, является ли это пользовательской функцией
|
||
var isUserFunction = IsUserDefinedFunction(fullFunctionName);
|
||
|
||
var label = isUserFunction ? $"FUNC {fullFunctionName}" : $"{functionName}()";
|
||
var nodeType = isUserFunction ? BpmnNodeType.Subprocess : BpmnNodeType.Task;
|
||
|
||
var funcNode = new BpmnNode
|
||
{
|
||
Id = NewId(),
|
||
Label = label,
|
||
Type = nodeType,
|
||
SubprocessId = isUserFunction ? SanitizeId(fullFunctionName) : string.Empty,
|
||
};
|
||
AddNode(currentProcess, funcNode);
|
||
}
|
||
|
||
private bool IsUserDefinedFunction(string functionName)
|
||
{
|
||
// Проверяем, есть ли такая функция в нашей диаграмме
|
||
var isInDiagram = Diagram.Processes.Any(p =>
|
||
p.Name.Contains(functionName, StringComparison.OrdinalIgnoreCase));
|
||
|
||
// Если функция есть в диаграмме, это пользовательская функция
|
||
if (isInDiagram) return true;
|
||
|
||
// Если функция не встроенная и имеет префикс схемы (например, dbo.), считаем ее пользовательской
|
||
if (functionName.Contains('.') && !Constants.SystemFunctions.Contains(functionName.Split('.')[1]))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
return !Constants.SystemFunctions.Contains(functionName);
|
||
}
|
||
|
||
public override void Visit(IfStatement node)
|
||
{
|
||
if (_processStack.Count == 0) return;
|
||
var currentProcess = _processStack.Peek();
|
||
|
||
// Извлекаем условие IF
|
||
var conditionText = GetConditionText(node.Predicate);
|
||
var truncatedCondition = TruncateText(conditionText, MaxConditionLength);
|
||
|
||
var properties = new Dictionary<string, string>
|
||
{
|
||
["condition"] = conditionText,
|
||
["condition_display"] = truncatedCondition
|
||
};
|
||
|
||
// Создаем узел шлюза для IF с условием
|
||
var gid = new BpmnNode
|
||
{
|
||
Id = NewId(),
|
||
Label = $"IF ({truncatedCondition})",
|
||
Type = BpmnNodeType.Gateway,
|
||
Properties = properties
|
||
};
|
||
AddNode(currentProcess, gid);
|
||
|
||
// Создаем узел ENDIF для объединения веток
|
||
var endid = new BpmnNode { Id = NewId(), Label = "ENDIF", Type = BpmnNodeType.Gateway };
|
||
currentProcess.Nodes.Add(endid);
|
||
|
||
// Обработка THEN ветки
|
||
if (node.ThenStatement != null)
|
||
{
|
||
_lastNodeByProcess[currentProcess.Id] = gid.Id;
|
||
var thenNode = new BpmnNode { Id = NewId(), Label = "THEN", Type = BpmnNodeType.Task };
|
||
AddNode(currentProcess, thenNode, edgeLabel: $"Да");
|
||
|
||
// Ручной обход THEN ветки
|
||
TraverseStatement(node.ThenStatement, currentProcess);
|
||
|
||
// После обработки THEN ветки добавляем край к ENDIF
|
||
if (_lastNodeByProcess.TryGetValue(currentProcess.Id, out var thenLastNode))
|
||
{
|
||
currentProcess.Edges.Add(new BpmnEdge
|
||
{
|
||
From = thenLastNode,
|
||
To = endid.Id
|
||
});
|
||
}
|
||
}
|
||
|
||
// Обработка ELSE ветки
|
||
if (node.ElseStatement != null)
|
||
{
|
||
_lastNodeByProcess[currentProcess.Id] = gid.Id;
|
||
var elseNode = new BpmnNode { Id = NewId(), Label = "ELSE", Type = BpmnNodeType.Task };
|
||
AddNode(currentProcess, elseNode, edgeLabel: $"Нет");
|
||
|
||
// Ручной обход ELSE ветки
|
||
TraverseStatement(node.ElseStatement, currentProcess);
|
||
|
||
// После обработки ELSE ветки добавляем край к ENDIF
|
||
if (_lastNodeByProcess.TryGetValue(currentProcess.Id, out var elseLastNode))
|
||
{
|
||
currentProcess.Edges.Add(new BpmnEdge
|
||
{
|
||
From = elseLastNode,
|
||
To = endid.Id
|
||
});
|
||
}
|
||
}
|
||
|
||
// Восстанавливаем последний узел после обработки IF
|
||
_lastNodeByProcess[currentProcess.Id] = endid.Id;
|
||
}
|
||
|
||
public override void Visit(MergeStatement node)
|
||
{
|
||
if (_processStack.Count == 0) return;
|
||
var currentProcess = _processStack.Peek();
|
||
|
||
var target = node.MergeSpecification.Target != null ? FindTargetNameForDml(node.MergeSpecification.Target) : "?";
|
||
var label = $"MERGE -> {target}";
|
||
|
||
AddNodeToCurrent(label, BpmnNodeType.Task);
|
||
}
|
||
|
||
public override void Visit(SetVariableStatement node)
|
||
{
|
||
if (_processStack.Count == 0) return;
|
||
var currentProcess = _processStack.Peek();
|
||
|
||
// Получаем имя переменной
|
||
var variableName = node.Variable?.Name ?? "?";
|
||
|
||
// Получаем текст выражения
|
||
var expressionText = GetExpressionText(node.Expression);
|
||
var truncatedExpression = TruncateText(expressionText, MaxConditionLength);
|
||
|
||
// Определяем оператор присваивания на основе AssignmentKind
|
||
string assignmentOp = "=";
|
||
|
||
assignmentOp = node.AssignmentKind switch
|
||
{
|
||
AssignmentKind.Equals => "=",
|
||
AssignmentKind.AddEquals => "+=",
|
||
AssignmentKind.SubtractEquals => "-=",
|
||
AssignmentKind.MultiplyEquals => "*=",
|
||
AssignmentKind.DivideEquals => "/=",
|
||
AssignmentKind.ModEquals => "%=",
|
||
AssignmentKind.BitwiseAndEquals => "&=",
|
||
AssignmentKind.BitwiseOrEquals => "|=",
|
||
AssignmentKind.BitwiseXorEquals => "^=",
|
||
_ => "="
|
||
};
|
||
|
||
// Проверяем, содержит ли выражение вызов функции
|
||
var containsFunctionCall = false;
|
||
if (node.Expression != null)
|
||
{
|
||
var functionFinder = new FunctionCallFinder();
|
||
node.Expression.Accept(functionFinder);
|
||
containsFunctionCall = functionFinder.FunctionCalls.Any(fc => IsUserDefinedFunction(fc.FunctionName?.Value ?? ""));
|
||
}
|
||
|
||
var label = $"SET {variableName} {assignmentOp} {truncatedExpression}";
|
||
|
||
var properties = new Dictionary<string, string>
|
||
{
|
||
["variable"] = variableName,
|
||
["expression"] = expressionText,
|
||
["expression_display"] = truncatedExpression,
|
||
["assignment_kind"] = node.AssignmentKind.ToString(),
|
||
["contains_function_call"] = containsFunctionCall.ToString()
|
||
};
|
||
|
||
AddNodeToCurrent(label, BpmnNodeType.Task, properties);
|
||
|
||
// Обходим выражение для поиска вызовов функций
|
||
if (node.Expression != null)
|
||
{
|
||
node.Expression.Accept(this);
|
||
}
|
||
}
|
||
|
||
public override void Visit(SetCommandStatement node)
|
||
{
|
||
if (_processStack.Count == 0) return;
|
||
var currentProcess = _processStack.Peek();
|
||
|
||
var commandText = GetCommandText(node);
|
||
var truncatedCommand = TruncateText(commandText, MaxConditionLength);
|
||
|
||
var label = $"SET {truncatedCommand}";
|
||
|
||
var properties = new Dictionary<string, string>
|
||
{
|
||
["command"] = commandText,
|
||
["command_display"] = truncatedCommand
|
||
};
|
||
|
||
AddNodeToCurrent(label, BpmnNodeType.Task, properties);
|
||
}
|
||
|
||
public override void Visit(GoToStatement node)
|
||
{
|
||
if (_processStack.Count == 0) return;
|
||
var currentProcess = _processStack.Peek();
|
||
|
||
var label = node.LabelName?.Value ?? "?";
|
||
var gotoNode = new BpmnNode
|
||
{
|
||
Id = NewId(),
|
||
Label = $"GOTO {label}",
|
||
Type = BpmnNodeType.Task
|
||
};
|
||
AddNode(currentProcess, gotoNode);
|
||
}
|
||
|
||
public override void Visit(TryCatchStatement node)
|
||
{
|
||
if (_processStack.Count == 0) return;
|
||
var currentProcess = _processStack.Peek();
|
||
|
||
var tryNode = new BpmnNode { Id = NewId(), Label = "TRY", Type = BpmnNodeType.Gateway };
|
||
var catchNode = new BpmnNode { Id = NewId(), Label = "CATCH", Type = BpmnNodeType.Gateway };
|
||
var endTryCatch = new BpmnNode { Id = NewId(), Label = "END TRY/CATCH", Type = BpmnNodeType.Gateway };
|
||
|
||
AddNode(currentProcess, tryNode);
|
||
currentProcess.Nodes.Add(catchNode);
|
||
currentProcess.Nodes.Add(endTryCatch);
|
||
|
||
// TRY блок
|
||
if (node.TryStatements != null)
|
||
{
|
||
_lastNodeByProcess[currentProcess.Id] = tryNode.Id;
|
||
foreach (var statement in node.TryStatements.Statements)
|
||
{
|
||
statement.Accept(this);
|
||
}
|
||
|
||
// Ребро от последнего узла TRY к endTryCatch
|
||
if (_lastNodeByProcess.TryGetValue(currentProcess.Id, out var tryLast))
|
||
{
|
||
currentProcess.Edges.Add(new BpmnEdge
|
||
{
|
||
From = tryLast,
|
||
To = endTryCatch.Id
|
||
});
|
||
}
|
||
}
|
||
|
||
// CATCH блок
|
||
if (node.CatchStatements != null)
|
||
{
|
||
_lastNodeByProcess[currentProcess.Id] = catchNode.Id;
|
||
currentProcess.Edges.Add(new BpmnEdge
|
||
{
|
||
From = tryNode.Id,
|
||
To = catchNode.Id,
|
||
Label = "Ошибка"
|
||
});
|
||
|
||
foreach (var statement in node.CatchStatements.Statements)
|
||
{
|
||
statement.Accept(this);
|
||
}
|
||
|
||
// Ребро от последнего узла CATCH к endTryCatch
|
||
if (_lastNodeByProcess.TryGetValue(currentProcess.Id, out var catchLast))
|
||
{
|
||
currentProcess.Edges.Add(new BpmnEdge
|
||
{
|
||
From = catchLast,
|
||
To = endTryCatch.Id
|
||
});
|
||
}
|
||
}
|
||
|
||
_lastNodeByProcess[currentProcess.Id] = endTryCatch.Id;
|
||
}
|
||
|
||
public override void Visit(WhileStatement node)
|
||
{
|
||
if (_processStack.Count == 0) return;
|
||
var currentProcess = _processStack.Peek();
|
||
|
||
// Извлекаем условие WHILE
|
||
var conditionText = GetConditionText(node.Predicate);
|
||
var truncatedCondition = TruncateText(conditionText, MaxConditionLength);
|
||
|
||
var properties = new Dictionary<string, string>
|
||
{
|
||
["condition"] = conditionText,
|
||
["condition_display"] = truncatedCondition,
|
||
["loop_type"] = "WHILE"
|
||
};
|
||
|
||
// Узел начала цикла
|
||
var loopStart = new BpmnNode
|
||
{
|
||
Id = NewId(),
|
||
Label = $"WHILE ({truncatedCondition})",
|
||
Type = BpmnNodeType.Hexagon,
|
||
Properties = properties
|
||
};
|
||
AddNode(currentProcess, loopStart);
|
||
|
||
// Сохраняем текущий последний узел
|
||
var savedLastNode = _lastNodeByProcess[currentProcess.Id];
|
||
|
||
// Обработка тела цикла
|
||
if (node.Statement != null)
|
||
{
|
||
// Добавляем ребро от gateway к первому узлу тела цикла
|
||
var bodyStartNode = new BpmnNode
|
||
{
|
||
Id = NewId(),
|
||
Label = "Тело цикла",
|
||
Type = BpmnNodeType.Task
|
||
};
|
||
AddNode(currentProcess, bodyStartNode, "Вход");
|
||
|
||
// Обход тела цикла
|
||
TraverseStatement(node.Statement, currentProcess);
|
||
|
||
// Добавляем ребро возврата в начало цикла от последнего узла тела
|
||
if (_lastNodeByProcess.TryGetValue(currentProcess.Id, out var loopBodyLast))
|
||
{
|
||
currentProcess.Edges.Add(new BpmnEdge
|
||
{
|
||
From = loopBodyLast,
|
||
To = loopStart.Id,
|
||
Label = "Повтор"
|
||
});
|
||
}
|
||
}
|
||
|
||
// Узел конца цикла (выход, когда условие ложно)
|
||
var loopEnd = new BpmnNode { Id = NewId(), Label = "END WHILE", Type = BpmnNodeType.Gateway };
|
||
currentProcess.Nodes.Add(loopEnd);
|
||
|
||
// Ребро выхода из цикла
|
||
currentProcess.Edges.Add(new BpmnEdge
|
||
{
|
||
From = loopStart.Id,
|
||
To = loopEnd.Id,
|
||
Label = "Выход"
|
||
});
|
||
|
||
// Восстанавливаем последний узел
|
||
_lastNodeByProcess[currentProcess.Id] = loopEnd.Id;
|
||
}
|
||
|
||
public override void Visit(BreakStatement node)
|
||
{
|
||
if (_processStack.Count == 0) return;
|
||
var currentProcess = _processStack.Peek();
|
||
|
||
var breakNode = new BpmnNode
|
||
{
|
||
Id = NewId(),
|
||
Label = "BREAK",
|
||
Type = BpmnNodeType.Task
|
||
};
|
||
AddNode(currentProcess, breakNode);
|
||
}
|
||
|
||
public override void Visit(ContinueStatement node)
|
||
{
|
||
if (_processStack.Count == 0) return;
|
||
var currentProcess = _processStack.Peek();
|
||
|
||
var continueNode = new BpmnNode
|
||
{
|
||
Id = NewId(),
|
||
Label = "CONTINUE",
|
||
Type = BpmnNodeType.Task
|
||
};
|
||
AddNode(currentProcess, continueNode);
|
||
}
|
||
|
||
public override void Visit(ReturnStatement node)
|
||
{
|
||
if (_processStack.Count == 0) return;
|
||
var currentProcess = _processStack.Peek();
|
||
|
||
var label = "RETURN";
|
||
if (node.Expression != null)
|
||
{
|
||
var exprText = GetExpressionText(node.Expression);
|
||
var truncatedExpr = TruncateText(exprText, MaxConditionLength);
|
||
label = $"RETURN {truncatedExpr}";
|
||
}
|
||
|
||
var returnNode = new BpmnNode
|
||
{
|
||
Id = NewId(),
|
||
Label = label,
|
||
Type = BpmnNodeType.Task
|
||
};
|
||
AddNode(currentProcess, returnNode);
|
||
}
|
||
|
||
public override void Visit(DeclareVariableStatement node)
|
||
{
|
||
if (_processStack.Count == 0) return;
|
||
var currentProcess = _processStack.Peek();
|
||
|
||
var declarations = new List<string>();
|
||
foreach (var declaration in node.Declarations)
|
||
{
|
||
var varName = declaration.VariableName?.Value ?? "?";
|
||
var dataType = declaration.DataType?.Name?.BaseIdentifier?.Value ?? "?";
|
||
declarations.Add($"{varName} {dataType}");
|
||
}
|
||
|
||
var label = "DECLARE";
|
||
if (declarations.Count > 0)
|
||
{
|
||
var declText = string.Join(", ", declarations);
|
||
var truncatedDecl = TruncateText(declText, MaxConditionLength);
|
||
label = $"DECLARE {truncatedDecl}";
|
||
}
|
||
|
||
AddNodeToCurrent(label, BpmnNodeType.Task);
|
||
}
|
||
|
||
public override void Visit(BeginEndBlockStatement node)
|
||
{
|
||
if (_processStack.Count == 0) return;
|
||
var currentProcess = _processStack.Peek();
|
||
|
||
// Просто обходим все операторы внутри блока
|
||
if (node.StatementList != null)
|
||
{
|
||
foreach (var statement in node.StatementList.Statements)
|
||
{
|
||
statement.Accept(this);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void TraverseStatement(TSqlFragment statement, BpmnProcess process)
|
||
{
|
||
if (statement is StatementList statementList)
|
||
{
|
||
foreach (var stmt in statementList.Statements)
|
||
{
|
||
stmt.Accept(this);
|
||
}
|
||
}
|
||
else if (statement is BeginEndBlockStatement beginEnd)
|
||
{
|
||
// Для блоков BEGIN...END обходим внутренние statements
|
||
if (beginEnd.StatementList != null)
|
||
{
|
||
foreach (var stmt in beginEnd.StatementList.Statements)
|
||
{
|
||
stmt.Accept(this);
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
statement.Accept(this);
|
||
}
|
||
}
|
||
|
||
public override void Visit(SelectStatement node)
|
||
{
|
||
if (_processStack.Count == 0) return;
|
||
var currentProcess = _processStack.Peek();
|
||
|
||
var label = "SELECT";
|
||
if (node.QueryExpression is QuerySpecification qs && qs.SelectElements != null)
|
||
{
|
||
label = $"SELECT ({qs.SelectElements.Count} полей)";
|
||
}
|
||
|
||
AddNodeToCurrent(label, BpmnNodeType.Task);
|
||
|
||
// Обходим все элементы SELECT для поиска вызовов функций
|
||
if (node.QueryExpression != null)
|
||
{
|
||
node.QueryExpression.Accept(this);
|
||
}
|
||
}
|
||
|
||
public override void Visit(InsertStatement node)
|
||
{
|
||
if (_processStack.Count == 0) return;
|
||
var currentProcess = _processStack.Peek();
|
||
|
||
var target = FindTargetNameForDml(node);
|
||
var label = "INSERT" + (string.IsNullOrEmpty(target) ? string.Empty : $" -> {target}");
|
||
AddNodeToCurrent(label, BpmnNodeType.Task);
|
||
}
|
||
|
||
public override void Visit(UpdateStatement node)
|
||
{
|
||
if (_processStack.Count == 0) return;
|
||
var currentProcess = _processStack.Peek();
|
||
|
||
var target = FindTargetNameForDml(node);
|
||
var label = "UPDATE" + (string.IsNullOrEmpty(target) ? string.Empty : $" -> {target}");
|
||
AddNodeToCurrent(label, BpmnNodeType.Task);
|
||
}
|
||
|
||
public override void Visit(DeleteStatement node)
|
||
{
|
||
if (_processStack.Count == 0) return;
|
||
var currentProcess = _processStack.Peek();
|
||
|
||
var target = FindTargetNameForDml(node);
|
||
var label = "DELETE" + (string.IsNullOrEmpty(target) ? string.Empty : $" -> {target}");
|
||
AddNodeToCurrent(label, BpmnNodeType.Task);
|
||
}
|
||
|
||
// Вспомогательные методы для извлечения текста
|
||
private string GetConditionText(BooleanExpression expression)
|
||
{
|
||
if (expression == null) return "";
|
||
|
||
try
|
||
{
|
||
// Пробуем получить через ScriptTokenStream
|
||
var tokens = expression.ScriptTokenStream;
|
||
if (tokens != null && expression.FirstTokenIndex >= 0 &&
|
||
expression.LastTokenIndex >= expression.FirstTokenIndex)
|
||
{
|
||
var text = string.Join("",
|
||
tokens.Skip(expression.FirstTokenIndex)
|
||
.Take(expression.LastTokenIndex - expression.FirstTokenIndex + 1)
|
||
.Select(t => t.Text));
|
||
return text.Trim();
|
||
}
|
||
|
||
// Fallback: используем ToString()
|
||
return expression.ToString()?.Trim() ?? "";
|
||
}
|
||
catch
|
||
{
|
||
return "";
|
||
}
|
||
}
|
||
|
||
private string GetExpressionText(ScalarExpression expression)
|
||
{
|
||
if (expression == null) return "";
|
||
|
||
try
|
||
{
|
||
var tokens = expression.ScriptTokenStream;
|
||
if (tokens != null && expression.FirstTokenIndex >= 0 && expression.LastTokenIndex >= expression.FirstTokenIndex)
|
||
{
|
||
var text = string.Join("",
|
||
tokens.Skip(expression.FirstTokenIndex)
|
||
.Take(expression.LastTokenIndex - expression.FirstTokenIndex + 1)
|
||
.Select(t => t.Text));
|
||
return text.Trim();
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// Игнорируем ошибки парсинга
|
||
}
|
||
|
||
return expression.ToString() ?? "";
|
||
}
|
||
|
||
private string GetCommandText(SetCommandStatement node)
|
||
{
|
||
if (node == null) return "";
|
||
|
||
try
|
||
{
|
||
var tokens = node.ScriptTokenStream;
|
||
if (tokens != null && node.FirstTokenIndex >= 0 && node.LastTokenIndex >= node.FirstTokenIndex)
|
||
{
|
||
var text = string.Join("",
|
||
tokens.Skip(node.FirstTokenIndex)
|
||
.Take(node.LastTokenIndex - node.FirstTokenIndex + 1)
|
||
.Select(t => t.Text));
|
||
return text.Trim();
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// Игнорируем ошибки парсинга
|
||
}
|
||
|
||
return node.ToString() ?? "";
|
||
}
|
||
|
||
private string TruncateText(string text, int maxLength)
|
||
{
|
||
if (string.IsNullOrEmpty(text)) return "";
|
||
if (text.Length <= maxLength) return text;
|
||
|
||
return text.Substring(0, maxLength - 3) + "...";
|
||
}
|
||
|
||
// AST helpers
|
||
private static string? FindSchemaObjectName(TSqlFragment fragment)
|
||
{
|
||
var finder = new SchemaNameFinder();
|
||
fragment.Accept(finder);
|
||
return finder.FoundName;
|
||
}
|
||
|
||
private static string? FindProcedureNameFromExecute(ExecuteStatement node)
|
||
{
|
||
// Prefer ProcedureReference inside the statement
|
||
var finder = new ProcedureReferenceFinder();
|
||
node.Accept(finder);
|
||
return finder.FoundName;
|
||
}
|
||
|
||
private static string? FindTargetNameForDml(TSqlFragment fragment)
|
||
{
|
||
var finder = new TargetTableFinder();
|
||
fragment.Accept(finder);
|
||
return finder.FoundName;
|
||
}
|
||
|
||
private class SchemaNameFinder : TSqlFragmentVisitor
|
||
{
|
||
public string? FoundName { get; private set; }
|
||
|
||
public override void Visit(SchemaObjectName node)
|
||
{
|
||
if (FoundName == null)
|
||
{
|
||
FoundName = GetSchemaObjectName(node);
|
||
}
|
||
base.Visit(node);
|
||
}
|
||
|
||
public override void Visit(ProcedureReferenceName node)
|
||
{
|
||
if (FoundName == null && node.ProcedureReference?.Name != null)
|
||
{
|
||
FoundName = GetSchemaObjectName(node.ProcedureReference.Name);
|
||
}
|
||
base.Visit(node);
|
||
}
|
||
|
||
private static string GetSchemaObjectName(SchemaObjectName node)
|
||
{
|
||
try
|
||
{
|
||
if (node.Identifiers != null && node.Identifiers.Count > 0)
|
||
{
|
||
return string.Join('.', node.Identifiers.Select(id => "[" + id.Value + "]"));
|
||
}
|
||
return "[" + (node.ToString() ?? "unknown") + "]";
|
||
}
|
||
catch
|
||
{
|
||
return "[unknown]";
|
||
}
|
||
}
|
||
}
|
||
|
||
private class ProcedureReferenceFinder : TSqlFragmentVisitor
|
||
{
|
||
public string? FoundName { get; private set; }
|
||
public override void Visit(ProcedureReference node)
|
||
{
|
||
if (FoundName == null)
|
||
{
|
||
try
|
||
{
|
||
if (node.Name != null)
|
||
{
|
||
if (node.Name.Identifiers != null && node.Name.Identifiers.Count > 0)
|
||
FoundName = string.Join('.', node.Name.Identifiers.Select(id => "[" + id.Value + "]"));
|
||
else
|
||
FoundName = "[" + node.Name.ToString() + "]";
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
FoundName = "[" + node.Name.ToString() + "]";
|
||
}
|
||
}
|
||
base.Visit(node);
|
||
}
|
||
|
||
public override void Visit(ExecuteSpecification node)
|
||
{
|
||
base.Visit(node);
|
||
}
|
||
}
|
||
|
||
private class TargetTableFinder : TSqlFragmentVisitor
|
||
{
|
||
public string? FoundName { get; private set; }
|
||
private int _bestDepth = int.MaxValue;
|
||
private int _depth = 0;
|
||
|
||
public override void Visit(TSqlFragment node)
|
||
{
|
||
_depth++;
|
||
base.Visit(node);
|
||
_depth--;
|
||
}
|
||
|
||
public override void Visit(NamedTableReference node)
|
||
{
|
||
if (node == null) return;
|
||
int depth = _depth;
|
||
if (depth < _bestDepth)
|
||
{
|
||
_bestDepth = depth;
|
||
try
|
||
{
|
||
if (node.SchemaObject != null && node.SchemaObject.Identifiers != null && node.SchemaObject.Identifiers.Count > 0)
|
||
FoundName = string.Join('.', node.SchemaObject.Identifiers.Select(id => "[" + id.Value + "]"));
|
||
else
|
||
FoundName = "[" + node.SchemaObject?.ToString() ?? node.ToString() + "]";
|
||
|
||
if (node.Alias != null && !string.IsNullOrWhiteSpace(node.Alias.Value))
|
||
FoundName += " AS [" + node.Alias.Value + "]";
|
||
}
|
||
catch
|
||
{
|
||
FoundName = "[" + node.SchemaObject?.ToString() ?? node.ToString() + "]";
|
||
}
|
||
}
|
||
|
||
base.Visit(node);
|
||
}
|
||
|
||
public override void Visit(VariableTableReference node)
|
||
{
|
||
if (node == null) return;
|
||
int depth = _depth;
|
||
if (depth < _bestDepth)
|
||
{
|
||
_bestDepth = depth;
|
||
FoundName = node.Variable?.ToString();
|
||
}
|
||
base.Visit(node);
|
||
}
|
||
|
||
public override void Visit(QueryDerivedTable node)
|
||
{
|
||
if (node == null) return;
|
||
int depth = _depth;
|
||
if (depth < _bestDepth && node.Alias != null && !string.IsNullOrWhiteSpace(node.Alias.Value))
|
||
{
|
||
_bestDepth = depth;
|
||
FoundName = node.Alias.Value + " (derived)";
|
||
}
|
||
|
||
// still traverse inside
|
||
base.Visit(node);
|
||
}
|
||
|
||
public override void Visit(CommonTableExpression node)
|
||
{
|
||
// CTE definitions: prefer not to pick CTE as target unless no other table found
|
||
base.Visit(node);
|
||
}
|
||
}
|
||
|
||
private static string SanitizeId(string s)
|
||
{
|
||
if (string.IsNullOrEmpty(s)) return "id";
|
||
var sb = new System.Text.StringBuilder();
|
||
foreach (var ch in s)
|
||
{
|
||
if (char.IsLetterOrDigit(ch)) sb.Append(ch);
|
||
else sb.Append('_');
|
||
}
|
||
return sb.ToString();
|
||
}
|
||
|
||
private class FunctionCallFinder : TSqlFragmentVisitor
|
||
{
|
||
public List<FunctionCall> FunctionCalls { get; } = new List<FunctionCall>();
|
||
|
||
public override void Visit(FunctionCall node)
|
||
{
|
||
FunctionCalls.Add(node);
|
||
base.Visit(node);
|
||
}
|
||
}
|
||
|
||
private string GetMultiPartIdentifierName(MultiPartIdentifier identifier)
|
||
{
|
||
if (identifier != null && identifier.Identifiers != null && identifier.Identifiers.Count > 0)
|
||
{
|
||
return string.Join(".", identifier.Identifiers.Select(id => id.Value));
|
||
}
|
||
return "";
|
||
}
|
||
}
|
||
} |