Skip to content

Release v1.20.0 - #445

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

Release v1.20.0#445
erikdarlingdata merged 22 commits into
mainfrom
dev

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

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

User-facing

Build and test

Closes on release

#430, #435, #436.

Verification

Full suite run on all three platforms, not just CI's:

result
ubuntu-latest (CI) 305 passed, 0 failed
windows-latest (the OS release.yml runs on) 306 passed, 0 failed
macOS ARM64 (local) 305 passed, 0 failed

The Windows run was deliberate: ci.yml and nightly.yml are both ubuntu-latest, so #442's new Microsoft.Testing.Platform flags had never executed on Windows, and the release job would have been the first time. Proved with a throwaway push-triggered job running the exact release.yml command before opening this.

dotnet build clean. The 9 warnings are the pre-existing MCP9005 obsolete-API uses in McpSmokeTests.cs.

🤖 Generated with Claude Code

erikdarlingdata and others added 22 commits August 3, 2026 12:52
Every Dependabot PR carried a red X from the review check, which failed
twice over instead of reviewing: claude-code-action rejects non-human
actors outright ("Workflow initiated by non-human actor: dependabot"),
and a dependabot-triggered run draws from the Dependabot secret store,
not the Actions one, so CLAUDE_CODE_OAUTH_TOKEN was empty regardless.
Both open bumps (#420, #422) build and pass tests; only this check
blocked them. A version bump has nothing an AI review would catch that
build-and-test doesn't, so skip - neutral, not red - in the same spirit
as the existing draft/fork/release-PR exclusions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dabot

Skip the AI review job on Dependabot PRs
Bumps ModelContextProtocol from 1.4.1 to 2.0.0
Bumps ModelContextProtocol.AspNetCore from 1.4.1 to 2.0.0

---
updated-dependencies:
- dependency-name: ModelContextProtocol
  dependency-version: 2.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
- dependency-name: ModelContextProtocol.AspNetCore
  dependency-version: 2.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
….bundle_e_sqlite3

Bumps Microsoft.SqlServer.TransactSql.ScriptDom from 180.59.2 to 180.78.1
Bumps SQLitePCLRaw.bundle_e_sqlite3 from 3.0.4 to 3.0.5

---
updated-dependencies:
- dependency-name: Microsoft.SqlServer.TransactSql.ScriptDom
  dependency-version: 180.78.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: patch-and-minor
- dependency-name: Microsoft.SqlServer.TransactSql.ScriptDom
  dependency-version: 180.78.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: patch-and-minor
- dependency-name: Microsoft.SqlServer.TransactSql.ScriptDom
  dependency-version: 180.78.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: patch-and-minor
- dependency-name: Microsoft.SqlServer.TransactSql.ScriptDom
  dependency-version: 180.78.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: patch-and-minor
- dependency-name: SQLitePCLRaw.bundle_e_sqlite3
  dependency-version: 3.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
…PlanShare/dev/patch-and-minor-dded2de96d

deps: Bump Microsoft.SqlServer.TransactSql.ScriptDom and SQLitePCLRaw.bundle_e_sqlite3
…nViewer.App/dev/multi-daa5a8fc91

deps: Bump ModelContextProtocol and ModelContextProtocol.AspNetCore
Bumps Avalonia from 11.3.18 to 11.3.19
Bumps Avalonia.Desktop from 11.3.18 to 11.3.19
Bumps Avalonia.Fonts.Inter from 11.3.18 to 11.3.19
Bumps Avalonia.Themes.Fluent from 11.3.18 to 11.3.19
Bumps ModelContextProtocol from 2.0.0 to 2.1.0
Bumps ModelContextProtocol.AspNetCore from 2.0.0 to 2.1.0

---
updated-dependencies:
- dependency-name: Avalonia
  dependency-version: 11.3.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Avalonia.Desktop
  dependency-version: 11.3.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Avalonia.Fonts.Inter
  dependency-version: 11.3.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Avalonia.Themes.Fluent
  dependency-version: 11.3.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: ModelContextProtocol
  dependency-version: 2.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: patch-and-minor
- dependency-name: ModelContextProtocol.AspNetCore
  dependency-version: 2.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: patch-and-minor
- dependency-name: Avalonia
  dependency-version: 11.3.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Avalonia.Desktop
  dependency-version: 11.3.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Avalonia.Fonts.Inter
  dependency-version: 11.3.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Avalonia.Themes.Fluent
  dependency-version: 11.3.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: ModelContextProtocol
  dependency-version: 2.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: patch-and-minor
- dependency-name: ModelContextProtocol.AspNetCore
  dependency-version: 2.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: patch-and-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
…nViewer.App/dev/patch-and-minor-7f3ba78d27

deps: Bump the patch-and-minor group with 6 updates
Interactive Microsoft Entra MFA could not work in Studio on Windows at all.
Current Microsoft.Data.SqlClient (7.0.2 here) routes
Authentication=ActiveDirectoryInteractive through the Windows WAM broker, and WAM
requires the application to supply the HWND that will own its account picker.
ServerConnection.BuildConnectionString set the authentication mode but nothing in
the repo ever registered an auth provider, so instead of a prompt users got
"0xwindow_handle_required / A window handle must be configured" and the connection
failed. Not tenant-specific: broken for every Windows user on a current build.
Studio 1.4.3 worked because it predates WAM being the default and fell back to a
browser.

Reported by joshdbe, who supplied the diagnostic that made it findable: 1.4.3
works, 1.19.1 does not. Same defect in PerformanceMonitor Lite
(erikdarlingdata/PerformanceMonitor#2184); doing Studio first because that is the
tool the reporter actually uses.

EntraInteractiveAuth.Register takes a handle provider and installs an
ActiveDirectoryAuthenticationProvider. Three deliberate choices:

- Registered ONCE at app startup rather than per connection.
  SqlAuthenticationProvider installs against the authentication METHOD, not a
  connection, so one call covers every SqlConnection the process opens. Studio
  opens them from several unrelated places (connection dialog, query session
  control, schema service) and threading a handle through all of them is a change
  every future call site could forget.

- WINDOWS ONLY. WAM does not exist on macOS or Linux, where interactive auth
  already works through the system browser and needs no handle, so registering a
  handle-supplying provider there would add a failure mode to the platforms
  Avalonia exists to serve.

- The handle is resolved PER PROMPT, not captured at startup, and prefers the
  active window over the main window. A platform handle is not valid until its
  window is sourced, and a connection is usually triggered from the connection
  dialog - parenting the picker to the main window behind it would let the picker
  appear behind the dialog the user is looking at.

The CLI gets a clear refusal instead of MSAL's error. planview accepts
--auth entra and has no window to give, so it would have failed deep inside MSAL
with a message that tells the operator nothing. It now says so up front and names
the modes that work headless. Deliberately NOT silently substituting another auth
mode: connecting as a different identity than the one asked for is worse than
refusing. Whether the CLI should grow device-code flow is the open question on
#425 rather than something to guess at here.

Tests cover what is verifiable without a tenant: null handle provider rejected at
wiring time rather than at prompt time, the non-Windows contract (Register
declines, the provider is never consulted, and IsSupported stays TRUE so the CLI
guard does not refuse entra on platforms where it works), and idempotent
registration, since SetProvider is process-wide and a second call would otherwise
silently replace the first once the SSMS extension wires in too.

Whether the picker actually authenticates can only be established against a real
Entra tenant on an Azure VM; the reporter has offered to test that and this is
not claimed as verified until he does.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PlanViewer.Core.Tests can wedge in a CoreCLR GC-suspension livelock on macOS ARM64
(.NET 10.0.8) and spin a full core until someone notices the heat. It happened
twice unprompted today: three hosts at ~108% CPU each, one of them wedged 22
minutes and still going when sampled. Root-caused from a sample: SuspendEE present
in all 2451 samples, the victim thread interrupted where CheckActivationSafePoint
can never succeed, and the mach-exception thread churning thread_get_state /
thread_set_state forever.

The load-bearing fact is that nothing inside the test host can stop it. xUnit
timeouts and in-test CancelAfter cannot fire because the execution engine itself is
suspended - that IS the livelock - and dotnet-stack and EventPipe hang for the same
reason. Only an out-of-process watchdog works, and both mechanisms here are
enforced by vstest.console, a separate process from the host it watches.

Two layers, deliberately:

- The three CI invocations (ci, nightly, release) now pass the blame-hang options,
  which is what produces a sequence file naming the test that wedged.
- RunSettingsFilePath in the test csproj points at hang-watchdog.runsettings, so a
  bare `dotnet test` with no flags gets a TestSessionTimeout backstop. This is the
  layer that matters most: BOTH incidents were plain local runs, so a fix living
  only in the workflow files would protect the machine that was never at risk and
  leave the laptop unprotected.

15 minutes is far above any real runtime (individual tests are milliseconds, the
whole suite normally well under a minute), so it only fires on a genuine wedge.

Verified rather than assumed, because a silently-ignored runsettings would look
exactly like a working one: temporarily set TestSessionTimeout to 1ms and a bare
`dotnet test` reported "Aborting test run: test run timeout of 1 milliseconds
exceeded", then restored it. The blame collector confirms itself active on a normal
run ("All tests finished running, Sequence file will not be generated"). Zero test
hosts survived any run.

Two XML-comment errors on the way in, both caught by the toolchain rather than by
me: '--' is illegal inside an XML comment, and I had written the flag names and a
dash-separated aside into comments in both the csproj and the runsettings.

Evidence for a future dotnet/runtime report is preserved outside the session
scratchpad at ~/Documents/dotnet-hang-evidence (sample, gzip, and a README
explaining the signature); related to dotnet/runtime#66759.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Out-of-process watchdog for the macOS ARM64 test-host GC-suspension livelock. Verified by forcing a 1ms TestSessionTimeout abort on a flagless dotnet test.
…efusal

Two findings from the PR review:

- ActiveWindowHandle is called by MSAL from whatever thread SqlClient's
  token acquisition runs on, while desktop.Windows is a UI-thread-owned
  collection that can be mutated mid-enumeration by a dialog opening or
  closing. Marshal the lookup through Dispatcher.UIThread (safe: no
  connection path blocks the UI thread on auth - every open in the app
  is async), and degrade to IntPtr.Zero if the dispatcher can't
  deliver, which is MSAL's normal no-handle failure rather than a new
  one.

- The CLI's --auth entra refusal had no test. Pin it with a real call
  through BuildServerConnection, made deterministic by an internal
  test-only reset of the process-wide one-way registration flag (the
  registration tests do run first in this process, so a skip-if-
  contaminated guard would have skipped on exactly the platform where
  the branch is reachable). The two test classes share a collection so
  the global state can't flip mid-test.

Also drop using directives already provided as global usings in both
projects, which the IDE flags on every touch of these files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Give the WAM broker a parent window handle so Entra MFA works (#425)
System.Text.Json defaults MaxDepth to 64 and throws past it. An operator
tree nests once per operator and each level costs two JSON levels (the
object, then the Children array), so a plan roughly 30 operators deep
exhausts the default. The throw came out of an Avalonia click handler,
which Avalonia does not guard, so the process died with no dialog and
nothing the user could act on.

The reported stack named the cause precisely: the path was
$.Statements.OperatorTree.Children.Children...(30 deep)...NodeId - thirty
CONSECUTIVE Children. That distinction decides the fix, because the
exception message offers two causes ("either be due to a cycle or if the
object depth is larger than the maximum allowed depth of 64") and they
want opposite remedies. The serialized type is OperatorResult, which has
Children and no parent link, so it is a tree and depth is the right read.

Worth recording for whoever hits this next: the INTERNAL model it is
mapped from, PlanNode, DOES carry a Parent back-reference that the parser
populates (ShowPlanParser.RelOp.cs). Serializing that type directly would
be a genuine cycle needing [JsonIgnore], not a bigger number.

The ceiling is now one shared constant, because four places serialize this
object and none of them could tell when they got it wrong: both Robot
Advice entry points (QuerySessionControl and MainWindow build the payload
independently), the MCP tool options, and the CLI's two option sets. Only
the reported one crashes the process; the others returned an error where
the caller expected a plan.

1024 is about 500 nested operators against the ~30 that used to fail, and
it is deliberately paired with a caller-side catch at both UI sites: a
serialization limit should never be able to take the app down, so the
headroom is not the only thing standing between a deep plan and a crash.

Tested: 137/137 in PlanViewer.Core.Tests, including a reproduction that
fails on the old inline options and passes on the shared ones. App and CLI
build with zero warnings.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bumps Avalonia from 11.3.19 to 11.3.20
Bumps Avalonia.Desktop from 11.3.19 to 11.3.20
Bumps Avalonia.Fonts.Inter from 11.3.19 to 11.3.20
Bumps Avalonia.Themes.Fluent from 11.3.19 to 11.3.20
Bumps Microsoft.AspNetCore.Components.WebAssembly from 10.0.10 to 10.0.11
Bumps Microsoft.AspNetCore.Components.WebAssembly.DevServer from 10.0.10 to 10.0.11
Bumps Microsoft.Data.Sqlite from 10.0.10 to 10.0.11
Bumps Microsoft.NET.Test.Sdk from 18.8.1 to 18.9.0
Bumps ModelContextProtocol from 2.1.0 to 2.2.0
Bumps ModelContextProtocol.AspNetCore from 2.1.0 to 2.2.0
Bumps System.CommandLine from 2.0.10 to 2.0.11

---
updated-dependencies:
- dependency-name: Microsoft.Data.Sqlite
  dependency-version: 10.0.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Avalonia
  dependency-version: 11.3.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Avalonia.Desktop
  dependency-version: 11.3.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Avalonia.Fonts.Inter
  dependency-version: 11.3.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Avalonia.Themes.Fluent
  dependency-version: 11.3.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: ModelContextProtocol
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: patch-and-minor
- dependency-name: ModelContextProtocol.AspNetCore
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: patch-and-minor
- dependency-name: System.CommandLine
  dependency-version: 2.0.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Microsoft.AspNetCore.Components.WebAssembly
  dependency-version: 10.0.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Microsoft.AspNetCore.Components.WebAssembly.DevServer
  dependency-version: 10.0.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Avalonia
  dependency-version: 11.3.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Avalonia.Desktop
  dependency-version: 11.3.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Avalonia.Fonts.Inter
  dependency-version: 11.3.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Avalonia.Themes.Fluent
  dependency-version: 11.3.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: patch-and-minor
- dependency-name: ModelContextProtocol
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: patch-and-minor
- dependency-name: ModelContextProtocol.AspNetCore
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: patch-and-minor
- dependency-name: System.CommandLine
  dependency-version: 2.0.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-and-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
#437)

Rule 12 flagged any predicate whose text contained CONVERT_IMPLICIT, and told the
user it "prevents an index seek". Which side the conversion is on decides whether
that is true, and the rule never looked.

Data type precedence picks the side, and SQL Server converts the LOWER-precedence
one. A numeric(18,0) column compared to an integer parameter converts the
PARAMETER up:

  [db].[dbo].[t].[UNIV_SEZIONI]=CONVERT_IMPLICIT(numeric(18,0),[@0],0)

The column is untouched, so it is still seekable, and SQL Server agrees: it emits
no PlanAffectingConvert warning on that plan, which is why SSMS shows no warning
icon while we showed one. The damaging shape is the mirror image,
CONVERT_IMPLICIT(nvarchar(40),[db].[dbo].[t].[DisplayName],0)=[@d], where every
row has to be converted before it can be compared.

Reproduced rather than reasoned about, because the seekability claim is the whole
question. Captured two actual plans of the reporter's repro from SQL Server 2025
under PARAMETERIZATION FORCED. Without an index on the search column it is a
scan; ADD an index on that same column and SQL Server produces an Index Seek
whose seek predicate is CONVERT_IMPLICIT(numeric(18,0),[@0],0) — it seeks
straight through the conversion. That settles it: the conversion was never what
prevented the seek. The scan fixture is committed as the test plan.

So the check reads what is INSIDE the conversion rather than splitting on the
comparison operator: the first argument is the target type and carries no
brackets, so a bracket-qualified column reference in the remainder is the
conversion input. Balanced-paren extraction is needed because the target type has
parentheses of its own — numeric(18,0), varchar(50) — so the first ')' is not the
end of the argument list.

Second change, and it is load-bearing rather than tidying: the function-on-column
check now walks every regex match instead of only the first. A parameter-side
conversion now falls through to that check and is skipped there as benign, so
with only the first match a conversion sitting to the LEFT of a genuine
function-on-column would hide it — trading the fixed false positive for a new
false negative. Verified by reverting just that hunk: Rule12d then fails with
"Collection was empty".

Behavior change, stated plainly: plans carrying a parameter-side conversion on a
scan lose their "Non-SARGable Predicate" warning. The reporter's plan now reports
Rule 11 instead — "scan with residual predicate, 50 of 50,000 rows survived
filtering, check that you have appropriate indexes" — which is the true and
actionable read of that plan. Nothing silently disappears.

The existing corpus is untouched: regenerating WarningBaseline.txt across all 38
committed plans changes nothing except the two lines each for the two new
fixtures. No committed plan was relying on the old verdict.

Tested: 71/71 in the analyzer classes (PlanAnalyzerTests,
WarningCharacterizationTests, PlanAnalysisPipelineTests, OperatorSelfTimeTests),
plus 130 more across the other classes. Both new tests fail on the unfixed rule —
Rule12c with "Collection was not empty", Rule12d with "Filter not matched".
dotnet build is clean, 0 warnings.

Not fixed here and not caused here: a full `dotnet test` does not finish on macOS
ARM64 — it wedges inside McpPlanPathPolicyTests with every thread blocked. It is
NOT the GC-suspension livelock from 5f84116 (no SuspendEE, 0% CPU, not spinning),
--blame-hang-timeout does not fire on it, and it reproduces on unmodified dev. CI
is ubuntu-latest, which is why it is invisible there.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
#430 was fixed by making the depth ceiling a shared constant and referencing it
from every serializer that writes an AnalysisResult. That enumeration missed two:
QueryStoreCommand builds its own JsonOptions and CompactJsonOptions, identical in
shape to AnalyzeCommand's, and neither ever got MaxDepth. So `planview querystore`
has kept failing on any plan deeper than roughly 30 operators for as long as
`analyze` has been fine.

It fails quietly, which is why nobody has reported it. The per-plan try/catch in
the sweep loop turns the JsonException into one "ERROR: ..." row in summary.txt
and moves to the next plan, so a Query Store sweep over a few hundred plans
silently analyzes all but the deep ones. Query Store batch analysis is exactly
where the deep plans are.

The interesting part is WHY it was missed, because that decides the fix. A shared
constant only helps the call sites that remember to reference it, and the options
themselves were still duplicated per command — so editing AnalyzeCommand's two
sets to add MaxDepth did nothing for the identical pair one file over, and
nothing anywhere said they were supposed to match. The options now live on
AnalysisJson next to the constant, as IndentedWithoutNulls and CompactWithoutNulls,
and both commands point at them. There is no longer a copy to forget.

Values are unchanged for analyze: same WriteIndented, same
JsonIgnoreCondition.WhenWritingNull, same ceiling. Only querystore's behavior
changes, and only from "fails on deep plans" to "does not".

The test walks the options instead of trusting the call sites, since trusting
them is what went wrong: every static JsonSerializerOptions on a *Command type in
PlanViewer.Cli must carry the ceiling. Verified by reverting just the
QueryStoreCommand hunk, at which point it fails naming both offenders —
"QueryStoreCommand.JsonOptions (MaxDepth 0), QueryStoreCommand.CompactJsonOptions
(MaxDepth 0)". A new command that rolls its own now fails here rather than in
someone's Query Store sweep.

Tested: 82 tests across AnalysisJsonDepthTests and the CLI/analysis classes, 0
failures. dotnet build clean, 0 warnings.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The reporter could not tell our advice apart from the engine's, and there was no
way to: both arrive as a PlanWarning carrying nothing but type, severity and
message, and both render identically. 14 warning constructions in
ShowPlanParser.Warnings.cs are lifted straight out of the plan's own <Warnings>
element; 47 across PlanAnalyzer*.cs are our rules reading plan shape. Nothing in
the model or the output said which was which.

The distinction is not cosmetic. A warning SQL Server wrote into the plan is a
record of what the engine did — it spilled, it converted, it had no statistics.
One of our rules is an inference, and an inference can be wrong about a
particular plan in a way the engine's own record cannot be. The same issue
supplies the example: we told this reporter a conversion prevented an index seek
on a plan SQL Server had raised no conversion warning about at all, and being
able to see "that line is ours, this line is the engine's" is what would have let
him weigh the two himself.

PlanWarning.Source defaults to PerformanceStudio, and everything the parser
produces is stamped SqlServer in ONE place — the single return of
ParseWarningsFromElement, which every parser warning already funnels through.
Stamping the 14 construction sites individually would have been the same bug as
#430's missed QueryStoreCommand: a rule you have to remember at each site is a
rule that eventually gets forgotten. A new engine warning added to that method is
attributed correctly without anyone thinking about it.

UX, since it was left to me:

- Only the ENGINE's warnings are tagged, as " [SQL Server]", in both the GUI
  warnings panel and the CLI text output. Tagging both kinds would put a badge on
  every line and carry no information — our own advice is what a reader already
  expects from a plan analyzer, so the marked case should be the exception.
- The tag sits next to the existing " [legacy]" tag and is built the same way,
  because that badge already established the idiom here.
- JSON and MCP consumers get it as a "source" field rather than having to parse a
  tag out of a string.

CLI output contract, flagged rather than slipped in: adding "source" to every
warning changes the bytes of `analyze --compact`, so
HistoricalCliContractTests.ExpectedCompactOutputSha256 is rolled. The change is
additive — nothing removed or renamed, so a consumer reading fields by name is
unaffected — but anything hashing or diffing whole output will see a difference.
That constant exists to make this a decision instead of a discovery, so it is
called out here rather than quietly updated.

The characterization baseline is untouched: it digests type, severity and message,
none of which changed.

Tested: 65 across WarningSourceTests, HistoricalCliContractTests,
WarningCharacterizationTests and PlanAnalyzerTests, 0 failures. The new tests pin
both kinds on one plan that carries both, that no warning type is ever produced as
both kinds across every committed plan, that only the engine's are tagged in text
output, and that the JSON field is populated. dotnet build clean.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…PlanShare/dev/patch-and-minor-87032c3abd

deps: Bump the patch-and-minor group with 11 updates
…443)

* Stop corrupting memory resolving an open file handle on macOS (#441)

fcntl(2) is variadic - int fcntl(int, int, ...) - and this called it through a
plain DllImport with a fixed third parameter:

    [DllImport("libc", EntryPoint = "fcntl", SetLastError = true)]
    private static extern int Fcntl(int fileDescriptor, int command, byte[] buffer);

On Apple arm64 the variadic ABI differs from AAPCS64: named arguments go in
registers, variadic arguments go on the STACK. A fixed-signature P/Invoke puts the
buffer in x2, and the callee never looks there.

It does not fail, which is why nothing caught it. Reproduced standalone, outside
the test suite, one open file:

    Arch  : Arm64
    FIXED rc=0 errno=0 len=0 -> ''

rc=0 is fcntl reporting SUCCESS. F_GETPATH on success writes the path into the
buffer it was handed; ours came back empty, so it wrote up to MAXPATHLEN bytes
through whatever pointer happened to be in that stack slot. An arbitrary ~1KB
write, on every call, into this process.

That is the whole of #441. `dotnet test` has been unrunnable on macOS - it wedged,
sometimes as a GC-suspension livelock spinning a core, sometimes as an
all-threads-blocked deadlock, which is exactly what an arbitrary write into runtime
memory looks like from the outside. Both shapes are gone.

Diagnosed as a prediction rather than described afterwards: the three tests in
McpPlanPathPolicyTests that pass are precisely the three that never reach
GetFinalPath, and both that reach it hung, deterministically, 5/5 and 2/2. It is
also why CI never saw it - Linux takes the /proc/self/fd branch and Windows takes
GetFinalPathNameByHandle, so only macOS goes anywhere near fcntl, and only arm64
has the mismatch. ubuntu-latest structurally cannot reproduce this.

The fix is libproc's proc_pidfdinfo, which answers the same question with a FIXED
signature, so ordinary marshalling is correct. .NET has no varargs P/Invoke on this
target at all - __arglist throws "Vararg calling convention not supported" - so
repairing the fcntl declaration in place is not available.

Worth recording, because I recommended the wrong fix on the issue first and it
sounds right: comparing fstat(fd) against stat(path) by device+inode does NOT work
here. stat() re-resolves the path, so if a symlink was swapped before the open it
follows the swap too and the inodes match. It detects nothing. Resolving the
descriptor back to its real path is the only thing that answers "where does this
handle actually live", which is the question the TOCTOU check exists to ask.

The struct offsets are derived in a comment from <sys/proc_info.h> rather than
being magic numbers, and the returned size is checked against the expected 1200
instead of trusted - so a layout change in a future macOS fails loudly rather than
quietly handing back the wrong bytes, which is the failure mode this file just came
out of.

Also note macOS answers with the CANONICAL path: /private/var/... where
Path.GetTempPath() reports /var/... The roots are canonicalized through
ResolveLinkTarget already, so containment still matches; the new test compares by
identity rather than by string for the same reason.

The old test asserted on the returned handle's Label and passed happily on Linux
and Windows while this call had never once worked on Apple silicon. The new test
asserts the thing that was actually broken - that the resolver returns the real path
of the file that is genuinely open - on whatever platform it runs.

Security note: GetFinalPath is the TOCTOU re-validation, confirming the path the
kernel really opened is still inside the advertised roots. On macOS arm64 it has
never performed that check. It failed CLOSED - the empty string reached
Path.GetFullPath, which throws ArgumentException, which is not in OpenAsync's
catch filter - so this was not an exploitable bypass, but the guarantee
OpenAsync_ValidatesAndReturnsTheSameOpenedHandle claims to prove was not being
provided, and the FileStream leaked on the way out.

Tested: full `dotnet test` on macOS ARM64 now finishes in 16 seconds, 305 passed, 0
failed, 2 skipped, twice. It did not finish at all before. McpPlanPathPolicyTests
went from 5/5 hangs to 3/3 passes.

General rule this earns: no variadic libc function through a plain DllImport in
this repo - fcntl, open, ioctl, the printf family. They look fine on x64 and
silently corrupt memory on Apple silicon.

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

* Write down the variadic P/Invoke rule that #441 earned

The defect cost months of unrunnable tests on macOS and was invisible to CI, and
nothing in the repo said not to do it. The specific trap is that it fails silently
in both directions: fcntl reports success, and the corruption lands somewhere else
entirely, so the crash never points at the call that caused it.

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

---------

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

Dependabot #434 fails to build, and not because of anything it did wrong:

  Microsoft.Testing.Platform.MSBuild.targets(320,5): error : Testing with VSTest
  target is no longer supported by Microsoft.Testing.Platform on .NET 10 SDK and
  later.

xunit.v3 4.0.0 drops VSTest on the .NET 10 SDK. There is no version of this bump
that keeps the old runner, so it is migrate or stay on 3.2.2. #433 is the same
family and passes only because bumping the VSTest adapter alone changes nothing.

The opt-in is repo-level, not per-project, which the target names outright
(_SupportsGlobalJsonTestRunner): global.json now selects the runner. Setting
TestingPlatformDotnetTestSupport in the csproj alone does not do it, and I tried
that first.

xunit.runner.visualstudio is dropped because it IS the VSTest adapter and has no
role here any more. That moots #433.

The watchdog from 5f84116 needed porting, and this is the part worth reading.
Both of its layers were VSTest-only: RunSettingsFilePath/TestSessionTimeout for
bare local runs, and the blame-hang options in the three workflows. The local
layer is now TestingPlatformCommandLineArguments carrying a 15m session timeout,
same value and same no-flags-to-remember property; the workflows pass the hangdump
options, which produce the same name-the-wedged-test artifact.

I verified the local layer the way the original was verified, because a silently
ignored property looks exactly like a working one: set it to 1ms temporarily and a
bare `dotnet test` cancelled with 0 tests succeeded, then restored it.

What the port does NOT recover, stated plainly because it would be easy to leave
implied: neither MTP mechanism stops the GC-suspension livelock the original
watchdog was written for. Measured against a genuinely wedged host on macOS ARM64,
not assumed - it ran 2m55s at 107% CPU against a 90s hangdump timeout, and 8
minutes against a 60s session timeout, ignoring both. The reason is the same one
5f84116 gives for xUnit timeouts: the diagnostics the platform needs are served by
the execution engine that is suspended. MTP does put the timeout in a separate
controller process, which was the property that mattered, and it still cannot win.
An ordinary hang, where the runtime is responsive, is killed fine.

So this is not a regression against VSTest so much as both runners being equally
powerless there, and I want to be careful not to claim more: I did not prove the
OLD TestSessionTimeout could kill a livelocked host either. It was verified at 1ms
on a healthy run, never against a real wedge. Filed separately.

Tested: 279 passed, 0 failed, 2 skipped under the new runner (the whole suite
except the Mcp/Repl classes, which wedge on macOS for reasons that predate this and
are the subject of that separate issue). The CI flag form was run verbatim to
confirm the options parse. dotnet build clean; the 9 warnings are the pre-existing
MCP9005 obsolete-API uses in McpSmokeTests.cs.

Two XML-comment '--' errors on the way in, exactly as 5f84116 warned. Noted in the
comment so the next person does not rediscover it.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Minor rather than patch. 1.19.x would understate it: #439 adds a "source" field
to every warning in the JSON and MCP output and a new badge in the app and CLI, and
#437 changes what an existing analysis rule concludes about a plan. Both are things
a consumer can notice, and one of them is output-shape.

What ships:

- #437 Rule 12 no longer calls a conversion non-SARGable when it converts the
  parameter rather than the column. Plans carrying a parameter-side conversion on a
  scan lose that warning and report the scan's residual predicate instead. Verified
  against all 38 committed plans: no other plan's verdict moves.
- #439 SQL Server's own warnings are now told apart from ours, tagged [SQL Server]
  in the app and CLI and carried as "source" in JSON/MCP. Additive, but it changes
  the bytes of analyze --compact.
- #431 Robot Advice no longer takes the app down on a deep plan.
- #438 querystore gets the same depth ceiling analyze got; it had been failing
  quietly on deep plans, one ERROR row per plan.
- #443 the macOS handle resolver no longer corrupts memory on Apple silicon.
- #425 Entra MFA works again (WAM parent window handle).

Not user-facing but worth knowing for anyone building from this tag: the suite runs
on Microsoft.Testing.Platform now (#442), and `dotnet test` finishes on macOS for
the first time (#443) - 307 tests, 305 passing, 13 seconds.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@erikdarlingdata
erikdarlingdata merged commit 8bbd6e8 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