Files
SQLLint/SQLLinter/Infrastructure/Diagram/BpmnBuilder.cs
2025-12-26 19:52:44 +03:00

980 lines
36 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
visitor.Diagram.AddMissingProcessEdges();
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 "";
}
}
}