Skip to content
Closed
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 @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Schema picker at the bottom of the Tables sidebar to switch the active schema, matching the database switcher pattern (#1296)
- Inline dropdown picker when editing ENUM and SET columns, covering MySQL, MariaDB, PostgreSQL, ClickHouse, DuckDB, and MongoDB JSON-schema enums (#1283)
- Filter rows show an enum dropdown for `=` and `!=` operators on enum columns (#1283)

Expand Down
6 changes: 6 additions & 0 deletions TablePro/Core/Database/DatabaseDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ protocol DatabaseDriver: AnyObject {
/// Fetch all tables in the database
func fetchTables() async throws -> [TableInfo]

func fetchTables(schema: String?) async throws -> [TableInfo]

/// Fetch columns for a specific table
func fetchColumns(table: String) async throws -> [ColumnInfo]

Expand Down Expand Up @@ -359,6 +361,10 @@ extension DatabaseDriver {
/// Default: no schema support (MySQL/SQLite don't use schemas in the same way)
func fetchSchemas() async throws -> [String] { [] }

func fetchTables(schema: String?) async throws -> [TableInfo] {
try await fetchTables()
}

func fetchProcedures(schema: String?) async throws -> [RoutineInfo] { [] }

func fetchFunctions(schema: String?) async throws -> [RoutineInfo] { [] }
Expand Down
1 change: 1 addition & 0 deletions TablePro/Core/Database/DatabaseManager+Sessions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ extension DatabaseManager {
session.currentSchema = schema
}
appSettingsStorage.saveLastSchema(schema, for: connectionId)
AppEvents.shared.currentSchemaChanged.send(connectionId)
}

func switchToSession(_ sessionId: UUID) {
Expand Down
2 changes: 2 additions & 0 deletions TablePro/Core/Events/AppEvents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ final class AppEvents {

let dataGridSettingsChanged = PassthroughSubject<Void, Never>()

let currentSchemaChanged = PassthroughSubject<UUID, Never>()

let aiSettingsChanged = PassthroughSubject<Void, Never>()

let terminalSettingsChanged = PassthroughSubject<Void, Never>()
Expand Down
49 changes: 31 additions & 18 deletions TablePro/Core/Plugins/PluginDriverAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -160,25 +160,38 @@ final class PluginDriverAdapter: DatabaseDriver, SchemaSwitchable {

func fetchTables() async throws -> [TableInfo] {
let pluginTables = try await pluginDriver.fetchTables(schema: pluginDriver.currentSchema)
return pluginTables.map { table in
let tableType: TableInfo.TableType
switch table.type.lowercased() {
case "table", "base table", "prefix":
tableType = .table
case "view":
tableType = .view
case "materialized view", "materialized_view":
tableType = .materializedView
case "foreign table", "foreign_table":
tableType = .foreignTable
case "system table", "system base table", "system view":
tableType = .systemTable
default:
Self.logger.warning("Unknown plugin table type \"\(table.type, privacy: .public)\" for \"\(table.name, privacy: .public)\"; defaulting to .table")
tableType = .table
}
return TableInfo(name: table.name, type: tableType, rowCount: table.rowCount)
return pluginTables.map { mapPluginTable($0, schemaFallback: nil) }
}

func fetchTables(schema: String?) async throws -> [TableInfo] {
let resolvedSchema = schema ?? pluginDriver.currentSchema
let pluginTables = try await pluginDriver.fetchTables(schema: resolvedSchema)
return pluginTables.map { mapPluginTable($0, schemaFallback: resolvedSchema) }
}

private func mapPluginTable(_ table: PluginTableInfo, schemaFallback: String?) -> TableInfo {
let tableType: TableInfo.TableType
switch table.type.lowercased() {
case "table", "base table", "prefix":
tableType = .table
case "view":
tableType = .view
case "materialized view", "materialized_view":
tableType = .materializedView
case "foreign table", "foreign_table":
tableType = .foreignTable
case "system table", "system base table", "system view":
tableType = .systemTable
default:
Self.logger.warning("Unknown plugin table type \"\(table.type, privacy: .public)\" for \"\(table.name, privacy: .public)\"; defaulting to .table")
tableType = .table
}
return TableInfo(
name: table.name,
type: tableType,
rowCount: table.rowCount,
schema: table.schema ?? schemaFallback
)
}

func fetchColumns(table: String) async throws -> [ColumnInfo] {
Expand Down
6 changes: 3 additions & 3 deletions TablePro/Core/Plugins/PluginManager+Registration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ extension PluginManager {

if let driver = instance as? any DriverPlugin {
if !declared.contains(.databaseDriver) {
Self.logger.warning("Plugin '\(pluginId)' conforms to DriverPlugin but does not declare .databaseDriver capability registering anyway")
Self.logger.warning("Plugin '\(pluginId)' conforms to DriverPlugin but does not declare .databaseDriver capability - registering anyway")
}
do {
try validateDriverDescriptor(type(of: driver), pluginId: pluginId)
Expand Down Expand Up @@ -57,7 +57,7 @@ extension PluginManager {

if let exportPlugin = instance as? any ExportFormatPlugin {
if !declared.contains(.exportFormat) {
Self.logger.warning("Plugin '\(pluginId)' conforms to ExportFormatPlugin but does not declare .exportFormat capability registering anyway")
Self.logger.warning("Plugin '\(pluginId)' conforms to ExportFormatPlugin but does not declare .exportFormat capability - registering anyway")
}
let formatId = type(of: exportPlugin).formatId
exportPlugins[formatId] = exportPlugin
Expand All @@ -67,7 +67,7 @@ extension PluginManager {

if let importPlugin = instance as? any ImportFormatPlugin {
if !declared.contains(.importFormat) {
Self.logger.warning("Plugin '\(pluginId)' conforms to ImportFormatPlugin but does not declare .importFormat capability registering anyway")
Self.logger.warning("Plugin '\(pluginId)' conforms to ImportFormatPlugin but does not declare .importFormat capability - registering anyway")
}
let formatId = type(of: importPlugin).formatId
importPlugins[formatId] = importPlugin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,18 +333,17 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
onDoubleClick: { [weak self] table in
guard let coordinator = self?.sessionState?.coordinator else { return }
let connectionId = coordinator.connectionId
let isView = table.type == .view
if let preview = WindowLifecycleMonitor.shared.previewWindow(for: connectionId),
let previewCoordinator = MainContentCoordinator.coordinator(for: preview.windowId) {
if previewCoordinator.tabManager.selectedTab?.tableContext.tableName == table.name {
previewCoordinator.promotePreviewTab()
} else {
previewCoordinator.promotePreviewTab()
coordinator.openTableTab(table.name, isView: isView)
coordinator.openTableTab(table)
}
} else {
coordinator.promotePreviewTab()
coordinator.openTableTab(table.name, isView: isView)
coordinator.openTableTab(table)
}
},
pendingTruncates: sessionPendingTruncatesBinding,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,15 @@ enum SessionStateFactory {
try tabMgr.addPreviewTableTab(
tableName: tableName,
databaseType: connection.type,
databaseName: payload.databaseName ?? activeDatabaseName
databaseName: payload.databaseName ?? activeDatabaseName,
schemaName: payload.schemaName
)
} else {
try tabMgr.addTableTab(
tableName: tableName,
databaseType: connection.type,
databaseName: payload.databaseName ?? activeDatabaseName
databaseName: payload.databaseName ?? activeDatabaseName,
schemaName: payload.schemaName
)
}
} catch {
Expand Down
54 changes: 53 additions & 1 deletion TablePro/Core/Services/Query/SchemaService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// TablePro
//

import Combine
import Foundation
import os

Expand All @@ -14,14 +15,24 @@ final class SchemaService {
private(set) var states: [UUID: SchemaState] = [:]
private(set) var procedures: [UUID: [RoutineInfo]] = [:]
private(set) var functions: [UUID: [RoutineInfo]] = [:]
private(set) var schemasInOrder: [UUID: [String]] = [:]

@ObservationIgnored private var lastLoadDates: [UUID: Date] = [:]
@ObservationIgnored private let loadDedup = OnceTask<UUID, [TableInfo]>()
@ObservationIgnored private let procedureDedup = OnceTask<UUID, [RoutineInfo]>()
@ObservationIgnored private let functionDedup = OnceTask<UUID, [RoutineInfo]>()
@ObservationIgnored private let schemasDedup = OnceTask<UUID, [String]>()
@ObservationIgnored private var schemaChangeCancellable: AnyCancellable?
@ObservationIgnored private static let logger = Logger(subsystem: "com.TablePro", category: "SchemaService")

init() {}
init() {
schemaChangeCancellable = AppEvents.shared.currentSchemaChanged
.sink { [weak self] connectionId in
Task { @MainActor [weak self] in
await self?.handleSchemaSwitch(connectionId: connectionId)
}
}
}

func state(for connectionId: UUID) -> SchemaState {
states[connectionId] ?? .idle
Expand All @@ -46,6 +57,10 @@ final class SchemaService {
procedures(for: connectionId) + functions(for: connectionId)
}

func schemas(for connectionId: UUID) -> [String] {
schemasInOrder[connectionId] ?? []
}

func load(connectionId: UUID, driver: DatabaseDriver, connection: DatabaseConnection) async {
switch state(for: connectionId) {
case .loaded:
Expand Down Expand Up @@ -107,19 +122,34 @@ final class SchemaService {
await loadDedup.cancel(key: connectionId)
await procedureDedup.cancel(key: connectionId)
await functionDedup.cancel(key: connectionId)
await schemasDedup.cancel(key: connectionId)
states.removeValue(forKey: connectionId)
procedures.removeValue(forKey: connectionId)
functions.removeValue(forKey: connectionId)
schemasInOrder.removeValue(forKey: connectionId)
lastLoadDates.removeValue(forKey: connectionId)
}

func refresh(connectionId: UUID) async {
guard let session = DatabaseManager.shared.activeSessions[connectionId],
let driver = session.driver else { return }
await invalidate(connectionId: connectionId)
await reload(connectionId: connectionId, driver: driver, connection: session.connection)
}

private func runLoad(
connectionId: UUID,
driver: DatabaseDriver,
connection: DatabaseConnection
) async {
states[connectionId] = .loading

if PluginManager.shared.supportsSchemaSwitching(for: connection.type) {
Task { [weak self] in await self?.loadSchemaList(connectionId: connectionId, driver: driver) }
} else {
schemasInOrder.removeValue(forKey: connectionId)
}

async let tablesTask: [TableInfo] = loadDedup.execute(key: connectionId) {
try await driver.fetchTables()
}
Expand Down Expand Up @@ -155,6 +185,21 @@ final class SchemaService {
}
}

private func loadSchemaList(connectionId: UUID, driver: DatabaseDriver) async {
do {
let allSchemas = try await schemasDedup.execute(key: connectionId) {
try await driver.fetchSchemas()
}
schemasInOrder[connectionId] = allSchemas
} catch is CancellationError {
return
} catch {
Self.logger.warning(
"[schema] fetchSchemas failed connId=\(connectionId, privacy: .public) error=\(error.localizedDescription, privacy: .public)"
)
}
}

private static func fetchRoutinesSafely(
connectionId: UUID,
kind: RoutineInfo.Kind,
Expand All @@ -172,4 +217,11 @@ final class SchemaService {
return []
}
}

private func handleSchemaSwitch(connectionId: UUID) async {
guard let session = DatabaseManager.shared.activeSessions[connectionId],
let driver = session.driver else { return }
await invalidate(connectionId: connectionId)
await reload(connectionId: connectionId, driver: driver, connection: session.connection)
}
}
18 changes: 16 additions & 2 deletions TablePro/Models/Query/QueryResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,16 @@ enum DatabaseError: Error, LocalizedError {

/// Information about a database table
struct TableInfo: Identifiable, Hashable, Sendable {
var id: String { "\(name)_\(type.rawValue)" }
var id: String {
if let schema, !schema.isEmpty {
return "\(schema).\(name)_\(type.rawValue)"
}
return "\(name)_\(type.rawValue)"
}
let name: String
let type: TableType
let rowCount: Int?
let schema: String?

enum TableType: String, Sendable {
case table = "TABLE"
Expand All @@ -86,13 +92,21 @@ struct TableInfo: Identifiable, Hashable, Sendable {
case systemTable = "SYSTEM TABLE"
}

init(name: String, type: TableType, rowCount: Int?, schema: String? = nil) {
self.name = name
self.type = type
self.rowCount = rowCount
self.schema = schema
}

static func == (lhs: TableInfo, rhs: TableInfo) -> Bool {
lhs.name == rhs.name && lhs.type == rhs.type
lhs.name == rhs.name && lhs.type == rhs.type && lhs.schema == rhs.schema
}

func hash(into hasher: inout Hasher) {
hasher.combine(name)
hasher.combine(type)
hasher.combine(schema)
}
}

Expand Down
Loading
Loading