Skip to content

Commit d443228

Browse files
Merge pull request #413 from erikdarlingdata/dev
Release v1.19.0
2 parents a6bb08f + 6bbe939 commit d443228

54 files changed

Lines changed: 4994 additions & 412 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/dependabot.yml‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@ version: 2
44
# or PlanViewer.Ssms.Installer, so any solution-level scan silently skips them.
55
# Every project directory is therefore listed explicitly below. If a new .csproj
66
# is added anywhere, add its directory here too.
7+
#
8+
# target-branch is REQUIRED on every entry here. Without it Dependabot opens PRs
9+
# against the *default* branch, which is `main` — and in this repo `main` is only
10+
# ever updated by a dev -> main release merge. Omitting it once already put two
11+
# dependency bumps directly onto `main`, behind `dev`.
712
updates:
813
- package-ecosystem: nuget
14+
target-branch: dev
915
directories:
1016
- "/src/PlanViewer.App"
1117
- "/src/PlanViewer.Core"
@@ -74,7 +80,27 @@ updates:
7480
# one is handled by hand.
7581
- dependency-name: "Velopack"
7682

83+
# Repl / Repl.Mcp / Repl.Testing (#388) are pre-1.0. Under semver a 0.x
84+
# minor bump is allowed to break, and Dependabot classifies 0.11 -> 0.12
85+
# as a *minor*, so the usual "majors only get their own PR" split does not
86+
# protect anything here. Patch bumps still flow, so genuine fixes arrive.
87+
# This matters more than for a normal dependency: the app ships an
88+
# auto-updater, so a breaking bump reaches users without them choosing it.
89+
- dependency-name: "Repl"
90+
update-types:
91+
- version-update:semver-major
92+
- version-update:semver-minor
93+
- dependency-name: "Repl.Mcp"
94+
update-types:
95+
- version-update:semver-major
96+
- version-update:semver-minor
97+
- dependency-name: "Repl.Testing"
98+
update-types:
99+
- version-update:semver-major
100+
- version-update:semver-minor
101+
77102
- package-ecosystem: github-actions
103+
target-branch: dev
78104
directory: "/"
79105
schedule:
80106
interval: weekly

‎.github/workflows/release.yml‎

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,11 @@ jobs:
3737
echo "EXISTS=false" >> $GITHUB_OUTPUT
3838
fi
3939
40-
- name: Create release
41-
if: steps.check.outputs.EXISTS == 'false'
42-
shell: bash
43-
env:
44-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45-
run: |
46-
gh release create "v${{ steps.version.outputs.VERSION }}" --title "v${{ steps.version.outputs.VERSION }}" --generate-notes --target main
40+
# NOTE: the release is deliberately NOT created here. It is created after
41+
# signing succeeds, further down. Creating it up front meant a signing
42+
# failure left a published, publicly-visible release with no app assets -
43+
# which is exactly what happened to v1.17.0 when the SignPath approval
44+
# window lapsed. Nothing before the signing step publishes anything now.
4745

4846
- name: Setup .NET 10.0
4947
uses: actions/setup-dotnet@v6
@@ -103,29 +101,12 @@ jobs:
103101
"BUILT=true" >> $env:GITHUB_OUTPUT
104102
Write-Host "SSMS extension built: $vsix"
105103
} else {
106-
Write-Host "::warning::SSMS extension did not produce the expected artifacts - release published without the VSIX (issue 343)"
104+
Write-Host "::warning::SSMS extension did not produce the expected artifacts - the release will be published without the VSIX (issue 343)"
107105
}
108106
109-
- name: Upload SSMS extension to release
110-
if: steps.ssms.outputs.BUILT == 'true'
111-
shell: pwsh
112-
env:
113-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
114-
VERSION: ${{ steps.version.outputs.VERSION }}
115-
run: gh release upload "v$env:VERSION" releases/PlanViewer.Ssms.vsix releases/InstallSsmsExtension.exe --clobber
116-
117-
# Publish to the SSMS Gallery (https://ssmsgallery.azurewebsites.net, issue 343).
118-
# The gallery extracts version/description from the vsixmanifest, so the
119-
# upload filename is irrelevant. continue-on-error: a gallery outage must
120-
# never fail the release. bash so `curl` is curl.exe, not the pwsh alias.
121-
- name: Publish SSMS extension to SSMS Gallery
122-
if: steps.ssms.outputs.BUILT == 'true'
123-
continue-on-error: true
124-
shell: bash
125-
run: |
126-
echo "Uploading PlanViewer.Ssms.vsix to the SSMS Gallery..."
127-
curl -sS -f "https://ssmsgallery.azurewebsites.net/api/upload" \
128-
-F "file=@releases/PlanViewer.Ssms.vsix"
107+
# The VSIX is built above but NOT uploaded here — uploading requires the
108+
# release to exist, and the release is not created until signing succeeds.
109+
# Both uploads happen after the signing step.
129110

