using SQLInsight.Core.Data; using SQLInsight.Core.Findings; using SQLInsight.Rules.Engine; using SQLInsight.Rules.Indexes; using static SQLInsight.Rules.Tests.TestInstances; namespace SQLInsight.Rules.Tests; public class IndexRuleTests { private static RuleResult Run(IRule rule, RuleContext context) => Assert.Single(new RuleEngine([rule]).Evaluate(context)); private static IndexInfo Index(int id, string? name, string type, string[] keys, string[]? include = null, bool unique = false, bool pk = false, long reads = 100, long updates = 100, long pages = 1000, long rows = 100_000, long forwarded = 0, string table = "Orders") => new() { Schema = "dbo", Table = table, IndexId = id, Name = name, Type = type, KeyColumns = keys, IncludedColumns = include ?? [], IsUnique = unique || pk, IsPrimaryKey = pk, UserSeeks = reads, UserUpdates = updates, UsedPages = pages, Rows = rows, ForwardedFetches = forwarded, }; private static RuleContext Indexes(TimeSpan uptime, IndexDatabase db) => With(new IndexSnapshot { CapturedAt = Now, SqlServerStartTime = Now.UtcDateTime - uptime, Databases = [db], }); private static readonly TimeSpan Month = TimeSpan.FromDays(30); [Fact] public void Missing_index_suggests_create_with_equality_columns_first() { var db = new IndexDatabase { Name = "Sales", Indexes = [Index(1, "PK_Orders", "CLUSTERED", ["Id"], pk: true)], MissingIndexes = [new MissingIndex { Table = "[Sales].[dbo].[Orders]", EqualityColumns = "[CustomerId]", InequalityColumns = "[OrderDate]", IncludedColumns = "[Total]", Seeks = 50_000, Scans = 0, AverageTotalUserCost = 12, AverageUserImpact = 95 }], }; var finding = Assert.Single(Run(new MissingIndexRule(), Indexes(Month, db)).Findings); Assert.Equal(Severity.Low, finding.Severity); Assert.Contains("CREATE INDEX [IX_Orders_CustomerId_OrderDate] ON [dbo].[Orders] ([CustomerId], [OrderDate]) INCLUDE ([Total]);", finding.FixScript, StringComparison.Ordinal); } [Fact] public void Missing_index_extends_an_existing_index_with_the_same_keys() { var db = new IndexDatabase { Name = "Sales", Indexes = [Index(1, "PK_Orders", "CLUSTERED", ["Id"], pk: true), Index(2, "IX_Orders_CustomerId", "NONCLUSTERED", ["CustomerId"], ["Status"])], MissingIndexes = [new MissingIndex { Table = "[Sales].[dbo].[Orders]", EqualityColumns = "[CustomerId]", IncludedColumns = "[Total]", Seeks = 100_000, Scans = 0, AverageTotalUserCost = 20, AverageUserImpact = 90 }], }; var finding = Assert.Single(Run(new MissingIndexRule(), Indexes(Month, db)).Findings); Assert.Contains("Udvid det eksisterende index IX_Orders_CustomerId", finding.Recommendation, StringComparison.Ordinal); Assert.Contains("INCLUDE ([Status], [Total]) WITH (DROP_EXISTING = ON)", finding.FixScript, StringComparison.Ordinal); } [Fact] public void Missing_index_warns_about_write_heavy_table() { var db = new IndexDatabase { Name = "Sales", Indexes = [Index(1, "PK_Orders", "CLUSTERED", ["Id"], pk: true, reads: 10, updates: 1_000_000)], MissingIndexes = [new MissingIndex { Table = "[Sales].[dbo].[Orders]", EqualityColumns = "[CustomerId]", Seeks = 50_000, Scans = 0, AverageTotalUserCost = 12, AverageUserImpact = 95 }], }; var finding = Assert.Single(Run(new MissingIndexRule(), Indexes(Month, db)).Findings); Assert.Contains(finding.Evidence, e => e.Value.Contains("skrives langt oftere", StringComparison.Ordinal)); } [Fact] public void Small_missing_index_suggestion_is_ignored() { var db = new IndexDatabase { Name = "Sales", MissingIndexes = [new MissingIndex { Table = "[Sales].[dbo].[Orders]", EqualityColumns = "[CustomerId]", Seeks = 50, Scans = 0, AverageTotalUserCost = 0.5, AverageUserImpact = 40 }], }; Assert.Equal(RuleOutcome.Passed, Run(new MissingIndexRule(), Indexes(Month, db)).Outcome); } [Fact] public void Unused_index_needs_seven_days_of_uptime() { var db = new IndexDatabase { Name = "Sales", Indexes = [Index(2, "IX_Unused", "NONCLUSTERED", ["Status"], reads: 0, updates: 50_000)] }; Assert.Equal(RuleOutcome.NotMeasurable, Run(new UnusedIndexRule(), Indexes(TimeSpan.FromDays(2), db)).Outcome); var finding = Assert.Single(Run(new UnusedIndexRule(), Indexes(Month, db)).Findings); Assert.Contains("DISABLE", finding.FixScript, StringComparison.Ordinal); } [Fact] public void Unique_and_primary_key_indexes_are_never_unused_candidates() { var db = new IndexDatabase { Name = "Sales", Indexes = [Index(2, "UX_Code", "NONCLUSTERED", ["Code"], unique: true, reads: 0, updates: 50_000), Index(1, "PK", "CLUSTERED", ["Id"], pk: true, reads: 0, updates: 50_000)], }; Assert.Equal(RuleOutcome.Passed, Run(new UnusedIndexRule(), Indexes(Month, db)).Outcome); } [Theory] [InlineData(new[] { "A" }, new string[0], new[] { "A" }, new string[0], true)] // exact duplicate [InlineData(new[] { "A", "B" }, new string[0], new[] { "A" }, new string[0], true)] // left prefix [InlineData(new[] { "A", "B" }, new[] { "C" }, new[] { "A" }, new[] { "C" }, true)] // include covered by include [InlineData(new[] { "A", "B" }, new string[0], new[] { "A" }, new[] { "B" }, true)] // include covered by key [InlineData(new[] { "A", "B" }, new string[0], new[] { "A" }, new[] { "C" }, false)] // include not covered [InlineData(new[] { "B", "A" }, new string[0], new[] { "A" }, new string[0], false)] // not a leading prefix public void Covers(string[] coverKeys, string[] coverInclude, string[] candidateKeys, string[] candidateInclude, bool expected) { var cover = Index(2, "IX_Cover", "NONCLUSTERED", coverKeys, coverInclude); var candidate = Index(3, "IX_Candidate", "NONCLUSTERED", candidateKeys, candidateInclude); Assert.Equal(expected, DuplicateIndexRule.Covers(cover, candidate)); } [Fact] public void Unique_index_is_not_covered_by_a_wider_index() { Assert.False(DuplicateIndexRule.Covers(Index(2, "IX_Wide", "NONCLUSTERED", ["A", "B"]), Index(3, "UX_A", "NONCLUSTERED", ["A"], unique: true))); } [Fact] public void Filtered_indexes_only_cover_the_same_filter() { var filtered = Index(2, "IX_Active", "NONCLUSTERED", ["A"]) with { Filter = "([Active]=(1))" }; Assert.False(DuplicateIndexRule.Covers(filtered, Index(3, "IX_A", "NONCLUSTERED", ["A"]))); } [Fact] public void Duplicate_rule_reports_each_pair_once() { var db = new IndexDatabase { Name = "Sales", Indexes = [ Index(1, "PK_Orders", "CLUSTERED", ["Id"], pk: true), Index(2, "IX_A", "NONCLUSTERED", ["CustomerId"]), Index(3, "IX_B", "NONCLUSTERED", ["CustomerId"]), ], }; var finding = Assert.Single(Run(new DuplicateIndexRule(), Indexes(Month, db)).Findings); Assert.Equal(Severity.Medium, finding.Severity); } [Fact] public void Heap_with_many_forwarded_fetches() { var db = new IndexDatabase { Name = "Sales", Indexes = [Index(0, null, "HEAP", [], forwarded: 250_000)] }; var finding = Assert.Single(Run(new ForwardedRecordsRule(), Indexes(Month, db)).Findings); Assert.Contains("ALTER TABLE [dbo].[Orders] REBUILD;", finding.FixScript, StringComparison.Ordinal); } [Fact] public void Stale_statistics_on_large_tables_only() { var db = new IndexDatabase { Name = "Sales", Statistics = [ new StatisticInfo { Schema = "dbo", Table = "Orders", Name = "IX_Orders_Date", Rows = 1_000_000, Modifications = 400_000 }, new StatisticInfo { Schema = "dbo", Table = "Small", Name = "PK_Small", Rows = 500, Modifications = 500 }, new StatisticInfo { Schema = "dbo", Table = "Stable", Name = "PK_Stable", Rows = 1_000_000, Modifications = 1_000 }, ], }; var finding = Assert.Single(Run(new StaleStatisticsRule(), Indexes(Month, db)).Findings); Assert.Equal("statistic: Sales.dbo.Orders.IX_Orders_Date", finding.Subject); } [Fact] public void Auto_statistics_off() { var off = Database(change: d => d with { IsAutoUpdateStatistics = false }); var finding = Assert.Single(Run(new AutoStatisticsRule(), With(DatabaseList(off))).Findings); Assert.Equal("ALTER DATABASE [Sales] SET AUTO_UPDATE_STATISTICS ON;", finding.FixScript); } }