Skip to content

Commit fab9f87

Browse files
committed
Add table delete/truncate confirmation dialog
Implement TablePlus-like confirmation dialog for delete/truncate table operations with options for: - Ignore foreign key checks - Cascade (delete dependent rows) Database-specific SQL generation: - MySQL/MariaDB: SET FOREIGN_KEY_CHECKS=0/1 - PostgreSQL: CASCADE keyword, session_replication_role - SQLite: PRAGMA foreign_keys
1 parent 3f7760c commit fab9f87

8 files changed

Lines changed: 412 additions & 37 deletions

TablePro/ContentView.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,9 @@ struct ContentView: View {
157157
showAllTablesMetadata()
158158
},
159159
pendingTruncates: sessionPendingTruncatesBinding,
160-
pendingDeletes: sessionPendingDeletesBinding
160+
pendingDeletes: sessionPendingDeletesBinding,
161+
tableOperationOptions: sessionTableOperationOptionsBinding,
162+
databaseType: currentSession?.connection.type ?? .sqlite
161163
)
162164
}
163165
.navigationSplitViewColumnWidth(min: 200, ideal: 250, max: 350)
@@ -169,6 +171,7 @@ struct ContentView: View {
169171
selectedTables: sessionSelectedTablesBinding,
170172
pendingTruncates: sessionPendingTruncatesBinding,
171173
pendingDeletes: sessionPendingDeletesBinding,
174+
tableOperationOptions: sessionTableOperationOptionsBinding,
172175
isInspectorPresented: $isInspectorPresented
173176
)
174177
.id(currentSession!.id)
@@ -249,6 +252,14 @@ struct ContentView: View {
249252
)
250253
}
251254

255+
private var sessionTableOperationOptionsBinding: Binding<[String: TableOperationOptions]> {
256+
createSessionBinding(
257+
get: { $0.tableOperationOptions },
258+
set: { $0.tableOperationOptions = $1 },
259+
defaultValue: [:]
260+
)
261+
}
262+
252263
// MARK: - Actions
253264

