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 @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- SQL Server: connections work when the login can only reach its own database, such as an Azure SQL contained user. The database is now sent during login. Previously it was switched afterward, which the server rejected with a "Login failed" error.
- Custom Copy and Cut shortcuts now take effect in the SQL editor.
- The Delete shortcut in the data grid now follows a custom binding.
- Find Next (Cmd+G) and Find Previous (Cmd+Shift+G) now work in the editor.
Expand Down
36 changes: 23 additions & 13 deletions Plugins/MSSQLDriverPlugin/FreeTDSConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,16 @@ nonisolated final class FreeTDSConnection: @unchecked Sendable {
}
defer { dbloginfree(login) }

_ = dbsetlname(login, options.user, Int32(DBSETUSER))
_ = dbsetlname(login, options.password, Int32(DBSETPWD))
_ = dbsetlname(login, options.applicationName, Int32(DBSETAPP))
_ = dbsetlname(login, "us_english", Int32(DBSETNATLANG))
_ = dbsetlname(login, "UTF-8", Int32(DBSETCHARSET))
for parameter in MSSQLLoginParameters.build(
user: options.user,
password: options.password,
applicationName: options.applicationName,
encryptionFlag: options.encryptionFlag,
database: options.database
) {
_ = dbsetlname(login, parameter.value, parameter.field.dbsetName)
}
_ = dbsetlversion(login, UInt8(DBVERSION_74))
_ = dbsetlname(login, options.encryptionFlag, Int32(DBSETENCRYPT))

// dbsetlogintime is process-global; setting before dbopen bounds this call. Concurrent
// connectSync from another FreeTDSConnection would race, but the serial connect queue and
Expand All @@ -172,13 +175,6 @@ nonisolated final class FreeTDSConnection: @unchecked Sendable {
throw MSSQLCoreError.connectionFailed("Failed to connect to \(options.host):\(options.port): \(msg)")
}

if !options.database.isEmpty {
if dbuse(proc, options.database) == FAIL {
_ = dbclose(proc)
throw MSSQLCoreError.connectionFailed("Cannot open database '\(options.database)'")
}
}

self.dbproc = proc
lock.lock()
_isConnected = true
Expand Down Expand Up @@ -551,3 +547,17 @@ nonisolated final class FreeTDSConnection: @unchecked Sendable {
return nil
}
}

private extension MSSQLLoginField {
var dbsetName: Int32 {
switch self {
case .user: return Int32(DBSETUSER)
case .password: return Int32(DBSETPWD)
case .application: return Int32(DBSETAPP)
case .nationalLanguage: return Int32(DBSETNATLANG)
case .charset: return Int32(DBSETCHARSET)
case .encryption: return Int32(DBSETENCRYPT)
case .database: return Int32(DBSETDBNAME)
}
}
}
42 changes: 42 additions & 0 deletions Plugins/MSSQLDriverPlugin/MSSQLLoginParameters.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Foundation

enum MSSQLLoginField: Equatable {
case user
case password
case application
case nationalLanguage
case charset
case encryption
case database
}

struct MSSQLLoginParameter: Equatable {
let field: MSSQLLoginField
let value: String
}

