Skip to content

Release v1.27.0 - #571

Merged
erikdarlingdata merged 56 commits into
mainfrom
dev
Sep 25, 2026
Merged

erikdarlingdata merged 56 commits into
mainfrom
dev

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Summary

  • This PR starts the v1.27.0 release again. The Release run for v1.27.0 #568 did not sign (v1.27.0 release did not sign after the SignPath action moved to v3 #569), so no v1.27.0 release exists. Merging this PR starts a fresh Release run.
  • The SignPath GitHub App is now installed on this repository. The missing app caused the signing failure.
  • The required check-version status fails on this PR, because main already has version 1.27.0 from v1.27.0 #568. This failure is expected, so merge as an admin. The version is still higher than the latest release, v1.26.0.
  • Use "Create a merge commit". v1.27.0 #568 was a squash merge, so the dev commits are not in the history of main. A merge commit adds them back.

Changes

  • The only file changes between main and dev come from Correct the SignPath comments for action v3 #570. It corrects comments in .github/workflows/release.yml and .signpath/policies/PerformanceStudio/release-signing.yml.
  • The "Files changed" tab shows more than that. GitHub compares dev with e6593df, the last commit that main and dev share. Main already has those changes from the v1.27.0 #568 squash merge.

Test Plan

  • build-and-test passes.
  • After the merge, the Release run signs the Windows build. Approve the signing request in SignPath.
  • The Release run creates the v1.27.0 release with all of its assets.

Part of #569.

Generated with Claude Code

https://claude.ai/code/session_011ujjGZ64tzhVqkEBfaxe4o

erikdarlingdata and others added 30 commits September 15, 2026 22:06
Captured from the 1.26.0 UI: the plan view with the rebuilt insights
strip and icon set, and Advice for Humans as severity cards.

Worth saying plainly: the screenshots the README already points at are
now stale. They show emoji toolbar icons, the old monospace advice dump
and the pre-overhaul properties panel, none of which the app looks like
any more. These two are the current article; swapping the README over
to them is a separate pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every screenshot the README pointed at showed the pre-1.26 UI: emoji
toolbar icons, the monospace advice dump, the properties panel before
the per-thread rows were folded up, and a plan comparison that was a
wall of text. Someone landing on the repo saw a product that no longer
exists.

All seven recaptured from the shipped UI. Three captions were describing
the old behaviour and are rewritten rather than left to contradict the
picture above them: advice is cards now and not a "one-click text
report", comparison is a metric diff and not "side-by-side", and the
properties panel is worth describing in terms of what it stopped doing.

The two 1.26-suffixed files added for the release announcement are
folded into the canonical names here, so there is one copy of each.

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

Refresh the README screenshots for 1.26
ci.yml skipped itself on docs-only changes via paths-ignore. That is
fine while nothing depends on it and fatal the moment the check is
required: a workflow skipped by a path filter reports nothing at all,
GitHub leaves the check pending forever, and the PR can never merge.
The screenshots PR that just went in would have been unmergeable.

The filtering moves into the job. It always runs, so the check always
reports; the steps that cost anything are guarded and skip on a
docs-only change. This is the shape PerformanceMonitor's build.yml
already uses, and its comment says why.

The filter is a positive list of code paths rather than the old ignore
list inverted. paths-filter ORs the patterns within a filter, so a stack
of negated patterns matches whenever a file fails any one of them, which
for a docs-only change is always true. Listing what IS code cannot go
wrong that way. PlanViewer.Ssms and PlanViewer.Ssms.Installer stay out
because they are not in the solution and ci.yml never built them.

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

