Skip to content

Commit 5bda130

Browse files
committed
fix(plugins): qualify write statements and FK navigation with the table schema
Claude-Session: https://claude.ai/code/session_0198faM6VCrViRU4XwRoS1DC
1 parent b648ea6 commit 5bda130

9 files changed

Lines changed: 80 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2222

2323
- Raw filters in the data grid now apply on document and key-value databases; the typed text was being dropped before it reached the driver. (#1529)
2424
- Connecting to Oracle no longer crashes the app while reading certain server values during the handshake; a bad packet now fails the connection with an error instead. (#1746)
25-
- Opening a SQL Server or Oracle table or view outside the default schema no longer fails with "Invalid object name"; the data query now qualifies the table with its schema. (#1754)
25+
- Browsing and editing a SQL Server or Oracle table or view outside the default schema no longer fails with "Invalid object name" or writes to the wrong table; data, filter, and save queries now qualify the table with its schema. (#1754)
2626

2727
## [0.52.1] - 2026-06-22
2828

Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,24 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
313313
deletedRowIndices: Set<Int>,
314314
insertedRowIndices: Set<Int>
315315
) -> [(statement: String, parameters: [PluginCellValue])]? {
316+
generateStatements(
317+
table: table, schema: nil, columns: columns, primaryKeyColumns: primaryKeyColumns,
318+
changes: changes, insertedRowData: insertedRowData,
319+
deletedRowIndices: deletedRowIndices, insertedRowIndices: insertedRowIndices
320+
)
321+
}
322+
323+
func generateStatements(
324+
table: String,
325+
schema: String?,
326+
columns: [String],
327+
primaryKeyColumns: [String],
328+
changes: [PluginRowChange],
329+
insertedRowData: [Int: [PluginCellValue]],
330+
deletedRowIndices: Set<Int>,
331+
insertedRowIndices: Set<Int>
332+
) -> [(statement: String, parameters: [PluginCellValue])]? {
333+
let qualifiedTable = MSSQLSchemaQueries.qualifiedName(schema: schema, table: table)
316334
var statements: [(statement: String, parameters: [PluginCellValue])] = []
317335

318336
var deleteChanges: [PluginRowChange] = []
@@ -322,13 +340,15 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
322340
case .insert:
323341
guard insertedRowIndices.contains(change.rowIndex) else { continue }
324342
if let values = insertedRowData[change.rowIndex] {
325-
if let stmt = generateMssqlInsert(table: table, columns: columns, values: values) {
343+
if let stmt = generateMssqlInsert(
344+
table: table, qualifiedTable: qualifiedTable, columns: columns, values: values
345+
) {
326346
statements.append(stmt)
327347
}
328348
}
329349
case .update:
330350
if let stmt = generateMssqlUpdate(
331-
table: table, columns: columns,
351+
qualifiedTable: qualifiedTable, columns: columns,
332352
primaryKeyColumns: primaryKeyColumns, change: change
333353
) {
334354
statements.append(stmt)
@@ -342,7 +362,7 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
342362
if !deleteChanges.isEmpty {
343363
for change in deleteChanges {
344364
if let stmt = generateMssqlDelete(
345-
table: table, columns: columns,
365+
qualifiedTable: qualifiedTable, columns: columns,
346366
primaryKeyColumns: primaryKeyColumns, change: change
347367
) {
348368
statements.append(stmt)
@@ -355,6 +375,7 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
355375

356376
private func generateMssqlInsert(
357377
table: String,
378+
qualifiedTable: String,
358379
columns: [String],
359380
values: [PluginCellValue]
360381
) -> (statement: String, parameters: [PluginCellValue])? {
@@ -378,21 +399,19 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
378399

379400
let columnList = nonDefaultColumns.joined(separator: ", ")
380401
let placeholders = parameters.map { _ in "?" }.joined(separator: ", ")
381-
let escapedTable = "[\(table.replacingOccurrences(of: "]", with: "]]"))]"
382-
let sql = "INSERT INTO \(escapedTable) (\(columnList)) VALUES (\(placeholders))"
402+
let sql = "INSERT INTO \(qualifiedTable) (\(columnList)) VALUES (\(placeholders))"
383403
return (statement: sql, parameters: parameters)
384404
}
385405

386406
private func generateMssqlUpdate(
387-
table: String,
407+
qualifiedTable: String,
388408
columns: [String],
389409
primaryKeyColumns: [String],
390410
change: PluginRowChange
391411
) -> (statement: String, parameters: [PluginCellValue])? {
392412
guard !change.cellChanges.isEmpty else { return nil }
393413
guard let originalRow = change.originalRow else { return nil }
394414

395-
let escapedTable = "[\(table.replacingOccurrences(of: "]", with: "]]"))]"
396415
var parameters: [PluginCellValue] = []
397416

398417
let setClauses = change.cellChanges.map { cellChange -> String in
@@ -422,19 +441,18 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
422441

423442
let whereClause = conditions.joined(separator: " AND ")
424443
let topClause = primaryKeyColumns.isEmpty ? "TOP (1) " : ""
425-
let sql = "UPDATE \(topClause)\(escapedTable) SET \(setClauses) WHERE \(whereClause)"
444+
let sql = "UPDATE \(topClause)\(qualifiedTable) SET \(setClauses) WHERE \(whereClause)"
426445
return (statement: sql, parameters: parameters)
427446
}
428447

429448
private func generateMssqlDelete(
430-
table: String,
449+
qualifiedTable: String,
431450
columns: [String],
432451
primaryKeyColumns: [String],
433452
change: PluginRowChange
434453
) -> (statement: String, parameters: [PluginCellValue])? {
435454
guard let originalRow = change.originalRow else { return nil }
436455

437-
let escapedTable = "[\(table.replacingOccurrences(of: "]", with: "]]"))]"
438456
var parameters: [PluginCellValue] = []
439457
var conditions: [String] = []
440458

@@ -458,7 +476,7 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
458476

459477
let whereClause = conditions.joined(separator: " AND ")
460478
let topClause = primaryKeyColumns.isEmpty ? "TOP (1) " : ""
461-
let sql = "DELETE \(topClause)FROM \(escapedTable) WHERE \(whereClause)"
479+
let sql = "DELETE \(topClause)FROM \(qualifiedTable) WHERE \(whereClause)"
462480
return (statement: sql, parameters: parameters)
463481
}
464482

Plugins/OracleDriverPlugin/OraclePlugin.swift

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -778,24 +778,42 @@ final class OraclePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
778778
deletedRowIndices: Set<Int>,
779779
insertedRowIndices: Set<Int>
780780
) -> [(statement: String, parameters: [PluginCellValue])]? {
781+
generateStatements(
782+
table: table, schema: nil, columns: columns, primaryKeyColumns: primaryKeyColumns,
783+
changes: changes, insertedRowData: insertedRowData,
784+
deletedRowIndices: deletedRowIndices, insertedRowIndices: insertedRowIndices
785+
)
786+
}
787+
788+
func generateStatements(
789+
table: String,
790+
schema: String?,
791+
columns: [String],
792+
primaryKeyColumns: [String],
793+
changes: [PluginRowChange],
794+
insertedRowData: [Int: [PluginCellValue]],
795+
deletedRowIndices: Set<Int>,
796+
insertedRowIndices: Set<Int>
797+
) -> [(statement: String, parameters: [PluginCellValue])]? {
798+
let qualifiedTable = oracleQualifiedName(schema: schema, table: table)
781799
var statements: [(statement: String, parameters: [PluginCellValue])] = []
782800

783801
for change in changes {
784802
switch change.type {
785803
case .insert:
786804
guard insertedRowIndices.contains(change.rowIndex) else { continue }
787805
if let values = insertedRowData[change.rowIndex] {
788-
if let stmt = generateOracleInsert(table: table, columns: columns, values: values) {
806+
if let stmt = generateOracleInsert(qualifiedTable: qualifiedTable, columns: columns, values: values) {
789807
statements.append(stmt)
790808
}
791809
}
792810
case .update:
793-
if let stmt = generateOracleUpdate(table: table, columns: columns, change: change) {
811+
if let stmt = generateOracleUpdate(qualifiedTable: qualifiedTable, columns: columns, change: change) {
794812
statements.append(stmt)
795813
}
796814
case .delete:
797815
guard deletedRowIndices.contains(change.rowIndex) else { continue }
798-
if let stmt = generateOracleDelete(table: table, columns: columns, change: change) {
816+
if let stmt = generateOracleDelete(qualifiedTable: qualifiedTable, columns: columns, change: change) {
799817
statements.append(stmt)
800818
}
801819
}
@@ -809,7 +827,7 @@ final class OraclePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
809827
}
810828

811829
private func generateOracleInsert(
812-
table: String,
830+
qualifiedTable: String,
813831
columns: [String],
814832
values: [PluginCellValue]
815833
) -> (statement: String, parameters: [PluginCellValue])? {
@@ -832,18 +850,17 @@ final class OraclePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
832850

833851
let columnList = insertColumns.joined(separator: ", ")
834852
let valueList = valuesSQL.joined(separator: ", ")
835-
let sql = "INSERT INTO \(escapeOracleIdentifier(table)) (\(columnList)) VALUES (\(valueList))"
853+
let sql = "INSERT INTO \(qualifiedTable) (\(columnList)) VALUES (\(valueList))"
836854
return (statement: sql, parameters: parameters)
837855
}
838856

839857
private func generateOracleUpdate(
840-
table: String,
858+
qualifiedTable: String,
841859
columns: [String],
842860
change: PluginRowChange
843861
) -> (statement: String, parameters: [PluginCellValue])? {
844862
guard !change.cellChanges.isEmpty, let originalRow = change.originalRow else { return nil }
845863

846-
let escapedTable = escapeOracleIdentifier(table)
847864
var parameters: [PluginCellValue] = []
848865

849866
let setClauses = change.cellChanges.map { cellChange -> String in
@@ -868,18 +885,17 @@ final class OraclePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
868885
guard !conditions.isEmpty else { return nil }
869886

870887
let whereClause = conditions.joined(separator: " AND ")
871-
let sql = "UPDATE \(escapedTable) SET \(setClauses) WHERE \(whereClause) AND ROWNUM = 1"
888+
let sql = "UPDATE \(qualifiedTable) SET \(setClauses) WHERE \(whereClause) AND ROWNUM = 1"
872889
return (statement: sql, parameters: parameters)
873890
}
874891

875892
private func generateOracleDelete(
876-
table: String,
893+
qualifiedTable: String,
877894
columns: [String],
878895
change: PluginRowChange
879896
) -> (statement: String, parameters: [PluginCellValue])? {
880897
guard let originalRow = change.originalRow else { return nil }
881898

882-
let escapedTable = escapeOracleIdentifier(table)
883899
var parameters: [PluginCellValue] = []
884900
var conditions: [String] = []
885901

@@ -898,7 +914,7 @@ final class OraclePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
898914
guard !conditions.isEmpty else { return nil }
899915

900916
let whereClause = conditions.joined(separator: " AND ")
901-
let sql = "DELETE FROM \(escapedTable) WHERE \(whereClause) AND ROWNUM = 1"
917+
let sql = "DELETE FROM \(qualifiedTable) WHERE \(whereClause) AND ROWNUM = 1"
902918
return (statement: sql, parameters: parameters)
903919
}
904920

Plugins/TableProPluginKit/PluginDatabaseDriver.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ public protocol PluginDatabaseDriver: AnyObject, Sendable {
133133
func fetchFilteredRowCount(table: String, filters: [(column: String, op: String, value: String)], logicMode: String) async throws -> Int?
134134
// Statement generation (optional, for NoSQL plugins)
135135
func generateStatements(table: String, columns: [String], primaryKeyColumns: [String], changes: [PluginRowChange], insertedRowData: [Int: [PluginCellValue]], deletedRowIndices: Set<Int>, insertedRowIndices: Set<Int>) -> [(statement: String, parameters: [PluginCellValue])]?
136+
func generateStatements(table: String, schema: String?, columns: [String], primaryKeyColumns: [String], changes: [PluginRowChange], insertedRowData: [Int: [PluginCellValue]], deletedRowIndices: Set<Int>, insertedRowIndices: Set<Int>) -> [(statement: String, parameters: [PluginCellValue])]?
136137

137138
// Database switching (SQL Server USE, ClickHouse database switch, etc.)
138139
func switchDatabase(to database: String) async throws
@@ -314,6 +315,12 @@ public extension PluginDatabaseDriver {
314315
}
315316
func fetchFilteredRowCount(table: String, filters: [(column: String, op: String, value: String)], logicMode: String) async throws -> Int? { nil }
316317
func generateStatements(table: String, columns: [String], primaryKeyColumns: [String], changes: [PluginRowChange], insertedRowData: [Int: [PluginCellValue]], deletedRowIndices: Set<Int>, insertedRowIndices: Set<Int>) -> [(statement: String, parameters: [PluginCellValue])]? { nil }
318+
func generateStatements(table: String, schema: String?, columns: [String], primaryKeyColumns: [String], changes: [PluginRowChange], insertedRowData: [Int: [PluginCellValue]], deletedRowIndices: Set<Int>, insertedRowIndices: Set<Int>) -> [(statement: String, parameters: [PluginCellValue])]? {
319+
generateStatements(
320+
table: table, columns: columns, primaryKeyColumns: primaryKeyColumns, changes: changes,
321+
insertedRowData: insertedRowData, deletedRowIndices: deletedRowIndices, insertedRowIndices: insertedRowIndices
322+
)
323+
}
317324

318325
func generateAddColumnSQL(table: String, column: PluginColumnDefinition) -> String? { nil }
319326
func generateModifyColumnSQL(table: String, oldColumn: PluginColumnDefinition, newColumn: PluginColumnDefinition) -> String? { nil }

TablePro/Core/ChangeTracking/DataChangeManager.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ final class DataChangeManager: ChangeManaging {
5050
var insertedRowIndices: Set<Int> { pending.insertedRowIndices }
5151

5252
var tableName: String = ""
53+
var schemaName: String?
5354
var primaryKeyColumns: [String] = []
5455
/// First PK column, for contexts that need a single column (paste, filters)
5556
var primaryKeyColumn: String? { primaryKeyColumns.first }
@@ -92,12 +93,14 @@ final class DataChangeManager: ChangeManaging {
9293

9394
func configureForTable(
9495
tableName: String,
96+
schemaName: String? = nil,
9597
columns: [String],
9698
primaryKeyColumns: [String],
9799
databaseType: DatabaseType,
98100
triggerReload: Bool = true
99101
) {
100102
self.tableName = tableName
103+
self.schemaName = schemaName
101104
self.columns = columns
102105
self.primaryKeyColumns = primaryKeyColumns
103106
self.databaseType = databaseType
@@ -400,6 +403,7 @@ final class DataChangeManager: ChangeManaging {
400403
let pluginInsertedRowData: [Int: [PluginCellValue]] = insertedRowData
401404
if let statements = pluginDriver.generateStatements(
402405
table: tableName,
406+
schema: schemaName,
403407
columns: columns,
404408
primaryKeyColumns: primaryKeyColumns,
405409
changes: pluginChanges,
@@ -489,8 +493,9 @@ final class DataChangeManager: ChangeManaging {
489493
pending.snapshot(primaryKeyColumns: primaryKeyColumns, columns: columns)
490494
}
491495

492-
func restoreState(from state: TabChangeSnapshot, tableName: String, databaseType: DatabaseType) {
496+
func restoreState(from state: TabChangeSnapshot, tableName: String, schemaName: String? = nil, databaseType: DatabaseType) {
493497
self.tableName = tableName
498+
self.schemaName = schemaName
494499
self.columns = state.columns
495500
self.primaryKeyColumns = state.primaryKeyColumns
496501
self.databaseType = databaseType

TablePro/Core/Coordinators/QueryExecutionCoordinator+Helpers.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ extension QueryExecutionCoordinator {
179179
if parent.tabManager.selectedTabId == tabId {
180180
parent.changeManager.configureForTable(
181181
tableName: tableName ?? "",
182+
schemaName: parent.tabManager.tabs[idx].tableContext.schemaName,
182183
columns: columns,
183184
primaryKeyColumns: resolvedPKs,
184185
databaseType: conn.type

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ extension MainContentCoordinator {
8484
let tableRows = tabSessionRegistry.tableRows(for: tab.id)
8585
let filteredQuery = queryBuilder.buildFilteredQuery(
8686
tableName: referencedTable,
87-
schemaName: fkInfo.referencedSchema,
87+
schemaName: targetSchema,
8888
filters: [filter],
8989
columns: tableRows.columns,
9090
limit: tab.pagination.pageSize,

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,16 @@ extension MainContentCoordinator {
6969

7070
let pendingState = newTab.pendingChanges
7171
if pendingState.hasChanges {
72-
changeManager.restoreState(from: pendingState, tableName: newTab.tableContext.tableName ?? "", databaseType: connection.type)
72+
changeManager.restoreState(
73+
from: pendingState,
74+
tableName: newTab.tableContext.tableName ?? "",
75+
schemaName: newTab.tableContext.schemaName,
76+
databaseType: connection.type
77+
)
7378
} else {
7479
changeManager.configureForTable(
7580
tableName: newTab.tableContext.tableName ?? "",
81+
schemaName: newTab.tableContext.schemaName,
7682
columns: newRows.columns,
7783
primaryKeyColumns: newTab.tableContext.primaryKeyColumns.isEmpty
7884
? newRows.columns.prefix(1).map { $0 }

TablePro/Views/Main/Extensions/MainContentView+EventHandlers.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ extension MainContentView {
8686

8787
changeManager.configureForTable(
8888
tableName: tab.tableContext.tableName ?? "",
89+
schemaName: tab.tableContext.schemaName,
8990
columns: newColumns,
9091
primaryKeyColumns: tab.tableContext.primaryKeyColumns,
9192
databaseType: connection.type

0 commit comments

Comments
 (0)