130111
# ── SignPath code signing (Windows). Signing is REQUIRED for a release:
131112
# if the SignPath token is missing the job fails loudly rather than
@@ -172,6 +153,37 @@ jobs:
172153
Remove-Item -Recurse -Force publish/win-x64
173154
Copy-Item -Recurse signed/win-x64 publish/win-x64
174155
156+
# ── Everything above this line is reversible: nothing has been published.
157+
# The release is created only now that signed binaries exist. ──
158+
- name: Create release
159+
if: steps.check.outputs.EXISTS == 'false'
160+
shell: bash
161+
env:
162+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
163+
run: |
164+
gh release create "v${{ steps.version.outputs.VERSION }}" --title "v${{ steps.version.outputs.VERSION }}" --generate-notes --target main
165+
166+
- name: Upload SSMS extension to release
167+
if: steps.ssms.outputs.BUILT == 'true'
168+
shell: pwsh
169+
env:
170+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
171+
VERSION: ${{ steps.version.outputs.VERSION }}
172+
run: gh release upload "v$env:VERSION" releases/PlanViewer.Ssms.vsix releases/InstallSsmsExtension.exe --clobber
173+
174+
# Publish to the SSMS Gallery (https://ssmsgallery.azurewebsites.net, issue 343).
175+
# The gallery extracts version/description from the vsixmanifest, so the
176+
# upload filename is irrelevant. continue-on-error: a gallery outage must
177+
# never fail the release. bash so `curl` is curl.exe, not the pwsh alias.
178+
- name: Publish SSMS extension to SSMS Gallery
179+
if: steps.ssms.outputs.BUILT == 'true'
180+
continue-on-error: true
181+
shell: bash
182+
run: |
183+
echo "Uploading PlanViewer.Ssms.vsix to the SSMS Gallery..."
184+
curl -sS -f "https://ssmsgallery.azurewebsites.net/api/upload" \
185+
-F "file=@releases/PlanViewer.Ssms.vsix"
186+
175187
# ── Velopack (uses signed Windows binaries) ───────────────────────
176188
- name: Create Velopack release (Windows)
177189
shell: pwsh

‎CITATION.cff‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ authors:
99
website: "https://erikdarling.com"
1010
repository-code: "https://github.com/erikdarlingdata/PerformanceStudio"
1111
license: MIT
12-
version: "1.18.0"
12+
version: "1.19.0"
1313
date-released: "2026-07-25"
1414
keywords:
1515
- sql-server

‎README.md‎

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ dotnet build
128128
To verify the build:
129129

130130
```bash
131-
dotnet test tests/PlanViewer.Core.Tests # 37 tests should pass
131+
dotnet test PlanViewer.sln
132132
dotnet run --project src/PlanViewer.Cli -- analyze --help
133133
```
134134

