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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 13 additions & 0 deletions TablePro/Core/Database/DatabaseManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ internal final class TabPersistenceCoordinator {
tableName: tab.tableName,
isView: tab.isView,
databaseName: tab.databaseName,
schemaName: tab.schemaName,
sourceFileURL: tab.sourceFileURL
)
}
Expand Down
14 changes: 14 additions & 0 deletions TablePro/Core/Storage/AppSettingsStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions TablePro/Models/Query/EditorTabPayload.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
21 changes: 17 additions & 4 deletions TablePro/Models/Query/QueryTab.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ struct PersistedTab: Codable {
let tableName: String?
var isView: Bool = false
var databaseName: String = ""
var schemaName: String?
var sourceFileURL: URL?
}

Expand Down Expand Up @@ -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?
Expand Down Expand Up @@ -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 = []
Expand Down Expand Up @@ -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 = []
Expand All @@ -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))
Expand All @@ -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);"
}
}
}
Expand All @@ -532,6 +542,7 @@ struct QueryTab: Identifiable, Equatable {
tableName: tableName,
isView: isView,
databaseName: databaseName,
schemaName: schemaName,
sourceFileURL: sourceFileURL
)
}
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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
Expand Down
Loading
Loading