Fix CLI exit code and --output handling, and stop the web viewer crashing on plans with no statements - #604
Merged
Merged
Conversation
The web page indexed the first statement of the result without checking that there was one. XML that parses but is not a showplan, or a showplan with no statements, has no ParseError, so it got past the page's error handling and threw a NullReferenceException while the result view drew. PlanStatements.NoStatementsMessage decides, using the same traversal the result mapper uses. Index.razor calls it after parsing and after loading a shared plan, and shows the message the way it shows other load errors. The CLI already refused this input for "analyze <file>", but "analyze --server" and "query-store" only checked ParseError, so they wrote an empty analysis and reported OK. The check now lives in PlanAnalysisRunner.ParseFailure, which all three paths call, with the same message as before. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019n3G844aTidqrD6A6iMgza
The per-plan catch reported the error on stderr and in summary.txt, then the command went on and exited 0. A script or CI job could not tell that part of the run had failed. The loop now counts failed plans and the command sets Environment.ExitCode to 1 when the count is above zero, the same way "analyze --server" does. Plans that worked still get their files and their rows in the summary. A "Processed N plans: X succeeded, Y failed" line matches the analyze command's. The loop moved into QueryStoreCommand.AnalyzePlansAsync, which takes the fetched plans and a log writer, so a test can run it without a SQL Server. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019n3G844aTidqrD6A6iMgza
An --output value other than json, text, or both got through. The query-store command and "analyze --server" wrote no files for it and exited 0. "analyze <file>" printed json. PlanAnalysisRunner.CreateOutputOption builds the option with AcceptOnlyFromAmong, and both commands that take --output use it. A bad value is now a parse error: it names the allowed values, exits 1, and the command does no work. The help line lists the accepted values as well. Values are case sensitive, as they were where they were read. WriteResultFilesAsync also throws for an unknown format, before it writes anything or trims the result, so a caller that skips the option cannot get an empty run that looks fine. In query-store that is a failed plan, which the exit code fix turns into exit 1. Program.cs built the command tree inline. It now calls CliRoot.Create, so the tests walk the same tree and check every command that declares --output, including ones added later. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019n3G844aTidqrD6A6iMgza
Same problem as --output, found while fixing it. The query lowercases the value and falls back to CPU order for one it does not know, so a misspelled metric ran to the end ranked by CPU, and summary.txt still said "top by" the misspelling. The option now checks the value while the command line is parsed. It matches without regard to letter case, as the query does (and as --auth does), names the allowed values, and exits 1. The help text is unchanged: it is now built from the same list the check uses. This is a separate commit so it can be dropped on its own. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019n3G844aTidqrD6A6iMgza
AcceptOnlyFromAmong compares case-sensitively, so "-o JSON", which printed json for a single file before, became an error. The option now checks the value case-insensitively, as --order-by does, and both commands read it lowercased through ReadOutputFormat. The file writer still takes only the lowercase forms, which is what its callers pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019n3G844aTidqrD6A6iMgza
AcceptOnlyFromAmong also added the values as completions, which is where the help line's <both|json|text> came from. Add them back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019n3G844aTidqrD6A6iMgza
erikdarlingdata
marked this pull request as ready for review
September 28, 2026 21:43
|
Reviewed the CLI
|
Owner
Author
|
Both notes checked. QueryStoreAnalyzePlansTests runs well-formed non-showplan XML through the Query Store loop: it counts as a failed plan and writes no analysis files. The live path calls the same ParseFailure, which the tests call directly for all three no-statement inputs. Running the live path end to end needs a SQL Server, so it has no child-process test. The exit-code change for query-store goes in the release notes. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
This PR fixes three review findings, F4, F2, and F3. Each fix has its own commit. It also fixes a fourth problem of the same kind, in a separate commit.
F4: the web page crashed on a plan with no statements
The web page read the first statement of the result without checking that one exists. Some XML parses without an error and still has no statement. Well formed XML that is not a showplan is one case. A showplan with an empty batch is another. That input got past the page's error handling. The result view then threw a NullReferenceException while it drew.
The page now shows "Could not parse any statements from the plan XML" in the box that it uses for its other load errors. It checks after parsing. It checks again after it loads a shared plan, because a stored result with no statements fails the same way.
The decision is one Core method,
PlanStatements.NoStatementsMessage. It uses the same statement walk as the result mapper. It returns a message exactly when the mapped result has no statement. The Blazor page cannot be referenced fromPlanViewer.Core.Tests. For that reason the decision lives in Core, in a file that the Web project already links, and the tests pin it there. The page only calls it.I checked the desktop app and the CLI with the same input.
planview analyze <file>already refused it. The desktop app already handles it, with different text: "The plan parsed but contains no statements to display." That code is inPlanViewerControl, which another PR owns, so I left it alone. The web page uses the CLI's text. The MCP and REPL tools use the same words, with a final period.The CLI had a gap.
analyze --serverandquery-storeonly checked for a parse error. For a plan with no statements they wrote an empty analysis and reported OK.PlanAnalysisRunner.ParseFailurenow also returns the no-statements message, and all three CLI paths call it. The single-file path lost its own copy of the check. Its message and exit code did not change.F2:
query-storeexited 0 when a plan failedWhen one plan failed, the command printed the error and wrote an ERROR row in
summary.txt. The process still exited 0, so a script had no way to tell that the run was incomplete.The loop over the fetched plans now counts the plans that failed. The command sets exit code 1 when the count is above zero. That is the rule
analyze --serveralready follows, andProgram.csalready returns the value. Plans that worked keep their files and their summary rows. When it fetched more than one plan, the command also printsProcessed N plans: X succeeded, Y failed, asanalyzedoes for files.The loop moved into
QueryStoreCommand.AnalyzePlansAsync. It takes the fetched plans and a log writer, so a test can run it without a SQL Server. No command documents its exit codes, so I did not add documentation for this one.F3: an unknown
--outputvalue did nothingAn
--outputvalue other thanjson,text, orbothgot through.query-storeandanalyze --serverwrote no files for it and exited 0.analyze <file>printed json.PlanAnalysisRunner.CreateOutputOptionnow builds the option with a check on its value. The two commands that declare--output,analyzeandquery-store, both use it. A bad value is now a parse error. The message names the allowed values, the exit code is 1, and the command does no work. The help line shows<both|json|text>.WriteResultFilesAsyncalso throws anArgumentExceptionfor an unknown format. It throws before it writes a file and before it trims the operator trees. A caller that skips the option cannot get an empty run that looks fine. Inquery-store, that exception counts as a failed plan, so the exit code is 1.Two behavior notes:
--order-by.-o JSONon a single file printed json before, and it still does. Both commands read the value in lowercase throughReadOutputFormat.--server,analyze --output bothstill prints json. The help text says so. The README now says thatbothwrites files with--server.Program.csbuilt the command tree inline. It now calls a newCliRoot.Create. The tests walk that same tree and check every command that declares--output, so a command added later is checked too.Also fixed: an unknown
--order-byvalue inquery-storeI found this while fixing F3. It is the same problem. The query lowercases the value and falls back to CPU order for one it does not know. A misspelled metric ran to the end ranked by CPU, and
summary.txtstill said "top by" the misspelling.The option now checks the value while the command line is parsed. It ignores letter case, as the query does and as
--authdoes. It names the allowed values and exits 1. The help text did not change. It is now built from the same list that the check uses. This is a separate commit, so it can be dropped on its own.Which component(s) does this affect?
The web viewer (
PlanViewer.Web) is not in the template list. This PR changes it too. The documentation change is one line in the README CLI reference.How was this tested?
I ran everything on Windows. No SQL Server was involved. The tests use the fixture
row_goal_plan.sqlplan, and small inline XML for the inputs that have no statements.dotnet build PlanViewer.sln -c Debug --no-incrementalanddotnet build src/PlanViewer.Web -c Release --no-incremental: 0 warnings and 0 errors.This PR adds 28 tests in four classes. Where I say that a test fails without the fix, I removed the fix, ran the class, and put the fix back. The child-process tests use a small helper,
CliProcess. It runs the builtplanview.dllthe same wayHistoricalCliContractTestsdoes.NoStatementsGuardTests(11 tests) covers F4.ParseError. They are non-showplan XML, a showplan with an empty batch, and a showplan with no batch sequence. The mapped result has no statements. The Core decision returns the message.ParseFailurereturns the message for all three inputs.planview analyze <file>on non-showplan XML exits 1 with the same message and prints nothing on stdout.ParseFailurechange, 4 of these 11 tests fail. The tests for the web page decision do not compile without the new method.QueryStoreAnalyzePlansTests(2 tests) covers F2..sqlplan,.analysis.json, and.analysis.txtfiles, including the plan after the failures. The failed plans have no analysis files.summary.txthas four rows, and two of them are ERROR rows.failed++line, the first test fails (expected 2, actual 0).OutputOptionTests(13 tests) covers F3.CliRootfinds every command that declares--output. Each one refusesbogusafter--outputand after-o, with an error that namesbogusand all three allowed values. Each one acceptsjson,text, andboth, in any letter case.planview analyze <file> --output bogusandplanview query-store ... --output bogus. Both exit nonzero, name the allowed values on stderr, and do no work. A third runsplanview analyze <file> -o TEXTand checks that it prints exactly what-o textprints.bogus,JSON, and an empty string, with nothing written and the result unchanged.AnalyzePlansAsync, every plan counts as failed.AcceptOnlyFromAmong, 3 tests fail. Without the writer's check, 4 tests fail.OrderByOptionTests(3 tests) covers the--order-byfix.cpu.Not done
QueryStoreCommand.RunAsyncthat sets the exit code has no test, becauseRunAsyncneeds a live SQL Server. The tests cover the count that it uses.query-storestill exits 0 when Query Store has no data for the time range. No plan failed, so I left it.Checklist
dotnet build -c Debug)dotnet test)🤖 Generated with Claude Code
https://claude.ai/code/session_019n3G844aTidqrD6A6iMgza