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: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Show schemas as collapsible sections in the Tables sidebar (#1296)
- Schema picker at the bottom of the Tables sidebar to switch the active schema (#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)
- CSV/TSV inspector: open files from Finder or File > Open, edit cells, multi-condition filter (Cmd+F), multi-column sort (shift-click headers), add/remove/rename columns with type override, copy/paste rows as TSV, undo/redo, auto-reload on external changes. Large files stream from disk without loading into memory. (#1259)
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
4 changes: 2 additions & 2 deletions TablePro/Core/Events/AppEvents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ final class AppEvents {

let editorSettingsChanged = PassthroughSubject<Void, Never>()

let sidebarSettingsChanged = PassthroughSubject<Void, Never>()

let dataGridSettingsChanged = PassthroughSubject<Void, Never>()

let currentSchemaChanged = PassthroughSubject<UUID, Never>()

let aiSettingsChanged = PassthroughSubject<Void, Never>()

let terminalSettingsChanged = PassthroughSubject<Void, Never>()
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, schema: table.schema, isView: isView)
coordinator.openTableTab(table)
}
} else {
coordinator.promotePreviewTab()
coordinator.openTableTab(table.name, schema: table.schema, isView: isView)
coordinator.openTableTab(table)
}
},
pendingTruncates: sessionPendingTruncatesBinding,
Expand Down
161 changes: 28 additions & 133 deletions TablePro/Core/Services/Query/SchemaService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,14 @@ final class SchemaService {
@ObservationIgnored private let procedureDedup = OnceTask<UUID, [RoutineInfo]>()
@ObservationIgnored private let functionDedup = OnceTask<UUID, [RoutineInfo]>()
@ObservationIgnored private let schemasDedup = OnceTask<UUID, [String]>()
@ObservationIgnored private var settingsCancellable: AnyCancellable?
@ObservationIgnored private var lastDisplaySchemas: Bool = false
@ObservationIgnored private var schemaChangeCancellable: AnyCancellable?
@ObservationIgnored private static let logger = Logger(subsystem: "com.TablePro", category: "SchemaService")

init() {
lastDisplaySchemas = AppSettingsManager.shared.sidebar.displaySchemas
settingsCancellable = AppEvents.shared.sidebarSettingsChanged
.sink { [weak self] in
schemaChangeCancellable = AppEvents.shared.currentSchemaChanged
.sink { [weak self] connectionId in
Task { @MainActor [weak self] in
self?.handleSidebarSettingsChange()
await self?.handleSchemaSwitch(connectionId: connectionId)
}
}
}
Expand Down Expand Up @@ -91,13 +89,9 @@ final class SchemaService {
}

func reloadProcedures(connectionId: UUID, driver: DatabaseDriver) async {
let visibleSchemas = visibleSchemasForGroupedReload(connectionId: connectionId, driver: driver)
do {
let routines = try await procedureDedup.execute(key: connectionId) {
if let schemas = visibleSchemas {
return try await Self.fetchRoutinesAcrossSchemas(driver: driver, schemas: schemas, kind: .procedure)
}
return try await driver.fetchProcedures(schema: nil)
try await driver.fetchProcedures(schema: nil)
}
procedures[connectionId] = routines
} catch is CancellationError {
Expand All @@ -110,13 +104,9 @@ final class SchemaService {
}

func reloadFunctions(connectionId: UUID, driver: DatabaseDriver) async {
let visibleSchemas = visibleSchemasForGroupedReload(connectionId: connectionId, driver: driver)
do {
let routines = try await functionDedup.execute(key: connectionId) {
if let schemas = visibleSchemas {
return try await Self.fetchRoutinesAcrossSchemas(driver: driver, schemas: schemas, kind: .function)
}
return try await driver.fetchFunctions(schema: nil)
try await driver.fetchFunctions(schema: nil)
}
functions[connectionId] = routines
} catch is CancellationError {
Expand All @@ -140,29 +130,24 @@ final class SchemaService {
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

let wantsGrouping = AppSettingsManager.shared.sidebar.displaySchemas
&& PluginManager.shared.supportsSchemaSwitching(for: connection.type)

if wantsGrouping {
await runSchemaGroupedLoad(connectionId: connectionId, driver: driver, connection: connection)
} else {
await runFlatLoad(connectionId: connectionId, driver: driver, connection: connection)
let supportsSchemas = PluginManager.shared.supportsSchemaSwitching(for: connection.type)
if !supportsSchemas {
schemasInOrder.removeValue(forKey: connectionId)
}
}

private func runFlatLoad(
connectionId: UUID,
driver: DatabaseDriver,
connection: DatabaseConnection
) async {
schemasInOrder.removeValue(forKey: connectionId)

async let tablesTask: [TableInfo] = loadDedup.execute(key: connectionId) {
try await driver.fetchTables()
Expand All @@ -182,6 +167,9 @@ final class SchemaService {

let loadedProcedures = await proceduresTask
let loadedFunctions = await functionsTask
if supportsSchemas {
await loadSchemaList(connectionId: connectionId, driver: driver)
}

do {
let tables = try await tablesTask
Expand All @@ -199,102 +187,19 @@ final class SchemaService {
}
}

private func runSchemaGroupedLoad(
connectionId: UUID,
driver: DatabaseDriver,
connection: DatabaseConnection
) async {
let dbType = connection.type
let allSchemas: [String]
private func loadSchemaList(connectionId: UUID, driver: DatabaseDriver) async {
do {
allSchemas = try await schemasDedup.execute(key: connectionId) {
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); falling back to flat load"
"[schema] fetchSchemas failed connId=\(connectionId, privacy: .public) error=\(error.localizedDescription, privacy: .public)"
)
await runFlatLoad(connectionId: connectionId, driver: driver, connection: connection)
return
}

let systemSchemas = Set(PluginManager.shared.systemSchemaNames(for: dbType))
let visibleSchemas = allSchemas.filter { !systemSchemas.contains($0) }
schemasInOrder[connectionId] = visibleSchemas

async let tablesTask: [TableInfo] = loadDedup.execute(key: connectionId) {
try await Self.fetchTablesAcrossSchemas(driver: driver, schemas: visibleSchemas)
}
async let proceduresTask: [RoutineInfo] = Self.fetchRoutinesSafely(
connectionId: connectionId,
kind: .procedure,
dedup: procedureDedup,
fetch: { try await Self.fetchRoutinesAcrossSchemas(driver: driver, schemas: visibleSchemas, kind: .procedure) }
)
async let functionsTask: [RoutineInfo] = Self.fetchRoutinesSafely(
connectionId: connectionId,
kind: .function,
dedup: functionDedup,
fetch: { try await Self.fetchRoutinesAcrossSchemas(driver: driver, schemas: visibleSchemas, kind: .function) }
)

let loadedProcedures = await proceduresTask
let loadedFunctions = await functionsTask

do {
let tables = try await tablesTask
states[connectionId] = .loaded(tables)
procedures[connectionId] = loadedProcedures
functions[connectionId] = loadedFunctions
lastLoadDates[connectionId] = Date()
} catch is CancellationError {
return
} catch {
Self.logger.warning(
"[schema] grouped load failed connId=\(connectionId, privacy: .public) error=\(error.localizedDescription, privacy: .public)"
)
states[connectionId] = .failed(error.localizedDescription)
}
}

private func visibleSchemasForGroupedReload(connectionId: UUID, driver: DatabaseDriver) -> [String]? {
guard AppSettingsManager.shared.sidebar.displaySchemas else { return nil }
let schemas = schemasInOrder[connectionId] ?? []
guard !schemas.isEmpty else { return nil }
return schemas
}

private static func fetchTablesAcrossSchemas(
driver: DatabaseDriver,
schemas: [String]
) async throws -> [TableInfo] {
var aggregated: [TableInfo] = []
for schema in schemas {
try Task.checkCancellation()
let tables = try await driver.fetchTables(schema: schema)
aggregated.append(contentsOf: tables)
}
return aggregated
}

private static func fetchRoutinesAcrossSchemas(
driver: DatabaseDriver,
schemas: [String],
kind: RoutineInfo.Kind
) async throws -> [RoutineInfo] {
var aggregated: [RoutineInfo] = []
for schema in schemas {
try Task.checkCancellation()
let routines: [RoutineInfo]
switch kind {
case .procedure: routines = try await driver.fetchProcedures(schema: schema)
case .function: routines = try await driver.fetchFunctions(schema: schema)
}
aggregated.append(contentsOf: routines)
}
return aggregated
}

private static func fetchRoutinesSafely(
Expand All @@ -315,20 +220,10 @@ final class SchemaService {
}
}

private func handleSidebarSettingsChange() {
let now = AppSettingsManager.shared.sidebar.displaySchemas
guard now != lastDisplaySchemas else { return }
lastDisplaySchemas = now

let sessions = DatabaseManager.shared.activeSessions
for (connectionId, session) in sessions {
guard let driver = session.driver else { continue }
let connection = session.connection
Task { [weak self] in
guard let self else { return }
await self.invalidate(connectionId: connectionId)
await self.reload(connectionId: connectionId, driver: driver, connection: connection)
}
}
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)
}
}
9 changes: 0 additions & 9 deletions TablePro/Core/Storage/AppSettingsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,6 @@ final class AppSettingsManager {
}
}

var sidebar: SidebarSettings {
didSet {
storage.saveSidebar(sidebar)
appEvents.sidebarSettingsChanged.send(())
syncTracker.markDirty(.settings, id: "sidebar")
}
}

var keyboard: KeyboardSettings {
didSet {
storage.saveKeyboard(keyboard)
Expand Down Expand Up @@ -211,7 +203,6 @@ final class AppSettingsManager {
self.dataGrid = storage.loadDataGrid()
self.history = storage.loadHistory()
self.tabs = storage.loadTabs()
self.sidebar = storage.loadSidebar()
self.keyboard = storage.loadKeyboard()
self.ai = Self.migrateAI(storage.loadAI())
self.sync = storage.loadSync()
Expand Down
11 changes: 0 additions & 11 deletions TablePro/Core/Storage/AppSettingsStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ final class AppSettingsStorage {
static let dataGrid = "com.TablePro.settings.dataGrid"
static let history = "com.TablePro.settings.history"
static let tabs = "com.TablePro.settings.tabs"
static let sidebar = "com.TablePro.settings.sidebar"
static let keyboard = "com.TablePro.settings.keyboard"
static let ai = "com.TablePro.settings.ai"
static let sync = "com.TablePro.settings.sync"
Expand Down Expand Up @@ -101,16 +100,6 @@ final class AppSettingsStorage {
save(settings, key: Keys.tabs)
}

// MARK: - Sidebar Settings

func loadSidebar() -> SidebarSettings {
load(key: Keys.sidebar, default: .default)
}

func saveSidebar(_ settings: SidebarSettings) {
save(settings, key: Keys.sidebar)
}

// MARK: - Keyboard Settings

func loadKeyboard() -> KeyboardSettings {
Expand Down
14 changes: 11 additions & 3 deletions TablePro/Models/Query/QueryTabManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ final class QueryTabManager {
quoteIdentifier: quoteIdentifier
)
var newTab = QueryTab(
title: tableName,
title: Self.tabTitle(name: tableName, schema: schemaName, databaseType: databaseType),
query: query,
tabType: .table,
tableName: tableName
Expand All @@ -175,6 +175,14 @@ final class QueryTabManager {
selectedTabId = newTab.id
}

static func tabTitle(name: String, schema: String?, databaseType: DatabaseType) -> String {
guard let schema, !schema.isEmpty else { return name }
let defaultSchema = PluginMetadataRegistry.shared
.snapshot(forTypeId: databaseType.pluginTypeId)?
.schema.defaultSchemaName ?? ""
return schema == defaultSchema ? name : "\(schema).\(name)"
}

func addCreateTableTab(databaseName: String = "") {
let tabTitle = String(localized: "Create Table")
var newTab = QueryTab(title: tabTitle, tabType: .createTable)
Expand Down Expand Up @@ -248,7 +256,7 @@ final class QueryTabManager {
quoteIdentifier: quoteIdentifier
)
var newTab = QueryTab(
title: tableName,
title: Self.tabTitle(name: tableName, schema: schemaName, databaseType: databaseType),
query: query,
tabType: .table,
tableName: tableName
Expand Down Expand Up @@ -287,7 +295,7 @@ final class QueryTabManager {

var tab = tabs[selectedIndex]
tab.tabType = .table
tab.title = tableName
tab.title = Self.tabTitle(name: tableName, schema: schemaName, databaseType: databaseType)
tab.tableContext.tableName = tableName
tab.content.query = query
tab.schemaVersion += 1
Expand Down
17 changes: 0 additions & 17 deletions TablePro/Models/Settings/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -281,23 +281,6 @@ struct DataGridSettings: Codable, Equatable {
}
}

// MARK: - Sidebar Settings

struct SidebarSettings: Codable, Equatable {
var displaySchemas: Bool

static let `default` = SidebarSettings(displaySchemas: false)

init(displaySchemas: Bool = false) {
self.displaySchemas = displaySchemas
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
displaySchemas = try container.decodeIfPresent(Bool.self, forKey: .displaySchemas) ?? false
}
}

// MARK: - History Settings

/// History settings
Expand Down
Loading
Loading