@@ -100,41 +100,69 @@ final class MSSQLPlugin: NSObject, TableProPlugin, DriverPlugin {
100100
101101// MARK: - Global FreeTDS initialization
102102
103- private let freetdsLastErrorLock = NSLock ( )
104- private var _freetdsLastError = " "
103+ /// Per-connection error storage keyed by DBPROCESS pointer.
104+ /// Falls back to a global error string when the DBPROCESS is nil (pre-connection errors).
105+ private let freetdsErrorLock = NSLock ( )
106+ private var freetdsConnectionErrors : [ UnsafeRawPointer : String ] = [ : ]
107+ private var freetdsGlobalError = " "
108+
109+ private func freetdsGetError( for dbproc: UnsafeMutablePointer < DBPROCESS > ? ) -> String {
110+ freetdsErrorLock. lock ( )
111+ defer { freetdsErrorLock. unlock ( ) }
112+ if let dbproc {
113+ return freetdsConnectionErrors [ UnsafeRawPointer ( dbproc) ] ?? freetdsGlobalError
114+ }
115+ return freetdsGlobalError
116+ }
105117
106- private var freetdsLastError : String {
107- get {
108- freetdsLastErrorLock. lock ( )
109- defer { freetdsLastErrorLock. unlock ( ) }
110- return _freetdsLastError
118+ private func freetdsClearError( for dbproc: UnsafeMutablePointer < DBPROCESS > ? ) {
119+ freetdsErrorLock. lock ( )
120+ defer { freetdsErrorLock. unlock ( ) }
121+ if let dbproc {
122+ freetdsConnectionErrors [ UnsafeRawPointer ( dbproc) ] = nil
123+ } else {
124+ freetdsGlobalError = " "
111125 }
112- set {
113- freetdsLastErrorLock. lock ( )
114- defer { freetdsLastErrorLock. unlock ( ) }
115- _freetdsLastError = newValue
126+ }
127+
128+ private func freetdsSetError( _ msg: String , for dbproc: UnsafeMutablePointer < DBPROCESS > ? , overwrite: Bool = false ) {
129+ freetdsErrorLock. lock ( )
130+ defer { freetdsErrorLock. unlock ( ) }
131+ if let dbproc {
132+ let key = UnsafeRawPointer ( dbproc)
133+ if overwrite || ( freetdsConnectionErrors [ key] ? . isEmpty ?? true ) {
134+ freetdsConnectionErrors [ key] = msg
135+ }
136+ } else if overwrite || freetdsGlobalError. isEmpty {
137+ freetdsGlobalError = msg
116138 }
117139}
118140
141+ private func freetdsUnregister( _ dbproc: UnsafeMutablePointer < DBPROCESS > ) {
142+ freetdsErrorLock. lock ( )
143+ defer { freetdsErrorLock. unlock ( ) }
144+ freetdsConnectionErrors. removeValue ( forKey: UnsafeRawPointer ( dbproc) )
145+ }
146+
119147private let freetdsLogger = Logger ( subsystem: " com.TablePro " , category: " FreeTDSConnection " )
120148
121149private let freetdsInitOnce : Void = {
122150 _ = dbinit ( )
123- _ = dberrhandle { _ , _, dberr, _, dberrstr, oserrstr in
151+ _ = dberrhandle { dbproc , _, dberr, _, dberrstr, oserrstr in
124152 var msg = " db-lib error \( dberr) "
125153 if let s = dberrstr { msg += " : \( String ( cString: s) ) " }
126154 if let s = oserrstr, String ( cString: s) != " Success " { msg += " (os: \( String ( cString: s) ) ) " }
127155 freetdsLogger. error ( " FreeTDS: \( msg) " )
128- if freetdsLastError. isEmpty {
129- freetdsLastError = msg
130- }
156+ freetdsSetError ( msg, for: dbproc)
131157 return INT_CANCEL
132158 }
133- _ = dbmsghandle { _ , msgno, _, severity, msgtext, _, _, _ in
159+ _ = dbmsghandle { dbproc , msgno, _, severity, msgtext, _, _, _ in
134160 guard let text = msgtext else { return 0 }
135161 let msg = String ( cString: text)
136162 if severity > 10 {
137- freetdsLastError = msg
163+ // SQL Server sends informational messages first, error messages last —
164+ // overwrite so the most specific error is kept
165+ freetdsSetError ( msg, for: dbproc, overwrite: true )
138166 freetdsLogger. error ( " FreeTDS msg \( msgno) sev \( severity) : \( msg) " )
139167 } else {
140168 freetdsLogger. debug ( " FreeTDS msg \( msgno) : \( msg) " )
@@ -200,11 +228,12 @@ private final class FreeTDSConnection: @unchecked Sendable {
200228 _ = dbsetlname ( login, " UTF-8 " , Int32 ( DBSETCHARSET) )
201229 _ = dbsetlversion ( login, UInt8 ( DBVERSION_74) )
202230
203- freetdsLastError = " "
231+ freetdsClearError ( for : nil )
204232 let serverName = " \( host) : \( port) "
205233 guard let proc = dbopen ( login, serverName) else {
206- let detail = freetdsLastError. isEmpty ? " Check host, port, and credentials " : freetdsLastError
207- throw MSSQLPluginError . connectionFailed ( " Failed to connect to \( host) : \( port) — \( detail) " )
234+ let detail = freetdsGetError ( for: nil )
235+ let msg = detail. isEmpty ? " Check host, port, and credentials " : detail
236+ throw MSSQLPluginError . connectionFailed ( " Failed to connect to \( host) : \( port) — \( msg) " )
208237 }
209238
210239 if !database. isEmpty {
@@ -240,6 +269,7 @@ private final class FreeTDSConnection: @unchecked Sendable {
240269 lock. unlock ( )
241270
242271 if let handle = handle {
272+ freetdsUnregister ( handle)
243273 queue. async {
244274 _ = dbclose ( handle)
245275 }
@@ -274,13 +304,14 @@ private final class FreeTDSConnection: @unchecked Sendable {
274304 _isCancelled = false
275305 lock. unlock ( )
276306
277- freetdsLastError = " "
307+ freetdsClearError ( for : proc )
278308 if dbcmd ( proc, query) == FAIL {
279309 throw MSSQLPluginError . queryFailed ( " Failed to prepare query " )
280310 }
281311 if dbsqlexec ( proc) == FAIL {
282- let detail = freetdsLastError. isEmpty ? " Query execution failed " : freetdsLastError
283- throw MSSQLPluginError . queryFailed ( detail)
312+ let detail = freetdsGetError ( for: proc)
313+ let msg = detail. isEmpty ? " Query execution failed " : detail
314+ throw MSSQLPluginError . queryFailed ( msg)
284315 }
285316
286317 var allColumns : [ String ] = [ ]
0 commit comments