From 78339bd894485b090033a7d0f28afb3ff7ff3cfe Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Sun, 10 May 2026 21:32:09 +0200 Subject: [PATCH] Expose execution_type_desc filter to CLI and MCP Adds --execution-type to the CLI query-store command and execution_type to the get_query_store_top MCP tool, accepting regular/aborted/exception/ failed (= aborted + exception). Mirrors the desktop UI filter added in #321. Parsing logic is centralized in QueryStoreFilter.ParseExecutionType. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/PlanViewer.App/Mcp/McpQueryStoreTools.cs | 17 +++++++++++-- .../Commands/QueryStoreCommand.cs | 25 +++++++++++++++++-- src/PlanViewer.Core/Models/QueryStorePlan.cs | 20 +++++++++++++++ 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/PlanViewer.App/Mcp/McpQueryStoreTools.cs b/src/PlanViewer.App/Mcp/McpQueryStoreTools.cs index 40933c3..7f60c6e 100644 --- a/src/PlanViewer.App/Mcp/McpQueryStoreTools.cs +++ b/src/PlanViewer.App/Mcp/McpQueryStoreTools.cs @@ -70,7 +70,8 @@ public static async Task GetQueryStoreTop( [Description("Filter by Query Store plan ID.")] long? plan_id = null, [Description("Filter by query hash (hex, e.g. 0x1AB2C3D4).")] string? query_hash = null, [Description("Filter by query plan hash (hex, e.g. 0x1AB2C3D4).")] string? plan_hash = null, - [Description("Filter by module name (schema.name, supports % wildcards).")] string? module = null) + [Description("Filter by module name (schema.name, supports % wildcards).")] string? module = null, + [Description("Filter by execution type: regular, aborted, exception, or failed (= aborted + exception).")] string? execution_type = null) { try { @@ -84,9 +85,20 @@ public static async Task GetQueryStoreTop( if (hours_back < 1 || hours_back > 168) return "Invalid hours_back value. Must be between 1 and 168."; + string[]? executionTypes; + try + { + executionTypes = QueryStoreFilter.ParseExecutionType(execution_type); + } + catch (ArgumentException ex) + { + return ex.Message; + } + QueryStoreFilter? filter = null; if (query_id != null || plan_id != null || - query_hash != null || plan_hash != null || module != null) + query_hash != null || plan_hash != null || module != null || + executionTypes != null) { filter = new QueryStoreFilter { @@ -95,6 +107,7 @@ public static async Task GetQueryStoreTop( QueryHash = query_hash, QueryPlanHash = plan_hash, ModuleName = module, + ExecutionTypeDescs = executionTypes, }; } diff --git a/src/PlanViewer.Cli/Commands/QueryStoreCommand.cs b/src/PlanViewer.Cli/Commands/QueryStoreCommand.cs index 989bace..5908ed9 100644 --- a/src/PlanViewer.Cli/Commands/QueryStoreCommand.cs +++ b/src/PlanViewer.Cli/Commands/QueryStoreCommand.cs @@ -129,12 +129,18 @@ public static Command Create(ICredentialService? credentialService = null) Description = "Filter by module name (schema.name, supports % wildcards)" }; + var executionTypeOption = new Option("--execution-type") + { + Description = "Filter by execution type: regular, aborted, exception, or failed (= aborted + exception)" + }; + var cmd = new Command("query-store", "Analyze top queries from Query Store") { serverOption, databaseOption, topOption, orderByOption, hoursBackOption, outputDirOption, outputOption, compactOption, warningsOnlyOption, configOption, authOption, trustCertOption, loginOption, passwordOption, passwordStdinOption, - queryIdOption, planIdOption, queryHashOption, planHashOption, moduleOption + queryIdOption, planIdOption, queryHashOption, planHashOption, moduleOption, + executionTypeOption }; cmd.SetAction(async (parseResult, ct) => @@ -159,6 +165,7 @@ public static Command Create(ICredentialService? credentialService = null) var filterQueryHash = parseResult.GetValue(queryHashOption); var filterPlanHash = parseResult.GetValue(planHashOption); var filterModule = parseResult.GetValue(moduleOption); + var filterExecutionType = parseResult.GetValue(executionTypeOption); // Load .env file if present (CLI args take precedence) var env = ConnectionHelper.LoadEnvFile(); @@ -190,9 +197,22 @@ public static Command Create(ICredentialService? credentialService = null) return; } + string[]? executionTypes; + try + { + executionTypes = QueryStoreFilter.ParseExecutionType(filterExecutionType); + } + catch (ArgumentException ex) + { + Console.Error.WriteLine(ex.Message); + Environment.ExitCode = 1; + return; + } + QueryStoreFilter? filter = null; if (filterQueryId != null || filterPlanId != null || - filterQueryHash != null || filterPlanHash != null || filterModule != null) + filterQueryHash != null || filterPlanHash != null || filterModule != null || + executionTypes != null) { filter = new QueryStoreFilter { @@ -201,6 +221,7 @@ public static Command Create(ICredentialService? credentialService = null) QueryHash = filterQueryHash, QueryPlanHash = filterPlanHash, ModuleName = filterModule, + ExecutionTypeDescs = executionTypes, }; } diff --git a/src/PlanViewer.Core/Models/QueryStorePlan.cs b/src/PlanViewer.Core/Models/QueryStorePlan.cs index f5ef750..60df844 100644 --- a/src/PlanViewer.Core/Models/QueryStorePlan.cs +++ b/src/PlanViewer.Core/Models/QueryStorePlan.cs @@ -18,6 +18,26 @@ public class QueryStoreFilter /// Single value → equality predicate; multiple values (e.g. "Aborted","Exception" for "Failed") → IN predicate. /// public string[]? ExecutionTypeDescs { get; set; } + + /// + /// Parses a user-friendly execution-type string into the matching SQL execution_type_desc values. + /// Accepts (case-insensitive): regular, aborted, exception, failed (= aborted + exception), any. + /// Returns null when input is null, empty, or "any". Throws ArgumentException for unknown values. + /// + public static string[]? ParseExecutionType(string? input) + { + if (string.IsNullOrWhiteSpace(input)) return null; + return input.Trim().ToLowerInvariant() switch + { + "any" => null, + "regular" => ["Regular"], + "aborted" => ["Aborted"], + "exception" => ["Exception"], + "failed" => ["Aborted", "Exception"], + _ => throw new ArgumentException( + $"Unknown execution type '{input}'. Valid values: regular, aborted, exception, failed, any."), + }; + } } public class QueryStorePlan