using Microsoft.SqlServer.TransactSql.ScriptDom;
using SQLLinter.Core;
namespace SQLLinter.Infrastructure.Diagram;
///
/// Билдер диаграммы BPMN из T-SQL AST
///
public static class BpmnBuilder
{
///
/// Сборка диаграммы 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(diagram);
fragment.Accept(visitor);
visitor.Diagram.AddMissingProcessEdges();
return visitor.Diagram;
}
private class BpmnVisitor : TSqlFragmentVisitor
{
public BpmnDiagram Diagram { get; } = new BpmnDiagram();
private Dictionary _lastNodeByProcess = new();
private Stack _processStack = new Stack();
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? 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(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
{
["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
{
["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} полей)";
}
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 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 "";
}
}
}