Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b9ec66f
Keep the latest Query Store snapshot per interval as it is written (#…
erikdarlingdata Sep 23, 2026
e916041
PLAN_REGRESSION and its drill-down read the interval table where its …
erikdarlingdata Sep 23, 2026
fdfa460
The interval table and its pending rows purge at 15 days (#3953)
erikdarlingdata Sep 23, 2026
75a2728
Regressed queries carry the best plan's last run, and the force bot s…
erikdarlingdata Sep 23, 2026
b085c46
PLAN_REGRESSION says how old the faster plan is, and which source it …
erikdarlingdata Sep 23, 2026
c5e46db
Pin that nothing inside the Query Store batch transaction reads raw e…
erikdarlingdata Sep 23, 2026
2cf2368
Merge remote-tracking branch 'origin/dev' into fix/3953-qs-interval-l…
erikdarlingdata Sep 23, 2026
f6075fb
V140 carries the top-rung pins, and raw Query Store rows have one wri…
erikdarlingdata Sep 23, 2026
b86bc24
Merge origin/dev into fix/3953-qs-interval-latest, renumber V140 to V142
erikdarlingdata Sep 25, 2026
ec6c872
Fix two tests the merge's V140->V142 renumbering broke
erikdarlingdata Sep 25, 2026
ff6a368
Fix ViewerCollectionCaveatsGateTests for the new V142 top rung
erikdarlingdata Sep 25, 2026
41c581f
Renumber V142->V143 (conflict with #4216); CHANGELOG mentions MaxBest…
erikdarlingdata Sep 25, 2026
e2bf4f1
Trigger CI: Build did not run after V143 renumber
erikdarlingdata Sep 25, 2026
fc1055f
Tighten ReadBody end marker to exclude CollectConfigAuditFactsAsync s…
erikdarlingdata Sep 25, 2026
7459058
Revert "Tighten ReadBody end marker to exclude CollectConfigAuditFact…
erikdarlingdata Sep 25, 2026
b2efc9f
ci: trigger build on current HEAD
erikdarlingdata Sep 25, 2026
546376b
Merge dev: add migration 142 (index-object-stats-server-time) in orde…
erikdarlingdata Sep 25, 2026
43aba60
Fix duplicate collectionCaveatsOrdinal: remove const shadowing the va…
erikdarlingdata Sep 25, 2026
5f73485
Fix V142 and V143 rung tests after dev merge added V142 between them
erikdarlingdata Sep 25, 2026
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
2 changes: 1 addition & 1 deletion Darling/Darling.Tests/CollectionCaveatsRungTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public void TheRungCreatesOneTable_WithACompositePrimaryKey_AndNothingElse()

/// <summary>
/// The viewer probe's three sites carry this rung's sentinel at its own ordinal, and the map's arm for it
/// returns 141 — the top arm, since nothing has landed on top of it yet.
/// returns 141 — a position within the signature, not its end, now that V143 has appended its own.
/// </summary>
[Fact]
public void TheProbeMapsAStoreStoppedHereToThisRung()
Expand Down
14 changes: 10 additions & 4 deletions Darling/Darling.Tests/DarlingAnalysisPipelineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,10 @@ public void DrillDown_AllSql_CoversEveryQuery_TwoForTheSpikeMethod()
/* 17 collect methods, one query each, except CollectQueriesAtSpike (peak lookup +
queries-at-peak = 2). The reconstructed-chain method's DMV-snapshot fallback runs
through the shared PgBlockingPairRowQuery.DmvSnapshotSql, already pinned in the
fact-collector suite. */
Assert.Equal(LiteDrillDownMethodSurface.Length + 1, PgDrillDownCollector.AllSql.Count);
fact-collector suite. Plus the regressed-queries read's #3953 table twin, which Lite has no
store for: its DuckDB keeps no latest-snapshot interval table. */
Assert.Equal(LiteDrillDownMethodSurface.Length + 2, PgDrillDownCollector.AllSql.Count);
Assert.Contains(PgDrillDownCollector.RegressedQueriesTableSql, PgDrillDownCollector.AllSql);
}

[Fact]
Expand Down Expand Up @@ -211,11 +213,15 @@ public void DrillDown_AllSql_AnyValue_OnlyInTheRegressedQueriesQuery()
It is deliberate in the plan-regression re-detection and nowhere else — the same
confinement the fact collector pins for its PlanRegressionSql. */
Assert.Contains("any_value(query_text)", PgDrillDownCollector.RegressedQueriesSql, StringComparison.Ordinal);
Assert.Contains("any_value(query_text)", PgDrillDownCollector.RegressedQueriesTableSql, StringComparison.Ordinal);
foreach (var sql in PgDrillDownCollector.AllSql)
{
if (sql.Contains("any_value", StringComparison.OrdinalIgnoreCase))
{
Assert.Equal(PgDrillDownCollector.RegressedQueriesSql, sql);
/* The two regressed-queries reads (#3953: raw, and its interval-table twin), and nothing else. */
Assert.True(
sql == PgDrillDownCollector.RegressedQueriesSql || sql == PgDrillDownCollector.RegressedQueriesTableSql,
"any_value() outside the regressed-queries reads:\n" + sql);
}
}
}
Expand All @@ -240,7 +246,7 @@ statement text moved out of the fact row and has to be resolved back. Sourced fr
against a table that no longer exists. query_text_dim is the second (#3902): the
parameter-sensitivity drill-down resolves text for its five output rows by digest rather than
reading v_query_stats, whose join resolves it for every row in the window. */
var sideTables = new[] { QueryStoreTextStore.TableName, PayloadDimensions.QueryTextDimTable }
var sideTables = new[] { QueryStoreTextStore.TableName, PayloadDimensions.QueryTextDimTable, QueryStoreIntervalLatest.TableName }
.Select(t => t.Contains('.', StringComparison.Ordinal) ? t.Split('.')[^1] : t)
.ToHashSet(StringComparer.Ordinal);

Expand Down
99 changes: 99 additions & 0 deletions Darling/Darling.Tests/ForcePlanBotPolicyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

using System;
using System.Collections.Generic;
using System.Linq;
using PerformanceMonitor.Analysis;
using Xunit;
Expand Down Expand Up @@ -412,4 +413,102 @@ public void Normalize_ClampsTheValuesThatWouldDisarmTheCooldowns()
Assert.True(reckless.Enabled);
Assert.False(reckless.DryRun);
}
/* ---------------- #3953: the best plan's age ---------------- */

/// <summary>
/// The interval table makes the 14-day window real, so a best plan can be two weeks old. The unattended bot does
/// not act on one older than <see cref="ForcePlanBotPolicy.MaxBestPlanAgeDays"/>: it is Blocked with the reason
/// named, even with every gate open. A plan inside the age runs the ordinary decision, and a target with no age
/// (a finding from before the column, when raw's retention bounded it) is not gated.
/// </summary>
[Fact]
public void AStaleBestPlan_IsBlocked_AFreshOneIsNot_AndAnUnknownAgeIsNotGated()
{
var stale = Target() with { BestPlanLastSeenUtc = Now.AddDays(-(ForcePlanBotPolicy.MaxBestPlanAgeDays + 1)) };
var fresh = Target() with { BestPlanLastSeenUtc = Now.AddDays(-(ForcePlanBotPolicy.MaxBestPlanAgeDays - 1)) };

var staleDecision = Evaluate(stale, Enabled(dryRun: false), serverOptedIn: true);
Assert.Equal(ForcePlanBotDecisionKind.Blocked, staleDecision.Kind);
Assert.Equal(new[] { ForcePlanBotPolicy.ReasonBestPlanStale }, staleDecision.Reasons);

Assert.Equal(ForcePlanBotDecisionKind.Force, Evaluate(fresh, Enabled(dryRun: false), serverOptedIn: true).Kind);
Assert.Equal(ForcePlanBotDecisionKind.Force, Evaluate(Target(), Enabled(dryRun: false), serverOptedIn: true).Kind);
Assert.Equal("best_plan_stale", ForcePlanBotPolicy.ReasonBestPlanStale);
Assert.Equal(4, ForcePlanBotPolicy.MaxBestPlanAgeDays);
}

/// <summary>The drill-down's <c>best_plan_last_seen</c> reaches the target through the shared extractor.</summary>
[Fact]
public void TheExtractor_CarriesTheBestPlansLastSeen_FromTheDrillDown()
{
var lastSeen = new DateTime(2026, 8, 25, 13, 45, 0, DateTimeKind.Unspecified);
var finding = new AnalysisFinding
{
FindingId = 1,
AnalysisTime = Now,
ServerId = 42,
ServerName = "SQL01",
DatabaseName = "orders",
TimeRangeStart = Now.AddHours(-4),
TimeRangeEnd = Now,
Severity = 1.6,
Confidence = 1.0,
Category = "queries",
StoryPath = "PLAN_REGRESSION",
StoryPathHash = "hash_PLAN_REGRESSION",
StoryText = "story",
RootFactKey = "PLAN_REGRESSION",
DrillDown = new Dictionary<string, object>
{
["regressed_queries"] = new[]
{
new Dictionary<string, object?>
{
["database"] = "orders",
["query_id"] = 42L,
["best_plan_id"] = 7L,
["regression_factor"] = 10.0,
["best_plan_last_seen"] = lastSeen,
},
},
},
};

var target = Assert.Single(FactRemediation.ExtractPlanRegressionTargets(finding));
Assert.Equal(lastSeen, target.BestPlanLastSeenUtc);
}
/// <summary>
/// #3953: the advice states how old the faster plan is when the fact carries <c>best_plan_age_days</c>, and says
/// what it always said when it does not (a fact from before the key existed, or Lite's older payloads).
/// </summary>
[Fact]
public void ThePlanRegressionAdvice_StatesTheBestPlansAge_WhenTheFactCarriesIt()
{
static IReadOnlyDictionary<string, Fact> Facts(double? ageDays)
{
var metadata = new Dictionary<string, double>
{
["worst_regression_factor"] = 6,
["offender_count"] = 2,
["latest_cpu_per_exec_us"] = 60000,
["best_cpu_per_exec_us"] = 10000,
};
if (ageDays is double age)
{
metadata["best_plan_age_days"] = age;
}

return new Dictionary<string, Fact>
{
["PLAN_REGRESSION"] = new Fact { Source = "queries", Key = "PLAN_REGRESSION", Value = 6, Metadata = metadata },
};
}

Assert.Contains("the faster plan on record (it last ran 9 days ago), so",
FactAdvice.Compose("PLAN_REGRESSION", Facts(9.6))!.Investigation, StringComparison.Ordinal);
Assert.Contains("the faster plan on record (it last ran within the past day), so",
FactAdvice.Compose("PLAN_REGRESSION", Facts(0.4))!.Investigation, StringComparison.Ordinal);
Assert.Contains("the faster plan on record, so",
FactAdvice.Compose("PLAN_REGRESSION", Facts(null))!.Investigation, StringComparison.Ordinal);
}
}
21 changes: 12 additions & 9 deletions Darling/Darling.Tests/IndexObjectStatsServerTimeIndexRungTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,17 @@ namespace Darling.Tests;
/// <c>server_id</c> Filter — every OTHER server's rows in the newest chunk read and rejected before the two
/// this server's read wanted turned up.
///
/// <para>This file takes over the "I am the top rung" claims that moved off
/// <see cref="CollectionCaveatsRungTests"/> (V141) when this rung landed.</para>
/// <para>This file took over the "I am the top rung" claims that moved off
/// <see cref="CollectionCaveatsRungTests"/> (V141) when this rung landed. When
/// <see cref="QueryStoreIntervalLatestRungTests"/> (V143, #3953) landed, those claims moved on again.
/// What stays is everything true of this rung wherever it sits.</para>
/// </summary>
public sealed class IndexObjectStatsServerTimeIndexRungTests
{
private const int RungVersion = 142;

/// <summary>This rung's sentinel ordinal in the viewer probe — the last argument, because V142 is
/// (for now) the newest rung.</summary>
/// <summary>This rung's sentinel ordinal in the viewer probe — V143 (#3953) is now above it, so it
/// is no longer the last argument.</summary>
private const int ProbeOrdinal = 117;

private const string IndexName = "idx_index_object_stats_server_time";
Expand All @@ -51,8 +53,7 @@ public void TheRungIsRegisteredAtTheTopOfADenseLadder()
Assert.Equal("index-object-stats-server-time", V142.Name);
Assert.Equal(StorageVersion.SchemaVersion, PgMigrations.Scripts[^1].Version);
Assert.Equal(StorageVersion.SchemaVersion, versions.Max());
Assert.Equal(RungVersion, StorageVersion.SchemaVersion);
Assert.Same(V142, PgMigrations.Scripts[^1]);
Assert.True(RungVersion < StorageVersion.SchemaVersion);
Assert.Equal(versions.Distinct().OrderBy(v => v), versions);
}

