Skip to content

Plan analysis: rule 38 reads the server's edition and MAXDOP at every entry point (#4530) - #4601

Merged
erikdarlingdata merged 23 commits into
devfrom
plan-sync/4530-entry-points
Sep 28, 2026
Merged

erikdarlingdata merged 23 commits into
devfrom
plan-sync/4530-entry-points

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Fixes #4530
#4585 is merged; this is now against dev.
Part of #4511

What changes

Rule 38 (the Standard Edition batch-mode DOP-2 limitation) needs the server's edition and MAXDOP to give its Warning instead of an uninformative Info. This wires that metadata through every place a plan gets analyzed.

Entry point Metadata source
Darling MCP plan tools (analyze_query_plan, analyze_procedure_plan, analyze_query_store_plan) DarlingServerMetadataReader.ReadAsync — one Postgres store read per call
Lite MCP plan tools (same three tools) LocalDataService.GetServerMetadataForPlanAnalysisAsync — same shape, against DuckDB
Both drill-downs (PgDrillDownCollector.Plans.cs, Lite/Analysis/DrillDownCollector.Plans.cs) One store read per collector call
Darling Viewer's plan-opening sites: ViewerServerTab.Plans.cs (Plan Viewer tab), ViewerActualPlanFlow.OpenFloatingPlanAsync (the shared floated-plan host, now taking dataService/serverId), ProcedureHistoryWindow, QueryStatsHistoryWindow, QueryStoreHistoryWindow, WaitDrillDownWindow ViewerDataService.GetPlanAnalysisServerMetadataAsync (wraps DarlingServerMetadataReader.ReadAsync over the viewer's own NpgsqlDataSource)
Lite's plan-opening sites: ServerTab.Plans.cs (OpenPlanTab), FinOpsTab's High Impact/Expensive Queries menu, PlanViewerWindow.ShowPlanAsync/LoadPlanAsync (now take an optional ServerMetadata?), ProcedureHistoryWindow, QueryStatsHistoryWindow, QueryStoreHistoryWindow, WaitDrillDownWindow LocalDataService.GetServerMetadataForPlanAnalysisAsync
Pasted plans / analyze_plan_xml Unchanged — stays null, so rule 38 gives its Info branch, same as before
Fact-count path (PlanAdvisoryAggregator.Summarize) Unchanged — stays null, per the earlier step's reasoning

Every viewer/Lite host that opens a PlanViewerControl/PlanViewerWindow has a known server id in scope (_server.ServerId, _serverId, or the currently-selected server), so every site could be wired; none were left null for lack of an id.

Why a store read and not a live fetch. These entry points analyze stored plans with no live connection to the target server (the MCP tools and drill-downs read collected data; the viewer and Lite hosts have no target connection either). A stored read costs one indexed lookup per call — server_properties anchored on (server_id, collection_time) for the newest row, server_config/v_server_config for the newest max degree of parallelism value — never cached, never per-plan-node.

New shared-layer overloads (in PerformanceMonitor.PlanAnalysis, used by every call site above):

  • McpPlanAnalysisFormatter.BuildAnalysisResult(xml, serverName, source, identifier, ServerMetadata?, ct) — the existing 5-arg form forwards null.
  • PlanAdvisoryAggregator.ExtractCancellable(planXmls, ServerMetadata?, ct) — the existing 2-arg form forwards null.

Rule 38 itself (the analyzer logic, masking, message text) was already ported and pinned by an earlier step in this stack; this PR is entry-point wiring.

