Notice a plan arriving by every route, not just the one with a test (#447) - #481
Conversation
…447) Compare Plans came back disabled after running two queries, on a build containing the fix that was supposed to have sorted that out. #449 fixed the enablement rule and left the refresh: a plan produced by executing a query lands by having an existing tab's Content replaced, and #449 subscribed to the tab collection, which says nothing about that. The same shape sits at window level in Get Actual Plan on a file tab. Both tab controls now go through TabContentWatcher, which reports collection changes and content replacement, so the next path that produces a plan is correct without its author knowing the watcher exists. The five hand-written refreshes in Plans.cs go with it - there is one place that decides now. The owner lookup moves off TopLevel.GetTopLevel and onto the logical tree. A TabControl realises the selected tab and nothing else, so a session in a background tab could not see its own window, and a query left running while the user works elsewhere lands its plan in exactly that state - the fallback then reinstated the original bug. Erik's own table had Query Store down as broken; it is not. Its two plan-producing sites go through AddPlanTab, which did refresh. What was broken is both execution paths and the window-level actual plan. The tests now drive those paths rather than the file path they avoided last time. What still needs a SQL Server is producing plan XML, and only that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016a1AnKAHwcALrwdYVVrpgR
|
Reviewed the diff (QuerySessionControl.*, MainWindow.PlanViewer.cs/axaml.cs, new TabContentWatcher, and the expanded test file). The root-cause diagnosis holds up: Checked for issues and didn't find any:
No inline comments — nothing rises to a real finding. |
Fixes #447, which I closed once already and should not have.
What was actually wrong
#449 fixed the enablement rule. It did not fix the refresh, and joshdbe's report was never about the rule.
A plan gets into a session's sub-tabs two ways, and they are shaped differently:
AddPlanTab.loadingTab.Content = viewerswaps the plan in. The tab count never changes.#449 subscribed to
MainTabControl.Itemscollection-changed and called that the single seam. A collection subscription cannot see the second shape, so every plan produced by running a query landed without a word to anybody. Two queries in two tabs, Compare dead in both — exactly as reported, on a build containing the fix.The same shape sits at window level: Get Actual Plan on a file tab opens a spinner tab and later assigns
tab.Content = CreatePlanTabContent(actualViewer)over it. Second instance of the same bug, never reported because you need a server to reach it.And a third, found by the new tests rather than by reading:
UpdateCompareButtonStatelocated its window withTopLevel.GetTopLevel. ATabControlrealises the selected tab's content and nothing else, so a session sitting in a background tab has no visual root, cannot find its own window, and falls back to counting its own plans — which is the original bug, restored. Start a query in one tab, work in another while it runs, and its plan lands in precisely that state.Correcting my own table
I posted a table on the issue. Two of its three rows are wrong, and following it would have fixed the wrong things:
.sqlplanfile into a session refreshes, viaPlans.cs:109AddPlanTabhas exactly two callers and both are Query Store. Opening a plan file opens a window-level tab (MainWindow.FileOps.cs), which the collection subscription did catch.SubTabControl.Items.AddsitesAddPlanTab, which calledUpdateCompareButtonState. The fourItems.Addsites I pointed at add the QS grid, the QS overview and the history tab — containers, not plans.The new Query Store test passes on
devunmodified. It is in here as a pin on the seam swap below, not as a reproduction of anything.The seam
Helpers/TabContentWatcher.cs, wired once toMainTabControland once to each session'sSubTabControl. It reports collection changes and content replacement on any tab it holds.I considered just adding the missing calls at the three sites. The reason I did not is the reason we are here: #449's call sites were correct for the paths their author had in mind, and the next author did not know to look. A plan cannot reach the screen without either adding a tab or filling one in, so watching both is the smallest thing that is also complete — the next plan-producing path is right without anyone remembering this file exists.
Given that, the five hand-written
UpdateCompareButtonState()calls inPlans.csare gone. Leaving them would have meant two mechanisms doing one job, and the whole argument is that there should be one place that decides.Content replacement is watched through
TabItem.PropertyChangedfiltered toContentProperty, with subscriptions tracked in a set rather than derived from the collection-changed args — aResetcarries neitherOldItemsnorNewItemsand would leave stale subscriptions behind.The test, which is the part I got wrong last time
The old test file built its scenario from plan files, with a comment saying executed queries need a live SQL Server and the defect does not require one. The second half was true. The first half was the bug: a plan file opens a window-level tab, which is the one path that was already recomputing. Every assertion passed against a build where the reported behaviour was unchanged. A test that cannot reach the broken code is not coverage, and I shipped it as though it were.
Four new tests, driving the paths a plan actually arrives by:
RunningAQueryInEachOfTwoSessionsOffersCompareInBoth— the report, verbatim.RunningTwoQueriesInOneSessionOffersCompareThere— the case the pre-Offer Compare Plans across query sessions, not just within one (#447) #449 arithmetic did handle, now that the arithmetic is gone.QueryStorePlansOfferCompare_AndClosingOneTakesItAway— Query Store's add path, and closing a plan tab through its own close button.AnActualPlanArrivingInAnExistingWindowTabIsNoticed— window-level content replacement.No SQL Server. What needs one is producing plan XML, and only that:
ShowCapturedPlanis the landing step both execution paths end at, and it is now internal so a test can hand it a committed fixture in place of what the server would have returned. Same forOnQueryStorePlansSelected, which needs plans, not a connection.Red first, three ways. With the extractions kept so the tests compile, and behaviour put back to
dev:devbehaviour)TopLevel.GetTopLevelAll four green with the fix in.
What is still uncovered
CaptureAndShowPlanandGetActualPlan_Clickabove the landing step — connecting,SET STATISTICS XML ON, cancellation, the error paths. Unchanged by this and still server-only.OnHistoryPlanLoadRequestedgoes through the sameAddPlanTabas the grid path that is covered; one of the two is tested.TopLevelhole is fixed and the tests happen to exercise the unrealised case, but nothing asserts it deliberately — no test switches tabs mid-flight.Counts
388 tests on
origin/devbefore this, 0 failed, 2 skipped. 392 after, 0 failed, 2 skipped. Five consecutive full runs, stable, ~15s each.🤖 Generated with Claude Code
https://claude.ai/code/session_016a1AnKAHwcALrwdYVVrpgR