Skip to content

Commit a35a43c

Browse files
committed
Improve error reporting by capturing detailed FreeTDS messages
1 parent 2535416 commit a35a43c

File tree

1 file changed

+58
-11
lines changed

1 file changed

+58
-11
lines changed

Sources/SQLClientSwift/SQLClient.swift

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,32 @@ public enum SQLClientMessageKey {
1616
public static let severity = "severity"
1717
}
1818

19+
// MARK: - Error Capture
20+
21+
nonisolated(unsafe) private var lastFreeTDSError: String?
22+
private let lastErrorLock = NSLock()
23+
24+
private func setLastFreeTDSError(_ msg: String) {
25+
lastErrorLock.lock()
26+
lastFreeTDSError = msg
27+
lastErrorLock.unlock()
28+
}
29+
30+
private func getLastFreeTDSError() -> String? {
31+
lastErrorLock.lock()
32+
defer { lastErrorLock.unlock() }
33+
return lastFreeTDSError
34+
}
35+
1936
// MARK: - Errors
2037

2138
public enum SQLClientError: Error, LocalizedError {
2239
case alreadyConnected
2340
case notConnected
2441
case loginAllocationFailed
25-
case connectionFailed(server: String)
26-
case databaseSelectionFailed(String)
27-
case executionFailed
42+
case connectionFailed(server: String, detail: String? = nil)
43+
case databaseSelectionFailed(String, detail: String? = nil)
44+
case executionFailed(detail: String? = nil)
2845
case noCommandText
2946
case parameterCountMismatch
3047

@@ -33,9 +50,19 @@ public enum SQLClientError: Error, LocalizedError {
3350
case .alreadyConnected: return "Already connected to a server. Call disconnect() first."
3451
case .notConnected: return "Not connected. Call connect() before executing queries."
3552
case .loginAllocationFailed: return "FreeTDS could not allocate a login record."
36-
case .connectionFailed(let s): return "Could not connect to '\(s)'."
37-
case .databaseSelectionFailed(let db): return "Could not select database '\(db)'."
38-
case .executionFailed: return "SQL execution failed. Check SQLClientMessage notifications for details."
53+
case .connectionFailed(let s, let d):
54+
var msg = "Could not connect to '\(s)'."
55+
if let d = d, !d.isEmpty { msg += " (\(d))" }
56+
return msg
57+
case .databaseSelectionFailed(let db, let d):
58+
var msg = "Could not select database '\(db)'."
59+
if let d = d, !d.isEmpty { msg += " (\(d))" }
60+
return msg
61+
case .executionFailed(let d):
62+
var msg = "SQL execution failed."
63+
if let d = d, !d.isEmpty { msg += " (\(d))" }
64+
else { msg += " Check SQLClientMessage notifications for details." }
65+
return msg
3966
case .noCommandText: return "SQL command string was empty."
4067
case .parameterCountMismatch: return "Number of parameters does not match number of placeholders."
4168
}
@@ -134,6 +161,12 @@ private struct TDSHandle: @unchecked Sendable {
134161
public actor SQLClient {
135162
public static let shared = SQLClient()
136163

164+
/// Global debug flag, enabled via --debug argument or SQL_CLIENT_DEBUG env var.
165+
public static let debugEnabled: Bool = {
166+
ProcessInfo.processInfo.arguments.contains("--debug") ||
167+
ProcessInfo.processInfo.environment["SQL_CLIENT_DEBUG"] != nil
168+
}()
169+
137170
private static let initializeFreeTDS: Void = {
138171
dbinit()
139172
dberrhandle(SQLClient_errorHandler)
@@ -281,8 +314,11 @@ public actor SQLClient {
281314
}
282315

283316
private nonisolated func _connectSync(options: SQLClientConnectionOptions) throws -> (login: TDSHandle, connection: TDSHandle) {
317+
setLastFreeTDSError("")
318+
if SQLClient.debugEnabled { print("DEBUG: _connectSync - dblogin()") }
284319
guard let lgn = dblogin() else { throw SQLClientError.loginAllocationFailed }
285320

321+
if SQLClient.debugEnabled { print("DEBUG: _connectSync - setting login options") }
286322
dbsetlname(lgn, options.username, 2) // DBSETUSER
287323
dbsetlname(lgn, options.password, 3) // DBSETPWD
288324
dbsetlname(lgn, "SQLClientSwift", 5) // DBSETAPP
@@ -298,16 +334,23 @@ public actor SQLClient {
298334
if options.useUTF16 { dbsetlbool(lgn, 1, 1001) } // DBSETUTF16
299335
if options.loginTimeout > 0 { dbsetlogintime(Int32(options.loginTimeout)) }
300336

337+
if SQLClient.debugEnabled { print("DEBUG: _connectSync - dbopen(\(options.server))") }
301338
guard let conn = dbopen(lgn, options.server) else {
339+
let detail = getLastFreeTDSError()
340+
if SQLClient.debugEnabled { print("DEBUG: _connectSync - dbopen failed: \(detail ?? "unknown")") }
302341
dbloginfree(lgn)
303-
throw SQLClientError.connectionFailed(server: options.server)
342+
throw SQLClientError.connectionFailed(server: options.server, detail: detail)
304343
}
344+
if SQLClient.debugEnabled { print("DEBUG: _connectSync - dbopen success") }
305345

306346
if let db = options.database, !db.isEmpty {
347+
if SQLClient.debugEnabled { print("DEBUG: _connectSync - dbuse(\(db))") }
307348
guard dbuse(conn, db) != FAIL else {
349+
let detail = getLastFreeTDSError()
350+
if SQLClient.debugEnabled { print("DEBUG: _connectSync - dbuse failed: \(detail ?? "unknown")") }
308351
dbclose(conn)
309352
dbloginfree(lgn)
310-
throw SQLClientError.databaseSelectionFailed(db)
353+
throw SQLClientError.databaseSelectionFailed(db, detail: detail)
311354
}
312355
}
313356

@@ -320,14 +363,17 @@ public actor SQLClient {
320363
}
321364

322365
private nonisolated func _executeSync(sql: String, connection: TDSHandle, maxTextSize: Int) throws -> SQLClientResult {
366+
setLastFreeTDSError("")
323367
let conn = connection.pointer
324368

325369
// Ensure any previous results are cancelled before a new command
326370
dbcancel(conn)
327371

328372
_ = dbsetopt(conn, DBTEXTSIZE, "\(maxTextSize)", -1)
329373

330-
guard dbcmd(conn, sql) != FAIL, dbsqlexec(conn) != FAIL else { throw SQLClientError.executionFailed }
374+
guard dbcmd(conn, sql) != FAIL, dbsqlexec(conn) != FAIL else {
375+
throw SQLClientError.executionFailed(detail: getLastFreeTDSError())
376+
}
331377

332378
var tables: [[SQLRow]] = []
333379
var totalAffected: Int = -1
@@ -529,7 +575,8 @@ public actor SQLClient {
529575

530576
private func SQLClient_errorHandler(dbproc: OpaquePointer?, severity: Int32, dberr: Int32, oserr: Int32, dberrstr: UnsafeMutablePointer<CChar>?, oserrstr: UnsafeMutablePointer<CChar>?) -> Int32 {
531577
let msg = dberrstr.map { String(cString: $0) } ?? "Unknown FreeTDS error"
532-
if ProcessInfo.processInfo.environment["SQL_CLIENT_DEBUG"] != nil {
578+
setLastFreeTDSError("[\(dberr)] \(msg)")
579+
if SQLClient.debugEnabled {
533580
print("DEBUG SQL Error: [\(dberr)] \(msg) (severity: \(severity))")
534581
}
535582
NotificationCenter.default.post(name: .SQLClientMessage, object: nil, userInfo: [SQLClientMessageKey.code: Int(dberr), SQLClientMessageKey.message: msg, SQLClientMessageKey.severity: Int(severity)])
@@ -538,7 +585,7 @@ private func SQLClient_errorHandler(dbproc: OpaquePointer?, severity: Int32, dbe
538585

539586
private func SQLClient_messageHandler(dbproc: OpaquePointer?, msgno: DBINT, msgstate: Int32, severity: Int32, msgtext: UnsafeMutablePointer<CChar>?, srvname: UnsafeMutablePointer<CChar>?, proc: UnsafeMutablePointer<CChar>?, line: Int32) -> Int32 {
540587
let msg = msgtext.map { String(cString: $0) } ?? ""
541-
if severity > 0 && ProcessInfo.processInfo.environment["SQL_CLIENT_DEBUG"] != nil {
588+
if severity > 0 && SQLClient.debugEnabled {
542589
print("DEBUG SQL Message: [\(msgno)] \(msg) (severity: \(severity))")
543590
}
544591
NotificationCenter.default.post(name: .SQLClientMessage, object: nil, userInfo: [SQLClientMessageKey.code: Int(msgno), SQLClientMessageKey.message: msg, SQLClientMessageKey.severity: Int(severity)])

0 commit comments

Comments
 (0)