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
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

### Changed

- Converted `DatabaseType` from closed enum to string-based struct, enabling future plugin-defined database types
- 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)
- SQL autocomplete data types and CREATE TABLE options now use plugin-provided dialect data instead of hardcoded per-database switches
- `FilterSQLGenerator` now uses `SQLDialectDescriptor` data (regex syntax, boolean literals, LIKE escape style, pagination style) instead of `DatabaseType` switch statements
Expand Down
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ When adding a new driver: create a new plugin bundle under `Plugins/`, implement

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

### DatabaseType (String-Based Struct)

`DatabaseType` is a string-based struct (not an enum). Key rules:
- All `switch` statements on `DatabaseType` must include `default:` — the type is open
- Use static constants (`.mysql`, `.postgresql`) for known types
- Unknown types (from future plugins) are valid — they round-trip through Codable
- Use `DatabaseType.allKnownTypes` (not `allCases`) for the canonical list of built-in types

### Editor Architecture (CodeEditSourceEditor)

- **`SQLEditorTheme`** — single source of truth for editor colors/fonts
Expand Down
9 changes: 3 additions & 6 deletions TablePro/Core/ChangeTracking/SQLStatementGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,10 @@ struct SQLStatementGenerator {
self.quoteIdentifierFn = quoteIdentifier ?? quoteIdentifierFromDialect(dialect)
}

private static let dollarStyleTypes: Set<DatabaseType> = [.postgresql, .redshift, .duckdb]

private static func defaultParameterStyle(for databaseType: DatabaseType) -> ParameterStyle {
switch databaseType {
case .postgresql, .redshift, .duckdb:
return .dollar
case .mysql, .mariadb, .sqlite, .mongodb, .redis, .mssql, .oracle, .clickhouse:
return .questionMark
}
dollarStyleTypes.contains(databaseType) ? .dollar : .questionMark
}

// MARK: - Public API
Expand Down
23 changes: 7 additions & 16 deletions TablePro/Core/Database/DatabaseDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -303,28 +303,19 @@ extension DatabaseDriver {
guard seconds > 0 else { return }
let ms = seconds * 1_000
do {
switch connection.type {
case .mysql:
let type = connection.type
if type == .mysql {
_ = try await execute(query: "SET SESSION max_execution_time = \(ms)")
case .mariadb:
} else if type == .mariadb {
_ = try await execute(query: "SET SESSION max_statement_time = \(seconds)")
case .postgresql, .redshift:
} else if type == .postgresql || type == .redshift {
_ = try await execute(query: "SET statement_timeout = '\(ms)'")
case .sqlite:
break // SQLite busy_timeout handled by driver directly
case .duckdb:
break
case .mongodb:
break // MongoDB timeout handled per-operation by MongoDBDriver
case .redis:
break // Redis does not support session-level query timeouts
case .mssql:
} else if type == .mssql {
_ = try await execute(query: "SET LOCK_TIMEOUT \(ms)")
case .oracle:
break // Oracle timeout handled per-statement by OracleDriver
case .clickhouse:
} else if type == .clickhouse {
_ = try await execute(query: "SET max_execution_time = \(seconds)")
}
// sqlite, duckdb, mongodb, redis, oracle: no session-level timeout
} catch {
Logger(subsystem: "com.TablePro", category: "DatabaseDriver")
.warning("Failed to set query timeout: \(error.localizedDescription)")
Expand Down
16 changes: 16 additions & 0 deletions TablePro/Core/Plugins/PluginManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,22 @@ final class PluginManager {
}
}

// MARK: - Available Database Types

/// All database types with loaded plugins, ordered by display name.
var availableDatabaseTypes: [DatabaseType] {
var types: [DatabaseType] = []
for entry in plugins where entry.isEnabled {
if let typeId = entry.databaseTypeId {
types.append(DatabaseType(rawValue: typeId))
}
for additionalId in entry.additionalTypeIds {
types.append(DatabaseType(rawValue: additionalId))
}
}
return types.sorted { $0.rawValue < $1.rawValue }
}

// MARK: - Driver Availability

func isDriverAvailable(for databaseType: DatabaseType) -> Bool {
Expand Down
4 changes: 2 additions & 2 deletions TablePro/Core/Services/Infrastructure/DeeplinkHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ enum DeeplinkHandler {
guard let name = value("name"), !name.isEmpty,
let host = value("host"), !host.isEmpty,
let typeStr = value("type"),
let dbType = DatabaseType(rawValue: typeStr)
?? DatabaseType.allCases.first(where: {
let dbType = DatabaseType(validating: typeStr)
?? DatabaseType.allKnownTypes.first(where: {
$0.rawValue.lowercased() == typeStr.lowercased()
})
else {
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Core/Storage/ConnectionStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ private struct StoredConnection: Codable {
port: port,
database: database,
username: username,
type: DatabaseType(rawValue: type) ?? .mysql,
type: DatabaseType(rawValue: type),
sshConfig: sshConfig,
sslConfig: sslConfig,
color: parsedColor,
Expand Down
28 changes: 15 additions & 13 deletions TablePro/Core/Utilities/Connection/ConnectionURLFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,22 @@ struct ConnectionURLFormatter {

// MARK: - Private

private static let urlSchemeMap: [DatabaseType: String] = [
.mysql: "mysql",
.mariadb: "mariadb",
.postgresql: "postgresql",
.redshift: "redshift",
.sqlite: "sqlite",
.mongodb: "mongodb",
.redis: "redis",
.mssql: "sqlserver",
.oracle: "oracle",
.clickhouse: "clickhouse",
.duckdb: "duckdb",
]

private static func urlScheme(for type: DatabaseType) -> String {
switch type {
case .mysql: return "mysql"
case .mariadb: return "mariadb"
case .postgresql: return "postgresql"
case .redshift: return "redshift"
case .sqlite: return "sqlite"
case .mongodb: return "mongodb"
case .redis: return "redis"
case .mssql: return "sqlserver"
case .oracle: return "oracle"
case .clickhouse: return "clickhouse"
case .duckdb: return "duckdb"
}
urlSchemeMap[type] ?? type.rawValue.lowercased()
}

private static func formatSQLite(_ database: String) -> String {
Expand Down
7 changes: 4 additions & 3 deletions TablePro/Core/Utilities/SQL/SQLParameterInliner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ struct SQLParameterInliner {
/// - statement: The parameterized statement containing SQL with placeholders and bound values.
/// - databaseType: The database type, which determines placeholder style (`?` vs `$N`).
/// - Returns: A SQL string with placeholders replaced by formatted literal values.
private static let dollarPlaceholderTypes: Set<DatabaseType> = [.postgresql, .redshift, .duckdb]

static func inline(_ statement: ParameterizedStatement, databaseType: DatabaseType) -> String {
switch databaseType {
case .postgresql, .redshift, .duckdb:
if dollarPlaceholderTypes.contains(databaseType) {
return inlineDollarPlaceholders(statement.sql, parameters: statement.parameters)
case .mysql, .mariadb, .sqlite, .mongodb, .redis, .mssql, .oracle, .clickhouse:
} else {
return inlineQuestionMarkPlaceholders(statement.sql, parameters: statement.parameters)
}
}
Expand Down
Loading
Loading