using Microsoft.SqlServer.TransactSql.ScriptDom; using SQLLinter.Common; using SQLLinter.Infrastructure.Rules.Common; namespace SQLLinter.Infrastructure.Rules; public class NonSargableRule : BaseRuleVisitor, IRule { private readonly List errorsReported = new(); public override string Text => "Выполнение функций с предложениями фильтра или предикатами соединения может вызвать проблемы с производительностью."; public override void Visit(JoinTableReference node) { var predicateExpressionVisitor = new PredicateVisitor(); node.AcceptChildren(predicateExpressionVisitor); var multiClauseQuery = predicateExpressionVisitor.PredicatesFound; var joinVisitor = new JoinQueryVisitor(VisitorCallback, multiClauseQuery); node.AcceptChildren(joinVisitor); } public override void Visit(WhereClause node) { var predicateExpressionVisitor = new PredicateVisitor(); node.Accept(predicateExpressionVisitor); var multiClauseQuery = predicateExpressionVisitor.PredicatesFound; var childVisitor = new FunctionVisitor(VisitorCallback, multiClauseQuery); node.Accept(childVisitor); } private void VisitorCallback(TSqlFragment childNode) { if (errorsReported.Contains(childNode)) { return; } var dynamicSqlColumnAdjustment = GetDynamicSqlColumnOffset(childNode); errorsReported.Add(childNode); AddViolation(Name, Text, GetLineNumber(childNode), ColumnNumberCalculator.GetNodeColumnPosition(childNode) + dynamicSqlColumnAdjustment); } private class JoinQueryVisitor : TSqlFragmentVisitor { private readonly Action childCallback; private readonly bool isMultiClauseQuery; public JoinQueryVisitor(Action childCallback, bool multiClauseQuery) { this.childCallback = childCallback; isMultiClauseQuery = multiClauseQuery; } public override void Visit(BooleanComparisonExpression node) { var childVisitor = new FunctionVisitor(childCallback, isMultiClauseQuery); node.Accept(childVisitor); } } private class PredicateVisitor : TSqlFragmentVisitor { public bool PredicatesFound { get; private set; } public override void Visit(BooleanBinaryExpression node) { PredicatesFound = true; } } private class FunctionVisitor : TSqlFragmentVisitor { private readonly bool isMultiClause; private readonly Action childCallback; private bool hasColumnReferenceParameter; public FunctionVisitor(Action errorCallback, bool isMultiClause) { childCallback = errorCallback; this.isMultiClause = isMultiClause; } public override void Visit(FunctionCall node) { switch (node.FunctionName.Value.ToUpper()) { // разрешить предикаты isnull при наличии других фильтров case "ISNULL" when isMultiClause: return; case "DATEADD": case "DATEDIFF": case "DATEDIFF_BIG": case "DATENAME": case "DATEPART": case "DATETRUNC": case "DATE_BUCKET": hasColumnReferenceParameter = true; break; } FindColumnReferences(node); } public override void Visit(LeftFunctionCall node) { FindColumnReferences(node); } public override void Visit(RightFunctionCall node) { FindColumnReferences(node); } public override void Visit(ConvertCall node) { FindColumnReferences(node); } public override void Visit(CastCall node) { FindColumnReferences(node); } private void FindColumnReferences(TSqlFragment node) { var columnReferenceVisitor = new ColumnReferenceVisitor(); node.AcceptChildren(columnReferenceVisitor); if (columnReferenceVisitor.ColumnReferenceFound && (!hasColumnReferenceParameter || columnReferenceVisitor.ColumnReferenceCount > 1)) { childCallback(node); } } } private class ColumnReferenceVisitor : TSqlFragmentVisitor { public bool ColumnReferenceFound { get; private set; } public int ColumnReferenceCount { get; private set; } public override void Visit(ColumnReferenceExpression node) { ColumnReferenceCount++; ColumnReferenceFound = true; } } }