Skip to content

Add exact module filtering to Query Store MCP results - #4057

Merged
erikdarlingdata merged 8 commits into
erikdarlingdata:devfrom
kendra-little:query-store-module-filter-dev
Sep 23, 2026
Merged

erikdarlingdata merged 8 commits into
erikdarlingdata:devfrom
kendra-little:query-store-module-filter-dev

Conversation

@kendra-little

@kendra-little kendra-little commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Query Store results are ranked globally, so callers cannot reliably investigate a specific stored-procedure module when its queries fall outside the top results. The response also omits the module attribution already present in collected data.

Value

Callers can retrieve exact module-scoped Query Store history without increasing broad result limits, while retaining query and plan identity for follow-up analysis.

Summary

  • add an optional exact module-name filter to the Query Store MCP read
  • deduplicate cumulative interval snapshots before applying the filter
  • apply the filter before cost ranking and result limits
  • return module attribution with each result
  • keep the existing inexpensive, unfiltered retention-floor probe
  • preserve existing unfiltered behavior across the Lite and Darling surfaces

Validation

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

CHANGELOG entry

kendra-little and others added 4 commits September 23, 2026 18:01
…match

A module filter that matched nothing answered "empty" (the house word for a
true negative) even when the window held no Query Store rows at all, where the
filter is not the reason and the unfiltered read answers "unavailable". The
Darling message also told the caller to read effective_start, which the status
envelope did not carry.

- Darling: decide on the window floor the tool already probes. No floor keeps
  the unfiltered "unavailable"; otherwise "empty" with effective_start,
  effective_hours_back and window_truncated as hints, plus a sentence when the
  raw tier did not reach the whole window.
- Lite: the same decision through HasQueryStoreRowsInWindowAsync, a
  window-scoped twin of HasAnyQueryStoreStatAsync, asked only on this miss.
- Live pin on Darling in McpFilterSemanticsLivePostgresTests (the A13
  filter-in-the-query precedent): rank at top = 1, dedup of a re-collected
  interval, the miss's hints, and the empty-window "unavailable". Negative
  control: the old condition answers "empty" there. Lite pins the probe.
- McpPayloadContractCensusTests rosters the miss as the second window-floor
  publisher in DarlingMcpDataTools.cs.
…d its docs match the code

- Both heads go back to dev's text with one change, "Supports database and
  module filtering." (+11 chars, pinned deliberately). The ordering guidance
  lives in the parameter description, which is now 186 characters: the exact
  schema-qualified name, as get_top_procedures_by_cpu returns it in full_name,
  case-sensitive, applied before ranking, and ad-hoc statements are Adhoc.
