Skip to content

Commit 69d0634

Browse files
authored
feat: replace hardcoded DatabaseType switches with dynamic plugin metadata lookups (#306)
* feat: replace hardcoded DatabaseType switches with dynamic plugin metadata lookups (#305) * fix: address PR review feedback for plugin metadata lookups * fix: address PR review round 2 feedback - Fix Switch Connection button disabled state (connection switching always works) - Localize cascade description strings in TableOperationDialog - Sanitize file extension in ConnectionFormView filePathPrompt - Deduplicate aliased plugins via ObjectIdentifier in PluginManager * fix: address code review issues in plugin metadata PR - Remove .db from DuckDB file extensions to avoid collision with SQLite - Use supportsImport plugin lookup instead of editorLanguage check in sidebar - Wire SidebarContextMenuLogic.importVisible into production view body - Simplify window title to unconditional "\(langName) Query" pattern - Update tests to match supportsImport-based import visibility
1 parent 6e1b540 commit 69d0634

36 files changed

Lines changed: 485 additions & 372 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Changed
1111

12+
- Replaced ~40 hardcoded `DatabaseType` switches across ~20 UI files with dynamic plugin property lookups via `PluginManager`, so third-party plugins get correct UI behavior (colors, labels, editor language, feature toggles) automatically
13+
- Replaced `AppState.isMongoDB`/`isRedis` booleans with `AppState.editorLanguage: EditorLanguage` for extensible editor language detection
14+
- Theme colors now derived from plugin `brandColorHex` instead of hardcoded `Theme.mysqlColor` etc.
15+
- Sidebar labels ("Tables"/"Collections"/"Keys"), toolbar preview labels, and AI prompt language detection now use plugin metadata
16+
- Connection form, database switcher, type picker, file open handler, and toolbar all use plugin lookups for connection mode, authentication, import support, and system database names
1217
- Converted `DatabaseType` from closed enum to string-based struct, enabling future plugin-defined database types
1318
- Moved string literal escaping into plugin drivers via `escapeStringLiteral` on `PluginDatabaseDriver` and `DatabaseDriver` protocols; `SQLEscaping.escapeStringLiteral` now uses ANSI SQL escaping only (doubles single quotes, strips null bytes)
1419
- SQL autocomplete data types and CREATE TABLE options now use plugin-provided dialect data instead of hardcoded per-database switches

Plugins/DuckDBDriverPlugin/DuckDBPlugin.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ final class DuckDBPlugin: NSObject, TableProPlugin, DriverPlugin {
2424
static let requiresAuthentication = false
2525
static let connectionMode: ConnectionMode = .fileBased
2626
static let urlSchemes: [String] = ["duckdb"]
27-
static let fileExtensions: [String] = ["duckdb", "db"]
27+
static let fileExtensions: [String] = ["duckdb", "ddb"]
2828
static let brandColorHex = "#FFD900"
2929
static let supportsDatabaseSwitching = false
3030
static let systemDatabaseNames: [String] = ["information_schema", "pg_catalog"]

Plugins/MongoDBDriverPlugin/MongoDBPlugin.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ final class MongoDBPlugin: NSObject, TableProPlugin, DriverPlugin {
5353
static let supportsForeignKeys = false
5454
static let supportsSchemaEditing = false
5555
static let systemDatabaseNames: [String] = ["admin", "local", "config"]
56+
static let tableEntityName = "Collections"
57+
static let supportsForeignKeyDisable = false
5658
static let databaseGroupingStrategy: GroupingStrategy = .flat
5759
static let columnTypesByCategory: [String: [String]] = [
5860
"String": ["string", "objectId", "regex"],

Plugins/OracleDriverPlugin/OraclePlugin.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ final class OraclePlugin: NSObject, TableProPlugin, DriverPlugin {
2323

2424
// MARK: - UI/Capability Metadata
2525

26+
static let supportsForeignKeyDisable = false
2627
static let brandColorHex = "#C3160B"
2728
static let systemDatabaseNames: [String] = ["SYS", "SYSTEM", "OUTLN", "DBSNMP", "APPQOSSYS", "WMSYS", "XDB"]
2829
static let databaseGroupingStrategy: GroupingStrategy = .bySchema

Plugins/PostgreSQLDriverPlugin/PostgreSQLPlugin.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ final class PostgreSQLPlugin: NSObject, TableProPlugin, DriverPlugin {
4747
"XML": ["XML"]
4848
]
4949

50+
static let supportsCascadeDrop = true
51+
static let supportsForeignKeyDisable = false
52+
5053
static let sqlDialect: SQLDialectDescriptor? = SQLDialectDescriptor(
5154
identifierQuote: "\"",
5255
keywords: [

Plugins/RedisDriverPlugin/RedisPlugin.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ final class RedisPlugin: NSObject, TableProPlugin, DriverPlugin {
4242
static let supportsSchemaEditing = false
4343
static let supportsDatabaseSwitching = false
4444
static let supportsImport = false
45+
static let tableEntityName = "Keys"
46+
static let supportsForeignKeyDisable = false
4547
static let databaseGroupingStrategy: GroupingStrategy = .flat
4648
static let defaultGroupName = "db0"
4749
static let columnTypesByCategory: [String: [String]] = [

Plugins/TableProPluginKit/DriverPlugin.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ public protocol DriverPlugin: TableProPlugin {
3636
static var columnTypesByCategory: [String: [String]] { get }
3737
static var sqlDialect: SQLDialectDescriptor? { get }
3838
static var statementCompletions: [CompletionEntry] { get }
39+
static var tableEntityName: String { get }
40+
static var supportsCascadeDrop: Bool { get }
41+
static var supportsForeignKeyDisable: Bool { get }
3942
}
4043

4144
public extension DriverPlugin {
@@ -76,4 +79,7 @@ public extension DriverPlugin {
7679
}
7780
static var sqlDialect: SQLDialectDescriptor? { nil }
7881
static var statementCompletions: [CompletionEntry] { [] }
82+
static var tableEntityName: String { "Tables" }
83+
static var supportsCascadeDrop: Bool { false }
84+
static var supportsForeignKeyDisable: Bool { true }
7985
}

TablePro/AppDelegate+ConnectionHandler.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ enum QueuedURLEntry {
1616
case databaseURL(URL)
1717
case sqliteFile(URL)
1818
case duckdbFile(URL)
19+
case genericDatabaseFile(URL, DatabaseType)
1920
}
2021

2122
extension AppDelegate {
@@ -172,6 +173,51 @@ extension AppDelegate {
172173
}
173174
}
174175

176+
// MARK: - Generic Database File Handler
177+
178+
func handleGenericDatabaseFile(_ url: URL, type dbType: DatabaseType) {
179+
guard WindowOpener.shared.openWindow != nil else {
180+
queuedURLEntries.append(.genericDatabaseFile(url, dbType))
181+
scheduleQueuedURLProcessing()
182+
return
183+
}
184+
185+
let filePath = url.path(percentEncoded: false)
186+
let connectionName = url.deletingPathExtension().lastPathComponent
187+
188+
for (sessionId, session) in DatabaseManager.shared.activeSessions {
189+
if session.connection.type == dbType
190+
&& session.connection.database == filePath
191+
&& session.driver != nil {
192+
bringConnectionWindowToFront(sessionId)
193+
return
194+
}
195+
}
196+
197+
let connection = DatabaseConnection(
198+
name: connectionName,
199+
host: "",
200+
port: 0,
201+
database: filePath,
202+
username: "",
203+
type: dbType
204+
)
205+
206+
openNewConnectionWindow(for: connection)
207+
208+
Task { @MainActor in
209+
do {
210+
try await DatabaseManager.shared.connectToSession(connection)
211+
for window in NSApp.windows where self.isWelcomeWindow(window) {
212+
window.close()
213+
}
214+
} catch {
215+
connectionLogger.error("File open failed for '\(filePath, privacy: .public)' (\(dbType.rawValue)): \(error.localizedDescription)")
216+
await self.handleConnectionFailure(error)
217+
}
218+
}
219+
}
220+
175221
// MARK: - Unified Queue
176222

177223
func scheduleQueuedURLProcessing() {
@@ -203,6 +249,7 @@ extension AppDelegate {
203249
case .databaseURL(let url): self.handleDatabaseURL(url)
204250
case .sqliteFile(let url): self.handleSQLiteFile(url)
205251
case .duckdbFile(let url): self.handleDuckDBFile(url)
252+
case .genericDatabaseFile(let url, let dbType): self.handleGenericDatabaseFile(url, type: dbType)
206253
}
207254
}
208255
self.scheduleWelcomeWindowSuppression()

TablePro/AppDelegate+FileOpen.swift

Lines changed: 19 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,32 +14,21 @@ private let fileOpenLogger = Logger(subsystem: "com.TablePro", category: "FileOp
1414
extension AppDelegate {
1515
// MARK: - URL Classification
1616

17-
private static let databaseURLSchemes: Set<String> = [
18-
"postgresql", "postgres", "mysql", "mariadb", "sqlite",
19-
"mongodb", "mongodb+srv", "redis", "rediss", "redshift",
20-
"mssql", "sqlserver", "oracle", "duckdb"
21-
]
22-
23-
static let sqliteFileExtensions: Set<String> = [
24-
"sqlite", "sqlite3", "db3", "s3db", "sl3", "sqlitedb"
25-
]
26-
27-
static let duckdbFileExtensions: Set<String> = ["duckdb", "ddb"]
28-
2917
private func isDatabaseURL(_ url: URL) -> Bool {
3018
guard let scheme = url.scheme?.lowercased() else { return false }
3119
let base = scheme
3220
.replacingOccurrences(of: "+ssh", with: "")
3321
.replacingOccurrences(of: "+srv", with: "")
34-
return Self.databaseURLSchemes.contains(base) || Self.databaseURLSchemes.contains(scheme)
22+
let registeredSchemes = PluginManager.shared.allRegisteredURLSchemes
23+
return registeredSchemes.contains(base) || registeredSchemes.contains(scheme)
3524
}
3625

37-
private func isSQLiteFile(_ url: URL) -> Bool {
38-
Self.sqliteFileExtensions.contains(url.pathExtension.lowercased())
26+
private func isDatabaseFile(_ url: URL) -> Bool {
27+
PluginManager.shared.allRegisteredFileExtensions[url.pathExtension.lowercased()] != nil
3928
}
4029

41-
private func isDuckDBFile(_ url: URL) -> Bool {
42-
Self.duckdbFileExtensions.contains(url.pathExtension.lowercased())
30+
private func databaseTypeForFile(_ url: URL) -> DatabaseType? {
31+
PluginManager.shared.allRegisteredFileExtensions[url.pathExtension.lowercased()]
4332
}
4433

4534
// MARK: - Main Dispatch
@@ -68,20 +57,21 @@ extension AppDelegate {
6857
}
6958
}
7059

71-
let sqliteFiles = urls.filter { isSQLiteFile($0) }
72-
if !sqliteFiles.isEmpty {
73-
suppressWelcomeWindow()
74-
Task { @MainActor in
75-
for url in sqliteFiles { self.handleSQLiteFile(url) }
76-
self.scheduleWelcomeWindowSuppression()
77-
}
78-
}
79-
80-
let duckdbFiles = urls.filter { isDuckDBFile($0) }
81-
if !duckdbFiles.isEmpty {
60+
let databaseFiles = urls.filter { isDatabaseFile($0) }
61+
if !databaseFiles.isEmpty {
8262
suppressWelcomeWindow()
8363
Task { @MainActor in
84-
for url in duckdbFiles { self.handleDuckDBFile(url) }
64+
for url in databaseFiles {
65+
guard let dbType = self.databaseTypeForFile(url) else { continue }
66+
switch dbType {
67+
case .sqlite:
68+
self.handleSQLiteFile(url)
69+
case .duckdb:
70+
self.handleDuckDBFile(url)
71+
default:
72+
self.handleGenericDatabaseFile(url, type: dbType)
73+
}
74+
}
8575
self.scheduleWelcomeWindowSuppression()
8676
}
8777
}

TablePro/ContentView.swift

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import AppKit
99
import os
1010
import SwiftUI
11+
import TableProPluginKit
1112

1213
struct ContentView: View {
1314
private static let logger = Logger(subsystem: "com.TablePro", category: "ContentView")
@@ -40,11 +41,8 @@ struct ContentView: View {
4041
defaultTitle = tableName
4142
} else if let connectionId = payload?.connectionId,
4243
let connection = ConnectionStorage.shared.loadConnections().first(where: { $0.id == connectionId }) {
43-
switch connection.type {
44-
case .mongodb: defaultTitle = "MQL Query"
45-
case .redis: defaultTitle = "Redis CLI"
46-
default: defaultTitle = "SQL Query"
47-
}
44+
let langName = PluginManager.shared.queryLanguageName(for: connection.type)
45+
defaultTitle = "\(langName) Query"
4846
} else {
4947
defaultTitle = "SQL Query"
5048
}
@@ -94,8 +92,10 @@ struct ContentView: View {
9492
}
9593
AppState.shared.isConnected = true
9694
AppState.shared.safeModeLevel = session.connection.safeModeLevel
97-
AppState.shared.isMongoDB = session.connection.type == .mongodb
98-
AppState.shared.isRedis = session.connection.type == .redis
95+
AppState.shared.editorLanguage = PluginManager.shared.editorLanguage(for: session.connection.type)
96+
AppState.shared.currentDatabaseType = session.connection.type
97+
AppState.shared.supportsDatabaseSwitching = PluginManager.shared.supportsDatabaseSwitching(
98+
for: session.connection.type)
9999
}
100100
} else {
101101
currentSession = nil
@@ -119,8 +119,9 @@ struct ContentView: View {
119119
columnVisibility = .detailOnly
120120
AppState.shared.isConnected = false
121121
AppState.shared.safeModeLevel = .silent
122-
AppState.shared.isMongoDB = false
123-
AppState.shared.isRedis = false
122+
AppState.shared.editorLanguage = .sql
123+
AppState.shared.currentDatabaseType = nil
124+
AppState.shared.supportsDatabaseSwitching = true
124125

125126
// Close all native tab windows for this connection and
126127
// force AppKit to deallocate them instead of pooling.
@@ -150,8 +151,10 @@ struct ContentView: View {
150151
}
151152
AppState.shared.isConnected = true
152153
AppState.shared.safeModeLevel = newSession.connection.safeModeLevel
153-
AppState.shared.isMongoDB = newSession.connection.type == .mongodb
154-
AppState.shared.isRedis = newSession.connection.type == .redis
154+
AppState.shared.editorLanguage = PluginManager.shared.editorLanguage(for: newSession.connection.type)
155+
AppState.shared.currentDatabaseType = newSession.connection.type
156+
AppState.shared.supportsDatabaseSwitching = PluginManager.shared.supportsDatabaseSwitching(
157+
for: newSession.connection.type)
155158
}
156159
.onReceive(NotificationCenter.default.publisher(for: NSWindow.didBecomeKeyNotification)) { notification in
157160
// Only process notifications for our own window to avoid every
@@ -178,13 +181,16 @@ struct ContentView: View {
178181
if let session = DatabaseManager.shared.activeSessions[connectionId] {
179182
AppState.shared.isConnected = true
180183
AppState.shared.safeModeLevel = session.connection.safeModeLevel
181-
AppState.shared.isMongoDB = session.connection.type == .mongodb
182-
AppState.shared.isRedis = session.connection.type == .redis
184+
AppState.shared.editorLanguage = PluginManager.shared.editorLanguage(for: session.connection.type)
185+
AppState.shared.currentDatabaseType = session.connection.type
186+
AppState.shared.supportsDatabaseSwitching = PluginManager.shared.supportsDatabaseSwitching(
187+
for: session.connection.type)
183188
} else {
184189
AppState.shared.isConnected = false
185190
AppState.shared.safeModeLevel = .silent
186-
AppState.shared.isMongoDB = false
187-
AppState.shared.isRedis = false
191+
AppState.shared.editorLanguage = .sql
192+
AppState.shared.currentDatabaseType = nil
193+
AppState.shared.supportsDatabaseSwitching = true
188194
}
189195
}
190196
}

0 commit comments

Comments
 (0)