Skip to content

Commit 1b2d43a

Browse files
authored
Merge pull request #353 from datlechin/fix/table-ops-and-mysql-fk-after-db-switch
fix: add SQL fallbacks for DROP/TRUNCATE and fix MySQL FK metadata after db switch
2 parents 3d82787 + 1ae79ef commit 1b2d43a

5 files changed

Lines changed: 182 additions & 34 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2525
- Connection test not cleaning up SSH tunnel on completion
2626
- Test connection success indicator not resetting after field changes
2727
- SSH port field accepting invalid values
28+
- DROP TABLE and TRUNCATE TABLE sidebar operations producing no SQL for plugin-based drivers
29+
- Foreign key navigation arrows not appearing after switching databases with Cmd+K on MySQL
2830

2931
## [0.19.1] - 2026-03-16
3032

Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ final class MySQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
1313
private let config: DriverConnectionConfig
1414
private var mariadbConnection: MariaDBPluginConnection?
1515
private var _serverVersion: String?
16+
private var _activeDatabase: String
1617

1718
/// Detected server type from version string after connecting
1819
private var isMariaDB = false
@@ -49,6 +50,7 @@ final class MySQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
4950

5051
init(config: DriverConnectionConfig) {
5152
self.config = config
53+
self._activeDatabase = config.database
5254
}
5355

5456
// MARK: - Connection
@@ -61,7 +63,7 @@ final class MySQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
6163
port: config.port,
6264
user: config.username,
6365
password: config.password,
64-
database: config.database,
66+
database: _activeDatabase,
6567
sslConfig: sslConfig
6668
)
6769

@@ -223,7 +225,7 @@ final class MySQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
223225
}
224226

225227
func fetchAllColumns(schema: String?) async throws -> [String: [PluginColumnInfo]] {
226-
let dbName = config.database
228+
let dbName = _activeDatabase
227229
let escapedDb = dbName.replacingOccurrences(of: "'", with: "''")
228230
let query = """
229231
SELECT
@@ -310,7 +312,7 @@ final class MySQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
310312
}
311313

312314
func fetchForeignKeys(table: String, schema: String?) async throws -> [PluginForeignKeyInfo] {
313-
let dbName = config.database
315+
let dbName = _activeDatabase
314316
let escapedDb = dbName.replacingOccurrences(of: "'", with: "''")
315317
let escapedTable = table.replacingOccurrences(of: "'", with: "''")
316318

@@ -351,7 +353,7 @@ final class MySQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
351353
}
352354

353355
func fetchAllForeignKeys(schema: String?) async throws -> [String: [PluginForeignKeyInfo]] {
354-
let dbName = config.database
356+
let dbName = _activeDatabase
355357
let escapedDb = dbName.replacingOccurrences(of: "'", with: "''")
356358

357359
let query = """
@@ -394,7 +396,7 @@ final class MySQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
394396
}
395397

