diff --git a/Plugins/OracleDriverPlugin/OracleConnection.swift b/Plugins/OracleDriverPlugin/OracleConnection.swift index 04b7754cd..7507c6410 100644 --- a/Plugins/OracleDriverPlugin/OracleConnection.swift +++ b/Plugins/OracleDriverPlugin/OracleConnection.swift @@ -117,15 +117,16 @@ final class OracleConnectionWrapper: @unchecked Sendable { private let database: String private let serviceName: String - private let lock = NSLock() - private var _isConnected = false - private var nioConnection: OracleNIO.OracleConnection? + private struct LockedState: Sendable { + var isConnected = false + var nioConnection: OracleNIO.OracleConnection? + } + + private let state = OSAllocatedUnfairLock(initialState: LockedState()) private let nioLogger = Logging.Logger(label: "com.TablePro.oracle-nio") var isConnected: Bool { - lock.lock() - defer { lock.unlock() } - return _isConnected + state.withLock { $0.isConnected } } // MARK: - Initialization @@ -163,10 +164,10 @@ final class OracleConnectionWrapper: @unchecked Sendable { logger: nioLogger ) - lock.lock() - nioConnection = connection - _isConnected = true - lock.unlock() + state.withLock { current in + current.nioConnection = connection + current.isConnected = true + } osLogger.debug("Connected to Oracle \(self.host):\(self.port)/\(service)") } catch let sqlError as OracleSQLError { @@ -196,18 +197,18 @@ final class OracleConnectionWrapper: @unchecked Sendable { } func disconnect() { - lock.lock() - guard _isConnected else { - lock.unlock() - return + let connection = state.withLock { current -> OracleNIO.OracleConnection? in + guard current.isConnected else { return nil } + current.isConnected = false + let conn = current.nioConnection + current.nioConnection = nil + return conn } - _isConnected = false - let connection = nioConnection - nioConnection = nil - lock.unlock() + + guard let connection else { return } Task { - try? await connection?.close() + try? await connection.close() osLogger.debug("Disconnected from Oracle \(self.host):\(self.port)") } } @@ -215,12 +216,12 @@ final class OracleConnectionWrapper: @unchecked Sendable { // MARK: - Query Execution func executeQuery(_ query: String) async throws -> OracleQueryResult { - lock.lock() - guard let connection = nioConnection, _isConnected else { - lock.unlock() - throw OracleError.notConnected + let connection = try state.withLock { current -> OracleNIO.OracleConnection in + guard let conn = current.nioConnection, current.isConnected else { + throw OracleError.notConnected + } + return conn } - lock.unlock() // OracleNIO does not support concurrent queries on a single connection. // Serialize all queries to prevent state-machine corruption. @@ -296,12 +297,12 @@ final class OracleConnectionWrapper: @unchecked Sendable { _ query: String, continuation: AsyncThrowingStream.Continuation ) async throws { - lock.lock() - guard let connection = nioConnection, _isConnected else { - lock.unlock() - throw OracleError.notConnected + let connection = try state.withLock { current -> OracleNIO.OracleConnection in + guard let conn = current.nioConnection, current.isConnected else { + throw OracleError.notConnected + } + return conn } - lock.unlock() await queryGate.acquire() diff --git a/Plugins/OracleDriverPlugin/OraclePlugin.swift b/Plugins/OracleDriverPlugin/OraclePlugin.swift index d159e25fc..a3d8aed87 100644 --- a/Plugins/OracleDriverPlugin/OraclePlugin.swift +++ b/Plugins/OracleDriverPlugin/OraclePlugin.swift @@ -97,6 +97,51 @@ final class OraclePlugin: NSObject, TableProPlugin, DriverPlugin, PluginDiagnost func createDriver(config: DriverConnectionConfig) -> any PluginDatabaseDriver { OraclePluginDriver(config: config) } + + func diagnose(error: Error) -> PluginDiagnostic? { + guard let oracleError = error as? OracleError else { return nil } + let issuesURL = URL(string: "https://github.com/TableProApp/TablePro/issues") + switch oracleError.category { + case .authVerifierUnsupported(let flag): + return PluginDiagnostic( + title: String(localized: "Unsupported Password Verifier"), + message: oracleError.message, + suggestedActions: [ + String(localized: "Verify the user account exists and the password is correct."), + String(localized: "Ask your DBA to confirm the user has an 11G or 12C password verifier (SELECT password_versions FROM dba_users WHERE username = '')."), + String(localized: "If the verifier is brand-new (e.g. 23ai), file an issue with the verifier flag below.") + ], + diagnosticInfo: [ + DiagnosticEntry(label: "Verifier flag", value: flag) + ], + supportURL: issuesURL + ) + case .authConnectionDropped: + return PluginDiagnostic( + title: String(localized: "Connection Dropped During Handshake"), + message: oracleError.message, + suggestedActions: [ + String(localized: "If the same connection works in DBeaver or sqlplus, this is likely an OOB compatibility issue with cloud-hosted Oracle."), + String(localized: "TablePro 1.2.0 already gates OOB on the server flag, so most cases are resolved. If you still hit this, file an issue."), + String(localized: "Try disabling SSH tunnel or load balancer firewall rules between client and server.") + ], + supportURL: URL(string: "https://github.com/TableProApp/TablePro/issues/483") + ) + case .authVersionNotSupported: + return PluginDiagnostic( + title: String(localized: "Server Version Not Supported"), + message: oracleError.message, + suggestedActions: [ + String(localized: "TablePro requires Oracle 12c or later via the OracleNIO Swift driver."), + String(localized: "Check the user account's password_versions; only 10G, 11G, and 12C are supported."), + String(localized: "Rotate the password under modern auth if password_versions contains an unrecognized verifier.") + ], + supportURL: issuesURL + ) + case .generic, .notConnected, .connectionFailed, .queryFailed: + return nil + } + } } final class OraclePluginDriver: PluginDatabaseDriver, @unchecked Sendable { @@ -1104,51 +1149,6 @@ final class OraclePluginDriver: PluginDatabaseDriver, @unchecked Sendable { return raw.replacingOccurrences(of: "'", with: "''") } - func diagnose(error: Error) -> PluginDiagnostic? { - guard let oracleError = error as? OracleError else { return nil } - let issuesURL = URL(string: "https://github.com/TableProApp/TablePro/issues") - switch oracleError.category { - case .authVerifierUnsupported(let flag): - return PluginDiagnostic( - title: String(localized: "Unsupported Password Verifier"), - message: oracleError.message, - suggestedActions: [ - String(localized: "Verify the user account exists and the password is correct."), - String(localized: "Ask your DBA to confirm the user has an 11G or 12C password verifier (SELECT password_versions FROM dba_users WHERE username = '')."), - String(localized: "If the verifier is brand-new (e.g. 23ai), file an issue with the verifier flag below.") - ], - diagnosticInfo: [ - DiagnosticEntry(label: "Verifier flag", value: flag) - ], - supportURL: issuesURL - ) - case .authConnectionDropped: - return PluginDiagnostic( - title: String(localized: "Connection Dropped During Handshake"), - message: oracleError.message, - suggestedActions: [ - String(localized: "If the same connection works in DBeaver or sqlplus, this is likely an OOB compatibility issue with cloud-hosted Oracle."), - String(localized: "TablePro 1.2.0 already gates OOB on the server flag, so most cases are resolved. If you still hit this, file an issue."), - String(localized: "Try disabling SSH tunnel or load balancer firewall rules between client and server.") - ], - supportURL: URL(string: "https://github.com/TableProApp/TablePro/issues/483") - ) - case .authVersionNotSupported: - return PluginDiagnostic( - title: String(localized: "Server Version Not Supported"), - message: oracleError.message, - suggestedActions: [ - String(localized: "TablePro requires Oracle 12c or later via the OracleNIO Swift driver."), - String(localized: "Check the user account's password_versions; only 10G, 11G, and 12C are supported."), - String(localized: "Rotate the password under modern auth if password_versions contains an unrecognized verifier.") - ], - supportURL: issuesURL - ) - case .generic, .notConnected, .connectionFailed, .queryFailed: - return nil - } - } - private static let fromTableRegex = try? NSRegularExpression( pattern: #"FROM\s+(?:"([^"]+)"|(\w+))"#, options: .caseInsensitive