Some checks are pending
CI / Build og unit tests (push) Waiting to run
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Blocked by required conditions
Collector takes the 50 heaviest statements (CPU, last 24 h), fetches statement plans as text (dm_exec_query_plan returns NULL for deeply nested plans) and analyzes them in C#. Rules: - SQLI-PLAN-001 implicit conversion preventing an index seek - SQLI-PLAN-002 join without predicate (deliberate CROSS JOIN excluded) - SQLI-PLAN-003 non-inlined scalar UDF in a heavy statement - SQLI-PLAN-004 spills to tempdb, SQLI-PLAN-005 oversized memory grants (2016 SP2+ / 2017 CU3+ columns, new capability) Verified against real plans in SQL Server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3EExwndy8Z22EK2Qpt6BK
104 lines
4.5 KiB
C#
104 lines
4.5 KiB
C#
using SQLInsight.Core.Data;
|
|
using SQLInsight.Core.Findings;
|
|
using SQLInsight.Rules.Engine;
|
|
using SQLInsight.Rules.Performance;
|
|
using static SQLInsight.Rules.Tests.TestInstances;
|
|
|
|
namespace SQLInsight.Rules.Tests;
|
|
|
|
public class PlanRuleTests
|
|
{
|
|
private static RuleResult Run(IRule rule, RuleContext context) => Assert.Single(new RuleEngine([rule]).Evaluate(context));
|
|
|
|
private static AnalyzedStatement Statement(PlanWarnings? warnings = null, long execs = 1000, long workerUs = 120_000_000, long? spills = null, long? grantKb = null, long? usedKb = null) => new()
|
|
{
|
|
QueryHash = "0x1234ABCD",
|
|
DatabaseName = "Sales",
|
|
StatementText = "SELECT * FROM dbo.Customers WHERE Code = @code",
|
|
Executions = execs,
|
|
TotalWorkerTimeUs = workerUs,
|
|
TotalElapsedTimeUs = workerUs,
|
|
TotalLogicalReads = execs * 1000,
|
|
TotalSpills = spills,
|
|
TotalGrantKb = grantKb,
|
|
TotalUsedGrantKb = usedKb,
|
|
Warnings = warnings ?? new PlanWarnings(),
|
|
};
|
|
|
|
private static RuleContext Plans(params AnalyzedStatement[] statements) => With(new PlanAnalysisSnapshot { CapturedAt = Now, Statements = statements });
|
|
|
|
[Fact]
|
|
public void Clean_plan_passes_all_plan_rules()
|
|
{
|
|
foreach (var rule in new IRule[] { new ImplicitConversionRule(), new NoJoinPredicateRule(), new ScalarUdfRule(), new SpillRule(), new ExcessiveGrantRule() })
|
|
{
|
|
Assert.Equal(RuleOutcome.Passed, Run(rule, Plans(Statement(spills: 0, grantKb: 1000, usedKb: 900))).Outcome);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Implicit_conversion_lists_the_expression()
|
|
{
|
|
var statement = Statement(new PlanWarnings { SeekPreventingConversions = ["CONVERT_IMPLICIT(nvarchar(20),[Sales].[dbo].[Customers].[Code],0)=[@code]"] });
|
|
|
|
var finding = Assert.Single(Run(new ImplicitConversionRule(), Plans(statement)).Findings);
|
|
|
|
Assert.Equal("statement: 0x1234ABCD", finding.Subject);
|
|
Assert.Contains(finding.Evidence, e => e.Value.StartsWith("CONVERT_IMPLICIT", StringComparison.Ordinal));
|
|
Assert.Contains("AnsiString", finding.Recommendation, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Missing_join_predicate_is_high()
|
|
{
|
|
Assert.Equal(Severity.High, Assert.Single(Run(new NoJoinPredicateRule(), Plans(Statement(new PlanWarnings { NoJoinPredicate = true }))).Findings).Severity);
|
|
}
|
|
|
|
[Fact]
|
|
public void Deliberate_cross_join_is_not_reported()
|
|
{
|
|
var statement = Statement(new PlanWarnings { NoJoinPredicate = true }) with { StatementText = "SELECT * FROM dbo.Sizes s CROSS JOIN dbo.Colors c" };
|
|
|
|
Assert.Equal(RuleOutcome.Passed, Run(new NoJoinPredicateRule(), Plans(statement)).Outcome);
|
|
}
|
|
|
|
[Fact]
|
|
public void Scalar_udf_only_matters_in_heavy_statements()
|
|
{
|
|
var udf = new PlanWarnings { ScalarUdfs = ["[Sales].[dbo].[fn_Price]"] };
|
|
|
|
Assert.Equal(RuleOutcome.Findings, Run(new ScalarUdfRule(), Plans(Statement(udf))).Outcome);
|
|
Assert.Equal(RuleOutcome.Passed, Run(new ScalarUdfRule(), Plans(Statement(udf, workerUs: 5_000_000))).Outcome);
|
|
}
|
|
|
|
[Fact]
|
|
public void Spills_per_execution_above_threshold()
|
|
{
|
|
Assert.Equal(RuleOutcome.Findings, Run(new SpillRule(), Plans(Statement(execs: 100, spills: 500_000))).Outcome);
|
|
Assert.Equal(RuleOutcome.Passed, Run(new SpillRule(), Plans(Statement(execs: 100, spills: 5_000))).Outcome);
|
|
}
|
|
|
|
[Fact]
|
|
public void Oversized_grant_used_under_ten_percent()
|
|
{
|
|
Assert.Equal(RuleOutcome.Findings, Run(new ExcessiveGrantRule(), Plans(Statement(execs: 10, grantKb: 10 * 500 * 1024, usedKb: 10 * 20 * 1024))).Outcome);
|
|
Assert.Equal(RuleOutcome.Passed, Run(new ExcessiveGrantRule(), Plans(Statement(execs: 10, grantKb: 10 * 500 * 1024, usedKb: 10 * 300 * 1024))).Outcome);
|
|
}
|
|
|
|
[Fact]
|
|
public void Memory_rules_are_not_measurable_without_the_columns()
|
|
{
|
|
Assert.Equal(RuleOutcome.NotMeasurable, Run(new SpillRule(), WithVersion("14.0.1000.169", new PlanAnalysisSnapshot { CapturedAt = Now, Statements = [] })).Outcome);
|
|
}
|
|
|
|
[Fact]
|
|
public void Findings_are_capped_and_heaviest_first()
|
|
{
|
|
var statements = Enumerable.Range(1, 30).Select(i => Statement(new PlanWarnings { NoJoinPredicate = true }, workerUs: i * 1_000_000) with { QueryHash = $"0x{i:X4}" }).ToArray();
|
|
|
|
var findings = Run(new NoJoinPredicateRule(), Plans(statements)).Findings;
|
|
|
|
Assert.Equal(PlanRule.MaxFindingsPerRule, findings.Count);
|
|
Assert.Equal("statement: 0x001E", findings[0].Subject);
|
|
}
|
|
}
|