254265
private func connectToDatabase(_ connection: DatabaseConnection) {

TablePro/Models/ConnectionSession.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ struct ConnectionSession: Identifiable {
2222
var selectedTabId: UUID?
2323
var pendingTruncates: Set<String> = []
2424
var pendingDeletes: Set<String> = []
25+
var tableOperationOptions: [String: TableOperationOptions] = [:]
2526

2627
// Metadata
2728
let connectedAt: Date
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//
2+
// TableOperationOptions.swift
3+
// TablePro
4+
//
5+
// Model for table delete/truncate operation options.
6+
// Supports foreign key constraint handling and cascade operations.
7+
//
8+
9+
import Foundation
10+
11+
/// Options for table delete/truncate operations
12+
struct TableOperationOptions: Codable, Equatable {
13+
var ignoreForeignKeys: Bool = false
14+
var cascade: Bool = false
15+
}
16+
17+
/// Type of table operation
18+
enum TableOperationType: String, Codable {
19+
case truncate
20+
case delete
21+
}

TablePro/Views/Main/MainContentCoordinator.swift

Lines changed: 98 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,8 @@ final class MainContentCoordinator: ObservableObject {
554554

555555
func saveChanges(
556556
pendingTruncates: inout Set<String>,
557-
pendingDeletes: inout Set<String>
557+
pendingDeletes: inout Set<String>,
558+
tableOperationOptions: inout [String: TableOperationOptions]
558559
) {
559560
let hasEditedCells = changeManager.hasChanges
560561
let hasPendingTableOps = !pendingTruncates.isEmpty || !pendingDeletes.isEmpty
@@ -568,14 +569,13 @@ final class MainContentCoordinator: ObservableObject {
568569
}
569570

570571
if hasPendingTableOps {
571-
for tableName in pendingTruncates {
572-
let quotedName = connection.type.quoteIdentifier(tableName)
573-
allStatements.append("TRUNCATE TABLE \(quotedName)")
574-
}
575-
for tableName in pendingDeletes {
576-
let quotedName = connection.type.quoteIdentifier(tableName)
577-
allStatements.append("DROP TABLE \(quotedName)")
578-
}
572+
// Generate table operation SQL with FK/cascade options
573+
let tableOpStatements = generateTableOperationSQL(
574+
truncates: pendingTruncates,
575+
deletes: pendingDeletes,
576+
options: tableOperationOptions
577+
)
578+
allStatements.append(contentsOf: tableOpStatements)
579579
}
580580

581581
guard !allStatements.isEmpty else {
@@ -586,20 +586,107 @@ final class MainContentCoordinator: ObservableObject {
586586
}
587587

588588
let sql = allStatements.joined(separator: ";\n")
589-
executeCommitSQL(sql, clearTableOps: hasPendingTableOps, pendingTruncates: &pendingTruncates, pendingDeletes: &pendingDeletes)
589+
executeCommitSQL(
590+
sql,
591+
clearTableOps: hasPendingTableOps,
592+
pendingTruncates: &pendingTruncates,
593+
pendingDeletes: &pendingDeletes,
594+
tableOperationOptions: &tableOperationOptions
595+
)
596+
}
597+
598+
/// Generate SQL for table truncate/delete operations with FK/cascade options
599+
private func generateTableOperationSQL(
600+
truncates: Set<String>,
601+
deletes: Set<String>,
602+
options: [String: TableOperationOptions]
603+
) -> [String] {
604+
var statements: [String] = []
605+
let dbType = connection.type
606+
607+
// Check if any operation needs FK disabled
608+
let needsDisableFK = truncates.union(deletes).contains { tableName in
609+
options[tableName]?.ignoreForeignKeys == true
610+
}
611+
612+
if needsDisableFK {
613+
statements.append(fkDisableStatement(for: dbType))
614+
}
615+
616+
for tableName in truncates {
617+
let quotedName = dbType.quoteIdentifier(tableName)
618+
let opts = options[tableName] ?? TableOperationOptions()
619+
statements.append(truncateStatement(tableName: quotedName, options: opts, dbType: dbType))
620+
}
621+
622+
for tableName in deletes {
623+
let quotedName = dbType.quoteIdentifier(tableName)
624+
let opts = options[tableName] ?? TableOperationOptions()
625+
statements.append(dropTableStatement(tableName: quotedName, options: opts, dbType: dbType))
626+
}
627+
628+
if needsDisableFK {
629+
statements.append(fkEnableStatement(for: dbType))
630+
}
631+
632+
return statements
633+
}
634+
635+
private func fkDisableStatement(for dbType: DatabaseType) -> String {
636+
return switch dbType {
637+
case .mysql, .mariadb: "SET FOREIGN_KEY_CHECKS=0"
638+
case .postgresql: "SET session_replication_role = 'replica'"
639+
case .sqlite: "PRAGMA foreign_keys = OFF"
640+
}
641+
}
642+
643+
private func fkEnableStatement(for dbType: DatabaseType) -> String {
644+
return switch dbType {
645+
case .mysql, .mariadb: "SET FOREIGN_KEY_CHECKS=1"
646+
case .postgresql: "SET session_replication_role = 'origin'"
647+
case .sqlite: "PRAGMA foreign_keys = ON"
648+
}
649+
}
650+
651+
private func truncateStatement(tableName: String, options: TableOperationOptions, dbType: DatabaseType) -> String {
652+
return switch dbType {
653+
case .mysql, .mariadb: "TRUNCATE TABLE \(tableName)"
654+
case .postgresql: options.cascade ? "TRUNCATE TABLE \(tableName) CASCADE" : "TRUNCATE TABLE \(tableName)"
655+
case .sqlite: "DELETE FROM \(tableName)"
656+
}
657+
}
658+
659+
private func dropTableStatement(tableName: String, options: TableOperationOptions, dbType: DatabaseType) -> String {
660+
let cascade = options.cascade ? " CASCADE" : ""
661+
return switch dbType {
662+
case .mysql, .mariadb, .postgresql: "DROP TABLE \(tableName)\(cascade)"
663+
case .sqlite: "DROP TABLE \(tableName)"
664+
}
590665
}
591666

592667
private func executeCommitSQL(
593668
_ sql: String,
594669
clearTableOps: Bool,
595670
pendingTruncates: inout Set<String>,
596-
pendingDeletes: inout Set<String>
671+
pendingDeletes: inout Set<String>,
672+
tableOperationOptions: inout [String: TableOperationOptions]
597673
) {
598674
guard !sql.isEmpty else { return }
599675

600676
let deletedTables = Set(pendingDeletes)
677+
let truncatedTables = Set(pendingTruncates)
601678
let conn = connection
602679

680+
// Clear operations immediately (before async execution)
681+
if clearTableOps {
682+
pendingTruncates.removeAll()
683+
pendingDeletes.removeAll()
684+
// Clear options for processed tables
685+
for table in deletedTables.union(truncatedTables) {
686+
tableOperationOptions.removeValue(forKey: table)
687+
}
688+
}
689+
603690
Task {
604691
let overallStartTime = Date()
605692

TablePro/Views/Main/MainContentNotificationHandler.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ final class MainContentNotificationHandler: ObservableObject {
2626
private let selectedTables: Binding<Set<TableInfo>>
2727
private let pendingTruncates: Binding<Set<String>>
2828
private let pendingDeletes: Binding<Set<String>>
29+
private let tableOperationOptions: Binding<[String: TableOperationOptions]>
2930
private let isInspectorPresented: Binding<Bool>
3031
private let editingCell: Binding<CellPosition?>
3132

@@ -43,6 +44,7 @@ final class MainContentNotificationHandler: ObservableObject {
4344
selectedTables: Binding<Set<TableInfo>>,
4445
pendingTruncates: Binding<Set<String>>,
4546
pendingDeletes: Binding<Set<String>>,
47+
tableOperationOptions: Binding<[String: TableOperationOptions]>,
4648
isInspectorPresented: Binding<Bool>,
4749
editingCell: Binding<CellPosition?>
4850
) {
@@ -53,6 +55,7 @@ final class MainContentNotificationHandler: ObservableObject {
5355
self.selectedTables = selectedTables
5456
self.pendingTruncates = pendingTruncates
5557
self.pendingDeletes = pendingDeletes
58+
self.tableOperationOptions = tableOperationOptions
5659
self.isInspectorPresented = isInspectorPresented
5760
self.editingCell = editingCell
5861

@@ -308,9 +311,15 @@ final class MainContentNotificationHandler: ObservableObject {
308311
private func handleSaveChanges() {
309312
var truncates = pendingTruncates.wrappedValue
310313
var deletes = pendingDeletes.wrappedValue
311-
coordinator?.saveChanges(pendingTruncates: &truncates, pendingDeletes: &deletes)
314+
var options = tableOperationOptions.wrappedValue
315+
coordinator?.saveChanges(
316+
pendingTruncates: &truncates,
317+
pendingDeletes: &deletes,
318+
tableOperationOptions: &options
319+
)
312320
pendingTruncates.wrappedValue = truncates
313321
pendingDeletes.wrappedValue = deletes
322+
tableOperationOptions.wrappedValue = options
314323
}
315324

316325
// MARK: - UI Operations

TablePro/Views/MainContentView.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ struct MainContentView: View {
2121
@Binding var selectedTables: Set<TableInfo>
2222
@Binding var pendingTruncates: Set<String>
2323
@Binding var pendingDeletes: Set<String>
24+
@Binding var tableOperationOptions: [String: TableOperationOptions]
2425
@Binding var isInspectorPresented: Bool
2526

2627
// MARK: - State Objects
@@ -49,13 +50,15 @@ struct MainContentView: View {
4950
selectedTables: Binding<Set<TableInfo>>,
5051
pendingTruncates: Binding<Set<String>>,
5152
pendingDeletes: Binding<Set<String>>,
53+
tableOperationOptions: Binding<[String: TableOperationOptions]>,
5254
isInspectorPresented: Binding<Bool>
5355
) {
5456
self.connection = connection
5557
self._tables = tables
5658
self._selectedTables = selectedTables
5759
self._pendingTruncates = pendingTruncates
5860
self._pendingDeletes = pendingDeletes
61+
self._tableOperationOptions = tableOperationOptions
5962
self._isInspectorPresented = isInspectorPresented
6063

6164
// Create state objects
@@ -223,6 +226,7 @@ struct MainContentView: View {
223226
selectedTables: $selectedTables,
224227
pendingTruncates: $pendingTruncates,
225228
pendingDeletes: $pendingDeletes,
229+
tableOperationOptions: $tableOperationOptions,
226230
isInspectorPresented: $isInspectorPresented,
227231
editingCell: $editingCell
228232
)
@@ -394,6 +398,7 @@ struct MainContentView: View {
394398
selectedTables: .constant([]),
395399
pendingTruncates: .constant([]),
396400
pendingDeletes: .constant([]),
401+
tableOperationOptions: .constant([:]),
397402
isInspectorPresented: .constant(false)
398403
)
399404
.frame(width: 1000, height: 600)

0 commit comments

Comments
 (0)