Skip to content

Commit 837cf3b

Browse files
committed
fix: sidebar not refreshing after creating or dropping tables
1 parent e7b6647 commit 837cf3b

9 files changed

Lines changed: 110 additions & 26 deletions

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
- SSH port field accepting invalid values
2828
- DROP TABLE and TRUNCATE TABLE sidebar operations producing no SQL for plugin-based drivers
2929
- Foreign key navigation arrows not appearing after switching databases with Cmd+K on MySQL
30+
- Sidebar not refreshing after creating or dropping tables
3031

3132
## [0.19.1] - 2026-03-16
3233

TablePro.xcodeproj/project.pbxproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1758,7 +1758,7 @@
17581758
CODE_SIGN_STYLE = Automatic;
17591759
CURRENT_PROJECT_VERSION = 35;
17601760
DEAD_CODE_STRIPPING = YES;
1761-
DEVELOPMENT_TEAM = "";
1761+
DEVELOPMENT_TEAM = D7HJ5TFYCU;
17621762
ENABLE_APP_SANDBOX = NO;
17631763
ENABLE_HARDENED_RUNTIME = YES;
17641764
ENABLE_PREVIEWS = YES;
@@ -1831,7 +1831,7 @@
18311831
CURRENT_PROJECT_VERSION = 35;
18321832
DEAD_CODE_STRIPPING = YES;
18331833
DEPLOYMENT_POSTPROCESSING = YES;
1834-
DEVELOPMENT_TEAM = "";
1834+
DEVELOPMENT_TEAM = D7HJ5TFYCU;
18351835
ENABLE_APP_SANDBOX = NO;
18361836
ENABLE_HARDENED_RUNTIME = YES;
18371837
ENABLE_PREVIEWS = YES;
@@ -1867,7 +1867,7 @@
18671867
);
18681868
PRODUCT_BUNDLE_IDENTIFIER = com.TablePro;
18691869
PRODUCT_NAME = "$(TARGET_NAME)";
1870-
PROVISIONING_PROFILE_SPECIFIER = "TablePro Developer ID";
1870+
PROVISIONING_PROFILE_SPECIFIER = "";
18711871
REGISTER_APP_GROUPS = YES;
18721872
RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO;
18731873
RUNTIME_EXCEPTION_ALLOW_JIT = NO;

TablePro/Core/Autocomplete/SQLSchemaProvider.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,21 @@ actor SQLSchemaProvider {
101101
cachedDriver = nil
102102
}
103103

