Skip to content

Commit a4497ab

Browse files
committed
fix: persist and restore PostgreSQL schema on reconnect, fix stuck error dialog (#540)
1 parent c34f2a4 commit a4497ab

15 files changed

+138
-743
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2626

2727
### Fixed
2828

29+
- PostgreSQL: Schema name lost after app restart, causing "relation does not exist" errors for non-public schemas
30+
- Error dialog OK button not dismissing when a SwiftUI sheet is active, making the app unusable
2931
- SQL Server: Unicode characters (Thai, CJK, etc.) in nvarchar/nchar/ntext columns displaying as question marks
3032
- Globe+F (fn+F) fullscreen shortcut not working in SwiftUI lifecycle app
3133

TablePro/Core/Database/DatabaseManager.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,19 @@ final class DatabaseManager {
192192
// Initialize schema for drivers that support schema switching
193193
if let schemaDriver = driver as? SchemaSwitchable {
194194
activeSessions[connection.id]?.currentSchema = schemaDriver.currentSchema
195+
196+
// Restore user's last schema if different from default
197+
if let savedSchema = AppSettingsStorage.shared.loadLastSchema(for: connection.id),
198+
savedSchema != schemaDriver.currentSchema {
199+
do {
200+
try await schemaDriver.switchSchema(to: savedSchema)
201+
activeSessions[connection.id]?.currentSchema = savedSchema
202+
} catch {
203+
Self.logger.warning(
204+
"Failed to restore saved schema '\(savedSchema, privacy: .public)' for \(connection.id): \(error.localizedDescription, privacy: .public)"
205+
)
206+
}
207+
}
195208
}
196209

197210
// Run post-connect actions declared by the plugin

TablePro/Core/Services/Infrastructure/SessionStateFactory.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ enum SessionStateFactory {
7171
if let index = tabMgr.selectedTabIndex {
7272
tabMgr.tabs[index].isView = payload.isView
7373
tabMgr.tabs[index].isEditable = !payload.isView
74+
tabMgr.tabs[index].schemaName = payload.schemaName
7475
if payload.showStructure {
7576
tabMgr.tabs[index].showStructure = true
7677
}

TablePro/Core/Services/Infrastructure/TabPersistenceCoordinator.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ internal final class TabPersistenceCoordinator {
148148
tableName: tab.tableName,
149149
isView: tab.isView,
150150
databaseName: tab.databaseName,
151+
schemaName: tab.schemaName,
151152
sourceFileURL: tab.sourceFileURL
152153
)
153154
}

TablePro/Core/Storage/AppSettingsStorage.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,20 @@ final class AppSettingsStorage {
160160
defaults.string(forKey: "com.TablePro.lastSelectedDatabase.\(connectionId)")
161161
}
162162

163+
// MARK: - Last Selected Schema (per connection)
164+
165+
func saveLastSchema(_ schema: String?, for connectionId: UUID) {
166+
if let schema {
167+
defaults.set(schema, forKey: "com.TablePro.lastSelectedSchema.\(connectionId)")
168+
} else {
169+
defaults.removeObject(forKey: "com.TablePro.lastSelectedSchema.\(connectionId)")
170+
}
171+
}
172+
173+
func loadLastSchema(for connectionId: UUID) -> String? {
174+
defaults.string(forKey: "com.TablePro.lastSelectedSchema.\(connectionId)")
175+
}
176+
163177
// MARK: - Onboarding
164178

165179
/// Check if user has completed onboarding

TablePro/Models/Query/EditorTabPayload.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ internal struct EditorTabPayload: Codable, Hashable {
2121
internal let tableName: String?
2222
/// Database context (for multi-database connections)
2323
internal let databaseName: String?
24+
/// Schema context (for multi-schema connections, e.g. PostgreSQL)
25+
internal let schemaName: String?
2426
/// Initial SQL query (for .query tabs opened from files)
2527
internal let initialQuery: String?
2628
/// Whether this tab displays a database view (read-only)
@@ -44,6 +46,7 @@ internal struct EditorTabPayload: Codable, Hashable {
4446
tabType: TabType = .query,
4547
tableName: String? = nil,
4648
databaseName: String? = nil,
49+
schemaName: String? = nil,
4750
initialQuery: String? = nil,
4851
isView: Bool = false,
4952
showStructure: Bool = false,
@@ -58,6 +61,7 @@ internal struct EditorTabPayload: Codable, Hashable {
5861
self.tabType = tabType
5962
self.tableName = tableName
6063
self.databaseName = databaseName
64+
self.schemaName = schemaName
6165
self.initialQuery = initialQuery
6266
self.isView = isView
6367
self.showStructure = showStructure
@@ -75,6 +79,7 @@ internal struct EditorTabPayload: Codable, Hashable {
7579
tabType = try container.decode(TabType.self, forKey: .tabType)
7680
tableName = try container.decodeIfPresent(String.self, forKey: .tableName)
7781
databaseName = try container.decodeIfPresent(String.self, forKey: .databaseName)
82+
schemaName = try container.decodeIfPresent(String.self, forKey: .schemaName)
7883
initialQuery = try container.decodeIfPresent(String.self, forKey: .initialQuery)
7984
isView = try container.decodeIfPresent(Bool.self, forKey: .isView) ?? false
8085
showStructure = try container.decodeIfPresent(Bool.self, forKey: .showStructure) ?? false
@@ -99,6 +104,7 @@ internal struct EditorTabPayload: Codable, Hashable {
99104
self.tabType = tab.tabType
100105
self.tableName = tab.tableName
101106
self.databaseName = tab.databaseName
107+
self.schemaName = tab.schemaName
102108
self.initialQuery = tab.query
103109
self.isView = tab.isView
104110
self.showStructure = tab.showStructure

TablePro/Models/Query/QueryTab.swift

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ struct PersistedTab: Codable {
2626
let tableName: String?
2727
var isView: Bool = false
2828
var databaseName: String = ""
29+
var schemaName: String?
2930
var sourceFileURL: URL?
3031
}
3132

@@ -340,6 +341,7 @@ struct QueryTab: Identifiable, Equatable {
340341
var isEditable: Bool
341342
var isView: Bool // True for database views (read-only)
342343
var databaseName: String // Database this tab was opened in (for multi-database restore)
344+
var schemaName: String? // Schema this tab was opened in (for multi-schema restore, e.g. PostgreSQL)
343345
var showStructure: Bool // Toggle to show structure view instead of data
344346
var explainText: String?
345347
var explainExecutionTime: TimeInterval?
@@ -426,6 +428,7 @@ struct QueryTab: Identifiable, Equatable {
426428
self.isEditable = tabType == .table
427429
self.isView = false
428430
self.databaseName = ""
431+
self.schemaName = nil
429432
self.showStructure = false
430433
self.pendingChanges = TabPendingChanges()
431434
self.selectedRowIndices = []
@@ -460,6 +463,7 @@ struct QueryTab: Identifiable, Equatable {
460463
self.isEditable = persisted.tabType == .table && !persisted.isView
461464
self.isView = persisted.isView
462465
self.databaseName = persisted.databaseName
466+
self.schemaName = persisted.schemaName
463467
self.showStructure = false
464468
self.pendingChanges = TabPendingChanges()
465469
self.selectedRowIndices = []
@@ -479,6 +483,7 @@ struct QueryTab: Identifiable, Equatable {
479483
@MainActor static func buildBaseTableQuery(
480484
tableName: String,
481485
databaseType: DatabaseType,
486+
schemaName: String? = nil,
482487
quoteIdentifier: ((String) -> String)? = nil
483488
) -> String {
484489
let quote = quoteIdentifier ?? quoteIdentifierFromDialect(PluginManager.shared.sqlDialect(for: databaseType))
@@ -499,13 +504,18 @@ struct QueryTab: Identifiable, Equatable {
499504
case .bash:
500505
return "SCAN 0 MATCH * COUNT \(pageSize)"
501506
default:
502-
let quotedName = quote(tableName)
507+
let qualifiedName: String
508+
if let schema = schemaName, !schema.isEmpty {
509+
qualifiedName = "\(quote(schema)).\(quote(tableName))"
510+
} else {
511+
qualifiedName = quote(tableName)
512+
}
503513
switch PluginManager.shared.paginationStyle(for: databaseType) {
504514
case .offsetFetch:
505515
let orderBy = PluginManager.shared.offsetFetchOrderBy(for: databaseType)
506-
return "SELECT * FROM \(quotedName) \(orderBy) OFFSET 0 ROWS FETCH NEXT \(pageSize) ROWS ONLY;"
516+
return "SELECT * FROM \(qualifiedName) \(orderBy) OFFSET 0 ROWS FETCH NEXT \(pageSize) ROWS ONLY;"
507517
case .limit:
508-
return "SELECT * FROM \(quotedName) LIMIT \(pageSize);"
518+
return "SELECT * FROM \(qualifiedName) LIMIT \(pageSize);"
509519
}
510520
}
511521
}
@@ -532,6 +542,7 @@ struct QueryTab: Identifiable, Equatable {
532542
tableName: tableName,
533543
isView: isView,
534544
databaseName: databaseName,
545+
schemaName: schemaName,
535546
sourceFileURL: sourceFileURL
536547
)
537548
}
@@ -695,7 +706,7 @@ final class QueryTabManager {
695706
func replaceTabContent(
696707
tableName: String, databaseType: DatabaseType = .mysql,
697708
isView: Bool = false, databaseName: String = "",
698-
isPreview: Bool = false,
709+
schemaName: String? = nil, isPreview: Bool = false,
699710
quoteIdentifier: ((String) -> String)? = nil
700711
) -> Bool {
701712
guard let selectedId = selectedTabId,
@@ -707,6 +718,7 @@ final class QueryTabManager {
707718
let query = QueryTab.buildBaseTableQuery(
708719
tableName: tableName,
709720
databaseType: databaseType,
721+
schemaName: schemaName,
710722
quoteIdentifier: quoteIdentifier
711723
)
712724
let pageSize = AppSettingsManager.shared.dataGrid.defaultPageSize
@@ -733,6 +745,7 @@ final class QueryTabManager {
733745
tab.columnLayout = ColumnLayoutState()
734746
tab.pagination = PaginationState(pageSize: pageSize)
735747
tab.databaseName = databaseName
748+
tab.schemaName = schemaName
736749
tab.isPreview = isPreview
737750
tabs[selectedIndex] = tab
738751
return true

0 commit comments

Comments
 (0)