Skip to content

Commit 76000bf

Browse files
committed
fix: add SQL fallbacks for DROP/TRUNCATE and fix MySQL FK metadata after db switch
1 parent 945994a commit 76000bf

File tree

5 files changed

+175
-22
lines changed

5 files changed

+175
-22
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2424
- Connection test not cleaning up SSH tunnel on completion
2525
- Test connection success indicator not resetting after field changes
2626
- SSH port field accepting invalid values
27+
- DROP TABLE and TRUNCATE TABLE sidebar operations producing no SQL for plugin-based drivers
2728

2829
## [0.19.1] - 2026-03-16
2930

Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift

Lines changed: 7 additions & 4 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
@@ -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: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -105,26 +105,20 @@ extension MainContentCoordinator {
105105
private func truncateStatements(
106106
tableName: String, quotedName: String, options: TableOperationOptions, dbType: DatabaseType
107107
) -> [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
108+
guard let adapter = currentPluginDriverAdapter else { return [] }
109+
return adapter.truncateTableStatements(
110+
table: tableName, schema: nil, cascade: options.cascade
111+
)
115112
}
116113

117114
private func dropTableStatement(
118115
tableName: String, quotedName: String, isView: Bool,
119116
options: TableOperationOptions, dbType: DatabaseType
120117
) -> String {
121118
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
119+
guard let adapter = currentPluginDriverAdapter else { return "" }
120+
return adapter.dropObjectStatement(
121+
name: tableName, objectType: keyword, schema: nil, cascade: options.cascade
122+
)
129123
}
130124
}
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)