Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- 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)
- 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)
- 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)

## [0.52.1] - 2026-06-22

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,41 @@ public enum MSSQLSchemaQueries {
"[\(escapeBracket(schema))].[\(escapeBracket(table))]"
}

public static func qualifiedName(schema: String?, table: String) -> String {
guard let schema, !schema.isEmpty else {
return "[\(escapeBracket(table))]"
}
return bracketed(schema: schema, table: table)
}

public static func browse(
schema: String?,
table: String,
orderByClause: String,
offset: Int,
limit: Int
) -> String {
let target = qualifiedName(schema: schema, table: table)
return "SELECT * FROM \(target) \(orderByClause) OFFSET \(offset) ROWS FETCH NEXT \(limit) ROWS ONLY"
}

public static func filtered(
schema: String?,
table: String,
whereClause: String,
orderByClause: String,
offset: Int,
limit: Int
) -> String {
let target = qualifiedName(schema: schema, table: table)
var query = "SELECT * FROM \(target)"
if !whereClause.isEmpty {
query += " WHERE \(whereClause)"
}
query += " \(orderByClause) OFFSET \(offset) ROWS FETCH NEXT \(limit) ROWS ONLY"
return query
}

public static let currentSchema = "SELECT SCHEMA_NAME()"
public static let serverVersion = "SELECT @@VERSION"
public static let beginTransaction = "BEGIN TRANSACTION"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,57 @@ final class MSSQLSchemaQueriesTests: XCTestCase {
XCTAssertEqual(parsed?.referencedTable, "users")
XCTAssertEqual(parsed?.referencedColumn, "id")
}

func testQualifiedNamePrefixesSchema() {
XCTAssertEqual(MSSQLSchemaQueries.qualifiedName(schema: "sales", table: "routeCache"), "[sales].[routeCache]")
}

func testQualifiedNameOmitsSchemaWhenNilOrEmpty() {
XCTAssertEqual(MSSQLSchemaQueries.qualifiedName(schema: nil, table: "routeCache"), "[routeCache]")
XCTAssertEqual(MSSQLSchemaQueries.qualifiedName(schema: "", table: "routeCache"), "[routeCache]")
}

func testBrowseQualifiesNonDefaultSchema() {
let sql = MSSQLSchemaQueries.browse(
schema: "sales", table: "routeCache",
orderByClause: "ORDER BY (SELECT NULL)", offset: 0, limit: 200
)
XCTAssertEqual(
sql,
"SELECT * FROM [sales].[routeCache] ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT 200 ROWS ONLY"
)
}

func testBrowseWithoutSchemaStaysUnqualified() {
let sql = MSSQLSchemaQueries.browse(
schema: nil, table: "routeCache",
orderByClause: "ORDER BY (SELECT NULL)", offset: 10, limit: 50
)
XCTAssertEqual(
sql,
"SELECT * FROM [routeCache] ORDER BY (SELECT NULL) OFFSET 10 ROWS FETCH NEXT 50 ROWS ONLY"
)
}

func testFilteredQualifiesSchemaAndAppendsWhere() {
let sql = MSSQLSchemaQueries.filtered(
schema: "sales", table: "routeCache", whereClause: "[id] = 1",
orderByClause: "ORDER BY (SELECT NULL)", offset: 0, limit: 200
)
XCTAssertEqual(
sql,
"SELECT * FROM [sales].[routeCache] WHERE [id] = 1 ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT 200 ROWS ONLY"
)
}

