119 lines
3.8 KiB
C#
119 lines
3.8 KiB
C#
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<TrackedTransaction> TransactionLists { get; } = new List<TrackedTransaction>();
|
|
|
|
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<string> DatabasesUpdated { get; } = new HashSet<string>();
|
|
|
|
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<string> DatabasesUpdated { get; } = new HashSet<string>();
|
|
|
|
public override void Visit(NamedTableReference node)
|
|
{
|
|
if (node.SchemaObject.DatabaseIdentifier != null)
|
|
{
|
|
DatabasesUpdated.Add(node.SchemaObject.DatabaseIdentifier.Value);
|
|
}
|
|
}
|
|
}
|
|
}
|