Skip to content

Release v1.28.0 - #620

Merged
erikdarlingdata merged 117 commits into
mainfrom
dev
Sep 29, 2026
Merged

erikdarlingdata merged 117 commits into
mainfrom
dev

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Summary

This PR releases v1.28.0. It brings 31 PRs from dev to main: 30 merged since v1.27.0, plus the version bump in #619.

Before you merge, read these points:

  • Use "Create a merge commit". Do not squash. When v1.27.0 #568 was squashed, the dev commits never reached the history of main.
  • After the merge, the Release run asks for two SignPath approvals. The App comes first. The SSMS extension and its installer come second. Each request waits up to 30 minutes.
  • This is the first release that signs the SSMS files (Pin workflow actions to commit SHAs and sign the SSMS extension #610). If that request fails or times out, the release still ships, with the SSMS files unsigned and a warning on the run.
  • If the App signing fails, read the error text before you re-run. SignPath signs only the first 3 attempts of a run.
  • The merge also starts two deploys. deploy-planshare.yml ships the PlanShare server changes from Harden PlanShare: storage limit, daily upload budget, one client key #603 to stats.erikdarling.com. deploy-web.yml publishes the web viewer to plans.erikdarling.com.

Changes

Plan analysis and display:

Hardening:

App behavior:

CLI and web viewer:

Build and release:

Test Plan

  • The SSMS extension and its installer build from this tree with no warnings. CI never builds them, so I built them locally from a clean checkout with the commands release.yml runs. The VSSDK targets came from the Microsoft.VSSDK.BuildTools package, as they do on the runner.
  • The new VSIX has version 1.28.0 and the same files as the v1.27.0 VSIX, except that the license is now LICENSE.txt (b22fea5). Its manifest names LICENSE.txt too.
  • build-and-test and check-version pass on this PR.
  • After the merge, approve the App request in SignPath, then the SSMS request.
  • The Release run publishes v1.28.0 with all of its assets.
  • The run signs the SSMS files, or it shows the warning that they shipped unsigned.
  • deploy-planshare and deploy-web pass, and https://stats.erikdarling.com/api/stats answers.
  • The Velopack feed lists 1.28.0.

Generated with Claude Code

https://claude.ai/code/session_019tS6P95Dtzs4aeMpb1xqzX

erikdarlingdata and others added 30 commits September 24, 2026 23:38
SignPath's Pipeline Connector (action v3) reads .signpath/policies files
only when the signing policy references them as a Pipeline Policy. This
SignPath organization is on the OSS subscription, and its dashboard has no
Pipeline Policy setting (checked 2026-09-25), so nothing references the
file and SignPath does not check the runner rule. The old comment told
readers to add the reference in the dashboard, which is not possible.

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
…t-enforced

Say the SignPath policy file is not enforced
  Move all 34 package versions into Directory.Packages.props, preserving
  existing versions and project-specific metadata.

  Scope the framework reference entry to SSMS to avoid conflicting with
  the installer's implicit SDK reference.

  Update CI cache inputs and deployment triggers for the central file.
A StmtCond keeps its condition's own QueryPlan (and any UDF sub-plans)
under <Condition>. The parser fed each child of <Condition> back in as a
statement, so the condition became an empty STATEMENT placeholder and its
operator tree, hashes, missing indexes and warnings were lost. Parse the
StmtCond itself as the statement and read its plan and sub-plans from
<Condition> (new optional planContainerEl argument on ParseStatement).

ParseStatement read the statement attributes only after the no-QueryPlan
return, so a MULTIPLE PLAN statement lost the QueryHash and QueryPlanHash
it carries. Read them before that return.

Port of erikdarlingdata/PerformanceMonitor#4470. The two golden baselines
change because eager_table_spool_plan.sqlplan has a WHILE (SELECT ...)
condition that is now a real statement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G625JBNh45iTR1hpT4CxNR
…-plan-580

Parse IF-condition query plans and MULTIPLE PLAN hashes (#580)
#577: rule 5 reported a row-estimate mismatch on operators that never
executed. Skip ActualExecutions = 0, as rules 11, 12 and 29 already do.

#576: rule 6 dropped its scalar-UDF warning whenever the statement's
NonParallelPlanReason was one rule 3 explains, even when rule 3 was
disabled or gated out. Suppress it only when rule 3's Serial Plan finding
is actually on the statement.

#579: rules matched hints and keywords in the raw statement text, so
string literals and comments counted as code. Add MaskCommentsAndLiterals
and use it for rule 27 (OPTIMIZE FOR UNKNOWN) and for the same pattern in
rule 3 (MAXDOP 1), rule 20 (RECOMPILE), rule 28 (NOT IN), rule 37 (cursor
declaration) and the rule 26 row-goal cause.

#578: rule 30 grouped missing-index suggestions by schema and table, so
same-named tables in two databases looked like duplicates. Add the
database to the key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G625JBNh45iTR1hpT4CxNR
Review of #582 found rule 38 still matched MAXDOP 2 against the raw text,
so a MAXDOP 2 mentioned in a comment suppressed the Standard Edition DOP
warning. The app's parameters panel had the same raw OPTIMIZE FOR UNKNOWN
check for its annotation. Both now use MaskCommentsAndLiterals, which is
internal so the app can call it. Add direct tests for the helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G625JBNh45iTR1hpT4CxNR
The rule now runs only when ActualExecutions > 0, so the fallback to 1 can never apply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G625JBNh45iTR1hpT4CxNR
Each analyzer rule now stamps its number on the findings it adds (PlanWarning.RuleNumber),
and TryOverrideSeverity reads that number instead of matching WarningType against a
rule-to-name table. The table had no entry for rules 34-37 and 39, for rule 30's Low Impact
Index and Duplicate Index Suggestions, or for rule 10's RID Lookup, so overrides for them
were silently ignored. The table, its unused reverse map and the static constructor that
built it are gone. Engine warnings are still never overridden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G625JBNh45iTR1hpT4CxNR
Key severity overrides on the rule that emitted the finding (#575)
STRING_SPLIT, OPENJSON, GENERATE_SERIES and every DMV and DMF run as a Table-valued
function operator whose Object has no Database and no Schema. A user function always has
both. Rule 23's advice (rewrite as an inline function, or stage the rows in a #temp table)
is for code the user wrote, so the rule now skips an operator with neither part. The parser
records the schema on PlanNode.SchemaName for the check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G625JBNh45iTR1hpT4CxNR
Skip the engine's own functions in rule 23
CleanTempTableName found an internal temp table name's hex suffix by skipping trailing hex
digits. For a name that is all hex after the #, such as #deadbeef1 or a table variable's
internal name like #A1B2C3D4, the skip ran to the start and the name came back as a bare #.
Ported from PerformanceMonitor b31e5d18: a name with no underscore padding before its hex
run is returned unchanged, and the function never returns a bare #.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G625JBNh45iTR1hpT4CxNR
- HtmlExporter: map warning severity to a fixed CSS class name instead of
  writing the raw value into class attributes. A null severity no longer throws.
- ReproScriptBuilder: split "*/" and "/*" and fold line breaks in every value
  written into the header comment, so a plan's database name cannot end it.
- CliConnectionResolver: --trust-cert keeps encryption Mandatory, matching the
  direct-login path in ConnectionHelper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G625JBNh45iTR1hpT4CxNR
The header line still named SQL Server Performance Monitor, the product
this code was ported from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G625JBNh45iTR1hpT4CxNR
- Header cases for overlapping delimiters and for VT, FF, NEL, U+2028 and U+2029.
- HTML export cases for an attribute payload with no markup and for a warning
  on an operator.
- Comments say why the USE line keeps line breaks in the name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G625JBNh45iTR1hpT4CxNR
…ning

Harden HTML export, repro script header, and CLI encryption
A .env file in the working directory can pick the server and turn off
certificate validation for analyze and query-store. The CLI applied those
settings without a word. Now it prints one line on stderr that names the
file and the settings it supplied, never their values. A setting that a
command-line option overrode is not listed.

PasswordResolver asks for the .env password only when neither
--password-stdin nor --password gave one, so the list is exact. Its doc
comment and --password warning no longer mention a PLANVIEW_PASSWORD
environment variable, which the CLI never read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G625JBNh45iTR1hpT4CxNR
…characters

Review round 1 on #588:

- A PLANVIEW_ value with a control character now stops the command with an
  error that names the key and the file, never the value. The CLI prints the
  server and database later, and an escape sequence there could erase the
  notice. The file path and keys in the notice and the error show control
  characters as '?'.
- Both commands merge the file through one EnvFile.Fill method. With no
  server, analyze runs offline and takes nothing from the file. The file's
  password is used only with a login, so the notice lists only settings that
  had an effect.
- PasswordResolver.TryResolve takes an optional writer for its messages, so
  the tests no longer swap Console.Error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G625JBNh45iTR1hpT4CxNR
Say on stderr which settings came from a .env file
A plan about 450 operators deep overflowed the 1 MB stack of the UI thread
and the CLI's main thread before MaxParseDepth (1,000) could refuse it, and
the process ended. Parse and ParseAsync now walk the tree on a 32 MB thread
and join it, so the depth limit stops a deep plan. Same fix as
PerformanceMonitor#4551. The web viewer (WebAssembly) still parses inline.

ScopedDescendants and ResultMapper.MapNode use loops with their own stacks,
and HtmlExporter keeps its per-node text out of the recursive method, so
every step after the parse fits a 1 MB thread at the depth limit.

JSON output, limited to about 500 operator levels, now says the plan is too
deeply nested instead of "a possible object cycle" (CLI, Robot Advice, MCP,
web share). The CLI stops with the parse error when a plan does not parse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G625JBNh45iTR1hpT4CxNR
Simple and forced parameterization name their parameters @0, @1, ...,
and IsValidParameterName required a letter after the @. Those parameters
were dropped, so the script ran the statement without declaring them and
failed with "Must declare the scalar variable".

- IsValidParameterName allows a digit after the @.
- The name, type and literal checks use \A...\z instead of ^...$,
  because $ also matches before a final line break.
- IsValidDataType checks the shape of the type (1-3 dot-separated names,
  then an optional (n), (max), (p,s) or (n,name) suffix) instead of a
  character list. Same idea as PerformanceMonitor#4567, but it also keeps
  vector(3,float16), which SQL Server 2025 writes into plans, and it
  allows only plain spaces, not line breaks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G625JBNh45iTR1hpT4CxNR
- get_query_store_top reports a plan the parser refuses in load_error.
  It returned the plan as loaded, with no warnings.
- The web viewer's share button maps only the serializer's JsonException
  to TooDeepMessage. A reply from the server that isn't JSON gets the
  usual "Share failed" message again.
- HtmlExporter.WriteOperatorNode drops a parameter it never used.
- New tests: the HTML exporter at 1,000 levels on a 384 KB thread (the
  old exporter overflows there), the search's document order and RelOp
  skipping, the CLI's parse-failure message, and the too-deep messages
  from the CLI and the MCP tools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G625JBNh45iTR1hpT4CxNR
Parse deep plans on a large-stack thread (#589)
- A batch's plan lists each statement's parameters, so a name can appear
  more than once. Keeping @0/@1 made that common: each auto-parameterized
  statement numbers its own, and the script declared @1 twice and failed
  with Msg 134. A parameter that several statements use (for example a
  shared @id) had the same problem before #590.
  - Each name is now declared once.
  - A name that the statements give different types is left out with a
    warning. Those statements are usually literal text that doesn't use it.
- The type check's numbers and the compiled-value check use [0-9], not
  \d, which also matches other scripts' digits. (max) takes no second part.
- Tests for both multi-statement cases, the digit checks, and the omitted
  warning for a hostile name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G625JBNh45iTR1hpT4CxNR
Review round 2 for #590. A parameter listed by several statements now takes
the first compiled value that can go into the script, not the first entry,
so a statement compiled without sniffing no longer sets it to ?. When the
statements disagree, the script uses the first usable value and says so.
When every parameter is left out, the script says to see the warnings
instead of claiming the plan cache had none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G625JBNh45iTR1hpT4CxNR
Repro script: keep @0/@1 parameters, check data types by shape (#590)
erikdarlingdata and others added 29 commits September 28, 2026 18:23
An exchange's elapsed time is mostly spent waiting on the operators that
feed it and drain it, so Rule 35 could name a Parallelism operator when the
operator beside it was the real cost. Three committed plans already showed
it (serially-parallel gave the Sort 17,111 ms and the Repartition Streams
below it, which feeds the Sort, the same 17,111 ms), and a live plan from
SQL Server 2025 did too: an exchange feeding a spilling Sort was named at
47% of the statement on about 2.4 s of CPU per thread.

Rule 35 now skips exchanges, as the text report's "Expensive operators" list
already does. The three Parallelism rows come out of the warning baseline;
nothing else in it changes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019n3G844aTidqrD6A6iMgza
…n on a change

The box opened on Local whatever the setting or another grid had chosen, so it could
say Local beside times shown in Server mode. It now opens on TimeDisplayHelper.Current.
Changing the mode redrew the rows and the slicer but not the wait ribbon, which kept
the old mode's labels and tips until a resize.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019n3G844aTidqrD6A6iMgza
…bels

Fix three analyzer findings: Rule 9 unused grants, Rule 33 CE guess labels, Rule 35 exchanges
…nnection

Server time display reads each connection's own offset (E5)
…verwrites the first window's session (F9)

--new-instance skipped the single-instance slot entirely, so a second window
restored the running window's saved open-tab list. It opened a copy of every
tab, shared the running window's scratch buffer ids (so both windows wrote,
dropped and swept the same files), and whichever window closed last overwrote
the other's saved list.

--new-instance now claims the slot first. If it is free, the launch is an
ordinary one. If another instance holds it, this process is a secondary: it
restores nothing (a file argument still opens, otherwise the usual new tab),
never writes the saved list or a scratch buffer, never sweeps the buffer
folder, and keeps the list already on disk when it saves settings. It loses
crash recovery for its own tabs only; the unsaved-changes prompts are
unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019n3G844aTidqrD6A6iMgza
…RestoreOpenPlans is skipped

The secondary tests now keep the settings file and buffer folder as the owner
left them and compare them at start-up as well as at the end, so the sweep
and the clear-and-save that an ordinary start does are caught on their own.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019n3G844aTidqrD6A6iMgza
…nViewer.App/dev/patch-and-minor-0d3a8b6e00

deps: Bump Avalonia and 6 others
The comment said crash recovery only. Its file tabs are not reopened at the next
start after a clean close either, as the PR body already says.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019n3G844aTidqrD6A6iMgza
Every `uses:` line in .github/workflows now names a 40-character commit
SHA, with the exact version tag in a trailing comment. Each pin is the
commit that the major tag pointed at, so no action changes version:
checkout v7.0.1, setup-dotnet v6.0.0, upload-artifact v7.0.1,
deploy-pages v5.0.1, upload-pages-artifact v5.0.0, setup-msbuild v3.0.0,
signpath submit-signing-request v3.0, paths-filter v4.0.3 and
claude-code-action v1.0.236.

.github/dependabot.yml already has a github-actions entry (directory /,
target-branch dev, weekly on Monday), so Dependabot keeps proposing
updates to the pins and rewrites the version comments. No change there.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019n3G844aTidqrD6A6iMgza
release.yml built PlanViewer.Ssms.vsix and InstallSsmsExtension.exe but
uploaded them unsigned. They now go through SignPath, using the "Vsix"
artifact configuration in the PerformanceStudio project.

New steps, after the App is signed and before the release is created:
- Stage the two files at the root of ssms-unsigned/ and upload that
  folder as the Ssms-unsigned artifact (if-no-files-found: error).
- Submit it to SignPath: same organization, project, policy and 1800 s
  timeout as the App, artifact-configuration-slug Vsix, output in
  signed/ssms.
- If signing succeeded, copy both signed files over the ones in
  releases/. The release upload, the SSMS Gallery upload and the
  checksums all read from releases/, so they use the signed files.
- If it did not, write a ::warning:: annotation and ship the unsigned
  files, which is what the release did before.

SSMS problems must never block the cross-platform release, so the
staging, upload and signing steps are all continue-on-error. The copy
step is gated on the signing step's outcome, not its conclusion, and
copies both files or neither.

SHA256SUMS.txt now also lists PlanViewer.Ssms.vsix and
InstallSsmsExtension.exe when they were built. The line format is
unchanged.

Each SignPath request needs a manual approval, so a release run now
waits for two approvals: the App first, then the SSMS files.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019n3G844aTidqrD6A6iMgza
The listener retries until it gets the pipe, so a secondary took it once the owner
exited, and later launches handed their files to a window whose tabs are never
saved. Without it, such a launch finds no pipe, claims the slot, and runs as the new
owner. Also notes that --new-instance acts as an owner where named mutexes fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019n3G844aTidqrD6A6iMgza
…ner save

The comment said the write hands the list back unchanged. An owner save between the
read and the write is still lost, as the PR body already says.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019n3G844aTidqrD6A6iMgza
A second window from --new-instance no longer duplicates or overwrites the first window's session (F9)
- Replace step: if copying a signed file fails, put the unsigned pair
  back from ssms-unsigned/ and warn, instead of stopping the release.
  The job stops only if that restore fails, before anything is published.
- Upload SSMS extension to release: continue-on-error, so a failed SSMS
  upload no longer stops the App files from reaching a release that
  already exists.
- Installer: Release builds look for the VSIX only in the argument or
  next to the exe. The build-folder search is now Debug-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019n3G844aTidqrD6A6iMgza
Pin workflow actions to commit SHAs and sign the SSMS extension
A signed InstallSsmsExtension.exe now installs only a PlanViewer.Ssms.vsix that
has the same certificate. The check needs exactly one signature that verifies,
the same thumbprint as the installer's Authenticode certificate, and coverage of
every part and relationship except the signature's own. An unsigned installer
skips the check and prints one line, so dev builds and unsigned releases keep
working.

The installer copies the VSIX into a new temp folder, checks the copy, installs
the copy, then deletes the folder.

--verify-only <vsix> runs the same checks, installs nothing and never waits for
a key. The release workflow runs it on the signed pair. If it fails, the step
puts the unsigned pair back and writes a warning, and the release continues.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019n3G844aTidqrD6A6iMgza
The VSIX check now starts from the raw ZIP entries instead of the parts
that OPC lists. It passes only when every entry is one of these, matched
by exact name (names that differ only in case are different names):

- a part that the signature signs
- [Content_Types].xml
- a relationship part that the signature covers, or that belongs to the
  origin part or the signature part
- the origin part, when it is signed or empty
- the signature part
- a certificate part that has the certificate content type, is linked
  from the signature part, and holds exactly the installer certificate

Relationships of the origin part and the signature part may point only
to those entries. A second entry with the same name, in any case, fails.
No entry is classified by parsing what is in it.

The signer is compared with the installer certificate by its full raw
data, not by its thumbprint. Only the origin relationship of the package
is exempt from relationship cover. Before, every relationship with a
digital-signature type was exempt.

The project now references System.IO.Compression. The InternalsVisibleTo
entry is removed, because the test project it named does not exist.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019n3G844aTidqrD6A6iMgza
The VSIX held the license as an entry named LICENSE, with no extension.
Its content type came only from an Override entry in [Content_Types].xml.
OpenVsixSignTool rewrites that file without the override. The entry then
stopped being a part, stayed unsigned, and the installer check rejected
the signed VSIX.

An entry named LICENSE.txt gets its content type from the Default entry
for the txt extension, and the tool keeps that entry.

The Link metadata sets only the folder of a file in the VSIX, not its
name, so it cannot rename LICENSE. A VSSDK build with Link set to
LICENSE.txt fails with VSSDK1310. The StageVsixLicense target instead
copies the LICENSE file of the repo to obj as LICENSE.txt, and the VSIX
takes that copy. The LICENSE file of the repo does not change. The
manifest License element now names LICENSE.txt.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019n3G844aTidqrD6A6iMgza
CheckVsix compared the raw certificate of the signer with the
certificate of the installer after VerifySignatures. It now compares
first, so a VSIX from another signer is rejected before the expensive
verification. The verification uses the same Signer, so an accepted
file is still verified with the certificate that was compared.

A signature that holds no certificate returns the same reason as before:
the signature is not valid (CertificateRequired).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019n3G844aTidqrD6A6iMgza
…ture

Check the VSIX signature before a signed SSMS installer installs it
The early abort reason is part of the optimization result, so the App's
Runtime Summary now shows it on the row under Optimization with its label
indented. CE model moves up above Optimization, so Optimization and its
reason end the list, as in the issue's mockup.

The web Runtime card and the HTML export follow the same #215 E11 order,
so CE Model moves above Optimization there too. They have no Early abort
row, so there is nothing to nest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019tS6P95Dtzs4aeMpb1xqzX
The label printed both row counts N0 but took the percentage from the
unrounded values, so a Key Lookup that ran 117 times for 1 row read
"1 of 1 (89%)". PlanRowAccuracy, ported from PerformanceMonitor's
PlanRowAccuracy (#4684), adds the fewest decimals (up to 4, fixed-point)
at which the printed numbers give the printed percentage, and never
prints a non-zero value as 0. That label now reads "1 of 1.128 (89%)".

The App and Web node labels and the HTML export all use it, so all three
print the same numbers, and PerformanceMonitor prints the same string
for the same plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019tS6P95Dtzs4aeMpb1xqzX
Resolves the conflicts with dev's 2026-09-28 package bumps.
Directory.Packages.props takes dev's versions: Avalonia, Avalonia.Desktop,
Avalonia.Fonts.Inter, Avalonia.Headless and Avalonia.Themes.Fluent 12.1.3,
Microsoft.SqlServer.TransactSql.ScriptDom 180.117.0 and coverlet.collector
10.1.0. It also adds Microsoft.AspNetCore.Mvc.Testing 10.0.12, which dev
added to the test project. Every version pinned on dev now has the same
central version, and no PackageReference keeps a Version attribute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019tS6P95Dtzs4aeMpb1xqzX
build(deps): centralize NuGet package versions
Nest Early abort under Optimization and list CE model above it
Make the node row label agree with its percentage
Bumps all four version carriers together: Directory.Build.props, the
SSMS vsixmanifest, the SSMS AssemblyInfo (both attributes), and
CITATION.cff (version and date-released).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019tS6P95Dtzs4aeMpb1xqzX
@erikdarlingdata
erikdarlingdata merged commit 66a4797 into main Sep 29, 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.

2 participants