396398
func fetchApproximateRowCount(table: String, schema: String?) async throws -> Int? {
397-
let dbName = config.database
399+
let dbName = _activeDatabase
398400
let escapedDb = dbName.replacingOccurrences(of: "'", with: "''")
399401
let escapedTable = table.replacingOccurrences(of: "'", with: "''")
400402

@@ -578,6 +580,7 @@ final class MySQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
578580
func switchDatabase(to database: String) async throws {
579581
let escaped = database.replacingOccurrences(of: "`", with: "``")
580582
_ = try await execute(query: "USE `\(escaped)`")
583+
_activeDatabase = database
581584
}
582585

583586
// MARK: - Query Timeout

TablePro/Core/Plugins/PluginDriverAdapter.swift

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -334,12 +334,22 @@ final class PluginDriverAdapter: DatabaseDriver, SchemaSwitchable {
334334

335335
// MARK: - Table Operations
336336

337-
func truncateTableStatements(table: String, schema: String?, cascade: Bool) -> [String]? {
338-
pluginDriver.truncateTableStatements(table: table, schema: schema, cascade: cascade)
337+
func truncateTableStatements(table: String, schema: String?, cascade: Bool) -> [String] {
338+
if let stmts = pluginDriver.truncateTableStatements(table: table, schema: schema, cascade: cascade) {
339+
return stmts
340+
}
341+
let name = qualifiedName(table, schema: schema)
342+
let cascadeSuffix = cascade ? " CASCADE" : ""
343+
return ["TRUNCATE TABLE \(name)\(cascadeSuffix)"]
339344
}
340345

341-
func dropObjectStatement(name: String, objectType: String, schema: String?, cascade: Bool) -> String? {
342-
pluginDriver.dropObjectStatement(name: name, objectType: objectType, schema: schema, cascade: cascade)
346+
func dropObjectStatement(name: String, objectType: String, schema: String?, cascade: Bool) -> String {
347+
if let stmt = pluginDriver.dropObjectStatement(name: name, objectType: objectType, schema: schema, cascade: cascade) {
348+
return stmt
349+
}
350+
let qualName = qualifiedName(name, schema: schema)
351+
let cascadeSuffix = cascade ? " CASCADE" : ""
352+
return "DROP \(objectType) \(qualName)\(cascadeSuffix)"
343353
}
344354

345355
func foreignKeyDisableStatements() -> [String]? {
@@ -386,6 +396,14 @@ final class PluginDriverAdapter: DatabaseDriver, SchemaSwitchable {
386396
pluginDriver.escapeStringLiteral(value)
387397
}
388398

399+
// MARK: - Private Helpers
400+
401+
private func qualifiedName(_ name: String, schema: String?) -> String {
402+
let quoted = pluginDriver.quoteIdentifier(name)
403+
guard let schema, !schema.isEmpty else { return quoted }
404+
return "\(pluginDriver.quoteIdentifier(schema)).\(quoted)"
405+
}
406+
389407
// MARK: - Result Mapping
390408

391409
private func mapQueryResult(_ pluginResult: PluginQueryResult) -> QueryResult {

TablePro/Views/Main/Extensions/MainContentCoordinator+TableOperations.swift

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,6 @@ extension MainContentCoordinator {
3232
) -> [String] {
3333
var statements: [String] = []
3434
let dbType = connection.type
35-
let driver = DatabaseManager.shared.driver(for: connectionId)
36-
let quote: (String) -> String = driver?.quoteIdentifier
37-
?? quoteIdentifierFromDialect(PluginManager.shared.sqlDialect(for: dbType))
3835

3936
// Sort tables for consistent execution order
4037
let sortedTruncates = truncates.sorted()
@@ -50,10 +47,9 @@ extension MainContentCoordinator {
5047
}
5148

5249
for tableName in sortedTruncates {
53-
let quotedName = quote(tableName)
5450
let tableOptions = options[tableName] ?? TableOperationOptions()
5551
statements.append(contentsOf: truncateStatements(
56-
tableName: tableName, quotedName: quotedName, options: tableOptions, dbType: dbType
52+
tableName: tableName, options: tableOptions
5753
))
5854
}
5955

@@ -63,11 +59,10 @@ extension MainContentCoordinator {
6359
}()
6460

6561
for tableName in sortedDeletes {
66-
let quotedName = quote(tableName)
6762
let tableOptions = options[tableName] ?? TableOperationOptions()
6863
let stmt = dropTableStatement(
69-
tableName: tableName, quotedName: quotedName,
70-
isView: viewNames.contains(tableName), options: tableOptions, dbType: dbType
64+
tableName: tableName,
65+
isView: viewNames.contains(tableName), options: tableOptions
7166
)
7267
if !stmt.isEmpty {
7368
statements.append(stmt)
@@ -103,28 +98,21 @@ extension MainContentCoordinator {
10398
// MARK: - Private SQL Builders
10499

105100
private func truncateStatements(
106-
tableName: String, quotedName: String, options: TableOperationOptions, dbType: DatabaseType
101+
tableName: String, options: TableOperationOptions
107102
) -> [String] {
108-
guard let adapter = currentPluginDriverAdapter,
109-
let stmts = adapter.truncateTableStatements(
110-
table: tableName, schema: nil, cascade: options.cascade
111-
) else {
112-
return []
113-
}
114-
return stmts
103+
guard let adapter = currentPluginDriverAdapter else { return [] }
104+
return adapter.truncateTableStatements(
105+
table: tableName, schema: nil, cascade: options.cascade
106+
)
115107
}
116108

117109
private func dropTableStatement(
118-
tableName: String, quotedName: String, isView: Bool,
119-
options: TableOperationOptions, dbType: DatabaseType
110+
tableName: String, isView: Bool, options: TableOperationOptions
120111
) -> String {
121112
let keyword = isView ? "VIEW" : "TABLE"
122-
guard let adapter = currentPluginDriverAdapter,
123-
let stmt = adapter.dropObjectStatement(
124-
name: tableName, objectType: keyword, schema: nil, cascade: options.cascade
125-
) else {
126-
return ""
127-
}
128-
return stmt
113+
guard let adapter = currentPluginDriverAdapter else { return "" }
114+
return adapter.dropObjectStatement(
115+
name: tableName, objectType: keyword, schema: nil, cascade: options.cascade
116+
)
129117
}
130118
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
//
2+
// PluginDriverAdapterTableOpsTests.swift
3+
// TableProTests
4+
//
5+
6+
import Foundation
7+
@testable import TablePro
8+
import TableProPluginKit
9+
import Testing
10+
11+
private final class StubTableOpsDriver: PluginDatabaseDriver {
12+
var supportsSchemas: Bool { false }
13+
var supportsTransactions: Bool { false }
14+
var currentSchema: String? { nil }
15+
var serverVersion: String? { nil }
16+
17+
var truncateOverride: ((String, String?, Bool) -> [String]?)?
18+
var dropOverride: ((String, String, String?, Bool) -> String?)?
19+
20+
func truncateTableStatements(table: String, schema: String?, cascade: Bool) -> [String]? {
21+
truncateOverride?(table, schema, cascade)
22+
}
23+
24+
func dropObjectStatement(name: String, objectType: String, schema: String?, cascade: Bool) -> String? {
25+
dropOverride?(name, objectType, schema, cascade)
26+
}
27+
28+
func connect() async throws {}
29+
func disconnect() {}
30+
func ping() async throws {}
31+
func execute(query: String) async throws -> PluginQueryResult {
32+
PluginQueryResult(columns: [], columnTypeNames: [], rows: [], rowsAffected: 0, executionTime: 0)
33+
}
34+
35+
func fetchRowCount(query: String) async throws -> Int { 0 }
36+
func fetchRows(query: String, offset: Int, limit: Int) async throws -> PluginQueryResult {
37+
PluginQueryResult(columns: [], columnTypeNames: [], rows: [], rowsAffected: 0, executionTime: 0)
38+
}
39+
40+
func fetchTables(schema: String?) async throws -> [PluginTableInfo] { [] }
41+
func fetchColumns(table: String, schema: String?) async throws -> [PluginColumnInfo] { [] }
42+
func fetchIndexes(table: String, schema: String?) async throws -> [PluginIndexInfo] { [] }
43+
func fetchForeignKeys(table: String, schema: String?) async throws -> [PluginForeignKeyInfo] { [] }
44+
func fetchTableDDL(table: String, schema: String?) async throws -> String { "" }
45+
func fetchViewDefinition(view: String, schema: String?) async throws -> String { "" }
46+
func fetchTableMetadata(table: String, schema: String?) async throws -> PluginTableMetadata {
47+
PluginTableMetadata(tableName: table)
48+
}
49+
50+
func fetchDatabases() async throws -> [String] { [] }
51+
func fetchDatabaseMetadata(_ database: String) async throws -> PluginDatabaseMetadata {
52+
PluginDatabaseMetadata(name: database)
53+
}
54+
}
55+
56+
@Suite("PluginDriverAdapter table operations")
57+
struct PluginDriverAdapterTableOpsTests {
58+
private func makeAdapter(driver: StubTableOpsDriver) -> PluginDriverAdapter {
59+
let connection = DatabaseConnection(name: "Test", type: .postgresql)
60+
return PluginDriverAdapter(connection: connection, pluginDriver: driver)
61+
}
62+
63+
// MARK: - dropObjectStatement
64+
65+
@Test("Fallback produces DROP TABLE with quoted name")
66+
func dropTableFallback() {
67+
let adapter = makeAdapter(driver: StubTableOpsDriver())
68+
let result = adapter.dropObjectStatement(name: "users", objectType: "TABLE", schema: nil, cascade: false)
69+
#expect(result == "DROP TABLE \"users\"")
70+
}
71+
72+
@Test("Fallback produces DROP VIEW for views")
73+
func dropViewFallback() {
74+
let adapter = makeAdapter(driver: StubTableOpsDriver())
75+
let result = adapter.dropObjectStatement(name: "active_users", objectType: "VIEW", schema: nil, cascade: false)
76+
#expect(result == "DROP VIEW \"active_users\"")
77+
}
78+
79+
@Test("Fallback appends CASCADE when requested")
80+
func dropWithCascade() {
81+
let adapter = makeAdapter(driver: StubTableOpsDriver())
82+
let result = adapter.dropObjectStatement(name: "orders", objectType: "TABLE", schema: nil, cascade: true)
83+
#expect(result == "DROP TABLE \"orders\" CASCADE")
84+
}
85+
86+
@Test("Fallback includes schema qualification")
87+
func dropWithSchema() {
88+
let adapter = makeAdapter(driver: StubTableOpsDriver())
89+
let result = adapter.dropObjectStatement(name: "users", objectType: "TABLE", schema: "public", cascade: false)
90+
#expect(result == "DROP TABLE \"public\".\"users\"")
91+
}
92+
93+
@Test("Plugin override is returned when non-nil")
94+
func dropPluginOverride() {
95+
let driver = StubTableOpsDriver()
96+
driver.dropOverride = { name, objectType, _, _ in
97+
"DROP \(objectType) IF EXISTS `\(name)`"
98+
}
99+
let adapter = makeAdapter(driver: driver)
100+
let result = adapter.dropObjectStatement(name: "users", objectType: "TABLE", schema: nil, cascade: false)
101+
#expect(result == "DROP TABLE IF EXISTS `users`")
102+
}
103+
104+
// MARK: - truncateTableStatements
105+
106+
@Test("Fallback produces TRUNCATE TABLE with quoted name")
107+
func truncateFallback() {
108+
let adapter = makeAdapter(driver: StubTableOpsDriver())
109+
let result = adapter.truncateTableStatements(table: "users", schema: nil, cascade: false)
110+
#expect(result == ["TRUNCATE TABLE \"users\""])
111+
}
112+
113+
@Test("Fallback appends CASCADE when requested")
114+
func truncateWithCascade() {
115+
let adapter = makeAdapter(driver: StubTableOpsDriver())
116+
let result = adapter.truncateTableStatements(table: "orders", schema: nil, cascade: true)
117+
#expect(result == ["TRUNCATE TABLE \"orders\" CASCADE"])
118+
}
119+
120+
@Test("Fallback includes schema qualification")
121+
func truncateWithSchema() {
122+
let adapter = makeAdapter(driver: StubTableOpsDriver())
123+
let result = adapter.truncateTableStatements(table: "users", schema: "public", cascade: false)
124+
#expect(result == ["TRUNCATE TABLE \"public\".\"users\""])
125+
}
126+
127+
@Test("Plugin override is returned when non-nil")
128+
func truncatePluginOverride() {
129+
let driver = StubTableOpsDriver()
130+
driver.truncateOverride = { table, _, _ in
131+
["DELETE FROM `\(table)`", "ALTER TABLE `\(table)` AUTO_INCREMENT = 1"]
132+
}
133+
let adapter = makeAdapter(driver: driver)
134+
let result = adapter.truncateTableStatements(table: "users", schema: nil, cascade: false)
135+
#expect(result == ["DELETE FROM `users`", "ALTER TABLE `users` AUTO_INCREMENT = 1"])
136+
}
137+
}

0 commit comments

Comments
 (0)