@@ -149,6 +149,41 @@ planview analyze my_query.sqlplan --output text
149149
planview analyze my_query.sqlplan --output text --warnings-only
150150
```
151151

152+
### Explore plans interactively (Repl pilot)
153+
154+
Run `planview repl` to start a long-lived plan-exploration session. Zero arguments and the
155+
existing `analyze`, `query-store`, and `credential` commands continue to use the legacy
156+
System.CommandLine graph.
157+
158+
```text
159+
planview repl
160+
> open slow-query.sqlplan
161+
[plan slow-query-<opaque-id>]> summary
162+
[plan slow-query-<opaque-id>]> warnings --severity critical
163+
[plan slow-query-<opaque-id>]> operators --top 10
164+
[plan slow-query-<opaque-id>]> missing-indexes --json
165+
[plan slow-query-<opaque-id>]> close
166+
```
167+
168+
The same graph also works in one-shot and MCP modes:
169+
170+
```bash
171+
planview plan open slow-query.sqlplan --json --no-logo
172+
planview repl # explicit interactive alias
173+
planview mcp serve # stdio MCP server generated from the graph
174+
```
175+
176+
Bare `planview` preserves the legacy System.CommandLine behavior. Interactive exploration is
177+
entered only through `planview repl`; scripts can use explicit legacy or one-shot `plan` commands.
178+
179+
The pilot uses the stable `Repl` `0.11.0` release. Loaded plans live only for the lifetime of
180+
the process; `open` and `close` change only that in-memory catalog and never write the
181+
source plan. MCP `plan_open` validates and reads the same opened file handle inside client
182+
roots, accepts only `.sqlplan` files, and uses the server launch directory only when native
183+
roots are unsupported and no soft roots exist. Advertised empty roots deny access. MCP and
184+
local mode share the 16 MiB file/2-concurrent-open/32-session resource limits. See
185+
[`docs/repl-pilot.md`](docs/repl-pilot.md) for architecture, security, and compatibility notes.
186+
152187
### Capture and analyze plans from a live server
153188

154189
Connect to a SQL Server instance, run queries, and capture their execution plans automatically.

‎docs/repl-pilot.md‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Repl Read-only Pilot
2+
3+
## Goal
4+
5+
Add a Repl 0.11.0 command surface for loaded SQL Server plans while preserving the existing System.CommandLine CLI and its JSON contracts.
6+
7+
Read-only describes the analyzed plans and external systems: the pilot never writes a plan file, database, Query Store, or credential. `open` and `close` do mutate the process-local session catalog and are therefore not annotated as read-only MCP operations.
8+
9+
## Architecture
10+
11+
1. Put detached analysis snapshots plus `IPlanCatalog` and its thread-safe in-memory implementation in `PlanViewer.Core`. REPL sessions retain the analyzed projection rather than the mutable parser graph; desktop sessions keep their UI graph and expose a separately captured MCP projection.
12+
2. Put typed, renderer-free operations (`open`, `close`, `list`, `summary`, `warnings`, `missing indexes`, `expensive operators`) and the shared parse/analyze/score pipeline in `PlanViewer.Core`.
13+
3. Keep the Avalonia `PlanSessionManager` as a thin compatibility singleton over the Core catalog. Existing App MCP tools delegate to Core operations using the already-resolved session snapshot, avoiding a second catalog lookup while preserving their historical output behavior.
14+
4. Add a Repl `IReplModule` in `PlanViewer.Cli`, pinned to the stable `Repl`/`Repl.Mcp`/`Repl.Testing` 0.11.0 release. Launch Repl only for explicit `repl`, `plan`, `open`, or `mcp` prefixes; zero arguments and existing prefixes remain on System.CommandLine.
15+
5. Return typed values from Repl handlers so text, JSON, tests, and MCP are renderer concerns rather than handler concerns.
16+
17+
## Security and resource boundaries
18+
19+
Local CLI/Repl commands may open any accessible `.sqlplan` file, matching normal command-line file semantics. The generated MCP server applies a stricter policy:
20+
21+
- `plan_open` rejects lexically outside paths before probing them, opens an allowed candidate once, validates its kernel-resolved handle against resolved MCP roots, and parses that same handle. Containment is ordinal on Linux and case-insensitive on Windows and macOS to match their default filesystems and client URI casing; per-directory/per-volume case-sensitive configurations on Windows or macOS are outside this pilot's scope. NTFS alternate-data-stream paths are rejected.
22+
- The server launch directory is a fallback root only when native roots are unsupported and no soft roots are configured. Advertised-but-empty, invalid, unavailable, or unresponsive roots deny access; native roots negotiation has a 5-second server-side deadline.
23+
- MCP errors do not disclose absolute paths outside the allowed roots.
24+
- Only an explicit allow-list of plan tools is exported; automatic read-only resource promotion is disabled.
25+
- Files are read through a hard 16 MiB streaming ceiling even if they grow during loading. A forward-only XML preflight prohibits DTDs and rejects depth over 512, more than 10,000 structurally recognized statements/cursor operations, 100,000 operators, 250,000 total elements, or 1,000,000 total attributes before `XDocument` and the parser graph are materialized; the parsed graph is checked again before analysis. Streaming SHA-256 values bind the preflight and decoded passes to identical bytes, rejecting in-place content changes before materialization.
26+
- The limit of 2 concurrent opens is shared by every `PlanOperations` facade over the same catalog; excess calls wait asynchronously, honor request cancellation, and do not resolve paths or acquire files until admitted. Admission is coordinated atomically across those facades, with at most 32 sessions and a 64 MiB aggregate retained-analysis estimate. The estimate is reserved after streaming preflight and conservatively accounts for UTF-16 source text, statements (2 KiB each), operators (4 KiB each), XML elements (256 bytes each), and attributes (128 bytes each); `plan_close` releases it.
27+
- Warning responses return at most 500 items, missing-index responses at most 100 suggestions, and operator queries at most 100 items. The query paths count or rank by bounded top-K storage instead of materializing an additional full-tree result list. The generated CLI/MCP pilot admits at most 4 full-tree query traversals concurrently per catalog and rejects excess work; the existing desktop App MCP host remains exempt from that new cap. Result metadata reports truncation, and plan-controlled text fields are bounded before serialization.
28+
- `plan_close` provides explicit eviction and releases the retained-memory reservation. Session labels are capped at 48 characters and use a fresh 12-hex-character opaque suffix; active-ID collisions are retried.
29+
- Streaming preflight, direct bounded text decoding, per-statement/per-operator analysis, scoring, mapping, registration, and Repl query traversals honor cancellation. XML decoding reads the authorized stream directly with a pooled buffer rather than retaining a second byte-for-byte copy.
30+
31+
## TDD slices
32+
33+
- Catalog registration, bounded concurrent allocation, list, lookup, and removal.
34+
- File-open success plus extension, missing, empty, malformed, oversized, and complexity failures.
35+
- Summary and severity-filtered warning DTOs.
36+
- Expensive-operator ranking and `top` validation.
37+
- Missing-index result shape.
38+
- Repl one-shot graph, close/eviction, and interactive session persistence.
39+
- App MCP behavior characterization for warning scope, invalid severity handling, bare object names, numeric actual-stat defaults, and single-snapshot lookup.
40+
- Real stdio MCP coverage for the exact tool allow-list, annotations, empty resource list, roots confinement, advertised empty-root denial, symbolic-link escape rejection, and an allowed open/summary round trip.
41+
- Direct path-policy coverage pins same-handle validation/reading across a pathname swap where the platform permits the swap.
42+
43+
## Verification
44+
45+
- `dotnet build PlanViewer.sln -c Release`
46+
- `dotnet test PlanViewer.sln -c Release`
47+
- Compare legacy compact JSON output byte-for-byte against `upstream/dev` for a fixed `.sqlplan`.
48+
- Run Repl one-shot and persistent-session smoke tests.
49+
- Start the generated MCP server over stdio with a real `McpClient` and a bounded timeout.
50+
- Run `git diff --check`, package-vulnerability inspection, and added-line secret/injection scans.
51+
52+
## Command graph
53+
54+
```text
55+
open {path} # local convenience command; navigates to the plan
56+
plan open {path} # canonical open command
57+
plan list # process-local loaded plan sessions
58+
plan {id} summary
59+
plan {id} warnings [--severity Critical|Warning|Info]
60+
plan {id} expensive-operators [--top 10]
61+
plan {id} operators [--top 10] # local alias
62+
plan {id} missing-indexes
63+
plan {id} close # evict the in-memory session
64+
```
65+
66+
Inside an interactive plan context, the `plan {id}` prefix is omitted. Handlers return typed DTOs; operation results carry stable `JsonPropertyName` annotations, while catalog summaries retain their existing property casing. Rendering is selected by Repl (`--json`, text, YAML, or Markdown) rather than performed by handlers.
67+
68+
The generated MCP server identifies itself as `planview`. It exports the canonical commands only; the root `open` convenience command and `operators` alias remain local.
69+
70+
## Compatibility boundary
71+
72+
- `analyze`, `query-store`, `credential`, and their existing options remain on the original System.CommandLine root.
73+
- Legacy `analyze` keeps its existing input behavior. The pilot intentionally accepts only `.sqlplan` in `open`/`plan_open`; this narrow local-file boundary is not retrofitted onto historical commands.
74+
- The Repl graph is selected only for explicit `repl`, `plan`, `open`, or `mcp` prefixes; zero arguments preserve legacy routing.
75+
- A committed process test runs `analyze --compact` and compares the exact stdout SHA-256 with the pre-change baseline (`0c609fed8e250d9366eb9a6cd5eaf40b661ee30d7ba2546bd7726960592e9d87`).
76+
- The Avalonia MCP session manager implements `IPlanCatalog`; committed App-side tests pin the historical warning scope, invalid-severity message, bare `object_name`, numeric default metrics, and snapshot behavior.
77+
- The desktop MCP host constructs shared operations with `AnalyzerConfig.Default`, so an unrelated malformed `planview.config.json` cannot prevent that server from starting.
78+
- Sessions are process-local. Query Store mutations and credentials are intentionally outside this pilot.
79+
80+
## Dependency status
81+
82+
The pilot is pinned to the stable `0.11.0` packages. The CLI, persistent-session, and real MCP tests exercise the Repl APIs used by this integration.
83+
84+
The Repl repository is MIT licensed. The `0.11.0` NuGet packages still omit license metadata; correcting `PackageLicenseExpression` requires a subsequent Repl package publication and remains an upstream packaging follow-up rather than being represented as fixed by this repository.

0 commit comments

Comments
 (0)