Skip to content

Commit 59acc66

Browse files
committed
fix: quote reserved keyword column names in generated SQL (#373)
1 parent 4221e75 commit 59acc66

File tree

7 files changed

+147
-5
lines changed

7 files changed

+147
-5
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- SQL syntax error when editing columns with reserved keyword names (e.g., `database`, `table`, `order`) in MySQL/PostgreSQL/SQLite
13+
- High CPU usage and memory leaks at idle
14+
- N+1 query performance in foreign key fetching with bulk queries
15+
- Architecture-specific update delivery using `sparkle:hardwareRequirements`
16+
17+
### Changed
18+
19+
- Improved performance for medium and low severity bottlenecks (query history, tab persistence, sidebar rendering)
20+
1021
## [0.20.3] - 2026-03-18
1122

1223
### Added

TablePro/Core/ChangeTracking/DataChangeManager.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,7 @@ final class DataChangeManager {
696696
columns: columns,
697697
primaryKeyColumn: primaryKeyColumn,
698698
databaseType: databaseType,
699+
dialect: PluginManager.shared.sqlDialect(for: databaseType),
699700
quoteIdentifier: pluginDriver?.quoteIdentifier
700701
)
701702
let statements = generator.generateStatements(

TableProTests/Core/ChangeTracking/SQLStatementGeneratorMSSQLTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ struct SQLStatementGeneratorMSSQLTests {
2222
tableName: tableName,
2323
columns: columns,
2424
primaryKeyColumn: primaryKeyColumn,
25-
databaseType: .mssql
25+
databaseType: .mssql,
26+
dialect: PluginManager.shared.sqlDialect(for: .mssql)
2627
)
2728
}
2829

TableProTests/Core/ChangeTracking/SQLStatementGeneratorNoPKTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ struct SQLStatementGeneratorNoPKTests {
2323
tableName: tableName,
2424
columns: columns,
2525
primaryKeyColumn: primaryKeyColumn,
26-
databaseType: databaseType
26+
databaseType: databaseType,
27+
dialect: PluginManager.shared.sqlDialect(for: databaseType)
2728
)
2829
}
2930

TableProTests/Core/ChangeTracking/SQLStatementGeneratorPKRegressionTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ struct SQLStatementGeneratorPKRegressionTests {
2121
tableName: tableName,
2222
columns: columns,
2323
primaryKeyColumn: primaryKeyColumn,
24-
databaseType: databaseType
24+
databaseType: databaseType,
25+
dialect: PluginManager.shared.sqlDialect(for: databaseType)
2526
)
2627
}
2728

TableProTests/Core/ChangeTracking/SQLStatementGeneratorParameterStyleTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ struct SQLStatementGeneratorParameterStyleTests {
2727
columns: columns,
2828
primaryKeyColumn: primaryKeyColumn,
2929
databaseType: databaseType,
30-
parameterStyle: parameterStyle
30+
parameterStyle: parameterStyle,
31+
dialect: PluginManager.shared.sqlDialect(for: databaseType)
3132
)
3233
}
3334

TableProTests/Core/ChangeTracking/SQLStatementGeneratorTests.swift

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ struct SQLStatementGeneratorTests {
2424
tableName: tableName,
2525
columns: columns,
2626
primaryKeyColumn: primaryKeyColumn,
27-
databaseType: databaseType
27+
databaseType: databaseType,
28+
dialect: PluginManager.shared.sqlDialect(for: databaseType)
2829
)
2930
}
3031

