Skip to content

fix(datagrid): keep the query tab when navigating to a foreign key reference - #2018

Merged
datlechin merged 1 commit into
mainfrom
fix/fk-navigation-destroys-query-tab
Aug 4, 2026
Merged

fix(datagrid): keep the query tab when navigating to a foreign key reference#2018
datlechin merged 1 commit into
mainfrom
fix/fk-navigation-destroys-query-tab

Conversation

@datlechin

Copy link
Copy Markdown
Member

Problem

From a query tab: run a query whose results contain a foreign key column, then click the FK arrow in a cell. The referenced table opens, but the query tab is gone. Its SQL and its results are both lost, and there is no way back to them.

Root cause

navigateToFKReference decided "reuse the current tab" vs. "open a new tab" on openInNewTab || changeManager.hasChanges. changeManager.hasChanges only tracks pending cell edits, so running a SELECT never set it. A plain click therefore fell through to QueryTabManager.replaceTabContent, which rewrites the selected tab in place: tabType becomes .table, content.query is overwritten with the generated base-table SELECT, and display.resultSets and execution.lastExecutedAt are cleared. The tab keeps its UUID, so nothing in the app sees a tab closing. The work is just overwritten.

The predicate that should have stopped this already existed. QueryTab.holdsQueryWork carries the comment "A tab holding query work must not be silently reused in place", and isActiveTabReusable honours it. FK navigation was the one caller that never asked.

Fix

Extract the shared rule into selectedTabHoldsProtectedContent (pending cell edits, query work, or a committable create-table design) and have both isActiveTabReusable and the FK path derive from it, instead of two hand-rolled guards that had already drifted apart.

isActiveTabReusable keeps its extra sidebar-only checks (applied filters, user sort, pinned results). Those are deliberately not in the FK gate: FK navigation applies a filter itself, so every table tab reached by a previous FK hop has applied filters, and including them would make chained a -> b -> c navigation open a new tab at every hop.

Plain-click in-place navigation between table tabs is unchanged. That behaviour is documented in docs/features/data-grid.mdx and was added deliberately in af0f332 (#1421, #1427), and its test still passes as written. The defect was narrower: replacing a tab that holds work the user authored.

Also fixed on the same path

The in-place branch was missing four steps its sibling reuseActiveTab already performs:

  • saveLastFilters(for:) - the outgoing table's saved filters were dropped on every hop.
  • cancelTableLoad(for:) - the tab id is reused, so an in-flight load could complete after the swap and race the new content in.
  • restoreLastHiddenColumnsForTable() - replaceTabContent wipes columnLayout, so saved hidden columns for the target table never came back.
  • promotePreviewTab() - a preview source tab stayed replaceable by a later unrelated click.

Repeat clicks

Because a plain FK click from a query tab now opens a tab, clicking the same reference twice would otherwise leave two identical tabs. It now returns to the tab it already opened, matching on table, database, schema and the exact filter predicate. A click on a different referenced row always opens its own tab and never re-filters a tab opened for another row. Cmd-click never reuses.

Testing

swiftlint lint --strict is clean on all changed files. 47 tests pass with no failures (FKNavigationTests, OpenTableTabTests, QueryTabProtectionTests).

New:

  • an executed query tab survives a plain FK click, byte for byte, and the reference opens separately (this is the test that fails on main)
  • same for a query tab with typed but unexecuted SQL
  • same for a table tab with pending cell edits
  • a repeated identical click returns to the existing tab
  • a different referenced row opens its own tab and leaves the existing tab's filter alone
  • Cmd-click still opens a new tab when an identical target is already open
  • an in-place hop saves the outgoing filters and restores the target's hidden columns
  • an in-place hop cancels the outgoing tab's in-flight load
  • seven cases for selectedTabHoldsProtectedContent

No existing test's assertions changed. All five pre-existing FKNavigationTests and all six isActiveTabReusable tests pass unmodified, which confirms the extraction is behaviour-preserving. I also verified by inspection that a preview query tab is unreachable (isPreview is only ever set true alongside tabType = .table, and PersistedTab does not carry it), which is the one case where the rewrite could have diverged.

Notes

MainContentCoordinator.openTabInNewWindow is a seam so the decision is testable. The test suite deliberately never calls WindowManager.openTab, because it builds a real TabWindowController and NSWindow; its only test file covers the pure static tabbingIdentifier(for:) helper.

Two follow-ups, deliberately not in this PR:

  • openTableTab's navigationModel == .inPlace branch is a second replaceTabContent call site with no content guard. It is unreachable from FK navigation (Redis database switching only), but Redis tabs can be query tabs, so the same bug is reachable there. It is a one-line guard now that this predicate exists.
  • Every client that navigates in place ships back/forward history as the mitigation (DBeaver Ctrl+left/right, Sequel Ace Ctrl-Opt-left/right, TablePlus since 6.3.2). Worth considering as a feature.

@mintlify

mintlify Bot commented Aug 4, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
TablePro 🟢 Ready View Preview Aug 4, 2026, 6:48 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@datlechin
datlechin merged commit 842bf75 into main Aug 4, 2026
3 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