diff --git a/CHANGELOG.md b/CHANGELOG.md index a924b0375..40f3779a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Keyword autocomplete and SQL keyword suggestions now work in editors without a database connection, and favorites appear in the completion popup immediately instead of after a short delay. - Typing a favorite's keyword in the Quick Switcher now finds the saved query instead of ranking it below name matches. - PostgreSQL databases without a "public" schema now load tables from the first available schema, the schema selector also appears when only one schema exists, and the database list counts tables in every user schema instead of only "public". (#1662) +- Switching schemas no longer closes your open tabs or discards unsaved SQL. Tabs stay open, and the sidebar, schema chip, and autocomplete update to the new schema. (#1669) - Creating a table now turns the Create Table tab into the new table's tab instead of leaving the creation tab open next to a duplicate, and the sidebar shows the new table without a manual refresh. (#1664) - Cmd+S in the Create Table tab now creates the table, matching the Save shortcut everywhere else. (#1664) - Format Query can now be undone with Cmd+Z; the formatting is applied as a single editor edit instead of clearing the undo history. (#1645) diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift index 18a55de7a..d132173a3 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift @@ -448,18 +448,11 @@ extension MainContentCoordinator { return } - clearFilterState() let previousSchema = toolbarState.currentSchema toolbarState.currentSchema = schema do { try await DatabaseManager.shared.switchSchema(to: schema, for: connectionId) - - closeSiblingNativeWindows() - persistence.saveNowSync(tabs: tabManager.tabs, selectedTabId: tabManager.selectedTabId) - tabSessionRegistry.removeAll() - tabManager.tabs = [] - tabManager.selectedTabId = nil } catch { toolbarState.currentSchema = previousSchema diff --git a/TablePro/Views/Main/MainContentCoordinator.swift b/TablePro/Views/Main/MainContentCoordinator.swift index 1e4184cbd..cd110f992 100644 --- a/TablePro/Views/Main/MainContentCoordinator.swift +++ b/TablePro/Views/Main/MainContentCoordinator.swift @@ -419,6 +419,9 @@ final class MainContentCoordinator { .sink { [weak self] changedConnectionId in guard let self, changedConnectionId == self.connectionId else { return } Task { @MainActor in + if let schema = self.services.databaseManager.session(for: self.connectionId)?.currentSchema { + self.toolbarState.currentSchema = schema + } await self.refreshTables() } } diff --git a/TableProTests/Core/Autocomplete/SQLSchemaProviderTests.swift b/TableProTests/Core/Autocomplete/SQLSchemaProviderTests.swift index 71ff47db8..c974f6183 100644 --- a/TableProTests/Core/Autocomplete/SQLSchemaProviderTests.swift +++ b/TableProTests/Core/Autocomplete/SQLSchemaProviderTests.swift @@ -12,11 +12,15 @@ import Testing // MARK: - Mock Driver -final class MockDatabaseDriver: DatabaseDriver, @unchecked Sendable { +final class MockDatabaseDriver: DatabaseDriver, SchemaSwitchable, @unchecked Sendable { let connection: DatabaseConnection var status: ConnectionStatus = .connected var serverVersion: String? { nil } + var currentSchema: String? + var escapedSchema: String? + var switchSchemaCallCount = 0 + var tablesToReturn: [TableInfo] = [] var schemaTablesToReturn: [String: [TableInfo]] = [:] var columnsToReturn: [String: [ColumnInfo]] = [:] @@ -99,6 +103,11 @@ final class MockDatabaseDriver: DatabaseDriver, @unchecked Sendable { func beginTransaction() async throws {} func commitTransaction() async throws {} func rollbackTransaction() async throws {} + + func switchSchema(to schema: String) async throws { + switchSchemaCallCount += 1 + currentSchema = schema + } } // MARK: - Tests diff --git a/TableProTests/Views/SwitchSchemaTests.swift b/TableProTests/Views/SwitchSchemaTests.swift new file mode 100644 index 000000000..30670e2b0 --- /dev/null +++ b/TableProTests/Views/SwitchSchemaTests.swift @@ -0,0 +1,87 @@ +// +// SwitchSchemaTests.swift +// TableProTests +// +// Tests for the "switch schema" flow: switching the active schema changes +// the connection's search_path but must not close or clear open tabs. +// Regression coverage for #1669: switching schemas wiped every tab, +// discarding unsaved SQL in query editor tabs. +// + +import Foundation +import Testing + +@testable import TablePro + +@Suite("SwitchSchema") +@MainActor +struct SwitchSchemaTests { + private func makeTab(title: String, query: String, tabType: TabType, tableName: String? = nil) -> QueryTab { + QueryTab(title: title, query: query, tabType: tabType, tableName: tableName) + } + + private func withSchemaSwitchingConnection( + _ body: (DatabaseConnection, MockDatabaseDriver) -> Void + ) { + let connection = TestFixtures.makeConnection(type: .postgresql) + let driver = MockDatabaseDriver(connection: connection) + DatabaseManager.shared.injectSession( + ConnectionSession(connection: connection, driver: driver), + for: connection.id + ) + defer { DatabaseManager.shared.removeSession(for: connection.id) } + body(connection, driver) + } + + @Test("switchSchema keeps query and table tabs and their contents") + func switchSchemaPreservesTabs() async { + await withSchemaSwitchingConnection { connection, driver in + let tabManager = QueryTabManager() + let coordinator = MainContentCoordinator( + connection: connection, + tabManager: tabManager, + changeManager: DataChangeManager(), + toolbarState: ConnectionToolbarState() + ) + defer { coordinator.teardown() } + + let queryTab = makeTab(title: "Query 1", query: "SELECT 42", tabType: .query) + let tableTab = makeTab(title: "users", query: "SELECT * FROM users", tabType: .table, tableName: "users") + tabManager.tabs = [queryTab, tableTab] + tabManager.selectedTabId = queryTab.id + let idsBefore = tabManager.tabs.map(\.id) + + await coordinator.switchSchema(to: "s2") + + #expect(tabManager.tabs.map(\.id) == idsBefore) + #expect(tabManager.selectedTabId == queryTab.id) + #expect(tabManager.tabs.contains { $0.content.query == "SELECT 42" }) + #expect(driver.currentSchema == "s2") + #expect(coordinator.toolbarState.currentSchema == "s2") + } + } + + @Test("switchSchema leaves tabs untouched when the schema is unchanged") + func switchSchemaToSameSchemaKeepsTabs() async { + await withSchemaSwitchingConnection { connection, _ in + let tabManager = QueryTabManager() + let coordinator = MainContentCoordinator( + connection: connection, + tabManager: tabManager, + changeManager: DataChangeManager(), + toolbarState: ConnectionToolbarState() + ) + defer { coordinator.teardown() } + + let queryTab = makeTab(title: "Query 1", query: "SELECT 1", tabType: .query) + tabManager.tabs = [queryTab] + tabManager.selectedTabId = queryTab.id + + await coordinator.switchSchema(to: "s1") + await coordinator.switchSchema(to: "s1") + + #expect(tabManager.tabs.count == 1) + #expect(tabManager.tabs.first?.content.query == "SELECT 1") + } + } +}