enum MSSQLLoginParameters {
static let nationalLanguage = "us_english"
static let charset = "UTF-8"

static func build(
user: String,
password: String,
applicationName: String,
encryptionFlag: String,
database: String
) -> [MSSQLLoginParameter] {
var parameters = [
MSSQLLoginParameter(field: .user, value: user),
MSSQLLoginParameter(field: .password, value: password),
MSSQLLoginParameter(field: .application, value: applicationName),
MSSQLLoginParameter(field: .nationalLanguage, value: nationalLanguage),
MSSQLLoginParameter(field: .charset, value: charset),
MSSQLLoginParameter(field: .encryption, value: encryptionFlag)
]
if !database.isEmpty {
parameters.append(MSSQLLoginParameter(field: .database, value: database))
}
return parameters
}
}
4 changes: 4 additions & 0 deletions TableProMobile/TableProMobile.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
5AD1F1B82FB4456900296783 /* FreeTDS.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5AD1F1B72FB4456900296783 /* FreeTDS.xcframework */; };
5AD0CCDB2F0000000000F002 /* DuckDB.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5AD0CCDB2F0000000000F001 /* DuckDB.xcframework */; };
5AD1F2002FB5500000000002 /* FreeTDSConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AD1F2002FB5500000000001 /* FreeTDSConnection.swift */; };
5AD1F2012FB5500000000002 /* MSSQLLoginParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AD1F2012FB5500000000001 /* MSSQLLoginParameters.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -532,6 +533,7 @@
5AD1F1B72FB4456900296783 /* FreeTDS.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = FreeTDS.xcframework; path = ../Libs/ios/FreeTDS.xcframework; sourceTree = "<group>"; };
5AD0CCDB2F0000000000F001 /* DuckDB.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = DuckDB.xcframework; path = ../Libs/ios/DuckDB.xcframework; sourceTree = "<group>"; };
5AD1F2002FB5500000000001 /* FreeTDSConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = FreeTDSConnection.swift; path = ../Plugins/MSSQLDriverPlugin/FreeTDSConnection.swift; sourceTree = "<group>"; };
5AD1F2012FB5500000000001 /* MSSQLLoginParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MSSQLLoginParameters.swift; path = ../Plugins/MSSQLDriverPlugin/MSSQLLoginParameters.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
Expand Down Expand Up @@ -1681,6 +1683,7 @@
isa = PBXGroup;
children = (
5AD1F2002FB5500000000001 /* FreeTDSConnection.swift */,
5AD1F2012FB5500000000001 /* MSSQLLoginParameters.swift */,
);
name = "Recovered References";
sourceTree = "<group>";
Expand Down Expand Up @@ -1848,6 +1851,7 @@
buildActionMask = 2147483647;
files = (
5AD1F2002FB5500000000002 /* FreeTDSConnection.swift in Sources */,
5AD1F2012FB5500000000002 /* MSSQLLoginParameters.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
1 change: 1 addition & 0 deletions TableProTests/PluginTestSources/MSSQLLoginParameters.swift
46 changes: 46 additions & 0 deletions TableProTests/Plugins/MSSQLLoginParametersTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// MSSQLLoginParametersTests.swift
// TableProTests
//

import Foundation
import Testing

@Suite("MSSQLLoginParameters.build")
struct MSSQLLoginParametersTests {
private func build(database: String) -> [MSSQLLoginParameter] {
MSSQLLoginParameters.build(
user: "carrier",
password: "secret",
applicationName: "TablePro",
encryptionFlag: "require",
database: database
)
}

@Test("includes the database in the login packet when set")
func includesDatabaseWhenSet() {
let parameters = build(database: "tmsdevdb1")
#expect(parameters.contains(MSSQLLoginParameter(field: .database, value: "tmsdevdb1")))
}

@Test("omits the database when blank")
func omitsDatabaseWhenBlank() {
let fields = build(database: "").map(\.field)
#expect(!fields.contains(.database))
}

@Test("carries the credentials and encryption flag")
func carriesCredentials() {
let parameters = build(database: "tmsdevdb1")
#expect(parameters.contains(MSSQLLoginParameter(field: .user, value: "carrier")))
#expect(parameters.contains(MSSQLLoginParameter(field: .password, value: "secret")))
#expect(parameters.contains(MSSQLLoginParameter(field: .encryption, value: "require")))
}

@Test("sets us_english language to settle the initial login state")
func setsNationalLanguage() {
let parameters = build(database: "tmsdevdb1")
#expect(parameters.contains(MSSQLLoginParameter(field: .nationalLanguage, value: "us_english")))
}
}
4 changes: 2 additions & 2 deletions docs/databases/mssql.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Run `scripts/build-freetds.sh` first. Click **New Connection**, select **SQL Ser
| **Host** | `localhost` | |
| **Port** | `1433` | |
| **Username** | `sa` | SQL Server Authentication only (no Windows Auth) |
| **Database** | - | Optional. Leave empty to browse all databases. Switch with **Cmd+K** |
| **Database** | - | Optional. Leave empty to browse all databases. Switch with **Cmd+K**. Required for logins scoped to a single database, such as Azure SQL contained users |

## Example Configurations

Expand Down Expand Up @@ -96,7 +96,7 @@ For Azure SQL Database, pick **Required** or stricter. For SQL Server 2017+ on a

**Connection refused**: Enable TCP/IP in SQL Server Configuration Manager, verify SQL Server service running, check firewall port 1433, ensure Docker started.

**Login failed**: Verify credentials, check mixed-mode authentication enabled: `SELECT SERVERPROPERTY('IsIntegratedSecurityOnly');` Set to 2 for mixed mode.
**Login failed**: Verify credentials, check mixed-mode authentication enabled: `SELECT SERVERPROPERTY('IsIntegratedSecurityOnly');` Set to 2 for mixed mode. If the login only has access to one database (an Azure SQL contained user), set the **Database** field. TablePro sends it during login; without it the server authenticates against `master` and rejects the login.

**FreeTDS not found**: Run `scripts/build-freetds.sh` to compile and place library.

Expand Down
Loading