From 8d29ef383f12ff191aefc66fde3a780e2eb411f9 Mon Sep 17 00:00:00 2001 From: Orestes Zoupanos Date: Fri, 20 Mar 2026 09:42:47 +0000 Subject: [PATCH 1/3] fix warnings --- Dashboard/Analysis/FactScorer.cs | 14 ++-- .../Analysis/SqlServerDrillDownCollector.cs | 13 ++-- Dashboard/Analysis/SqlServerFactCollector.cs | 8 +-- Dashboard/Controls/FinOpsContent.xaml.cs | 1 - Dashboard/Controls/PlanViewerControl.xaml.cs | 2 +- Dashboard/Mcp/McpAnalysisTools.cs | 14 ++-- Dashboard/Services/PlanAnalyzer.cs | 4 +- Installer.Tests/AdversarialTests.cs | 70 +++++++++++-------- Installer.Tests/Helpers/TestDatabaseHelper.cs | 20 +++--- Installer.Tests/IdempotencyTests.cs | 4 +- Installer.Tests/VersionDetectionTests.cs | 12 ++-- Lite.Tests/FactCollectorMiseryTests.cs | 12 ++-- Lite/Analysis/DrillDownCollector.cs | 4 +- Lite/Analysis/DuckDbFactCollector.cs | 8 +-- Lite/Analysis/FactScorer.cs | 8 +-- Lite/Controls/PlanViewerControl.xaml.cs | 2 +- Lite/Mcp/McpAnalysisTools.cs | 10 +-- Lite/Services/ArchiveService.cs | 8 ++- Lite/Services/PlanAnalyzer.cs | 4 +- 19 files changed, 122 insertions(+), 96 deletions(-) diff --git a/Dashboard/Analysis/FactScorer.cs b/Dashboard/Analysis/FactScorer.cs index 8238298..7834795 100644 --- a/Dashboard/Analysis/FactScorer.cs +++ b/Dashboard/Analysis/FactScorer.cs @@ -308,8 +308,10 @@ private static double ScoreBadActorFact(Fact fact) /// private static double ScoreAnomalyFact(Fact fact) { - if (fact.Key.StartsWith("ANOMALY_CPU_SPIKE") || fact.Key.StartsWith("ANOMALY_READ_LATENCY") - || fact.Key.StartsWith("ANOMALY_WRITE_LATENCY")) + if ( fact.Key.StartsWith("ANOMALY_CPU_SPIKE" , StringComparison.OrdinalIgnoreCase) + || fact.Key.StartsWith("ANOMALY_READ_LATENCY" , StringComparison.OrdinalIgnoreCase) + || fact.Key.StartsWith("ANOMALY_WRITE_LATENCY", StringComparison.OrdinalIgnoreCase) + ) { // Deviation-based scoring: 2σ = 0.5, 4σ = 1.0 var deviation = fact.Metadata.GetValueOrDefault("deviation_sigma"); @@ -319,7 +321,7 @@ private static double ScoreAnomalyFact(Fact fact) return base_score * confidence; } - if (fact.Key.StartsWith("ANOMALY_WAIT_")) + if (fact.Key.StartsWith("ANOMALY_WAIT_", StringComparison.OrdinalIgnoreCase)) { // Ratio-based scoring: 5x = 0.5, 20x = 1.0 var ratio = fact.Metadata.GetValueOrDefault("ratio"); @@ -327,7 +329,9 @@ private static double ScoreAnomalyFact(Fact fact) return 0.5 + 0.5 * Math.Min((ratio - 5.0) / 15.0, 1.0); } - if (fact.Key.StartsWith("ANOMALY_BLOCKING_SPIKE") || fact.Key.StartsWith("ANOMALY_DEADLOCK_SPIKE")) + if ( fact.Key.StartsWith("ANOMALY_BLOCKING_SPIKE", StringComparison.OrdinalIgnoreCase) + || fact.Key.StartsWith("ANOMALY_DEADLOCK_SPIKE", StringComparison.OrdinalIgnoreCase) + ) { // Ratio-based: 3x = 0.5, 10x = 1.0 var ratio = fact.Metadata.GetValueOrDefault("ratio"); @@ -859,7 +863,7 @@ private static (double concerning, double? critical)? GetWaitThresholds(string w /// /// An amplifier definition: a named predicate that boosts severity when matched. /// -internal class AmplifierDefinition +internal sealed class AmplifierDefinition { public string Description { get; set; } = string.Empty; public double Boost { get; set; } diff --git a/Dashboard/Analysis/SqlServerDrillDownCollector.cs b/Dashboard/Analysis/SqlServerDrillDownCollector.cs index 050ffe3..cf43597 100644 --- a/Dashboard/Analysis/SqlServerDrillDownCollector.cs +++ b/Dashboard/Analysis/SqlServerDrillDownCollector.cs @@ -62,10 +62,15 @@ public async Task EnrichFindingsAsync(List findings, AnalysisCo if (pathKeys.Contains("QUERY_SPILLS")) await CollectTopSpillingQueries(finding, context); - if (pathKeys.Contains("IO_READ_LATENCY_MS") || pathKeys.Contains("IO_WRITE_LATENCY_MS")) + if ( pathKeys.Contains("IO_READ_LATENCY_MS") + || pathKeys.Contains("IO_WRITE_LATENCY_MS") + ) await CollectFileLatencyBreakdown(finding, context); - if (pathKeys.Contains("LCK") || pathKeys.Contains("LCK_M_S") || pathKeys.Contains("LCK_M_IS")) + if ( pathKeys.Contains("LCK") + || pathKeys.Contains("LCK_M_S") + || pathKeys.Contains("LCK_M_IS") + ) await CollectLockModeBreakdown(finding, context); if (pathKeys.Contains("DB_CONFIG")) @@ -77,7 +82,7 @@ public async Task EnrichFindingsAsync(List findings, AnalysisCo if (pathKeys.Contains("MEMORY_GRANT_PENDING")) await CollectPendingGrants(finding, context); - if (pathKeys.Any(k => k.StartsWith("BAD_ACTOR_"))) + if (pathKeys.Any(k => k.StartsWith("BAD_ACTOR_", StringComparison.OrdinalIgnoreCase))) await CollectBadActorDetail(finding, context); // Plan analysis: for findings with top queries, analyze their cached plans @@ -618,7 +623,7 @@ private async Task CollectPlanAnalysis(AnalysisFinding finding, AnalysisContext // Only analyze plans for bad actor findings (1 plan each). // Skip top_cpu_queries (5 plans would be too heavy). - if (!finding.RootFactKey.StartsWith("BAD_ACTOR_")) return; + if (!finding.RootFactKey.StartsWith("BAD_ACTOR_", StringComparison.OrdinalIgnoreCase)) return; var queryHash = finding.RootFactKey.Replace("BAD_ACTOR_", ""); if (string.IsNullOrEmpty(queryHash)) return; diff --git a/Dashboard/Analysis/SqlServerFactCollector.cs b/Dashboard/Analysis/SqlServerFactCollector.cs index a99d9aa..446e427 100644 --- a/Dashboard/Analysis/SqlServerFactCollector.cs +++ b/Dashboard/Analysis/SqlServerFactCollector.cs @@ -1671,15 +1671,15 @@ private static void GroupParallelismWaits(List facts, AnalysisContext cont /// private static bool IsGeneralLockWait(string waitType) { - if (!waitType.StartsWith("LCK_M_")) return false; + if (!waitType.StartsWith("LCK_M_", StringComparison.OrdinalIgnoreCase)) return false; // Keep individual: reader/writer locks if (waitType is "LCK_M_S" or "LCK_M_IS") return false; // Keep individual: range locks (serializable/repeatable read) - if (waitType.StartsWith("LCK_M_RS_") || - waitType.StartsWith("LCK_M_RIn_") || - waitType.StartsWith("LCK_M_RX_")) return false; + if (waitType.StartsWith("LCK_M_RS_", StringComparison.OrdinalIgnoreCase) || + waitType.StartsWith("LCK_M_RIn_", StringComparison.OrdinalIgnoreCase) || + waitType.StartsWith("LCK_M_RX_", StringComparison.OrdinalIgnoreCase)) return false; // Everything else (X, U, IX, SIX, BU, IU, UIX, etc.) -> group return true; diff --git a/Dashboard/Controls/FinOpsContent.xaml.cs b/Dashboard/Controls/FinOpsContent.xaml.cs index 3533b83..e2a3a6c 100644 --- a/Dashboard/Controls/FinOpsContent.xaml.cs +++ b/Dashboard/Controls/FinOpsContent.xaml.cs @@ -16,7 +16,6 @@ using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Data; -using System.Windows.Controls.Primitives; using System.Windows.Media; using Microsoft.Win32; using PerformanceMonitorDashboard.Helpers; diff --git a/Dashboard/Controls/PlanViewerControl.xaml.cs b/Dashboard/Controls/PlanViewerControl.xaml.cs index 43590b7..3ec40d4 100644 --- a/Dashboard/Controls/PlanViewerControl.xaml.cs +++ b/Dashboard/Controls/PlanViewerControl.xaml.cs @@ -454,7 +454,7 @@ private WpfPath CreateElbowConnector(PlanNode parent, PlanNode child) }; } - private object BuildEdgeTooltipContent(PlanNode child) + private Border BuildEdgeTooltipContent(PlanNode child) { var grid = new Grid { MinWidth = 240 }; grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); diff --git a/Dashboard/Mcp/McpAnalysisTools.cs b/Dashboard/Mcp/McpAnalysisTools.cs index 72ffa00..c20db79 100644 --- a/Dashboard/Mcp/McpAnalysisTools.cs +++ b/Dashboard/Mcp/McpAnalysisTools.cs @@ -433,15 +433,17 @@ public static System.Collections.Generic.List GetForStoryPath(string sto { if (!ByFactKey.TryGetValue(key, out var recommendations)) { - if (key.StartsWith("BAD_ACTOR_")) + if (key.StartsWith("BAD_ACTOR_", StringComparison.OrdinalIgnoreCase)) ByFactKey.TryGetValue("BAD_ACTOR", out recommendations); - else if (key.StartsWith("ANOMALY_CPU")) + else if (key.StartsWith("ANOMALY_CPU", StringComparison.OrdinalIgnoreCase)) ByFactKey.TryGetValue("ANOMALY_CPU", out recommendations); - else if (key.StartsWith("ANOMALY_WAIT_")) + else if (key.StartsWith("ANOMALY_WAIT_", StringComparison.OrdinalIgnoreCase)) ByFactKey.TryGetValue("ANOMALY_WAIT", out recommendations); - else if (key.StartsWith("ANOMALY_BLOCKING") || key.StartsWith("ANOMALY_DEADLOCK")) + else if (key.StartsWith("ANOMALY_BLOCKING", StringComparison.OrdinalIgnoreCase) + || key.StartsWith("ANOMALY_DEADLOCK", StringComparison.OrdinalIgnoreCase)) ByFactKey.TryGetValue("ANOMALY_BLOCKING", out recommendations); - else if (key.StartsWith("ANOMALY_READ") || key.StartsWith("ANOMALY_WRITE")) + else if (key.StartsWith("ANOMALY_READ", StringComparison.OrdinalIgnoreCase) + || key.StartsWith("ANOMALY_WRITE", StringComparison.OrdinalIgnoreCase)) ByFactKey.TryGetValue("ANOMALY_IO", out recommendations); if (recommendations == null) continue; } @@ -459,7 +461,7 @@ public static System.Collections.Generic.List GetForStoryPath(string sto } } -internal record ToolRecommendation( +internal sealed record ToolRecommendation( string Tool, string Reason, System.Collections.Generic.Dictionary? SuggestedParams = null); diff --git a/Dashboard/Services/PlanAnalyzer.cs b/Dashboard/Services/PlanAnalyzer.cs index bc4fe05..6a25568 100644 --- a/Dashboard/Services/PlanAnalyzer.cs +++ b/Dashboard/Services/PlanAnalyzer.cs @@ -325,7 +325,7 @@ private static void AnalyzeStatement(PlanStatement stmt) private static void CheckForTableVariables(PlanNode node, bool isModification, ref bool hasTableVar, ref bool modifiesTableVar) { - if (!string.IsNullOrEmpty(node.ObjectName) && node.ObjectName.StartsWith("@")) + if (!string.IsNullOrEmpty(node.ObjectName) && node.ObjectName.StartsWith("@", StringComparison.OrdinalIgnoreCase)) { hasTableVar = true; if (isModification && (node.PhysicalOp.Contains("Insert", StringComparison.OrdinalIgnoreCase) @@ -573,7 +573,7 @@ private static void AnalyzeNode(PlanNode node, PlanStatement stmt) "Leading wildcard LIKE prevents an index seek — SQL Server must scan every row. If substring search performance is critical, consider a full-text index or a trigram-based approach.", "CASE expression in predicate" => "CASE expression in a predicate prevents an index seek. Rewrite using separate WHERE clauses combined with OR, or split into multiple queries.", - _ when nonSargableReason.StartsWith("Function call") => + _ when nonSargableReason.StartsWith("Function call", StringComparison.OrdinalIgnoreCase) => $"{nonSargableReason} prevents an index seek. Remove the function from the column side — apply it to the parameter instead, or create a computed column with the expression and index that.", _ => $"{nonSargableReason} prevents an index seek, forcing a scan." diff --git a/Installer.Tests/AdversarialTests.cs b/Installer.Tests/AdversarialTests.cs index 6f62c44..2908b6b 100644 --- a/Installer.Tests/AdversarialTests.cs +++ b/Installer.Tests/AdversarialTests.cs @@ -36,11 +36,11 @@ public async Task UpgradeFailure_DoesNotDropDatabase() // Insert a canary row we can check survived using (var conn = new SqlConnection(TestDatabaseHelper.GetTestDbConnectionString())) { - await conn.OpenAsync(); + await conn.OpenAsync(TestContext.Current.CancellationToken); using var cmd = new SqlCommand(@" CREATE TABLE config.canary_data (id int NOT NULL, value nvarchar(50) NOT NULL); INSERT INTO config.canary_data VALUES (1, 'must_survive');", conn); - await cmd.ExecuteNonQueryAsync(); + await cmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken); } // Create a poisoned upgrade that will fail @@ -58,17 +58,19 @@ public async Task UpgradeFailure_DoesNotDropDatabase() dir.RootPath, TestDatabaseHelper.GetTestDbConnectionString(), "2.0.0", - "2.1.0"); + "2.1.0", + cancellationToken: TestContext.Current.CancellationToken + ); Assert.True(failureCount > 0, "Upgrade should have failed"); // The critical assertion: database and data must still exist using (var conn = new SqlConnection(TestDatabaseHelper.GetTestDbConnectionString())) { - await conn.OpenAsync(); + await conn.OpenAsync(TestContext.Current.CancellationToken); using var cmd = new SqlCommand( "SELECT value FROM config.canary_data WHERE id = 1;", conn); - var result = await cmd.ExecuteScalarAsync(); + var result = await cmd.ExecuteScalarAsync(TestContext.Current.CancellationToken); Assert.Equal("must_survive", result?.ToString()); } } @@ -91,10 +93,10 @@ public async Task PartialInstall_InstallScriptsRecover() // Verify: only installation_history exists, no collect/report schemas using (var conn = new SqlConnection(TestDatabaseHelper.GetTestDbConnectionString())) { - await conn.OpenAsync(); + await conn.OpenAsync(TestContext.Current.CancellationToken); using var cmd = new SqlCommand( "SELECT COUNT(*) FROM sys.tables WHERE schema_id != SCHEMA_ID('config');", conn); - var nonConfigTables = (int)(await cmd.ExecuteScalarAsync())!; + var nonConfigTables = (int)(await cmd.ExecuteScalarAsync(TestContext.Current.CancellationToken))!; Assert.Equal(0, nonConfigTables); } @@ -112,18 +114,18 @@ public async Task PartialInstall_InstallScriptsRecover() var fileName = Path.GetFileName(file); try { - var sql = await File.ReadAllTextAsync(file); + var sql = await File.ReadAllTextAsync(file, TestContext.Current.CancellationToken); sql = RewriteForTestDatabase(sql); var batches = SplitGoBatches(sql); using var conn = new SqlConnection(connectionString); - await conn.OpenAsync(); + await conn.OpenAsync(TestContext.Current.CancellationToken); foreach (var batch in batches) { if (string.IsNullOrWhiteSpace(batch)) continue; using var cmd = new SqlCommand(batch, conn) { CommandTimeout = 120 }; - try { await cmd.ExecuteNonQueryAsync(); } + try { await cmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken); } catch (SqlException ex) { if (IsExpectedTestFailure(ex, fileName)) continue; @@ -144,12 +146,12 @@ public async Task PartialInstall_InstallScriptsRecover() // Verify core tables were created from the partial state using (var conn = new SqlConnection(TestDatabaseHelper.GetTestDbConnectionString())) { - await conn.OpenAsync(); + await conn.OpenAsync(TestContext.Current.CancellationToken); using var cmd = new SqlCommand(@" SELECT COUNT(*) FROM sys.tables WHERE schema_id = SCHEMA_ID('collect') AND name IN ('wait_stats', 'query_stats', 'cpu_utilization_stats');", conn); - var collectTables = (int)(await cmd.ExecuteScalarAsync())!; + var collectTables = (int)(await cmd.ExecuteScalarAsync(TestContext.Current.CancellationToken))!; Assert.True(collectTables >= 3, $"Expected at least 3 collect tables, got {collectTables}"); } } @@ -190,17 +192,19 @@ public async Task CriticalFileFailure_AbortsInstallation() var result = await InstallationService.ExecuteInstallationAsync( TestDatabaseHelper.GetTestDbConnectionString(), files, - cleanInstall: false); + cleanInstall: false, + cancellationToken: TestContext.Current.CancellationToken + ); Assert.False(result.Success); Assert.True(result.FilesFailed >= 1); // Verify abort: scripts after 02_ must NOT have run using var conn = new SqlConnection(TestDatabaseHelper.GetTestDbConnectionString()); - await conn.OpenAsync(); + await conn.OpenAsync(TestContext.Current.CancellationToken); using var cmd = new SqlCommand( "SELECT OBJECT_ID('dbo.should_not_exist', 'U');", conn); - var obj = await cmd.ExecuteScalarAsync(); + var obj = await cmd.ExecuteScalarAsync(TestContext.Current.CancellationToken); Assert.True(obj == null || obj == DBNull.Value, "03_config.sql should not have executed after 02_ critical failure"); } @@ -243,13 +247,13 @@ await InstallationService.ExecuteAllUpgradesAsync( // Version must still be 2.0.0 — no SUCCESS row written for 2.1.0 using var conn = new SqlConnection(TestDatabaseHelper.GetTestDbConnectionString()); - await conn.OpenAsync(); + await conn.OpenAsync(TestContext.Current.CancellationToken); using var cmd = new SqlCommand(@" SELECT TOP 1 installer_version FROM config.installation_history WHERE installation_status = 'SUCCESS' ORDER BY installation_date DESC;", conn); - var version = await cmd.ExecuteScalarAsync(); + var version = await cmd.ExecuteScalarAsync(TestContext.Current.CancellationToken); Assert.Equal("2.0.0", version?.ToString()); } @@ -278,16 +282,18 @@ public async Task NonCriticalFileFailure_ContinuesInstallation() var result = await InstallationService.ExecuteInstallationAsync( TestDatabaseHelper.GetTestDbConnectionString(), files, - cleanInstall: false); + cleanInstall: false, + cancellationToken: TestContext.Current.CancellationToken + ); // 04_ failed but 05_ should have run Assert.True(result.FilesFailed >= 1); using var conn = new SqlConnection(TestDatabaseHelper.GetTestDbConnectionString()); - await conn.OpenAsync(); + await conn.OpenAsync(TestContext.Current.CancellationToken); using var cmd = new SqlCommand( "SELECT OBJECT_ID('dbo.proof_it_continued', 'U');", conn); - var obj = await cmd.ExecuteScalarAsync(); + var obj = await cmd.ExecuteScalarAsync(TestContext.Current.CancellationToken); Assert.True(obj != null && obj != DBNull.Value, "05_ should have executed despite 04_ failure"); } @@ -313,7 +319,9 @@ public async Task CorruptSqlContent_FailsGracefully() var result = await InstallationService.ExecuteInstallationAsync( TestDatabaseHelper.GetTestDbConnectionString(), files, - cleanInstall: false); + cleanInstall: false, + cancellationToken: TestContext.Current.CancellationToken + ); // Should complete (not throw), with 04_ counted as failed Assert.True(result.FilesFailed >= 1); @@ -337,7 +345,9 @@ public async Task EmptySqlFile_DoesNotCrash() var result = await InstallationService.ExecuteInstallationAsync( TestDatabaseHelper.GetTestDbConnectionString(), files, - cleanInstall: false); + cleanInstall: false, + cancellationToken: TestContext.Current.CancellationToken + ); Assert.True(result.Success); } @@ -351,7 +361,9 @@ public async Task EmptySqlFile_DoesNotCrash() public async Task VersionCheck_Sql2022_IsSupported() { var info = await InstallationService.TestConnectionAsync( - TestDatabaseHelper.GetConnectionString()); + TestDatabaseHelper.GetConnectionString(), + cancellationToken: TestContext.Current.CancellationToken + ); Assert.True(info.IsConnected); Assert.True(info.ProductMajorVersion >= 13, @@ -409,7 +421,7 @@ public async Task VersionDetection_ConnectionFailure_ReturnsNull() // Intentionally bad connection string var badConnStr = "Server=DOESNOTEXIST;Database=master;User Id=sa;Password=x;TrustServerCertificate=true;Connect Timeout=2;"; - var version = await InstallationService.GetInstalledVersionAsync(badConnStr); + var version = await InstallationService.GetInstalledVersionAsync(badConnStr, cancellationToken: TestContext.Current.CancellationToken); // GUI swallows exceptions and returns null. // This means a transient network failure could cause the GUI to treat @@ -431,14 +443,14 @@ public async Task RestrictedPermissions_FreshInstall_FailsClearly() // Verify the login can connect but has no dbcreator using (var conn = new SqlConnection(restrictedConnStr)) { - await conn.OpenAsync(); + await conn.OpenAsync(TestContext.Current.CancellationToken); using var cmd = new SqlCommand("SELECT IS_SRVROLEMEMBER('dbcreator');", conn); - var isDbCreator = await cmd.ExecuteScalarAsync(); + var isDbCreator = await cmd.ExecuteScalarAsync(TestContext.Current.CancellationToken); Assert.Equal(0, Convert.ToInt32(isDbCreator)); } // Version detection should return null (no PerformanceMonitor database) - var version = await InstallationService.GetInstalledVersionAsync(restrictedConnStr); + var version = await InstallationService.GetInstalledVersionAsync(restrictedConnStr, cancellationToken: TestContext.Current.CancellationToken); Assert.Null(version); // Try to install — 01_install_database.sql should fail on CREATE DATABASE @@ -455,7 +467,9 @@ IF DB_ID(N'PerformanceMonitor_RestrictedTest') IS NULL var result = await InstallationService.ExecuteInstallationAsync( restrictedConnStr, files, - cleanInstall: false); + cleanInstall: false, + cancellationToken: TestContext.Current.CancellationToken + ); // Must fail — and because 01_ is critical, it should abort Assert.False(result.Success); diff --git a/Installer.Tests/Helpers/TestDatabaseHelper.cs b/Installer.Tests/Helpers/TestDatabaseHelper.cs index 7a9111f..8f5b9f6 100644 --- a/Installer.Tests/Helpers/TestDatabaseHelper.cs +++ b/Installer.Tests/Helpers/TestDatabaseHelper.cs @@ -19,18 +19,18 @@ public static string GetTestDbConnectionString() public static async Task CreateTestDatabaseAsync() { using var connection = new SqlConnection(GetConnectionString()); - await connection.OpenAsync(); + await connection.OpenAsync(TestContext.Current.CancellationToken); using var cmd = new SqlCommand($@" IF DB_ID(N'{TestDatabaseName}') IS NULL CREATE DATABASE [{TestDatabaseName}];", connection); - await cmd.ExecuteNonQueryAsync(); + await cmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken); } public static async Task DropTestDatabaseAsync() { using var connection = new SqlConnection(GetConnectionString()); - await connection.OpenAsync(); + await connection.OpenAsync(TestContext.Current.CancellationToken); using var cmd = new SqlCommand($@" IF DB_ID(N'{TestDatabaseName}') IS NOT NULL @@ -38,7 +38,7 @@ IF DB_ID(N'{TestDatabaseName}') IS NOT NULL ALTER DATABASE [{TestDatabaseName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{TestDatabaseName}]; END;", connection); - await cmd.ExecuteNonQueryAsync(); + await cmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken); } /// @@ -50,7 +50,7 @@ public static async Task CreatePartialInstallationAsync(string version) await CreateTestDatabaseAsync(); using var connection = new SqlConnection(GetTestDbConnectionString()); - await connection.OpenAsync(); + await connection.OpenAsync(TestContext.Current.CancellationToken); // Must match the real schema from 01_install_database.sql exactly, // otherwise CREATE OR ALTER VIEW on config.current_version will fail @@ -83,7 +83,7 @@ INSERT INTO config.installation_history VALUES (N'{version}', N'SUCCESS', N'UPGRADE', @@VERSION, N'Test');", connection); - await cmd.ExecuteNonQueryAsync(); + await cmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken); } /// @@ -95,7 +95,7 @@ public static async Task CreateInstallationWithNoSuccessRowsAsync() await CreateTestDatabaseAsync(); using var connection = new SqlConnection(GetTestDbConnectionString()); - await connection.OpenAsync(); + await connection.OpenAsync(TestContext.Current.CancellationToken); using var cmd = new SqlCommand(@" IF SCHEMA_ID('config') IS NULL @@ -120,7 +120,7 @@ installation_notes nvarchar(max) NULL, CONSTRAINT PK_installation_history PRIMARY KEY CLUSTERED (installation_id) );", connection); - await cmd.ExecuteNonQueryAsync(); + await cmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken); } /// @@ -131,7 +131,7 @@ public static async Task CreateInstallationWithOnlyFailedRowsAsync() await CreateInstallationWithNoSuccessRowsAsync(); using var connection = new SqlConnection(GetTestDbConnectionString()); - await connection.OpenAsync(); + await connection.OpenAsync(TestContext.Current.CancellationToken); using var cmd = new SqlCommand(@" INSERT INTO config.installation_history @@ -139,6 +139,6 @@ INSERT INTO config.installation_history VALUES (N'2.0.0', N'FAILED', N'UPGRADE', @@VERSION, N'Test');", connection); - await cmd.ExecuteNonQueryAsync(); + await cmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken); } } diff --git a/Installer.Tests/IdempotencyTests.cs b/Installer.Tests/IdempotencyTests.cs index 7d37fa1..9ab0ba6 100644 --- a/Installer.Tests/IdempotencyTests.cs +++ b/Installer.Tests/IdempotencyTests.cs @@ -100,7 +100,7 @@ private static List GetFilteredInstallFiles(string installDir) var batches = SplitBatches(sql); using var connection = new SqlConnection(connectionString); - await connection.OpenAsync(); + await connection.OpenAsync(TestContext.Current.CancellationToken); foreach (var batch in batches) { @@ -113,7 +113,7 @@ private static List GetFilteredInstallFiles(string installDir) try { - await cmd.ExecuteNonQueryAsync(); + await cmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken); } catch (SqlException ex) { diff --git a/Installer.Tests/VersionDetectionTests.cs b/Installer.Tests/VersionDetectionTests.cs index 2002518..8eb5406 100644 --- a/Installer.Tests/VersionDetectionTests.cs +++ b/Installer.Tests/VersionDetectionTests.cs @@ -81,7 +81,7 @@ public async Task MultipleSuccessRows_ReturnsLatest() // Add a newer success row using var connection = new SqlConnection(TestDatabaseHelper.GetTestDbConnectionString()); - await connection.OpenAsync(); + await connection.OpenAsync(TestContext.Current.CancellationToken); using var cmd = new SqlCommand(@" -- Use explicit future date to ensure this row sorts first INSERT INTO config.installation_history @@ -89,7 +89,7 @@ INSERT INTO config.installation_history VALUES (N'2.2.0', N'SUCCESS', N'UPGRADE', @@VERSION, N'Test', DATEADD(HOUR, 1, SYSDATETIME()));", connection); - await cmd.ExecuteNonQueryAsync(); + await cmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken); var version = await GetInstalledVersionFromTestDbAsync(); Assert.Equal("2.2.0", version); @@ -106,19 +106,19 @@ INSERT INTO config.installation_history try { using var connection = new SqlConnection(TestDatabaseHelper.GetConnectionString()); - await connection.OpenAsync(); + await connection.OpenAsync(TestContext.Current.CancellationToken); // Check if database exists using var dbCheckCmd = new SqlCommand($@" SELECT database_id FROM sys.databases WHERE name = N'{testDbName}';", connection); - var dbExists = await dbCheckCmd.ExecuteScalarAsync(); + var dbExists = await dbCheckCmd.ExecuteScalarAsync(TestContext.Current.CancellationToken); if (dbExists == null || dbExists == DBNull.Value) return null; // Check if installation_history table exists using var tableCheckCmd = new SqlCommand($@" SELECT OBJECT_ID(N'{testDbName}.config.installation_history', N'U');", connection); - var tableExists = await tableCheckCmd.ExecuteScalarAsync(); + var tableExists = await tableCheckCmd.ExecuteScalarAsync(TestContext.Current.CancellationToken); if (tableExists == null || tableExists == DBNull.Value) return null; @@ -128,7 +128,7 @@ SELECT TOP 1 installer_version FROM {testDbName}.config.installation_history WHERE installation_status = 'SUCCESS' ORDER BY installation_date DESC;", connection); - var version = await versionCmd.ExecuteScalarAsync(); + var version = await versionCmd.ExecuteScalarAsync(TestContext.Current.CancellationToken); if (version != null && version != DBNull.Value) return version.ToString(); diff --git a/Lite.Tests/FactCollectorMiseryTests.cs b/Lite.Tests/FactCollectorMiseryTests.cs index b22b213..288700d 100644 --- a/Lite.Tests/FactCollectorMiseryTests.cs +++ b/Lite.Tests/FactCollectorMiseryTests.cs @@ -279,7 +279,7 @@ public async Task SingleCollectionPoint_CpuStillCollected() // Insert just one CPU data point manually using var readLock = _duckDb.AcquireReadLock(); using var connection = _duckDb.CreateConnection(); - await connection.OpenAsync(); + await connection.OpenAsync(TestContext.Current.CancellationToken); using var cmd = connection.CreateCommand(); cmd.CommandText = @" @@ -292,7 +292,7 @@ INSERT INTO cpu_utilization_stats cmd.Parameters.Add(new DuckDBParameter { Value = TestDataSeeder.TestServerId }); cmd.Parameters.Add(new DuckDBParameter { Value = TestDataSeeder.TestServerName }); cmd.Parameters.Add(new DuckDBParameter { Value = TestDataSeeder.TestPeriodEnd.AddMinutes(-30) }); - await cmd.ExecuteNonQueryAsync(); + await cmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken); var collector = new DuckDbFactCollector(_duckDb); var context = TestDataSeeder.CreateTestContext(); @@ -733,7 +733,7 @@ public async Task TraceFlags_OnlySessionFlags_ProducesNoFact() // Seed a session-level trace flag (not global) using var readLock = _duckDb.AcquireReadLock(); using var connection = _duckDb.CreateConnection(); - await connection.OpenAsync(); + await connection.OpenAsync(TestContext.Current.CancellationToken); using var cmd = connection.CreateCommand(); cmd.CommandText = @" @@ -745,7 +745,7 @@ INSERT INTO trace_flags cmd.Parameters.Add(new DuckDBParameter { Value = TestDataSeeder.TestPeriodEnd }); cmd.Parameters.Add(new DuckDBParameter { Value = TestDataSeeder.TestServerId }); cmd.Parameters.Add(new DuckDBParameter { Value = TestDataSeeder.TestServerName }); - await cmd.ExecuteNonQueryAsync(); + await cmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken); var collector = new DuckDbFactCollector(_duckDb); var context = TestDataSeeder.CreateTestContext(); @@ -773,7 +773,7 @@ public async Task QueryStats_ZeroExecutions_ProducesNoFacts() // Seed query_stats with delta_execution_count = 0 (stale row) using var readLock = _duckDb.AcquireReadLock(); using var connection = _duckDb.CreateConnection(); - await connection.OpenAsync(); + await connection.OpenAsync(TestContext.Current.CancellationToken); using var cmd = connection.CreateCommand(); cmd.CommandText = @" @@ -786,7 +786,7 @@ INSERT INTO query_stats cmd.Parameters.Add(new DuckDBParameter { Value = TestDataSeeder.TestPeriodEnd.AddMinutes(-30) }); cmd.Parameters.Add(new DuckDBParameter { Value = TestDataSeeder.TestServerId }); cmd.Parameters.Add(new DuckDBParameter { Value = TestDataSeeder.TestServerName }); - await cmd.ExecuteNonQueryAsync(); + await cmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken); var collector = new DuckDbFactCollector(_duckDb); var context = TestDataSeeder.CreateTestContext(); diff --git a/Lite/Analysis/DrillDownCollector.cs b/Lite/Analysis/DrillDownCollector.cs index 928d20f..b3d515e 100644 --- a/Lite/Analysis/DrillDownCollector.cs +++ b/Lite/Analysis/DrillDownCollector.cs @@ -74,7 +74,7 @@ public async Task EnrichFindingsAsync(List findings, AnalysisCo if (pathKeys.Contains("MEMORY_GRANT_PENDING")) await CollectPendingGrants(finding, context); - if (pathKeys.Any(k => k.StartsWith("BAD_ACTOR_"))) + if (pathKeys.Any(k => k.StartsWith("BAD_ACTOR_", StringComparison.OrdinalIgnoreCase))) await CollectBadActorDetail(finding, context); // Plan analysis: for findings with top queries, analyze their cached plans @@ -563,7 +563,7 @@ private async Task CollectPlanAnalysis(AnalysisFinding finding, AnalysisContext // Only analyze plans for bad actor findings (1 plan each). // Skip top_cpu_queries (5 plans would be too heavy). - if (!finding.RootFactKey.StartsWith("BAD_ACTOR_")) return; + if (!finding.RootFactKey.StartsWith("BAD_ACTOR_", StringComparison.OrdinalIgnoreCase)) return; var queryHash = finding.RootFactKey.Replace("BAD_ACTOR_", ""); if (string.IsNullOrEmpty(queryHash)) return; diff --git a/Lite/Analysis/DuckDbFactCollector.cs b/Lite/Analysis/DuckDbFactCollector.cs index 24644d5..b6b12b9 100644 --- a/Lite/Analysis/DuckDbFactCollector.cs +++ b/Lite/Analysis/DuckDbFactCollector.cs @@ -1563,15 +1563,15 @@ private static void GroupParallelismWaits(List facts, AnalysisContext cont /// private static bool IsGeneralLockWait(string waitType) { - if (!waitType.StartsWith("LCK_M_")) return false; + if (!waitType.StartsWith("LCK_M_", StringComparison.OrdinalIgnoreCase)) return false; // Keep individual: reader/writer locks if (waitType is "LCK_M_S" or "LCK_M_IS") return false; // Keep individual: range locks (serializable/repeatable read) - if (waitType.StartsWith("LCK_M_RS_") || - waitType.StartsWith("LCK_M_RIn_") || - waitType.StartsWith("LCK_M_RX_")) return false; + if (waitType.StartsWith("LCK_M_RS_", StringComparison.OrdinalIgnoreCase) || + waitType.StartsWith("LCK_M_RIn_", StringComparison.OrdinalIgnoreCase) || + waitType.StartsWith("LCK_M_RX_", StringComparison.OrdinalIgnoreCase)) return false; // Everything else (X, U, IX, SIX, BU, IU, UIX, etc.) → group return true; diff --git a/Lite/Analysis/FactScorer.cs b/Lite/Analysis/FactScorer.cs index a9fc97e..1ec47f7 100644 --- a/Lite/Analysis/FactScorer.cs +++ b/Lite/Analysis/FactScorer.cs @@ -308,8 +308,8 @@ private static double ScoreBadActorFact(Fact fact) /// private static double ScoreAnomalyFact(Fact fact) { - if (fact.Key.StartsWith("ANOMALY_CPU_SPIKE") || fact.Key.StartsWith("ANOMALY_READ_LATENCY") - || fact.Key.StartsWith("ANOMALY_WRITE_LATENCY")) + if (fact.Key.StartsWith("ANOMALY_CPU_SPIKE", StringComparison.OrdinalIgnoreCase) || fact.Key.StartsWith("ANOMALY_READ_LATENCY", StringComparison.OrdinalIgnoreCase) + || fact.Key.StartsWith("ANOMALY_WRITE_LATENCY", StringComparison.OrdinalIgnoreCase)) { // Deviation-based scoring: 2σ = 0.5, 4σ = 1.0 var deviation = fact.Metadata.GetValueOrDefault("deviation_sigma"); @@ -319,7 +319,7 @@ private static double ScoreAnomalyFact(Fact fact) return base_score * confidence; } - if (fact.Key.StartsWith("ANOMALY_WAIT_")) + if (fact.Key.StartsWith("ANOMALY_WAIT_", StringComparison.OrdinalIgnoreCase)) { // Ratio-based scoring: 5x = 0.5, 20x = 1.0 var ratio = fact.Metadata.GetValueOrDefault("ratio"); @@ -327,7 +327,7 @@ private static double ScoreAnomalyFact(Fact fact) return 0.5 + 0.5 * Math.Min((ratio - 5.0) / 15.0, 1.0); } - if (fact.Key.StartsWith("ANOMALY_BLOCKING_SPIKE") || fact.Key.StartsWith("ANOMALY_DEADLOCK_SPIKE")) + if (fact.Key.StartsWith("ANOMALY_BLOCKING_SPIKE", StringComparison.OrdinalIgnoreCase) || fact.Key.StartsWith("ANOMALY_DEADLOCK_SPIKE", StringComparison.OrdinalIgnoreCase)) { // Ratio-based: 3x = 0.5, 10x = 1.0 var ratio = fact.Metadata.GetValueOrDefault("ratio"); diff --git a/Lite/Controls/PlanViewerControl.xaml.cs b/Lite/Controls/PlanViewerControl.xaml.cs index b740cde..fc40d6a 100644 --- a/Lite/Controls/PlanViewerControl.xaml.cs +++ b/Lite/Controls/PlanViewerControl.xaml.cs @@ -471,7 +471,7 @@ private WpfPath CreateElbowConnector(PlanNode parent, PlanNode child) }; } - private object BuildEdgeTooltipContent(PlanNode child) + private Border BuildEdgeTooltipContent(PlanNode child) { var grid = new Grid { MinWidth = 240 }; grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); diff --git a/Lite/Mcp/McpAnalysisTools.cs b/Lite/Mcp/McpAnalysisTools.cs index 9a194fc..9544b46 100644 --- a/Lite/Mcp/McpAnalysisTools.cs +++ b/Lite/Mcp/McpAnalysisTools.cs @@ -794,15 +794,15 @@ public static List GetForStoryPath(string storyPath) if (!ByFactKey.TryGetValue(key, out var recommendations)) { // Handle dynamic keys by checking prefix - if (key.StartsWith("BAD_ACTOR_")) + if (key.StartsWith("BAD_ACTOR_", StringComparison.OrdinalIgnoreCase)) ByFactKey.TryGetValue("BAD_ACTOR", out recommendations); - else if (key.StartsWith("ANOMALY_CPU")) + else if (key.StartsWith("ANOMALY_CPU", StringComparison.OrdinalIgnoreCase)) ByFactKey.TryGetValue("ANOMALY_CPU", out recommendations); - else if (key.StartsWith("ANOMALY_WAIT_")) + else if (key.StartsWith("ANOMALY_WAIT_", StringComparison.OrdinalIgnoreCase)) ByFactKey.TryGetValue("ANOMALY_WAIT", out recommendations); - else if (key.StartsWith("ANOMALY_BLOCKING") || key.StartsWith("ANOMALY_DEADLOCK")) + else if (key.StartsWith("ANOMALY_BLOCKING", StringComparison.OrdinalIgnoreCase) || key.StartsWith("ANOMALY_DEADLOCK", StringComparison.OrdinalIgnoreCase)) ByFactKey.TryGetValue("ANOMALY_BLOCKING", out recommendations); - else if (key.StartsWith("ANOMALY_READ") || key.StartsWith("ANOMALY_WRITE")) + else if (key.StartsWith("ANOMALY_READ", StringComparison.OrdinalIgnoreCase) || key.StartsWith("ANOMALY_WRITE", StringComparison.OrdinalIgnoreCase)) ByFactKey.TryGetValue("ANOMALY_IO", out recommendations); if (recommendations == null) continue; } diff --git a/Lite/Services/ArchiveService.cs b/Lite/Services/ArchiveService.cs index 505da8a..70c66cb 100644 --- a/Lite/Services/ArchiveService.cs +++ b/Lite/Services/ArchiveService.cs @@ -261,11 +261,13 @@ private void CompactParquetFiles() if (month != null && table != null) { var key = (month, table); - if (!groups.ContainsKey(key)) + if (!groups.TryGetValue(key, out List? value)) { - groups[key] = []; + value = []; + groups[key] = value; } - groups[key].Add(file); + + value.Add(file); } else { diff --git a/Lite/Services/PlanAnalyzer.cs b/Lite/Services/PlanAnalyzer.cs index 0905dd7..5f7e326 100644 --- a/Lite/Services/PlanAnalyzer.cs +++ b/Lite/Services/PlanAnalyzer.cs @@ -325,7 +325,7 @@ private static void AnalyzeStatement(PlanStatement stmt) private static void CheckForTableVariables(PlanNode node, bool isModification, ref bool hasTableVar, ref bool modifiesTableVar) { - if (!string.IsNullOrEmpty(node.ObjectName) && node.ObjectName.StartsWith("@")) + if (!string.IsNullOrEmpty(node.ObjectName) && node.ObjectName.StartsWith("@", StringComparison.OrdinalIgnoreCase)) { hasTableVar = true; if (isModification && (node.PhysicalOp.Contains("Insert", StringComparison.OrdinalIgnoreCase) @@ -573,7 +573,7 @@ private static void AnalyzeNode(PlanNode node, PlanStatement stmt) "Leading wildcard LIKE prevents an index seek — SQL Server must scan every row. If substring search performance is critical, consider a full-text index or a trigram-based approach.", "CASE expression in predicate" => "CASE expression in a predicate prevents an index seek. Rewrite using separate WHERE clauses combined with OR, or split into multiple queries.", - _ when nonSargableReason.StartsWith("Function call") => + _ when nonSargableReason.StartsWith("Function call", StringComparison.OrdinalIgnoreCase) => $"{nonSargableReason} prevents an index seek. Remove the function from the column side — apply it to the parameter instead, or create a computed column with the expression and index that.", _ => $"{nonSargableReason} prevents an index seek, forcing a scan." From a565e6358cbef6d2168bd193ab00f2e772ab7a34 Mon Sep 17 00:00:00 2001 From: Orestes Zoupanos Date: Fri, 20 Mar 2026 09:43:29 +0000 Subject: [PATCH 2/3] minor version Nuget updates --- Dashboard/Dashboard.csproj | 6 +++--- Lite/PerformanceMonitorLite.csproj | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Dashboard/Dashboard.csproj b/Dashboard/Dashboard.csproj index aa54e6f..32d0ef3 100644 --- a/Dashboard/Dashboard.csproj +++ b/Dashboard/Dashboard.csproj @@ -35,12 +35,12 @@ - - + + - + diff --git a/Lite/PerformanceMonitorLite.csproj b/Lite/PerformanceMonitorLite.csproj index be54641..8f7a6f8 100644 --- a/Lite/PerformanceMonitorLite.csproj +++ b/Lite/PerformanceMonitorLite.csproj @@ -49,16 +49,16 @@ - + - + - + From 1b3a3b4023dce670a25eef3e9f4881a7c43e45e3 Mon Sep 17 00:00:00 2001 From: Orestes Zoupanos Date: Fri, 20 Mar 2026 09:44:11 +0000 Subject: [PATCH 3/3] fix error NETSDK1151 The referenced project '..\InstallerGui\InstallerGui.csproj' is a self-contained executable. A self-contained executable cannot be referenced by a non self-contained executable. For more information, see https://aka.ms/netsdk1151 Installer.Tests C:\Program Files\dotnet\sdk\10.0.104\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.targets 1330 --- Installer.Tests/Installer.Tests.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Installer.Tests/Installer.Tests.csproj b/Installer.Tests/Installer.Tests.csproj index 9a3f257..c930745 100644 --- a/Installer.Tests/Installer.Tests.csproj +++ b/Installer.Tests/Installer.Tests.csproj @@ -7,6 +7,7 @@ enable true CA1849;CA2007;CA1508;CA1822;CA1805;CA1510;CA1816;CA1861;CA1845;CA2201;CS4014;NU1701;CA1001;CA1848;CA1852;CA1305;CA1860;CA1707;CA1507;CA1806 + false