Skip to content

Commit 5479130

Browse files
committed
fix(tabs): keep a restored table tab on its own database and load it after reconnect
1 parent 1767c99 commit 5479130

9 files changed

Lines changed: 66 additions & 20 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3030
- The row detail panel no longer stays blank when a table is opened in a second tab. The panel now shows the selected row right away instead of only after the inspector is toggled.
3131
- The sidebar filter now stays put when you open another tab. Before, opening a second table tab cleared the filter text and reset the table list to show everything.
3232
- Fixed a crash when browsing the database tree on servers with many schemas, such as PostgreSQL. The sidebar tree is now a native outline view that loads each database and schema on demand.
33+
- Reopening the app no longer shows a "Not connected to database" error when it restores a table tab on a connection that is still reconnecting, such as one over SSH. The tab waits for the connection and loads on its own.
3334

3435
## [0.51.1] - 2026-06-16
3536

TablePro/Core/Coordinators/QueryExecutionCoordinator+Helpers.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -524,8 +524,11 @@ extension QueryExecutionCoordinator {
524524
}
525525

526526
func restoreSchemaAndRunQuery(_ schema: String) async {
527-
guard let driver = DatabaseManager.shared.driver(for: parent.connectionId),
528-
let schemaDriver = driver as? SchemaSwitchable,
527+
guard let driver = DatabaseManager.shared.driver(for: parent.connectionId) else {
528+
parent.needsLazyLoad = true
529+
return
530+
}
531+
guard let schemaDriver = driver as? SchemaSwitchable,
529532
schemaDriver.currentSchema != nil else {
530533
parent.runQuery()
531534
return

TablePro/Core/Database/DatabaseManager+Health.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,9 @@ extension DatabaseManager {
280280
session.driver = driver
281281
session.status = .connected
282282
session.effectiveConnection = effectiveConnection
283+
if let schemaDriver = driver as? SchemaSwitchable {
284+
session.currentSchema = schemaDriver.currentSchema
285+
}
283286
if let passwordOverride, !session.connection.usesAWSIAM {
284287
session.cachedPassword = passwordOverride
285288
}

TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -383,19 +383,21 @@ extension MainContentCoordinator {
383383
}
384384

385385
/// Switch to a different database (called from database switcher)
386-
func switchDatabase(to database: String) async {
387-
clearFilterState()
386+
func switchDatabase(to database: String, clearTabs: Bool = true) async {
387+
if clearTabs { clearFilterState() }
388388
let previousDatabase = toolbarState.currentDatabase
389389
toolbarState.currentDatabase = database
390390

391391
do {
392392
try await DatabaseManager.shared.switchDatabase(to: database, for: connectionId)
393393

394-
closeSiblingNativeWindows()
395-
persistence.saveNowSync(tabs: tabManager.tabs, selectedTabId: tabManager.selectedTabId)
396-
tabSessionRegistry.removeAll()
397-
tabManager.tabs = []
398-
tabManager.selectedTabId = nil
394+
if clearTabs {
395+
closeSiblingNativeWindows()
396+
persistence.saveNowSync(tabs: tabManager.tabs, selectedTabId: tabManager.selectedTabId)
397+
tabSessionRegistry.removeAll()
398+
tabManager.tabs = []
399+
tabManager.selectedTabId = nil
400+
}
399401
await SchemaService.shared.invalidate(connectionId: connectionId)
400402

401403
await refreshTables()

TablePro/Views/Main/Extensions/MainContentCoordinator+TabSwitch.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,10 @@ extension MainContentCoordinator {
9696
)
9797
changeManager.reloadVersion += 1
9898
Task {
99-
await switchDatabase(to: newTab.tableContext.databaseName)
99+
await switchDatabase(to: newTab.tableContext.databaseName, clearTabs: false)
100+
lazyLoadCurrentTabIfNeeded()
100101
}
101-
return // switchDatabase will re-execute the query
102+
return
102103
}
103104
}
104105

TablePro/Views/Main/Extensions/MainContentView+Helpers.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,12 @@ extension MainContentView {
3232
!selectedTab.tableContext.databaseName.isEmpty,
3333
selectedTab.tableContext.databaseName != session.activeDatabase
3434
{
35-
Task { await coordinator.switchDatabase(to: selectedTab.tableContext.databaseName) }
35+
Task {
36+
await coordinator.switchDatabase(
37+
to: selectedTab.tableContext.databaseName, clearTabs: false
38+
)
39+
coordinator.lazyLoadCurrentTabIfNeeded()
40+
}
3641
} else if let selectedTab = tabManager.selectedTab,
3742
let tabSchema = selectedTab.tableContext.schemaName,
3843
!tabSchema.isEmpty,

TablePro/Views/Main/Extensions/MainContentView+Setup.swift

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -188,16 +188,13 @@ extension MainContentView {
188188
: activeDatabase.flatMap { $0.isEmpty ? nil : $0 }
189189

190190
Task {
191-
var contextChanged = false
192191
if let targetDatabase, targetDatabase != session.activeDatabase {
193-
await coordinator.switchDatabase(to: targetDatabase)
194-
contextChanged = true
192+
await coordinator.switchDatabase(to: targetDatabase, clearTabs: false)
195193
}
196194
if let activeSchema, !activeSchema.isEmpty, activeSchema != session.currentSchema {
197195
await coordinator.switchSchema(to: activeSchema)
198-
contextChanged = true
199196
}
200-
if isTableTab, !contextChanged {
197+
if isTableTab {
201198
coordinator.lazyLoadCurrentTabIfNeeded()
202199
}
203200
}

TablePro/Views/Main/MainContentCoordinator.swift

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -963,13 +963,13 @@ final class MainContentCoordinator {
963963
)
964964
switch decision {
965965
case .authorized:
966-
executeQueryInternal(sql)
966+
executeQueryInternal(sql, isAutoLoad: true)
967967
case .denied(let reason):
968968
tabManager.mutate(at: index) { $0.execution.errorMessage = reason }
969969
}
970970
}
971971
} else {
972-
executeQueryInternal(sql)
972+
executeQueryInternal(sql, isAutoLoad: true)
973973
}
974974
}
975975

@@ -1116,7 +1116,8 @@ final class MainContentCoordinator {
11161116
}
11171117

11181118
internal func executeQueryInternal(
1119-
_ sql: String
1119+
_ sql: String,
1120+
isAutoLoad: Bool = false
11201121
) {
11211122
guard let (selectedTab, index) = tabManager.selectedTabAndIndex,
11221123
!selectedTab.execution.isExecuting else { return }
@@ -1169,6 +1170,21 @@ final class MainContentCoordinator {
11691170
currentQueryTask = Task { [weak self] in
11701171
guard let self else { return }
11711172

1173+
if isAutoLoad {
1174+
do {
1175+
try await services.databaseManager.ensureConnected(conn)
1176+
} catch {
1177+
await MainActor.run { [weak self] in
1178+
guard let self else { return }
1179+
tabManager.mutate(tabId: tabId) { $0.execution.isExecuting = false }
1180+
currentQueryTask = nil
1181+
toolbarState.setExecuting(false)
1182+
needsLazyLoad = true
1183+
}
1184+
return
1185+
}
1186+
}
1187+
11721188
let schemaTask: Task<FetchedTableSchema, Error>?
11731189
if needsMetadataFetch, let tableName {
11741190
schemaTask = Task { try await QueryExecutor.fetchTableSchema(connectionId: connId, tableName: tableName) }
@@ -1262,6 +1278,10 @@ final class MainContentCoordinator {
12621278
toolbarState.setExecuting(false)
12631279
if error is CancellationError || Task.isCancelled { return }
12641280
guard capturedGeneration == queryGeneration else { return }
1281+
if isAutoLoad, services.databaseManager.driver(for: connectionId)?.status != .connected {
1282+
needsLazyLoad = true
1283+
return
1284+
}
12651285
handleQueryExecutionError(error, sql: sql, tabId: tabId, connection: conn)
12661286
}
12671287
}

TableProTests/Views/Main/MainContentCoordinatorLazyLoadTests.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,20 @@ struct MainContentCoordinatorLazyLoadTests {
165165
#expect(coordinator.needsLazyLoad == true)
166166
}
167167

168+
@Test("restoreSchemaAndRunQuery defers via needsLazyLoad instead of running a query when the driver is not ready")
169+
func restoreSchemaDefersWhenDriverNil() async {
170+
let (coordinator, tabManager) = makeCoordinator()
171+
let tabId = addTableTab(to: tabManager)
172+
coordinator.needsLazyLoad = false
173+
174+
await coordinator.restoreSchemaAndRunQuery("public")
175+
176+
#expect(coordinator.needsLazyLoad == true)
177+
if let idx = tabManager.tabs.firstIndex(where: { $0.id == tabId }) {
178+
#expect(tabManager.tabs[idx].execution.isExecuting == false)
179+
}
180+
}
181+
168182
// MARK: - Idempotency
169183

170184
@Test("Idempotent: repeated calls with the same loaded state are no-ops")

0 commit comments

Comments
 (0)