@@ -49,6 +49,8 @@ final class DatabaseManager {
4949 /// The health monitor skips pings while a query is running to avoid
5050 /// racing on non-thread-safe driver connections.
5151 @ObservationIgnored private var queriesInFlight : [ UUID : Int ] = [ : ]
52+ /// Tracks when the first query started for each session (used for staleness detection).
53+ @ObservationIgnored private var queryStartTimes : [ UUID : Date ] = [ : ]
5254
5355 /// Current session (computed from currentSessionId)
5456 var currentSession : ConnectionSession ? {
@@ -130,7 +132,11 @@ final class DatabaseManager {
130132 // Close tunnel if SSH was established
131133 if connection. sshConfig. enabled {
132134 Task {
133- try ? await SSHTunnelManager . shared. closeTunnel ( connectionId: connection. id)
135+ do {
136+ try await SSHTunnelManager . shared. closeTunnel ( connectionId: connection. id)
137+ } catch {
138+ Self . logger. warning ( " SSH tunnel cleanup failed for \( connection. name) : \( error. localizedDescription) " )
139+ }
134140 }
135141 }
136142 removeSessionEntry ( for: connection. id)
@@ -220,7 +226,11 @@ final class DatabaseManager {
220226 // Close tunnel if connection failed
221227 if connection. sshConfig. enabled {
222228 Task {
223- try ? await SSHTunnelManager . shared. closeTunnel ( connectionId: connection. id)
229+ do {
230+ try await SSHTunnelManager . shared. closeTunnel ( connectionId: connection. id)
231+ } catch {
232+ Self . logger. warning ( " SSH tunnel cleanup failed for \( connection. name) : \( error. localizedDescription) " )
233+ }
224234 }
225235 }
226236
@@ -256,7 +266,11 @@ final class DatabaseManager {
256266
257267 // Close SSH tunnel if exists
258268 if session. connection. sshConfig. enabled {
259- try ? await SSHTunnelManager . shared. closeTunnel ( connectionId: session. connection. id)
269+ do {
270+ try await SSHTunnelManager . shared. closeTunnel ( connectionId: session. connection. id)
271+ } catch {
272+ Self . logger. warning ( " SSH tunnel cleanup failed for \( session. connection. name) : \( error. localizedDescription) " )
273+ }
260274 }
261275
262276 // Stop health monitoring
@@ -343,11 +357,15 @@ final class DatabaseManager {
343357 operation: ( ) async throws -> T
344358 ) async throws -> T {
345359 queriesInFlight [ sessionId, default: 0 ] += 1
360+ if queriesInFlight [ sessionId] == 1 {
361+ queryStartTimes [ sessionId] = Date ( )
362+ }
346363 defer {
347364 if let count = queriesInFlight [ sessionId] , count > 1 {
348365 queriesInFlight [ sessionId] = count - 1
349366 } else {
350367 queriesInFlight. removeValue ( forKey: sessionId)
368+ queryStartTimes. removeValue ( forKey: sessionId)
351369 }
352370 }
353371 return try await operation ( )
@@ -402,13 +420,21 @@ final class DatabaseManager {
402420 result = try await driver. testConnection ( )
403421 } catch {
404422 if connection. sshConfig. enabled {
405- try ? await SSHTunnelManager . shared. closeTunnel ( connectionId: connection. id)
423+ do {
424+ try await SSHTunnelManager . shared. closeTunnel ( connectionId: connection. id)
425+ } catch {
426+ Self . logger. warning ( " SSH tunnel cleanup failed for \( connection. name) : \( error. localizedDescription) " )
427+ }
406428 }
407429 throw error
408430 }
409431
410432 if connection. sshConfig. enabled {
411- try ? await SSHTunnelManager . shared. closeTunnel ( connectionId: connection. id)
433+ do {
434+ try await SSHTunnelManager . shared. closeTunnel ( connectionId: connection. id)
435+ } catch {
436+ Self . logger. warning ( " SSH tunnel cleanup failed for \( connection. name) : \( error. localizedDescription) " )
437+ }
412438 }
413439
414440 return result
@@ -532,7 +558,16 @@ final class DatabaseManager {
532558 guard let self else { return false }
533559 // Skip ping while a user query is in-flight to avoid racing
534560 // on the same non-thread-safe driver connection.
535- guard await self . queriesInFlight [ connectionId] == nil else { return true }
561+ // Allow ping if the query appears stuck (exceeds timeout + grace period).
562+ if await self . queriesInFlight [ connectionId] != nil {
563+ let queryTimeout = await TimeInterval ( AppSettingsManager . shared. general. queryTimeoutSeconds)
564+ let maxStale = max ( queryTimeout, 300 ) // At least 5 minutes
565+ if let startTime = await self . queryStartTimes [ connectionId] ,
566+ Date ( ) . timeIntervalSince ( startTime) < maxStale {
567+ return true // Query still within expected time
568+ }
569+ // Query appears stuck — fall through to ping
570+ }
536571 guard let mainDriver = await self . activeSessions [ connectionId] ? . driver else {
537572 return false
538573 }
@@ -547,12 +582,13 @@ final class DatabaseManager {
547582 guard let self else { return false }
548583 guard let session = await self . activeSessions [ connectionId] else { return false }
549584 do {
550- let driver = try await self . reconnectDriver ( for: session)
585+ let driver = try await self . trackOperation ( sessionId: connectionId) {
586+ try await self . reconnectDriver ( for: session)
587+ }
551588 await self . updateSession ( connectionId) { session in
552589 session. driver = driver
553590 session. status = . connected
554591 }
555-
556592 return true
557593 } catch {
558594 return false
@@ -619,13 +655,21 @@ final class DatabaseManager {
619655
620656 if let savedSchema = session. currentSchema,
621657 let schemaDriver = driver as? SchemaSwitchable {
622- try ? await schemaDriver. switchSchema ( to: savedSchema)
658+ do {
659+ try await schemaDriver. switchSchema ( to: savedSchema)
660+ } catch {
661+ Self . logger. warning ( " Failed to restore schema ' \( savedSchema) ' on reconnect: \( error. localizedDescription) " )
662+ }
623663 }
624664
625665 // Restore database for MSSQL if session had a non-default database
626666 if let savedDatabase = session. currentDatabase,
627667 let adapter = driver as? PluginDriverAdapter {
628- try ? await adapter. switchDatabase ( to: savedDatabase)
668+ do {
669+ try await adapter. switchDatabase ( to: savedDatabase)
670+ } catch {
671+ Self . logger. warning ( " Failed to restore database ' \( savedDatabase) ' on reconnect: \( error. localizedDescription) " )
672+ }
629673 }
630674
631675 return driver
@@ -659,8 +703,8 @@ final class DatabaseManager {
659703 await stopHealthMonitor ( for: sessionId)
660704
661705 do {
662- // Disconnect existing drivers
663- session . driver? . disconnect ( )
706+ // Disconnect existing driver (re-fetch to avoid stale local reference)
707+ activeSessions [ sessionId ] ? . driver? . disconnect ( )
664708
665709 // Recreate SSH tunnel if needed and build effective connection
666710 let effectiveConnection = try await buildEffectiveConnection ( for: session. connection)
@@ -681,13 +725,21 @@ final class DatabaseManager {
681725
682726 if let savedSchema = activeSessions [ sessionId] ? . currentSchema,
683727 let schemaDriver = driver as? SchemaSwitchable {
684- try ? await schemaDriver. switchSchema ( to: savedSchema)
728+ do {
729+ try await schemaDriver. switchSchema ( to: savedSchema)
730+ } catch {
731+ Self . logger. warning ( " Failed to restore schema ' \( savedSchema) ' on reconnect: \( error. localizedDescription) " )
732+ }
685733 }
686734
687735 // Restore database for MSSQL if session had a non-default database
688736 if let savedDatabase = activeSessions [ sessionId] ? . currentDatabase,
689737 let adapter = driver as? PluginDriverAdapter {
690- try ? await adapter. switchDatabase ( to: savedDatabase)
738+ do {
739+ try await adapter. switchDatabase ( to: savedDatabase)
740+ } catch {
741+ Self . logger. warning ( " Failed to restore database ' \( savedDatabase) ' on reconnect: \( error. localizedDescription) " )
742+ }
691743 }
692744
693745 // Update session
@@ -741,8 +793,7 @@ final class DatabaseManager {
741793
742794 let maxRetries = 5
743795 for retryCount in 0 ..< maxRetries {
744- // Exponential backoff: 2s, 4s, 8s, 16s, 32s (capped at 60s)
745- let delay = min ( 60.0 , 2.0 * pow( 2.0 , Double ( retryCount) ) )
796+ let delay = ExponentialBackoff . delay ( for: retryCount + 1 , maxDelay: 60 )
746797 Self . logger. info ( " SSH reconnect attempt \( retryCount + 1 ) / \( maxRetries) in \( delay) s for: \( session. connection. name) " )
747798 try ? await Task . sleep ( nanoseconds: UInt64 ( delay * 1_000_000_000 ) )
748799
@@ -768,18 +819,20 @@ final class DatabaseManager {
768819
769820 nonisolated private static let startupLogger = Logger ( subsystem: " com.TablePro " , category: " DatabaseManager " )
770821
822+ @discardableResult
771823 nonisolated private func executeStartupCommands(
772824 _ commands: String ? , on driver: DatabaseDriver , connectionName: String
773- ) async {
825+ ) async -> [ ( statement : String , error : String ) ] {
774826 guard let commands, !commands. trimmingCharacters ( in: . whitespacesAndNewlines) . isEmpty else {
775- return
827+ return [ ]
776828 }
777829
778830 let statements = commands
779831 . components ( separatedBy: CharacterSet ( charactersIn: " ; \n " ) )
780832 . map { $0. trimmingCharacters ( in: . whitespacesAndNewlines) }
781833 . filter { !$0. isEmpty }
782834
835+ var failures : [ ( statement: String , error: String ) ] = [ ]
783836 for statement in statements {
784837 do {
785838 _ = try await driver. execute ( query: statement)
@@ -790,8 +843,10 @@ final class DatabaseManager {
790843 Self . startupLogger. warning (
791844 " Startup command failed for ' \( connectionName) ': \( statement) — \( error. localizedDescription) "
792845 )
846+ failures. append ( ( statement: statement, error: error. localizedDescription) )
793847 }
794848 }
849+ return failures
795850 }
796851
797852 // MARK: - Schema Changes
0 commit comments