Skip to content

Release v1.21.0 - #454

Merged
erikdarlingdata merged 6 commits into
mainfrom
dev
Aug 21, 2026
Merged

Release v1.21.0#454
erikdarlingdata merged 6 commits into
mainfrom
dev

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Release v1.21.0. Merging this fires release.yml: signs the Windows binaries through SignPath and publishes the GitHub release.

All three user-facing fixes were reported against 1.20.0 in the two days since it shipped.

User-facing

  • [FEATURE] In the top SELECT’s warning pop-up, link every message to its origin. #440 (pgfiore) — warnings record which operator produced them and link to it. Findings with no operator behind them — High Compile CPU happened before a row was read — deliberately carry no link, because a wrong one is worse than none. The statement panel also gained an Operator Warnings index, since the warnings worth navigating to were previously reachable only by already having clicked the operator carrying them. Adds origin_node_ids to JSON/MCP output.
  • [BUG] Tricky to compare plans for 2 separate queries #447 (joshdbe) — Compare Plans works across query sessions. Two queries in two tabs is the ordinary case and the button was disabled for it, even though the picker behind it could always see both.
  • [BUG] Error Cut off #448 (joshdbe) — a failed query shows its whole error. It was cut three times over: truncated to 100 characters, clipped by a non-wrapping label, inside a panel fixed at 300px. Now full, wrapped, and selectable.

Also: Query Store errors are no longer cut to 80 characters before reaching a status bar that already trims, and the bar carries the full text as a tooltip.

The suite can test the UI now

#447 and #448 were both real bugs that no test could reach — every test worked on Core models and the defects were in the UI. This release carries a headless Avalonia harness and eight tests over those two defects, each verified by reverting the fix rather than assumed.

Verification

  • 320 passing, 0 failed, across three consecutive full runs on macOS.
  • build-and-test green on ubuntu-latest for every constituent PR.
  • dotnet build clean; the 9 warnings are the pre-existing MCP9005 obsolete-API uses in McpSmokeTests.cs.

No Windows probe this time: release.yml's test command is unchanged since v1.20.0 and has now shipped successfully once.

Closes on release

#440, #447, #448.

🤖 Generated with Claude Code

erikdarlingdata and others added 6 commits August 21, 2026 13:37
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>
…#449)

UpdateCompareButtonState counted plans in the session's OWN sub-tabs, so two
queries in two separate sessions - one plan each - left the button disabled in
both, which is precisely the comparison it exists for.

The plans were always reachable. MainWindow.CollectAllPlanTabs spans sessions and
labels them "Query 1 > Plan", and the file-mode Compare button has always used it.
That is why the reporter's workaround worked: saving a plan and reopening it gave
him a file tab, whose button looks at the window-wide collection. Only the session
button was looking at the wrong one, in both its enablement and its own narrower
picker.

So the session button now asks the window for both, and hands off to the window's
picker rather than keeping a second one that cannot see past its own session.

The refresh is ONE subscription to MainTabControl.Items rather than a call added at
each of the sixteen places that add or remove a tab. That is the same reasoning as
#438, #439 and #440, and it matters more here than usual: a plan appearing in one
session changes whether Compare is available in every OTHER session, so the refresh
has to be window-wide and a call site that gets forgotten leaves a stale button
somewhere the author never looked.

Kept a fallback to the session's own count for when there is no owning MainWindow -
the control not yet attached, or hosted somewhere else - so the button is never
left in a stale state rather than throwing.

Tooltip updated, because it said "Compare two plan tabs" and now means something
wider.

No automated test, and I would rather say so than pretend. This is UI wiring and
the repo has no headless Avalonia harness; a test that would have caught it needs
two constructed sessions and a window. Building that harness for one bug is
disproportionate, and putting Avalonia into the test process is not something to do
casually given the test-host wedge in #441. A test on the arithmetic would be
hollow - count >= 2 was never the broken part, the SCOPE was.

Verifiable without a SQL Server, which is worth recording because two sessions
holding real plans otherwise need a live connection: open two .sqlplan files, then
New Query. The session's Compare button is enabled and its picker lists both file
plans. On dev it stays disabled, because the session has no plans of its own and
IsEnabled="False" is the XAML default.

Tested: 314 total, 312 passed, 0 failed - unchanged, since nothing here touches
Core. App builds clean and runs without exceptions on two plan tabs.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…#450)

A failed query reported an error that was cut off, and it was cut off three
separate times on the way to the screen:

- ex.Message[..100] + "..." at four sites in Execution.cs. A hundred characters
  does not even clear "Msg 208, Level 16, State 1, Server X, Line 1" before the
  sentence naming the actual problem starts, so the truncation reliably removed the
  only part worth reading.
- statusLabel had no TextWrapping, so it defaulted to NoWrap and clipped whatever
  survived the truncation.
- loadingPanel is a fixed Width = 300, sized for a spinner and a Cancel button
  rather than for prose.

Any one of those alone would have cut a real SQL error. Together they made the
message close to useless, which matches the report.

All four catch sites now go through one helper rather than repeating the display
logic - the same reasoning as #438, #439 and #440, and the reason it matters here
is that the four sites were already identical and already wrong in the same way,
which is what a copied line does over time.

The helper widens the panel on failure (MaxWidth rather than Width, so a short
error stays compact and a long one is bounded at a readable measure instead of
running the whole window), wraps, and colours it as an error.

