Skip to content

Expose Query Store execution outcomes in MCP results - #4060

Merged
erikdarlingdata merged 5 commits into
erikdarlingdata:devfrom
kendra-little:query-store-execution-type
Sep 23, 2026
Merged

erikdarlingdata merged 5 commits into
erikdarlingdata:devfrom
kendra-little:query-store-execution-type

Conversation

@kendra-little

Copy link
Copy Markdown
Contributor

Problem

Query Store records whether executions completed normally, were aborted, or ended with an exception, but the MCP read currently blends those populations and omits the outcome.

Value

Callers can isolate aborted and exception executions and see the outcome on every result without increasing broad result limits.

Summary

  • return the Query Store execution outcome with each result
  • add an optional Regular, Aborted, or Exception filter
  • apply the filter after interval deduplication and before ranking
  • preserve separate outcome populations in Lite and Darling
  • expose the filter through the web read surface

Validation

  • focused mixed-outcome Query Store regression coverage
  • Darling and Lite contract coverage
  • Darling and Lite test-project builds
  • public disclosure scan over code, history, branch, commit text, and this PR text

kendra-little and others added 3 commits September 23, 2026 18:40
Review fixes on top of the execution-outcome filter:

- A filter that matches nothing while the unfiltered read has rows now
  answers `empty` on both SKUs, instead of falling through to "Query Store
  may not be enabled". Most queries never abort, so this was the common
  answer to an Aborted or Exception filter.
- The filter validates through McpHelpers.ValidateChoice against a shared
  QueryStoreExecutionTypes set, replacing the two copies of an "INVALID"
  sentinel normalizer.
- The filter moves into the dedup CTE, ahead of the ROW_NUMBER. The outcome
  is in the partition, so the result is identical and the window sort only
  sees the outcome asked for.
- Lite keeps MAX(module_name). Grouping on it could split one plan into two
  rows when the object name resolved differently between collections.
- The Darling viewer's grid groups by execution_type_desc too, so it matches
  Lite's grid (which this PR already split) and the twinned MCP read.
- Tests: a Lite case with one plan and two outcomes in one interval, and
  live Darling assertions for the split, the filter, the empty answer, the
  refusal and the viewer grid.
@erikdarlingdata
erikdarlingdata force-pushed the query-store-execution-type branch from c4501bd to 1cbcafd Compare September 23, 2026 20:49
@erikdarlingdata

erikdarlingdata commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

I pushed a review commit on top of yours rather than leaving a list. The first half of it is the refusal you already fixed; here is the rest.

A filtered miss said Query Store may be off. Most queries never abort, so execution_type: "Aborted" returns nothing far more often than not. Both SKUs then ran the not-collected chain and ended at unavailable, "Query Store may not be enabled", which the unfiltered rows prove false. Now, when the filter matches nothing, one unfiltered top-1 read decides: if it has rows, the answer is empty with a sentence saying it is a measured zero (McpHelpers.QueryStoreExecutionTypeEmpty, shared so both SKUs word it the same). Otherwise the existing chain runs unchanged. The extra read happens only on that path.

Validation uses the house idiom. McpHelpers.ValidateChoice against a shared McpHelpers.QueryStoreExecutionTypes, then the canonical spelling downstream, the same way object_kind and status do it. That replaces the two copies of the "INVALID" sentinel normalizer, and the refusal now lists the accepted values in the standard wording.

Both grids now split by outcome, not just Lite's. GetQueryStoreTopQueriesAsync in Lite also feeds the Lite Query Store grid, so this PR already split that grid by outcome. The Darling viewer's grid still took MAX(execution_type_desc), which shows "Regular" for any mixed group (it sorts last) while the averages blend a timeout's duration into the plan's normal cost. So the two apps' grids disagreed, and the MCP reader's "twins the viewer" comment stopped being true. The viewer now groups on it too, and its pin in ViewerQueriesTests moved with it. One known gap is left: the grid's drill-downs (slicer overlay, history window) are still per query and plan, so opening an Aborted row shows all outcomes.

Lite keeps MAX(module_name). module_name is resolved at collection time, with 'Unknown' as the fallback, so grouping on it splits one plan into two rows whenever the name resolved differently between collections (a rename, a drop, one failed lookup).

The filter runs before the dedup's ROW_NUMBER. execution_type_desc is in the partition, so filtering first gives the same rows, and the window sort sees only the outcome asked for. The same change is made on both SKUs.

