@@ -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