func testFilteredOmitsWhenWhereClauseEmpty() {
let sql = MSSQLSchemaQueries.filtered(
schema: "sales", table: "routeCache", whereClause: "",
orderByClause: "ORDER BY (SELECT NULL)", offset: 0, limit: 200
)
XCTAssertEqual(
sql,
"SELECT * FROM [sales].[routeCache] ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT 200 ROWS ONLY"
)
}
}
88 changes: 66 additions & 22 deletions Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,24 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
deletedRowIndices: Set<Int>,
insertedRowIndices: Set<Int>
) -> [(statement: String, parameters: [PluginCellValue])]? {
generateStatements(
table: table, schema: nil, columns: columns, primaryKeyColumns: primaryKeyColumns,
changes: changes, insertedRowData: insertedRowData,
deletedRowIndices: deletedRowIndices, insertedRowIndices: insertedRowIndices
)
}

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])]? {
let qualifiedTable = MSSQLSchemaQueries.qualifiedName(schema: schema, table: table)
var statements: [(statement: String, parameters: [PluginCellValue])] = []

var deleteChanges: [PluginRowChange] = []
Expand All @@ -322,13 +340,15 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
case .insert:
guard insertedRowIndices.contains(change.rowIndex) else { continue }
if let values = insertedRowData[change.rowIndex] {
if let stmt = generateMssqlInsert(table: table, columns: columns, values: values) {
if let stmt = generateMssqlInsert(
table: table, qualifiedTable: qualifiedTable, columns: columns, values: values
) {
statements.append(stmt)
}
}
case .update:
if let stmt = generateMssqlUpdate(
table: table, columns: columns,
qualifiedTable: qualifiedTable, columns: columns,
primaryKeyColumns: primaryKeyColumns, change: change
) {
statements.append(stmt)
Expand All @@ -342,7 +362,7 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
if !deleteChanges.isEmpty {
for change in deleteChanges {
if let stmt = generateMssqlDelete(
table: table, columns: columns,
qualifiedTable: qualifiedTable, columns: columns,
primaryKeyColumns: primaryKeyColumns, change: change
) {
statements.append(stmt)
Expand All @@ -355,6 +375,7 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {

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

let columnList = nonDefaultColumns.joined(separator: ", ")
let placeholders = parameters.map { _ in "?" }.joined(separator: ", ")
let escapedTable = "[\(table.replacingOccurrences(of: "]", with: "]]"))]"
let sql = "INSERT INTO \(escapedTable) (\(columnList)) VALUES (\(placeholders))"
let sql = "INSERT INTO \(qualifiedTable) (\(columnList)) VALUES (\(placeholders))"
return (statement: sql, parameters: parameters)
}

private func generateMssqlUpdate(
table: String,
qualifiedTable: String,
columns: [String],
primaryKeyColumns: [String],
change: PluginRowChange
) -> (statement: String, parameters: [PluginCellValue])? {
guard !change.cellChanges.isEmpty else { return nil }
guard let originalRow = change.originalRow else { return nil }

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

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

let whereClause = conditions.joined(separator: " AND ")
let topClause = primaryKeyColumns.isEmpty ? "TOP (1) " : ""
let sql = "UPDATE \(topClause)\(escapedTable) SET \(setClauses) WHERE \(whereClause)"
let sql = "UPDATE \(topClause)\(qualifiedTable) SET \(setClauses) WHERE \(whereClause)"
return (statement: sql, parameters: parameters)
}

private func generateMssqlDelete(
table: String,
qualifiedTable: String,
columns: [String],
primaryKeyColumns: [String],
change: PluginRowChange
) -> (statement: String, parameters: [PluginCellValue])? {
guard let originalRow = change.originalRow else { return nil }

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

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

let whereClause = conditions.joined(separator: " AND ")
let topClause = primaryKeyColumns.isEmpty ? "TOP (1) " : ""
let sql = "DELETE \(topClause)FROM \(escapedTable) WHERE \(whereClause)"
let sql = "DELETE \(topClause)FROM \(qualifiedTable) WHERE \(whereClause)"
return (statement: sql, parameters: parameters)
}

Expand Down Expand Up @@ -562,13 +580,26 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
limit: Int,
offset: Int
) -> String? {
let quotedTable = mssqlQuoteIdentifier(table)
var query = "SELECT * FROM \(quotedTable)"
buildBrowseQuery(
table: table, schema: nil, sortColumns: sortColumns,
columns: columns, limit: limit, offset: offset
)
}

func buildBrowseQuery(
table: String,
schema: String?,
sortColumns: [(columnIndex: Int, ascending: Bool)],
columns: [String],
limit: Int,
offset: Int
) -> String? {
let orderBy = PluginSQLFilter.buildOrderByClause(
sortColumns: sortColumns, columns: columns, quoteIdentifier: mssqlQuoteIdentifier
) ?? "ORDER BY (SELECT NULL)"
query += " \(orderBy) OFFSET \(offset) ROWS FETCH NEXT \(limit) ROWS ONLY"
return query
return MSSQLSchemaQueries.browse(
schema: schema, table: table, orderByClause: orderBy, offset: offset, limit: limit
)
}

func buildFilteredQuery(
Expand All @@ -580,8 +611,22 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
limit: Int,
offset: Int
) -> String? {
let quotedTable = mssqlQuoteIdentifier(table)
var query = "SELECT * FROM \(quotedTable)"
buildFilteredQuery(
table: table, schema: nil, filters: filters, logicMode: logicMode,
sortColumns: sortColumns, columns: columns, limit: limit, offset: offset
)
}

func buildFilteredQuery(
table: String,
schema: String?,
filters: [(column: String, op: String, value: String)],
logicMode: String,
sortColumns: [(columnIndex: Int, ascending: Bool)],
columns: [String],
limit: Int,
offset: Int
) -> String? {
let whereClause = PluginSQLFilter.buildWhereClause(
filters: filters,
logicMode: logicMode,
Expand All @@ -591,14 +636,13 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable {
"\(quoted) LIKE '%\(value.replacingOccurrences(of: "'", with: "''"))%'"
}
)
if !whereClause.isEmpty {
query += " WHERE \(whereClause)"
}
let orderBy = PluginSQLFilter.buildOrderByClause(
sortColumns: sortColumns, columns: columns, quoteIdentifier: mssqlQuoteIdentifier
) ?? "ORDER BY (SELECT NULL)"
query += " \(orderBy) OFFSET \(offset) ROWS FETCH NEXT \(limit) ROWS ONLY"
return query
return MSSQLSchemaQueries.filtered(
schema: schema, table: table, whereClause: whereClause,
orderByClause: orderBy, offset: offset, limit: limit
)
}

// MARK: - Query Building Helpers
Expand Down
Loading
Loading