Skip to content

Commit c0d9dde

Browse files
vkuttypCopilot
andcommitted
feat: support SQL Server connection string format
Add MSSQLConnection.Configuration.init(connectionString:) that parses the standard SQL Server connection string format: Server=host,port;Database=db;User Id=sa;Password=secret; Encrypt=True;TrustServerCertificate=True;Connect Timeout=30; Application Intent=ReadOnly; Supported keys (case-insensitive): - Server / Data Source → host, with optional ,port suffix - Database / Initial Catalog → database name - User Id / UID → username - Password / PWD → password - Domain → enables NTLM/Windows auth - Encrypt → True/False/Disable → .require/.prefer/.disable - TrustServerCertificate → True skips cert verification - Connect Timeout → seconds (default 30) - Application Intent → ReadOnly for AG read replicas Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e7d9722 commit c0d9dde

1 file changed

Lines changed: 74 additions & 0 deletions

File tree

Sources/MSSQLNio/MSSQLConnection.swift

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,80 @@ public final class MSSQLConnection: SQLDatabase, @unchecked Sendable {
9797
self.queryTimeout = queryTimeout
9898
self.readOnly = readOnly
9999
}
100+
101+
/// Initialise from a SQL Server connection string.
102+
///
103+
/// Supported keys (case-insensitive):
104+
/// - `Server` / `Data Source` — host, or `host,port`
105+
/// - `Database` / `Initial Catalog`
106+
/// - `User Id` / `UID`
107+
/// - `Password` / `PWD`
108+
/// - `Domain` — enables NTLM/Windows auth
109+
/// - `Encrypt` — `True` → `.require`, `False` → `.disable`
110+
/// - `TrustServerCertificate` — `True` skips certificate verification
111+
/// - `Connect Timeout` — seconds (default 30)
112+
/// - `Application Intent` — `ReadOnly` sets read-only mode
113+
///
114+
/// Example:
115+
/// ```
116+
/// Server=myServer;Database=myDb;User Id=sa;Password=secret;
117+
/// Encrypt=True;TrustServerCertificate=True;
118+
/// ```
119+
public init(connectionString: String) throws {
120+
// Parse key=value pairs separated by semicolons
121+
var pairs: [String: String] = [:]
122+
for part in connectionString.split(separator: ";", omittingEmptySubsequences: true) {
123+
let kv = part.split(separator: "=", maxSplits: 1)
124+
guard kv.count == 2 else { continue }
125+
let key = kv[0].trimmingCharacters(in: .whitespaces).lowercased()
126+
let value = kv[1].trimmingCharacters(in: .whitespaces)
127+
pairs[key] = value
128+
}
129+
130+
func get(_ keys: String...) -> String? {
131+
keys.first(where: { pairs[$0.lowercased()] != nil }).flatMap { pairs[$0.lowercased()] }
132+
}
133+
func bool(_ keys: String...) -> Bool {
134+
get(keys[0], keys.dropFirst().joined())?.lowercased() == "true"
135+
}
136+
137+
// Server / Data Source — accepts "host" or "host,port"
138+
let serverRaw = get("server", "data source") ?? "localhost"
139+
if serverRaw.contains(",") {
140+
let parts = serverRaw.split(separator: ",", maxSplits: 1)
141+
self.host = String(parts[0]).trimmingCharacters(in: .whitespaces)
142+
self.port = Int(parts[1].trimmingCharacters(in: .whitespaces)) ?? 1433
143+
} else {
144+
self.host = serverRaw
145+
self.port = 1433
146+
}
147+
148+
guard let db = get("database", "initial catalog") else {
149+
throw SQLError.connectionError("Connection string missing 'Database' / 'Initial Catalog'")
150+
}
151+
self.database = db
152+
self.username = get("user id", "uid") ?? ""
153+
self.password = get("password", "pwd") ?? ""
154+
self.domain = get("domain")
155+
156+
// Encrypt → tls
157+
if let enc = get("encrypt") {
158+
switch enc.lowercased() {
159+
case "true", "yes", "mandatory": self.tls = .require
160+
case "false", "no", "optional": self.tls = .prefer
161+
case "disable": self.tls = .disable
162+
default: self.tls = .prefer
163+
}
164+
} else {
165+
self.tls = .prefer
166+
}
167+
168+
self.trustServerCertificate = bool("trustservercertificate")
169+
self.connectTimeout = get("connect timeout", "connection timeout")
170+
.flatMap { Double($0) } ?? 30
171+
self.readOnly = get("applicationintent", "application intent")?
172+
.lowercased() == "readonly"
173+
}
100174
}
101175

102176
// MARK: - Internal state

0 commit comments

Comments
 (0)