@@ -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
2138public 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 {
134161public 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
530576private 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
539586private 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