On SQL Server 2005 twenty rules stood as "not measurable" (Query Store, system_health, memory columns …) between the real gaps, and counted in the overview's "regler ikke målbare". They are not gaps: the version simply lacks what they look at. - New RuleOutcome.NotApplicable (last in the enum: outcomes are stored as numbers, so existing rows keep their meaning). - A rule or collector missing only capabilities inherent to the instance (version-gated, plan cache memory columns, or Azure-managed) is not applicable, with the version each needs: "Ikke relevant for SQL Server 2005: kræver QueryStore (fra SQL Server 2016)". Missing settings or rights (HADR not enabled …), collector failures and permission errors stay "not measurable". CollectedData.MarkNotApplicable carries this from collectors to the rules that use their data; community rules below their minimumVersion are not applicable too. - Counts are unchanged in shape: the overview, KPI, JSON API and Prometheus count outcomes 2 and 3 only. The instance page lists the rest in a collapsed "Ikke relevant for SQL Server 2005 (n)" (details/summary, no JS) after the real gaps. Report controls whose core rule does not apply count neither as met nor as not measured. Tests: engine (version vs. setting vs. collected data), inherent capabilities, the existing version tests now expect not applicable, report control status, and the web page and overview count. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
130 lines
5.1 KiB
C#
130 lines
5.1 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 QueryStoreRuleTests
|
||
{
|
||
private static RuleResult Run(IRule rule, RuleContext context) => Assert.Single(new RuleEngine([rule]).Evaluate(context));
|
||
|
||
private static QueryPlanStats Plan(long query, long plan, bool recent, long execs, double avgMs, double maxMs = 0, double stdevMs = 0) => new()
|
||
{
|
||
QueryId = query,
|
||
PlanId = plan,
|
||
IsRecent = recent,
|
||
Executions = execs,
|
||
TotalDurationUs = execs * avgMs * 1000,
|
||
TotalCpuUs = execs * avgMs * 800,
|
||
TotalLogicalReads = execs * 1000,
|
||
MaxStdevDurationUs = stdevMs * 1000,
|
||
MaxDurationUs = (maxMs == 0 ? avgMs : maxMs) * 1000,
|
||
MinDurationUs = avgMs * 500,
|
||
QueryText = "SELECT * FROM dbo.Orders WHERE CustomerId = @p0",
|
||
ObjectName = "dbo.GetOrders",
|
||
};
|
||
|
||
private static RuleContext Store(params QueryPlanStats[] plans) =>
|
||
With(new QueryStoreSnapshot { CapturedAt = Now, Databases = [new QueryStoreDatabase { Name = "Sales", Plans = plans }] });
|
||
|
||
[Fact]
|
||
public void New_slow_plan_is_a_regression_with_force_script()
|
||
{
|
||
var result = Run(new PlanRegressionRule(), Store(
|
||
Plan(7, 1, recent: false, execs: 5000, avgMs: 20),
|
||
Plan(7, 1, recent: true, execs: 200, avgMs: 20),
|
||
Plan(7, 2, recent: true, execs: 3000, avgMs: 400)));
|
||
|
||
var finding = Assert.Single(result.Findings);
|
||
Assert.Equal(Severity.High, finding.Severity); // 3000 × 380 ms = 1140 s extra
|
||
Assert.Equal("query: Sales/7", finding.Subject);
|
||
Assert.Contains("@query_id = 7, @plan_id = 1", finding.FixScript, StringComparison.Ordinal);
|
||
Assert.StartsWith("USE [Sales];", finding.FixScript, StringComparison.Ordinal);
|
||
}
|
||
|
||
[Fact]
|
||
public void Slowdown_too_small_to_matter_is_ignored()
|
||
{
|
||
// 3× slower, but only 20 executions × 40 ms = 0.8 s extra per day.
|
||
var result = Run(new PlanRegressionRule(), Store(
|
||
Plan(7, 1, recent: false, execs: 100, avgMs: 20),
|
||
Plan(7, 2, recent: true, execs: 20, avgMs: 60)));
|
||
|
||
Assert.Equal(RuleOutcome.Passed, result.Outcome);
|
||
}
|
||
|
||
[Fact]
|
||
public void Same_plan_getting_slower_is_not_a_plan_regression()
|
||
{
|
||
var result = Run(new PlanRegressionRule(), Store(
|
||
Plan(7, 1, recent: false, execs: 5000, avgMs: 20),
|
||
Plan(7, 1, recent: true, execs: 5000, avgMs: 200)));
|
||
|
||
Assert.Equal(RuleOutcome.Passed, result.Outcome);
|
||
}
|
||
|
||
[Fact]
|
||
public void Failed_forced_plan_is_high_with_unforce_script()
|
||
{
|
||
var context = With(new QueryStoreSnapshot
|
||
{
|
||
CapturedAt = Now,
|
||
Databases = [new QueryStoreDatabase { Name = "Sales", Plans = [], FailedForcedPlans = [new FailedForcedPlan(7, 3, 12, "NO_INDEX", "SELECT 1")] }],
|
||
});
|
||
|
||
var finding = Assert.Single(Run(new FailedForcedPlanRule(), context).Findings);
|
||
|
||
Assert.Equal(Severity.High, finding.Severity);
|
||
Assert.Contains("Genopret det index", finding.Recommendation, StringComparison.Ordinal);
|
||
Assert.Contains("sp_query_store_unforce_plan @query_id = 7, @plan_id = 3", finding.FixScript, StringComparison.Ordinal);
|
||
}
|
||
|
||
[Fact]
|
||
public void Plans_with_very_different_durations_indicate_parameter_sensitivity()
|
||
{
|
||
var finding = Assert.Single(Run(new ParameterSensitivityRule(), Store(
|
||
Plan(9, 1, recent: true, execs: 900, avgMs: 5),
|
||
Plan(9, 2, recent: true, execs: 100, avgMs: 2000))).Findings);
|
||
|
||
Assert.Contains("2 planer", finding.Explanation, StringComparison.Ordinal);
|
||
Assert.Contains("sp_query_store_set_hints", finding.FixScript, StringComparison.Ordinal); // 2022 test instance
|
||
}
|
||
|
||
[Fact]
|
||
public void Volatile_single_plan_indicates_parameter_sensitivity()
|
||
{
|
||
var result = Run(new ParameterSensitivityRule(), Store(Plan(9, 1, recent: true, execs: 5000, avgMs: 50, maxMs: 8000, stdevMs: 300)));
|
||
|
||
Assert.Equal(RuleOutcome.Findings, result.Outcome);
|
||
}
|
||
|
||
[Fact]
|
||
public void Stable_query_is_not_parameter_sensitive()
|
||
{
|
||
var result = Run(new ParameterSensitivityRule(), Store(Plan(9, 1, recent: true, execs: 5000, avgMs: 50, maxMs: 120, stdevMs: 10)));
|
||
|
||
Assert.Equal(RuleOutcome.Passed, result.Outcome);
|
||
}
|
||
|
||
[Fact]
|
||
public void No_hints_script_before_2022()
|
||
{
|
||
var context = WithVersion("15.0.4490.9", new QueryStoreSnapshot
|
||
{
|
||
CapturedAt = Now,
|
||
Databases = [new QueryStoreDatabase { Name = "Sales", Plans = [Plan(9, 1, true, 900, 5), Plan(9, 2, true, 100, 2000)] }],
|
||
});
|
||
|
||
Assert.Null(Assert.Single(Run(new ParameterSensitivityRule(), context).Findings).FixScript);
|
||
}
|
||
|
||
[Fact]
|
||
public void Query_store_rules_are_not_applicable_before_2016()
|
||
{
|
||
var result = Run(new PlanRegressionRule(), WithVersion("12.0.6024.0"));
|
||
|
||
Assert.Equal(RuleOutcome.NotApplicable, result.Outcome);
|
||
}
|
||
}
|