Tests. The new Lite test seeded three different queries with one outcome each, so it could not see a blended plan. TopQueries_OnePlanWithTwoOutcomes_ReturnsOneRowPerOutcome seeds one plan and one interval with 100 Regular and 3 Aborted executions, and asserts two rows with their own counts and averages, plus the filtered read. On Darling, the live data-tools test now plants the same pair and checks the unfiltered split, a lower-case filter, the empty answer, the refusal's hints.parameter, and the viewer grid.

Budget ceiling. Your per-parameter lines were right. On the old base the tools/list total also went 124 bytes over its ceiling, and my first commit raised it. That raise is gone: dev's #3898 conversions moved this tool's reading guidance off tools/list, so after merging dev both SKUs fit under dev's own ceilings (Darling measures 251,004 bytes against 329,494). The per-outcome behaviour is stated in the reading guide instead, after the <<GUIDE>> marker, where it costs no budget.

Merge. dev is merged in (b56adea73). It merged without conflicts; the only follow-up was the budget and guide change above (c077d2258).

Verified locally: Darling.Tests and Lite.Tests both build with 0 warnings and 0 errors. These Darling classes pass:

Class Tests
DarlingMcpDataToolsSurfaceAndSqlTests 62
DarlingMcpDataToolsLivePostgresTests (live, against TimescaleDB) 1
McpToolsListBudgetTests 5
McpPayloadContractCensusTests 68
McpPageContractTests 43
DarlingCustomViewsTests 71
QueryStoreTopWindowTests 5
DocCommentHygieneTests 77
McpZeroIsAMeasurementTests 20
ViewerQueriesSqlTests 32
DarlingComposeTests 280
the runtime-precondition classes, live included 39

Not verified locally:

  • Lite.Tests, including the new Lite test, runs in CI only.
  • Two ViewerQueriesLivePostgresTests cases fail here only because WPF's PresentationCore does not load on macOS, so the live viewer-dedup case rides CI.

Suggested for the PR body (the combined CHANGELOG PR reads it from there):

CHANGELOG entry

  • get_query_store_top (Darling and Lite) returns each row's Query Store execution outcome and takes an optional execution_type filter (Regular, Aborted or Exception). Both Query Store grids now show one row per outcome instead of blending aborted executions into a plan's averages.

Dev's erikdarlingdata#3898 conversions moved get_query_store_top's reading guidance off
tools/list, so the new execution_type parameter now fits under both SKUs'
existing tools/list ceilings (Darling measures 251,004 bytes against
329,494). The +124-byte ceiling raises from the review commit are dropped
in favour of dev's values.
The sentence rides the reading guide on both SKUs, after the <<GUIDE>>
marker, so the tools/list head and its budget are unchanged.
@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 23, 2026 21:15
@erikdarlingdata
erikdarlingdata merged commit d1bd879 into erikdarlingdata:dev Sep 23, 2026
16 checks passed
erikdarlingdata added a commit to kendra-little/PerformanceMonitor that referenced this pull request Sep 23, 2026
erikdarlingdata#4060's execution_type

erikdarlingdata#4060 gave get_query_store_top an execution_type filter on the same read, row
type, parameters and filtered-miss path. The two are combined:

- SQL: execution_type stays $6, filtered in the dedup CTE as erikdarlingdata#4060 placed it;
  module_name becomes $7, still applied after the dedup and before the ranking.
  Rows carry execution_type and module_name; Darling's QueryStoreRow reader
  maps all 17 columns in projection order. Lite binds the outcome, then the
  module, after the database list.
- Parameters, both SKUs: ..., as_of, execution_type, module_name (the parity
  theory, the web catalog and the dispatch follow).
- A filtered miss takes erikdarlingdata#4060's test for both filters: the same read without
  the filters, scoped to the database. It replaces this PR's server-wide window
  floor on Darling and Lite's HasQueryStoreRowsInWindowAsync, which is removed;
  erikdarlingdata#4060's probe answers the same question, scoped to the database the read
  was. An execution_type-only miss keeps erikdarlingdata#4060's message word for word; a miss
  with module_name goes through the new shared McpHelpers.QueryStoreModuleEmpty,
  which names both filters, and on Darling still carries the served window as
  hints.
- The tool-guide tails (after <<GUIDE>>, so the budgeted heads are dev's)
  describe both filters and the one miss rule.
- Tests: the Darling live pin gains a module plus execution_type miss; Lite
  gains a database + outcome + module binding test, and loses the removed
  probe's test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants