Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- The ER diagram now arranges tables in a compact layout that fills the canvas in both directions, keeps tables linked by foreign keys together, and tints each group of connected tables with its own header color. (#1755)
- When an Oracle server closes the connection during login, the error dialog now shows which handshake phase it stopped at, so dropped-handshake failures (for example on Oracle 11g) are easier to pin down. (#1746)

### Fixed

Expand Down
21 changes: 10 additions & 11 deletions Plugins/OracleDriverPlugin/OracleConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ struct OracleError: Error {
case protocolError
case authVerifierUnsupported(flag: String)
case authVersionNotSupported
case authConnectionDropped
case authConnectionDropped(phase: String?)
}

let message: String
Expand Down Expand Up @@ -191,7 +191,8 @@ final class OracleConnectionWrapper: @unchecked Sendable {
osLogger.debug("Connected to Oracle \(target)")
} catch let sqlError as OracleSQLError {
let detail = Self.connectFailureDetail(sqlError)
osLogger.error("Oracle connection failed: \(detail)")
let phase = sqlError.handshakePhase ?? "unknown"
osLogger.error("Oracle connection failed at phase \(phase, privacy: .public) (\(sqlError.code.description, privacy: .public)): \(detail)")
if let sslError = OracleSSLClassifier.classifySSLError(detail) {
throw sslError
}
Expand All @@ -215,16 +216,14 @@ final class OracleConnectionWrapper: @unchecked Sendable {
}

private func classifyConnectError(_ error: OracleSQLError) -> OracleError.Category {
let codeDescription = error.code.description
if codeDescription.hasPrefix("unsupportedVerifierType") {
return .authVerifierUnsupported(flag: codeDescription)
}
switch codeDescription {
case "uncleanShutdown":
return .authConnectionDropped
case "serverVersionNotSupported":
switch OracleConnectErrorClassifier.classify(error.code.description) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid depending on an unversioned PluginKit symbol

This makes the downloadable Oracle plugin link against OracleConnectErrorClassifier, a symbol that does not exist in already-released TablePro apps whose currentPluginKitVersion is still 18. Because the Oracle plugin Info.plist still declares TableProPluginKitVersion 18 and has no TableProMinAppVersion, local zip/bundle installs are accepted by validateBundleVersions in those older apps, then the bundle can fail to load with an unresolved symbol. Keep this classifier in the Oracle plugin or add an app/version gate for the plugin binary when consuming a new PluginKit API.

Useful? React with 👍 / 👎.

case .verifierUnsupported(let flag):
return .authVerifierUnsupported(flag: flag)
case .versionNotSupported:
return .authVersionNotSupported
default:
case .connectionDropped:
return .authConnectionDropped(phase: error.handshakePhase)
case .connectionFailed:
return .connectionFailed
}
}
Expand Down
6 changes: 4 additions & 2 deletions Plugins/OracleDriverPlugin/OraclePlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,15 +140,17 @@ final class OraclePlugin: NSObject, TableProPlugin, DriverPlugin, PluginDiagnost
],
supportURL: issuesURL
)
case .authConnectionDropped:
case .authConnectionDropped(let phase):
return PluginDiagnostic(
title: String(localized: "Connection Dropped During Handshake"),
message: oracleError.message,
suggestedActions: [
String(localized: "Check for a firewall, VPN, or load balancer between you and the server that closes connections mid-handshake."),
String(localized: "If the listener endpoint is TLS-only (TCPS), set the SSL mode in the connection's SSL settings."),
String(localized: "Confirm the host and port reach the database listener directly, not a proxy that resets unknown traffic.")
String(localized: "Confirm the host and port reach the database listener directly, not a proxy that resets unknown traffic."),
String(localized: "If this is Oracle 11g, open an issue and include the handshake phase shown below.")
],
diagnosticInfo: phase.map { [DiagnosticEntry(label: String(localized: "Handshake phase"), value: $0)] } ?? [],
supportURL: URL(string: "https://github.com/TableProApp/TablePro/issues/483")
)
case .authVersionNotSupported:
Expand Down
24 changes: 24 additions & 0 deletions Plugins/TableProPluginKit/OracleConnectErrorClassifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Foundation

public enum OracleConnectFailure: Sendable, Equatable {
case verifierUnsupported(flag: String)
case versionNotSupported
case connectionDropped
case connectionFailed
}

public enum OracleConnectErrorClassifier {
public static func classify(_ codeDescription: String) -> OracleConnectFailure {
if codeDescription.hasPrefix("unsupportedVerifierType") {
return .verifierUnsupported(flag: codeDescription)
}
switch codeDescription {
case "uncleanShutdown":
return .connectionDropped
case "serverVersionNotSupported":
return .versionNotSupported
default:
return .connectionFailed
}
}
}
29 changes: 28 additions & 1 deletion TableProTests/Plugins/OracleConnectionErrorTests.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Testing
import TableProPluginKit
import Testing

@Suite("Oracle channel-fatal error classification")
struct OracleConnectionErrorTests {
Expand All @@ -17,3 +17,30 @@ struct OracleConnectionErrorTests {
#expect(!OracleChannelFatalCode.isChannelFatal("malformedStatement"))
}
}

@Suite("Oracle connect error classification")
struct OracleConnectErrorClassifierTests {
@Test("An unclean shutdown is a dropped handshake")
func uncleanShutdownIsDropped() {
#expect(OracleConnectErrorClassifier.classify("uncleanShutdown") == .connectionDropped)
}

@Test("An unsupported server version is reported as such")
func serverVersionNotSupported() {
#expect(OracleConnectErrorClassifier.classify("serverVersionNotSupported") == .versionNotSupported)
}

@Test("An unsupported verifier carries its flag through")
func verifierCarriesFlag() {
#expect(
OracleConnectErrorClassifier.classify("unsupportedVerifierType(0x939)")
== .verifierUnsupported(flag: "unsupportedVerifierType(0x939)")
)
}

@Test("Any other code falls back to a generic connection failure")
func unknownIsConnectionFailed() {
#expect(OracleConnectErrorClassifier.classify("connectionError") == .connectionFailed)
#expect(OracleConnectErrorClassifier.classify("server") == .connectionFailed)
}
}
2 changes: 2 additions & 0 deletions docs/databases/oracle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,6 @@ Columns with types not yet supported render as `<unsupported: type>` rather than

**Instant Client not found**: Download Basic package, extract to `/usr/local/oracle/instantclient`, set `DYLD_LIBRARY_PATH`

**Connection dropped during handshake**: The server closed the connection mid-login. The error dialog shows the handshake phase it stopped at (for example `dataTypeNegotiation` or `authentication`). Check for a firewall, VPN, or proxy that resets traffic, and confirm the host and port reach the listener directly. On Oracle 11g, open an issue and include the handshake phase.

**Limitations**: Username/password only, BFILE shows locator metadata only (content fetch via DBMS_LOB not supported), PL/SQL limited to anonymous blocks.
Loading