Make the CI check report on every PR
Bumps [signpath/github-action-submit-signing-request](https://github.com/signpath/github-action-submit-signing-request) from 2 to 3.
- [Release notes](https://github.com/signpath/github-action-submit-signing-request/releases)
- [Commits](SignPath/github-action-submit-signing-request@v2...v3)

---
updated-dependencies:
- dependency-name: signpath/github-action-submit-signing-request
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps Avalonia from 11.3.20 to 11.3.22
Bumps Avalonia.Desktop from 11.3.20 to 11.3.22
Bumps Avalonia.Fonts.Inter from 11.3.20 to 11.3.22
Bumps Avalonia.Headless from 11.3.20 to 11.3.22
Bumps Avalonia.Themes.Fluent from 11.3.20 to 11.3.22
Bumps Microsoft.Data.SqlClient from 7.0.3 to 7.1.0
Bumps Microsoft.Data.SqlClient.Extensions.Azure from 7.0.3 to 7.1.0
Bumps Microsoft.NET.Test.Sdk from 18.10.0 to 18.10.1
Bumps Microsoft.Testing.Extensions.HangDump from 2.4.0 to 2.4.1

---
updated-dependencies:
- dependency-name: Avalonia
  dependency-version: 11.3.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Avalonia.Desktop
  dependency-version: 11.3.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Avalonia.Fonts.Inter
  dependency-version: 11.3.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Avalonia.Themes.Fluent
  dependency-version: 11.3.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Microsoft.Data.SqlClient
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: patch-and-minor
- dependency-name: Microsoft.Data.SqlClient.Extensions.Azure
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: patch-and-minor
- dependency-name: Microsoft.Data.SqlClient
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: patch-and-minor
- dependency-name: Microsoft.Data.SqlClient.Extensions.Azure
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: patch-and-minor
- dependency-name: Microsoft.Data.SqlClient
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: patch-and-minor
- dependency-name: Microsoft.Data.SqlClient.Extensions.Azure
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: patch-and-minor
- dependency-name: Avalonia
  dependency-version: 11.3.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Avalonia.Desktop
  dependency-version: 11.3.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Avalonia.Fonts.Inter
  dependency-version: 11.3.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Avalonia.Headless
  dependency-version: 11.3.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Avalonia.Themes.Fluent
  dependency-version: 11.3.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Microsoft.Data.SqlClient
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: patch-and-minor
- dependency-name: Microsoft.Data.SqlClient.Extensions.Azure
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: patch-and-minor
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.10.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Microsoft.Testing.Extensions.HangDump
  dependency-version: 2.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Connecting to a server from a fresh Query tab changed neither of the
two things the "Get started" overlay watched (typed text, open
documents), so the opaque panel kept covering the editor and kept
offering "Connect to a server" on a session that just did. The editor
was alive and focused underneath, but nothing said so.

A connected session is in use even with nothing typed: the empty-state
predicate now also requires having no server connection, and the
connect block refreshes it right after flipping the toolbar, before
the metadata round trips, so the editor appears the moment the dialog
closes. The connection dialog is the only place a session ever gains
a connection, and nothing ever takes one away, so that one call site
covers every path.

TheOverviewDoesNotCountAsHavingOpenedSomething pinned the old
behavior (overlay up on a connected session, a state the app can no
longer reach); its real claim, that the Overview opens no documents,
was already carried by its strip assertion, so its overlay pin now
points down.

Fixes #540

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr43FouJ6RizUyoWQeR5FX
…nect

Show the editor as soon as a session connects
The connection dialog only closes with true after it opened a real
connection and enumerated the databases the login can see, through
the database the user named — the one some logins (Azure SQL DB, JIT
access) can open when master is off limits. Both connect handlers
then flipped their toolbar green and re-enumerated through a second,
hardcoded-master connection, swallowing failure: a server blip in
that window, or a master-blocked login every time, left a green
"connected" toolbar over a disabled, empty database picker.

The dialog now hands its list over (ResultDatabases) and the session
and plan toolbars fill their pickers from it; both redundant
enumerations are deleted, so the green flip is always backed by the
connection that just succeeded, and connecting costs one round trip
fewer. The plan-side enumeration's READ UNCOMMITTED hint moves to the
dialog's query, which replaced it.

The standalone actual-plan tab told the same lie by a different
route: SetConnectionStatus painted the label green while the picker
stayed at its XAML-disabled default, and _planConnection was never
set, so a filled picker could not have switched the connection string
anyway. AdoptConnection takes the validated connection plus the
dialog's list and wires all of it.

Follow-up to #540 / #541.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr43FouJ6RizUyoWQeR5FX
…databases

Fill the database picker from the connection that just opened
A user held the 1.26 card view against the old report and named
Server Context and Summary as the sections the report did better
(#540). He was half right: the summary counts earn their chips, the
critical count being the one red thing on screen, but the seven
neutral context facts were chips too, and seven facts in identical
pills make a row to decode where the report had a block to scan. A
chip earns its keep by popping out of a row; a fact that is supposed
to be there pops nothing.

The facts are labelled rows again - Hardware, Instance, Database -
muted label, full-strength value, the same convention the runtime
summary uses. The pills that remain from the context are the
settings that deviate from a healthy default (RCSI, stats off,
scoped configs), on their severity colours, which is the one job a
pill has. Summary chips and the warning-type tags stay as they were.

The new test also pins the backfilled backgrounds on these rows: the
press-anywhere test's fixture carries no ServerContext, so without
it these are the one set of blocks the glyph-only hit-test trap
could quietly reclaim.

Fixes the remaining half of #540.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr43FouJ6RizUyoWQeR5FX
…acts

Give the advice header's context facts their labels back
…s/dev/signpath/github-action-submit-signing-request-3

ci: bump signpath/github-action-submit-signing-request from 2 to 3
…nViewer.App/dev/patch-and-minor-4858b7243e

deps: Bump the patch-and-minor group with 9 updates
Nine package lines move together, because none of them builds without the
others: the App's Avalonia core, Desktop, Themes.Fluent, Fonts.Inter and
Controls.DataGrid to 12.1.2, AvaloniaEdit and AvaloniaEdit.TextMate to 12.0.0
(no 12.1.x exists), ScottPlot.Avalonia to 5.1.59, and the test project's
Avalonia.Headless to 12.1.2. Nothing loud enforces that atomicity: every
nuspec floor is minimum-only, so a partial bump restores and compiles green
and then dies at the first HeadlessUnitTestSession.StartNew with an
Avalonia-internals TypeLoadException. If that signature ever appears, check
the two csprojs before suspecting anything else.

TopLevel.PlatformSettings is removed in 12, and it is the only hard compile
break in the whole project. The replacement is the VisualExtensions method,
whose using was already present; it returns null when detached exactly as the
old null-conditional chain did, so the Control fallback keeps its meaning.

Baseline before the bump, for the zero-deleted gate: 713 tests total, 712
passed, 1 skipped (the pre-existing non-Windows Entra contract), 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr43FouJ6RizUyoWQeR5FX
TextBox.Watermark is obsolete in 12 in favour of PlaceholderText, across 12
XAML sites and one code-behind constructor. The literal Watermark= rename is
only safe because the app has no UseFloatingWatermark site; that one renames
to UseFloatingPlaceholder, so nobody should generalize this recipe. The proxy
input's parameter and the two prose comments that talk about "the watermark"
move with it, because a control named one thing and described as another is
how the next reader loses an afternoon.

The CS0618 pragma in MainWindow wrapped nothing but a string array: the
drag/drop migration moved the deprecated calls out from under it some time
ago, and in 12 the APIs its comment names do not exist at all.

Three comments stated mechanisms that 12 changed:

- The ScottPlot line carried a do-not-bump-yet gate pointing at this very
  migration. It is executed, so the gate goes rather than being reworded.
- The SkiaSharp pin's comment claimed it prevents issue #139. It never could:
  the collision is ScottPlot's NoDependencies package shipping its own
  libSkiaSharp.so, a different package id that a PackageReference on this one
  cannot constrain. Under 12 the pin is inert on top of that, since
  Avalonia.Skia already floors the same id at the same version. The comment
  now says what is true and why the pin stays anyway.
- DataGrid 12.0.1 (PR #241) moved AllowAutoHide propagation out of
  OnApplyTemplate and into a template binding on each PART_ bar. The binding
  reads the same attached property off the same object, so App.axaml's rule
  still reaches the grids, but the reason it works is no longer the one the
  comment gave.

The one-line assertion added to the DataGrid scrollbar test is the only
falsifiable guard on the GetPlatformSettings migration: a manual Ctrl+C on
Windows cannot tell a dead lookup from a live one, because the fallback the
guard uses is Control and Control is what Windows reports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr43FouJ6RizUyoWQeR5FX
Three things in HeadlessUi were written against 11 and are no longer true of
the package it wraps.

The reason for hand-rolling instead of using Avalonia.Headless.XUnit has
expired without the conclusion changing: the package asked for xunit v2 when
this was written and asks for xunit.v3.extensibility.core 3.2.2 now, which
unifies upward against this project's 4.0.1. Adopting it is possible and
still wrong, because [AvaloniaFact] offers neither the #474 drain nor the
canary. Leaving the dead objection in place would have let the next reader
check it, find it false, and switch on that basis.

The #474 note claimed a mechanism 12 half-fixed. The FontManager is still
disposed one line before ResetForUnitTests runs the queue, so the throw is
exactly as available as it was and the drain is still the only thing that
prevents it. What 12 changed is the aftermath: scope disposal moved into a
finally and the failure is routed to the dispatch's task, so one test dies
instead of every test after it. Stated that way round because the original
prescription, read quickly, invites deleting the drain.

Which leads to the guard on GetResult(). 12 reports a teardown failure
through the dispatch's own task, awaited outside both catches, so it arrived
as a throw from Dispatch rather than as a returned failure. Probed under
12.1.2 rather than reasoned about: a queued job that throws during teardown
comes out of GetResult(), and when the body had already failed its assertion,
the teardown exception is what the caller sees and the assertion message is
gone. That inverts the precedence Run documents, so the wrap keeps the body's
failure first.

The metrics paragraph is new and is the one place this branch explains why
layout numbers across six test files moved: 12 drops headless's stub shaper
for real HarfBuzz over its own embedded font, which makes a character one em
wide instead of a flat 10 DIP and a line 1.0898 em tall instead of 0.8. Every
re-measured threshold carries its number and points here for the reason.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr43FouJ6RizUyoWQeR5FX
Two assertions and four comments, all on the width axis, all re-measured
rather than relaxed.

At a 1520px viewport the session toolbar's overflow menu now holds four
entries instead of three. The fourth is QS Overview, which is the next name
in CollapseOrder, and no protected command moved: Connect, Execute and
Execute-with-estimate are all still on the row, and every button in front of
the tail is still at the same X. That is a wider row, not a different one.

The numbers behind it, measured at 12.1.2: the row's natural width is 2116px
where it was 1910, and the uncollapsible floor -- Connect, the server label,
the database picker and the two plan verbs -- is 746px where it was 708. Both
are about 1.1x, which is what FontSize / 10 comes to at the toolbar's 11px
text. The chevron's 28px height did not move, because nothing about it is
text.

The DataGrid scrollbar comment is corrected on both halves, since both were
wrong in different directions. The mechanism half described DataGrid
assigning AllowAutoHide in code, which PR #241 replaced with a template
binding in 12.0.1 -- the app's rule still reaches the bars, for a different
reason than the comment gave. The premise half said the rows measure
zero-high for want of a font. Under 12 there is a real font with real
metrics and the rows still do not realize, so the comment now says what
actually stops them, which is that the grid generates no columns at all.
That is pre-existing and left alone here; this commit corrects the
explanation, not the test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr43FouJ6RizUyoWQeR5FX
The rewritten pin comment asserted that the NoDependencies package overwrites
the right .so at publish time. That is the shape of issue #139, but it is not
what this graph does, and a comment that states a failure mode as current
when the branch's own publish check disproves it is the same defect the rest
of this commit series is removing.

Measured instead of assumed: a linux-x64 self-contained publish puts a
libSkiaSharp.so that is SHA256-identical to SkiaSharp.NativeAssets.Linux
3.119.4's copy and different from the NoDependencies 3.119.0 one, alongside
managed SkiaSharp 3.119.4. The deps file names only the 3.119.4 package as
the native provider, though NoDependencies is still in the restore graph
underneath ScottPlot. The point the comment exists to make is unchanged: the
pin is not what decides that, because it cannot reach the other package id.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr43FouJ6RizUyoWQeR5FX
The rewritten header said the row is narrower than either harness figure on
real fonts. Nothing here measures that. The 1536-logical story is what the
file is about and stands on its own; the two numbers are harness thresholds,
so say that instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr43FouJ6RizUyoWQeR5FX
The metrics paragraph gave 1.0898 em and then four whole-DIP measurements,
which do not multiply out unless you know layout rounds the desired height
up. Say that, so the next reader checking 1.0898 x 12 against the 14 in a
test does not conclude one of them is wrong.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr43FouJ6RizUyoWQeR5FX
Avalonia 12 hosts a Window inside a TopLevelHost, so a Window is no longer
the root of its own visual tree. GetParentWindow read VisualRoot and cast it,
which now always misses, and it threw "No parent window" for every dialog a
query session owns: the connection dialog, the format dialog, the plan
dialogs and the open-in-editor overwrite prompt. Three tests caught it;
opening any of those four in the app would have.

This is the second half of the same breaking change the DataGridBehaviors fix
handled, and the more dangerous half. TopLevel.PlatformSettings was deleted,
so the compiler stopped the build and the migration probe saw it. VisualRoot
still exists, still compiles, and still returns an object -- just not the one
the cast wants -- so nothing reports it until something asks for a window at
runtime. Worth remembering the next time a green build is mistaken for a
migrated one.

TopLevel.GetTopLevel is what the rest of the app already uses for this, at
thirteen sites including QueryStore.cs and QueryStoreHistoryControl, and it
still resolves to the Window under the new hosting. Measured rather than
assumed on an attached session: the visual chain now ends MainWindow <
TopLevelHost, while GetTopLevel and FindAncestorOfType<Window> both return
MainWindow and the clipboard still resolves through it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr43FouJ6RizUyoWQeR5FX
…ctor

Two comment strengthenings the numbers alone do not carry.

The overflow assertion now records what is invariant versus what is merely
current. How many commands sit in the menu at a given width follows the
harness's text metrics and is expected to move with them. Which commands may
leave at all does not: Connect, Execute and Execute-with-estimate are absent
from CollapseOrder and must never appear in that menu at any width. The
exact-collection assertion is what enforces it, which is the reason it stays
an exact collection rather than a count.

The metrics paragraph now says why the height factor is 1.36x and not the
1.25x someone will reach for: 1.25 is the em box alone, and this face carries
a line gap on top of it. The font manager reports ascent 819, descent 205 and
gap 92 over an em of 1024, which are the OS/2 typographic values rather than
the hhea pair that would have meant 0.854 em. So 1.36x is the tell for a
height that moved, and a height that moved by anything else has a different
cause.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr43FouJ6RizUyoWQeR5FX
Avalonia 12's headless bootstrap loses a race often enough to fail one
arbitrary test in roughly a quarter of full-suite runs. It throws from
Dispatcher.VerifyAccess while EnsureIsolatedApplication builds the per-
dispatch Application: AvaloniaHeadlessPlatform.Initialize constructs a
Compositor, and DefaultRenderLoop.Add finds a different thread owning the
dispatcher. No code of ours appears above HeadlessUi.Dispatch in the stack.

Measured before writing anything: 2 of 6 full-suite runs on this bump, 1 of 6
more with the two classes that were red for an unrelated reason excluded, and
1 of 6 with xunit parallelization disabled outright, which is what rules out
test concurrency as the cause. The same measurement on 11.3.22 was 0 of 6. A
different test is the victim every time. There is no newer Avalonia to take.

So the dispatch is retried once, and only when the dispatched delegate was
never entered. That is the whole safety argument: this does not re-run a
test, it starts one that never ran. The started flag is the first statement
inside the delegate, so "the body never began" is a fact rather than an
inference, and the exception match is narrow on purpose -- the type AND one
of the two upstream frames -- so a thread-affinity bug in our own code names
our own frames and gets reported rather than papered over. One retry only;
twice in a row and the run goes red. Every occurrence writes a line to
stderr, because a retry nobody can see is how a 1-in-4 flake turns into a
1-in-400 mystery.

Verified against the real race rather than a simulation of it: over twenty
consecutive full runs the marker appeared three times and no run failed. Ten
consecutive runs through dotnet test, the way CI runs it, were green at 713
tests.

Remove it when an Avalonia release fixes the race. The removal condition and
the evidence rates are in the doc comment, along with the ten-run check that
makes deleting it falsifiable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr43FouJ6RizUyoWQeR5FX
The retry's stderr line now points at the opened issue (#544) instead
of a placeholder, and the harness doc's three metrics paragraphs are
three sibling paragraphs rather than one nested inside another.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr43FouJ6RizUyoWQeR5FX
Both read-only schema viewers installed a TextMate installation at
construction and never disposed it. An installation owns a
tokenization model whose thread roots itself against collection, so
every schema tab ever closed leaked a live thread for the life of the
process.

Both now run the query editor's own lifecycle: install on attach
(guarded so a re-attach after a dispose reinstalls), dispose and null
on detach. Detach fires on tab switches as well as closes, which is
exactly the dance the query editor has always survived, on 11.4.1's
reuse-the-disposed-transformer quirk today and on 12's explicit
ownership when the upgrade lands.

Fixes #546

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr43FouJ6RizUyoWQeR5FX
…dispose

Dispose the schema viewers' TextMate installations
Avalonia 12 completes a style-applied animation when its visual
detaches - a handler its changelog does not document, outside the
PlaybackBehavior pause guard - and re-attaching re-applies the style
without resurrecting the finished animation. Switch tabs while a
fetch or capture is running and its indeterminate bar comes back
frozen mid-track, indistinguishable from a hang. Avalonia 11 had no
detach handling at all, so the bar just kept sweeping.

ProgressBarBehaviors.RestartOnReattach deactivates and reactivates
the :indeterminate style one dispatcher hop apart on reattach, which
spawns a fresh animation instance. Applied to all seven bars whose
hosts detach mid-run: the two execution overlays, the actual-plan
loading tab, and the QS grid, wait-stats, QS history and Overview
loaders.

Proven against the live symptom in a driven run, both directions:
five window captures spanning a full animation cycle after a tab
round-trip were pixel-identical without this, and sweep again with
it. The headless suite cannot see animations, so that driven pair is
the regression evidence.

This was the plan's pre-classified bucket (b) item for the manual
pass, found exactly where the review predicted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr43FouJ6RizUyoWQeR5FX
erikdarlingdata and others added 26 commits September 21, 2026 21:19
No app code ever referenced it: the package ships its own
RegistryOptions and ThemeName under the AvaloniaEdit.TextMate.Grammars
namespace, and every call site binds the TextMateSharp.Grammars pair
instead, which arrives through the TextMateSharp.Grammars 2.0.4
reference that stays. Dropping it also removes Newtonsoft.Json 13.0.1
and the stale TextMateSharp 1.0.23 floor from the resolved graph.

Kept out of the Avalonia 12 migration deliberately (a runtime-graph
change had no place in that release); shipping now as its own change.

Fixes #545

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr43FouJ6RizUyoWQeR5FX
AccessText takes the character after the first underscore as an
access key and strips the underscore from display, so "Treat [ ] _
as literals" rendered without the one character that is half of what
the checkbox is about. Doubling it is the escape: __ renders as a
literal underscore and registers no access key.

Fixes #547

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr43FouJ6RizUyoWQeR5FX
The existing DataGrid case turns out to assert AllowAutoHide on bars
that never apply their template, because the grid it builds has no
columns and Avalonia's DataGrid does not auto-generate them — so no
row realizes and nothing overflows. The bound flag it checks is still
meaningful, and it stays exactly as it was; its construction comment
now says what is actually happening instead of claiming a real grid.

The new sibling gives a grid columns, makes it overflow both ways,
and pins the state every grid in the app actually runs in: templated
bars exist, each keeps AllowAutoHide from the attached property the
App.axaml rule sets (12 binds it in-template), and the vertical bar
is visible. Zero templated bars realize without columns, so the
NotEmpty assertion alone separates the two cases.

Fixes #548

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr43FouJ6RizUyoWQeR5FX
…ckage

Drop the unused AvaloniaEdit.TextMate.Grammars package
…label

Let the escape-brackets label keep its underscore
…-test

Pin the scrollbar contract on a grid whose bars actually realize
The attach handler focused the editor synchronously, mid-event, while
the tab strip and window were still arbitrating focus - so whatever
ran after the handler won, and the first keystrokes into a fresh
query tab landed nowhere until something was clicked. The same in a
real window on Avalonia 11 and 12; invisible headlessly, because no
arbitration runs there.

Focusing one Loaded-priority hop later lands after the layout pass
and sticks. Focus() on a control whose tab was switched away before
the post fires is a benign no-op in both Avalonia lines (no visual
root means no focus manager means false, not a throw), so the
deferral needs no guard. The other FocusEditor call sites run from
input handlers after arbitration settles and stay synchronous.

Drive-verified both directions: fresh launch, Ctrl+N, immediate
typing - lost before the change, colorized in the editor after it.

Fixes #551

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sr43FouJ6RizUyoWQeR5FX
IsFunctionOnColumnSide split the whole predicate at its first comparison
operator. In a compound predicate that put every later comparison, column
and all, on the function's side, so
[t].[A]=CONVERT_IMPLICIT(int,[@1],0) AND [t].[B]=CONVERT(tinyint,[@2],0)
reported a conversion of a parameter as a function on a column. The same
mistake fired on the everyday date range
[d]>=dateadd(day,(-7),getdate()) AND [d]<getdate().

The side check now reads only the comparison that holds the function:
the text between the nearest AND/OR on either side, split at every depth
so parenthesized OR groups come apart too, and never inside a string
literal or a bracketed name.

Two more false positives from the same family, fixed with it:
- ISNULL/COALESCE was flagged wherever it appeared. It now gets the same
  side check, so ISNULL(@p, 0) on the parameter side is quiet while
  col = ISNULL(@p, col) is still caught, and a parameter-side ISNULL no
  longer steals the message from a real function on a column.
- LIKE was not recognized as a comparison, so [col] like upper([@p]) fell
  to the assume-the-worst default and was flagged.

DetectNonSargablePattern is split out as an internal seam so predicate
shapes can be tested as raw strings, the way #436 tested
ConvertImplicitWrapsColumn. The reporter's plan is a new fixture. Both
golden masters change only by that fixture: the warning digest gains its
section (a true Scan With Predicate, no Non-SARGable), and the comparison
digest gains its self and next pairings plus the one predecessor pairing
that now points at it.

Fixes #556

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E9G1hWs6Nk4h4U2gn7TSy5
…te-sides

Read only the function's own comparison in the non-SARGable check
Rule 15 matched the operator shape alone. WHERE t.A IN (@p1, @p2) on an
indexed column builds the same Nested Loops / Merge Interval / TopN Sort /
Concatenation / Constant Scan chain as a dynamic seek over the parameter
values, so a query with no join was told to rewrite as UNION ALL (#558).

The rule now also reads what each lookup branch produces, and fires only
when at least one branch takes a value from another input: an outer column,
or an expression the outer input computed ([Expr1002] for ON t.A = o.X + 1
OR ...). Branches of parameters, variables, literals and functions of them
are a parameter list. A branch with nothing to read keeps the warning.

Fixtures: the reporter's plan, plus a mixed column/parameter OR join and an
OR join on outer expressions, both captured on SQL Server 2022.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ZVrq8tpA2DBPEFEqahFK6
…r-in-list

Join OR Clause: skip the dynamic seek for a list of parameters (#558)
A plan from the plan cache or Query Store records an sp_executesql
statement with its declaration list in front: "(@p1 int, @p2 int)SELECT".
Substitution read those names as references, so the advice, exports, MCP
tools, "Copy Query Text (with values)" and planview's text output showed
"(10 int, 20 int)SELECT ...", which neither matches the plan nor runs.

Only the statement after the list gets values now, and when it gets any,
the list is dropped. With no values the text stays as the plan recorded
it. A list that never closes (the plan cut the text off at 4,000
characters inside it) is left alone. parameterized_statement_text still
carries the original.

ReproScriptBuilder had its own copy of the list parser; both now use
ParameterSubstitution.DeclarationListEnd, which the web project can see.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ZVrq8tpA2DBPEFEqahFK6
…ation-list

Leave the sp_executesql declaration list out of substituted statement text
Expensive Operator compared one operator's own time against the whole
statement's elapsed time and fired at a 20% share. In a statement that
finishes in under a second, one or two operators always take most of
the (tiny) time just because there is almost nothing else to divide it
among, so the share pointed at nothing.

Add a 1,000ms floor on statement elapsed time before the rule runs, matching
the floor rule 19 uses for compile CPU and rule 4 uses to call UDF time
Critical.

Closes #562
…riable column

Rule 12 relies on ColumnReferenceRegex, which requires a dotted name
([table].[col]) to recognize a column. An aliased table-variable column
renders dotted through the alias (e.g. "abs(@tv.[X] as [v].[X])=(1)"), so that
case already worked. An unaliased one renders as a bare bracketed name with no
dotted qualifier at all (e.g. "abs([X])=(1)"), confirmed on SQL Server 2016,
2017, 2019, 2022 and 2025. The regex cannot see that as a column on its own,
since the same shape could be a parameter or an expression, so the function
call and the implicit conversion in the fixtures below never got flagged.

Add IsTableVariable(node) and thread an isTableVariableScan flag from
DetectNonSargablePredicate through DetectNonSargablePattern into a new shared
IsColumnReference helper, used by both ConvertImplicitWrapsColumn and
IsFunctionOnColumnSide. On a confirmed table-variable scan, a bare name now
also counts as a column, unless it is a parameter or variable, an
optimizer-generated expression, or a function call. Every other scan keeps
today's behavior, since the flag defaults to false.

Also corrects two comments that claimed an unaliased table-variable column
renders as [@tv].[col] — no SQL Server version does this.

Closes #561
The rewritten ColumnReferenceRegex comment said an aliased table-variable
column failed the old pattern. It did not: the alias part [v].[X] has no @,
so the first cut matched it. The [@tv].[col] shape that the first cut was
blamed for never occurs on SQL Server 2016 through 2025, so the history goes
and the comment states only what each version renders.

IsColumnReference now names the case it still reads wrong: a bare outer
reference from another unaliased table variable (#564).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ZVrq8tpA2DBPEFEqahFK6
…or-floor

Require 1,000ms statement elapsed before rule 35 fires
…are-columns

Detect a non-SARGable function or conversion on an unaliased table-variable column
…ility

Rule 12 counted every column-shaped name in a scan predicate as the
scanned table's own column, including a value a Nested Loops join
passed in from its outer input one row at a time. A function or
conversion wrapping only that outer reference does not cost the scan a
seek, so it should not be flagged.

DetectNonSargablePredicate now builds a ScanIdentity (alias, table,
whether it is a table variable, and the bare outer reference names a
Nested Loops ancestor passes in) and threads it down through
IsColumnReference, which checks ownership by alias or table instead of
just matching any dotted or bare name.

Closes #564

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ZVrq8tpA2DBPEFEqahFK6
The parser cleans a temp table's full tempdb name (#t, underscores, then a
hex suffix) down to #t in the scan's ObjectName. The unaliased owner in the
predicate was compared raw, so a plan that spells the full name in both
places would lose a real warning on the temp table's own column.
CleanTempTableName is now internal and the owner goes through it too.

Two string tests pin it: the scan's own column under its full name is
flagged, and another temp table's full name is not. Removing the cleaning
fails the first one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ZVrq8tpA2DBPEFEqahFK6
…columns

Skip outer references when checking scan predicates for non-SARGability (#564)
The v1.27.0 Release run did not sign. Action v3 sends requests to
SignPath's new Pipeline Connector, which needs the SignPath GitHub App on
the repository, and SignPath signs only the first 3 attempts of a run. The
app is now installed.

release.yml: note the app requirement and the 3-attempt limit above the
signing step, and correct the timeout comment, which still said a failed
signing leaves a published release.

release-signing.yml: note that the Pipeline Connector reads this file only
when the signing policy references it as a Pipeline Policy. Correct the
reasons for leaving out build.disallow_reruns (the release is now created
after signing) and branch_rulesets (main now has the "Protect main"
ruleset).

Comments only. Part of #569.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ujjGZ64tzhVqkEBfaxe4o
Correct the SignPath comments for action v3
@erikdarlingdata
erikdarlingdata merged commit 7a16042 into main Sep 25, 2026
3 of 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