Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Qualify table tabs before switching schemas

When a table tab was opened from the current schema, many schema-switching drivers leave tableContext.schemaName nil (for example PostgreSQL fetchTables returns PluginTableInfo(name:type:), and PluginDriverAdapter.fetchTables() maps current-schema tables with no fallback schema). Those tabs therefore store unqualified browse/edit SQL such as SELECT ... FROM "users"; after this call changes the connection search path while keeping the tab, refreshing, filtering, or saving edits in that preserved tab can target users in the newly selected schema instead of the table the tab originally displayed whenever schemas share table names. Please stamp existing table tabs with the previous schema (or otherwise qualify/close them) before switching.

Useful? React with 👍 / 👎.


closeSiblingNativeWindows()
persistence.saveNowSync(tabs: tabManager.tabs, selectedTabId: tabManager.selectedTabId)
tabSessionRegistry.removeAll()
tabManager.tabs = []
tabManager.selectedTabId = nil
} catch {
toolbarState.currentSchema = previousSchema

Expand Down
3 changes: 3 additions & 0 deletions TablePro/Views/Main/MainContentCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Expand Down
11 changes: 10 additions & 1 deletion TableProTests/Core/Autocomplete/SQLSchemaProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]] = [:]
Expand Down Expand Up @@ -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
Expand Down
87 changes: 87 additions & 0 deletions TableProTests/Views/SwitchSchemaTests.swift
Original file line number Diff line number Diff line change
@@ -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

Check failure on line 38 in TableProTests/Views/SwitchSchemaTests.swift

View workflow job for this annotation

GitHub Actions / macOS App Tests

cannot pass function of type '@Concurrent (DatabaseConnection, MockDatabaseDriver) async -> Void' to parameter expecting synchronous function type
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

Check failure on line 66 in TableProTests/Views/SwitchSchemaTests.swift

View workflow job for this annotation

GitHub Actions / macOS App Tests

cannot pass function of type '@Concurrent (DatabaseConnection, MockDatabaseDriver) async -> Void' to parameter expecting synchronous function type
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")
}
}
}
Loading