@@ -1208,4 +1209,129 @@ struct SQLStatementGeneratorTests {
12081209
#expect(stmt.sql.range(of: "\"email\" = $2") != nil)
12091210
#expect(stmt.sql.range(of: "\"id\" = $3") != nil)
12101211
}
1212+
1213+
// MARK: - Reserved Keyword Column Name Regression (GH-373)
1214+
1215+
@Test("UPDATE quotes reserved keyword column names in MySQL")
1216+
func testUpdateQuotesReservedKeywordColumnMySQL() {
1217+
let generator = makeGenerator(
1218+
tableName: "connections",
1219+
columns: ["id", "database", "table", "order"],
1220+
primaryKeyColumn: "id"
1221+
)
1222+
let changes: [RowChange] = [
1223+
RowChange(
1224+
rowIndex: 0,
1225+
type: .update,
1226+
cellChanges: [
1227+
CellChange(rowIndex: 0, columnIndex: 1, columnName: "database", oldValue: "old_db", newValue: "new_db")
1228+
],
1229+
originalRow: ["1", "old_db", "users", "5"]
1230+
)
1231+
]
1232+
1233+
let statements = generator.generateStatements(
1234+
from: changes,
1235+
insertedRowData: [:],
1236+
deletedRowIndices: [],
1237+
insertedRowIndices: []
1238+
)
1239+
1240+
#expect(statements.count == 1)
1241+
let stmt = statements[0]
1242+
#expect(stmt.sql.contains("`database` = ?"))
1243+
#expect(stmt.sql.contains("WHERE `id` = ?"))
1244+
#expect(!stmt.sql.contains("SET database ="))
1245+
}
1246+
1247+
@Test("INSERT quotes reserved keyword column names in MySQL")
1248+
func testInsertQuotesReservedKeywordColumnMySQL() {
1249+
let generator = makeGenerator(
1250+
tableName: "connections",
1251+
columns: ["id", "database", "order"],
1252+
primaryKeyColumn: "id"
1253+
)
1254+
let insertedRowData: [Int: [String?]] = [
1255+
0: ["1", "mydb", "5"]
1256+
]
1257+
let changes: [RowChange] = [
1258+
RowChange(rowIndex: 0, type: .insert, cellChanges: [], originalRow: nil)
1259+
]
1260+
1261+
let statements = generator.generateStatements(
1262+
from: changes,
1263+
insertedRowData: insertedRowData,
1264+
deletedRowIndices: [],
1265+
insertedRowIndices: [0]
1266+
)
1267+
1268+
#expect(statements.count == 1)
1269+
let stmt = statements[0]
1270+
#expect(stmt.sql.contains("`database`"))
1271+
#expect(stmt.sql.contains("`order`"))
1272+
#expect(!stmt.sql.contains("(database,"))
1273+
#expect(!stmt.sql.contains(", order)"))
1274+
}
1275+
1276+
@Test("DELETE quotes reserved keyword column names in MySQL (no PK)")
1277+
func testDeleteQuotesReservedKeywordColumnMySQL() {
1278+
let generator = makeGenerator(
1279+
tableName: "connections",
1280+
columns: ["id", "database", "select"],
1281+
primaryKeyColumn: nil
1282+
)
1283+
let changes: [RowChange] = [
1284+
RowChange(
1285+
rowIndex: 0,
1286+
type: .delete,
1287+
cellChanges: [],
1288+
originalRow: ["1", "mydb", "foo"]
1289+
)
1290+
]
1291+
1292+
let statements = generator.generateStatements(
1293+
from: changes,
1294+
insertedRowData: [:],
1295+
deletedRowIndices: [0],
1296+
insertedRowIndices: []
1297+
)
1298+
1299+
#expect(statements.count == 1)
1300+
let stmt = statements[0]
1301+
#expect(stmt.sql.contains("`database`"))
1302+
#expect(stmt.sql.contains("`select`"))
1303+
}
1304+
1305+
@Test("UPDATE quotes reserved keyword column names in PostgreSQL")
1306+
func testUpdateQuotesReservedKeywordColumnPostgreSQL() {
1307+
let generator = makeGenerator(
1308+
tableName: "connections",
1309+
columns: ["id", "database", "order"],
1310+
primaryKeyColumn: "id",
1311+
databaseType: .postgresql
1312+
)
1313+
let changes: [RowChange] = [
1314+
RowChange(
1315+
rowIndex: 0,
1316+
type: .update,
1317+
cellChanges: [
1318+
CellChange(rowIndex: 0, columnIndex: 1, columnName: "database", oldValue: "old_db", newValue: "new_db")
1319+
],
1320+
originalRow: ["1", "old_db", "5"]
1321+
)
1322+
]
1323+
1324+
let statements = generator.generateStatements(
1325+
from: changes,
1326+
insertedRowData: [:],
1327+
deletedRowIndices: [],
1328+
insertedRowIndices: []
1329+
)
1330+
1331+
#expect(statements.count == 1)
1332+
let stmt = statements[0]
1333+
// PostgreSQL uses double quotes
1334+
#expect(stmt.sql.contains("\"database\" = $1"))
1335+
#expect(stmt.sql.contains("\"id\" = $2"))
1336+
}
12111337
}

0 commit comments

Comments
 (0)