152 lines
4.9 KiB
C#
152 lines
4.9 KiB
C#
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<TSqlFragment> 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<TSqlFragment> childCallback;
|
||
private readonly bool isMultiClauseQuery;
|
||
|
||
public JoinQueryVisitor(Action<TSqlFragment> 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<TSqlFragment> childCallback;
|
||
private bool hasColumnReferenceParameter;
|
||
|
||
public FunctionVisitor(Action<TSqlFragment> 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;
|
||
}
|
||
}
|
||
}
|