You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[BUG] - MCP Server get_query_trend silently truncates to raw-table retention window, ignoring requested hours_back #2353
Microsoft SQL Server 2022 (RTM-CU16) (KB5048033) - 16.0.4165.4 (X64) Nov 6 2024 19:24:49 Copyright (C) 2022 Microsoft Corporation Standard Edition (64-bit) on Windows Server 2022 Datacenter 10.0 (Build 20348: ) (Hypervisor)
Windows Version
Windows Server 2025 Datacenter, 2009, 26100
Describe the Bug
The get_query_trend MCP tool accepts an hours_back parameter and echoes it back unchanged in its JSON response, but its underlying query only reads the raw query_stats hypertable (DarlingTrendReader.QueryHistorySql), which is physically pruned by a hardcoded 4-day TimescaleDB retention policy (TimescaleSupport.RawRetentionInterval = "4 days") — independent of and much shorter than the collector's advertised 30-day query_stats retention (CollectorScheduleDefaults).
Unlike other Darling readers (e.g. ComposeSourceRouter), get_query_trend has no fallback to the query_stats_hourly / query_stats_daily continuous aggregates, so any request for more than ~4 days of history silently returns only whatever raw data hasn't aged out yet, with no indication in the response that the window was truncated.
Steps to Reproduce
Identify a query_hash with execution history older than 4 days (via get_top_queries_by_cpu or get_query_store_top).
Call get_query_trend with that query_hash, its database_name, server_name, and hours_back=168 (7 days).
Inspect the returned trend array's collection_time values (min/max).
Expected Behavior
The trend array should span the full requested 168-hour (7-day) window — or, if the true retention horizon is genuinely shorter, the response should clearly indicate the effective window actually served (e.g. an effective_hours_back / clamped flag) rather than silently returning less data than requested while echoing hours_back: 168 unchanged.
Actual Behavior
The response reports "hours_back": 168 but the trend array only contains data points spanning ~54 hours — the actual surviving span of the raw query_stats table, not the requested window. There is no field in the response signaling that the data was truncated or that only a subset of the requested window was returned. For comparison, get_query_store_top with the same hours_back=168 correctly returns data spanning the full 7 days, because it reads Query Store's own interval-identity data rather than the raw per-poll query_stats deltas.
Error Messages / Log Output
N/A
Screenshots
No response
Additional Context
Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingTrendReader.cs ( QueryHistorySql / GetQueryHistoryAsync ), invoked from GetQueryTrend in Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpTrendTools.cs
Confirmed, and your diagnosis is correct in every particular. I checked each claim against the code rather than reproducing from the description:
TimescaleSupport.RawRetentionInterval = "4 days" — TimescaleSupport.cs:1678, with RawRetentionSpan = TimeSpan.FromDays(4) alongside it.
DarlingTrendReader.QueryHistorySql reads the raw table only. Its FROM clauses are query_stats (lines 283 and 339) with no query_stats_hourly / query_stats_daily anywhere in the file.
The rollups exist and something else already routes to them.ComposeCaggCatalog (Compose/ComposeSourceRouter.cs:103) maps query_stats to query_stats_hourly / query_stats_daily, and the router picks a tier "by the AGE of the window's oldest point, never by the display grain."
So the tool advertises a window it structurally cannot serve, and the tier that could serve it is already built, already populated, and already reachable from a router twenty lines away.
You undersold it — it does not just omit, it asserts
The empty path is worse than silent truncation (DarlingMcpTrendTools.cs:227):
returnMcpHelpers.Status("empty",$"No history found for query_hash '{query_hash}' in database '{database_name}' within the last {hours_back} hours.");
For a query whose history aged out of the raw tier but is sitting in the hourly CAGG, this does not return a short array — it returns "No history found ... within the last 168 hours", which is a false statement rather than an incomplete one. An agent reading that concludes the query did not run. That is the worst possible failure for a monitoring tool: not "I have less than you asked for", but "it did not happen."
That distinction is why this is a bug rather than a documentation gap, and it is the part I would fix first.
Scope: it is this one reader, and you found the right one
Worth stating so nobody widens this unnecessarily. Only the three rolled tables drop at the 4-day raw horizon — TimescaleSupport.cs:2130 says so directly, and ComposeCaggCatalog lists exactly query_stats, procedure_stats, query_store_stats. The other readers in DarlingTrendReader go through v_memory_stats / v_perfmon_stats / v_file_io_stats, which are plain passthroughs (CREATE OR REPLACE VIEW v_memory_stats AS SELECT * FROM memory_stats) over tables that keep their full configured retention. They read raw too, but their raw is the whole horizon, so they are unaffected.
get_query_trend is the only tool in that file touching a rolled table's raw tier. You picked the single affected one out of five.
The fix
Two parts, and I want both rather than either:
Route through the existing tiering. The query_stats CAGG carries database_name, query_hash and object_name as dimensions — get_query_trend filters on database_name and query_hash, both of which are in that set. So the aggregate can actually serve this query; this is wiring an existing router in, not building a new path.
Report the window actually served. Even tiered, there is still a horizon (the daily CAGGs eventually end), so honesty cannot be a side effect of having enough data. The response should carry the effective window alongside the requested one, and the empty case must never claim absence over a span it did not read. Your suggested effective_hours_back is the right shape.
Part 2 is small and removes the false statement; part 1 makes the data actually appear. Part 2 without part 1 would be honest and still unhelpful, which is why both.
Timing
Not in 3.5.0 — that build is in soak and I am not widening it. This lands on dev after the tag.
One note on your version field: you reported 3.4.0, and I confirmed this is still present on dev today, so nothing about it is fixed in the meantime. Thank you for tracing it to the SQL rather than stopping at "the numbers look short" — the ComposeSourceRouter comparison is what made this a ten-minute confirmation instead of an afternoon.
I'm not sure if this goes hand in hand with the above changes but I'm also encountering a query timeout when the MCP server runs some agg queries against the query_store data collected. I can see that's it hardcoded to 15s in PerformanceMonitor.Darling.Service\Compose\ComposeSpec.cs. Would you prefer a separate issue raised asking for the timeout to be increased (if possible)?
Separate issue, and I have raised it for you: #2357. No need to file it — you have already done the hard part by finding the constant.
You are right about the location. ComposeSpec.StatementTimeout = "15s" is the per-session statement_timeout on the viewer and mcp roles, and it is a genuinely different code path from this one, so it would not have been fixed alongside it.
Two things I found while confirming it that are worth knowing now:
Bumping the constant would not have helped you. It is applied in role PROVISIONING DDL — DarlingManagedRoles and tools/provision-roles.sql — deliberately not a versioned migration, because a role's statement_timeout has no probeable schema footprint and tying it to the schema version would break the viewer's connect-time version gate. Your roles already exist with 15s baked in, so a new build alone would change nothing. Any real fix has to re-apply it, and that is now part of #2357 rather than something you would have discovered the hard way.
I would rather find out why it is slow before making the ceiling higher.ComposeSourceRouter already tiers compose panels raw → hourly → daily by the age of the window's oldest point. A panel timing out over a wide window is exactly what a tier that failed to route looks like — and that would be a bug in the same family as the one you reported here, where the fix is routing rather than waiting longer.
So if you can add to #2357 which tool or panel timed out and roughly the window width you asked for, that tells us which of the two it is. If it turns out to be genuinely heavy work on a big store, the constant still should not be a constant — that part I am convinced of either way.
Fixed in #2356. Thanks again @carlei1978 — your follow-up about the compose timeout is tracked separately as #2357.
get_query_trend now routes to the hourly rollup when the window reaches past the raw tier's 4-day horizon, and the response carries source, effective_hours_back, bucket and truncated so it describes the data rather than echoing the request. The empty path no longer asserts absence over a span it never read.
Two things worth recording. Columns the rollup does not carry come back null, never zero — on an aggregate row a zero reads as "none observed", a measurement nobody made. And writing the test caught the tiering rule being wrong on the first pass: comparing the window start against the window's END makes a two-hour window from ten days ago look recent — it is recent relative to its own end — and routes it to a tier that dropped those rows six days earlier. Retention answers to wall clock, so wall clock is the parameter.
Activity
erikdarlingdata commentedon Aug 19, 2026
Confirmed, and your diagnosis is correct in every particular. I checked each claim against the code rather than reproducing from the description:
TimescaleSupport.RawRetentionInterval = "4 days"—TimescaleSupport.cs:1678, withRawRetentionSpan = TimeSpan.FromDays(4)alongside it.DarlingTrendReader.QueryHistorySqlreads the raw table only. ItsFROMclauses arequery_stats(lines 283 and 339) with noquery_stats_hourly/query_stats_dailyanywhere in the file.ComposeCaggCatalog(Compose/ComposeSourceRouter.cs:103) mapsquery_statstoquery_stats_hourly/query_stats_daily, and the router picks a tier "by the AGE of the window's oldest point, never by the display grain."So the tool advertises a window it structurally cannot serve, and the tier that could serve it is already built, already populated, and already reachable from a router twenty lines away.
You undersold it — it does not just omit, it asserts
The empty path is worse than silent truncation (
DarlingMcpTrendTools.cs:227):For a query whose history aged out of the raw tier but is sitting in the hourly CAGG, this does not return a short array — it returns "No history found ... within the last 168 hours", which is a false statement rather than an incomplete one. An agent reading that concludes the query did not run. That is the worst possible failure for a monitoring tool: not "I have less than you asked for", but "it did not happen."
That distinction is why this is a bug rather than a documentation gap, and it is the part I would fix first.
Scope: it is this one reader, and you found the right one
Worth stating so nobody widens this unnecessarily. Only the three rolled tables drop at the 4-day raw horizon —
TimescaleSupport.cs:2130says so directly, andComposeCaggCataloglists exactlyquery_stats,procedure_stats,query_store_stats. The other readers inDarlingTrendReadergo throughv_memory_stats/v_perfmon_stats/v_file_io_stats, which are plain passthroughs (CREATE OR REPLACE VIEW v_memory_stats AS SELECT * FROM memory_stats) over tables that keep their full configured retention. They read raw too, but their raw is the whole horizon, so they are unaffected.get_query_trendis the only tool in that file touching a rolled table's raw tier. You picked the single affected one out of five.The fix
Two parts, and I want both rather than either:
query_statsCAGG carriesdatabase_name,query_hashandobject_nameas dimensions —get_query_trendfilters ondatabase_nameandquery_hash, both of which are in that set. So the aggregate can actually serve this query; this is wiring an existing router in, not building a new path.effective_hours_backis the right shape.Part 2 is small and removes the false statement; part 1 makes the data actually appear. Part 2 without part 1 would be honest and still unhelpful, which is why both.
Timing
Not in 3.5.0 — that build is in soak and I am not widening it. This lands on
devafter the tag.One note on your version field: you reported 3.4.0, and I confirmed this is still present on
devtoday, so nothing about it is fixed in the meantime. Thank you for tracing it to the SQL rather than stopping at "the numbers look short" — theComposeSourceRoutercomparison is what made this a ten-minute confirmation instead of an afternoon.carlei1978 commentedon Aug 19, 2026
Thank you :)
I'm not sure if this goes hand in hand with the above changes but I'm also encountering a query timeout when the MCP server runs some agg queries against the query_store data collected. I can see that's it hardcoded to 15s in PerformanceMonitor.Darling.Service\Compose\ComposeSpec.cs. Would you prefer a separate issue raised asking for the timeout to be increased (if possible)?
erikdarlingdata commentedon Aug 19, 2026
Separate issue, and I have raised it for you: #2357. No need to file it — you have already done the hard part by finding the constant.
You are right about the location.
ComposeSpec.StatementTimeout = "15s"is the per-sessionstatement_timeouton theviewerandmcproles, and it is a genuinely different code path from this one, so it would not have been fixed alongside it.Two things I found while confirming it that are worth knowing now:
Bumping the constant would not have helped you. It is applied in role PROVISIONING DDL —
DarlingManagedRolesandtools/provision-roles.sql— deliberately not a versioned migration, because a role'sstatement_timeouthas no probeable schema footprint and tying it to the schema version would break the viewer's connect-time version gate. Your roles already exist with 15s baked in, so a new build alone would change nothing. Any real fix has to re-apply it, and that is now part of #2357 rather than something you would have discovered the hard way.I would rather find out why it is slow before making the ceiling higher.
ComposeSourceRouteralready tiers compose panels raw → hourly → daily by the age of the window's oldest point. A panel timing out over a wide window is exactly what a tier that failed to route looks like — and that would be a bug in the same family as the one you reported here, where the fix is routing rather than waiting longer.So if you can add to #2357 which tool or panel timed out and roughly the window width you asked for, that tells us which of the two it is. If it turns out to be genuinely heavy work on a big store, the constant still should not be a constant — that part I am convinced of either way.
The fix for this issue is up as #2356.
Serve get_query_trend from the tier that can answer, and say which on…
Serve get_query_trend from the tier that can answer, and say which on…
erikdarlingdata commentedon Aug 19, 2026
Fixed in #2356. Thanks again @carlei1978 — your follow-up about the compose timeout is tracked separately as #2357.
get_query_trendnow routes to the hourly rollup when the window reaches past the raw tier's 4-day horizon, and the response carriessource,effective_hours_back,bucketandtruncatedso it describes the data rather than echoing the request. The empty path no longer asserts absence over a span it never read.Two things worth recording. Columns the rollup does not carry come back null, never zero — on an aggregate row a zero reads as "none observed", a measurement nobody made. And writing the test caught the tiering rule being wrong on the first pass: comparing the window start against the window's END makes a two-hour window from ten days ago look recent — it is recent relative to its own end — and routes it to a tier that dropped those rows six days earlier. Retention answers to wall clock, so wall clock is the parameter.
Report the window get_query_store_top actually served
Report the window get_query_store_top actually served
Report the window get_query_store_top actually served (#2364) (#2367)
Give the web a per-query drill-down, which is what makes get_query_tr…
Lite's get_query_trend carries the disclosure block its Darling twin …
The trend family's window floor is window_truncated on both SKUs, not…