Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8c79f0c
query_store plan/text fetch goes activity-driven: the store is the wa…
erikdarlingdata Aug 19, 2026
45a91c3
Aurora detection calls aurora_version() instead of looking it up in p…
erikdarlingdata Aug 19, 2026
a9eb603
The store's scale test asserts compression work, not TimescaleDB's th…
erikdarlingdata Aug 19, 2026
118c8b5
Bump the nuget-patch-and-minor group with 7 updates, with lock files …
dependabot[bot] Aug 19, 2026
1c73682
Darling MCP declares its peer stores instead of answering "unknown se…
erikdarlingdata Aug 19, 2026
b2fc786
Diagnose an unreadable install location in the service itself (#2185)…
erikdarlingdata Aug 19, 2026
a042995
Bound the query_store watermark read to the window the clamp can actu…
erikdarlingdata Aug 19, 2026
b1a511b
Strip the extended-length prefix before classifying the install locat…
erikdarlingdata Aug 19, 2026
7330e03
Stop pretty-printing MCP tool results (#2350) (#2355)
erikdarlingdata Aug 19, 2026
4537b76
Serve get_query_trend from the tier that can answer, and say which on…
erikdarlingdata Aug 19, 2026
b738c98
Add --harden-files, the actor that can apply the ACLs the service onl…
erikdarlingdata Aug 19, 2026
7af1469
Run the test suites under Microsoft.Testing.Platform instead of VSTes…
erikdarlingdata Aug 19, 2026
c4aa8de
Say whether a server is still monitored, so an old Last Updated is le…
erikdarlingdata Aug 19, 2026
3ecd5f2
Accumulate occurrence totals for every fingerprinted alert, not just …
erikdarlingdata Aug 19, 2026
b78f02e
Name the inventory snapshot for what it is, and show real freshness b…
erikdarlingdata Aug 19, 2026
ed38496
Report the window get_query_store_top actually served (#2364) (#2367)
erikdarlingdata Aug 19, 2026
166ebf3
Put Database and LastEventUtc on the incident projection (#2361) (#2368)
erikdarlingdata Aug 19, 2026
f29f2dd
Make the compose statement_timeout a store setting instead of a const…
erikdarlingdata Aug 19, 2026
0ef9071
Alert on database file size growth, graded per server (#2349) (#2370)
erikdarlingdata Aug 19, 2026
c610c99
Harden for the registered service account, not the caller (#2372)
erikdarlingdata Aug 19, 2026
c472162
Describe what --harden-files actually verifies (#2373)
erikdarlingdata Aug 19, 2026
eb28346
Wait for the baseline to land instead of assuming one sweep seeded it…
erikdarlingdata Aug 19, 2026
8b9fbb5
Compress query_store_stats on query identity instead of on time (#2377)
erikdarlingdata Aug 19, 2026
a00c9aa
Revert "Compress query_store_stats on query identity instead of on ti…
erikdarlingdata Aug 19, 2026
bf7c3ab
Fold [Unreleased] into [3.5.0] for the release (#2380)
erikdarlingdata Aug 19, 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
10 changes: 5 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -275,19 +275,19 @@ jobs:
# spin-up and its filter-drift risk.
- name: Run Lite tests
if: steps.filter.outputs.lite == 'true' || steps.filter.outputs.core == 'true' || steps.filter.outputs.root == 'true' || github.event_name == 'release'
run: dotnet test Lite.Tests/Lite.Tests.csproj -c Release --no-build --verbosity normal
run: dotnet run --project Lite.Tests/Lite.Tests.csproj -c Release --no-build

- name: Run Installer tests
if: steps.filter.outputs.installer == 'true' || steps.filter.outputs.installer_core == 'true' || steps.filter.outputs.root == 'true' || github.event_name == 'release'
run: dotnet test deprecated/Installer.Tests/Installer.Tests.csproj -c Release --no-build --verbosity normal --filter "FullyQualifiedName!~VersionDetectionTests&FullyQualifiedName!~IdempotencyTests&FullyQualifiedName!~AdversarialTests"
run: dotnet run --project deprecated/Installer.Tests/Installer.Tests.csproj -c Release --no-build -- -class- "Installer.Tests.VersionDetectionTests" -class- "Installer.Tests.IdempotencyTests" -class- "Installer.Tests.AdversarialTests"

- name: Run Dashboard tests
if: steps.filter.outputs.dashboard == 'true' || steps.filter.outputs.core == 'true' || steps.filter.outputs.root == 'true' || github.event_name == 'release'
run: dotnet test deprecated/Dashboard.Tests/Dashboard.Tests.csproj -c Release --no-build --verbosity normal
run: dotnet run --project deprecated/Dashboard.Tests/Dashboard.Tests.csproj -c Release --no-build

- name: Run Darling tests
if: steps.filter.outputs.darling == 'true' || steps.filter.outputs.core == 'true' || steps.filter.outputs.root == 'true' || github.event_name == 'release'
run: dotnet test Darling/Darling.Tests/Darling.Tests.csproj -c Release --no-build --verbosity normal
run: dotnet run --project Darling/Darling.Tests/Darling.Tests.csproj -c Release --no-build

- name: Get version
if: steps.fastpath.outputs.engaged != 'true'
Expand Down Expand Up @@ -712,7 +712,7 @@ jobs:
env:
DARLING_TEST_PG: "Host=127.0.0.1;Port=5541;Username=darling;Database=darling"
DARLING_TEST_PGRUNTIME: ${{ github.workspace }}\Darling\artifacts\pg-runtime
run: dotnet test Darling/Darling.Tests/Darling.Tests.csproj -c Release --no-build --verbosity normal --logger "trx;LogFileName=darling-pr.trx" --results-directory TestResults
run: dotnet run --project Darling/Darling.Tests/Darling.Tests.csproj -c Release --no-build -- -trx TestResults/darling-pr.trx

- name: Stop PostgreSQL
if: always() && steps.filter.outputs.darling == 'true'
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ jobs:
dotnet restore Darling/PerformanceMonitor.Darling.Viewer/PerformanceMonitor.Darling.Viewer.csproj --locked-mode

- name: Run tests
run: dotnet test Lite.Tests/Lite.Tests.csproj -c Release --verbosity normal
run: dotnet run --project Lite.Tests/Lite.Tests.csproj -c Release

- name: Publish Lite
run: dotnet publish Lite/PerformanceMonitorLite.csproj -c Release -o publish/Lite
Expand Down Expand Up @@ -376,7 +376,7 @@ jobs:
# sees a FRESH store, so nothing else can catch an upgrade path that breaks.
DARLING_TEST_PGRUNTIME_OLD: ${{ github.workspace }}\Darling\artifacts\upgrade-fixture\old\pg-runtime
DARLING_TEST_PGRUNTIME_NEWZIP: ${{ github.workspace }}\Darling\artifacts\pg-runtime.zip
run: dotnet test Darling/Darling.Tests/Darling.Tests.csproj -c Release --no-build --verbosity normal --logger "trx;LogFileName=darling-nightly.trx" --results-directory TestResults
run: dotnet run --project Darling/Darling.Tests/Darling.Tests.csproj -c Release --no-build -- -trx TestResults/darling-nightly.trx

- name: Stop PostgreSQL
if: always()
Expand Down
35 changes: 32 additions & 3 deletions CHANGELOG.md

Large diffs are not rendered by default.

130 changes: 130 additions & 0 deletions Darling/Darling.Tests/ActivityDrivenPlanFetchStoreTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* Copyright (c) 2026 Erik Darling, Darling Data LLC
*
* This file is part of the SQL Server Performance Monitor.
*
* Licensed under the MIT License. See LICENSE file in the project root for full license information.
*/

using System;
using System.Linq;
using PerformanceMonitor.Darling.Storage;
using PerformanceMonitor.Darling.Viewer;
using Xunit;

namespace Darling.Tests;

/// <summary>
/// The V77 rung (#2312 Finding 2) — the schema strokes behind the activity-driven plan/text fetch: the
/// plan map's <c>digest</c> goes nullable (the content-less marker for plans whose XML the engine cannot
/// persist), <c>query_store_text</c> gains <c>query_hash</c> (the Query Store reset detector), and the
/// retired <c>planwm:</c>/<c>textwm:</c> watermark state rows are deleted wholesale. These facts pin the
/// rung's place on the ladder, the viewer probe's newest-first arm, and the migration SQL's load-bearing
/// strokes. The fetch behavior itself is pinned in <c>QueryStorePlanFetchTests</c> and exercised live in
/// the gated Postgres suites.
/// </summary>
public sealed class ActivityDrivenPlanFetchStoreTests
{
/* ---------------- the rung ---------------- */

[Fact]
public void TheRungIsTheTopOfADenseLadder()
{
var versions = PgMigrations.Scripts.Select(s => s.Version).ToList();

/* #2357 added V78, so this rung is no longer the maximum. What stays true: it is PRESENT, the
ladder is ordered and dense, and the build's schema version tracks the maximum. */
Assert.Contains(77, versions);
Assert.Equal(StorageVersion.SchemaVersion, versions.Max());
Assert.Equal(versions.Distinct().OrderBy(v => v), versions);

/* Dense above the one sanctioned historical hole at V45. */
var above = versions.Where(v => v > 45).OrderBy(v => v).ToList();
Assert.Equal(Enumerable.Range(above[0], above.Count), above);

Assert.Equal("activity-driven-plan-fetch", PgMigrations.Scripts.Single(s => s.Version == 77).Name);
}

/// <summary>The three strokes, each load-bearing and none allowed to drift out of the rung: without
/// the nullable digest the NULL-XML marker cannot land, without query_hash the reset detector has no
/// stored baseline, and without the deletes the orphaned watermark rows live forever (collector_state
/// has no retention, and the prune set no longer owns those prefixes).</summary>
[Fact]
public void TheRungCarriesAllThreeStrokes()
{
var sql = PgMigrations.Scripts.Single(s => s.Version == 77).Sql;

Assert.Contains("ALTER TABLE collect.query_store_plan_map ALTER COLUMN digest DROP NOT NULL", sql, StringComparison.Ordinal);
Assert.Contains("ALTER TABLE collect.query_store_text ADD COLUMN IF NOT EXISTS query_hash text", sql, StringComparison.Ordinal);
Assert.Contains("DELETE FROM collector_state WHERE collector_name = 'query_store_plan_xml' AND state_key LIKE 'planwm:%'", sql, StringComparison.Ordinal);
Assert.Contains("DELETE FROM collector_state WHERE collector_name = 'query_store_text' AND state_key LIKE 'textwm:%'", sql, StringComparison.Ordinal);
}

/* ---------------- the viewer probe ---------------- */

[Fact]
public void TheProbeMapsAStoreAtExactly77To77()
{
/* #2357 added V78, so this rung is no longer the top — the "I am the top" claim moves to the newest
rung's own test (ComposeStatementTimeoutStoreTests). What stays true forever is the arm itself: a
store migrated to EXACTLY 77 must answer 77 rather than falling through to 76. */
Assert.Equal(StorageVersion.SchemaVersion, ViewerDataService.RequiredStoreSchemaVersion);

/* 52 positional sentinels, then this rung's own by name. Anything a LATER rung appends is padded
by InvokeMap from the method's arity, so this count stays fixed as the ladder grows. */
var all = Enumerable.Repeat(true, 52).Cast<object>().ToArray();

Assert.Equal(77, InvokeMap(all, hasQueryStoreTextHash: true));
Assert.Equal(76, InvokeMap(all, hasQueryStoreTextHash: false));
}

[Fact]
public void TheProbeAsksForTheColumn_AndTheThreePlacesAgree()
{
Assert.Contains(
"table_name = 'query_store_text' AND column_name = 'query_hash'",
ViewerDataService.StoreSchemaProbeSql, StringComparison.Ordinal);

var mapParameters = typeof(ViewerDataService)
.GetMethod("MapProbedSchemaVersion", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!
.GetParameters().Length;

var viewerSource = ReadViewerSource();

/* The reader must hand over exactly one argument per map parameter: ordinals are 0-based, so the
highest is Count - 1, and the next one up must NOT appear. */
Assert.Contains($"reader.GetBoolean({mapParameters - 1})", viewerSource, StringComparison.Ordinal);
Assert.DoesNotContain($"reader.GetBoolean({mapParameters})", viewerSource, StringComparison.Ordinal);
}

/* ---------------- helpers ---------------- */

private static int InvokeMap(object[] leading, bool hasQueryStoreTextHash)
{
var method = typeof(ViewerDataService)
.GetMethod("MapProbedSchemaVersion", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!;

/* Parameters appended by LATER rungs are padded FALSE, so this fact keeps exercising its own arm
rather than a newer one. Derived from the method's arity rather than named by hand: listing them
made every new rung break this file, which is exactly what V79 (#2349) did. */
var args = leading.Concat(new object[] { hasQueryStoreTextHash }).ToArray();
args = args
.Concat(Enumerable.Repeat((object)false, method.GetParameters().Length - args.Length))
.ToArray();

return (int)method.Invoke(null, args)!;
}

private static string ReadViewerSource([System.Runtime.CompilerServices.CallerFilePath] string thisFile = "")
{
var dir = System.IO.Path.GetDirectoryName(thisFile)!;
var relative = System.IO.Path.Combine("Darling", "PerformanceMonitor.Darling.Viewer", "ViewerDataService.cs");
while (dir is not null && !System.IO.File.Exists(System.IO.Path.Combine(dir, relative)))
{
dir = System.IO.Path.GetDirectoryName(dir);
}

Assert.NotNull(dir);
return System.IO.File.ReadAllText(System.IO.Path.Combine(dir!, relative));
}
}
18 changes: 18 additions & 0 deletions Darling/Darling.Tests/AlertEngineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ test switches on exactly the check it pins (a disabled check must not even fetch
/* #1984: DarlingConfig defaults (40% / 1 GB); enable stays the class's opt-in OFF. */
public int PvsThresholdPercent { get; set; } = 40;
public int PvsFloorGb { get; set; } = 1;

/* #2349: OFF in the fakes so existing expectations are untouched. */
public bool FileGrowthEnabled { get; set; }
public int FileGrowthRiseMb { get; set; } = 10240;
public int FileGrowthVolumePercent { get; set; } = 60;
public int FileGrowthLookbackMinutes { get; set; } = 60;
public int LongRunningJobMultiplier { get; set; } = 3;
public int FailedJobLookbackMinutes { get; set; } = 60;
public int CooldownMinutes { get; set; } = 5;
Expand Down Expand Up @@ -136,6 +142,12 @@ public Task<List<LongRunningQueryInfo>> GetLongRunningQueriesAsync(
public Task<List<VolumeFreeSpaceInfo>> GetVolumeFreeSpaceAsync(string serverKey, CancellationToken cancellationToken = default) =>
Task.FromResult(new List<VolumeFreeSpaceInfo>(Volumes));

/* #2349: empty on purpose. These tests exercise other alerts, and a fabricated file would
make the file-growth gate fire inside an unrelated scenario. */
public Task<List<DatabaseFileGrowthInfo>> GetDatabaseFileGrowthAsync(
string serverKey, int lookbackMinutes, CancellationToken cancellationToken = default) =>
Task.FromResult(new List<DatabaseFileGrowthInfo>());

public Task<TempDbSpaceInfo?> GetTempDbSpaceAsync(string serverKey, CancellationToken cancellationToken = default) =>
Task.FromResult(TempDb);

Expand Down Expand Up @@ -1365,6 +1377,12 @@ public Task<List<LongRunningQueryInfo>> GetLongRunningQueriesAsync(string server
throw new InvalidOperationException("store down");
public Task<List<VolumeFreeSpaceInfo>> GetVolumeFreeSpaceAsync(string serverKey, CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("store down");

/* #2349: empty on purpose. These tests exercise other alerts, and a fabricated file would
make the file-growth gate fire inside an unrelated scenario. */
public Task<List<DatabaseFileGrowthInfo>> GetDatabaseFileGrowthAsync(
string serverKey, int lookbackMinutes, CancellationToken cancellationToken = default) =>
Task.FromResult(new List<DatabaseFileGrowthInfo>());
public Task<TempDbSpaceInfo?> GetTempDbSpaceAsync(string serverKey, CancellationToken cancellationToken = default) =>
throw new InvalidOperationException("store down");
public Task<List<PvsPressureInfo>> GetPvsPressureAsync(string serverKey, CancellationToken cancellationToken = default) =>
Expand Down
6 changes: 6 additions & 0 deletions Darling/Darling.Tests/AlertStoredValueTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ private sealed class Settings : IAlertEngineSettings
public int CollectionFailureThreshold { get; set; } = 10;
public int PvsThresholdPercent { get; set; } = 40;
public int PvsFloorGb { get; set; } = 1;

/* #2349: OFF in the fakes so existing expectations are untouched. */
public bool FileGrowthEnabled { get; set; }
public int FileGrowthRiseMb { get; set; } = 10240;
public int FileGrowthVolumePercent { get; set; } = 60;
public int FileGrowthLookbackMinutes { get; set; } = 60;
public int LongRunningJobMultiplier { get; set; } = 3;
public int FailedJobLookbackMinutes { get; set; } = 60;
public int CooldownMinutes { get; set; } = 5;
Expand Down
12 changes: 5 additions & 7 deletions Darling/Darling.Tests/AzureForeignStatePruneTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,17 +125,15 @@ public void ADatabaseNamedRegistrationIsPruned(string catalog)
[Fact]
public void BothArmsPruneEveryPerDatabasePrefix()
{
Assert.Equal(5, QueryStorePerDatabaseState.PrunableKeys.Count);
Assert.Contains(QueryStorePerDatabaseState.PrunableKeys,
k => k.Prefix == QueryStorePlanXmlState.WatermarkKeyPrefix);
/* #2312 shrank this from five to three: the planwm:/textwm: watermark families retired with the
watermarks themselves (the fetches are activity-driven against the store now), and V77 deleted
their orphaned rows wholesale — a dropped-database prune has nothing left to own there. */
Assert.Equal(3, QueryStorePerDatabaseState.PrunableKeys.Count);
Assert.Contains(QueryStorePerDatabaseState.PrunableKeys,
k => k.Prefix == QueryStoreBackfillState.DoneKeyPrefix);
Assert.Contains(QueryStorePerDatabaseState.PrunableKeys,
k => k.Prefix == QueryStoreBackfillState.HoleKeyPrefix);
/* #2150: the text watermark, keyed prefix + databaseName exactly like the plan watermark. */
Assert.Contains(QueryStorePerDatabaseState.PrunableKeys,
k => k.Prefix == QueryStoreTextState.WatermarkKeyPrefix);
/* #2312: the open-interval refresh stamp, the fifth per-database prefix. */
/* #2312: the open-interval refresh stamp. */
Assert.Contains(QueryStorePerDatabaseState.PrunableKeys,
k => k.Prefix == QueryStoreOpenIntervalState.WatermarkKeyPrefix);

Expand Down
Loading
Loading