fix: stop the schema refresh from blanking the sidebar and the structure tab counts (#1916)#1952
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 570e0495fe
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| await schemaService.refreshLoadedSchemaTables( | ||
| connectionId: connectionId, | ||
| driver: driver | ||
| ) |
There was a problem hiding this comment.
Prune tables from schemas removed during refresh
When a hierarchical-schema refresh drops or renames a schema that had previously been expanded, this reloads every entry in perSchemaStates, including schemas absent from the newly fetched schema list. A missing-schema fetchTables normally fails, and runSchemaLoad deliberately preserves the prior loaded state on failure; allLoadedTables then continues to include those stale tables and feeds them to autocomplete. Filter/prune per-schema state against the refreshed schemasInOrder before retaining it (and only reload schemas that still exist).
Useful? React with 👍 / 👎.
Fixes #1916.
Frame-by-frame on the reporter's recording, a structure save produces two separate flickers. Deleting an index at t=9.25s:
Columns (24) │ Indexes (4) │ Foreign Keys (3)Columns (24) │ Indexes │ Foreign Keys (3)Indexes (3), third re-layoutBoth saves in the video do it, and at 41.7s the sidebar is still blank after the index list has already refreshed.
Root cause
One modeling error in two places: state that conflates do we have data with is a fetch in flight, so refreshing destroys what it is refreshing.
Sidebar.
SchemaState.loadingcarries no payload andtables(for:)returns[]for anything but.loaded, whilerunLoadwrotestates[id] = .loadingbefore the network call. SoSidebarView.flatContent'scase .loading where tables.isEmptymatched on every refresh and thecase .loaded, .loading: tableListfallback below it was unreachable. The view could not tell a cold load from a warm refresh because the data was already gone.Structure counts.
loadedTabs.removeAll()cleared the loaded flag for every tab at once, buttabLabel(for:)gates each count onloadedTabs.contains(...). That flag was only being cleared so DDL and triggers would refetch later; the counts were collateral damage.Safe Mode is not a second trigger. It is a pure gate with no reload path, so it only makes users watch the screen during its confirm prompt.
Fix
SchemaServicenow fetches first and commits over the old value:.loadingis entered only when there is no loaded content; an in-flight refresh is signalled separately throughisRefreshing, guarded by load generation so a superseded load cannot clear the current one's flag.markLoadFailed, which already had the "keep loaded data" guard, so a transient refresh error no longer swaps the sidebar for an error screen.invalidate(real teardown: disconnect, database switch) is split fromprepareForReload(cancel in-flight fetches, keep content).SchemaRefreshServicewas callinginvalidateon every hierarchical refresh, which wiped the tree to.idle.refreshLoadedSchemaTablesre-fetches expanded schemas in place, so keeping content does not turn into serving stale content.StructureTabDataStatesplits "has data" (drives the count) from "needs refetch" (drives the reload), so marking everything stale never blanks a count. Both reload sites fetch columns, indexes and foreign keys in onewithMetadataDriverblock and commit all three in a single synchronous write: one render instead of three..monospacedDigit()keeps a4→3change from reflowing segments too.Removing the blank would have silently removed all feedback from a slow manual refresh, so
DelayedProgressIndicatorshows a small spinner in the sidebar footer only past ~500ms. It sits before theSpacer(), so nothing shifts when it appears.Why this shape
DatabaseTreeMetadataService.reloadTablesInPlacealready had the correct pattern andmarkLoadFailedalready had the guard.SchemaServicewas the outlier.DataGrip shipped a fix for this exact behaviour in 2024.3, describing the old one as "a full introspection of the entire schema... time-consuming and often disrupted the workflow". Postico fixed the tree-collapse half in v1.2.
Tests
14 new tests. The key one gates the driver's
fetchTableson a continuation and asserts mid-flight thattables(for:)still returns the old list and the state is not.loading, the assertion whose absence let this ship. Also covers cold load still reporting.loading, a failed refresh keeping loaded tables,prepareForReloadvsinvalidate, and per-schema refresh.42/42 pass, including every pre-existing suite in this area (
SchemaServiceTests,SchemaServiceHierarchicalTests,SchemaServiceRoutinesTests,DatabaseTreeMetadataServiceTests). SwiftLint--strictclean.Not verified: the visual result. That needs the app running against a real PostgreSQL connection, so it is worth reproducing the issue's steps before merge.
Follow-ups, deliberately not in this PR
.truefalsedatabase. Visible in the same recording (green optimistic row says YES at 41.3s, reloaded row says NO at 41.7s).StructureRowProvider.customDropdownOptionsregisters options for the Indexes Type column but not Unique, so the generic boolean fallback offers"true"/"false"whileStructureEditingSupport.updateIndexonly accepts"YES"/"1". Separate data-correctness bug, deserves its own fix and changelog line.https://claude.ai/code/session_01WKxR4tvL8bPU4NfsULyYNA