Expand Down Expand Up @@ -103,8 +104,8 @@ public void TheProbeMapsAStoreStoppedHereToThisRung()
var method = typeof(ViewerDataService).GetMethod("MapProbedSchemaVersion", BindingFlags.NonPublic | BindingFlags.Static)!;
var arity = method.GetParameters().Length;

/* This rung IS the newest sentinel for now, so its ordinal is the last parameter. */
Assert.Equal(ProbeOrdinal, arity - 1);
/* V143 (#3953) is now above this rung, so ProbeOrdinal is no longer the last parameter. */
Assert.True(ProbeOrdinal < arity - 1);
Assert.Equal("hasIndexObjectStatsServerTimeIndex", method.GetParameters()[ProbeOrdinal].Name);

var atThisRung = Enumerable.Range(0, arity).Select(i => (object)(i <= ProbeOrdinal)).ToArray();
Expand All @@ -114,12 +115,14 @@ public void TheProbeMapsAStoreStoppedHereToThisRung()
behind[ProbeOrdinal] = false;
Assert.Equal(141, (int)method.Invoke(null, behind)!);

/* In the source, this rung's arm sits ABOVE V141's and returns this build's version. */
/* In the source, this rung's arm sits between V143's (above) and V141's (below). */
var thisArm = viewer.IndexOf("if (hasIndexObjectStatsServerTimeIndex)", StringComparison.Ordinal);
var previousArm = viewer.IndexOf("if (hasCollectionCaveats)", StringComparison.Ordinal);
var nextArm = viewer.IndexOf("if (hasQueryStoreIntervalLatest)", StringComparison.Ordinal);
Assert.True(thisArm >= 0, "the viewer has no V142 sentinel arm — a fully-migrated store would map one rung low");
Assert.True(previousArm >= 0, "the previous rung's arm is gone, so this pin is comparing against nothing");
Assert.True(thisArm < previousArm, "the V142 arm sits below V141's, so a current store maps one rung low");
Assert.True(nextArm < thisArm, "the V143 arm should sit above V142's arm");
Assert.Contains(
"return " + RungVersion.ToString(CultureInfo.InvariantCulture) + ";",
viewer[thisArm..previousArm], StringComparison.Ordinal);
Expand Down
20 changes: 16 additions & 4 deletions Darling/Darling.Tests/PgFactCollectorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,11 @@ public void ImplementsTheSharedSeam_WithLitesCollectMethodSurface()
public void AllSql_CoversEveryQuery_OnePerCollectMethodPlusTheDmvFallback()
{
/* 32 collect methods (31 fact readers plus the #3538 coverage witness), one query each, plus
the DMV-snapshot fallback the blocking-chain method appends through PgBlockingPairRowQuery. */
Assert.Equal(LiteCollectMethodSurface.Length + 1, PgFactCollector.AllSql.Count);
the DMV-snapshot fallback the blocking-chain method appends through PgBlockingPairRowQuery, plus
PLAN_REGRESSION's #3953 table twin: the plan-regression method runs one of two reads per server,
and Lite has no store for the second (its DuckDB keeps no latest-snapshot interval table). */
Assert.Equal(LiteCollectMethodSurface.Length + 2, PgFactCollector.AllSql.Count);
Assert.Contains(PgFactCollector.PlanRegressionTableSql, PgFactCollector.AllSql);
Assert.Contains(PgBlockingPairRowQuery.DmvSnapshotSql, PgFactCollector.AllSql);
Assert.Contains(PgFactCollector.CoverageSql, PgFactCollector.AllSql);
}
Expand Down Expand Up @@ -243,11 +246,15 @@ public void AllSql_AnyValue_OnlyInThePlanRegressionQuery()
/* any_value() is standard SQL:2023, in Postgres since 16 (product minimum PG is 17).
It is deliberate in the plan-regression aggregation and nowhere else. */
Assert.Contains("any_value(query_plan_hash)", PgFactCollector.PlanRegressionSql, StringComparison.Ordinal);
Assert.Contains("any_value(query_plan_hash)", PgFactCollector.PlanRegressionTableSql, StringComparison.Ordinal);
foreach (var sql in PgFactCollector.AllSql)
{
if (sql.Contains("any_value", StringComparison.OrdinalIgnoreCase))
{
Assert.Equal(PgFactCollector.PlanRegressionSql, sql);
/* The two PLAN_REGRESSION reads (#3953: raw, and its interval-table twin), and nothing else. */
Assert.True(
sql == PgFactCollector.PlanRegressionSql || sql == PgFactCollector.PlanRegressionTableSql,
"any_value() outside the PLAN_REGRESSION reads:\n" + sql);
}
}
}
Expand Down Expand Up @@ -289,8 +296,13 @@ so its right-hand operand parses as a relation name.
foreach (Match m in Regex.Matches(scanSql, @"\b(?:FROM|JOIN)\s+(\w+)", RegexOptions.IgnoreCase))
{
var target = m.Groups[1].Value;

/* #3953: the latest-snapshot interval table is the one non-collector relation a fact reads,
sourced from its Storage class rather than restated, as #2150 did for query_store_text in
the drill-down guard. */
Assert.True(
views.Contains(target) || tables.Contains(target) || ctes.Contains(target),
views.Contains(target) || tables.Contains(target) || ctes.Contains(target)
|| target == QueryStoreIntervalLatest.TableName,
$"FROM/JOIN target '{target}' resolves to no V4 view, collector table, or CTE in:\n{sql}");
}
}
Expand Down
Loading
Loading