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: 15 additions & 2 deletions src/PlanViewer.App/Mcp/McpQueryStoreTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ public static async Task<string> 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
{
Expand All @@ -84,9 +85,20 @@ public static async Task<string> 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
{
Expand All @@ -95,6 +107,7 @@ public static async Task<string> GetQueryStoreTop(
QueryHash = query_hash,
QueryPlanHash = plan_hash,
ModuleName = module,
ExecutionTypeDescs = executionTypes,
};
}

Expand Down
25 changes: 23 additions & 2 deletions src/PlanViewer.Cli/Commands/QueryStoreCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,18 @@ public static Command Create(ICredentialService? credentialService = null)
Description = "Filter by module name (schema.name, supports % wildcards)"
};

var executionTypeOption = new Option<string?>("--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) =>
Expand All @@ -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();
Expand Down Expand Up @@ -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
{
Expand All @@ -201,6 +221,7 @@ public static Command Create(ICredentialService? credentialService = null)
QueryHash = filterQueryHash,
QueryPlanHash = filterPlanHash,
ModuleName = filterModule,
ExecutionTypeDescs = executionTypes,
};
}

Expand Down
20 changes: 20 additions & 0 deletions src/PlanViewer.Core/Models/QueryStorePlan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,26 @@ public class QueryStoreFilter
/// Single value → equality predicate; multiple values (e.g. "Aborted","Exception" for "Failed") → IN predicate.
/// </summary>
public string[]? ExecutionTypeDescs { get; set; }

/// <summary>
/// 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.
/// </summary>
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
Expand Down