Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1021,11 +1021,26 @@ private async void QueryStore_Click(object? sender, RoutedEventArgs e)

SetStatus("");

// Check if wait stats are supported (SQL 2017+ / Azure) and capture is enabled
var supportsWaitStats = _serverMetadata?.SupportsQueryStoreWaitStats ?? false;
if (supportsWaitStats)
{
try
{
var connStr = _serverConnection!.GetConnectionString(_credentialService, _selectedDatabase!);
supportsWaitStats = await QueryStoreService.IsWaitStatsCaptureEnabledAsync(connStr);
}
catch
{
supportsWaitStats = false;
}
}

// Build database list from the current DatabaseBox
var databases = DatabaseBox.Items.OfType<string>().ToList();

var grid = new QueryStoreGridControl(_serverConnection!, _credentialService,
_selectedDatabase!, databases);
_selectedDatabase!, databases, supportsWaitStats);
grid.PlansSelected += OnQueryStorePlansSelected;

var headerText = new TextBlock
Expand Down
21 changes: 18 additions & 3 deletions src/PlanViewer.App/Controls/QueryStoreGridControl.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public partial class QueryStoreGridControl : UserControl
private bool _initialOrderByLoaded;
private bool _suppressRangeChanged;
private string? _waitHighlightCategory;
private bool _waitStatsSupported; // false until version + capture mode confirmed
private bool _waitStatsEnabled = true;
private bool _waitPercentMode;

Expand All @@ -50,12 +51,13 @@ public partial class QueryStoreGridControl : UserControl
public string Database => _database;

public QueryStoreGridControl(ServerConnection serverConnection, ICredentialService credentialService,
string initialDatabase, List<string> databases)
string initialDatabase, List<string> databases, bool supportsWaitStats = false)
{
_serverConnection = serverConnection;
_credentialService = credentialService;
_database = initialDatabase;
_connectionString = serverConnection.GetConnectionString(credentialService, initialDatabase);
_waitStatsSupported = supportsWaitStats;
_slicerDaysBack = AppSettingsService.Load().QueryStoreSlicerDays;
InitializeComponent();
ResultsGrid.ItemsSource = _filteredRows;
Expand All @@ -69,6 +71,19 @@ public QueryStoreGridControl(ServerConnection serverConnection, ICredentialServi
WaitStatsProfile.CategoryDoubleClicked += OnWaitCategoryDoubleClicked;
WaitStatsProfile.CollapsedChanged += OnWaitStatsCollapsedChanged;

if (!_waitStatsSupported)
{
// Hide wait stats panel and column when server doesn't support it
WaitStatsProfile.Collapse();
WaitStatsChevronButton.IsVisible = false;
WaitStatsSplitter.IsVisible = false;
SlicerRow.ColumnDefinitions[2].Width = new GridLength(0);
var waitProfileCol = ResultsGrid.Columns
.FirstOrDefault(c => c.SortMemberPath == "WaitGrandTotalSort");
if (waitProfileCol != null)
waitProfileCol.IsVisible = false;
}

// Auto-fetch with default settings on connect
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
{
Expand Down Expand Up @@ -192,7 +207,7 @@ private async System.Threading.Tasks.Task FetchPlansForRangeAsync()
SelectToggleButton.Content = "Select None";

// Fetch wait stats in parallel (non-blocking for plan display)
if (_waitStatsEnabled && _slicerStartUtc.HasValue && _slicerEndUtc.HasValue)
if (_waitStatsSupported && _waitStatsEnabled && _slicerStartUtc.HasValue && _slicerEndUtc.HasValue)
_ = FetchWaitStatsAsync(_slicerStartUtc.Value, _slicerEndUtc.Value, ct);
}
catch (OperationCanceledException)
Expand Down Expand Up @@ -460,7 +475,7 @@ private void OnWaitStatsCollapsedChanged(object? sender, bool collapsed)
if (waitProfileCol != null)
waitProfileCol.IsVisible = !collapsed;

if (!collapsed && _slicerStartUtc.HasValue && _slicerEndUtc.HasValue)
if (!collapsed && _waitStatsSupported && _slicerStartUtc.HasValue && _slicerEndUtc.HasValue)
{
// Re-fetch wait stats when expanding — reuse the shared CTS
var ct = _fetchCts?.Token ?? CancellationToken.None;
Expand Down
6 changes: 6 additions & 0 deletions src/PlanViewer.Core/Models/ServerMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ public class ServerMetadata
/// </summary>
public bool SupportsScopedConfigs =>
IsAzure || (int.TryParse(ProductVersion?.Split('.')[0], out var major) && major >= 13);

/// <summary>
/// Whether sys.query_store_wait_stats is available (SQL 2017+ or Azure).
/// </summary>
public bool SupportsQueryStoreWaitStats =>
IsAzure || (int.TryParse(ProductVersion?.Split('.')[0], out var major) && major >= 14);
}

public class DatabaseMetadata
Expand Down
30 changes: 30 additions & 0 deletions src/PlanViewer.Core/Services/QueryStoreService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,36 @@ GROUP BY DATEADD(HOUR, DATEDIFF(HOUR, 0, rsi.start_time), 0)

// ── Wait stats ─────────────────────────────────────────────────────────

/// <summary>
/// Checks whether Query Store wait stats capture is enabled for the connected database.
/// Returns false on SQL Server 2016 (where the option doesn't exist) or when capture is OFF.
/// </summary>
public static async Task<bool> IsWaitStatsCaptureEnabledAsync(
string connectionString, CancellationToken ct = default)
{
const string sql = @"
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT CASE
WHEN EXISTS (
SELECT 1 FROM sys.database_query_store_options
WHERE wait_stats_capture_mode_desc = 'ON'
) THEN 1 ELSE 0 END;";

try
{
await using var conn = new SqlConnection(connectionString);
await conn.OpenAsync(ct);
await using var cmd = new SqlCommand(sql, conn) { CommandTimeout = 10 };
var result = await cmd.ExecuteScalarAsync(ct);
return result is int i && i == 1;
}
catch
{
// Column doesn't exist on SQL 2016, or query store not enabled
return false;
}
}

// Excluded: 11 = Idle, 18 = User Wait
private const string WaitCategoryExclusion = "AND ws.wait_category NOT IN (11, 18)";

Expand Down
Loading