This push adds the reader-level pins the earlier wiring steps skipped. The prior test plan (PlanSync4530EntryPointsTests) only ever fed a hand-built ServerMetadata into the analyzer — it never proved that DarlingServerMetadataReader or Lite's GetServerMetadataForPlanAnalysisAsync actually READ the edition and MAXDOP off a real store. A wrong table or column name in either reader's SQL would return null silently, and rule 38 would stay on its Info branch on every real install with nothing here to catch it. Three new pins close that gap:

  • Darling/Darling.Tests/PlanSync4530ReaderLiveTests.cs — seeds collect.server_properties (edition "Standard Edition (64-bit)") and collect.server_config (max degree of parallelism = 8) on a migrated scratch store, asserts DarlingServerMetadataReader.ReadAsync returns them, then runs the same DOP-2 batch-mode plan XML through McpPlanAnalysisFormatter.BuildAnalysisResult — the product's own MCP-tool call path — and confirms rule 38 gives its Warning; a server with no rows returns null and rule 38 stays Info. Also a source-census pin that Lite's reader SQL names the same view (v_server_properties/v_server_config) and columns (edition, max degree of parallelism, value_in_use) as Darling's reader.
  • Darling/Darling.Tests/PlanSync4530DrillDownLiveTests.cs — seeds the same Standard/MAXDOP-8 server plus a query-plan row, runs PgDrillDownCollector.EnrichFindingsAsync on a PLAN_WARNING finding, and asserts the drill-down's plan_warnings carries the rule-38 Warning — proving the drill-down's own store read (CollectPlanAdvisoryDetail → DarlingServerMetadataReader.ReadAsync → PlanAdvisoryAggregator.ExtractCancellable), not just the shared formatter.
  • Lite.Tests/PlanServerMetadataReaderTests.cs — a build-only (net10.0-windows; CI runs it) pin for LocalDataService.GetServerMetadataForPlanAnalysisAsync against a seeded DuckDB fixture (copied setup from SharedDuckDbFixture/AnalysisAsOfAnchorTests's pattern): edition + MAXDOP round-trip, and a no-rows case returns null.

Test plan

  • Darling/Darling.Tests/PlanSync4530EntryPointsTests.cs (landed earlier, unchanged): pins the shared-layer BuildAnalysisResult/ExtractCancellable metadata overloads with a Warning vs. Info contrast on the same plan XML.
  • New this push, in-process run on an own TimescaleDB container (cleaned up after): PlanSync4530ReaderLiveTests, PlanSync4530DrillDownLiveTests, PlanSync4530Tests, PlanSync4530EntryPointsTests — Total: 15, Errors: 0, Failed: 0, Skipped: 0, Not Run: 0.
  • RED confirmed on PlanSync4530ReaderLiveTests: renamed the reader's FROM server_properties clause to a nonexistent table, rebuilt, re-ran — the new live pin failed (the read returns null, the assertion on metadata.Edition fails). Reverted, rebuilt clean.
  • Census family, same container — Total: 278, Errors: 0, Failed: 0, Skipped: 0, Not Run: 0 (DailyDeadlockWindowCensusTests, EntraProviderPackageCensusTests, FileGrowthRiseUnitCensusTests, McpPayloadContractCensusTests, McpServiceParameterDiSeatCensusTests, MeasurementContractCensusTests, MigrationDataMovingRungCensusPins, PgSettingScrubCandidateCensusTests, PlanForceActionDetailCensus(Tests), PlanSync4535RuleNumberCensusTests, RemoteCollectorServiceCancellationCensusTests, SameStatementPileupSourceCensusTests, StoreApplicationNameCensusTests, StoreSessionTimeZonePinCensusTests, WebExceptionTextCensusTests, McpToolsListBudgetTests).
  • CommentFilterAdoptionTests, DocCommentHygieneTests, LiveCleanupConversionRatchetTests also run clean; LivePostgresCollectionHygieneTests has one pre-existing failure unrelated to this change — EveryClassUsingTheSharedStore_IsSerializedOrDocumentsWhyNot throws ReflectionTypeLoadException on PresentationFramework (a WPF-type platform limitation on macOS, not this diff).
  • Build (Release, -p:EnableWindowsTargeting=true), 0 errors / 0 warnings: Darling.Tests, Darling/PerformanceMonitor.Darling.Service, Lite.Tests.
  • Lite.Tests.PlanServerMetadataReaderTests builds clean (0 errors, 0 warnings) but does not run here (net10.0-windows discovery limitation on macOS); CI decides it. Read against the code: its setup copies AnalysisAsOfAnchorTests's SharedDuckDbFixture pattern exactly (constructor ResetData, AcquireReadLock around every insert, IDisposable cleanup of the seed connection) — the pattern the WINDOWS-ONLY PINS rule calls out as CI-safe.
  • DarlingMcpPlanToolsLivePostgresTests was not run (needs the shared live-postgres collection fixture beyond this container's ad-hoc setup); it is unaffected by this push and, as corrected below, was never claimed to cover the metadata wiring — only the tool surface and fetch/analyze round-trips.

CHANGELOG

SECTION: Changed
ENTRY: - Rule 38 can now flag a Standard Edition DOP limit as a Warning ([#4601]) - Plan analysis reads each server's collected edition and MAXDOP wherever it analyzes a stored plan (the MCP plan tools, the drill-downs, and the Darling Viewer and Lite plan viewers). A plan that ran at DOP 2 with batch-mode operators on a Standard Edition server with MAXDOP above 2 now gets a Warning; it stays Info when the edition isn't known.
REF: [#4601]: #4601

scrubcheck: clean.

erikdarlingdata and others added 23 commits September 28, 2026 09:15
Ports PerformanceStudio's AnalyzerConfig/RulesConfig (disabled rules,
severity overrides) and the top-level ServerMetadata fields into
PerformanceMonitor.PlanAnalysis, adds PlanWarning.RuleNumber, and adds
Analyze/Run overloads that take (config, serverMetadata, cancellationToken)
and thread both down to the statement and node walks. The existing
overloads still forward to AnalyzerConfig.Default and a null metadata, and
no rule reads either new parameter yet, so analyzer output is unchanged.
…ing (#4535)

Ports erikdarlingdata/PerformanceStudio's severity-override pass: overrides now find a warning's rule via PlanWarning.RuleNumber instead of matching WarningType against a rule-to-name table, and the engine's own warnings (Source == SqlServer) are always skipped. Also ports PlanStatements.EnumerateAllWithContainer, with EnumerateAll delegating to it so the plain and container-carrying walks stay one walk.
#4535, #4566)

The plumbing step's RuleNumber_DefaultsToNull_UntilLaterStepsStampIt pinned the
state partway through the per-rule configuration rollout (only rule 3 stamped).
Every step that stamps a rule has landed on this branch, so that pin is wrong
by design now.

Replaces it with PlanSync4535RuleNumberCensusTests: a code census over every
new PlanWarning construction in PlanAnalyzer.cs (each must set RuleNumber,
except a SqlServer-sourced initializer), a runtime census over the existing
plan fixtures (every analyzer finding carries a non-null RuleNumber), and a
cross-step check that disabling every stamped rule number leaves zero analyzer
findings.
Adds three tests the earlier #4530 steps skipped:

- a live Postgres pin that seeds collect.server_properties and
  collect.server_config on a migrated store and asserts
  DarlingServerMetadataReader.ReadAsync returns the edition and MAXDOP,
  then runs the same plan XML through the analysis formatter and
  confirms rule 38 (Standard Edition DOP 2 limitation) gives its
  Warning, plus a no-rows case that stays on the Info branch;
- a drill-down pin: a PLAN_WARNING finding's drill-down surfaces the
  rule-38 Warning when PgDrillDownCollector runs against a seeded
  Standard Edition / MAXDOP 8 server;
- a Lite.Tests pin (build-only on macOS) for
  LocalDataService.GetServerMetadataForPlanAnalysisAsync against a
  seeded DuckDB fixture, plus a Darling.Tests source-census pin that
  the Lite reader's SQL names the same view and columns Darling's
  migrated schema and reader use.

Ported the reader's column-name RED by temporarily renaming the
FROM clause, confirming the new live pin fails, then reverting.
…and seed every required server_properties column in the Lite metadata test
…oints

# Conflicts:
#	PerformanceMonitor.Ui/PlanViewerControl.xaml.cs
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.

1 participant