Skip to content

Commit 2332b31

Browse files
committed
feat: convert DatabaseType from enum to string-based struct
1 parent 6c3da7d commit 2332b31

19 files changed

Lines changed: 361 additions & 331 deletions

CHANGELOG.md

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

1010
### Changed
1111

12+
- Converted `DatabaseType` from closed enum to string-based struct, enabling future plugin-defined database types
1213
- 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)
1314
- SQL autocomplete data types and CREATE TABLE options now use plugin-provided dialect data instead of hardcoded per-database switches
1415
- `FilterSQLGenerator` now uses `SQLDialectDescriptor` data (regex syntax, boolean literals, LIKE escape style, pagination style) instead of `DatabaseType` switch statements

CLAUDE.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ When adding a new driver: create a new plugin bundle under `Plugins/`, implement
7070

7171
When adding a new method to the driver protocol: add to `PluginDatabaseDriver` (with default implementation), then update `PluginDriverAdapter` to bridge it to `DatabaseDriver`.
7272

73+
### DatabaseType (String-Based Struct)
74+
75+
`DatabaseType` is a string-based struct (not an enum). Key rules:
76+
- All `switch` statements on `DatabaseType` must include `default:` — the type is open
77+
- Use static constants (`.mysql`, `.postgresql`) for known types
78+
- Unknown types (from future plugins) are valid — they round-trip through Codable
79+
- Use `DatabaseType.allKnownTypes` (not `allCases`) for the canonical list of built-in types
80+
7381
### Editor Architecture (CodeEditSourceEditor)
7482

7583
- **`SQLEditorTheme`** — single source of truth for editor colors/fonts

TablePro/Core/ChangeTracking/SQLStatementGenerator.swift

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,10 @@ struct SQLStatementGenerator {
4444
self.quoteIdentifierFn = quoteIdentifier ?? quoteIdentifierFromDialect(dialect)
4545
}
4646

47+
private static let dollarStyleTypes: Set<DatabaseType> = [.postgresql, .redshift, .duckdb]
48+
4749
private static func defaultParameterStyle(for databaseType: DatabaseType) -> ParameterStyle {
48-
switch databaseType {
49-
case .postgresql, .redshift, .duckdb:
50-
return .dollar
51-
case .mysql, .mariadb, .sqlite, .mongodb, .redis, .mssql, .oracle, .clickhouse:
52-
return .questionMark
53-
}
50+
dollarStyleTypes.contains(databaseType) ? .dollar : .questionMark
5451
}
5552

5653
// MARK: - Public API

