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 ""; } } }