sqlinsight-pro/tests/SQLInsight.Rules.Tests/QueryStoreRuleTests.cs
Søren Eggert Lundsteen Olsen 244153e9e4 Rules the version does not support are "not applicable", not "not measurable"
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>
2026-10-01 16:05:39 +02:00

130 lines
5.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
}