The plan parser reads IF-condition query plans and MULTIPLE PLAN statement hashes (#4468) - #4470
Merged
Merged
Conversation
…4468) The plan parser dropped a StmtCond IF-condition's own QueryPlan (parsed as a bare statement element with no plan of its own, per the old blanket recursion into Condition's children) and the QueryHash/QueryPlanHash of MULTIPLE PLAN statements (read only when a QueryPlan child exists). Condition/QueryPlan is now parsed with the existing cursor-path helper so its statement attributes and operator tree come from the StmtCond element; Condition/UDF sub-plans are walked too. ParseStmtAttributes now runs before the no-QueryPlan early return, so a plan-less statement still gets its hashes and other statement-level attributes.
erikdarlingdata
marked this pull request as ready for review
September 27, 2026 16:05
erikdarlingdata
deleted the
fix/4468-showplan-cond-and-multiple-plan
branch
September 27, 2026 16:05
This was referenced Sep 27, 2026
Parse IF-condition query plans and MULTIPLE PLAN hashes (#580)
erikdarlingdata/PerformanceStudio#581
Merged
erikdarlingdata
added a commit
to nmummau/PerformanceStudio
that referenced
this pull request
Sep 29, 2026
…ata#580) 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
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.
Fixes #4468.
Why
ShowPlanParser.Parsedropped two kinds of statement that carry their own plan or hashes:StmtCondwithStatementType="COND WITH QUERY"(anIF EXISTS (SELECT …)condition): the condition'sown
QueryPlan, under<Condition>, was never parsed as a statement. It came back with an emptyStatementType, nullQueryHash/QueryPlanHash, and a placeholderSTATEMENTroot — its real operatortree, its missing-index suggestions, and every warning
PlanAnalyzerwould raise on them were lost.StmtSimplewithStatementType="MULTIPLE PLAN": a statement element that carriesQueryHashandQueryPlanHashbut noQueryPlanchild came back with both hashes null.Both hashes are what Query Store and plan-cache matching join on, so a batch with either shape under-reported
its own statements to every downstream reader: the plan-analysis MCP tools, the drill-down collectors that
aggregate missing indexes and warnings across a set of plans, and the plan viewer.
What changes
PerformanceMonitor.PlanAnalysis/ShowPlanParser.cs:ParseStatementAndChildren(StmtCond branch): per the showplan XSD (StmtCondType/Condition),Conditionholds the condition's own
QueryPlan(0 or 1) plus optionalUDFsub-plans — never a nestedStmt*element.The old code recursed into
Condition's children with the same statement parser used forThen/Else, whichhanded the bare
QueryPlanelement toParseStatement(no attributes of its own to read). The fix reads thecondition's
QueryPlandirectly and parses it with the existingParseQueryPlanAsStatementhelper (alreadyused for the cursor path), so its
StatementText,StatementType,QueryHash,QueryPlanHash, and the restof
ParseStmtAttributesall come from theStmtCondelement, and its plan comes fromCondition/QueryPlan.Condition/UDFsub-plans are now also walked.Then/Elserecursion is unchanged.ParseStatement:ParseStmtAttributes(which readsQueryHash,QueryPlanHash,StatementId, and therest of the statement-level attributes — none of which depend on a
QueryPlanchild existing) now runs beforethe no-
QueryPlanearly return, not only after it. The synthetic placeholder root node is unchanged.PerformanceStudio's parser (
src/PlanViewer.Core/Services/ShowPlanParser.csin erikdarlingdata/PerformanceStudio) has the sameStmtCondrecursion shape. This PR does not change it.
Test plan
New file
Darling/Darling.Tests/ShowPlanParserCondAndMultiplePlanTests.cs, five facts against the issue's ownrepro XML (synthetic
dbo.t,dbo.u,[db]):StatementCount_IsThree_OneConditionOneThenOneMultiplePlan— the batch parses to exactly 3 statements(the condition's own plan, the
Thenbranch'sRETURN, and theMULTIPLE PLANsibling).CondWithQuery_KeepsItsOwnHashesAndRealOperatorRoot—QueryHash/QueryPlanHashmatch theStmtCondelement's own hashes, and the statement's root wraps the real
Table ScanRelOp, not a bare placeholder.CondWithQuery_KeepsItsMissingIndexSuggestion— the[t]/[id]equality missing index (impact 90.5) isparsed onto the statement AND surfaces through the batch-wide
ParsedPlan.AllMissingIndexesrollup that thedrill-down collectors and
PlanAdvisoryAggregatorboth read.ThenBranch_StillHasItsReturnStatement— theThenbranch'sRETURN NONEstatement is unaffected.MultiplePlan_KeepsItsHashesAndPlaceholderRoot— theMULTIPLE PLANstatement's hashes are present and itsplaceholder root (no operator tree, since there's no
QueryPlan) is kept.RED on
origin/dev(git worktree add --detachat2a33914ae, test file copied in, no other change):3 of 5 facts failed at runtime (
CondWithQuery_KeepsItsOwnHashesAndRealOperatorRoot,CondWithQuery_KeepsItsMissingIndexSuggestion,MultiplePlan_KeepsItsHashesAndPlaceholderRoot) — 2/5 passedbecause they don't depend on either bug.
Total: 5, Errors: 0, Failed: 3.Mutations (each applied, run, reverted):
StmtCond/Conditionbranch to the old blanket recursion → the twoCondWithQuery_*factswent RED (
Total: 5, Failed: 2); reverted.ParseStmtAttributes(stmt, stmtEl)back behind thequeryPlanEl == nullreturn inParseStatement→MultiplePlan_KeepsItsHashesAndPlaceholderRootwent RED (Total: 5, Failed: 1); reverted.Green after the fix, on this Mac (
Darling.Tests.dllin-process,Microsoft.WindowsDesktop.Appframeworkentry removed from the runtimeconfig): the new class (5) plus
McpPlanAnalysisEnvelopeTests(the otherDarling.Tests class touching
PlanAnalysis) andDocCommentHygieneTests—Total: 88, Errors: 0, Failed: 0.Both
Darling.TestsandLite.Testsbuild Release with-p:EnableWindowsTargeting=true: 0 warnings, 0 errors.No Lite-specific parser tests exist to name (
Lite.Testshas noShowPlanParser/PlanAnalysis-named testfile);
Lite.Tests.dlldoesn't run in-process on this Mac (WindowsBase discovery failure) — this change carriesno Lite-only behavior, so CI's normal run is the only additional signal.
CHANGELOG entry
SECTION: Fixed
ENTRY:
IF EXISTS (SELECT …)condition's operators and missing-index suggestions were dropped, and aMULTIPLE PLANstatement came back with no hashes.REF:
[The plan parser reads IF-condition query plans and MULTIPLE PLAN statement hashes (#4468) #4470]: The plan parser reads IF-condition query plans and MULTIPLE PLAN statement hashes (#4468) #4470