fix(tabs): reload only the tabs showing a changed table, never interrupt one holding edits, and read SQLite result columns after the first step - #3148
Merged
Conversation
…upt one holding edits, and read SQLite result columns after the first step
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
This branch was successfully deployed
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.
Root cause
The table a change named was used to pick which tabs to reload, but only by the object channel. Everything else used
AppCommands.refreshData, and each window answered it by reloading its selected tab, whatever table that tab showed. That covered row import, Create Table, structure saves, rebuilds, column moves, trigger edits and Insert Document. Before reloading, the window committed any half-typed cell and asked "Discard Unsaved Changes?". On confirm it also cleared the window's sidebar-staged drops and truncates. Background tabs on the changed table were never visited. They kept their rows, andcanAutoLoadTableTabrefused to reload them when shown, because they had rows and had already run.The object channel (
DatabaseObjectChange.rows, used by Refresh Materialized View) reached background tabs only through memory eviction. Eviction discards rows and refuses a tab whose last result was empty. Its selected-tab path went throughhandleRefresh, which prompted, and which in Structure mode refreshed only the structure. So switching back to Data showed the old rows and columns.A grid Save announced nothing, so a second tab on the same table, in this window or another, kept the pre-save rows.
A reload after a structure change reused the metadata the tab already held.
isMetadataCachedonly asked whether the tab had metadata, never whether the definition had changed since. So the old primary key, defaults, nullability, generated columns and row-match policy were applied to the new columns. When the new result reported no primary key, the old one was carried over, so a dropped key survived and the next edit was built against it.A tab's column-scoped
SELECTis built from a cached column list before anything fetches the table. A SQL file import, a new enum label and a session context switch never dropped that list, before this branch or after it. So a tab with hidden columns reloaded with a select list that named a dropped column or left out an added one. A load that started before a second definition change could also write its old column list back into the cache after the change had dropped it.A row count that started before a change could finish after it, while its tab was in the background, and put the old total back. The reload that answered the change kept that total. Above the automatic-count threshold it skipped counting, so Next and Last stayed bounded by the table as it was.
A structure session left on DDL or Triggers was marked stale, but its next mount fetched only columns, indexes, foreign keys and checks. The mount does not change the selected sub-tab, so nothing fetched the one on screen. Applying staged edits from a background tab had the same gap before this branch.
The selected tab put a reload off while a cell editor or viewer was open, or while it held edits, and nothing asked again once they were gone. Closing a read-only viewer, cancelling an editor with Escape, or discarding the edits left the tab showing old rows until
Cmd+Ror a tab switch.The SQLite UI test exposed one more cause.
SQLiteLocalBackendread column names right aftersqlite3_prepare_v2, which compiles against the connection's cached schema. Only the firstsqlite3_stepnotices that another connection altered the table and prepares the statement again. The structure editor alters on a pooled connection, so the first query on the session connection after every structure save named the old columns and dropped the new column's values. libSQL's local backend had the same read.Fix
TableFreshnesson eachTabSession: a stale mark separate from eviction. It keeps the rows, and a tab with no rows can be marked.DatabaseObjectChange.changedAt,DataRefreshRequest.changedAt).pendingChange).canAutoLoadTableTabtreats a stale tab like an evicted one, after its error and pending-edit guards.TableRowsRefreshPlan: a pure planner. It marks every addressed table tab and leaves out the tab that made the change when that tab reloads itself. It reloads the selected tab only when that tab holds no edits and has no cell editor or viewer open.MainContentCoordinator.refreshTableTabs,applyDataRefreshandapplyObjectChangeshare that plan, and none of them asks a question.resumeDeferredTableRefresh()runs a reload the selected tab put off, once what was in the way is gone. It asks the plan again (TableRowsRefreshPlan.action(for:state:owing:)) with the change the tab still owes, so a new overlay, a staged edit or the tab's own load still stands in the way.DataGridViewDelegate.dataGridDidCloseCellOverlay). An editor records its commit after it removes itself, which is why it waits a turn.DatabaseObjectChange.Kind.structure, sent by structure save, rebuild, column move and trigger create, edit and drop for their one table. It marks a definition change and marks clean background structure sessions stale.DataRefreshRequestnames no table, so it drops the whole cache.TableFreshness.definitionIsCurrent).isMetadataCachedanswers false. The reload waits for the table's schema and commits it with the rows in phase 1, and the same fetch refills the column list.QueryExecutionCoordinator.resolvedPrimaryKeys, which never carries the old keys across a definition change.applyPhase1Result, before phase 2 counts. That reload's claim already cancels and fences any older count, so nothing can put the old total back after it.StructureEditingSession.tabsFetchedOnMountlists the sub-tabs the change manager is baselined from, then the selected one, andloadInitialDatafetches that list..rowsis now sent by row import, Insert Document, Create Table and the grid Save.DataRefreshRequestis kept for SQL file import, session context switches and enum labels. It marks a definition change, because each of those can change one.isApplyingStagedStructureEditsis removed. It existed only to stand down against the apply's own broad broadcast, which no longer exists.SQLiteResultColumns.stepFirstreads column names and declared types after the first step.TableProSQLiteCoreand importsCSQLite, never the SDK'sSQLite3, whoselink "sqlite3"put macOS's library on the plugin's link line.runStatement,streamQuery) and the libSQL local backend both use it.Verified
TableProandAllPlugins(all 40 plugins) PASS.TableFreshnessTests,TabSessionRegistryTests,TableRowsRefreshPlanTests,CatalogChangeWindowTests,DataRefreshScopeTests,StructureEditingSessionTests,KeyHandlingTableViewOverlayTests.MainContentCoordinatorLazyLoadTests,DatabaseManagerSchemaChangeRoutingTests,ResolvedPrimaryKeysTests,CellOverlayEditorMovementTests,ValueFilterChangeGuardTests,Phase2RowCountGuardTests,SchemaColumnStoreTests,DatabaseObjectToolsTests,OpenTableTabTests,SQLiteResultColumnsTestsandVendoredSQLiteImportTests.refreshForgetsCachedColumnsBeforeTheReload,answeringReadRetiresALateTotal,deferredChangeReloadsOnceTheEditsAreGone,mountFetchesTheSelectedSubTabandclosingAnOverlayTellsTheOwnerOnTheNextTurn.aPreChangeDefinitionStaysOutOfTheColumnCachefailed.resumingDuringTeardownReloadsNothingfailed.TableChangeReloadUITests(SQLite sample, fixture re-seeded at each launch), 5 of 5 passed on the final code:Cmd+R, and the first tab keeps its own sort.Cmd+R.StructureTabIdentityUITests,StructureForeignKeyEditUITests,SidebarTableTabUITests,InspectorEditReachesGridUITestsandValueFilterEditUITests: 7 of 7 passed. They ran before the last two small edits, a parameter refactor and the teardown guard.SQLiteDrivernorLibSQLDriverPluginhas alibsqlite3load command.sqlite3_stepand the column functions resolve inside each binary as non-external symbols from the vendored archive.SQLiteResultColumns.stepFirst.swiftlint lint --stricton the 48 Swift files in the commit: 0 violations.verify.sh docspassed.review, three rounds, every finding fixed here. Round 3 found that a structure change arriving while edits were staged was dropped rather than owed (now owed and fetched once the edits are applied, removed or discarded), that an unmounted Structure view missed the broad refresh, and that a reload put off for edits did not resume when those edits were undone.Deliberately not fixed here
Cmd+R.DatabaseObjectChange.matchesstill compares the stored schema, not the resolved scope.No UI test for the MongoDB two-window flows: CI has no MongoDB server. No UI test for a reload resuming when a cell overlay closes: an overlay closes as soon as its window loses key or its tab is switched, so only a change that finishes in the background, such as a long import, lands while one is open, and no deterministic UI flow reaches that. Grid and coordinator unit tests cover both.