diff --git a/Lite/Models/ServerConnection.cs b/Lite/Models/ServerConnection.cs
index 5f347a9..539d7c4 100644
--- a/Lite/Models/ServerConnection.cs
+++ b/Lite/Models/ServerConnection.cs
@@ -95,6 +95,13 @@ public bool UseWindowsAuth
///
public bool MultiSubnetFailover { get; set; } = false;
+ ///
+ /// User databases to skip in per-database collectors (query_store, file_io_stats, etc.).
+ /// System databases (master/tempdb/model/msdb) and the connection database itself are always
+ /// excluded by the collectors and aren't represented here.
+ ///
+ public System.Collections.Generic.List ExcludedDatabases { get; set; } = new();
+
///
/// Server name with "(Read-Only)" suffix when ReadOnlyIntent is enabled.
/// Used for sidebar subtitle and status text.
diff --git a/Lite/Services/RemoteCollectorService.DatabaseSize.cs b/Lite/Services/RemoteCollectorService.DatabaseSize.cs
index ad765fd..7d9227a 100644
--- a/Lite/Services/RemoteCollectorService.DatabaseSize.cs
+++ b/Lite/Services/RemoteCollectorService.DatabaseSize.cs
@@ -30,7 +30,7 @@ private async Task CollectDatabaseSizeStatsAsync(ServerConnection server, C
var serverStatus = _serverManager.GetConnectionStatus(server.Id);
bool isAzureSqlDb = serverStatus?.SqlEngineEdition == 5;
- const string onPremQuery = @"
+ string onPremQuery = @"
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SET NOCOUNT ON;
@@ -52,6 +52,7 @@ FROM sys.databases AS d
WHERE d.state_desc = N'ONLINE'
AND d.database_id > 0
AND HAS_DBACCESS(d.name) = 1
+ /*EXCLUSION_FILTER_CURSOR*/
ORDER BY
d.name;
@@ -131,11 +132,19 @@ LEFT JOIN #file_space AS fs
ON fs.database_id = mf.database_id
AND fs.file_id = mf.file_id
WHERE d.state_desc = N'ONLINE'
+/*EXCLUSION_FILTER_OUTER*/
ORDER BY
d.name,
mf.file_id
OPTION(RECOMPILE);";
+ /* Both filter sites (cursor SELECT and final SELECT) are in outer T-SQL, not nested dynamic SQL,
+ so parameter bindings work fine and the same @excl_db_N can be referenced twice. */
+ var (dbSizeExclusionClause, _) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
+ onPremQuery = onPremQuery
+ .Replace("/*EXCLUSION_FILTER_CURSOR*/", dbSizeExclusionClause)
+ .Replace("/*EXCLUSION_FILTER_OUTER*/", dbSizeExclusionClause);
+
const string azureSqlDbQuery = @"
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
@@ -231,6 +240,8 @@ ORDER BY
using var sqlConnection = await CreateConnectionAsync(server, cancellationToken);
using var command = new SqlCommand(onPremQuery, sqlConnection);
command.CommandTimeout = CommandTimeoutSeconds;
+ var (_, dbSizeExclusionParams) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
+ foreach (var p in dbSizeExclusionParams) command.Parameters.Add(p);
using var reader = await command.ExecuteReaderAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
diff --git a/Lite/Services/RemoteCollectorService.FileIo.cs b/Lite/Services/RemoteCollectorService.FileIo.cs
index 203573e..a1ac8a8 100644
--- a/Lite/Services/RemoteCollectorService.FileIo.cs
+++ b/Lite/Services/RemoteCollectorService.FileIo.cs
@@ -83,8 +83,13 @@ LEFT JOIN sys.databases AS d
WHERE (vfs.database_id > 4 OR vfs.database_id = 2)
AND vfs.database_id < 32761
AND vfs.database_id <> ISNULL(DB_ID(N'PerformanceMonitor'), 0)
+/*EXCLUSION_FILTER*/
OPTION(RECOMPILE);";
+ /* Azure path filters via GetAzureDatabaseListAsync; on-prem path injects here */
+ var (fileIoExclusionClause, _) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
+ query = query.Replace("/*EXCLUSION_FILTER*/", isAzureSqlDb ? string.Empty : fileIoExclusionClause);
+
var serverId = GetServerId(server);
var collectionTime = DateTime.UtcNow;
var rowsCollected = 0;
@@ -125,6 +130,8 @@ AND vfs.database_id < 32761
{
using var sqlConnection = await CreateConnectionAsync(server, cancellationToken);
using var command = new SqlCommand(query, sqlConnection) { CommandTimeout = CommandTimeoutSeconds };
+ var (_, fileIoExclusionParams) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
+ foreach (var p in fileIoExclusionParams) command.Parameters.Add(p);
using var reader = await command.ExecuteReaderAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
fileStats.Add(ReadFileIoRow(reader));
diff --git a/Lite/Services/RemoteCollectorService.ProcedureStats.cs b/Lite/Services/RemoteCollectorService.ProcedureStats.cs
index 58a5057..3bd37c3 100644
--- a/Lite/Services/RemoteCollectorService.ProcedureStats.cs
+++ b/Lite/Services/RemoteCollectorService.ProcedureStats.cs
@@ -32,7 +32,7 @@ so the standard NOT IN filter excludes everything. Use a simplified query. */
/* total_spills/min_spills/max_spills exist in dm_exec_procedure_stats and dm_exec_trigger_stats
on all supported versions, but do NOT exist in dm_exec_function_stats on any version.
Use dynamic SQL to handle this. */
- const string standardQuery = @"
+ string standardQuery = @"
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
DECLARE
@@ -81,6 +81,7 @@ INNER JOIN sys.databases AS d
WHERE d.state = 0
AND pa.dbid NOT IN (1, 3, 4, 32761, 32767, ISNULL(DB_ID(N''PerformanceMonitor''), 0))
AND s.last_execution_time >= DATEADD(MINUTE, -10, GETDATE())
+/*EXCLUSION_FILTER*/
UNION ALL
@@ -136,6 +137,7 @@ INNER JOIN sys.databases AS d
WHERE d.state = 0
AND pa.dbid NOT IN (1, 3, 4, 32761, 32767, ISNULL(DB_ID(N''PerformanceMonitor''), 0))
AND s.last_execution_time >= DATEADD(MINUTE, -10, GETDATE())
+/*EXCLUSION_FILTER*/
UNION ALL
@@ -178,6 +180,7 @@ INNER JOIN sys.databases AS d
WHERE d.state = 0
AND pa.dbid NOT IN (1, 3, 4, 32761, 32767, ISNULL(DB_ID(N''PerformanceMonitor''), 0))
AND s.last_execution_time >= DATEADD(MINUTE, -10, GETDATE())
+/*EXCLUSION_FILTER*/
) AS combined
ORDER BY total_elapsed_time DESC
OPTION(RECOMPILE);' AS nvarchar(max));
@@ -220,9 +223,19 @@ No trigger stats or function stats — Azure SQL DB scope is single-database. */
FROM sys.dm_exec_procedure_stats AS s
WHERE s.database_id = DB_ID()
AND s.last_execution_time >= DATEADD(MINUTE, -10, GETDATE())
+/*EXCLUSION_FILTER*/
ORDER BY s.total_elapsed_time DESC
OPTION(RECOMPILE);";
+ /* Standard query is dynamic SQL (built into @sql then passed to sp_executesql), so the
+ exclusion filter is interpolated as literal N'...' values rather than parameter bindings.
+ Names come from a user-picked checklist of existing DBs, escaped against single-quote
+ injection. forNestedDynamicSql=true doubles the escape because @sql is itself a
+ single-quoted T-SQL string at the outer layer. */
+ string standardExclusionClause = BuildDatabaseExclusionLiteralClause(
+ server.ExcludedDatabases, "d.name", forNestedDynamicSql: true);
+ standardQuery = standardQuery.Replace("/*EXCLUSION_FILTER*/", standardExclusionClause);
+
string query = isAzureSqlDb ? azureSqlDbQuery : standardQuery;
var serverId = GetServerId(server);
diff --git a/Lite/Services/RemoteCollectorService.QuerySnapshots.cs b/Lite/Services/RemoteCollectorService.QuerySnapshots.cs
index c18523c..6e1de96 100644
--- a/Lite/Services/RemoteCollectorService.QuerySnapshots.cs
+++ b/Lite/Services/RemoteCollectorService.QuerySnapshots.cs
@@ -232,6 +232,18 @@ private async Task CollectQuerySnapshotsAsync(ServerConnection server, Canc
var query = BuildQuerySnapshotsQuery(supportsLiveQueryPlan, isAzureSqlDatabase);
+ /* Append the per-database exclusion filter to the WHERE clause. The base query joins
+ sys.databases AS d, so we filter on d.name. When ExcludedDatabases is empty the
+ clause is "" so nothing changes. */
+ var (qsExclusionClause, _) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
+ if (!string.IsNullOrEmpty(qsExclusionClause))
+ {
+ /* Inject just before the OPTION clause so it lands inside the WHERE. */
+ query = query.Replace(
+ "ORDER BY der.cpu_time DESC",
+ qsExclusionClause + "\nORDER BY der.cpu_time DESC");
+ }
+
var serverId = GetServerId(server);
var collectionTime = DateTime.UtcNow;
var rowsCollected = 0;
@@ -242,6 +254,8 @@ private async Task CollectQuerySnapshotsAsync(ServerConnection server, Canc
using var sqlConnection = await CreateConnectionAsync(server, cancellationToken);
using var command = new SqlCommand(query, sqlConnection);
command.CommandTimeout = CommandTimeoutSeconds;
+ var (_, qsExclusionParams) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
+ foreach (var p in qsExclusionParams) command.Parameters.Add(p);
using var reader = await command.ExecuteReaderAsync(cancellationToken);
sqlSw.Stop();
diff --git a/Lite/Services/RemoteCollectorService.QueryStats.cs b/Lite/Services/RemoteCollectorService.QueryStats.cs
index b47705a..4a139c3 100644
--- a/Lite/Services/RemoteCollectorService.QueryStats.cs
+++ b/Lite/Services/RemoteCollectorService.QueryStats.cs
@@ -31,7 +31,7 @@ Use a simplified query that skips plan_attributes entirely — there's only one
var serverStatus = _serverManager.GetConnectionStatus(server.Id);
bool isAzureSqlDb = serverStatus.SqlEngineEdition == 5;
- const string standardQuery = @"
+ string standardQuery = @"
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT /* PerformanceMonitorLite */ TOP (200)
@@ -106,10 +106,14 @@ INNER JOIN sys.databases AS d
WHERE pa.dbid NOT IN (1, 2, 3, 4, 32761, 32767, ISNULL(DB_ID(N'PerformanceMonitor'), 0))
AND st.text NOT LIKE N'%PerformanceMonitorLite%'
AND qs.last_execution_time >= DATEADD(MINUTE, -10, GETDATE())
+/*EXCLUSION_FILTER*/
ORDER BY
qs.total_elapsed_time DESC
OPTION(RECOMPILE);";
+ var (exclusionClause, _) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
+ standardQuery = standardQuery.Replace("/*EXCLUSION_FILTER*/", exclusionClause);
+
/* Azure SQL DB: skip plan_attributes, use DB_NAME() for the single database context */
const string azureSqlDbQuery = @"
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
@@ -229,6 +233,11 @@ qs.total_elapsed_time DESC
{
using var command = new SqlCommand(query, sqlConnection);
command.CommandTimeout = CommandTimeoutSeconds;
+ if (!isAzureSqlDb)
+ {
+ var (_, freshParams) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
+ foreach (var p in freshParams) command.Parameters.Add(p);
+ }
using var reader = await command.ExecuteReaderAsync(cancellationToken);
diff --git a/Lite/Services/RemoteCollectorService.QueryStore.cs b/Lite/Services/RemoteCollectorService.QueryStore.cs
index 9265c32..5da8420 100644
--- a/Lite/Services/RemoteCollectorService.QueryStore.cs
+++ b/Lite/Services/RemoteCollectorService.QueryStore.cs
@@ -32,7 +32,7 @@ Uses sys.database_query_store_options.actual_state instead of
var serverStatus = _serverManager.GetConnectionStatus(server.Id);
bool isAzureSqlDb = serverStatus?.SqlEngineEdition == 5;
- const string onPremDbQuery = @"
+ string onPremDbQuery = @"
SET NOCOUNT ON;
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
@@ -60,6 +60,7 @@ AND d.name <> N'PerformanceMonitor'
drs.database_id IS NULL /*not in any AG*/
OR drs.is_primary_replica = 1 /*primary replica*/
)
+ /*EXCLUSION_FILTER*/
OPTION(RECOMPILE);
OPEN db_check;
@@ -103,7 +104,7 @@ FROM @result
ORDER BY
name;";
- const string azureDbQuery = @"
+ string azureDbQuery = @"
SET NOCOUNT ON;
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
@@ -123,6 +124,7 @@ WHERE d.database_id > 4
AND d.database_id < 32761
AND d.state_desc = N'ONLINE'
AND d.name <> N'PerformanceMonitor'
+ /*EXCLUSION_FILTER*/
OPTION(RECOMPILE);
OPEN db_check;
@@ -166,6 +168,10 @@ FROM @result
ORDER BY
name;";
+ var (exclusionClause, _) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
+ onPremDbQuery = onPremDbQuery.Replace("/*EXCLUSION_FILTER*/", exclusionClause);
+ azureDbQuery = azureDbQuery.Replace("/*EXCLUSION_FILTER*/", exclusionClause);
+
string dbQuery = isAzureSqlDb ? azureDbQuery : onPremDbQuery;
var serverId = GetServerId(server);
@@ -187,6 +193,8 @@ ORDER BY
using (var dbCommand = new SqlCommand(dbQuery, sqlConnection))
{
dbCommand.CommandTimeout = CommandTimeoutSeconds;
+ var (_, exclusionParams) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
+ foreach (var p in exclusionParams) dbCommand.Parameters.Add(p);
using var dbReader = await dbCommand.ExecuteReaderAsync(cancellationToken);
while (await dbReader.ReadAsync(cancellationToken))
{
diff --git a/Lite/Services/RemoteCollectorService.ServerConfig.cs b/Lite/Services/RemoteCollectorService.ServerConfig.cs
index 66f4b60..6feecd7 100644
--- a/Lite/Services/RemoteCollectorService.ServerConfig.cs
+++ b/Lite/Services/RemoteCollectorService.ServerConfig.cs
@@ -149,6 +149,8 @@ private async Task CollectDatabaseConfigAsync(ServerConnection server, Canc
is_optimized_locking_on = d.is_optimized_locking_on";
}
+ var (dbConfigExclusionClause, _) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
+
var query = $@"
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
@@ -158,6 +160,7 @@ FROM sys.databases AS d
WHERE (d.database_id > 4 OR d.database_id = 2)
AND d.database_id < 32761
AND d.name <> N'PerformanceMonitor'
+{dbConfigExclusionClause}
ORDER BY d.name
OPTION(RECOMPILE);";
@@ -173,6 +176,8 @@ ORDER BY d.name
using var sqlConnection = await CreateConnectionAsync(server, cancellationToken);
using var command = new SqlCommand(query, sqlConnection);
command.CommandTimeout = CommandTimeoutSeconds;
+ var (_, dbConfigExclusionParams) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
+ foreach (var p in dbConfigExclusionParams) command.Parameters.Add(p);
using var reader = await command.ExecuteReaderAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
@@ -329,7 +334,7 @@ private async Task CollectDatabaseScopedConfigAsync(ServerConnection server
var serverStatus = _serverManager.GetConnectionStatus(server.Id);
bool isAzureSqlDb = serverStatus?.SqlEngineEdition == 5;
- const string onPremDbQuery = @"
+ string onPremDbQuery = @"
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT
@@ -347,10 +352,11 @@ AND d.name <> N'PerformanceMonitor'
drs.database_id IS NULL /*not in any AG*/
OR drs.is_primary_replica = 1 /*primary replica*/
)
+/*EXCLUSION_FILTER*/
ORDER BY d.name
OPTION(RECOMPILE);";
- const string azureDbQuery = @"
+ string azureDbQuery = @"
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT
@@ -360,9 +366,14 @@ FROM sys.databases AS d
AND d.database_id < 32761
AND d.name <> N'PerformanceMonitor'
AND d.state_desc = N'ONLINE'
+/*EXCLUSION_FILTER*/
ORDER BY d.name
OPTION(RECOMPILE);";
+ var (scopedExclusionClause, _) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
+ onPremDbQuery = onPremDbQuery.Replace("/*EXCLUSION_FILTER*/", scopedExclusionClause);
+ azureDbQuery = azureDbQuery.Replace("/*EXCLUSION_FILTER*/", scopedExclusionClause);
+
string dbQuery = isAzureSqlDb ? azureDbQuery : onPremDbQuery;
var serverId = GetServerId(server);
@@ -379,6 +390,8 @@ ORDER BY d.name
using (var dbCommand = new SqlCommand(dbQuery, sqlConnection))
{
dbCommand.CommandTimeout = CommandTimeoutSeconds;
+ var (_, scopedExclusionParams) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
+ foreach (var p in scopedExclusionParams) dbCommand.Parameters.Add(p);
using var dbReader = await dbCommand.ExecuteReaderAsync(cancellationToken);
while (await dbReader.ReadAsync(cancellationToken))
{
diff --git a/Lite/Services/RemoteCollectorService.WaitingTasks.cs b/Lite/Services/RemoteCollectorService.WaitingTasks.cs
index bd9373b..aaa7d2d 100644
--- a/Lite/Services/RemoteCollectorService.WaitingTasks.cs
+++ b/Lite/Services/RemoteCollectorService.WaitingTasks.cs
@@ -24,7 +24,7 @@ public partial class RemoteCollectorService
///
private async Task CollectWaitingTasksAsync(ServerConnection server, CancellationToken cancellationToken)
{
- const string query = @"
+ string query = @"
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT /* PerformanceMonitorLite */
@@ -42,8 +42,12 @@ LEFT JOIN sys.databases AS d
AND wt.session_id <> @@SPID
AND wt.wait_type IS NOT NULL
AND er.database_id <> ISNULL(DB_ID(N'PerformanceMonitor'), 0)
+/*EXCLUSION_FILTER*/
OPTION(RECOMPILE);";
+ var (exclusionClause, _) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
+ query = query.Replace("/*EXCLUSION_FILTER*/", exclusionClause);
+
var serverId = GetServerId(server);
var collectionTime = DateTime.UtcNow;
var rowsCollected = 0;
@@ -54,6 +58,8 @@ AND wt.wait_type IS NOT NULL
using var sqlConnection = await CreateConnectionAsync(server, cancellationToken);
using var command = new SqlCommand(query, sqlConnection);
command.CommandTimeout = CommandTimeoutSeconds;
+ var (_, exclusionParams) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "d.name");
+ foreach (var p in exclusionParams) command.Parameters.Add(p);
using var reader = await command.ExecuteReaderAsync(cancellationToken);
sqlSw.Stop();
diff --git a/Lite/Services/RemoteCollectorService.cs b/Lite/Services/RemoteCollectorService.cs
index fdecb7d..055f370 100644
--- a/Lite/Services/RemoteCollectorService.cs
+++ b/Lite/Services/RemoteCollectorService.cs
@@ -634,15 +634,18 @@ protected async Task> GetAzureDatabaseListAsync(ServerConnection se
InitialCatalog = "master"
}.ConnectionString;
+ var (exclusionClause, exclusionParams) = BuildDatabaseExclusionFilter(server.ExcludedDatabases, "name");
+
var databases = new List();
try
{
using var conn = new SqlConnection(connStr);
await conn.OpenAsync(cancellationToken);
using var cmd = new SqlCommand(
- "SELECT name FROM sys.databases WHERE state_desc = N'ONLINE' AND database_id > 0 ORDER BY name;",
+ $"SELECT name FROM sys.databases WHERE state_desc = N'ONLINE' AND database_id > 0 {exclusionClause} ORDER BY name;",
conn)
{ CommandTimeout = CommandTimeoutSeconds };
+ foreach (var p in exclusionParams) cmd.Parameters.Add(p);
using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
while (await reader.ReadAsync(cancellationToken))
databases.Add(reader.GetString(0));
@@ -668,6 +671,54 @@ protected async Task> GetAzureDatabaseListAsync(ServerConnection se
}
}
+ ///
+ /// Builds a SQL fragment and matching SqlParameters for excluding the supplied database names.
+ /// When the list is empty, returns ("", []) so callers can splice without effect.
+ /// Each name is parameterized — works on every supported SQL Server version (no STRING_SPLIT/OPENJSON
+ /// compatibility-level dependency).
+ ///
+ /// Names from server.ExcludedDatabases.
+ /// SQL column to filter, e.g. "d.name".
+ internal static (string Clause, List Parameters) BuildDatabaseExclusionFilter(
+ IList? excludedDatabaseNames, string columnExpression)
+ {
+ if (excludedDatabaseNames is null || excludedDatabaseNames.Count == 0)
+ return (string.Empty, new List());
+
+ var paramNames = new List(excludedDatabaseNames.Count);
+ var sqlParams = new List(excludedDatabaseNames.Count);
+ for (int i = 0; i < excludedDatabaseNames.Count; i++)
+ {
+ string p = $"@excl_db_{i}";
+ paramNames.Add(p);
+ sqlParams.Add(new SqlParameter(p, System.Data.SqlDbType.NVarChar, 128) { Value = excludedDatabaseNames[i] });
+ }
+ return ($"AND {columnExpression} NOT IN ({string.Join(", ", paramNames)})", sqlParams);
+ }
+
+ ///
+ /// Builds a SQL fragment with database names interpolated as literal N'...' values.
+ /// Use this for dynamic SQL paths where parameter binding is awkward (e.g. inside
+ /// a string passed to sp_executesql). Names come from user-picked checklists of
+ /// existing databases, so literal interpolation with single-quote escaping is safe.
+ /// When forNestedDynamicSql=true, doubles the escape for use inside an outer T-SQL
+ /// string that itself becomes a dynamic-SQL @sql variable.
+ ///
+ internal static string BuildDatabaseExclusionLiteralClause(
+ IList? excludedDatabaseNames, string columnExpression, bool forNestedDynamicSql = false)
+ {
+ if (excludedDatabaseNames is null || excludedDatabaseNames.Count == 0)
+ return string.Empty;
+
+ string escapedQuote = forNestedDynamicSql ? "''" : "'";
+ string Escape(string s) => forNestedDynamicSql
+ ? s.Replace("'", "''''")
+ : s.Replace("'", "''");
+
+ var quoted = excludedDatabaseNames.Select(n => $"N{escapedQuote}{Escape(n)}{escapedQuote}");
+ return $"AND {columnExpression} NOT IN ({string.Join(", ", quoted)})";
+ }
+
private static List SingleDbOrEmpty(string? targetDb)
{
if (string.IsNullOrEmpty(targetDb) || string.Equals(targetDb, "master", StringComparison.OrdinalIgnoreCase))
diff --git a/Lite/Windows/ExcludedDatabasesDialog.xaml b/Lite/Windows/ExcludedDatabasesDialog.xaml
new file mode 100644
index 0000000..7fbbae2
--- /dev/null
+++ b/Lite/Windows/ExcludedDatabasesDialog.xaml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Lite/Windows/ExcludedDatabasesDialog.xaml.cs b/Lite/Windows/ExcludedDatabasesDialog.xaml.cs
new file mode 100644
index 0000000..733fa09
--- /dev/null
+++ b/Lite/Windows/ExcludedDatabasesDialog.xaml.cs
@@ -0,0 +1,174 @@
+/*
+ * Copyright (c) 2026 Erik Darling, Darling Data LLC
+ *
+ * This file is part of the SQL Server Performance Monitor Lite.
+ *
+ * Licensed under the MIT License. See LICENSE file in the project root for full license information.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Media;
+using Microsoft.Data.SqlClient;
+using PerformanceMonitorLite.Models;
+using PerformanceMonitorLite.Services;
+
+namespace PerformanceMonitorLite.Windows;
+
+public partial class ExcludedDatabasesDialog : Window
+{
+ private readonly ServerManager _serverManager;
+ private readonly ServerConnection _server;
+ private ObservableCollection _items = new();
+
+ public bool ExclusionsModified { get; private set; }
+
+ public ExcludedDatabasesDialog(ServerManager serverManager, ServerConnection server)
+ {
+ InitializeComponent();
+ _serverManager = serverManager;
+ _server = server;
+ HeaderText.Text = $"Excluded Databases — {server.DisplayNameWithIntent}";
+ Loaded += async (_, _) => await LoadAsync();
+ }
+
+ private async Task LoadAsync()
+ {
+ StatusText.Text = "Loading databases…";
+ DatabasesItemsControl.ItemsSource = null;
+
+ try
+ {
+ var liveDatabases = await GetUserDatabasesAsync();
+ var existingExclusions = _server.ExcludedDatabases ?? new List();
+
+ var liveSet = new HashSet(liveDatabases, StringComparer.OrdinalIgnoreCase);
+
+ _items = new ObservableCollection();
+
+ foreach (var name in liveDatabases.OrderBy(n => n, StringComparer.OrdinalIgnoreCase))
+ {
+ _items.Add(new DatabaseExclusionItem
+ {
+ Name = name,
+ DisplayName = name,
+ IsExcluded = existingExclusions.Contains(name, StringComparer.OrdinalIgnoreCase),
+ IsEnabled = true,
+ IsStale = false
+ });
+ }
+
+ /* Stale entries: in exclusion list but not present on the server. Greyed, disabled, pre-checked. */
+ foreach (var name in existingExclusions
+ .Where(n => !liveSet.Contains(n))
+ .OrderBy(n => n, StringComparer.OrdinalIgnoreCase))
+ {
+ _items.Add(new DatabaseExclusionItem
+ {
+ Name = name,
+ DisplayName = $"{name} (missing)",
+ IsExcluded = true,
+ IsEnabled = false,
+ IsStale = true
+ });
+ }
+
+ DatabasesItemsControl.ItemsSource = _items;
+ StatusText.Text = $"{liveDatabases.Count} database(s) on this server, {existingExclusions.Count} currently excluded.";
+ }
+ catch (Exception ex)
+ {
+ StatusText.Text = $"Failed to load: {ex.Message}";
+ MessageBox.Show(this,
+ $"Could not read database list from '{_server.DisplayNameWithIntent}':\n\n{ex.Message}",
+ "Load Failed",
+ MessageBoxButton.OK,
+ MessageBoxImage.Error);
+ }
+ }
+
+ private async Task> GetUserDatabasesAsync()
+ {
+ var connectionString = _server.GetConnectionString(_serverManager.CredentialService);
+ var builder = new SqlConnectionStringBuilder(connectionString)
+ {
+ InitialCatalog = "master",
+ ConnectTimeout = 10
+ };
+
+ using var connection = new SqlConnection(builder.ConnectionString);
+ await connection.OpenAsync();
+
+ using var cmd = new SqlCommand(@"
+SELECT d.name
+FROM sys.databases AS d
+WHERE d.database_id > 4
+AND d.state_desc = N'ONLINE'
+AND d.database_id < 32761 /*exclude contained AG system databases*/
+ORDER BY d.name;", connection);
+ cmd.CommandTimeout = 30;
+
+ var names = new List();
+ using var reader = await cmd.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ names.Add(reader.GetString(0));
+ }
+ return names;
+ }
+
+ private void Save_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ _server.ExcludedDatabases = _items
+ .Where(i => i.IsExcluded)
+ .Select(i => i.Name)
+ .ToList();
+
+ _serverManager.UpdateServer(_server);
+ ExclusionsModified = true;
+ DialogResult = true;
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show(this,
+ $"Failed to save exclusions:\n\n{ex.Message}",
+ "Save Failed",
+ MessageBoxButton.OK,
+ MessageBoxImage.Error);
+ }
+ }
+
+ private void Cancel_Click(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ }
+}
+
+public class DatabaseExclusionItem : INotifyPropertyChanged
+{
+ public string Name { get; set; } = "";
+ public string DisplayName { get; set; } = "";
+ private bool _isExcluded;
+ public bool IsExcluded
+ {
+ get => _isExcluded;
+ set { _isExcluded = value; OnPropertyChanged(nameof(IsExcluded)); }
+ }
+ public bool IsEnabled { get; set; } = true;
+ public bool IsStale { get; set; }
+
+ public Brush ForegroundBrush => IsStale
+ ? (Brush)Application.Current.FindResource("ForegroundMutedBrush")
+ : (Brush)Application.Current.FindResource("ForegroundBrush");
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+ private void OnPropertyChanged(string propertyName)
+ => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+}
diff --git a/Lite/Windows/ManageServersWindow.xaml b/Lite/Windows/ManageServersWindow.xaml
index 4fd1156..3f687c6 100644
--- a/Lite/Windows/ManageServersWindow.xaml
+++ b/Lite/Windows/ManageServersWindow.xaml
@@ -9,6 +9,11 @@
+
+
+
+
@@ -66,6 +71,7 @@
+
diff --git a/Lite/Windows/ManageServersWindow.xaml.cs b/Lite/Windows/ManageServersWindow.xaml.cs
index 03b6b8e..aae0ccd 100644
--- a/Lite/Windows/ManageServersWindow.xaml.cs
+++ b/Lite/Windows/ManageServersWindow.xaml.cs
@@ -70,6 +70,25 @@ private void EditSelected()
}
}
+ private void EditMenuItem_Click(object sender, RoutedEventArgs e) => EditSelected();
+
+ private void DeleteMenuItem_Click(object sender, RoutedEventArgs e) => DeleteButton_Click(sender, e);
+
+ private void ExcludedDatabases_Click(object sender, RoutedEventArgs e)
+ {
+ if (ServersGrid.SelectedItem is not ServerConnection selected)
+ {
+ return;
+ }
+
+ var dialog = new ExcludedDatabasesDialog(_serverManager, selected) { Owner = this };
+ if (dialog.ShowDialog() == true && dialog.ExclusionsModified)
+ {
+ ServersChanged = true;
+ RefreshGrid();
+ }
+ }
+
private void DeleteButton_Click(object sender, RoutedEventArgs e)
{
if (ServersGrid.SelectedItem is not ServerConnection selected)