- McpToolsListBudget pins the new parameter on both SKUs. The total ceilings
  rise deliberately with a erikdarlingdata#4057 change-log line: Darling by the measured 246
  bytes, Lite by the same 246 (the identical change through the shared schema
  code; Lite's runner cannot execute on the macOS rig, so CI confirms it).
- DarlingDataReader: the floor probe's doc comment no longer says "filtered"
  (the probe is deliberately unfiltered), and QueryStoreTopSql's parameter list
  names $5 and $6.
@erikdarlingdata

Copy link
Copy Markdown
Owner

Thanks for this. The filter is in the right place on both SKUs: after the interval dedup and before the ranking and the cap. A live check against PostgreSQL returned a cooler target at top = 1, with its re-collected interval counted once.

I pushed two commits to this branch (0739144d5, 777cd9801), so please pull before pushing again.

  • A miss is empty only when there was something to miss. A module_name filter that matched nothing answered empty, the house word for a true negative, even when the window held no Query Store rows at all. In that case the filter isn't the reason, and the unfiltered read answers unavailable. The Darling message also pointed at effective_start, which the status envelope didn't carry.
    • Darling now decides on the window floor it already probes, and the miss carries effective_start, effective_hours_back and window_truncated as hints.
    • Lite asks a window-scoped HasQueryStoreRowsInWindowAsync, only on the miss.
  • A live Darling pin in McpFilterSemanticsLivePostgresTests, beside the parallel_only one it follows. It covers the rank at top = 1, the dedup, the miss's hints, and the empty-window answer. It goes red with the old condition.
  • The tools/list budget. The heads went back to dev's text, plus "Supports database and module filtering." The guidance moved into a shorter parameter description (the name as get_top_procedures_by_cpu returns it in full_name, with Adhoc for ad-hoc statements). The new parameter is pinned and the totals are raised with a change-log line on both SKUs. McpToolsListBudgetTests would otherwise have failed on both.
  • Doc comments. The floor probe's doc no longer says "filtered", and QueryStoreTopSql names $5 and $6.
  • Changelog. A ## CHANGELOG entry is added to the description; the release changelog is built from those.

AsOfWindowAnchorTests.EveryToolThatTakesTheAnchor_ActuallyUsesIt requires every LocalDataService read an anchored tool makes to pass asOfUtc by name, so a read that could take the anchor and is not given it cannot pass unseen. The probe was given windowEnd positionally, which answered for the right window but read as a dropped anchor. Named now. AsOfWindowAnchorTests: 30/30.
dev's erikdarlingdata#4065 (erikdarlingdata#3898) converted get_query_store_top to a two-tier description:
a 422-char head, byte-identical on both SKUs, and the old text as the
get_tool_guide tail. The four conflicts were that description and its budget
pin on each SKU.

- The head is taken from dev unchanged, so its pins (McpToolGuideHeads) and
  budget line stand as dev set them.
- The module_name guidance goes in the tail, which tools/list does not serve:
  "Supports database filtering." becomes the module_name rules (exact,
  case-sensitive, schema-qualified, Adhoc / Unknown, after dedup and before
  ranking) and how a miss answers.
- The tools/list total ceilings go back to dev's values on both SKUs, with this
  PR's change-log lines removed. After dev's conversions the Darling total
  measures 251,115 bytes against 329,494, so this PR no longer needs a raise.
  The only budget change left is the module_name parameter's pin line.
… reads

HasQueryStoreRowsInWindowAsync sat beside HasAnyQueryStoreStatAsync in LocalDataService.QueryStats.cs, and its FROM v_query_store_stats made that file's row types read as able to reach Query Store. ConsumedTimestampFrameDisciplineTests resolves a render site's column through the relations its file reads, so three timestamp columns that query_stats and query_store_stats share in different clock frames stopped resolving: judged render sites fell from 43 to 40 (CI: build and Darling PG shard 0). The probe moves to LocalDataService.QueryStore.cs, which already reads the view; QueryStats.cs is back to dev byte for byte. The move is proved by bisect: with only QueryStats.cs from this PR the census judged 40, with it at dev 43.

The QueryStore.cs source censuses do not move: 6 dedup partitions, 8 rn filters, no execution_count-first ORDER BY, 2 COALESCE buckets, and the probe says it is deliberately not deduped. ConsumedTimestampFrameDisciplineTests 17/17, AsOfWindowAnchorTests 30/30.
@erikdarlingdata

Copy link
Copy Markdown
Owner

Merged current dev into the branch and fixed two CI failures. The branch is now at a90b0b27f, so please pull before pushing again.

  • Conflicts. get_collection_log and get_query_store_top reading guides move off tools/list (#3898) #4065 had moved get_query_store_top to a two-tier description (The MCP surface costs ~100k tokens of context before the first call: 157 tools, 336 KB of tools/list, 86 KB of instructions #3898). The head is dev's, unchanged. The module_name rules and how a miss answers now sit in the tail that get_tool_guide serves. That makes my earlier note out of date: after dev's conversions, neither SKU's tools/list total needs raising. The only budget change left is the module_name parameter's pin line.
  • AsOfWindowAnchorTests. The Lite miss probe now takes asOfUtc: by name, as the census requires of every anchored read.
  • ConsumedTimestampFrameDisciplineTests (judged render sites fell from 43 to 40). The probe had been placed in LocalDataService.QueryStats.cs, so that file now appeared to read v_query_store_stats. That left three timestamp columns shared across the two tables unresolved. The probe now lives in LocalDataService.QueryStore.cs beside the other Query Store reads, and QueryStats.cs is back to dev. A bisect confirmed that one file was the cause.

The full Darling suite ran against live PostgreSQL at dev and at the merged head. The only difference is that render census, which this push fixes.

@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 23, 2026 21:15
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.
@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 23, 2026 21:46
@erikdarlingdata

Copy link
Copy Markdown
Owner

dev moved again, and #4060 (your execution_type filter) had landed on the same read, so I merged dev in and combined the two filters. The branch is now at 1e939b7af.

  • SQL: execution_type stays $6, in the dedup CTE where Expose Query Store execution outcomes in MCP results #4060 put it. module_name is $7, after the dedup and before the ranking. Rows carry both fields. Lite binds the outcome, then the module, after the database list, and a new Lite test covers all three together.
  • Parameters: …, as_of, execution_type, module_name on both SKUs.
  • A filtered miss: it now uses Expose Query Store execution outcomes in MCP results #4060's test for both filters, the same read without the filters scoped to the database, instead of this PR's server-wide window check. Lite's HasQueryStoreRowsInWindowAsync is gone. An execution_type-only miss keeps your message word for word. A miss with module_name goes through a new shared McpHelpers.QueryStoreModuleEmpty, which names both filters and, on Darling, still carries the window read as hints.

The full Darling suite against live PostgreSQL fails exactly the same tests at dev and at this head. Auto-merge is armed on this commit.

@erikdarlingdata
erikdarlingdata merged commit 609c224 into erikdarlingdata:dev Sep 23, 2026
17 of 18 checks passed
erikdarlingdata added a commit that referenced this pull request Sep 23, 2026
…4057) (#4084)

TopQueries_ModuleFilterRunsAfterIntervalDedup had no [Fact] attribute, so xUnit never ran it and
the Lite.Tests build warned xUnit1013. Adding the attribute makes it run. It passes.


Claude-Session: https://claude.ai/code/session_014nCo36doEcUWapnXbJvY7L

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
erikdarlingdata added a commit that referenced this pull request Sep 24, 2026
* Add 48 buffered CHANGELOG entries in one splice

Adds 48 entries and 48 link refs (#4057, #4077, #4079, #4081, #4082, #4083, #4084, #4085, #4086, #4087, #4088, #4089, #4090, #4091, #4092, #4093, #4095, #4096, #4099, #4100, #4101, #4103, #4105, #4106, #4107, #4108, #4109, #4110, #4111, #4113, #4114, #4115, #4116, #4117, #4118, #4119, #4120, #4121, #4122, #4123, #4124, #4125, #4126, #4127, #4131, #4133, #4136, #4141). Each PR's entry was buffered, and this lands every entry whose PR was merged on origin/dev when it ran.

Entries found under a bare section line (none unless the old script ran again) move into the matching ### section, below the new entries.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC

* Changelog splice: add #4137's entry, which merged before the splice but was missed

#4137 (pg_plan_capture reads csvlog) merged at 05:57Z with a CHANGELOG entry section in its
body. It was neither spliced nor listed as needing no entry. Its entry goes under Fixed, right
after #4136's, and #4136's last sentence now points to it instead of saying plan capture is
unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUU29PuGg9TUFBsACgZg2K

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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