Skip to content

Commit cd9d6ff

Browse files
committed
feat: replace hardcoded DatabaseType switches with dynamic plugin metadata lookups (#305)
1 parent 23cdac8 commit cd9d6ff

34 files changed

+409
-358
lines changed

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/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+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+
break
73+
}
74+
}
8575
self.scheduleWelcomeWindowSuppression()
8676
}
8777
}

TablePro/ContentView.swift

Lines changed: 16 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 == "SQL" ? "SQL Query" : langName == "MQL" ? "MQL Query" : langName
4846
} else {
4947
defaultTitle = "SQL Query"
5048
}
@@ -94,8 +92,9 @@ 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.supportsDatabaseSwitching = PluginManager.shared.supportsDatabaseSwitching(
97+
for: session.connection.type)
9998
}
10099
} else {
101100
currentSession = nil
@@ -119,8 +118,8 @@ struct ContentView: View {
119118
columnVisibility = .detailOnly
120119
AppState.shared.isConnected = false
121120
AppState.shared.safeModeLevel = .silent
122-
AppState.shared.isMongoDB = false
123-
AppState.shared.isRedis = false
121+
AppState.shared.editorLanguage = .sql
122+
AppState.shared.supportsDatabaseSwitching = true
124123

125124
// Close all native tab windows for this connection and
126125
// force AppKit to deallocate them instead of pooling.
@@ -150,8 +149,9 @@ struct ContentView: View {
150149
}
151150
AppState.shared.isConnected = true
152151
AppState.shared.safeModeLevel = newSession.connection.safeModeLevel
153-
AppState.shared.isMongoDB = newSession.connection.type == .mongodb
154-
AppState.shared.isRedis = newSession.connection.type == .redis
152+
AppState.shared.editorLanguage = PluginManager.shared.editorLanguage(for: newSession.connection.type)
153+
AppState.shared.supportsDatabaseSwitching = PluginManager.shared.supportsDatabaseSwitching(
154+
for: newSession.connection.type)
155155
}
156156
.onReceive(NotificationCenter.default.publisher(for: NSWindow.didBecomeKeyNotification)) { notification in
157157
// Only process notifications for our own window to avoid every
@@ -178,13 +178,14 @@ struct ContentView: View {
178178
if let session = DatabaseManager.shared.activeSessions[connectionId] {
179179
AppState.shared.isConnected = true
180180
AppState.shared.safeModeLevel = session.connection.safeModeLevel
181-
AppState.shared.isMongoDB = session.connection.type == .mongodb
182-
AppState.shared.isRedis = session.connection.type == .redis
181+
AppState.shared.editorLanguage = PluginManager.shared.editorLanguage(for: session.connection.type)
182+
AppState.shared.supportsDatabaseSwitching = PluginManager.shared.supportsDatabaseSwitching(
183+
for: session.connection.type)
183184
} else {
184185
AppState.shared.isConnected = false
185186
AppState.shared.safeModeLevel = .silent
186-
AppState.shared.isMongoDB = false
187-
AppState.shared.isRedis = false
187+
AppState.shared.editorLanguage = .sql
188+
AppState.shared.supportsDatabaseSwitching = true
188189
}
189190
}
190191
}

TablePro/Core/AI/AIPromptTemplates.swift

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,26 @@ import Foundation
1010
/// Centralized prompt templates for AI-powered editor features
1111
enum AIPromptTemplates {
1212
/// Build a prompt asking AI to explain a query
13-
static func explainQuery(_ query: String, databaseType: DatabaseType = .mysql) -> String {
13+
@MainActor static func explainQuery(_ query: String, databaseType: DatabaseType = .mysql) -> String {
1414
let (typeName, lang) = queryInfo(for: databaseType)
1515
return "Explain this \(typeName):\n\n```\(lang)\n\(query)\n```"
1616
}
1717

1818
/// Build a prompt asking AI to optimize a query
19-
static func optimizeQuery(_ query: String, databaseType: DatabaseType = .mysql) -> String {
19+
@MainActor static func optimizeQuery(_ query: String, databaseType: DatabaseType = .mysql) -> String {
2020
let (typeName, lang) = queryInfo(for: databaseType)
2121
return "Optimize this \(typeName) for better performance:\n\n```\(lang)\n\(query)\n```"
2222
}
2323

2424
/// Build a prompt asking AI to fix a query that produced an error
25-
static func fixError(query: String, error: String, databaseType: DatabaseType = .mysql) -> String {
25+
@MainActor static func fixError(query: String, error: String, databaseType: DatabaseType = .mysql) -> String {
2626
let (typeName, lang) = queryInfo(for: databaseType)
2727
return "This \(typeName) failed with an error. Please fix it.\n\nQuery:\n```\(lang)\n\(query)\n```\n\nError: \(error)"
2828
}
2929

30-
private static func queryInfo(for databaseType: DatabaseType) -> (typeName: String, language: String) {
31-
switch databaseType {
32-
case .mongodb:
33-
return ("MongoDB query", "javascript")
34-
case .redis:
35-
return ("Redis command", "bash")
36-
default:
37-
return ("SQL query", "sql")
38-
}
30+
@MainActor private static func queryInfo(for databaseType: DatabaseType) -> (typeName: String, language: String) {
31+
let langName = PluginManager.shared.queryLanguageName(for: databaseType)
32+
let lang = PluginManager.shared.editorLanguage(for: databaseType).codeBlockTag
33+
return ("\(langName) query", lang)
3934
}
4035
}

TablePro/Core/AI/AISchemaContext.swift

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import Foundation
99
import os
10+
import TableProPluginKit
1011

1112
/// Builds schema context for AI system prompts
1213
struct AISchemaContext {
@@ -18,7 +19,7 @@ struct AISchemaContext {
1819
// MARK: - Public
1920

2021
/// Build a system prompt including database context
21-
static func buildSystemPrompt(
22+
@MainActor static func buildSystemPrompt(
2223
databaseType: DatabaseType,
2324
databaseName: String,
2425
tables: [TableInfo],
@@ -55,12 +56,7 @@ struct AISchemaContext {
5556
if settings.includeCurrentQuery,
5657
let query = currentQuery,
5758
!query.isEmpty {
58-
let lang: String
59-
switch databaseType {
60-
case .mongodb: lang = "javascript"
61-
case .redis: lang = "bash"
62-
default: lang = "sql"
63-
}
59+
let lang = PluginManager.shared.editorLanguage(for: databaseType).codeBlockTag
6460
parts.append("\n## Current Query\n```\(lang)\n\(query)\n```")
6561
}
6662

@@ -70,21 +66,12 @@ struct AISchemaContext {
7066
parts.append("\n## Recent Query Results\n\(results)")
7167
}
7268

73-
if databaseType == .mongodb {
74-
parts.append(
75-
"\nProvide MongoDB shell queries using `javascript` fenced code blocks."
76-
)
77-
parts.append(
78-
"Use MongoDB shell syntax (db.collection.find(), etc.), not SQL."
79-
)
80-
} else if databaseType == .redis {
81-
parts.append(
82-
"\nProvide Redis commands using `bash` fenced code blocks."
83-
)
84-
parts.append(
85-
"Use Redis CLI syntax (GET, SET, HGETALL, etc.), not SQL."
86-
)
87-
} else {
69+
let editorLang = PluginManager.shared.editorLanguage(for: databaseType)
70+
let langName = PluginManager.shared.queryLanguageName(for: databaseType)
71+
let langTag = editorLang.codeBlockTag
72+
73+
switch editorLang {
74+
case .sql:
8875
parts.append(
8976
"\nProvide SQL queries appropriate for"
9077
+ " \(databaseType.rawValue) syntax when applicable."
@@ -93,6 +80,13 @@ struct AISchemaContext {
9380
"When writing SQL, use the correct identifier quoting"
9481
+ " for \(databaseType.rawValue)."
9582
)
83+
default:
84+
parts.append(
85+
"\nProvide \(langName) queries using `\(langTag)` fenced code blocks."
86+
)
87+
parts.append(
88+
"Use \(langName) syntax, not SQL."
89+
)
9690
}
9791

9892
return parts.joined(separator: "\n")

0 commit comments

Comments
 (0)