TablePro/Core/Database/DatabaseDriver.swift

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -303,28 +303,19 @@ extension DatabaseDriver {
303303
guard seconds > 0 else { return }
304304
let ms = seconds * 1_000
305305
do {
306-
switch connection.type {
307-
case .mysql:
306+
let type = connection.type
307+
if type == .mysql {
308308
_ = try await execute(query: "SET SESSION max_execution_time = \(ms)")
309-
case .mariadb:
309+
} else if type == .mariadb {
310310
_ = try await execute(query: "SET SESSION max_statement_time = \(seconds)")
311-
case .postgresql, .redshift:
311+
} else if type == .postgresql || type == .redshift {
312312
_ = try await execute(query: "SET statement_timeout = '\(ms)'")
313-
case .sqlite:
314-
break // SQLite busy_timeout handled by driver directly
315-
case .duckdb:
316-
break
317-
case .mongodb:
318-
break // MongoDB timeout handled per-operation by MongoDBDriver
319-
case .redis:
320-
break // Redis does not support session-level query timeouts
321-
case .mssql:
313+
} else if type == .mssql {
322314
_ = try await execute(query: "SET LOCK_TIMEOUT \(ms)")
323-
case .oracle:
324-
break // Oracle timeout handled per-statement by OracleDriver
325-
case .clickhouse:
315+
} else if type == .clickhouse {
326316
_ = try await execute(query: "SET max_execution_time = \(seconds)")
327317
}
318+
// sqlite, duckdb, mongodb, redis, oracle: no session-level timeout
328319
} catch {
329320
Logger(subsystem: "com.TablePro", category: "DatabaseDriver")
330321
.warning("Failed to set query timeout: \(error.localizedDescription)")

TablePro/Core/Plugins/PluginManager.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,22 @@ final class PluginManager {
319319
}
320320
}
321321

322+
// MARK: - Available Database Types
323+
324+
/// All database types with loaded plugins, ordered by display name.
325+
var availableDatabaseTypes: [DatabaseType] {
326+
var types: [DatabaseType] = []
327+
for entry in plugins {
328+
if let typeId = entry.databaseTypeId {
329+
types.append(DatabaseType(rawValue: typeId))
330+
}
331+
for additionalId in entry.additionalTypeIds {
332+
types.append(DatabaseType(rawValue: additionalId))
333+
}
334+
}
335+
return types.sorted { $0.rawValue < $1.rawValue }
336+
}
337+
322338
// MARK: - Driver Availability
323339

324340
func isDriverAvailable(for databaseType: DatabaseType) -> Bool {

TablePro/Core/Services/Infrastructure/DeeplinkHandler.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ enum DeeplinkHandler {
8787
guard let name = value("name"), !name.isEmpty,
8888
let host = value("host"), !host.isEmpty,
8989
let typeStr = value("type"),
90-
let dbType = DatabaseType(rawValue: typeStr)
91-
?? DatabaseType.allCases.first(where: {
90+
let dbType = DatabaseType(validating: typeStr)
91+
?? DatabaseType.allKnownTypes.first(where: {
9292
$0.rawValue.lowercased() == typeStr.lowercased()
9393
})
9494
else {

TablePro/Core/Storage/ConnectionStorage.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,7 @@ private struct StoredConnection: Codable {
586586
port: port,
587587
database: database,
588588
username: username,
589-
type: DatabaseType(rawValue: type) ?? .mysql,
589+
type: DatabaseType(rawValue: type),
590590
sshConfig: sshConfig,
591591
sslConfig: sslConfig,
592592
color: parsedColor,

TablePro/Core/Utilities/Connection/ConnectionURLFormatter.swift

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,22 @@ struct ConnectionURLFormatter {
2626

2727
// MARK: - Private
2828

29+
private static let urlSchemeMap: [DatabaseType: String] = [
30+
.mysql: "mysql",
31+
.mariadb: "mariadb",
32+
.postgresql: "postgresql",
33+
.redshift: "redshift",
34+
.sqlite: "sqlite",
35+
.mongodb: "mongodb",
36+
.redis: "redis",
37+
.mssql: "sqlserver",
38+
.oracle: "oracle",
39+
.clickhouse: "clickhouse",
40+
.duckdb: "duckdb",
41+
]
42+
2943
private static func urlScheme(for type: DatabaseType) -> String {
30-
switch type {
31-
case .mysql: return "mysql"
32-
case .mariadb: return "mariadb"
33-
case .postgresql: return "postgresql"
34-
case .redshift: return "redshift"
35-
case .sqlite: return "sqlite"
36-
case .mongodb: return "mongodb"
37-
case .redis: return "redis"
38-
case .mssql: return "sqlserver"
39-
case .oracle: return "oracle"
40-
case .clickhouse: return "clickhouse"
41-
case .duckdb: return "duckdb"
42-
}
44+
urlSchemeMap[type] ?? type.rawValue.lowercased()
4345
}
4446

4547
private static func formatSQLite(_ database: String) -> String {

TablePro/Core/Utilities/SQL/SQLParameterInliner.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@ struct SQLParameterInliner {
1717
/// - statement: The parameterized statement containing SQL with placeholders and bound values.
1818
/// - databaseType: The database type, which determines placeholder style (`?` vs `$N`).
1919
/// - Returns: A SQL string with placeholders replaced by formatted literal values.
20+
private static let dollarPlaceholderTypes: Set<DatabaseType> = [.postgresql, .redshift, .duckdb]
21+
2022
static func inline(_ statement: ParameterizedStatement, databaseType: DatabaseType) -> String {
21-
switch databaseType {
22-
case .postgresql, .redshift, .duckdb:
23+
if dollarPlaceholderTypes.contains(databaseType) {
2324
return inlineDollarPlaceholders(statement.sql, parameters: statement.parameters)
24-
case .mysql, .mariadb, .sqlite, .mongodb, .redis, .mssql, .oracle, .clickhouse:
25+
} else {
2526
return inlineQuestionMarkPlaceholders(statement.sql, parameters: statement.parameters)
2627
}
2728
}

0 commit comments

Comments
 (0)