diff --git a/SQLLinter.CLI/Program.cs b/SQLLinter.CLI/Program.cs
index 9ee73ca..f7056e2 100644
--- a/SQLLinter.CLI/Program.cs
+++ b/SQLLinter.CLI/Program.cs
@@ -54,7 +54,7 @@ namespace SQLLinter.CLI
var diagramer = new Diagramer(bpmn, fragmentBuilder, sqlStreamReaderBuilder);
- using (StreamReader reader = new StreamReader(@"C:\Users\frost\Downloads\Telegram Desktop\tdostdetail.sql"))
+ using (StreamReader reader = new StreamReader(@"C:\Users\frost\Downloads\Telegram Desktop\test.sql"))
{
linter.Run("test.sql", reader.BaseStream);
diagramer.Run("test.sql", reader.BaseStream);
diff --git a/SQLLinter/Infrastructure/Diagram/BpmnArrowType.cs b/SQLLinter/Infrastructure/Diagram/BpmnArrowType.cs
new file mode 100644
index 0000000..c3822b7
--- /dev/null
+++ b/SQLLinter/Infrastructure/Diagram/BpmnArrowType.cs
@@ -0,0 +1,12 @@
+namespace SQLLinter.Infrastructure.Diagram;
+
+///
+/// Типы связей между узлами BPMN
+///
+public enum BpmnArrowType
+{
+ /// Обычная связь
+ Default,
+ /// Пунктирная связь (например, для вызова подпроцесса)
+ Dashed,
+}
diff --git a/SQLLinter/Infrastructure/Diagram/BpmnBuilder.cs b/SQLLinter/Infrastructure/Diagram/BpmnBuilder.cs
index 05abed0..c51eb1e 100644
--- a/SQLLinter/Infrastructure/Diagram/BpmnBuilder.cs
+++ b/SQLLinter/Infrastructure/Diagram/BpmnBuilder.cs
@@ -1,51 +1,31 @@
-using Microsoft.SqlServer.TransactSql.ScriptDom;
+using Microsoft.SqlServer.TransactSql.ScriptDom;
+using SQLLinter.Core;
namespace SQLLinter.Infrastructure.Diagram;
-public enum BpmnNodeType
-{
- Start,
- End,
- Task,
- Gateway,
- Subprocess,
-}
-
-public class BpmnNode
-{
- public string Id { get; set; } = string.Empty;
- public string Label { get; set; } = string.Empty;
- public BpmnNodeType Type { get; set; }
-}
-
-public class BpmnEdge
-{
- public string From { get; set; } = string.Empty;
- public string To { get; set; } = string.Empty;
- public string Label { get; set; } = string.Empty;
- public bool Dashed { get; set; }
-}
-
-public class BpmnProcess
-{
- public string Id { get; set; } = string.Empty;
- public string Name { get; set; } = string.Empty;
- public List Nodes { get; set; } = new();
- public List Edges { get; set; } = new();
-}
-
-public class BpmnDiagram
-{
- public BpmnProcess Main { get; set; } = new BpmnProcess { Id = "Main", Name = "Main" };
- public List Subprocesses { get; set; } = new();
-}
-
+///
+/// Билдер диаграммы BPMN из T-SQL AST
+///
public static class BpmnBuilder
{
- public static BpmnDiagram Build(TSqlFragment fragment)
+ ///
+ /// Сборка диаграммы BPMN из T-SQL AST
+ ///
+ ///
+ ///
+ public static BpmnDiagram Build(TSqlFragment fragment) => Build(fragment, new());
+
+ ///
+ /// Сборка диаграммы BPMN из T-SQL AST с использованием существующей диаграммы
+ ///
+ ///
+ ///
+ ///
+ public static BpmnDiagram Build(TSqlFragment fragment, BpmnDiagram diagram)
{
- var visitor = new BpmnVisitor();
+ var visitor = new BpmnVisitor(diagram);
fragment.Accept(visitor);
+ visitor.Diagram.AddMissingProcessEdges();
return visitor.Diagram;
}
@@ -54,160 +34,750 @@ public static class BpmnBuilder
public BpmnDiagram Diagram { get; } = new BpmnDiagram();
private Dictionary _lastNodeByProcess = new();
- private BpmnProcess _currentProcess;
+ private Stack _processStack = new Stack();
private int _nodeCounter = 0;
+ private const int MaxConditionLength = 50;
- public BpmnVisitor()
+ public BpmnVisitor() : this(new BpmnDiagram()) { }
+
+ public BpmnVisitor(BpmnDiagram diagram)
{
- _currentProcess = Diagram.Main;
- // create a Start node for main
- var start = new BpmnNode { Id = NewId(), Label = "Start", Type = BpmnNodeType.Start };
- Diagram.Main.Nodes.Add(start);
- _lastNodeByProcess[Diagram.Main.Id] = start.Id;
+ Diagram = diagram;
+ _nodeCounter = diagram.Processes.Sum(p => p.Nodes.Count()) + 1;
}
private string NewId() => "n" + (_nodeCounter++);
- private void AddNode(BpmnProcess proc, BpmnNode node)
+ 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 });
+ 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)
+ private void AddNodeToCurrent(string label, BpmnNodeType type, Dictionary? properties = null)
{
- var node = new BpmnNode { Id = NewId(), Label = label, Type = type };
- AddNode(_currentProcess, node);
+ 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(properties);
+ }
+
+ AddNode(currentProcess, node);
}
- public override void Visit(TSqlScript node)
+ private void ProcessProcedureOrFunction(TSqlStatement node, string objectType)
{
- _currentProcess = Diagram.Main;
- if (!_lastNodeByProcess.ContainsKey(_currentProcess.Id)) _lastNodeByProcess[_currentProcess.Id] = null;
- base.Visit(node);
-
- // add End node for main process
- var end = new BpmnNode { Id = NewId(), Label = "End", Type = BpmnNodeType.End };
- AddNode(Diagram.Main, end);
- }
-
- public override void Visit(CreateProcedureStatement node)
- {
- // AST-based extraction of procedure name
- var name = FindSchemaObjectName(node) ?? ("proc" + Diagram.Subprocesses.Count);
+ // Извлечение имени объекта на основе AST
+ var name = FindSchemaObjectName(node) ?? ($"{objectType}" + Diagram.Processes.Count);
var pid = SanitizeId(name);
- var proc = new BpmnProcess { Id = pid, Name = name };
+ var proc = new BpmnProcess { Id = pid, Name = $"{name} ({objectType})" };
- // synthetic pool/label node
- var poolNode = new BpmnNode { Id = pid + "_pool", Label = name, Type = BpmnNodeType.Subprocess };
- proc.Nodes.Add(poolNode);
- // create start node for subprocess
- var start = new BpmnNode { Id = pid + "_start", Label = "Start", Type = BpmnNodeType.Start };
+ // создать начальный узел
+ var start = new BpmnNode { Id = pid + "_start", Label = "Начало", Type = BpmnNodeType.Start };
proc.Nodes.Add(start);
- // set last node pointer to start
_lastNodeByProcess[proc.Id] = start.Id;
- Diagram.Subprocesses.Add(proc);
+ Diagram.Processes.Add(proc);
- var old = _currentProcess;
- _currentProcess = proc;
- if (!_lastNodeByProcess.ContainsKey(proc.Id)) _lastNodeByProcess[proc.Id] = start.Id;
+ // Помещаем процесс в стек
+ _processStack.Push(proc);
- base.Visit(node);
+ // Используем специальный visitor для поиска StatementList
+ var statementCollector = new StatementListCollector();
+ node.Accept(statementCollector);
- // after visiting procedure body add End node
- var end = new BpmnNode { Id = pid + "_end", Label = "End", Type = BpmnNodeType.End };
+ if (statementCollector.FoundStatementList != null)
+ {
+ statementCollector.FoundStatementList.Accept(this);
+ }
+
+ // после посещения тела добавляем конечный узел
+ var end = new BpmnNode { Id = pid + "_end", Label = "Конец", Type = BpmnNodeType.End };
AddNode(proc, end);
- _currentProcess = old;
+ // Убираем процесс из стека
+ _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)
{
- // AST-based extraction for called proc
+ if (_processStack.Count == 0) return;
+ var currentProcess = _processStack.Peek();
+
+ // Извлечение на основе AST для вызываемого процесса
var called = FindProcedureNameFromExecute(node);
- var label = called != null ? $"EXEC {called}" : node.ToString();
+ var label = called != null ? $"EXEC {called}" : "EXEC";
- // task in current process
- var task = new BpmnNode { Id = NewId(), Label = label, Type = BpmnNodeType.Task };
- AddNode(_currentProcess, task);
+ // задача в текущем процессе
+ var task = new BpmnNode { Id = NewId(), Label = label!, Type = BpmnNodeType.Subprocess, SubprocessId = called is null ? string.Empty : SanitizeId(called), };
+ AddNode(currentProcess, task);
- // if calling known subprocess, add dashed edge from this node to subprocess start
- if (called != null)
+ // Обходим параметры вызова
+ if (node.ExecuteSpecification != null)
{
- var proc = Diagram.Subprocesses.FirstOrDefault(p => string.Equals(p.Name, called, StringComparison.OrdinalIgnoreCase) || string.Equals(p.Id, SanitizeId(called), StringComparison.OrdinalIgnoreCase));
- if (proc != null)
- {
- // add dashed edge in main process (from this task to subprocess pool node)
- Diagram.Main.Edges.Add(new BpmnEdge { From = task.Id, To = proc.Nodes.First().Id, Dashed = true });
- }
+ 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);
}
- base.Visit(node);
+ 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)
{
- var gid = new BpmnNode { Id = NewId(), Label = "IF", Type = BpmnNodeType.Gateway };
- AddNode(_currentProcess, gid);
+ if (_processStack.Count == 0) return;
+ var currentProcess = _processStack.Peek();
- // then / else placeholders
+ // Извлекаем условие IF
+ var conditionText = GetConditionText(node.Predicate);
+ var truncatedCondition = TruncateText(conditionText, MaxConditionLength);
+
+ var properties = new Dictionary
+ {
+ ["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);
- _currentProcess.Edges.Add(new BpmnEdge { From = gid.Id, To = thenNode.Id });
+ 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);
- _currentProcess.Edges.Add(new BpmnEdge { From = gid.Id, To = elseNode.Id });
+ 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
+ });
+ }
}
- base.Visit(node);
+ // Восстанавливаем последний узел после обработки 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
+ {
+ ["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
+ {
+ ["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
+ {
+ ["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();
+ 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})";
+ label = $"SELECT ({qs.SelectElements.Count} полей)";
}
+
AddNodeToCurrent(label, BpmnNodeType.Task);
- base.Visit(node);
+
+ // Обходим все элементы 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);
+ var label = "INSERT" + (string.IsNullOrEmpty(target) ? string.Empty : $" -> {target}");
AddNodeToCurrent(label, BpmnNodeType.Task);
- base.Visit(node);
}
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);
+ var label = "UPDATE" + (string.IsNullOrEmpty(target) ? string.Empty : $" -> {target}");
AddNodeToCurrent(label, BpmnNodeType.Task);
- base.Visit(node);
}
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);
+ var label = "DELETE" + (string.IsNullOrEmpty(target) ? string.Empty : $" -> {target}");
AddNodeToCurrent(label, BpmnNodeType.Task);
- base.Visit(node);
+ }
+
+ // Вспомогательные методы для извлечения текста
+ 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
@@ -236,52 +806,40 @@ public static class BpmnBuilder
private class SchemaNameFinder : TSqlFragmentVisitor
{
public string? FoundName { get; private set; }
+
public override void Visit(SchemaObjectName node)
{
if (FoundName == null)
{
- try
- {
- if (node.Identifiers != null && node.Identifiers.Count > 0)
- {
- FoundName = string.Join('.', node.Identifiers.Select(id => id.Value));
- }
- else
- {
- FoundName = node.ToString();
- }
- }
- catch
- {
- FoundName = node.ToString();
- }
+ FoundName = GetSchemaObjectName(node);
}
base.Visit(node);
}
public override void Visit(ProcedureReferenceName node)
{
- if (FoundName == null)
+ if (FoundName == null && node.ProcedureReference?.Name != null)
{
- try
- {
- if (node.ProcedureReference != null && node.ProcedureReference.Name != null)
- {
- // procedure reference name may contain identifiers
- FoundName = node.ProcedureReference.Name.ToString();
- }
- else
- {
- FoundName = node.ToString();
- }
- }
- catch
- {
- FoundName = node.ToString();
- }
+ 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
@@ -296,14 +854,14 @@ public static class BpmnBuilder
if (node.Name != null)
{
if (node.Name.Identifiers != null && node.Name.Identifiers.Count > 0)
- FoundName = string.Join('.', node.Name.Identifiers.Select(id => id.Value));
+ FoundName = string.Join('.', node.Name.Identifiers.Select(id => "[" + id.Value + "]"));
else
- FoundName = node.Name.ToString();
+ FoundName = "[" + node.Name.ToString() + "]";
}
}
catch
{
- FoundName = node.ToString();
+ FoundName = "[" + node.Name.ToString() + "]";
}
}
base.Visit(node);
@@ -338,16 +896,16 @@ public static class BpmnBuilder
try
{
if (node.SchemaObject != null && node.SchemaObject.Identifiers != null && node.SchemaObject.Identifiers.Count > 0)
- FoundName = string.Join('.', node.SchemaObject.Identifiers.Select(id => id.Value));
+ FoundName = string.Join('.', node.SchemaObject.Identifiers.Select(id => "[" + id.Value + "]"));
else
- FoundName = node.SchemaObject?.ToString() ?? node.ToString();
+ FoundName = "[" + node.SchemaObject?.ToString() ?? node.ToString() + "]";
if (node.Alias != null && !string.IsNullOrWhiteSpace(node.Alias.Value))
- FoundName += " AS " + node.Alias.Value;
+ FoundName += " AS [" + node.Alias.Value + "]";
}
catch
{
- FoundName = node.SchemaObject?.ToString() ?? node.ToString();
+ FoundName = "[" + node.SchemaObject?.ToString() ?? node.ToString() + "]";
}
}
@@ -398,5 +956,25 @@ public static class BpmnBuilder
}
return sb.ToString();
}
+
+ private class FunctionCallFinder : TSqlFragmentVisitor
+ {
+ public List FunctionCalls { get; } = new List();
+
+ 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 "";
+ }
}
-}
+}
\ No newline at end of file
diff --git a/SQLLinter/Infrastructure/Diagram/BpmnDiagram.cs b/SQLLinter/Infrastructure/Diagram/BpmnDiagram.cs
new file mode 100644
index 0000000..90892db
--- /dev/null
+++ b/SQLLinter/Infrastructure/Diagram/BpmnDiagram.cs
@@ -0,0 +1,13 @@
+namespace SQLLinter.Infrastructure.Diagram;
+
+///
+/// Диаграмма BPMN, объединяющая процессы
+///
+public class BpmnDiagram
+{
+ /// Процессы диаграммы
+ public List Processes { get; set; } = new();
+
+ /// Глобальные связи между процессами
+ public List GlobalEdges { get; set; } = new();
+}
diff --git a/SQLLinter/Infrastructure/Diagram/BpmnDiagramExtensions.cs b/SQLLinter/Infrastructure/Diagram/BpmnDiagramExtensions.cs
new file mode 100644
index 0000000..1738ae0
--- /dev/null
+++ b/SQLLinter/Infrastructure/Diagram/BpmnDiagramExtensions.cs
@@ -0,0 +1,34 @@
+namespace SQLLinter.Infrastructure.Diagram;
+
+///
+/// Расширения для диаграммы BPMN
+///
+public static class BpmnDiagramExtensions
+{
+ ///
+ /// Добавления отсутсвующих связей между процессами и их подпроцессами
+ ///
+ ///
+ public static void AddMissingProcessEdges(this BpmnDiagram diagram)
+ {
+ foreach (var subprocess in diagram.Processes
+ .SelectMany(p => p.Nodes.Select(n => new { node = n, procId = p.Id }))
+ .Where(n => n.node.Type == BpmnNodeType.Subprocess && !string.IsNullOrEmpty(n.node.SubprocessId) && n.node.SubprocessId != n.procId)
+ .Select(n => n.node)
+ )
+ {
+ if (diagram.Processes.Any(p => p.Id == subprocess.SubprocessId)
+ && !diagram.GlobalEdges.Any(t => t.From == subprocess.Id && t.To == subprocess.SubprocessId)
+ )
+ {
+ // Добавить пунктирный край от узла подпроцесса к началу подпроцесса
+ diagram.GlobalEdges.Add(new BpmnEdge
+ {
+ From = subprocess.Id,
+ To = subprocess.SubprocessId + "_start",
+ ArrowType = BpmnArrowType.Dashed,
+ });
+ }
+ }
+ }
+}
diff --git a/SQLLinter/Infrastructure/Diagram/BpmnEdge.cs b/SQLLinter/Infrastructure/Diagram/BpmnEdge.cs
new file mode 100644
index 0000000..b968d32
--- /dev/null
+++ b/SQLLinter/Infrastructure/Diagram/BpmnEdge.cs
@@ -0,0 +1,19 @@
+namespace SQLLinter.Infrastructure.Diagram;
+
+///
+/// Связь между узлами BPMN
+///
+public class BpmnEdge
+{
+ /// Идентификатор узла-источника
+ public string From { get; set; } = string.Empty;
+
+ /// Идентификатор узла-приемника
+ public string To { get; set; } = string.Empty;
+
+ /// Метка/название связи
+ public string Label { get; set; } = string.Empty;
+
+ /// Тип стрелки связи
+ public BpmnArrowType ArrowType { get; set; } = BpmnArrowType.Default;
+}
diff --git a/SQLLinter/Infrastructure/Diagram/BpmnNode.cs b/SQLLinter/Infrastructure/Diagram/BpmnNode.cs
new file mode 100644
index 0000000..7f669f7
--- /dev/null
+++ b/SQLLinter/Infrastructure/Diagram/BpmnNode.cs
@@ -0,0 +1,22 @@
+namespace SQLLinter.Infrastructure.Diagram;
+
+///
+/// Узел BPMN
+///
+public class BpmnNode
+{
+ /// Уникальный идентификатор узла
+ public string Id { get; set; } = string.Empty;
+
+ /// Метка/название узла
+ public string Label { get; set; } = string.Empty;
+
+ /// Тип узла
+ public BpmnNodeType Type { get; set; }
+
+ /// Идентификатор подпроцесса (если тип узла - Subprocess)
+ public string SubprocessId { get; set; } = string.Empty;
+
+ /// Дополнительные свойства узла
+ public Dictionary Properties { get; set; } = new();
+}
diff --git a/SQLLinter/Infrastructure/Diagram/BpmnNodeType.cs b/SQLLinter/Infrastructure/Diagram/BpmnNodeType.cs
new file mode 100644
index 0000000..e03032e
--- /dev/null
+++ b/SQLLinter/Infrastructure/Diagram/BpmnNodeType.cs
@@ -0,0 +1,20 @@
+namespace SQLLinter.Infrastructure.Diagram;
+
+///
+/// Типы узлов BPMN
+///
+public enum BpmnNodeType
+{
+ /// Стартовый узел процесса
+ Start,
+ /// Конечный узел процесса
+ End,
+ /// Задача
+ Task,
+ /// Шлюз (разветвление/объединение)
+ Gateway,
+ /// Шлюз (разветвление/объединение)
+ Hexagon,
+ /// Подпроцесс (вызов другого процесса)
+ Subprocess,
+}
diff --git a/SQLLinter/Infrastructure/Diagram/BpmnProcess.cs b/SQLLinter/Infrastructure/Diagram/BpmnProcess.cs
new file mode 100644
index 0000000..d92e169
--- /dev/null
+++ b/SQLLinter/Infrastructure/Diagram/BpmnProcess.cs
@@ -0,0 +1,19 @@
+namespace SQLLinter.Infrastructure.Diagram;
+
+///
+/// Процесс BPMN
+///
+public class BpmnProcess
+{
+ /// Уникальный идентификатор процесса
+ public string Id { get; set; } = string.Empty;
+
+ /// Название процесса
+ public string Name { get; set; } = string.Empty;
+
+ /// Узлы процесса
+ public List Nodes { get; set; } = new();
+
+ /// Связи процесса
+ public List Edges { get; set; } = new();
+}
diff --git a/SQLLinter/Infrastructure/Diagram/MermaidRenderer.cs b/SQLLinter/Infrastructure/Diagram/MermaidRenderer.cs
index 9c1f667..839bcbd 100644
--- a/SQLLinter/Infrastructure/Diagram/MermaidRenderer.cs
+++ b/SQLLinter/Infrastructure/Diagram/MermaidRenderer.cs
@@ -1,4 +1,4 @@
-using System.Text;
+using System.Text;
namespace SQLLinter.Infrastructure.Diagram;
@@ -9,114 +9,72 @@ public static class MermaidRenderer
var sb = new StringBuilder();
sb.AppendLine("flowchart TB");
- // main: print Start, then tasks/gateways, then End
- var mainStart = diagram.Main.Nodes.Where(n => n.Type == BpmnNodeType.Start).ToList();
- var mainTasks = diagram.Main.Nodes.Where(n => n.Type == BpmnNodeType.Task || n.Type == BpmnNodeType.Gateway).ToList();
- var mainEnd = diagram.Main.Nodes.Where(n => n.Type == BpmnNodeType.End).ToList();
-
- foreach (var node in mainStart)
- {
- var nid = SanitizeId(node.Id);
- var label = Escape(node.Label);
- sb.AppendLine($" {nid}((\"{label}\"))");
- }
- foreach (var node in mainTasks)
- {
- var nid = SanitizeId(node.Id);
- var label = Escape(node.Label);
- if (node.Type == BpmnNodeType.Gateway)
- sb.AppendLine($" {nid}{{\"{label}\"}}");
- else
- sb.AppendLine($" {nid}[\"{label}\"]");
- }
- foreach (var node in mainEnd)
- {
- var nid = SanitizeId(node.Id);
- var label = Escape(node.Label);
- sb.AppendLine($" {nid}((\"{label}\"))");
- }
-
- sb.AppendLine();
-
- // subprocesses as subgraphs: pool label, start, tasks/gateways, end
- foreach (var proc in diagram.Subprocesses)
+ // -----------------------------------------
+ // Рендер каждого процесса как subgraph
+ // -----------------------------------------
+ foreach (var proc in diagram.Processes)
{
var procId = SanitizeId(proc.Id);
sb.AppendLine($" subgraph {procId} [\"{Escape(proc.Name)}\"]");
- sb.AppendLine($" direction TB");
+ sb.AppendLine(" direction TB");
- var startNodes = proc.Nodes.Where(n => n.Type == BpmnNodeType.Start).ToList();
- var taskNodes = proc.Nodes.Where(n => n.Type == BpmnNodeType.Task || n.Type == BpmnNodeType.Gateway).ToList();
- var endNodes = proc.Nodes.Where(n => n.Type == BpmnNodeType.End).ToList();
- var poolNodes = proc.Nodes.Where(n => n.Type == BpmnNodeType.Subprocess).ToList();
-
- // pool label nodes (usually first)
- foreach (var node in poolNodes)
+ foreach (var node in proc.Nodes)
{
var nid = SanitizeId(node.Id);
var label = Escape(node.Label);
- sb.AppendLine($" {nid}[\"{label}\"]");
+
+ label = node.Type switch
+ {
+ BpmnNodeType.Start => $@"((""{label}""))",
+ BpmnNodeType.Task => $@"[""{label}""]",
+ BpmnNodeType.Subprocess => $@"[[""{label}""]]",
+ BpmnNodeType.Gateway => $@"{{""{label}""}}",
+ BpmnNodeType.Hexagon => $@"{{{{""{label}""}}}}",
+ BpmnNodeType.End => $@"((""{label}""))",
+ _ => $@">""{label}""]"
+ };
+
+ sb.AppendLine($" {nid}{label}");
}
- foreach (var node in startNodes)
- {
- var nid = SanitizeId(node.Id);
- var label = Escape(node.Label);
- sb.AppendLine($" {nid}((\"{label}\"))");
- }
- foreach (var node in taskNodes)
- {
- var nid = SanitizeId(node.Id);
- var label = Escape(node.Label);
- if (node.Type == BpmnNodeType.Gateway)
- sb.AppendLine($" {nid}{{\"{label}\"}}");
- else
- sb.AppendLine($" {nid}[\"{label}\"]");
- }
- foreach (var node in endNodes)
- {
- var nid = SanitizeId(node.Id);
- var label = Escape(node.Label);
- sb.AppendLine($" {nid}((\"{label}\"))");
- }
-
- // edges inside subprocess
+ // Edges внутри процесса
foreach (var e in proc.Edges)
{
var from = SanitizeId(e.From);
var to = SanitizeId(e.To);
- var arrow = e.Dashed ? "-.->" : "-->";
+ var arrow = e.ArrowType switch
+ {
+ BpmnArrowType.Dashed => "-.->",
+ _ => "-->",
+ };
var lbl = string.IsNullOrWhiteSpace(e.Label) ? string.Empty : $" |{Escape(e.Label)}|";
- sb.AppendLine($" {from} {arrow} {to}{lbl}");
+
+ sb.AppendLine($" {from} {arrow}{lbl} {to}");
}
sb.AppendLine(" end");
sb.AppendLine();
}
- // main edges
- foreach (var e in diagram.Main.Edges)
+ // -----------------------------------------
+ // Рендер глобальных связей между процессами
+ // -----------------------------------------
+ foreach (var e in diagram.GlobalEdges)
{
var from = SanitizeId(e.From);
var to = SanitizeId(e.To);
- var arrow = e.Dashed ? "-.->" : "-->";
+ var arrow = e.ArrowType switch
+ {
+ BpmnArrowType.Dashed => "-.->",
+ _ => "-->",
+ };
var lbl = string.IsNullOrWhiteSpace(e.Label) ? string.Empty : $" |{Escape(e.Label)}|";
- sb.AppendLine($" {from} {arrow} {to}{lbl}");
+ sb.AppendLine($" {from} {arrow}{lbl} {to}");
}
return sb.ToString();
}
- public static string RenderMarkdown(BpmnDiagram diagram)
- {
- var content = ToMermaidContent(diagram);
- var sb = new StringBuilder();
- sb.AppendLine("```mermaid");
- sb.Append(content);
- sb.AppendLine("```");
- return sb.ToString();
- }
-
private static string Escape(string s)
{
if (s == null) return string.Empty;
@@ -134,4 +92,4 @@ public static class MermaidRenderer
}
return sb.ToString();
}
-}
+}
\ No newline at end of file
diff --git a/SQLLinter/Infrastructure/Diagram/SqlDiagramProcessor.cs b/SQLLinter/Infrastructure/Diagram/SqlDiagramProcessor.cs
index 9876378..a09f015 100644
--- a/SQLLinter/Infrastructure/Diagram/SqlDiagramProcessor.cs
+++ b/SQLLinter/Infrastructure/Diagram/SqlDiagramProcessor.cs
@@ -57,8 +57,7 @@ public class SqlDiagramProcessor : ISqlDiagramProcessor
return;
}
- var diagramm = BpmnBuilder.Build(fragment);
- _bpmnDiagram.Subprocesses.Add(diagramm.Main);
+ BpmnBuilder.Build(fragment, _bpmnDiagram);
}
private Stream GetFileContents(string filePath)
diff --git a/SQLLinter/Infrastructure/Reporters/HTMLReporter.cs b/SQLLinter/Infrastructure/Reporters/HTMLReporter.cs
deleted file mode 100644
index e69de29..0000000
diff --git a/SQLLinter/Infrastructure/Reporters/MarkdownFileReporter.cs b/SQLLinter/Infrastructure/Reporters/MarkdownFileReporter.cs
deleted file mode 100644
index e69de29..0000000
diff --git a/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter.js b/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter.js
index 33753b3..dca362d 100644
--- a/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter.js
+++ b/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter.js
@@ -93,8 +93,8 @@ class DiagramViewer {
this.scale = 1;
this.tx = 0;
this.ty = 0;
- this.minScale = 0.1;
- this.maxScale = 6;
+ this.minScale = 0.3;
+ this.maxScale = 12;
this.originalViewBox = null;
this.minimapSvg = null;