diff --git a/Lite.Tests/DismissedArchiveSidecarTests.cs b/Lite.Tests/DismissedArchiveSidecarTests.cs new file mode 100644 index 0000000..53c61d5 --- /dev/null +++ b/Lite.Tests/DismissedArchiveSidecarTests.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using DuckDB.NET.Data; +using PerformanceMonitorLite.Database; +using PerformanceMonitorLite.Tests.Helpers; +using Xunit; + +namespace PerformanceMonitorLite.Tests; + +/// +/// Tests that the dismissed_archive_alerts sidecar table allows archived alerts +/// to be dismissed, and that the archive view filters them out correctly. +/// +public class DismissedArchiveSidecarTests : IDisposable +{ + private readonly string _tempDir; + private readonly string _dbPath; + private readonly TestAlertDataHelper _helper; + + public DismissedArchiveSidecarTests() + { + _tempDir = Path.Combine(Path.GetTempPath(), "LiteTests_" + Guid.NewGuid().ToString("N")[..8]); + Directory.CreateDirectory(_tempDir); + _dbPath = Path.Combine(_tempDir, "test.duckdb"); + _helper = new TestAlertDataHelper(_dbPath); + } + + public void Dispose() + { + try + { + if (Directory.Exists(_tempDir)) + Directory.Delete(_tempDir, recursive: true); + } + catch + { + /* Best-effort cleanup */ + } + } + + private async Task InitializeDatabaseAsync() + { + var initializer = new DuckDbInitializer(_dbPath); + await initializer.InitializeAsync(); + + var connection = new DuckDBConnection($"Data Source={_dbPath}"); + await connection.OpenAsync(TestContext.Current.CancellationToken); + return connection; + } + + [Fact] + public async Task SidecarTable_ExistsAfterInit() + { + using var connection = await InitializeDatabaseAsync(); + + using var cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT COUNT(1) FROM information_schema.tables WHERE table_name = 'dismissed_archive_alerts'"; + var count = Convert.ToInt32(await cmd.ExecuteScalarAsync(TestContext.Current.CancellationToken)); + + Assert.Equal(1, count); + } + + [Fact] + public async Task SidecarTable_HasCorrectColumns() + { + using var connection = await InitializeDatabaseAsync(); + + using var cmd = connection.CreateCommand(); + cmd.CommandText = @" +SELECT column_name +FROM information_schema.columns +WHERE table_name = 'dismissed_archive_alerts' +ORDER BY ordinal_position"; + + var columns = new List(); + using var reader = await cmd.ExecuteReaderAsync(TestContext.Current.CancellationToken); + while (await reader.ReadAsync(TestContext.Current.CancellationToken)) + columns.Add(reader.GetString(0)); + + Assert.Equal(new[] { "alert_time", "server_id", "metric_name", "dismissed_at" }, columns); + } + + [Fact] + public async Task SidecarInsert_HidesArchivedAlertFromView() + { + using var connection = await InitializeDatabaseAsync(); + + var alertTime = DateTime.UtcNow.AddDays(-14); + var archivedAlerts = new List + { + TestAlertDataHelper.CreateAlert( + alertTime: alertTime, + serverId: 1, + metricName: "High CPU", + serverName: "Server1") + }; + await _helper.CreateArchivedAlertsParquetAsync(connection, archivedAlerts); + await _helper.RefreshArchiveViewsAsync(); + + // Verify alert is visible before sidecar insert + using var beforeCmd = connection.CreateCommand(); + beforeCmd.CommandText = "SELECT COUNT(1) FROM v_config_alert_log WHERE metric_name = 'High CPU'"; + var beforeCount = Convert.ToInt64(await beforeCmd.ExecuteScalarAsync(TestContext.Current.CancellationToken)); + Assert.Equal(1, beforeCount); + + // Insert into sidecar + using var insertCmd = connection.CreateCommand(); + insertCmd.CommandText = @" +INSERT INTO dismissed_archive_alerts (alert_time, server_id, metric_name) +VALUES ($1, $2, $3)"; + insertCmd.Parameters.Add(new DuckDBParameter { Value = alertTime }); + insertCmd.Parameters.Add(new DuckDBParameter { Value = 1 }); + insertCmd.Parameters.Add(new DuckDBParameter { Value = "High CPU" }); + await insertCmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken); + + // Verify alert is now hidden from view + using var afterCmd = connection.CreateCommand(); + afterCmd.CommandText = "SELECT COUNT(1) FROM v_config_alert_log WHERE metric_name = 'High CPU'"; + var afterCount = Convert.ToInt64(await afterCmd.ExecuteScalarAsync(TestContext.Current.CancellationToken)); + Assert.Equal(0, afterCount); + } + + [Fact] + public async Task SidecarInsert_DoesNotAffectLiveAlerts() + { + using var connection = await InitializeDatabaseAsync(); + + // Insert a live alert + var liveTime = DateTime.UtcNow.AddHours(-1); + await _helper.InsertLiveAlertAsync(connection, liveTime, 1, "Server1", "Blocking"); + + // Insert an archived alert + var archiveTime = DateTime.UtcNow.AddDays(-14); + var archivedAlerts = new List + { + TestAlertDataHelper.CreateAlert( + alertTime: archiveTime, + serverId: 1, + metricName: "High CPU", + serverName: "Server1") + }; + await _helper.CreateArchivedAlertsParquetAsync(connection, archivedAlerts); + await _helper.RefreshArchiveViewsAsync(); + + // Dismiss the archived alert via sidecar + using var insertCmd = connection.CreateCommand(); + insertCmd.CommandText = @" +INSERT INTO dismissed_archive_alerts (alert_time, server_id, metric_name) +VALUES ($1, $2, $3)"; + insertCmd.Parameters.Add(new DuckDBParameter { Value = archiveTime }); + insertCmd.Parameters.Add(new DuckDBParameter { Value = 1 }); + insertCmd.Parameters.Add(new DuckDBParameter { Value = "High CPU" }); + await insertCmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken); + + // Live alert should still be visible + using var checkCmd = connection.CreateCommand(); + checkCmd.CommandText = "SELECT COUNT(1) FROM v_config_alert_log WHERE metric_name = 'Blocking'"; + var liveCount = Convert.ToInt64(await checkCmd.ExecuteScalarAsync(TestContext.Current.CancellationToken)); + Assert.Equal(1, liveCount); + + // Archived alert should be hidden + using var archiveCmd = connection.CreateCommand(); + archiveCmd.CommandText = "SELECT COUNT(1) FROM v_config_alert_log WHERE metric_name = 'High CPU'"; + var archiveCount = Convert.ToInt64(await archiveCmd.ExecuteScalarAsync(TestContext.Current.CancellationToken)); + Assert.Equal(0, archiveCount); + } + + [Fact] + public async Task SidecarInsert_PreventsDuplicates() + { + using var connection = await InitializeDatabaseAsync(); + + var alertTime = DateTime.UtcNow.AddDays(-14); + + // Insert the same sidecar entry twice using the NOT EXISTS pattern + for (int i = 0; i < 2; i++) + { + using var cmd = connection.CreateCommand(); + cmd.CommandText = @" +INSERT INTO dismissed_archive_alerts (alert_time, server_id, metric_name) +SELECT $1, $2, $3 +WHERE NOT EXISTS ( + SELECT 1 FROM dismissed_archive_alerts + WHERE alert_time = $1 + AND server_id = $2 + AND metric_name = $3 +)"; + cmd.Parameters.Add(new DuckDBParameter { Value = alertTime }); + cmd.Parameters.Add(new DuckDBParameter { Value = 1 }); + cmd.Parameters.Add(new DuckDBParameter { Value = "High CPU" }); + await cmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken); + } + + // Verify only one row exists + using var countCmd = connection.CreateCommand(); + countCmd.CommandText = "SELECT COUNT(1) FROM dismissed_archive_alerts"; + var count = Convert.ToInt64(await countCmd.ExecuteScalarAsync(TestContext.Current.CancellationToken)); + Assert.Equal(1, count); + } + + [Fact] + public async Task DismissAll_HandlesLiveAndArchivedAlerts() + { + using var connection = await InitializeDatabaseAsync(); + + // Insert live alerts + var liveTime1 = DateTime.UtcNow.AddHours(-1); + var liveTime2 = DateTime.UtcNow.AddHours(-2); + await _helper.InsertLiveAlertAsync(connection, liveTime1, 1, "Server1", "High CPU"); + await _helper.InsertLiveAlertAsync(connection, liveTime2, 1, "Server1", "Blocking"); + + // Create archived alerts + var archiveTime = DateTime.UtcNow.AddDays(-3); + var archivedAlerts = new List + { + TestAlertDataHelper.CreateAlert( + alertTime: archiveTime, + serverId: 1, + metricName: "Deadlock Detected", + serverName: "Server1") + }; + await _helper.CreateArchivedAlertsParquetAsync(connection, archivedAlerts); + await _helper.RefreshArchiveViewsAsync(); + + // Verify 3 alerts visible + using var beforeCmd = connection.CreateCommand(); + beforeCmd.CommandText = "SELECT COUNT(1) FROM v_config_alert_log WHERE dismissed = FALSE"; + var beforeCount = Convert.ToInt64(await beforeCmd.ExecuteScalarAsync(TestContext.Current.CancellationToken)); + Assert.Equal(3, beforeCount); + + // Dismiss all live alerts + using var updateCmd = connection.CreateCommand(); + updateCmd.CommandText = @" +UPDATE config_alert_log +SET dismissed = TRUE +WHERE dismissed = FALSE"; + var liveAffected = await updateCmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken); + Assert.Equal(2, liveAffected); + + // Insert archived alert into sidecar (simulates what DismissAllVisibleAlertsAsync does) + using var sidecarCmd = connection.CreateCommand(); + sidecarCmd.CommandText = @" +INSERT INTO dismissed_archive_alerts (alert_time, server_id, metric_name) +SELECT v.alert_time, v.server_id, v.metric_name +FROM v_config_alert_log v +WHERE v.dismissed = FALSE +AND NOT EXISTS ( + SELECT 1 FROM config_alert_log l + WHERE l.alert_time = v.alert_time + AND l.server_id = v.server_id + AND l.metric_name = v.metric_name +) +AND NOT EXISTS ( + SELECT 1 FROM dismissed_archive_alerts d + WHERE d.alert_time = v.alert_time + AND d.server_id = v.server_id + AND d.metric_name = v.metric_name +)"; + var archivedAffected = await sidecarCmd.ExecuteNonQueryAsync(TestContext.Current.CancellationToken); + Assert.Equal(1, archivedAffected); + + // All alerts should now be hidden from view + using var afterCmd = connection.CreateCommand(); + afterCmd.CommandText = "SELECT COUNT(1) FROM v_config_alert_log WHERE dismissed = FALSE"; + var afterCount = Convert.ToInt64(await afterCmd.ExecuteScalarAsync(TestContext.Current.CancellationToken)); + Assert.Equal(0, afterCount); + } + + [Fact] + public async Task SchemaVersion_IsUpdatedTo23() + { + using var connection = await InitializeDatabaseAsync(); + + using var cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT MAX(version) FROM schema_version"; + var version = Convert.ToInt32(await cmd.ExecuteScalarAsync(TestContext.Current.CancellationToken)); + + Assert.Equal(23, version); + } +} diff --git a/Lite.Tests/DuckDbSchemaTests.cs b/Lite.Tests/DuckDbSchemaTests.cs index a1b68f5..c0526f6 100644 --- a/Lite.Tests/DuckDbSchemaTests.cs +++ b/Lite.Tests/DuckDbSchemaTests.cs @@ -138,8 +138,8 @@ public void SchemaStatements_MatchTableCount() foreach (var _ in Schema.GetAllTableStatements()) tableCount++; - /* 28 tables from Schema (schema_version is created separately by DuckDbInitializer) */ - Assert.Equal(28, tableCount); + /* 29 tables from Schema (schema_version is created separately by DuckDbInitializer) */ + Assert.Equal(29, tableCount); } [Fact] diff --git a/Lite/Database/DuckDbInitializer.cs b/Lite/Database/DuckDbInitializer.cs index 5bbca8d..e7291e7 100644 --- a/Lite/Database/DuckDbInitializer.cs +++ b/Lite/Database/DuckDbInitializer.cs @@ -86,7 +86,7 @@ public void Dispose() /// /// Current schema version. Increment this when schema changes require table rebuilds. /// - internal const int CurrentSchemaVersion = 22; + internal const int CurrentSchemaVersion = 23; private readonly string _archivePath; @@ -599,6 +599,21 @@ New tables only — no existing table changes needed. Tables created by throw; } } + + if (fromVersion < 23) + { + _logger?.LogInformation("Running migration to v23: adding dismissed_archive_alerts sidecar table"); + try + { + await ExecuteNonQueryAsync(connection, Schema.CreateDismissedArchiveAlertsTable); + await ExecuteNonQueryAsync(connection, Schema.CreateDismissedArchiveAlertsIndex); + } + catch (Exception ex) + { + _logger?.LogError(ex, "Migration to v23 failed"); + throw; + } + } } /// @@ -686,9 +701,22 @@ public async Task CreateArchiveViewsAsync() { var globPath = parquetGlob.Replace("\\", "/"); if (table == "config_alert_log") - viewSql = $"CREATE OR REPLACE VIEW v_{table} AS SELECT *, 'live' AS source FROM {table} UNION ALL BY NAME SELECT *, 'archive' AS source FROM read_parquet('{globPath}', union_by_name=true)"; + { + viewSql = $@"CREATE OR REPLACE VIEW v_{table} AS +SELECT *, 'live' AS source FROM {table} +UNION ALL BY NAME +SELECT *, 'archive' AS source FROM read_parquet('{globPath}', union_by_name=true) p +WHERE NOT EXISTS ( + SELECT 1 FROM dismissed_archive_alerts d + WHERE d.alert_time = p.alert_time + AND d.server_id = p.server_id + AND d.metric_name = p.metric_name +)"; + } else + { viewSql = $"CREATE OR REPLACE VIEW v_{table} AS SELECT * FROM {table} UNION ALL BY NAME SELECT * FROM read_parquet('{globPath}', union_by_name=true)"; + } } else { diff --git a/Lite/Database/Schema.cs b/Lite/Database/Schema.cs index 41aae34..09ccc1a 100644 --- a/Lite/Database/Schema.cs +++ b/Lite/Database/Schema.cs @@ -1,765 +1,779 @@ -using System.Collections.Generic; - -namespace PerformanceMonitorLite.Database; - -/// -/// Contains all DuckDB table schema definitions as SQL constants. -/// -public static class Schema -{ - public const string CreateServersTable = @" -CREATE TABLE IF NOT EXISTS servers ( - server_id INTEGER PRIMARY KEY, - server_name VARCHAR NOT NULL, - display_name VARCHAR, - use_windows_auth BOOLEAN NOT NULL DEFAULT TRUE, - username VARCHAR, - is_enabled BOOLEAN NOT NULL DEFAULT TRUE, - created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - modified_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP -)"; - - public const string CreateCollectionScheduleTable = @" -CREATE TABLE IF NOT EXISTS collection_schedule ( - schedule_id INTEGER PRIMARY KEY, - collector_name VARCHAR NOT NULL UNIQUE, - enabled BOOLEAN NOT NULL DEFAULT TRUE, - frequency_minutes INTEGER NOT NULL DEFAULT 15, - last_run_time TIMESTAMP, - next_run_time TIMESTAMP, - max_duration_minutes INTEGER DEFAULT 5, - retention_days INTEGER DEFAULT 30, - description VARCHAR, - created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - modified_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP -)"; - - public const string CreateCollectionLogTable = @" -CREATE TABLE IF NOT EXISTS collection_log ( - log_id BIGINT PRIMARY KEY, - server_id INTEGER NOT NULL, - server_name VARCHAR, - collector_name VARCHAR NOT NULL, - collection_time TIMESTAMP NOT NULL, - duration_ms INTEGER, - status VARCHAR NOT NULL, - error_message VARCHAR, - rows_collected INTEGER, - sql_duration_ms INTEGER, - duckdb_duration_ms INTEGER -)"; - - public const string CreateWaitStatsTable = @" -CREATE TABLE IF NOT EXISTS wait_stats ( - collection_id BIGINT PRIMARY KEY, - collection_time TIMESTAMP NOT NULL, - server_id INTEGER NOT NULL, - server_name VARCHAR NOT NULL, - wait_type VARCHAR NOT NULL, - waiting_tasks_count BIGINT, - wait_time_ms BIGINT, - signal_wait_time_ms BIGINT, - delta_waiting_tasks BIGINT, - delta_wait_time_ms BIGINT, - delta_signal_wait_time_ms BIGINT -)"; - - public const string CreateQueryStatsTable = @" -CREATE TABLE IF NOT EXISTS query_stats ( - collection_id BIGINT PRIMARY KEY, - collection_time TIMESTAMP NOT NULL, - server_id INTEGER NOT NULL, - server_name VARCHAR NOT NULL, - database_name VARCHAR, - query_hash VARCHAR, - query_plan_hash VARCHAR, - creation_time TIMESTAMP, - last_execution_time TIMESTAMP, - execution_count BIGINT, - total_worker_time BIGINT, - total_elapsed_time BIGINT, - total_logical_reads BIGINT, - total_logical_writes BIGINT, - total_physical_reads BIGINT, - total_clr_time BIGINT, - total_rows BIGINT, - total_spills BIGINT, - min_worker_time BIGINT, - max_worker_time BIGINT, - min_elapsed_time BIGINT, - max_elapsed_time BIGINT, - min_physical_reads BIGINT, - max_physical_reads BIGINT, - min_rows BIGINT, - max_rows BIGINT, - min_dop BIGINT, - max_dop BIGINT, - min_grant_kb BIGINT, - max_grant_kb BIGINT, - min_used_grant_kb BIGINT, - max_used_grant_kb BIGINT, - min_ideal_grant_kb BIGINT, - max_ideal_grant_kb BIGINT, - min_reserved_threads BIGINT, - max_reserved_threads BIGINT, - min_used_threads BIGINT, - max_used_threads BIGINT, - min_spills BIGINT, - max_spills BIGINT, - query_text VARCHAR, - query_plan_xml VARCHAR, - sql_handle VARCHAR, - plan_handle VARCHAR, - delta_execution_count BIGINT, - delta_worker_time BIGINT, - delta_elapsed_time BIGINT, - delta_logical_reads BIGINT, - delta_logical_writes BIGINT, - delta_physical_reads BIGINT, - delta_rows BIGINT, - delta_spills BIGINT -)"; - - public const string CreateCpuUtilizationStatsTable = @" -CREATE TABLE IF NOT EXISTS cpu_utilization_stats ( - collection_id BIGINT PRIMARY KEY, - collection_time TIMESTAMP NOT NULL, - server_id INTEGER NOT NULL, - server_name VARCHAR NOT NULL, - sample_time TIMESTAMP NOT NULL, - sqlserver_cpu_utilization INTEGER, - other_process_cpu_utilization INTEGER -)"; - - public const string CreateFileIoStatsTable = @" -CREATE TABLE IF NOT EXISTS file_io_stats ( - collection_id BIGINT PRIMARY KEY, - collection_time TIMESTAMP NOT NULL, - server_id INTEGER NOT NULL, - server_name VARCHAR NOT NULL, - database_name VARCHAR, - file_name VARCHAR, - file_type VARCHAR, - physical_name VARCHAR, - size_mb DECIMAL(18,2), - num_of_reads BIGINT, - num_of_writes BIGINT, - read_bytes BIGINT, - write_bytes BIGINT, - io_stall_read_ms BIGINT, - io_stall_write_ms BIGINT, - io_stall_queued_read_ms BIGINT, - io_stall_queued_write_ms BIGINT, - delta_reads BIGINT, - delta_writes BIGINT, - delta_read_bytes BIGINT, - delta_write_bytes BIGINT, - delta_stall_read_ms BIGINT, - delta_stall_write_ms BIGINT, - delta_stall_queued_read_ms BIGINT, - delta_stall_queued_write_ms BIGINT -)"; - - public const string CreateMemoryStatsTable = @" -CREATE TABLE IF NOT EXISTS memory_stats ( - collection_id BIGINT PRIMARY KEY, - collection_time TIMESTAMP NOT NULL, - server_id INTEGER NOT NULL, - server_name VARCHAR NOT NULL, - total_physical_memory_mb DECIMAL(18,2), - available_physical_memory_mb DECIMAL(18,2), - total_page_file_mb DECIMAL(18,2), - available_page_file_mb DECIMAL(18,2), - system_memory_state VARCHAR, - sql_memory_model VARCHAR, - target_server_memory_mb DECIMAL(18,2), - total_server_memory_mb DECIMAL(18,2), - buffer_pool_mb DECIMAL(18,2), - plan_cache_mb DECIMAL(18,2), - max_workers_count INTEGER, - current_workers_count INTEGER -)"; - - public const string CreateMemoryClerksTable = @" -CREATE TABLE IF NOT EXISTS memory_clerks ( - collection_id BIGINT PRIMARY KEY, - collection_time TIMESTAMP NOT NULL, - server_id INTEGER NOT NULL, - server_name VARCHAR NOT NULL, - clerk_type VARCHAR NOT NULL, - memory_mb DECIMAL(18,2) -)"; - - public const string CreateDeadlocksTable = @" -CREATE TABLE IF NOT EXISTS deadlocks ( - deadlock_id BIGINT PRIMARY KEY, - collection_time TIMESTAMP NOT NULL, - server_id INTEGER NOT NULL, - server_name VARCHAR NOT NULL, - deadlock_time TIMESTAMP, - victim_process_id VARCHAR, - victim_sql_text VARCHAR, - deadlock_graph_xml VARCHAR -)"; - - public const string CreateProcedureStatsTable = @" -CREATE TABLE IF NOT EXISTS procedure_stats ( - collection_id BIGINT PRIMARY KEY, - collection_time TIMESTAMP NOT NULL, - server_id INTEGER NOT NULL, - server_name VARCHAR NOT NULL, - database_name VARCHAR, - schema_name VARCHAR, - object_name VARCHAR, - object_type VARCHAR, - cached_time TIMESTAMP, - last_execution_time TIMESTAMP, - execution_count BIGINT, - total_worker_time BIGINT, - total_elapsed_time BIGINT, - total_logical_reads BIGINT, - total_physical_reads BIGINT, - total_logical_writes BIGINT, - min_worker_time BIGINT, - max_worker_time BIGINT, - min_elapsed_time BIGINT, - max_elapsed_time BIGINT, - min_logical_reads BIGINT, - max_logical_reads BIGINT, - min_physical_reads BIGINT, - max_physical_reads BIGINT, - min_logical_writes BIGINT, - max_logical_writes BIGINT, - total_spills BIGINT, - min_spills BIGINT, - max_spills BIGINT, - sql_handle VARCHAR, - plan_handle VARCHAR, - delta_execution_count BIGINT, - delta_worker_time BIGINT, - delta_elapsed_time BIGINT, - delta_logical_reads BIGINT, - delta_logical_writes BIGINT, - delta_physical_reads BIGINT -)"; - - public const string CreateQueryStoreStatsTable = @" -CREATE TABLE IF NOT EXISTS query_store_stats ( - collection_id BIGINT PRIMARY KEY, - collection_time TIMESTAMP NOT NULL, - server_id INTEGER NOT NULL, - server_name VARCHAR NOT NULL, - database_name VARCHAR NOT NULL, - query_id BIGINT, - plan_id BIGINT, - execution_type_desc VARCHAR, - first_execution_time TIMESTAMP, - last_execution_time TIMESTAMP, - module_name VARCHAR, - query_text VARCHAR, - query_hash VARCHAR, - execution_count BIGINT, - avg_duration_us BIGINT, - min_duration_us BIGINT, - max_duration_us BIGINT, - avg_cpu_time_us BIGINT, - min_cpu_time_us BIGINT, - max_cpu_time_us BIGINT, - avg_logical_io_reads BIGINT, - min_logical_io_reads BIGINT, - max_logical_io_reads BIGINT, - avg_logical_io_writes BIGINT, - min_logical_io_writes BIGINT, - max_logical_io_writes BIGINT, - avg_physical_io_reads BIGINT, - min_physical_io_reads BIGINT, - max_physical_io_reads BIGINT, - avg_clr_time_us BIGINT, - min_clr_time_us BIGINT, - max_clr_time_us BIGINT, - min_dop BIGINT, - max_dop BIGINT, - avg_query_max_used_memory BIGINT, - min_query_max_used_memory BIGINT, - max_query_max_used_memory BIGINT, - avg_rowcount BIGINT, - min_rowcount BIGINT, - max_rowcount BIGINT, - avg_num_physical_io_reads BIGINT, - min_num_physical_io_reads BIGINT, - max_num_physical_io_reads BIGINT, - avg_log_bytes_used BIGINT, - min_log_bytes_used BIGINT, - max_log_bytes_used BIGINT, - avg_tempdb_space_used BIGINT, - min_tempdb_space_used BIGINT, - max_tempdb_space_used BIGINT, - plan_type VARCHAR, - plan_forcing_type VARCHAR, - is_forced_plan BOOLEAN, - force_failure_count BIGINT, - last_force_failure_reason VARCHAR, - compatibility_level INTEGER, - query_plan_text VARCHAR, - query_plan_hash VARCHAR -)"; - - public const string CreateQuerySnapshotsTable = @" -CREATE TABLE IF NOT EXISTS query_snapshots ( - collection_id BIGINT PRIMARY KEY, - collection_time TIMESTAMP NOT NULL, - server_id INTEGER NOT NULL, - server_name VARCHAR NOT NULL, - session_id INTEGER, - database_name VARCHAR, - elapsed_time_formatted VARCHAR, - query_text VARCHAR, - query_plan VARCHAR, - live_query_plan VARCHAR, - status VARCHAR, - blocking_session_id INTEGER, - wait_type VARCHAR, - wait_time_ms BIGINT, - wait_resource VARCHAR, - cpu_time_ms BIGINT, - total_elapsed_time_ms BIGINT, - reads BIGINT, - writes BIGINT, - logical_reads BIGINT, - granted_query_memory_gb DECIMAL(18,2), - transaction_isolation_level VARCHAR, - dop INTEGER, - parallel_worker_count INTEGER, - login_name VARCHAR, - host_name VARCHAR, - program_name VARCHAR, - open_transaction_count INTEGER, - percent_complete DECIMAL(5,2) -)"; - - public const string CreateTempdbStatsTable = @" -CREATE TABLE IF NOT EXISTS tempdb_stats ( - collection_id BIGINT PRIMARY KEY, - collection_time TIMESTAMP NOT NULL, - server_id INTEGER NOT NULL, - server_name VARCHAR NOT NULL, - user_object_reserved_mb DECIMAL(18,2), - internal_object_reserved_mb DECIMAL(18,2), - version_store_reserved_mb DECIMAL(18,2), - total_reserved_mb DECIMAL(18,2), - unallocated_mb DECIMAL(18,2), - total_sessions_using_tempdb BIGINT, - top_session_id INTEGER, - top_session_tempdb_mb DECIMAL(18,2) -)"; - - public const string CreatePerfmonStatsTable = @" -CREATE TABLE IF NOT EXISTS perfmon_stats ( - collection_id BIGINT PRIMARY KEY, - collection_time TIMESTAMP NOT NULL, - server_id INTEGER NOT NULL, - server_name VARCHAR NOT NULL, - object_name VARCHAR NOT NULL, - counter_name VARCHAR NOT NULL, - instance_name VARCHAR, - cntr_value BIGINT, - delta_cntr_value BIGINT, - sample_interval_seconds INTEGER -)"; - - public const string CreateServerConfigTable = @" -CREATE TABLE IF NOT EXISTS server_config ( - config_id BIGINT PRIMARY KEY, - capture_time TIMESTAMP NOT NULL, - server_id INTEGER NOT NULL, - server_name VARCHAR NOT NULL, - configuration_name VARCHAR NOT NULL, - value_configured BIGINT, - value_in_use BIGINT, - is_dynamic BOOLEAN, - is_advanced BOOLEAN -)"; - - public const string CreateMemoryGrantStatsTable = @" -CREATE TABLE IF NOT EXISTS memory_grant_stats ( - collection_id BIGINT PRIMARY KEY, - collection_time TIMESTAMP NOT NULL, - server_id INTEGER NOT NULL, - server_name VARCHAR NOT NULL, - resource_semaphore_id SMALLINT, - pool_id INTEGER, - target_memory_mb DECIMAL(18,2), - max_target_memory_mb DECIMAL(18,2), - total_memory_mb DECIMAL(18,2), - available_memory_mb DECIMAL(18,2), - granted_memory_mb DECIMAL(18,2), - used_memory_mb DECIMAL(18,2), - grantee_count INTEGER, - waiter_count INTEGER, - timeout_error_count BIGINT, - forced_grant_count BIGINT, - timeout_error_count_delta BIGINT, - forced_grant_count_delta BIGINT -)"; - - public const string CreateWaitingTasksTable = @" -CREATE TABLE IF NOT EXISTS waiting_tasks ( - collection_id BIGINT PRIMARY KEY, - collection_time TIMESTAMP NOT NULL, - server_id INTEGER NOT NULL, - server_name VARCHAR NOT NULL, - session_id INTEGER, - wait_type VARCHAR, - wait_duration_ms BIGINT, - blocking_session_id INTEGER, - resource_description VARCHAR, - database_name VARCHAR -)"; - - public const string CreateBlockedProcessReportsTable = @" -CREATE TABLE IF NOT EXISTS blocked_process_reports ( - blocked_report_id BIGINT PRIMARY KEY, - collection_time TIMESTAMP NOT NULL, - server_id INTEGER NOT NULL, - server_name VARCHAR NOT NULL, - event_time TIMESTAMP, - database_name VARCHAR, - blocked_spid INTEGER, - blocked_ecid INTEGER, - blocking_spid INTEGER, - blocking_ecid INTEGER, - wait_time_ms BIGINT, - wait_resource VARCHAR, - lock_mode VARCHAR, - blocked_status VARCHAR, - blocked_isolation_level VARCHAR, - blocked_log_used BIGINT, - blocked_transaction_count INTEGER, - blocked_client_app VARCHAR, - blocked_host_name VARCHAR, - blocked_login_name VARCHAR, - blocked_sql_text VARCHAR, - blocking_status VARCHAR, - blocking_isolation_level VARCHAR, - blocking_client_app VARCHAR, - blocking_host_name VARCHAR, - blocking_login_name VARCHAR, - blocking_sql_text VARCHAR, - blocked_transaction_name VARCHAR, - blocking_transaction_name VARCHAR, - blocked_last_tran_started TIMESTAMP, - blocking_last_tran_started TIMESTAMP, - blocked_last_batch_started TIMESTAMP, - blocking_last_batch_started TIMESTAMP, - blocked_last_batch_completed TIMESTAMP, - blocking_last_batch_completed TIMESTAMP, - blocked_priority INTEGER, - blocking_priority INTEGER, - blocked_process_report_xml VARCHAR -)"; - - public const string CreateDatabaseConfigTable = @" -CREATE TABLE IF NOT EXISTS database_config ( - config_id BIGINT PRIMARY KEY, - capture_time TIMESTAMP NOT NULL, - server_id INTEGER NOT NULL, - server_name VARCHAR NOT NULL, - database_name VARCHAR NOT NULL, - state_desc VARCHAR, - compatibility_level INTEGER, - collation_name VARCHAR, - recovery_model VARCHAR, - is_read_only BOOLEAN, - is_auto_close_on BOOLEAN, - is_auto_shrink_on BOOLEAN, - is_auto_create_stats_on BOOLEAN, - is_auto_update_stats_on BOOLEAN, - is_auto_update_stats_async_on BOOLEAN, - is_read_committed_snapshot_on BOOLEAN, - snapshot_isolation_state VARCHAR, - is_parameterization_forced BOOLEAN, - is_query_store_on BOOLEAN, - is_encrypted BOOLEAN, - is_trustworthy_on BOOLEAN, - is_db_chaining_on BOOLEAN, - is_broker_enabled BOOLEAN, - is_cdc_enabled BOOLEAN, - is_mixed_page_allocation_on BOOLEAN, - log_reuse_wait_desc VARCHAR, - page_verify_option VARCHAR, - target_recovery_time_seconds INTEGER, - delayed_durability VARCHAR, - is_accelerated_database_recovery_on BOOLEAN, - is_memory_optimized_enabled BOOLEAN, - is_optimized_locking_on BOOLEAN -)"; - - // Index definitions - public const string CreateWaitStatsIndex = @" -CREATE INDEX IF NOT EXISTS idx_wait_stats_time ON wait_stats(server_id, collection_time)"; - - public const string CreateQueryStatsIndex = @" -CREATE INDEX IF NOT EXISTS idx_query_stats_time ON query_stats(server_id, collection_time)"; - - public const string CreateProcedureStatsIndex = @" -CREATE INDEX IF NOT EXISTS idx_procedure_stats_time ON procedure_stats(server_id, collection_time)"; - - public const string CreateQueryStoreIndex = @" -CREATE INDEX IF NOT EXISTS idx_query_store_time ON query_store_stats(server_id, collection_time)"; - - public const string CreateQuerySnapshotsIndex = @" -CREATE INDEX IF NOT EXISTS idx_query_snapshots_time ON query_snapshots(server_id, collection_time)"; - - public const string CreateCpuIndex = @" -CREATE INDEX IF NOT EXISTS idx_cpu_time ON cpu_utilization_stats(server_id, collection_time)"; - - public const string CreateFileIoIndex = @" -CREATE INDEX IF NOT EXISTS idx_file_io_time ON file_io_stats(server_id, collection_time)"; - - public const string CreateMemoryIndex = @" -CREATE INDEX IF NOT EXISTS idx_memory_time ON memory_stats(server_id, collection_time)"; - - public const string CreateTempdbIndex = @" -CREATE INDEX IF NOT EXISTS idx_tempdb_time ON tempdb_stats(server_id, collection_time)"; - - public const string CreatePerfmonIndex = @" -CREATE INDEX IF NOT EXISTS idx_perfmon_time ON perfmon_stats(server_id, collection_time)"; - - public const string CreateDeadlocksIndex = @" -CREATE INDEX IF NOT EXISTS idx_deadlocks_time ON deadlocks(server_id, collection_time)"; - - public const string CreateCollectionLogIndex = @" -CREATE INDEX IF NOT EXISTS idx_collection_log_time ON collection_log(server_id, collection_time)"; - - public const string CreateMemoryGrantStatsIndex = @" -CREATE INDEX IF NOT EXISTS idx_memory_grant_stats_time ON memory_grant_stats(server_id, collection_time)"; - - public const string CreateWaitingTasksIndex = @" -CREATE INDEX IF NOT EXISTS idx_waiting_tasks_time ON waiting_tasks(server_id, collection_time)"; - - public const string CreateBlockedProcessReportsIndex = @" -CREATE INDEX IF NOT EXISTS idx_blocked_process_reports_time ON blocked_process_reports(server_id, collection_time)"; - - public const string CreateMemoryClerksIndex = @" -CREATE INDEX IF NOT EXISTS idx_memory_clerks_time ON memory_clerks(server_id, collection_time)"; - - public const string CreateDatabaseScopedConfigTable = @" -CREATE TABLE IF NOT EXISTS database_scoped_config ( - config_id BIGINT PRIMARY KEY, - capture_time TIMESTAMP NOT NULL, - server_id INTEGER NOT NULL, - server_name VARCHAR NOT NULL, - database_name VARCHAR NOT NULL, - configuration_name VARCHAR NOT NULL, - value VARCHAR, - value_for_secondary VARCHAR -)"; - - public const string CreateDatabaseScopedConfigIndex = @" -CREATE INDEX IF NOT EXISTS idx_database_scoped_config_time ON database_scoped_config(server_id, capture_time)"; - - public const string CreateTraceFlagsTable = @" -CREATE TABLE IF NOT EXISTS trace_flags ( - config_id BIGINT PRIMARY KEY, - capture_time TIMESTAMP NOT NULL, - server_id INTEGER NOT NULL, - server_name VARCHAR NOT NULL, - trace_flag INTEGER NOT NULL, - status BOOLEAN NOT NULL, - is_global BOOLEAN NOT NULL, - is_session BOOLEAN NOT NULL -)"; - - public const string CreateTraceFlagsIndex = @" -CREATE INDEX IF NOT EXISTS idx_trace_flags_time ON trace_flags(server_id, capture_time)"; - - public const string CreateRunningJobsTable = @" -CREATE TABLE IF NOT EXISTS running_jobs ( - collection_time TIMESTAMP NOT NULL, - server_id INTEGER NOT NULL, - server_name VARCHAR NOT NULL, - job_name VARCHAR NOT NULL, - job_id VARCHAR NOT NULL, - job_enabled BOOLEAN NOT NULL, - start_time TIMESTAMP NOT NULL, - current_duration_seconds BIGINT NOT NULL, - avg_duration_seconds BIGINT NOT NULL, - p95_duration_seconds BIGINT NOT NULL, - successful_run_count BIGINT NOT NULL, - is_running_long BOOLEAN NOT NULL, - percent_of_average DECIMAL(10,1) -)"; - - public const string CreateRunningJobsIndex = @" -CREATE INDEX IF NOT EXISTS idx_running_jobs_time ON running_jobs(server_id, collection_time)"; - - public const string CreateDatabaseSizeStatsTable = @" -CREATE TABLE IF NOT EXISTS database_size_stats ( - collection_id BIGINT PRIMARY KEY, - collection_time TIMESTAMP NOT NULL, - server_id INTEGER NOT NULL, - server_name VARCHAR NOT NULL, - database_name VARCHAR NOT NULL, - database_id INTEGER NOT NULL, - file_id INTEGER NOT NULL, - file_type_desc VARCHAR NOT NULL, - file_name VARCHAR NOT NULL, - physical_name VARCHAR NOT NULL, - total_size_mb DECIMAL(19,2) NOT NULL, - used_size_mb DECIMAL(19,2), - auto_growth_mb DECIMAL(19,2), - max_size_mb DECIMAL(19,2), - recovery_model_desc VARCHAR, - compatibility_level INTEGER, - state_desc VARCHAR, - volume_mount_point VARCHAR, - volume_total_mb DECIMAL(19,2), - volume_free_mb DECIMAL(19,2), - is_percent_growth BOOLEAN, - growth_pct INTEGER, - vlf_count INTEGER -)"; - - public const string CreateDatabaseSizeStatsIndex = @" -CREATE INDEX IF NOT EXISTS idx_database_size_stats_time ON database_size_stats(server_id, collection_time)"; - - public const string CreateServerPropertiesTable = @" -CREATE TABLE IF NOT EXISTS server_properties ( - collection_id BIGINT PRIMARY KEY, - collection_time TIMESTAMP NOT NULL, - server_id INTEGER NOT NULL, - server_name VARCHAR NOT NULL, - edition VARCHAR NOT NULL, - product_version VARCHAR NOT NULL, - product_level VARCHAR NOT NULL, - product_update_level VARCHAR, - engine_edition INTEGER NOT NULL, - cpu_count INTEGER NOT NULL, - hyperthread_ratio INTEGER NOT NULL, - physical_memory_mb BIGINT NOT NULL, - socket_count INTEGER, - cores_per_socket INTEGER, - is_hadr_enabled BOOLEAN, - is_clustered BOOLEAN, - enterprise_features VARCHAR, - service_objective VARCHAR -)"; - - public const string CreateServerPropertiesIndex = @" -CREATE INDEX IF NOT EXISTS idx_server_properties_time ON server_properties(server_id, collection_time)"; - - public const string CreateSessionStatsTable = @" -CREATE TABLE IF NOT EXISTS session_stats ( - collection_id BIGINT PRIMARY KEY, - collection_time TIMESTAMP NOT NULL, - server_id INTEGER NOT NULL, - server_name VARCHAR NOT NULL, - program_name VARCHAR NOT NULL, - connection_count BIGINT NOT NULL, - running_count INTEGER NOT NULL, - sleeping_count INTEGER NOT NULL, - dormant_count INTEGER NOT NULL, - total_cpu_time_ms BIGINT, - total_reads BIGINT, - total_writes BIGINT, - total_logical_reads BIGINT -)"; - - public const string CreateSessionStatsIndex = @" -CREATE INDEX IF NOT EXISTS idx_session_stats_time ON session_stats(server_id, collection_time)"; - - public const string CreateAlertLogTable = @" -CREATE TABLE IF NOT EXISTS config_alert_log ( - alert_time TIMESTAMP NOT NULL, - server_id INTEGER NOT NULL, - server_name VARCHAR NOT NULL, - metric_name VARCHAR NOT NULL, - current_value DOUBLE NOT NULL, - threshold_value DOUBLE NOT NULL, - alert_sent BOOLEAN NOT NULL DEFAULT false, - notification_type VARCHAR NOT NULL DEFAULT 'tray', - send_error VARCHAR, - dismissed BOOLEAN NOT NULL DEFAULT false, - muted BOOLEAN NOT NULL DEFAULT false, - detail_text VARCHAR -)"; - - public const string CreateMuteRulesTable = @" -CREATE TABLE IF NOT EXISTS config_mute_rules ( - id VARCHAR NOT NULL PRIMARY KEY, - enabled BOOLEAN NOT NULL DEFAULT true, - created_at_utc TIMESTAMP NOT NULL, - expires_at_utc TIMESTAMP, - reason VARCHAR, - server_name VARCHAR, - metric_name VARCHAR, - database_pattern VARCHAR, - query_text_pattern VARCHAR, - wait_type_pattern VARCHAR, - job_name_pattern VARCHAR -)"; - - /// - /// Returns all table creation statements in order. - /// - public static IEnumerable GetAllTableStatements() - { - yield return CreateServersTable; - yield return CreateCollectionScheduleTable; - yield return CreateCollectionLogTable; - yield return CreateWaitStatsTable; - yield return CreateQueryStatsTable; - yield return CreateCpuUtilizationStatsTable; - yield return CreateFileIoStatsTable; - yield return CreateMemoryStatsTable; - yield return CreateMemoryClerksTable; - yield return CreateDeadlocksTable; - yield return CreateProcedureStatsTable; - yield return CreateQueryStoreStatsTable; - yield return CreateQuerySnapshotsTable; - yield return CreateTempdbStatsTable; - yield return CreatePerfmonStatsTable; - yield return CreateServerConfigTable; - yield return CreateDatabaseConfigTable; - yield return CreateMemoryGrantStatsTable; - yield return CreateWaitingTasksTable; - yield return CreateBlockedProcessReportsTable; - yield return CreateDatabaseScopedConfigTable; - yield return CreateTraceFlagsTable; - yield return CreateRunningJobsTable; - yield return CreateDatabaseSizeStatsTable; - yield return CreateServerPropertiesTable; - yield return CreateSessionStatsTable; - yield return CreateAlertLogTable; - yield return CreateMuteRulesTable; - } - - /// - /// Returns all index creation statements. - /// - public static IEnumerable GetAllIndexStatements() - { - yield return CreateWaitStatsIndex; - yield return CreateQueryStatsIndex; - yield return CreateProcedureStatsIndex; - yield return CreateQueryStoreIndex; - yield return CreateQuerySnapshotsIndex; - yield return CreateCpuIndex; - yield return CreateFileIoIndex; - yield return CreateMemoryIndex; - yield return CreateTempdbIndex; - yield return CreatePerfmonIndex; - yield return CreateDeadlocksIndex; - yield return CreateCollectionLogIndex; - yield return CreateMemoryGrantStatsIndex; - yield return CreateWaitingTasksIndex; - yield return CreateBlockedProcessReportsIndex; - yield return CreateMemoryClerksIndex; - yield return CreateDatabaseScopedConfigIndex; - yield return CreateTraceFlagsIndex; - yield return CreateRunningJobsIndex; - yield return CreateDatabaseSizeStatsIndex; - yield return CreateServerPropertiesIndex; - yield return CreateSessionStatsIndex; - } -} +using System.Collections.Generic; + +namespace PerformanceMonitorLite.Database; + +/// +/// Contains all DuckDB table schema definitions as SQL constants. +/// +public static class Schema +{ + public const string CreateServersTable = @" +CREATE TABLE IF NOT EXISTS servers ( + server_id INTEGER PRIMARY KEY, + server_name VARCHAR NOT NULL, + display_name VARCHAR, + use_windows_auth BOOLEAN NOT NULL DEFAULT TRUE, + username VARCHAR, + is_enabled BOOLEAN NOT NULL DEFAULT TRUE, + created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + modified_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP +)"; + + public const string CreateCollectionScheduleTable = @" +CREATE TABLE IF NOT EXISTS collection_schedule ( + schedule_id INTEGER PRIMARY KEY, + collector_name VARCHAR NOT NULL UNIQUE, + enabled BOOLEAN NOT NULL DEFAULT TRUE, + frequency_minutes INTEGER NOT NULL DEFAULT 15, + last_run_time TIMESTAMP, + next_run_time TIMESTAMP, + max_duration_minutes INTEGER DEFAULT 5, + retention_days INTEGER DEFAULT 30, + description VARCHAR, + created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + modified_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP +)"; + + public const string CreateCollectionLogTable = @" +CREATE TABLE IF NOT EXISTS collection_log ( + log_id BIGINT PRIMARY KEY, + server_id INTEGER NOT NULL, + server_name VARCHAR, + collector_name VARCHAR NOT NULL, + collection_time TIMESTAMP NOT NULL, + duration_ms INTEGER, + status VARCHAR NOT NULL, + error_message VARCHAR, + rows_collected INTEGER, + sql_duration_ms INTEGER, + duckdb_duration_ms INTEGER +)"; + + public const string CreateWaitStatsTable = @" +CREATE TABLE IF NOT EXISTS wait_stats ( + collection_id BIGINT PRIMARY KEY, + collection_time TIMESTAMP NOT NULL, + server_id INTEGER NOT NULL, + server_name VARCHAR NOT NULL, + wait_type VARCHAR NOT NULL, + waiting_tasks_count BIGINT, + wait_time_ms BIGINT, + signal_wait_time_ms BIGINT, + delta_waiting_tasks BIGINT, + delta_wait_time_ms BIGINT, + delta_signal_wait_time_ms BIGINT +)"; + + public const string CreateQueryStatsTable = @" +CREATE TABLE IF NOT EXISTS query_stats ( + collection_id BIGINT PRIMARY KEY, + collection_time TIMESTAMP NOT NULL, + server_id INTEGER NOT NULL, + server_name VARCHAR NOT NULL, + database_name VARCHAR, + query_hash VARCHAR, + query_plan_hash VARCHAR, + creation_time TIMESTAMP, + last_execution_time TIMESTAMP, + execution_count BIGINT, + total_worker_time BIGINT, + total_elapsed_time BIGINT, + total_logical_reads BIGINT, + total_logical_writes BIGINT, + total_physical_reads BIGINT, + total_clr_time BIGINT, + total_rows BIGINT, + total_spills BIGINT, + min_worker_time BIGINT, + max_worker_time BIGINT, + min_elapsed_time BIGINT, + max_elapsed_time BIGINT, + min_physical_reads BIGINT, + max_physical_reads BIGINT, + min_rows BIGINT, + max_rows BIGINT, + min_dop BIGINT, + max_dop BIGINT, + min_grant_kb BIGINT, + max_grant_kb BIGINT, + min_used_grant_kb BIGINT, + max_used_grant_kb BIGINT, + min_ideal_grant_kb BIGINT, + max_ideal_grant_kb BIGINT, + min_reserved_threads BIGINT, + max_reserved_threads BIGINT, + min_used_threads BIGINT, + max_used_threads BIGINT, + min_spills BIGINT, + max_spills BIGINT, + query_text VARCHAR, + query_plan_xml VARCHAR, + sql_handle VARCHAR, + plan_handle VARCHAR, + delta_execution_count BIGINT, + delta_worker_time BIGINT, + delta_elapsed_time BIGINT, + delta_logical_reads BIGINT, + delta_logical_writes BIGINT, + delta_physical_reads BIGINT, + delta_rows BIGINT, + delta_spills BIGINT +)"; + + public const string CreateCpuUtilizationStatsTable = @" +CREATE TABLE IF NOT EXISTS cpu_utilization_stats ( + collection_id BIGINT PRIMARY KEY, + collection_time TIMESTAMP NOT NULL, + server_id INTEGER NOT NULL, + server_name VARCHAR NOT NULL, + sample_time TIMESTAMP NOT NULL, + sqlserver_cpu_utilization INTEGER, + other_process_cpu_utilization INTEGER +)"; + + public const string CreateFileIoStatsTable = @" +CREATE TABLE IF NOT EXISTS file_io_stats ( + collection_id BIGINT PRIMARY KEY, + collection_time TIMESTAMP NOT NULL, + server_id INTEGER NOT NULL, + server_name VARCHAR NOT NULL, + database_name VARCHAR, + file_name VARCHAR, + file_type VARCHAR, + physical_name VARCHAR, + size_mb DECIMAL(18,2), + num_of_reads BIGINT, + num_of_writes BIGINT, + read_bytes BIGINT, + write_bytes BIGINT, + io_stall_read_ms BIGINT, + io_stall_write_ms BIGINT, + io_stall_queued_read_ms BIGINT, + io_stall_queued_write_ms BIGINT, + delta_reads BIGINT, + delta_writes BIGINT, + delta_read_bytes BIGINT, + delta_write_bytes BIGINT, + delta_stall_read_ms BIGINT, + delta_stall_write_ms BIGINT, + delta_stall_queued_read_ms BIGINT, + delta_stall_queued_write_ms BIGINT +)"; + + public const string CreateMemoryStatsTable = @" +CREATE TABLE IF NOT EXISTS memory_stats ( + collection_id BIGINT PRIMARY KEY, + collection_time TIMESTAMP NOT NULL, + server_id INTEGER NOT NULL, + server_name VARCHAR NOT NULL, + total_physical_memory_mb DECIMAL(18,2), + available_physical_memory_mb DECIMAL(18,2), + total_page_file_mb DECIMAL(18,2), + available_page_file_mb DECIMAL(18,2), + system_memory_state VARCHAR, + sql_memory_model VARCHAR, + target_server_memory_mb DECIMAL(18,2), + total_server_memory_mb DECIMAL(18,2), + buffer_pool_mb DECIMAL(18,2), + plan_cache_mb DECIMAL(18,2), + max_workers_count INTEGER, + current_workers_count INTEGER +)"; + + public const string CreateMemoryClerksTable = @" +CREATE TABLE IF NOT EXISTS memory_clerks ( + collection_id BIGINT PRIMARY KEY, + collection_time TIMESTAMP NOT NULL, + server_id INTEGER NOT NULL, + server_name VARCHAR NOT NULL, + clerk_type VARCHAR NOT NULL, + memory_mb DECIMAL(18,2) +)"; + + public const string CreateDeadlocksTable = @" +CREATE TABLE IF NOT EXISTS deadlocks ( + deadlock_id BIGINT PRIMARY KEY, + collection_time TIMESTAMP NOT NULL, + server_id INTEGER NOT NULL, + server_name VARCHAR NOT NULL, + deadlock_time TIMESTAMP, + victim_process_id VARCHAR, + victim_sql_text VARCHAR, + deadlock_graph_xml VARCHAR +)"; + + public const string CreateProcedureStatsTable = @" +CREATE TABLE IF NOT EXISTS procedure_stats ( + collection_id BIGINT PRIMARY KEY, + collection_time TIMESTAMP NOT NULL, + server_id INTEGER NOT NULL, + server_name VARCHAR NOT NULL, + database_name VARCHAR, + schema_name VARCHAR, + object_name VARCHAR, + object_type VARCHAR, + cached_time TIMESTAMP, + last_execution_time TIMESTAMP, + execution_count BIGINT, + total_worker_time BIGINT, + total_elapsed_time BIGINT, + total_logical_reads BIGINT, + total_physical_reads BIGINT, + total_logical_writes BIGINT, + min_worker_time BIGINT, + max_worker_time BIGINT, + min_elapsed_time BIGINT, + max_elapsed_time BIGINT, + min_logical_reads BIGINT, + max_logical_reads BIGINT, + min_physical_reads BIGINT, + max_physical_reads BIGINT, + min_logical_writes BIGINT, + max_logical_writes BIGINT, + total_spills BIGINT, + min_spills BIGINT, + max_spills BIGINT, + sql_handle VARCHAR, + plan_handle VARCHAR, + delta_execution_count BIGINT, + delta_worker_time BIGINT, + delta_elapsed_time BIGINT, + delta_logical_reads BIGINT, + delta_logical_writes BIGINT, + delta_physical_reads BIGINT +)"; + + public const string CreateQueryStoreStatsTable = @" +CREATE TABLE IF NOT EXISTS query_store_stats ( + collection_id BIGINT PRIMARY KEY, + collection_time TIMESTAMP NOT NULL, + server_id INTEGER NOT NULL, + server_name VARCHAR NOT NULL, + database_name VARCHAR NOT NULL, + query_id BIGINT, + plan_id BIGINT, + execution_type_desc VARCHAR, + first_execution_time TIMESTAMP, + last_execution_time TIMESTAMP, + module_name VARCHAR, + query_text VARCHAR, + query_hash VARCHAR, + execution_count BIGINT, + avg_duration_us BIGINT, + min_duration_us BIGINT, + max_duration_us BIGINT, + avg_cpu_time_us BIGINT, + min_cpu_time_us BIGINT, + max_cpu_time_us BIGINT, + avg_logical_io_reads BIGINT, + min_logical_io_reads BIGINT, + max_logical_io_reads BIGINT, + avg_logical_io_writes BIGINT, + min_logical_io_writes BIGINT, + max_logical_io_writes BIGINT, + avg_physical_io_reads BIGINT, + min_physical_io_reads BIGINT, + max_physical_io_reads BIGINT, + avg_clr_time_us BIGINT, + min_clr_time_us BIGINT, + max_clr_time_us BIGINT, + min_dop BIGINT, + max_dop BIGINT, + avg_query_max_used_memory BIGINT, + min_query_max_used_memory BIGINT, + max_query_max_used_memory BIGINT, + avg_rowcount BIGINT, + min_rowcount BIGINT, + max_rowcount BIGINT, + avg_num_physical_io_reads BIGINT, + min_num_physical_io_reads BIGINT, + max_num_physical_io_reads BIGINT, + avg_log_bytes_used BIGINT, + min_log_bytes_used BIGINT, + max_log_bytes_used BIGINT, + avg_tempdb_space_used BIGINT, + min_tempdb_space_used BIGINT, + max_tempdb_space_used BIGINT, + plan_type VARCHAR, + plan_forcing_type VARCHAR, + is_forced_plan BOOLEAN, + force_failure_count BIGINT, + last_force_failure_reason VARCHAR, + compatibility_level INTEGER, + query_plan_text VARCHAR, + query_plan_hash VARCHAR +)"; + + public const string CreateQuerySnapshotsTable = @" +CREATE TABLE IF NOT EXISTS query_snapshots ( + collection_id BIGINT PRIMARY KEY, + collection_time TIMESTAMP NOT NULL, + server_id INTEGER NOT NULL, + server_name VARCHAR NOT NULL, + session_id INTEGER, + database_name VARCHAR, + elapsed_time_formatted VARCHAR, + query_text VARCHAR, + query_plan VARCHAR, + live_query_plan VARCHAR, + status VARCHAR, + blocking_session_id INTEGER, + wait_type VARCHAR, + wait_time_ms BIGINT, + wait_resource VARCHAR, + cpu_time_ms BIGINT, + total_elapsed_time_ms BIGINT, + reads BIGINT, + writes BIGINT, + logical_reads BIGINT, + granted_query_memory_gb DECIMAL(18,2), + transaction_isolation_level VARCHAR, + dop INTEGER, + parallel_worker_count INTEGER, + login_name VARCHAR, + host_name VARCHAR, + program_name VARCHAR, + open_transaction_count INTEGER, + percent_complete DECIMAL(5,2) +)"; + + public const string CreateTempdbStatsTable = @" +CREATE TABLE IF NOT EXISTS tempdb_stats ( + collection_id BIGINT PRIMARY KEY, + collection_time TIMESTAMP NOT NULL, + server_id INTEGER NOT NULL, + server_name VARCHAR NOT NULL, + user_object_reserved_mb DECIMAL(18,2), + internal_object_reserved_mb DECIMAL(18,2), + version_store_reserved_mb DECIMAL(18,2), + total_reserved_mb DECIMAL(18,2), + unallocated_mb DECIMAL(18,2), + total_sessions_using_tempdb BIGINT, + top_session_id INTEGER, + top_session_tempdb_mb DECIMAL(18,2) +)"; + + public const string CreatePerfmonStatsTable = @" +CREATE TABLE IF NOT EXISTS perfmon_stats ( + collection_id BIGINT PRIMARY KEY, + collection_time TIMESTAMP NOT NULL, + server_id INTEGER NOT NULL, + server_name VARCHAR NOT NULL, + object_name VARCHAR NOT NULL, + counter_name VARCHAR NOT NULL, + instance_name VARCHAR, + cntr_value BIGINT, + delta_cntr_value BIGINT, + sample_interval_seconds INTEGER +)"; + + public const string CreateServerConfigTable = @" +CREATE TABLE IF NOT EXISTS server_config ( + config_id BIGINT PRIMARY KEY, + capture_time TIMESTAMP NOT NULL, + server_id INTEGER NOT NULL, + server_name VARCHAR NOT NULL, + configuration_name VARCHAR NOT NULL, + value_configured BIGINT, + value_in_use BIGINT, + is_dynamic BOOLEAN, + is_advanced BOOLEAN +)"; + + public const string CreateMemoryGrantStatsTable = @" +CREATE TABLE IF NOT EXISTS memory_grant_stats ( + collection_id BIGINT PRIMARY KEY, + collection_time TIMESTAMP NOT NULL, + server_id INTEGER NOT NULL, + server_name VARCHAR NOT NULL, + resource_semaphore_id SMALLINT, + pool_id INTEGER, + target_memory_mb DECIMAL(18,2), + max_target_memory_mb DECIMAL(18,2), + total_memory_mb DECIMAL(18,2), + available_memory_mb DECIMAL(18,2), + granted_memory_mb DECIMAL(18,2), + used_memory_mb DECIMAL(18,2), + grantee_count INTEGER, + waiter_count INTEGER, + timeout_error_count BIGINT, + forced_grant_count BIGINT, + timeout_error_count_delta BIGINT, + forced_grant_count_delta BIGINT +)"; + + public const string CreateWaitingTasksTable = @" +CREATE TABLE IF NOT EXISTS waiting_tasks ( + collection_id BIGINT PRIMARY KEY, + collection_time TIMESTAMP NOT NULL, + server_id INTEGER NOT NULL, + server_name VARCHAR NOT NULL, + session_id INTEGER, + wait_type VARCHAR, + wait_duration_ms BIGINT, + blocking_session_id INTEGER, + resource_description VARCHAR, + database_name VARCHAR +)"; + + public const string CreateBlockedProcessReportsTable = @" +CREATE TABLE IF NOT EXISTS blocked_process_reports ( + blocked_report_id BIGINT PRIMARY KEY, + collection_time TIMESTAMP NOT NULL, + server_id INTEGER NOT NULL, + server_name VARCHAR NOT NULL, + event_time TIMESTAMP, + database_name VARCHAR, + blocked_spid INTEGER, + blocked_ecid INTEGER, + blocking_spid INTEGER, + blocking_ecid INTEGER, + wait_time_ms BIGINT, + wait_resource VARCHAR, + lock_mode VARCHAR, + blocked_status VARCHAR, + blocked_isolation_level VARCHAR, + blocked_log_used BIGINT, + blocked_transaction_count INTEGER, + blocked_client_app VARCHAR, + blocked_host_name VARCHAR, + blocked_login_name VARCHAR, + blocked_sql_text VARCHAR, + blocking_status VARCHAR, + blocking_isolation_level VARCHAR, + blocking_client_app VARCHAR, + blocking_host_name VARCHAR, + blocking_login_name VARCHAR, + blocking_sql_text VARCHAR, + blocked_transaction_name VARCHAR, + blocking_transaction_name VARCHAR, + blocked_last_tran_started TIMESTAMP, + blocking_last_tran_started TIMESTAMP, + blocked_last_batch_started TIMESTAMP, + blocking_last_batch_started TIMESTAMP, + blocked_last_batch_completed TIMESTAMP, + blocking_last_batch_completed TIMESTAMP, + blocked_priority INTEGER, + blocking_priority INTEGER, + blocked_process_report_xml VARCHAR +)"; + + public const string CreateDatabaseConfigTable = @" +CREATE TABLE IF NOT EXISTS database_config ( + config_id BIGINT PRIMARY KEY, + capture_time TIMESTAMP NOT NULL, + server_id INTEGER NOT NULL, + server_name VARCHAR NOT NULL, + database_name VARCHAR NOT NULL, + state_desc VARCHAR, + compatibility_level INTEGER, + collation_name VARCHAR, + recovery_model VARCHAR, + is_read_only BOOLEAN, + is_auto_close_on BOOLEAN, + is_auto_shrink_on BOOLEAN, + is_auto_create_stats_on BOOLEAN, + is_auto_update_stats_on BOOLEAN, + is_auto_update_stats_async_on BOOLEAN, + is_read_committed_snapshot_on BOOLEAN, + snapshot_isolation_state VARCHAR, + is_parameterization_forced BOOLEAN, + is_query_store_on BOOLEAN, + is_encrypted BOOLEAN, + is_trustworthy_on BOOLEAN, + is_db_chaining_on BOOLEAN, + is_broker_enabled BOOLEAN, + is_cdc_enabled BOOLEAN, + is_mixed_page_allocation_on BOOLEAN, + log_reuse_wait_desc VARCHAR, + page_verify_option VARCHAR, + target_recovery_time_seconds INTEGER, + delayed_durability VARCHAR, + is_accelerated_database_recovery_on BOOLEAN, + is_memory_optimized_enabled BOOLEAN, + is_optimized_locking_on BOOLEAN +)"; + + // Index definitions + public const string CreateWaitStatsIndex = @" +CREATE INDEX IF NOT EXISTS idx_wait_stats_time ON wait_stats(server_id, collection_time)"; + + public const string CreateQueryStatsIndex = @" +CREATE INDEX IF NOT EXISTS idx_query_stats_time ON query_stats(server_id, collection_time)"; + + public const string CreateProcedureStatsIndex = @" +CREATE INDEX IF NOT EXISTS idx_procedure_stats_time ON procedure_stats(server_id, collection_time)"; + + public const string CreateQueryStoreIndex = @" +CREATE INDEX IF NOT EXISTS idx_query_store_time ON query_store_stats(server_id, collection_time)"; + + public const string CreateQuerySnapshotsIndex = @" +CREATE INDEX IF NOT EXISTS idx_query_snapshots_time ON query_snapshots(server_id, collection_time)"; + + public const string CreateCpuIndex = @" +CREATE INDEX IF NOT EXISTS idx_cpu_time ON cpu_utilization_stats(server_id, collection_time)"; + + public const string CreateFileIoIndex = @" +CREATE INDEX IF NOT EXISTS idx_file_io_time ON file_io_stats(server_id, collection_time)"; + + public const string CreateMemoryIndex = @" +CREATE INDEX IF NOT EXISTS idx_memory_time ON memory_stats(server_id, collection_time)"; + + public const string CreateTempdbIndex = @" +CREATE INDEX IF NOT EXISTS idx_tempdb_time ON tempdb_stats(server_id, collection_time)"; + + public const string CreatePerfmonIndex = @" +CREATE INDEX IF NOT EXISTS idx_perfmon_time ON perfmon_stats(server_id, collection_time)"; + + public const string CreateDeadlocksIndex = @" +CREATE INDEX IF NOT EXISTS idx_deadlocks_time ON deadlocks(server_id, collection_time)"; + + public const string CreateCollectionLogIndex = @" +CREATE INDEX IF NOT EXISTS idx_collection_log_time ON collection_log(server_id, collection_time)"; + + public const string CreateMemoryGrantStatsIndex = @" +CREATE INDEX IF NOT EXISTS idx_memory_grant_stats_time ON memory_grant_stats(server_id, collection_time)"; + + public const string CreateWaitingTasksIndex = @" +CREATE INDEX IF NOT EXISTS idx_waiting_tasks_time ON waiting_tasks(server_id, collection_time)"; + + public const string CreateBlockedProcessReportsIndex = @" +CREATE INDEX IF NOT EXISTS idx_blocked_process_reports_time ON blocked_process_reports(server_id, collection_time)"; + + public const string CreateMemoryClerksIndex = @" +CREATE INDEX IF NOT EXISTS idx_memory_clerks_time ON memory_clerks(server_id, collection_time)"; + + public const string CreateDatabaseScopedConfigTable = @" +CREATE TABLE IF NOT EXISTS database_scoped_config ( + config_id BIGINT PRIMARY KEY, + capture_time TIMESTAMP NOT NULL, + server_id INTEGER NOT NULL, + server_name VARCHAR NOT NULL, + database_name VARCHAR NOT NULL, + configuration_name VARCHAR NOT NULL, + value VARCHAR, + value_for_secondary VARCHAR +)"; + + public const string CreateDatabaseScopedConfigIndex = @" +CREATE INDEX IF NOT EXISTS idx_database_scoped_config_time ON database_scoped_config(server_id, capture_time)"; + + public const string CreateTraceFlagsTable = @" +CREATE TABLE IF NOT EXISTS trace_flags ( + config_id BIGINT PRIMARY KEY, + capture_time TIMESTAMP NOT NULL, + server_id INTEGER NOT NULL, + server_name VARCHAR NOT NULL, + trace_flag INTEGER NOT NULL, + status BOOLEAN NOT NULL, + is_global BOOLEAN NOT NULL, + is_session BOOLEAN NOT NULL +)"; + + public const string CreateTraceFlagsIndex = @" +CREATE INDEX IF NOT EXISTS idx_trace_flags_time ON trace_flags(server_id, capture_time)"; + + public const string CreateRunningJobsTable = @" +CREATE TABLE IF NOT EXISTS running_jobs ( + collection_time TIMESTAMP NOT NULL, + server_id INTEGER NOT NULL, + server_name VARCHAR NOT NULL, + job_name VARCHAR NOT NULL, + job_id VARCHAR NOT NULL, + job_enabled BOOLEAN NOT NULL, + start_time TIMESTAMP NOT NULL, + current_duration_seconds BIGINT NOT NULL, + avg_duration_seconds BIGINT NOT NULL, + p95_duration_seconds BIGINT NOT NULL, + successful_run_count BIGINT NOT NULL, + is_running_long BOOLEAN NOT NULL, + percent_of_average DECIMAL(10,1) +)"; + + public const string CreateRunningJobsIndex = @" +CREATE INDEX IF NOT EXISTS idx_running_jobs_time ON running_jobs(server_id, collection_time)"; + + public const string CreateDatabaseSizeStatsTable = @" +CREATE TABLE IF NOT EXISTS database_size_stats ( + collection_id BIGINT PRIMARY KEY, + collection_time TIMESTAMP NOT NULL, + server_id INTEGER NOT NULL, + server_name VARCHAR NOT NULL, + database_name VARCHAR NOT NULL, + database_id INTEGER NOT NULL, + file_id INTEGER NOT NULL, + file_type_desc VARCHAR NOT NULL, + file_name VARCHAR NOT NULL, + physical_name VARCHAR NOT NULL, + total_size_mb DECIMAL(19,2) NOT NULL, + used_size_mb DECIMAL(19,2), + auto_growth_mb DECIMAL(19,2), + max_size_mb DECIMAL(19,2), + recovery_model_desc VARCHAR, + compatibility_level INTEGER, + state_desc VARCHAR, + volume_mount_point VARCHAR, + volume_total_mb DECIMAL(19,2), + volume_free_mb DECIMAL(19,2), + is_percent_growth BOOLEAN, + growth_pct INTEGER, + vlf_count INTEGER +)"; + + public const string CreateDatabaseSizeStatsIndex = @" +CREATE INDEX IF NOT EXISTS idx_database_size_stats_time ON database_size_stats(server_id, collection_time)"; + + public const string CreateServerPropertiesTable = @" +CREATE TABLE IF NOT EXISTS server_properties ( + collection_id BIGINT PRIMARY KEY, + collection_time TIMESTAMP NOT NULL, + server_id INTEGER NOT NULL, + server_name VARCHAR NOT NULL, + edition VARCHAR NOT NULL, + product_version VARCHAR NOT NULL, + product_level VARCHAR NOT NULL, + product_update_level VARCHAR, + engine_edition INTEGER NOT NULL, + cpu_count INTEGER NOT NULL, + hyperthread_ratio INTEGER NOT NULL, + physical_memory_mb BIGINT NOT NULL, + socket_count INTEGER, + cores_per_socket INTEGER, + is_hadr_enabled BOOLEAN, + is_clustered BOOLEAN, + enterprise_features VARCHAR, + service_objective VARCHAR +)"; + + public const string CreateServerPropertiesIndex = @" +CREATE INDEX IF NOT EXISTS idx_server_properties_time ON server_properties(server_id, collection_time)"; + + public const string CreateSessionStatsTable = @" +CREATE TABLE IF NOT EXISTS session_stats ( + collection_id BIGINT PRIMARY KEY, + collection_time TIMESTAMP NOT NULL, + server_id INTEGER NOT NULL, + server_name VARCHAR NOT NULL, + program_name VARCHAR NOT NULL, + connection_count BIGINT NOT NULL, + running_count INTEGER NOT NULL, + sleeping_count INTEGER NOT NULL, + dormant_count INTEGER NOT NULL, + total_cpu_time_ms BIGINT, + total_reads BIGINT, + total_writes BIGINT, + total_logical_reads BIGINT +)"; + + public const string CreateSessionStatsIndex = @" +CREATE INDEX IF NOT EXISTS idx_session_stats_time ON session_stats(server_id, collection_time)"; + + public const string CreateAlertLogTable = @" +CREATE TABLE IF NOT EXISTS config_alert_log ( + alert_time TIMESTAMP NOT NULL, + server_id INTEGER NOT NULL, + server_name VARCHAR NOT NULL, + metric_name VARCHAR NOT NULL, + current_value DOUBLE NOT NULL, + threshold_value DOUBLE NOT NULL, + alert_sent BOOLEAN NOT NULL DEFAULT false, + notification_type VARCHAR NOT NULL DEFAULT 'tray', + send_error VARCHAR, + dismissed BOOLEAN NOT NULL DEFAULT false, + muted BOOLEAN NOT NULL DEFAULT false, + detail_text VARCHAR +)"; + + public const string CreateMuteRulesTable = @" +CREATE TABLE IF NOT EXISTS config_mute_rules ( + id VARCHAR NOT NULL PRIMARY KEY, + enabled BOOLEAN NOT NULL DEFAULT true, + created_at_utc TIMESTAMP NOT NULL, + expires_at_utc TIMESTAMP, + reason VARCHAR, + server_name VARCHAR, + metric_name VARCHAR, + database_pattern VARCHAR, + query_text_pattern VARCHAR, + wait_type_pattern VARCHAR, + job_name_pattern VARCHAR +)"; + + public const string CreateDismissedArchiveAlertsTable = @" +CREATE TABLE IF NOT EXISTS dismissed_archive_alerts ( + alert_time TIMESTAMP NOT NULL, + server_id INTEGER NOT NULL, + metric_name VARCHAR NOT NULL, + dismissed_at TIMESTAMP NOT NULL DEFAULT current_timestamp +)"; + + public const string CreateDismissedArchiveAlertsIndex = @" +CREATE INDEX IF NOT EXISTS idx_dismissed_archive_alerts +ON dismissed_archive_alerts (alert_time, server_id, metric_name)"; + + /// + /// Returns all table creation statements in order. + /// + public static IEnumerable GetAllTableStatements() + { + yield return CreateServersTable; + yield return CreateCollectionScheduleTable; + yield return CreateCollectionLogTable; + yield return CreateWaitStatsTable; + yield return CreateQueryStatsTable; + yield return CreateCpuUtilizationStatsTable; + yield return CreateFileIoStatsTable; + yield return CreateMemoryStatsTable; + yield return CreateMemoryClerksTable; + yield return CreateDeadlocksTable; + yield return CreateProcedureStatsTable; + yield return CreateQueryStoreStatsTable; + yield return CreateQuerySnapshotsTable; + yield return CreateTempdbStatsTable; + yield return CreatePerfmonStatsTable; + yield return CreateServerConfigTable; + yield return CreateDatabaseConfigTable; + yield return CreateMemoryGrantStatsTable; + yield return CreateWaitingTasksTable; + yield return CreateBlockedProcessReportsTable; + yield return CreateDatabaseScopedConfigTable; + yield return CreateTraceFlagsTable; + yield return CreateRunningJobsTable; + yield return CreateDatabaseSizeStatsTable; + yield return CreateServerPropertiesTable; + yield return CreateSessionStatsTable; + yield return CreateAlertLogTable; + yield return CreateMuteRulesTable; + yield return CreateDismissedArchiveAlertsTable; + } + + /// + /// Returns all index creation statements. + /// + public static IEnumerable GetAllIndexStatements() + { + yield return CreateWaitStatsIndex; + yield return CreateQueryStatsIndex; + yield return CreateProcedureStatsIndex; + yield return CreateQueryStoreIndex; + yield return CreateQuerySnapshotsIndex; + yield return CreateCpuIndex; + yield return CreateFileIoIndex; + yield return CreateMemoryIndex; + yield return CreateTempdbIndex; + yield return CreatePerfmonIndex; + yield return CreateDeadlocksIndex; + yield return CreateCollectionLogIndex; + yield return CreateMemoryGrantStatsIndex; + yield return CreateWaitingTasksIndex; + yield return CreateBlockedProcessReportsIndex; + yield return CreateMemoryClerksIndex; + yield return CreateDatabaseScopedConfigIndex; + yield return CreateTraceFlagsIndex; + yield return CreateRunningJobsIndex; + yield return CreateDatabaseSizeStatsIndex; + yield return CreateServerPropertiesIndex; + yield return CreateSessionStatsIndex; + yield return CreateDismissedArchiveAlertsIndex; + } +} diff --git a/Lite/Services/LocalDataService.AlertHistory.cs b/Lite/Services/LocalDataService.AlertHistory.cs index 0e093bf..75d7deb 100644 --- a/Lite/Services/LocalDataService.AlertHistory.cs +++ b/Lite/Services/LocalDataService.AlertHistory.cs @@ -103,6 +103,7 @@ ORDER BY alert_time DESC /// /// Dismisses specific alerts by marking them as dismissed in DuckDB. /// Identifies rows by (alert_time, server_id, metric_name) composite key. + /// If an alert only exists in archived parquet, inserts into dismissed_archive_alerts instead. /// public async Task DismissAlertsAsync(List alerts) { @@ -113,6 +114,7 @@ public async Task DismissAlertsAsync(List alerts) using var connection = await OpenConnectionAsync(); int totalAffected = 0; + int archivedDismissed = 0; foreach (var alert in alerts) { @@ -130,17 +132,37 @@ UPDATE config_alert_log var affected = await command.ExecuteNonQueryAsync(); totalAffected += affected; - if (affected == 0 && App.LogAlertDismissals) - AppLogger.Warn("AlertDismiss", $"No rows updated for alert: time={alert.AlertTime:O}, server_id={alert.ServerId}, metric={alert.MetricName} — may be archived to parquet"); + if (affected == 0) + { + using var sidecarCmd = connection.CreateCommand(); + sidecarCmd.CommandText = @" +INSERT INTO dismissed_archive_alerts (alert_time, server_id, metric_name) +SELECT $1, $2, $3 +WHERE NOT EXISTS ( + SELECT 1 FROM dismissed_archive_alerts + WHERE alert_time = $1 + AND server_id = $2 + AND metric_name = $3 +)"; + sidecarCmd.Parameters.Add(new DuckDBParameter { Value = alert.AlertTime }); + sidecarCmd.Parameters.Add(new DuckDBParameter { Value = alert.ServerId }); + sidecarCmd.Parameters.Add(new DuckDBParameter { Value = alert.MetricName }); + var sidecarAffected = await sidecarCmd.ExecuteNonQueryAsync(); + archivedDismissed += sidecarAffected; + + if (App.LogAlertDismissals) + AppLogger.Info("AlertDismiss", $"Archived alert dismissed via sidecar: time={alert.AlertTime:O}, server_id={alert.ServerId}, metric={alert.MetricName}"); + } } if (App.LogAlertDismissals) - AppLogger.Info("AlertDismiss", $"Dismiss complete: {totalAffected} row(s) updated out of {alerts.Count} selected"); - return totalAffected; + AppLogger.Info("AlertDismiss", $"Dismiss complete: {totalAffected} live + {archivedDismissed} archived out of {alerts.Count} selected"); + return totalAffected + archivedDismissed; } /// /// Dismisses all visible (non-dismissed) alerts matching the current filter criteria. + /// Updates the live table, then inserts any remaining archived alerts into the sidecar table. /// public async Task DismissAllVisibleAlertsAsync(int hoursBack, int? serverId = null) { @@ -173,10 +195,62 @@ UPDATE config_alert_log command.Parameters.Add(new DuckDBParameter { Value = cutoff }); } - var affected = await command.ExecuteNonQueryAsync(); + var liveAffected = await command.ExecuteNonQueryAsync(); + + // Dismiss any remaining archived alerts that matched the filter + using var sidecarCmd = connection.CreateCommand(); + if (serverId.HasValue) + { + sidecarCmd.CommandText = @" +INSERT INTO dismissed_archive_alerts (alert_time, server_id, metric_name) +SELECT v.alert_time, v.server_id, v.metric_name +FROM v_config_alert_log v +WHERE v.alert_time >= $1 +AND v.server_id = $2 +AND v.dismissed = FALSE +AND NOT EXISTS ( + SELECT 1 FROM config_alert_log l + WHERE l.alert_time = v.alert_time + AND l.server_id = v.server_id + AND l.metric_name = v.metric_name +) +AND NOT EXISTS ( + SELECT 1 FROM dismissed_archive_alerts d + WHERE d.alert_time = v.alert_time + AND d.server_id = v.server_id + AND d.metric_name = v.metric_name +)"; + sidecarCmd.Parameters.Add(new DuckDBParameter { Value = cutoff }); + sidecarCmd.Parameters.Add(new DuckDBParameter { Value = serverId.Value }); + } + else + { + sidecarCmd.CommandText = @" +INSERT INTO dismissed_archive_alerts (alert_time, server_id, metric_name) +SELECT v.alert_time, v.server_id, v.metric_name +FROM v_config_alert_log v +WHERE v.alert_time >= $1 +AND v.dismissed = FALSE +AND NOT EXISTS ( + SELECT 1 FROM config_alert_log l + WHERE l.alert_time = v.alert_time + AND l.server_id = v.server_id + AND l.metric_name = v.metric_name +) +AND NOT EXISTS ( + SELECT 1 FROM dismissed_archive_alerts d + WHERE d.alert_time = v.alert_time + AND d.server_id = v.server_id + AND d.metric_name = v.metric_name +)"; + sidecarCmd.Parameters.Add(new DuckDBParameter { Value = cutoff }); + } + + var archivedAffected = await sidecarCmd.ExecuteNonQueryAsync(); + if (App.LogAlertDismissals) - AppLogger.Info("AlertDismiss", $"Dismiss all complete: {affected} row(s) updated (cutoff={cutoff:O})"); - return affected; + AppLogger.Info("AlertDismiss", $"Dismiss all complete: {liveAffected} live + {archivedAffected} archived (cutoff={cutoff:O})"); + return liveAffected + archivedAffected; } }