Skip to content

Commit 20efb75

Browse files
desperadoxhyxuhengyudatlechin
authored
fix(sidebar): refresh table tree after dropping a table (#1714)
* fix(sidebar): refresh table tree after dropping a table * refactor(sidebar): refresh dropped tables in place to avoid tree churn --------- Signed-off-by: Ngô Quốc Đạt <datlechin@gmail.com> Co-authored-by: xuhengyu <xuhengyu@aspirecn.com> Co-authored-by: Ngô Quốc Đạt <datlechin@gmail.com>
1 parent 73cc3df commit 20efb75

4 files changed

Lines changed: 114 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2727
- A new database group now appears in the connection list right away instead of only after restarting the app. (#1704)
2828
- The SQL formatter keeps nested indentation for UNION, UNION ALL, INTERSECT, and EXCEPT inside a derived table or CTE, and puts the closing parenthesis of a subquery on its own line instead of collapsing it onto the last SELECT. (#1698)
2929
- Toolbar button tooltips now show each action's real keyboard shortcut and follow your custom bindings, instead of a fixed value. The Switch Connection tooltip showed the wrong shortcut. (#1694)
30+
- Deleting a table from the sidebar now removes it from the tree right away on multi-database servers like MySQL and PostgreSQL. The tree kept showing the dropped table until you refreshed it by hand. (#1714)
3031
- 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.
3132
- 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.
3233
- 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.

TablePro/Core/Services/Query/DatabaseTreeMetadataService.swift

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,37 @@ final class DatabaseTreeMetadataService {
208208
_ = await (tables, routines)
209209
}
210210

211+
func refreshLoadedTables(connectionId: UUID) async {
212+
let keys = tablesState.keys.filter { $0.connectionId == connectionId }
213+
await withTaskGroup(of: Void.self) { group in
214+
for key in keys {
215+
group.addTask { @MainActor in
216+
await self.reloadTablesInPlace(key)
217+
}
218+
}
219+
}
220+
}
221+
222+
private func reloadTablesInPlace(_ key: ObjectsKey) async {
223+
guard isConnected(key.connectionId) else { return }
224+
await tablesDedup.cancel(key: key)
225+
do {
226+
let list = try await tablesDedup.execute(key: key) { [self] in
227+
try await withDriver(connectionId: key.connectionId, database: key.database) { driver in
228+
try await driver.fetchTables(schema: key.schema)
229+
}
230+
}
231+
let next: MetadataLoadState<[TableInfo]> = .loaded(list)
232+
guard tablesState[key] != next else { return }
233+
tablesState[key] = next
234+
} catch is CancellationError {
235+
} catch {
236+
Self.logger.warning(
237+
"tables refresh failed db=\(key.database, privacy: .public) schema=\(key.schema ?? "nil", privacy: .public) error=\(error.localizedDescription, privacy: .public)"
238+
)
239+
}
240+
}
241+
211242
// MARK: - Lifecycle
212243

213244
func handleReconnect(connectionId: UUID) async {
@@ -292,7 +323,7 @@ final class DatabaseTreeMetadataService {
292323
return ObjectsKey(connectionId: connectionId, database: database, schema: normalized)
293324
}
294325

295-
static func connectionObjectKeys(
326+
nonisolated static func connectionObjectKeys(
296327
tableKeys: some Sequence<ObjectsKey>,
297328
routineKeys: some Sequence<ObjectsKey>,
298329
connectionId: UUID

TablePro/Views/Main/MainContentCoordinator.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,7 @@ final class MainContentCoordinator {
615615
} catch {
616616
Self.logger.warning("Schema refresh failed: \(error.localizedDescription, privacy: .public)")
617617
}
618+
await DatabaseTreeMetadataService.shared.refreshLoadedTables(connectionId: connectionId)
618619
await reconcilePostSchemaLoad()
619620
}
620621

TableProTests/Core/Services/Query/DatabaseTreeMetadataServiceTests.swift

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22
@testable import TablePro
3+
import TableProPluginKit
34
import Testing
45

56
@Suite("DatabaseTreeMetadataService")
@@ -53,3 +54,82 @@ struct DatabaseTreeMetadataServiceTests {
5354
#expect(keys == [mine])
5455
}
5556
}
57+
58+
@Suite("DatabaseTreeMetadataService refreshLoadedTables")
59+
@MainActor
60+
struct DatabaseTreeMetadataServiceRefreshTests {
61+
@Test("reload drops previously loaded tables and refetches the current list")
62+
func refreshReloadsLoadedTables() async {
63+
let connection = TestFixtures.makeConnection()
64+
let driver = MockDatabaseDriver(connection: connection)
65+
driver.schemaTablesToReturn = ["public": [TestFixtures.makeTableInfo(name: "users")]]
66+
67+
var session = ConnectionSession(connection: connection, driver: driver)
68+
session.status = .connected
69+
DatabaseManager.shared.injectSession(session, for: connection.id)
70+
71+
let service = DatabaseTreeMetadataService.shared
72+
let database = connection.database
73+
74+
await service.loadTables(connectionId: connection.id, database: database, schema: "public")
75+
let initial = service.tables(connectionId: connection.id, database: database, schema: "public")
76+
#expect(initial.map(\.name) == ["users"])
77+
78+
driver.schemaTablesToReturn = ["public": []]
79+
await service.refreshLoadedTables(connectionId: connection.id)
80+
81+
let refreshed = service.tables(connectionId: connection.id, database: database, schema: "public")
82+
#expect(refreshed.isEmpty)
83+
84+
await service.handleDisconnect(connectionId: connection.id)
85+
DatabaseManager.shared.removeSession(for: connection.id)
86+
}
87+
88+
@Test("reload refetches every loaded schema, not just the one that changed")
89+
func refreshReloadsAllLoadedSchemas() async {
90+
let connection = TestFixtures.makeConnection()
91+
let driver = MockDatabaseDriver(connection: connection)
92+
driver.schemaTablesToReturn = [
93+
"public": [TestFixtures.makeTableInfo(name: "users")],
94+
"sales": [TestFixtures.makeTableInfo(name: "orders")]
95+
]
96+
97+
var session = ConnectionSession(connection: connection, driver: driver)
98+
session.status = .connected
99+
DatabaseManager.shared.injectSession(session, for: connection.id)
100+
101+
let service = DatabaseTreeMetadataService.shared
102+
let database = connection.database
103+
104+
await service.loadTables(connectionId: connection.id, database: database, schema: "public")
105+
await service.loadTables(connectionId: connection.id, database: database, schema: "sales")
106+
107+
driver.schemaTablesToReturn = [
108+
"public": [],
109+
"sales": [TestFixtures.makeTableInfo(name: "orders"), TestFixtures.makeTableInfo(name: "invoices")]
110+
]
111+
await service.refreshLoadedTables(connectionId: connection.id)
112+
113+
let publicTables = service.tables(connectionId: connection.id, database: database, schema: "public")
114+
let salesTables = service.tables(connectionId: connection.id, database: database, schema: "sales")
115+
#expect(publicTables.isEmpty)
116+
#expect(salesTables.map(\.name) == ["orders", "invoices"])
117+
118+
await service.handleDisconnect(connectionId: connection.id)
119+
DatabaseManager.shared.removeSession(for: connection.id)
120+
}
121+
122+
@Test("refresh is a no-op when no tables are loaded for the connection")
123+
func refreshWithoutLoadedTablesIsNoOp() async {
124+
let connection = TestFixtures.makeConnection()
125+
126+
await DatabaseTreeMetadataService.shared.refreshLoadedTables(connectionId: connection.id)
127+
128+
let tables = DatabaseTreeMetadataService.shared.tables(
129+
connectionId: connection.id,
130+
database: connection.database,
131+
schema: "public"
132+
)
133+
#expect(tables.isEmpty)
134+
}
135+
}

0 commit comments

Comments
 (0)