104+
func invalidateTables() {
105+
tables.removeAll()
106+
}
107+
108+
func updateTables(_ newTables: [TableInfo]) {
109+
tables = newTables
110+
}
111+
112+
func fetchFreshTables() async throws -> [TableInfo] {
113+
guard let driver = cachedDriver else { return [] }
114+
let fresh = try await driver.fetchTables()
115+
tables = fresh
116+
return fresh
117+
}
118+
104119
/// Find table name from alias
105120
func resolveAlias(_ aliasOrName: String, in references: [TableReference]) -> String? {
106121
// First check if it's an alias

TablePro/ViewModels/SidebarViewModel.swift

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import SwiftUI
1313

1414
/// Abstraction over table fetching for testability
1515
protocol TableFetcher: Sendable {
16-
func fetchTables() async throws -> [TableInfo]
16+
func fetchTables(force: Bool) async throws -> [TableInfo]
1717
}
1818

1919
/// Production implementation that uses DatabaseManager, with optional schema provider cache
@@ -26,17 +26,24 @@ struct LiveTableFetcher: TableFetcher {
2626
self.schemaProvider = schemaProvider
2727
}
2828

29-
func fetchTables() async throws -> [TableInfo] {
29+
func fetchTables(force: Bool) async throws -> [TableInfo] {
3030
if let provider = schemaProvider {
31-
let cached = await provider.getTables()
32-
if !cached.isEmpty {
33-
return cached
31+
if force {
32+
let fresh = try await provider.fetchFreshTables()
33+
if !fresh.isEmpty { return fresh }
34+
} else {
35+
let cached = await provider.getTables()
36+
if !cached.isEmpty { return cached }
3437
}
3538
}
3639
guard let driver = await DatabaseManager.shared.driver(for: connectionId) else {
3740
return []
3841
}
39-
return try await driver.fetchTables()
42+
let fetched = try await driver.fetchTables()
43+
if let provider = schemaProvider {
44+
await provider.updateTables(fetched)
45+
}
46+
return fetched
4047
}
4148
}
4249

@@ -145,27 +152,27 @@ final class SidebarViewModel {
145152

146153
// MARK: - Table Loading
147154

148-
func loadTables() {
155+
func loadTables(force: Bool = false) {
149156
guard !isLoading else { return }
150157
isLoading = true
151158
errorMessage = nil
152159
loadTask = Task {
153-
await loadTablesAsync()
160+
await loadTablesAsync(force: force)
154161
}
155162
}
156163

157164
func forceLoadTables() {
158165
loadTask?.cancel()
159166
loadTask = nil
160167
isLoading = false
161-
loadTables()
168+
loadTables(force: true)
162169
}
163170

164-
private func loadTablesAsync() async {
171+
private func loadTablesAsync(force: Bool = false) async {
165172
let previousSelectedName: String? = tables.isEmpty ? nil : selectedTables.first?.name
166173

167174
do {
168-
let fetchedTables = try await tableFetcher.fetchTables()
175+
let fetchedTables = try await tableFetcher.fetchTables(force: force)
169176
tables = fetchedTables
170177

171178
// Clean up stale entries for tables that no longer exist

TablePro/Views/Main/MainContentCoordinator.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,10 @@ final class MainContentCoordinator {
313313
}
314314

315315
func reloadSidebar() {
316-
sidebarViewModel?.forceLoadTables()
316+
Task {
317+
await schemaProvider.invalidateTables()
318+
sidebarViewModel?.forceLoadTables()
319+
}
317320
}
318321

319322
/// Explicit cleanup called from `onDisappear`. Releases schema provider

TableProTests/ViewModels/LiveTableFetcherTests.swift

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ struct LiveTableFetcherTests {
9090
#expect(initialCallCount == 1)
9191

9292
let fetcher = LiveTableFetcher(connectionId: UUID(), schemaProvider: provider)
93-
let result = try await fetcher.fetchTables()
93+
let result = try await fetcher.fetchTables(force: false)
9494

9595
#expect(result.count == 3)
9696
#expect(result.map(\.name) == ["users", "orders", "products"])
@@ -102,15 +102,15 @@ struct LiveTableFetcherTests {
102102
let provider = SQLSchemaProvider()
103103

104104
let fetcher = LiveTableFetcher(connectionId: UUID(), schemaProvider: provider)
105-
let result = try await fetcher.fetchTables()
105+
let result = try await fetcher.fetchTables(force: false)
106106

107107
#expect(result.isEmpty)
108108
}
109109

110110
@Test("works without schema provider using direct driver fetch")
111111
func worksWithoutSchemaProvider() async throws {
112112
let fetcher = LiveTableFetcher(connectionId: UUID())
113-
let result = try await fetcher.fetchTables()
113+
let result = try await fetcher.fetchTables(force: false)
114114

115115
#expect(result.isEmpty)
116116
}
@@ -131,11 +131,65 @@ struct LiveTableFetcherTests {
131131
let fetcher = LiveTableFetcher(connectionId: UUID(), schemaProvider: provider)
132132

133133
for _ in 0..<3 {
134-
let result = try await fetcher.fetchTables()
134+
let result = try await fetcher.fetchTables(force: false)
135135
#expect(result.count == 2)
136136
#expect(result.map(\.name) == ["accounts", "transactions"])
137137
}
138138

139139
#expect(mockDriver.fetchTablesCallCount == 1)
140140
}
141+
142+
@Test("force: true bypasses schema provider cache and hits driver")
143+
func forceBypassesCache() async throws {
144+
let initialTables = [
145+
TestFixtures.makeTableInfo(name: "users"),
146+
TestFixtures.makeTableInfo(name: "orders")
147+
]
148+
149+
let mockDriver = MockDatabaseDriver()
150+
mockDriver.tablesToReturn = initialTables
151+
152+
let provider = SQLSchemaProvider()
153+
await provider.loadSchema(using: mockDriver)
154+
155+
let freshTables = [
156+
TestFixtures.makeTableInfo(name: "users"),
157+
TestFixtures.makeTableInfo(name: "orders"),
158+
TestFixtures.makeTableInfo(name: "new_table")
159+
]
160+
mockDriver.tablesToReturn = freshTables
161+
162+
let callCountBefore = mockDriver.fetchTablesCallCount
163+
164+
let fetcher = LiveTableFetcher(connectionId: UUID(), schemaProvider: provider)
165+
let result = try await fetcher.fetchTables(force: true)
166+
167+
#expect(result.count == 3)
168+
#expect(result.map(\.name) == ["users", "orders", "new_table"])
169+
#expect(mockDriver.fetchTablesCallCount == callCountBefore + 1)
170+
}
171+
172+
@Test("force: true writes fresh tables back into schema provider")
173+
func forcedFetchUpdatesSchemaProvider() async throws {
174+
let initialTables = [TestFixtures.makeTableInfo(name: "old_table")]
175+
176+
let mockDriver = MockDatabaseDriver()
177+
mockDriver.tablesToReturn = initialTables
178+
179+
let provider = SQLSchemaProvider()
180+
await provider.loadSchema(using: mockDriver)
181+
182+
await provider.invalidateTables()
183+
let freshTables = [
184+
TestFixtures.makeTableInfo(name: "alpha"),
185+
TestFixtures.makeTableInfo(name: "beta")
186+
]
187+
mockDriver.tablesToReturn = freshTables
188+
189+
let fetcher = LiveTableFetcher(connectionId: UUID(), schemaProvider: provider)
190+
_ = try await fetcher.fetchTables(force: true)
191+
192+
let cached = await provider.getTables()
193+
#expect(cached.map(\.name).sorted() == ["alpha", "beta"])
194+
}
141195
}

TableProTests/ViewModels/SidebarViewModelTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ private struct MockTableFetcher: TableFetcher {
1616
var tables: [TableInfo]
1717
var error: Error?
1818

19-
func fetchTables() async throws -> [TableInfo] {
19+
func fetchTables(force: Bool) async throws -> [TableInfo] {
2020
if let error { throw error }
2121
return tables
2222
}

TableProTests/Views/Main/CoordinatorReloadSidebarTests.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@ import Testing
1818
private final class FetchTrackingTableFetcher: TableFetcher, @unchecked Sendable {
1919
private let lock = NSLock()
2020
private var _fetchCount = 0
21+
private var _forceCount = 0
2122

22-
var fetchCount: Int {
23-
lock.withLock { _fetchCount }
24-
}
23+
var fetchCount: Int { lock.withLock { _fetchCount } }
24+
var forceCount: Int { lock.withLock { _forceCount } }
2525

26-
func fetchTables() async throws -> [TableInfo] {
27-
lock.withLock { _fetchCount += 1 }
26+
func fetchTables(force: Bool) async throws -> [TableInfo] {
27+
lock.withLock {
28+
_fetchCount += 1
29+
if force { _forceCount += 1 }
30+
}
2831
return []
2932
}
3033
}
@@ -77,6 +80,7 @@ struct CoordinatorReloadSidebarTests {
7780
try? await Task.sleep(nanoseconds: 100_000_000)
7881

7982
#expect(mockFetcher.fetchCount > 0)
83+
#expect(mockFetcher.forceCount > 0)
8084
}
8185

8286
@Test("reloadSidebar is safe when sidebarViewModel is nil")

TableProTests/Views/SwitchDatabaseTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import Testing
1818
private struct MockTableFetcher: TableFetcher {
1919
var tables: [TableInfo]
2020

21-
func fetchTables() async throws -> [TableInfo] {
21+
func fetchTables(force: Bool) async throws -> [TableInfo] {
2222
tables
2323
}
2424
}

0 commit comments

Comments
 (0)