diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d871bd8e..bc32c0cb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - SSH tunnels no longer pin a CPU core after the connection drops. A dropped tunnel is now detected and torn down instead of spinning in its relay loop. (#1769) +- SQL Server table browsing and edits now use the active schema when opening objects outside `dbo`. (#1774) ## [0.53.0] - 2026-06-25 diff --git a/Packages/TableProCore/Sources/TableProMSSQLCore/MSSQLSchemaQueries.swift b/Packages/TableProCore/Sources/TableProMSSQLCore/MSSQLSchemaQueries.swift index 2224683bd..a46fdbb33 100644 --- a/Packages/TableProCore/Sources/TableProMSSQLCore/MSSQLSchemaQueries.swift +++ b/Packages/TableProCore/Sources/TableProMSSQLCore/MSSQLSchemaQueries.swift @@ -20,6 +20,16 @@ public enum MSSQLSchemaQueries { return bracketed(schema: schema, table: table) } + public static func resolvedObjectSchema(_ schema: String?, currentSchema: String?) -> String? { + if let schema, !schema.isEmpty { + return schema + } + guard let currentSchema, !currentSchema.isEmpty else { + return nil + } + return currentSchema + } + public static func browse( schema: String?, table: String, diff --git a/Packages/TableProCore/Tests/TableProMSSQLCoreTests/MSSQLSchemaQueriesTests.swift b/Packages/TableProCore/Tests/TableProMSSQLCoreTests/MSSQLSchemaQueriesTests.swift index 43c552ccb..c857b9eff 100644 --- a/Packages/TableProCore/Tests/TableProMSSQLCoreTests/MSSQLSchemaQueriesTests.swift +++ b/Packages/TableProCore/Tests/TableProMSSQLCoreTests/MSSQLSchemaQueriesTests.swift @@ -132,6 +132,20 @@ final class MSSQLSchemaQueriesTests: XCTestCase { XCTAssertEqual(MSSQLSchemaQueries.qualifiedName(schema: "", table: "routeCache"), "[routeCache]") } + func testResolvedObjectSchemaPrefersExplicitSchema() { + XCTAssertEqual(MSSQLSchemaQueries.resolvedObjectSchema("audit", currentSchema: "sales"), "audit") + } + + func testResolvedObjectSchemaFallsBackToCurrentSchema() { + XCTAssertEqual(MSSQLSchemaQueries.resolvedObjectSchema(nil, currentSchema: "sales"), "sales") + XCTAssertEqual(MSSQLSchemaQueries.resolvedObjectSchema("", currentSchema: "sales"), "sales") + } + + func testResolvedObjectSchemaReturnsNilWhenBothSchemasAreBlank() { + XCTAssertNil(MSSQLSchemaQueries.resolvedObjectSchema(nil, currentSchema: nil)) + XCTAssertNil(MSSQLSchemaQueries.resolvedObjectSchema("", currentSchema: "")) + } + func testBrowseQualifiesNonDefaultSchema() { let sql = MSSQLSchemaQueries.browse( schema: "sales", table: "routeCache", @@ -143,6 +157,18 @@ final class MSSQLSchemaQueriesTests: XCTestCase { ) } + func testBrowseUsesResolvedCurrentSchema() { + let schema = MSSQLSchemaQueries.resolvedObjectSchema(nil, currentSchema: "sales") + let sql = MSSQLSchemaQueries.browse( + schema: schema, 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", diff --git a/Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift b/Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift index 1fcdfdc5a..b84250ece 100644 --- a/Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift +++ b/Plugins/MSSQLDriverPlugin/MSSQLPlugin.swift @@ -330,7 +330,7 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable { deletedRowIndices: Set, insertedRowIndices: Set ) -> [(statement: String, parameters: [PluginCellValue])]? { - let qualifiedTable = MSSQLSchemaQueries.qualifiedName(schema: schema, table: table) + let qualifiedTable = MSSQLSchemaQueries.qualifiedName(schema: resolvedObjectSchema(schema), table: table) var statements: [(statement: String, parameters: [PluginCellValue])] = [] var deleteChanges: [PluginRowChange] = [] @@ -598,7 +598,7 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable { sortColumns: sortColumns, columns: columns, quoteIdentifier: mssqlQuoteIdentifier ) ?? "ORDER BY (SELECT NULL)" return MSSQLSchemaQueries.browse( - schema: schema, table: table, orderByClause: orderBy, offset: offset, limit: limit + schema: resolvedObjectSchema(schema), table: table, orderByClause: orderBy, offset: offset, limit: limit ) } @@ -640,7 +640,7 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable { sortColumns: sortColumns, columns: columns, quoteIdentifier: mssqlQuoteIdentifier ) ?? "ORDER BY (SELECT NULL)" return MSSQLSchemaQueries.filtered( - schema: schema, table: table, whereClause: whereClause, + schema: resolvedObjectSchema(schema), table: table, whereClause: whereClause, orderByClause: orderBy, offset: offset, limit: limit ) } @@ -651,6 +651,10 @@ final class MSSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable { quoteIdentifier(identifier) } + private func resolvedObjectSchema(_ schema: String?) -> String? { + MSSQLSchemaQueries.resolvedObjectSchema(schema, currentSchema: _currentSchema) + } + private func mssqlEscapeValue(_ value: String) -> String { let trimmed = value.trimmingCharacters(in: .whitespaces) if trimmed.caseInsensitiveCompare("NULL") == .orderedSame { return "NULL" }