Skip to content

Link every warning to the operator it came from (#440) - #446

Merged
erikdarlingdata merged 1 commit into
devfrom
feat/440-link-warnings-to-origin
Aug 21, 2026
Merged

Link every warning to the operator it came from (#440)#446
erikdarlingdata merged 1 commit into
devfrom
feat/440-link-warnings-to-origin

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Implements #440. Warnings now know which operator they came from, and the statement panel links to it.

The design question is when not to link

PlanWarning.OriginNodeIds is a list, because there are three honest answers and they're genuinely different:

  • A key lookup came from exactly one operator.
  • A table variable warning came from every operator that touched one — several on a real plan. That rule already walked the tree and knew precisely which; it threw the answer away before emitting. It doesn't any more.
  • High Compile CPU happened before a single row was read, and SQL Server reports UDF Execution at the statement level only. Those have no operator origin and now say so, so the UI offers no link rather than one that goes somewhere arbitrary.

Sending a reader to the wrong operator is worse than sending them nowhere, because they'd believe it. Both directions are pinned by tests.

Stamped in one place

Operator warnings get their origin at the end of AnalyzeNode, not at the 26 sites that add one — same reasoning as the provenance stamp in #439 and the ceiling in #438. A rule you must remember at every construction site is a rule that eventually gets forgotten, and the once it's forgotten the UI quietly drops a link that existed. It only fills what a rule left empty, so a rule that knows better keeps its own answer.

Scope call worth reviewing

The pop-up the issue is about shows statement warnings, and few of those can attribute to an operator. Linking only those would not have served the huge-plan case that motivated the request at all.

The warnings that do have origins are the operator ones — and until now the only way to see one was to have already clicked the operator carrying it, which is no help when you don't know which operator to click. That's the chicken-and-egg pgfiore is describing.

So the statement panel also gains an Operator Warnings section indexing every warning in the tree, each one a link. Nothing is removed from the per-operator panel; this is an index into it. Collapsed by default, because on a large plan it's the longest section in the panel and expanding it would push the statement's own details off screen — the opposite of the problem being solved.

If you'd rather ship only the statement-warning links, drop that block and the rest stands.

Placement

The tree walk lives in Core as WarningIndex, not beside the panel that renders it: it's a walk over Core's own models with nothing UI about it, and there it can be tested without standing up Avalonia. Explicit stack rather than recursion — a deep plan is precisely the case this feature exists for, and #430 was a crash caused by assuming operator trees are shallow.

Contract

origin_node_ids is additive on every warning, so ExpectedCompactOutputSha256 is rolled — second time, both additive, both deliberate. I verified it's the only new key rather than assuming: the full key set on a warning is otherwise unchanged.

WarningBaseline.txt does not move — it digests type, severity and message, none of which changed, so no committed plan's verdict is affected.

Tests

314 total, 312 passed, 0 failed. New tests pin that across every committed plan no operator warning is left without an origin or points away from its own node; that the table variable warning names the operators that touch one; and that High Compile CPU and UDF Execution claim none. The index is compared against an independent recursive walk rather than a hand-written count, so it can't drift as fixtures are added.

Verified in the app

Confirmed by hand on two plans, since the click is the part no test here can reach:

  • excellent-parallel-spill.sqlplan — the Operator Warnings (12) index, each entry navigating to its operator; the 8 plan-wide statement warnings correctly showing no arrow.
  • table_variable_plan.sqlplan — the one statement warning that can attribute, carrying its link.

Navigation and scroll-into-view behave. Erik checked it on a real display; I could not, because this box has no reachable display session (screencapture fails), which is why the app was launched for him rather than screenshotted.

🤖 Generated with Claude Code

A warning did not record where it came from, so on a large plan there was no way to
get from a finding to the thing that produced it. pgfiore's framing was exactly
right: "if the plan is huge and the warning origin is murky, it would help to click
a warning and be beamed up to a specific step."

PlanWarning.OriginNodeIds carries it, and the interesting half of this feature is
knowing when to say nothing. Three honest answers, not one:

- A key lookup came from exactly one operator.
- A table variable warning came from every operator that touched one, which on a
  real plan is several. That rule already walked the tree and knew precisely which
  ones; it threw the answer away before emitting. It does not any more.
- "High Compile CPU" happened before a single row was read, and SQL Server reports
  "UDF Execution" at the statement level only. Those have NO operator origin and
  now say so, so the UI offers no link rather than one that goes somewhere
  arbitrary. Sending a reader to the wrong operator is worse than sending them
  nowhere, because they would believe it.

Operator warnings are stamped in ONE place, at the end of AnalyzeNode, rather than
at the 26 sites that add one - the same reasoning as the provenance stamp in #439
and the ceiling in #438. A rule you have to remember at every construction site is
a rule that eventually gets forgotten, and the once it is forgotten the UI quietly
drops a link that existed. It only fills what a rule left empty, so a rule that
knows better keeps its own answer.

The scope call worth reviewing. The pop-up the issue is about shows STATEMENT
warnings, and few of those can attribute to an operator - so linking only those
would not have served the huge-plan case that motivated the request at all. The
warnings that do have origins are the operator ones, and until now the only way to
see one was to have already clicked the operator carrying it, which is no help when
you do not know which operator to click. So the statement panel also gains an
"Operator Warnings" section indexing every warning in the tree, each one a link.
Nothing is removed from the per-operator panel; this is an index into it. It is
collapsed by default because on a large plan it is the longest section in the panel
and expanding it would push the statement's own details off screen, which is the
opposite of the problem being solved.

The tree walk lives in Core as WarningIndex rather than beside the panel that
renders it: it is a walk over Core's own models with nothing UI about it, and there
it can be tested without standing up Avalonia. It uses an explicit stack rather
than recursion, because a deep plan is precisely the case this feature exists for
and #430 was a crash caused by assuming operator trees are shallow.

CLI output contract: "origin_node_ids" is additive on every warning, so
ExpectedCompactOutputSha256 is rolled - second time, both additive, both
deliberate. Verified that origin_node_ids is the ONLY new key rather than assuming
it: the full key set on a warning is otherwise unchanged.

WarningBaseline.txt does not move. It digests type, severity and message, none of
which changed, so no committed plan's verdict is affected.

Tested: 314 total, 312 passed, 0 failed. The new tests pin both directions - that
across every committed plan no operator warning is left without an origin or points
away from its own node, that the table variable warning names the operators that
touch one, and that High Compile CPU and UDF Execution claim none. The index is
compared against an independent recursive walk rather than a hand-written count, so
it cannot drift as fixtures are added.

Not verified: the click itself. This box has no reachable display session, so
screencapture fails and I could not watch a warning navigate. The app was launched
on a plan carrying both kinds of warning and ran clean with no exceptions, and the
logic underneath is covered, but someone with a screen should confirm the scroll
lands where it should before this is trusted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Aug 21, 2026

Copy link
Copy Markdown

Reviewed the diff. This is a clean, well-scoped change — traced the data flow end to end and didn't find correctness issues:

  • WarningIndex.CollectOperatorWarnings uses an explicit stack (not recursion), consistent with the [BUG] Robot Advice crashes the app #430 deep-tree crash fix it references.
  • The new "Operator Warnings" / navigation-link UI (ShowPropertiesPanel in PlanViewerControl.Properties.cs) is gated behind node.Parent == null && _currentStatement != null, so s.RootNode and _currentStatement always match what's actually rendered on PlanCanvasTryNavigateToNode's lookup against _nodeBorderMap can't target a node from a different statement's tree. Verified this by tracing RenderStatement (clears PlanCanvas/_nodeBorderMap per statement) against where ShowPropertiesPanel is invoked.
  • CheckForTableVariables's two new optional list params (referencingNodeIds/modifyingNodeIds) are correctly threaded to the two distinct statement warnings (all referencing operators vs. only modifying ones) — no cross-contamination between the two.
  • The single-point stamping in AnalyzeNode (PlanAnalyzer.Node.cs) only fills OriginNodeIds when empty, and grepping for all OriginNodeIds assignments confirms no rule currently sets an origin other than the node's own id or the two table-variable lists — consistent with the new EveryOperatorWarningKnowsItsOperator test's invariant.
  • origin_node_ids is additive-only in AnalysisResult.cs/JSON output, and the CLI contract hash roll in HistoricalCliContractTests is justified by that.
  • WarningIndex.cs is new but isn't referenced by PlanViewer.Web (no Blazor UI change here), so it correctly isn't added to PlanViewer.Web.csproj's linked-file list — not a convention violation.
  • Test coverage (WarningOriginTests.cs) is solid: pins both directions (origin present where expected, absent where "High Compile CPU"/"UDF Execution" have no responsible operator), cross-checks the index against an independent tree walk rather than a hardcoded count, and covers the null-root edge case.

No blocking findings. Nice attention to the "sending a reader to the wrong operator is worse than nowhere" invariant, and it's actually enforced by tests, not just asserted in the description.

Note: I wasn't able to run dotnet build/dotnet test in this sandbox (command approval unavailable), so this review is based on static tracing only — worth confirming CI is green before merge.

@erikdarlingdata
erikdarlingdata merged commit 9c3fceb into dev Aug 21, 2026
4 checks passed
@erikdarlingdata
erikdarlingdata deleted the feat/440-link-warnings-to-origin branch August 21, 2026 11:38
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