Skip to content

Commit 83b91a7

Browse files
committed
fix: unify sidebar and data grid save pipelines for all database drivers
1 parent 264d2e1 commit 83b91a7

4 files changed

Lines changed: 58 additions & 114 deletions

File tree

TablePro/Core/ChangeTracking/DataChangeManager.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -621,7 +621,23 @@ final class DataChangeManager {
621621
// MARK: - SQL Generation
622622

623623
func generateSQL() throws -> [ParameterizedStatement] {
624-
// Try plugin dispatch first (handles MongoDB, Redis, and future NoSQL plugins)
624+
try generateSQL(
625+
for: changes,
626+
insertedRowData: insertedRowData,
627+
deletedRowIndices: deletedRowIndices,
628+
insertedRowIndices: insertedRowIndices
629+
)
630+
}
631+
632+
/// Unified statement generation for both data grid and sidebar edits.
633+
/// Routes through plugin driver for NoSQL databases, falls back to SQLStatementGenerator for SQL.
634+
func generateSQL(
635+
for changes: [RowChange],
636+
insertedRowData: [Int: [String?]] = [:],
637+
deletedRowIndices: Set<Int> = [],
638+
insertedRowIndices: Set<Int> = []
639+
) throws -> [ParameterizedStatement] {
640+
// Try plugin dispatch first (handles MongoDB, Redis, etcd, and future NoSQL plugins)
625641
if let pluginDriver {
626642
let pluginChanges = changes.map { change -> PluginRowChange in
627643
PluginRowChange(

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

Lines changed: 17 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
//
77

88
import Foundation
9-
import TableProPluginKit
109

1110
extension MainContentCoordinator {
1211
// MARK: - Sidebar Save
@@ -17,129 +16,39 @@ extension MainContentCoordinator {
1716
) async throws {
1817
guard let tab = tabManager.selectedTab,
1918
!selectedRowIndices.isEmpty,
20-
let tableName = tab.tableName
19+
tab.tableName != nil
2120
else {
2221
return
2322
}
2423

2524
let editedFields = editState.getEditedFields()
2625
guard !editedFields.isEmpty else { return }
2726

28-
if connection.type == .redis {
29-
var redisStatements: [ParameterizedStatement] = []
30-
for rowIndex in selectedRowIndices.sorted() {
31-
guard rowIndex < tab.resultRows.count else { continue }
32-
let row = tab.resultRows[rowIndex]
33-
let commands = generateSidebarRedisCommands(
34-
originalRow: row.values,
35-
editedFields: editedFields,
36-
columns: tab.resultColumns
37-
)
38-
redisStatements += commands.map { ParameterizedStatement(sql: $0, parameters: []) }
39-
}
40-
guard !redisStatements.isEmpty else { return }
41-
try await executeSidebarChanges(statements: redisStatements)
42-
} else {
43-
let generator = SQLStatementGenerator(
44-
tableName: tableName,
45-
columns: tab.resultColumns,
46-
primaryKeyColumn: changeManager.primaryKeyColumn,
47-
databaseType: connection.type,
48-
quoteIdentifier: changeManager.pluginDriver?.quoteIdentifier
49-
)
50-
51-
var statements: [ParameterizedStatement] = []
52-
for rowIndex in selectedRowIndices.sorted() {
53-
guard rowIndex < tab.resultRows.count else { continue }
54-
let originalRow = tab.resultRows[rowIndex].values
55-
56-
let cellChanges = editedFields.map { field in
27+
// Build RowChange array from sidebar edits
28+
let changes: [RowChange] = selectedRowIndices.sorted().compactMap { rowIndex in
29+
guard rowIndex < tab.resultRows.count else { return nil }
30+
let originalRow = tab.resultRows[rowIndex].values
31+
return RowChange(
32+
rowIndex: rowIndex,
33+
type: .update,
34+
cellChanges: editedFields.map { field in
5735
CellChange(
5836
rowIndex: rowIndex,
5937
columnIndex: field.columnIndex,
6038
columnName: field.columnName,
6139
oldValue: originalRow[field.columnIndex],
6240
newValue: field.newValue
6341
)
64-
}
65-
let change = RowChange(
66-
rowIndex: rowIndex,
67-
type: .update,
68-
cellChanges: cellChanges,
69-
originalRow: originalRow
70-
)
71-
72-
if let stmt = generator.generateUpdateSQL(for: change) {
73-
statements.append(stmt)
74-
}
75-
}
76-
guard !statements.isEmpty else { return }
77-
try await executeSidebarChanges(statements: statements)
78-
}
79-
80-
runQuery()
81-
}
82-
83-
private func generateSidebarRedisCommands(
84-
originalRow: [String?],
85-
editedFields: [(columnIndex: Int, columnName: String, newValue: String?)],
86-
columns: [String]
87-
) -> [String] {
88-
guard let keyIndex = columns.firstIndex(of: "Key"),
89-
keyIndex < originalRow.count,
90-
let originalKey = originalRow[keyIndex]
91-
else {
92-
return []
93-
}
94-
95-
var commands: [String] = []
96-
var effectiveKey = originalKey
97-
98-
for field in editedFields {
99-
switch field.columnName {
100-
case "Key":
101-
if let newKey = field.newValue, newKey != originalKey {
102-
commands.append("RENAME \(redisEscape(originalKey)) \(redisEscape(newKey))")
103-
effectiveKey = newKey
104-
}
105-
case "Value":
106-
if let newValue = field.newValue {
107-
// Only use SET for string-type keys — other types need specific commands
108-
let typeIndex = columns.firstIndex(of: "Type")
109-
let keyType = typeIndex.flatMap {
110-
$0 < originalRow.count ? originalRow[$0]?.uppercased() : nil
111-
}
112-
if keyType == nil || keyType == "STRING" || keyType == "NONE" {
113-
commands.append("SET \(redisEscape(effectiveKey)) \(redisEscape(newValue))")
114-
}
115-
// Non-string types: skip (editing Value for complex types not supported via sidebar)
116-
}
117-
case "TTL":
118-
if let ttlStr = field.newValue, let ttl = Int(ttlStr), ttl >= 0 {
119-
commands.append("EXPIRE \(redisEscape(effectiveKey)) \(ttl)")
120-
} else if field.newValue == nil || field.newValue == "-1" {
121-
commands.append("PERSIST \(redisEscape(effectiveKey))")
122-
}
123-
default:
124-
break
125-
}
42+
},
43+
originalRow: originalRow
44+
)
12645
}
12746

128-
return commands
129-
}
47+
// Route through the unified statement generation pipeline
48+
let statements = try changeManager.generateSQL(for: changes)
49+
guard !statements.isEmpty else { return }
50+
try await executeSidebarChanges(statements: statements)
13051

131-
private func redisEscape(_ value: String) -> String {
132-
let needsQuoting =
133-
value.isEmpty || value.contains(where: { $0.isWhitespace || $0 == "\"" || $0 == "'" })
134-
if needsQuoting {
135-
let escaped =
136-
value
137-
.replacingOccurrences(of: "\\", with: "\\\\")
138-
.replacingOccurrences(of: "\"", with: "\\\"")
139-
.replacingOccurrences(of: "\n", with: "\\n")
140-
.replacingOccurrences(of: "\r", with: "\\r")
141-
return "\"\(escaped)\""
142-
}
143-
return value
52+
runQuery()
14453
}
14554
}

TablePro/Views/Main/MainContentCommandActions.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -417,11 +417,12 @@ final class MainContentCommandActions {
417417
// Check if we're in structure view mode
418418
if coordinator?.tabManager.selectedTab?.showStructure == true {
419419
coordinator?.structureActions?.saveChanges?()
420-
} else if rightPanelState.editState.hasEdits {
421-
// Save sidebar edits if the right panel has pending changes
422-
rightPanelState.onSave?()
423-
} else {
424-
// Handle data grid changes
420+
} else if coordinator?.changeManager.hasChanges == true
421+
|| !pendingTruncates.wrappedValue.isEmpty
422+
|| !pendingDeletes.wrappedValue.isEmpty {
423+
// Handle data grid changes (prioritize over sidebar edits since
424+
// data grid edits are synced to sidebar editState, and the data grid
425+
// path uses the correct plugin driver for statement generation)
425426
var truncates = pendingTruncates.wrappedValue
426427
var deletes = pendingDeletes.wrappedValue
427428
var options = tableOperationOptions.wrappedValue
@@ -433,6 +434,9 @@ final class MainContentCommandActions {
433434
pendingTruncates.wrappedValue = truncates
434435
pendingDeletes.wrappedValue = deletes
435436
tableOperationOptions.wrappedValue = options
437+
} else if rightPanelState.editState.hasEdits {
438+
// Save sidebar-only edits (edits made directly in the right panel)
439+
rightPanelState.onSave?()
436440
}
437441
}
438442

TablePro/Views/Results/CellTextField.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,21 @@ final class CellTextField: NSTextField {
7878
/// Custom text field cell that provides a field editor with custom context menu behavior
7979
final class CellTextFieldCell: NSTextFieldCell {
8080
private class CellFieldEditor: NSTextView {
81+
/// Key equivalents that should commit the edit and bubble up to the menu bar.
82+
private static let menuKeyEquivalents: Set<String> = ["s"]
83+
84+
override func performKeyEquivalent(with event: NSEvent) -> Bool {
85+
if event.modifierFlags.contains(.command),
86+
let chars = event.charactersIgnoringModifiers,
87+
Self.menuKeyEquivalents.contains(chars) {
88+
// Commit the inline edit so the change is recorded in DataChangeManager
89+
// before the menu action (e.g. Cmd+S save) fires.
90+
window?.makeFirstResponder(nil)
91+
return false
92+
}
93+
return super.performKeyEquivalent(with: event)
94+
}
95+
8196
override func rightMouseDown(with event: NSEvent) {
8297
window?.makeFirstResponder(nil)
8398

0 commit comments

Comments
 (0)