Skip to content

Commit 3aa3f4f

Browse files
committed
fix: address PR review - backslash escaping, access control, PK guard, translations
1 parent c3ee476 commit 3aa3f4f

3 files changed

Lines changed: 83 additions & 14 deletions

File tree

TablePro/Core/Utilities/SQL/SQLRowToStatementConverter.swift

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44

55
import Foundation
66

7-
struct SQLRowToStatementConverter {
8-
let tableName: String
9-
let columns: [String]
10-
let primaryKeyColumn: String?
11-
let databaseType: DatabaseType
7+
internal struct SQLRowToStatementConverter {
8+
internal let tableName: String
9+
internal let columns: [String]
10+
internal let primaryKeyColumn: String?
11+
internal let databaseType: DatabaseType
1212

1313
private static let maxRows = 50_000
1414

15-
func generateInserts(rows: [[String?]]) -> String {
15+
internal func generateInserts(rows: [[String?]]) -> String {
1616
let capped = rows.prefix(Self.maxRows)
1717
let quotedTable = quoteColumn(tableName)
1818
let quotedColumns = columns.map { quoteColumn($0) }.joined(separator: ", ")
@@ -23,7 +23,7 @@ struct SQLRowToStatementConverter {
2323
}.joined(separator: "\n")
2424
}
2525

26-
func generateUpdates(rows: [[String?]]) -> String {
26+
internal func generateUpdates(rows: [[String?]]) -> String {
2727
let capped = rows.prefix(Self.maxRows)
2828

2929
return capped.map { row in
@@ -39,9 +39,10 @@ struct SQLRowToStatementConverter {
3939
let setClause: String
4040
let whereClause: String
4141

42-
if let pkColumn = primaryKeyColumn {
43-
let pkIndex = columns.firstIndex(of: pkColumn)
44-
let pkValue = pkIndex.map { row.indices.contains($0) ? row[$0] : nil } ?? nil
42+
if let pkColumn = primaryKeyColumn,
43+
let pkIndex = columns.firstIndex(of: pkColumn),
44+
row.indices.contains(pkIndex) {
45+
let pkValue = row[pkIndex]
4546

4647
let setClauses = columns.enumerated().compactMap { index, col -> String? in
4748
guard col != pkColumn else { return nil }
@@ -83,7 +84,11 @@ struct SQLRowToStatementConverter {
8384
guard let value else {
8485
return "NULL"
8586
}
86-
return "'\(value.replacingOccurrences(of: "'", with: "''"))'"
87+
var escaped = value.replacingOccurrences(of: "'", with: "''")
88+
if databaseType == .mysql || databaseType == .mariadb {
89+
escaped = escaped.replacingOccurrences(of: "\\", with: "\\\\")
90+
}
91+
return "'\(escaped)'"
8792
}
8893

8994
private func quoteColumn(_ name: String) -> String {

TablePro/Resources/Localizable.xcstrings

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4066,7 +4066,20 @@
40664066
}
40674067
},
40684068
"Copy as" : {
4069-
4069+
"localizations" : {
4070+
"vi" : {
4071+
"stringUnit" : {
4072+
"state" : "translated",
4073+
"value" : "Sao chép dạng"
4074+
}
4075+
},
4076+
"zh-Hans" : {
4077+
"stringUnit" : {
4078+
"state" : "translated",
4079+
"value" : "复制为"
4080+
}
4081+
}
4082+
}
40704083
},
40714084
"Copy as URL" : {
40724085
"localizations" : {
@@ -8455,7 +8468,20 @@
84558468
}
84568469
},
84578470
"INSERT Statement(s)" : {
8458-
8471+
"localizations" : {
8472+
"vi" : {
8473+
"stringUnit" : {
8474+
"state" : "translated",
8475+
"value" : "Câu lệnh INSERT"
8476+
}
8477+
},
8478+
"zh-Hans" : {
8479+
"stringUnit" : {
8480+
"state" : "translated",
8481+
"value" : "INSERT 语句"
8482+
}
8483+
}
8484+
}
84598485
},
84608486
"Inspector" : {
84618487
"localizations" : {
@@ -16644,7 +16670,20 @@
1664416670
}
1664516671
},
1664616672
"UPDATE Statement(s)" : {
16647-
16673+
"localizations" : {
16674+
"vi" : {
16675+
"stringUnit" : {
16676+
"state" : "translated",
16677+
"value" : "Câu lệnh UPDATE"
16678+
}
16679+
},
16680+
"zh-Hans" : {
16681+
"stringUnit" : {
16682+
"state" : "translated",
16683+
"value" : "UPDATE 语句"
16684+
}
16685+
}
16686+
}
1664816687
},
1664916688
"Updated" : {
1665016689
"localizations" : {

TableProTests/Core/Utilities/SQLRowToStatementConverterTests.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,31 @@ struct SQLRowToStatementConverterTests {
138138
#expect(update == "UPDATE \"users\" SET \"name\" = 'Alice', \"email\" = 'alice@example.com' WHERE \"id\" = '1';")
139139
}
140140

141+
@Test("MySQL escapes backslashes in values")
142+
func mysqlBackslashEscaping() {
143+
let converter = makeConverter(databaseType: .mysql)
144+
let result = converter.generateInserts(rows: [["1", "C:\\Users\\test", "a@b.com"]])
145+
#expect(result == "INSERT INTO `users` (`id`, `name`, `email`) VALUES ('1', 'C:\\\\Users\\\\test', 'a@b.com');")
146+
}
147+
148+
@Test("PostgreSQL does not escape backslashes")
149+
func postgresqlNoBackslashEscaping() {
150+
let converter = makeConverter(databaseType: .postgresql)
151+
let result = converter.generateInserts(rows: [["1", "C:\\Users\\test", "a@b.com"]])
152+
#expect(result == "INSERT INTO \"users\" (\"id\", \"name\", \"email\") VALUES ('1', 'C:\\Users\\test', 'a@b.com');")
153+
}
154+
155+
@Test("UPDATE falls back to all-column WHERE when PK not in columns")
156+
func updatePkNotInColumnsFallsBack() {
157+
let converter = makeConverter(
158+
columns: ["name", "email"],
159+
primaryKeyColumn: "id",
160+
databaseType: .mysql
161+
)
162+
let result = converter.generateUpdates(rows: [["Alice", "alice@example.com"]])
163+
#expect(result == "UPDATE `users` SET `name` = 'Alice', `email` = 'alice@example.com' WHERE `name` = 'Alice' AND `email` = 'alice@example.com';")
164+
}
165+
141166
// MARK: - Edge Cases
142167

143168
@Test("Empty rows input returns empty string")

0 commit comments

Comments
 (0)