using Microsoft.SqlServer.TransactSql.ScriptDom; using SQLLinter.Common; namespace SQLLinter.Infrastructure.Rules; public class CrossDatabaseTransactionRule : BaseRuleVisitor, IRule { public override string Text => "Межбазовые вставки или обновления, включенные в транзакцию, могут привести к повреждению данных."; public override void Visit(TSqlBatch node) { var childTransactionVisitor = new ChildTransactionVisitor(); node.Accept(childTransactionVisitor); foreach (var transaction in childTransactionVisitor.TransactionLists) { var childInsertUpdateQueryVisitor = new ChildInsertUpdateQueryVisitor(transaction); node.Accept(childInsertUpdateQueryVisitor); if (childInsertUpdateQueryVisitor.DatabasesUpdated.Count > 1) { AddViolation( Name, Text, GetLineNumber(transaction.Begin), GetColumnNumber(transaction.Begin)); } } } public class TrackedTransaction { public BeginTransactionStatement Begin { get; set; } public CommitTransactionStatement Commit { get; set; } } public class ChildTransactionVisitor : TSqlFragmentVisitor { public List TransactionLists { get; } = new List(); public override void Visit(BeginTransactionStatement node) { TransactionLists.Add(new TrackedTransaction { Begin = node }); } public override void Visit(CommitTransactionStatement node) { var firstUncomitted = TransactionLists.LastOrDefault(x => x.Commit == null); if (firstUncomitted != null) { firstUncomitted.Commit = node; } } } public class ChildInsertUpdateQueryVisitor : TSqlFragmentVisitor { private readonly TrackedTransaction transaction; private readonly ChildDatabaseNameVisitor childDatabaseNameVisitor = new ChildDatabaseNameVisitor(); public ChildInsertUpdateQueryVisitor(TrackedTransaction transaction) { this.transaction = transaction; } public HashSet DatabasesUpdated { get; } = new HashSet(); public override void Visit(InsertStatement node) { GetDatabasesUpdated(node); } public override void Visit(UpdateStatement node) { GetDatabasesUpdated(node); } private void GetDatabasesUpdated(TSqlFragment node) { if (IsWithinTransaction(node)) { node.Accept(childDatabaseNameVisitor); DatabasesUpdated.UnionWith(childDatabaseNameVisitor.DatabasesUpdated); } } private bool IsWithinTransaction(TSqlFragment node) { if (node.StartLine == transaction.Begin?.StartLine && node.StartColumn < transaction.Begin?.StartColumn) { return false; } if (node.StartLine == transaction.Commit?.StartLine && node.StartColumn > transaction.Commit?.StartColumn) { return false; } return node.StartLine >= transaction.Begin?.StartLine && node.StartLine <= transaction.Commit?.StartLine; } } public class ChildDatabaseNameVisitor : TSqlFragmentVisitor { public HashSet DatabasesUpdated { get; } = new HashSet(); public override void Visit(NamedTableReference node) { if (node.SchemaObject.DatabaseIdentifier != null) { DatabasesUpdated.Add(node.SchemaObject.DatabaseIdentifier.Value); } } } }