The label is now a SelectableTextBlock. A SQL error is the one string in this app a
user most needs to get out and paste somewhere else, and it could not be selected.

Verified against a real server rather than by reading: SQL Server 2025 in Docker,
and a query against a deliberately long object name produces a 191 character error
where the old path stopped mid-word.

Not fixed here, and flagged rather than folded in: QueryStore.cs:219 truncates at 80
characters before handing the text to a status bar that already does
TextTrimming="CharacterEllipsis". Redundant and lossy, same family, but a different
surface and no issue filed against it.

No automated test. This is UI wiring and the repo has no headless Avalonia harness;
a test that would have caught it needs a constructed control tree. Two bugs in one
evening now sit in that gap, which is worth a decision about Avalonia.Headless
rather than a hollow test asserting that a string is not truncated by code that no
longer truncates it.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both of those were real bugs that no test in this suite could reach, because every
test here works on Core models and the defects were in the UI. Two in one evening
made it a gap worth closing rather than apologising for twice.

Avalonia.Headless.XUnit exists and would have been less code, but at 11.3.20 it
depends on xunit.core 2.4.0 - xunit v2 - and this suite runs xunit.v3. Two xunit
frameworks in one test project to obtain an attribute is a worse trade than owning
fifteen lines, so HeadlessUi drives HeadlessUnitTestSession directly, which is
runner-agnostic and is what that package wraps anyway.

Two things learned the hard way and written into the harness so nobody repeats
them:

- The session runs the REAL App, not a stub. A bare Application looked tidier but
  does not load the application XAML, and MainWindow's toolbars resolve styles from
  it - FindResource("AppButton") throws without them, which surfaces as an
  unrelated-looking "Failed to open" dialog rather than anything resembling a
  missing style. App.OnFrameworkInitializationCompleted only creates a window under
  a classic desktop lifetime, which a headless session is not, so nothing is
  spawned behind the tests.
- One session for the whole assembly, created lazily and never disposed. Avalonia
  allows one Application per process, and tearing a session down while another test
  class may still be queued is a good way to reintroduce the kind of wedge #441 was
  about.

The tests were checked by reverting the fixes, which is the only way to know they
test anything. #448's two - full text, and panel no longer spinner-sized - fail
against the old truncation. #447's fail against the old scope.

Worth recording, because it nearly fooled me: my first attempt at reverting #447
reverted only UpdateCompareButtonState, and all three tests still passed. That was
not the tests being weak - it was the revert being incomplete, because the
window-level subscription was still doing the work. Reverting BOTH halves fails two
of the three, and the one that still passes is the one asserting a single plan is
never enough to compare, which should hold either way. A partial revert is not a
check.

Stability, since putting Avalonia into the test process is exactly the shape of
change that produced #441: three consecutive full runs, 320 passing, 0 failed,
15-16 seconds each, no wedges.

Two entry points became internal for this - MainWindow.LoadPlanFile and
NewQuery_Click, plus QuerySessionControl.ShowExecutionFailure. InternalsVisibleTo
to the test project already existed.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…452)

Same family as #448, different surface. QueryStore.cs cut the message to 80
characters and appended an ellipsis before handing it to StatusText, which already
has TextTrimming="CharacterEllipsis". The trimming is the control's job and it does
it against the actual available width; doing it again in code threw away text the
control would otherwise have kept.

It also cost the only way to recover the rest. The bar is one line, so a long
message is readable on hover or not at all - SetStatus now sets the tip to the full
text, which costs nothing when the message fits and is the difference between a
truncated error and a recoverable one when it does not. That applies to every
status, not just this one, because every status goes through the same bar with the
same one-line constraint.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Minor rather than patch, on the same reasoning as 1.20.0: #440 adds
"origin_node_ids" to every warning in the JSON and MCP output, and an output-shape
change is not a patch even when it is additive.

What ships, all of it reported by users of 1.20.0 in the two days since it went
out:

- #440 Warnings record which operator produced them. The ones that can be linked
  are clickable in the app and carry origin_node_ids in JSON; the ones that
  genuinely have no operator behind them - High Compile CPU happened before a row
  was read - deliberately have none, because a wrong link is worse than no link.
  The statement panel also gained an Operator Warnings index, since the warnings
  worth navigating to were the ones you could previously only reach by already
  having clicked the operator carrying them.
- #447 Compare Plans is offered across query sessions. Two queries in two tabs is
  the ordinary case and the button was disabled for it, even though the picker
  behind it could always see both.
- #448 A failed query shows its whole error. It was being cut three times over -
  truncated to 100 characters, clipped by a label that did not wrap, inside a panel
  fixed at 300px - and the error is now selectable, which matters for the one
  string in this app people most need to paste elsewhere.

Also in, not user-facing: Query Store errors are no longer cut to 80 characters
before reaching a status bar that already trims, and the status bar carries the
full text as a tooltip so a long message is recoverable rather than lost.

And the suite can now test the UI. #447 and #448 were both real bugs that nothing
here could reach, so this release also carries a headless Avalonia harness and
eight tests over the two defects, each checked by reverting the fix rather than
assumed.

Tested: 320 passing, 0 failed, across three consecutive full runs. dotnet build
clean.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@erikdarlingdata
erikdarlingdata merged commit 62b3abf into main Aug 21, 2026
4 checks passed
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