diff --git a/CHANGELOG.md b/CHANGELOG.md index f8169f9d..76173022 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- PostgreSQL: Schema name lost after app restart, causing "relation does not exist" errors for non-public schemas +- Error dialog OK button not dismissing when a SwiftUI sheet is active, making the app unusable - SQL Server: Unicode characters (Thai, CJK, etc.) in nvarchar/nchar/ntext columns displaying as question marks - Globe+F (fn+F) fullscreen shortcut not working in SwiftUI lifecycle app diff --git a/TablePro/Core/Database/DatabaseManager.swift b/TablePro/Core/Database/DatabaseManager.swift index 9927cb44..d935f249 100644 --- a/TablePro/Core/Database/DatabaseManager.swift +++ b/TablePro/Core/Database/DatabaseManager.swift @@ -192,6 +192,19 @@ final class DatabaseManager { // Initialize schema for drivers that support schema switching if let schemaDriver = driver as? SchemaSwitchable { activeSessions[connection.id]?.currentSchema = schemaDriver.currentSchema + + // Restore user's last schema if different from default + if let savedSchema = AppSettingsStorage.shared.loadLastSchema(for: connection.id), + savedSchema != schemaDriver.currentSchema { + do { + try await schemaDriver.switchSchema(to: savedSchema) + activeSessions[connection.id]?.currentSchema = savedSchema + } catch { + Self.logger.warning( + "Failed to restore saved schema '\(savedSchema, privacy: .public)' for \(connection.id): \(error.localizedDescription, privacy: .public)" + ) + } + } } // Run post-connect actions declared by the plugin diff --git a/TablePro/Core/Services/Infrastructure/SessionStateFactory.swift b/TablePro/Core/Services/Infrastructure/SessionStateFactory.swift index ba76be53..053dda09 100644 --- a/TablePro/Core/Services/Infrastructure/SessionStateFactory.swift +++ b/TablePro/Core/Services/Infrastructure/SessionStateFactory.swift @@ -71,6 +71,7 @@ enum SessionStateFactory { if let index = tabMgr.selectedTabIndex { tabMgr.tabs[index].isView = payload.isView tabMgr.tabs[index].isEditable = !payload.isView + tabMgr.tabs[index].schemaName = payload.schemaName if payload.showStructure { tabMgr.tabs[index].showStructure = true } diff --git a/TablePro/Core/Services/Infrastructure/TabPersistenceCoordinator.swift b/TablePro/Core/Services/Infrastructure/TabPersistenceCoordinator.swift index 185ce705..f2266d48 100644 --- a/TablePro/Core/Services/Infrastructure/TabPersistenceCoordinator.swift +++ b/TablePro/Core/Services/Infrastructure/TabPersistenceCoordinator.swift @@ -148,6 +148,7 @@ internal final class TabPersistenceCoordinator { tableName: tab.tableName, isView: tab.isView, databaseName: tab.databaseName, + schemaName: tab.schemaName, sourceFileURL: tab.sourceFileURL ) } diff --git a/TablePro/Core/Storage/AppSettingsStorage.swift b/TablePro/Core/Storage/AppSettingsStorage.swift index bbbd6918..e2e5ffec 100644 --- a/TablePro/Core/Storage/AppSettingsStorage.swift +++ b/TablePro/Core/Storage/AppSettingsStorage.swift @@ -160,6 +160,20 @@ final class AppSettingsStorage { defaults.string(forKey: "com.TablePro.lastSelectedDatabase.\(connectionId)") } + // MARK: - Last Selected Schema (per connection) + + func saveLastSchema(_ schema: String?, for connectionId: UUID) { + if let schema { + defaults.set(schema, forKey: "com.TablePro.lastSelectedSchema.\(connectionId)") + } else { + defaults.removeObject(forKey: "com.TablePro.lastSelectedSchema.\(connectionId)") + } + } + + func loadLastSchema(for connectionId: UUID) -> String? { + defaults.string(forKey: "com.TablePro.lastSelectedSchema.\(connectionId)") + } + // MARK: - Onboarding /// Check if user has completed onboarding diff --git a/TablePro/Models/Query/EditorTabPayload.swift b/TablePro/Models/Query/EditorTabPayload.swift index b9e2f8b2..fdd7888d 100644 --- a/TablePro/Models/Query/EditorTabPayload.swift +++ b/TablePro/Models/Query/EditorTabPayload.swift @@ -21,6 +21,8 @@ internal struct EditorTabPayload: Codable, Hashable { internal let tableName: String? /// Database context (for multi-database connections) internal let databaseName: String? + /// Schema context (for multi-schema connections, e.g. PostgreSQL) + internal let schemaName: String? /// Initial SQL query (for .query tabs opened from files) internal let initialQuery: String? /// Whether this tab displays a database view (read-only) @@ -44,6 +46,7 @@ internal struct EditorTabPayload: Codable, Hashable { tabType: TabType = .query, tableName: String? = nil, databaseName: String? = nil, + schemaName: String? = nil, initialQuery: String? = nil, isView: Bool = false, showStructure: Bool = false, @@ -58,6 +61,7 @@ internal struct EditorTabPayload: Codable, Hashable { self.tabType = tabType self.tableName = tableName self.databaseName = databaseName + self.schemaName = schemaName self.initialQuery = initialQuery self.isView = isView self.showStructure = showStructure @@ -75,6 +79,7 @@ internal struct EditorTabPayload: Codable, Hashable { tabType = try container.decode(TabType.self, forKey: .tabType) tableName = try container.decodeIfPresent(String.self, forKey: .tableName) databaseName = try container.decodeIfPresent(String.self, forKey: .databaseName) + schemaName = try container.decodeIfPresent(String.self, forKey: .schemaName) initialQuery = try container.decodeIfPresent(String.self, forKey: .initialQuery) isView = try container.decodeIfPresent(Bool.self, forKey: .isView) ?? false showStructure = try container.decodeIfPresent(Bool.self, forKey: .showStructure) ?? false @@ -99,6 +104,7 @@ internal struct EditorTabPayload: Codable, Hashable { self.tabType = tab.tabType self.tableName = tab.tableName self.databaseName = tab.databaseName + self.schemaName = tab.schemaName self.initialQuery = tab.query self.isView = tab.isView self.showStructure = tab.showStructure diff --git a/TablePro/Models/Query/QueryTab.swift b/TablePro/Models/Query/QueryTab.swift index 65b6f4fc..8b866a92 100644 --- a/TablePro/Models/Query/QueryTab.swift +++ b/TablePro/Models/Query/QueryTab.swift @@ -26,6 +26,7 @@ struct PersistedTab: Codable { let tableName: String? var isView: Bool = false var databaseName: String = "" + var schemaName: String? var sourceFileURL: URL? } @@ -340,6 +341,7 @@ struct QueryTab: Identifiable, Equatable { var isEditable: Bool var isView: Bool // True for database views (read-only) var databaseName: String // Database this tab was opened in (for multi-database restore) + var schemaName: String? // Schema this tab was opened in (for multi-schema restore, e.g. PostgreSQL) var showStructure: Bool // Toggle to show structure view instead of data var explainText: String? var explainExecutionTime: TimeInterval? @@ -426,6 +428,7 @@ struct QueryTab: Identifiable, Equatable { self.isEditable = tabType == .table self.isView = false self.databaseName = "" + self.schemaName = nil self.showStructure = false self.pendingChanges = TabPendingChanges() self.selectedRowIndices = [] @@ -460,6 +463,7 @@ struct QueryTab: Identifiable, Equatable { self.isEditable = persisted.tabType == .table && !persisted.isView self.isView = persisted.isView self.databaseName = persisted.databaseName + self.schemaName = persisted.schemaName self.showStructure = false self.pendingChanges = TabPendingChanges() self.selectedRowIndices = [] @@ -479,6 +483,7 @@ struct QueryTab: Identifiable, Equatable { @MainActor static func buildBaseTableQuery( tableName: String, databaseType: DatabaseType, + schemaName: String? = nil, quoteIdentifier: ((String) -> String)? = nil ) -> String { let quote = quoteIdentifier ?? quoteIdentifierFromDialect(PluginManager.shared.sqlDialect(for: databaseType)) @@ -499,13 +504,18 @@ struct QueryTab: Identifiable, Equatable { case .bash: return "SCAN 0 MATCH * COUNT \(pageSize)" default: - let quotedName = quote(tableName) + let qualifiedName: String + if let schema = schemaName, !schema.isEmpty { + qualifiedName = "\(quote(schema)).\(quote(tableName))" + } else { + qualifiedName = quote(tableName) + } switch PluginManager.shared.paginationStyle(for: databaseType) { case .offsetFetch: let orderBy = PluginManager.shared.offsetFetchOrderBy(for: databaseType) - return "SELECT * FROM \(quotedName) \(orderBy) OFFSET 0 ROWS FETCH NEXT \(pageSize) ROWS ONLY;" + return "SELECT * FROM \(qualifiedName) \(orderBy) OFFSET 0 ROWS FETCH NEXT \(pageSize) ROWS ONLY;" case .limit: - return "SELECT * FROM \(quotedName) LIMIT \(pageSize);" + return "SELECT * FROM \(qualifiedName) LIMIT \(pageSize);" } } } @@ -532,6 +542,7 @@ struct QueryTab: Identifiable, Equatable { tableName: tableName, isView: isView, databaseName: databaseName, + schemaName: schemaName, sourceFileURL: sourceFileURL ) } @@ -695,7 +706,7 @@ final class QueryTabManager { func replaceTabContent( tableName: String, databaseType: DatabaseType = .mysql, isView: Bool = false, databaseName: String = "", - isPreview: Bool = false, + schemaName: String? = nil, isPreview: Bool = false, quoteIdentifier: ((String) -> String)? = nil ) -> Bool { guard let selectedId = selectedTabId, @@ -707,6 +718,7 @@ final class QueryTabManager { let query = QueryTab.buildBaseTableQuery( tableName: tableName, databaseType: databaseType, + schemaName: schemaName, quoteIdentifier: quoteIdentifier ) let pageSize = AppSettingsManager.shared.dataGrid.defaultPageSize @@ -733,6 +745,7 @@ final class QueryTabManager { tab.columnLayout = ColumnLayoutState() tab.pagination = PaginationState(pageSize: pageSize) tab.databaseName = databaseName + tab.schemaName = schemaName tab.isPreview = isPreview tabs[selectedIndex] = tab return true diff --git a/TablePro/Resources/Localizable.xcstrings b/TablePro/Resources/Localizable.xcstrings index 6f95b159..715aa208 100644 --- a/TablePro/Resources/Localizable.xcstrings +++ b/TablePro/Resources/Localizable.xcstrings @@ -251,7 +251,6 @@ } }, "(%lld active)" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -1279,9 +1278,6 @@ } } } - }, - "%lld row(s) affected" : { - }, "%lld row%@ affected" : { "localizations" : { @@ -2909,9 +2905,6 @@ } } } - }, - "Add at least one column with a name and type" : { - }, "Add Check Constraint" : { "localizations" : { @@ -2979,31 +2972,9 @@ } } } - }, - "Add columns to see the CREATE TABLE statement" : { - }, "Add Connection" : { - "localizations" : { - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bağlantı Ekle" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Thêm kết nối" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "添加连接" - } - } - } + }, "Add filter" : { "localizations" : { @@ -3026,12 +2997,8 @@ } } } - }, - "Add Filter" : { - }, "Add Filter (Cmd+Shift+F)" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -3052,9 +3019,6 @@ } } } - }, - "Add filter row" : { - }, "Add Folder..." : { "localizations" : { @@ -3386,9 +3350,6 @@ } } } - }, - "AI-Powered Assistant" : { - }, "AI-powered SQL completions appear as ghost text while typing. Press Tab to accept, Escape to dismiss." : { "localizations" : { @@ -3817,24 +3778,6 @@ "state" : "new", "value" : "An external link wants to add a database connection:\n\nName: %1$@\n%2$@" } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Harici bir bağlantı veritabanı bağlantısı eklemek istiyor:\n\nAd: %@\n%@" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Một liên kết bên ngoài muốn thêm kết nối cơ sở dữ liệu:\n\nTên: %@\n%@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "外部链接请求添加数据库连接:\n\n名称:%@\n%@" - } } } }, @@ -3845,24 +3788,6 @@ "state" : "new", "value" : "An external link wants to open a query on connection \"%1$@\":\n\n%2$@" } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Harici bir bağlantı “%@” bağlantısında sorgu açmak istiyor:\n\n%@" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Một liên kết bên ngoài muốn mở truy vấn trên kết nối “%@”:\n\n%@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "外部链接请求在连接“%@”上打开查询:\n\n%@" - } } } }, @@ -3997,9 +3922,6 @@ } } } - }, - "API token" : { - }, "API Token" : { "localizations" : { @@ -4028,9 +3950,6 @@ } } } - }, - "API Token Required" : { - }, "Appearance" : { "localizations" : { @@ -4076,12 +3995,8 @@ } } } - }, - "Apply" : { - }, "Apply All" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -4124,12 +4039,8 @@ } } } - }, - "Apply filters" : { - }, "Apply this filter" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -4152,7 +4063,6 @@ } }, "Apply This Filter" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -4416,12 +4326,6 @@ } } } - }, - "Ask for API token on every connection" : { - - }, - "Ask for password on every connection" : { - }, "Auth" : { "localizations" : { @@ -5168,9 +5072,6 @@ } } } - }, - "Browse, edit, and manage your data with ease" : { - }, "Browse..." : { "localizations" : { @@ -5682,9 +5583,6 @@ } } } - }, - "Charset:" : { - }, "Chat" : { "localizations" : { @@ -6025,7 +5923,6 @@ } }, "Clear search" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -6048,7 +5945,6 @@ } }, "Clear Search" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -6382,15 +6278,6 @@ } } } - }, - "Close Others" : { - - }, - "Close preview" : { - - }, - "Close Result Tab" : { - }, "Close Tab" : { "localizations" : { @@ -6481,9 +6368,6 @@ } } } - }, - "Collation:" : { - }, "Color" : { "localizations" : { @@ -6552,7 +6436,6 @@ } }, "Column" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -6672,70 +6555,13 @@ } }, "Column Reorder Failed" : { - "localizations" : { - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sütun Yeniden Sıralama Başarısız" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sắp xếp lại cột thất bại" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "列重排失败" - } - } - } + }, "Column reorder failed: %@" : { - "localizations" : { - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sütun yeniden sıralama başarısız: %@" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sắp xếp lại cột thất bại: %@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "列重排失败:%@" - } - } - } + }, "Column reorder is not supported for this database type" : { - "localizations" : { - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bu veritabanı türü için sütun yeniden sıralama desteklenmiyor" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Loại cơ sở dữ liệu này không hỗ trợ sắp xếp lại cột" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "此数据库类型不支持列重排" - } - } - } + }, "Column: %@" : { "localizations" : { @@ -6825,9 +6651,6 @@ } } } - }, - "Comma-separated values. Compatible with Excel and most tools." : { - }, "Command Preview" : { "extractionState" : "stale", @@ -7068,9 +6891,6 @@ } } } - }, - "Connect to popular databases with full feature support" : { - }, "Connect to the internet to verify your license." : { "localizations" : { @@ -7195,24 +7015,6 @@ "state" : "new", "value" : "Connection \"%1$@\" has a script that will run before connecting:\n\n%2$@" } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bağlantı “%@” bağlanmadan önce çalışacak bir komut dosyasına sahip:\n\n%@" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kết nối “%@” có một script sẽ chạy trước khi kết nối:\n\n%@" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "连接“%@”有一个将在连接前运行的脚本:\n\n%@" - } } } }, @@ -7728,9 +7530,6 @@ } } } - }, - "Copied to clipboard" : { - }, "Copied!" : { "localizations" : { @@ -7929,9 +7728,6 @@ } } } - }, - "Copy error message" : { - }, "Copy Name" : { "localizations" : { @@ -8401,9 +8197,6 @@ } } } - }, - "Create New Table..." : { - }, "Create New Tag" : { "localizations" : { @@ -8470,12 +8263,6 @@ } } } - }, - "Create Table" : { - - }, - "Create Table Failed" : { - }, "Created" : { "localizations" : { @@ -8545,7 +8332,6 @@ } }, "CURDATE()" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -8702,7 +8488,6 @@ } }, "CURRENT_TIMESTAMP()" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -8775,7 +8560,6 @@ } }, "CURTIME()" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -10205,9 +9989,6 @@ } } } - }, - "Delete Selected" : { - }, "Delete SSH Profile?" : { "localizations" : { @@ -10880,7 +10661,6 @@ } }, "Duplicate filter" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -11396,28 +11176,6 @@ } } }, - "Enable AI Features" : { - "localizations" : { - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "AI Özelliklerini Etkinleştir" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bật tính năng AI" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "启用 AI 功能" - } - } - } - }, "Enable inline suggestions" : { "localizations" : { "tr" : { @@ -11638,9 +11396,6 @@ } } } - }, - "Engine:" : { - }, "Enter a name for this filter preset" : { "localizations" : { @@ -11708,19 +11463,6 @@ } } }, - "Enter table name" : { - - }, - "Enter the %@ for \"%@\"" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Enter the %1$@ for \"%2$@\"" - } - } - } - }, "Enter the passphrase to decrypt and import connections." : { "localizations" : { "tr" : { @@ -12006,9 +11748,6 @@ } } } - }, - "Excel spreadsheet with formatting support." : { - }, "Execute" : { "localizations" : { @@ -13003,7 +12742,6 @@ } }, "Failed to decompress file: %@" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -13138,26 +12876,7 @@ } }, "Failed to generate SQL for column reorder" : { - "localizations" : { - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sütun yeniden sıralama için SQL oluşturulamadı" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Không thể tạo SQL để sắp xếp lại cột" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "无法生成列重排的 SQL" - } - } - } + }, "Failed to import DDL: %@" : { "extractionState" : "stale", @@ -13317,7 +13036,6 @@ } }, "Failed to load preview using encoding: %@. Try selecting a different text encoding." : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -13340,7 +13058,6 @@ } }, "Failed to load preview: %@" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -13947,12 +13664,8 @@ } } } - }, - "Filter column" : { - }, "Filter column: %@" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -14017,12 +13730,8 @@ } } } - }, - "Filter operator" : { - }, "Filter operator: %@" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -14043,12 +13752,8 @@ } } } - }, - "Filter options" : { - }, "Filter presets" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -14071,7 +13776,6 @@ } }, "Filter settings" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -14094,7 +13798,6 @@ } }, "Filter Settings" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -14115,12 +13818,6 @@ } } } - }, - "Filter Settings..." : { - - }, - "Filter value" : { - }, "Filter with column" : { "localizations" : { @@ -14321,9 +14018,6 @@ } } } - }, - "Foreign Keys" : { - }, "Forever" : { "localizations" : { @@ -14368,9 +14062,6 @@ } } } - }, - "Format SQL" : { - }, "Format:" : { "localizations" : { @@ -14547,9 +14238,6 @@ } } } - }, - "Get intelligent SQL suggestions and query assistance" : { - }, "Get Started" : { "localizations" : { @@ -15038,7 +14726,6 @@ } }, "History Limit:" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -15414,26 +15101,7 @@ } }, "Import Connection from Link" : { - "localizations" : { - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bağlantıyı Linkten İçe Aktar" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nhập kết nối từ liên kết" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "从链接导入连接" - } - } - } + }, "Import Connections" : { "localizations" : { @@ -16045,6 +15713,7 @@ } }, "Indexes" : { + "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -16469,9 +16138,6 @@ } } } - }, - "Interactive Data Grid" : { - }, "Interface" : { "localizations" : { @@ -16519,26 +16185,7 @@ } }, "Invalid column indices for reorder operation" : { - "localizations" : { - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yeniden sıralama işlemi için geçersiz sütun dizinleri" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chỉ mục cột không hợp lệ cho thao tác sắp xếp lại" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "重排操作的列索引无效" - } - } - } + }, "Invalid connection URL format" : { "localizations" : { @@ -18263,9 +17910,6 @@ } } } - }, - "Location" : { - }, "Maintenance" : { "localizations" : { @@ -18465,9 +18109,6 @@ } } } - }, - "Max Bytes Billed" : { - }, "Max schema tables: %lld" : { "localizations" : { @@ -18800,9 +18441,6 @@ } } } - }, - "MongoDB query language. Use to import into MongoDB." : { - }, "Move Down" : { "extractionState" : "stale", @@ -18851,26 +18489,7 @@ } }, "Move Group to..." : { - "localizations" : { - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Grubu Taşı..." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Di chuyển nhóm đến..." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "移动分组到..." - } - } - } + }, "Move to" : { "localizations" : { @@ -19057,9 +18676,6 @@ } } } - }, - "MySQL, PostgreSQL & SQLite" : { - }, "Name" : { "localizations" : { @@ -19594,26 +19210,7 @@ } }, "New Subgroup" : { - "localizations" : { - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yeni Alt Grup" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nhóm con mới" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "新建子分组" - } - } - } + }, "New Tab" : { "localizations" : { @@ -19702,9 +19299,6 @@ } } } - }, - "Next Result" : { - }, "Next Tab" : { "localizations" : { @@ -19823,26 +19417,7 @@ } }, "No active database connection" : { - "localizations" : { - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aktif veritabanı bağlantısı yok" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Không có kết nối cơ sở dữ liệu đang hoạt động" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "没有活动的数据库连接" - } - } - } + }, "No AI provider configured. Go to Settings > AI to add one." : { "localizations" : { @@ -20773,9 +20348,6 @@ } } } - }, - "No rows returned" : { - }, "No saved connection named \"%@\"." : { "localizations" : { @@ -21066,26 +20638,7 @@ } }, "None (Top Level)" : { - "localizations" : { - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Yok (Üst Düzey)" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Không (Cấp cao nhất)" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "无(顶级)" - } - } - } + }, "Normal" : { "localizations" : { @@ -21375,7 +20928,6 @@ } }, "NOW()" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -21596,12 +21148,6 @@ } } } - }, - "OAuth Client ID" : { - - }, - "OAuth Client Secret" : { - }, "Offset" : { "localizations" : { @@ -21867,12 +21413,6 @@ } } } - }, - "Open File" : { - - }, - "Open File..." : { - }, "Open MQL Editor" : { "extractionState" : "stale", @@ -21898,48 +21438,10 @@ } }, "Open Query" : { - "localizations" : { - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sorguyu Aç" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mở truy vấn" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "打开查询" - } - } - } + }, "Open Query from Link" : { - "localizations" : { - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Linkten Sorgu Aç" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mở truy vấn từ liên kết" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "从链接打开查询" - } - } - } + }, "Open Redis CLI" : { "extractionState" : "stale", @@ -22371,26 +21873,7 @@ } }, "Parent Group" : { - "localizations" : { - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Üst Grup" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nhóm cha" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "父分组" - } - } - } + }, "Partition" : { "localizations" : { @@ -22479,9 +21962,6 @@ } } } - }, - "password" : { - }, "Password" : { "localizations" : { @@ -22526,9 +22006,6 @@ } } } - }, - "Password Required" : { - }, "Passwords will be encrypted with the passphrase you provide." : { "localizations" : { @@ -22712,9 +22189,6 @@ } } } - }, - "Pin Result" : { - }, "Pink" : { "localizations" : { @@ -23043,16 +22517,6 @@ } } }, - "Plugin was built with PluginKit version %lld, but version %lld is required. Please update the plugin." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Plugin was built with PluginKit version %1$lld, but version %2$lld is required. Please update the plugin." - } - } - } - }, "Plugins" : { "localizations" : { "tr" : { @@ -23304,26 +22768,7 @@ } }, "Pre-connect script was cancelled" : { - "localizations" : { - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bağlantı öncesi komut dosyası iptal edildi" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Script trước khi kết nối đã bị hủy" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "连接前脚本已取消" - } - } - } + }, "Precision" : { "extractionState" : "stale", @@ -23664,9 +23109,6 @@ } } } - }, - "Preview Query" : { - }, "Preview Schema Changes" : { "localizations" : { @@ -23756,9 +23198,6 @@ } } } - }, - "Previous Result" : { - }, "Previous Tab" : { "localizations" : { @@ -24109,9 +23548,6 @@ } } } - }, - "Project ID" : { - }, "Prompt at Connect" : { "localizations" : { @@ -24343,7 +23779,6 @@ } }, "Query executing..." : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -24410,7 +23845,6 @@ } }, "Query History:" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -24477,7 +23911,6 @@ } }, "Quick search across all columns..." : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -25263,9 +24696,6 @@ } } } - }, - "Remove all filters and reload" : { - }, "Remove filter" : { "localizations" : { @@ -25310,9 +24740,6 @@ } } } - }, - "Remove filter row" : { - }, "Remove Folder" : { "localizations" : { @@ -25655,15 +25082,6 @@ } } } - }, - "Reset" : { - - }, - "Reset All Settings" : { - - }, - "Reset All Settings to Defaults" : { - }, "Reset to Defaults" : { "localizations" : { @@ -25730,9 +25148,6 @@ } } } - }, - "Results" : { - }, "Retention" : { "localizations" : { @@ -26214,26 +25629,7 @@ } }, "Run Script" : { - "localizations" : { - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Komut Dosyasını Çalıştır" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Chạy script" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "运行脚本" - } - } - } + }, "Safe Mode" : { "localizations" : { @@ -26391,7 +25787,6 @@ } }, "Save and load filter presets" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -26434,9 +25829,6 @@ } } } - }, - "Save As" : { - }, "Save as Favorite" : { "localizations" : { @@ -26526,9 +25918,6 @@ } } } - }, - "Save As..." : { - }, "Save Changes" : { "localizations" : { @@ -26689,9 +26078,6 @@ } } } - }, - "Save SQL file" : { - }, "Save Table Template" : { "extractionState" : "stale", @@ -27114,9 +26500,6 @@ } } } - }, - "Second filter value" : { - }, "Second value is required for BETWEEN" : { "localizations" : { @@ -27257,9 +26640,6 @@ } } } - }, - "Secure Connections" : { - }, "SELECT * FROM users WHERE id = 1;" : { "extractionState" : "stale", @@ -27438,12 +26818,8 @@ } } } - }, - "Select filter column" : { - }, "Select filter for %@" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -27464,9 +26840,6 @@ } } } - }, - "Select filter operator" : { - }, "Select Plugin" : { "localizations" : { @@ -27512,9 +26885,6 @@ } } } - }, - "Select SQL files to open" : { - }, "Select Tab %lld" : { "localizations" : { @@ -27681,9 +27051,6 @@ } } } - }, - "Service Account Key" : { - }, "Service Name" : { "extractionState" : "stale", @@ -28532,12 +27899,6 @@ } } } - }, - "Size All Columns to Fit" : { - - }, - "Size to Fit" : { - }, "Size:" : { "extractionState" : "stale", @@ -28627,9 +27988,6 @@ } } } - }, - "Smart SQL Editor" : { - }, "Smooth" : { "localizations" : { @@ -28674,9 +28032,6 @@ } } } - }, - "Some columns in this preset don't exist in the current table" : { - }, "Something went wrong (error %lld). Try again in a moment." : { "localizations" : { @@ -28944,11 +28299,9 @@ } } } - }, - "SQL INSERT statements. Use to recreate data in another database." : { - }, "SQL Preview" : { + "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -29336,9 +28689,6 @@ } } } - }, - "SSH tunneling and SSL/TLS encryption support" : { - }, "SSH User" : { "localizations" : { @@ -29838,9 +29188,6 @@ } } } - }, - "Structured data format. Ideal for APIs and web applications." : { - }, "Success" : { "localizations" : { @@ -30049,7 +29396,6 @@ } }, "Sync" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -30070,9 +29416,6 @@ } } } - }, - "Sync (Pro)" : { - }, "Sync Categories" : { "localizations" : { @@ -30317,7 +29660,6 @@ } }, "Syncs connections, settings, and history across your Macs via iCloud." : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -30338,9 +29680,6 @@ } } } - }, - "Syncs connections, settings, and SSH profiles across your Macs via iCloud." : { - }, "Syncs passwords via iCloud Keychain (end-to-end encrypted)." : { "localizations" : { @@ -30391,9 +29730,6 @@ } } } - }, - "Syntax highlighting, autocomplete, and multi-tab editing" : { - }, "System" : { "localizations" : { @@ -30619,9 +29955,6 @@ } } } - }, - "Table Name:" : { - }, "Table: %@" : { "localizations" : { @@ -31912,9 +31245,6 @@ } } } - }, - "This will reset all settings across every section to their default values." : { - }, "Tier:" : { "localizations" : { @@ -32412,12 +31742,6 @@ } } } - }, - "Toggle Results" : { - - }, - "Toggle Results (⌘⌥R)" : { - }, "Toggle Table Browser" : { "localizations" : { @@ -32464,26 +31788,7 @@ } }, "Top Level" : { - "localizations" : { - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Üst Düzey" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cấp cao nhất" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "顶级" - } - } - } + }, "Total Size" : { "localizations" : { @@ -33212,9 +32517,6 @@ } } } - }, - "Unpin" : { - }, "Unset" : { "localizations" : { @@ -33637,7 +32939,6 @@ } }, "UTC_TIMESTAMP()" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -34407,12 +33708,8 @@ } } } - }, - "WHERE clause" : { - }, "WHERE clause..." : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -34854,12 +34151,6 @@ } } } - }, - "Zoom In" : { - - }, - "Zoom Out" : { - } }, "version" : "1.0" diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+FKNavigation.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+FKNavigation.swift index 8495b3ca..5d83c07f 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+FKNavigation.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+FKNavigation.swift @@ -71,11 +71,13 @@ extension MainContentCoordinator { } // Replace current tab content with the referenced table + let currentSchema = DatabaseManager.shared.session(for: connectionId)?.currentSchema let needsQuery = tabManager.replaceTabContent( tableName: referencedTable, databaseType: connection.type, isView: false, - databaseName: currentDatabase + databaseName: currentDatabase, + schemaName: currentSchema ) if needsQuery, let tabIndex = tabManager.selectedTabIndex { diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+MultiStatement.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+MultiStatement.swift index c9664a56..a8e8a21f 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+MultiStatement.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+MultiStatement.swift @@ -216,7 +216,7 @@ extension MainContentCoordinator { AlertHelper.showErrorSheet( title: String(localized: "Query Execution Failed"), message: contextMsg, - window: NSApp.keyWindow + window: contentWindow ) } } diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift index ae8b7844..51d90d5b 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift @@ -34,6 +34,8 @@ extension MainContentCoordinator { currentDatabase = connection.database } + let currentSchema = DatabaseManager.shared.session(for: connectionId)?.currentSchema + // Fast path: if this table is already the active tab in the same database, skip all work if let current = tabManager.selectedTab, current.tabType == .table, @@ -90,6 +92,7 @@ extension MainContentCoordinator { if let tabIndex = tabManager.selectedTabIndex { tabManager.tabs[tabIndex].isView = isView tabManager.tabs[tabIndex].isEditable = !isView + tabManager.tabs[tabIndex].schemaName = currentSchema tabManager.tabs[tabIndex].pagination.reset() AppState.shared.isCurrentTabEditable = !isView && tableName.isEmpty == false toolbarState.isTableTab = true @@ -116,7 +119,8 @@ extension MainContentCoordinator { if tabManager.replaceTabContent( tableName: tableName, databaseType: connection.type, - databaseName: currentDatabase + databaseName: currentDatabase, + schemaName: currentSchema ) { filterStateManager.clearAll() if let tabIndex = tabManager.selectedTabIndex { @@ -143,6 +147,7 @@ extension MainContentCoordinator { tabType: .table, tableName: tableName, databaseName: currentDatabase, + schemaName: currentSchema, isView: isView, showStructure: showStructure ) @@ -152,7 +157,7 @@ extension MainContentCoordinator { // Preview tab mode: reuse or create a preview tab instead of a new native window if AppSettingsManager.shared.tabs.enablePreviewTabs { - openPreviewTab(tableName, isView: isView, databaseName: currentDatabase, showStructure: showStructure) + openPreviewTab(tableName, isView: isView, databaseName: currentDatabase, schemaName: currentSchema, showStructure: showStructure) return } @@ -162,6 +167,7 @@ extension MainContentCoordinator { tabType: .table, tableName: tableName, databaseName: currentDatabase, + schemaName: currentSchema, isView: isView, showStructure: showStructure ) @@ -172,7 +178,8 @@ extension MainContentCoordinator { func openPreviewTab( _ tableName: String, isView: Bool = false, - databaseName: String = "", showStructure: Bool = false + databaseName: String = "", schemaName: String? = nil, + showStructure: Bool = false ) { // Check if a preview window already exists for this connection if let preview = WindowLifecycleMonitor.shared.previewWindow(for: connectionId) { @@ -193,6 +200,7 @@ extension MainContentCoordinator { databaseType: connection.type, isView: isView, databaseName: databaseName, + schemaName: schemaName, isPreview: true ) previewCoordinator.filterStateManager.clearAll() @@ -228,6 +236,7 @@ extension MainContentCoordinator { tabType: .table, tableName: tableName, databaseName: databaseName, + schemaName: schemaName, isView: isView, showStructure: showStructure ) @@ -242,6 +251,7 @@ extension MainContentCoordinator { databaseType: connection.type, isView: isView, databaseName: databaseName, + schemaName: schemaName, isPreview: true ) filterStateManager.clearAll() @@ -264,6 +274,7 @@ extension MainContentCoordinator { tabType: .table, tableName: tableName, databaseName: databaseName, + schemaName: schemaName, isView: isView, showStructure: showStructure, isPreview: true @@ -377,6 +388,7 @@ extension MainContentCoordinator { session.currentDatabase = database session.currentSchema = nil } + AppSettingsStorage.shared.saveLastSchema(nil, for: connectionId) await DatabaseManager.shared.reconnectSession(connectionId) } else if pm.supportsSchemaSwitching(for: connection.type) { // Redshift, Oracle: schema switching @@ -413,7 +425,7 @@ extension MainContentCoordinator { AlertHelper.showErrorSheet( title: String(localized: "Database Switch Failed"), message: error.localizedDescription, - window: NSApplication.shared.keyWindow + window: contentWindow ) } } @@ -446,6 +458,7 @@ extension MainContentCoordinator { DatabaseManager.shared.updateSession(connectionId) { session in session.currentSchema = schema } + AppSettingsStorage.shared.saveLastSchema(schema, for: connectionId) await loadSchema() @@ -459,7 +472,7 @@ extension MainContentCoordinator { AlertHelper.showErrorSheet( title: String(localized: "Schema Switch Failed"), message: error.localizedDescription, - window: NSApplication.shared.keyWindow + window: contentWindow ) } } diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+QueryHelpers.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+QueryHelpers.swift index b8466371..378189cb 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+QueryHelpers.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+QueryHelpers.swift @@ -414,7 +414,7 @@ extension MainContentCoordinator { let wantsAIFix = await AlertHelper.showQueryErrorWithAIOption( title: String(localized: "Query Execution Failed"), message: errorMessage, - window: NSApp.keyWindow + window: contentWindow ) if wantsAIFix { showAIChatPanel() @@ -424,12 +424,36 @@ extension MainContentCoordinator { AlertHelper.showErrorSheet( title: String(localized: "Query Execution Failed"), message: errorMessage, - window: NSApp.keyWindow + window: contentWindow ) } } } + /// Restore schema on the driver and run the query for the current tab. + /// Unlike `switchSchema`, this does NOT clear tabs or sidebar — it only + /// switches the driver's search_path so the restored tab's query succeeds. + func restoreSchemaAndRunQuery(_ schema: String) async { + guard let driver = DatabaseManager.shared.driver(for: connectionId), + let schemaDriver = driver as? SchemaSwitchable else { + runQuery() + return + } + do { + try await schemaDriver.switchSchema(to: schema) + DatabaseManager.shared.updateSession(connectionId) { session in + session.currentSchema = schema + } + toolbarState.databaseName = schema + await loadSchema() + reloadSidebar() + } catch { + Self.logger.warning("Failed to restore schema '\(schema, privacy: .public)': \(error.localizedDescription, privacy: .public)") + return + } + runQuery() + } + /// Build column exclusions for a table using cached column type info. /// Returns empty if no cached types exist (first load uses SELECT *). func columnExclusions(for tableName: String) -> [ColumnExclusion] { diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+SaveChanges.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+SaveChanges.swift index a610badb..405e815f 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+SaveChanges.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+SaveChanges.swift @@ -298,7 +298,7 @@ extension MainContentCoordinator { AlertHelper.showErrorSheet( title: String(localized: "Save Failed"), message: error.localizedDescription, - window: NSApplication.shared.keyWindow + window: contentWindow ) // Restore operations on failure so user can retry diff --git a/TablePro/Views/Main/MainContentCoordinator.swift b/TablePro/Views/Main/MainContentCoordinator.swift index ac1906de..a1b96f65 100644 --- a/TablePro/Views/Main/MainContentCoordinator.swift +++ b/TablePro/Views/Main/MainContentCoordinator.swift @@ -86,6 +86,10 @@ final class MainContentCoordinator { /// Direct reference to right panel state — enables showing AI panel programmatically @ObservationIgnored weak var rightPanelState: RightPanelState? + /// Direct reference to this coordinator's content window, used for presenting alerts. + /// Avoids NSApp.keyWindow which may return a sheet window, causing stuck dialogs. + @ObservationIgnored weak var contentWindow: NSWindow? + // MARK: - Published State var schemaProvider: SQLSchemaProvider diff --git a/TablePro/Views/Main/MainContentView.swift b/TablePro/Views/Main/MainContentView.swift index 0e819673..23fade2e 100644 --- a/TablePro/Views/Main/MainContentView.swift +++ b/TablePro/Views/Main/MainContentView.swift @@ -523,7 +523,8 @@ struct MainContentView: View { if let tableName = restoredTabs[i].tableName { restoredTabs[i].query = QueryTab.buildBaseTableQuery( tableName: tableName, - databaseType: connection.type + databaseType: connection.type, + schemaName: restoredTabs[i].schemaName ) } } @@ -633,6 +634,7 @@ struct MainContentView: View { isPreview: isPreview ) viewWindow = window + coordinator.contentWindow = window isKeyWindow = window.isKeyWindow // Native proxy icon (Cmd+click shows path in Finder) and dirty dot @@ -841,6 +843,15 @@ struct MainContentView: View { selectedTab.databaseName != session.activeDatabase { Task { await coordinator.switchDatabase(to: selectedTab.databaseName) } + } else if let selectedTab = tabManager.selectedTab, + let tabSchema = selectedTab.schemaName, + !tabSchema.isEmpty, + tabSchema != session.currentSchema + { + // Restore schema on the driver without clearing tabs (unlike switchSchema which resets UI) + Task { + await coordinator.restoreSchemaAndRunQuery(tabSchema) + } } else { coordinator.runQuery() }