Skip to content

Commit 699cca4

Browse files
authored
feat(plugin-oracle): native network encryption, redirect following, clearer listener errors (#483) (#1482)
* feat(plugin-oracle): native network encryption, redirect following, clearer listener errors (#483) * refactor(plugin-oracle): drop redundant listener-refused branch in connect error classification * build(plugin-oracle): bump oracle-nio pin to include native encryption cleanup * refactor(plugin-oracle): map listener refusal codes to readable reasons
1 parent 635b2d3 commit 699cca4

9 files changed

Lines changed: 98 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
### Fixed
11-
12-
- Query result columns now follow the order in the SELECT. Adding or removing a column no longer leaves new columns stuck at the end of the grid. (#1565)
13-
- JSON file import works again. It failed to load in 0.48.0.
14-
- SQL export quotes empty or malformed values in numeric columns instead of writing them unquoted, which could produce invalid INSERT statements.
15-
1610
### Added
1711

1812
- Each filter row has a checkbox to turn it on or off and an Apply button to filter by just that row. The main Apply runs every active filter, and disabled filters stay in the panel for later. (#1561)
1913
- Importing connections from other apps now detects duplicates by host, port, database, and username, and lets you replace, add a copy, or skip each one before import.
14+
- Oracle connections negotiate Native Network Encryption when the server asks for it, so servers with `SQLNET.ENCRYPTION_SERVER` or `SQLNET.CRYPTO_CHECKSUM_SERVER` set to REQUIRED now connect (AES with a SHA crypto-checksum), matching what SQL Developer and DBeaver do. (#483)
15+
- Oracle connections follow listener redirects, so RAC SCAN listeners, shared server, and load-balanced setups now connect instead of failing during the handshake. (#483)
2016

2117
### Changed
2218

@@ -30,12 +26,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3026

3127
### Fixed
3228

29+
- Query result columns now follow the order in the SELECT. Adding or removing a column no longer leaves new columns stuck at the end of the grid. (#1565)
30+
- JSON file import works again. It failed to load in 0.48.0.
31+
- SQL export quotes empty or malformed values in numeric columns instead of writing them unquoted, which could produce invalid INSERT statements.
3332
- SQL Server: connections work when the login can only reach its own database, such as an Azure SQL contained user. The database is now sent during login. Previously it was switched afterward, which the server rejected with a "Login failed" error.
3433
- Custom Copy and Cut shortcuts now take effect in the SQL editor.
3534
- The Delete shortcut in the data grid now follows a custom binding.
3635
- Find Next (Cmd+G) and Find Previous (Cmd+Shift+G) now work in the editor.
3736
- Pagination buttons no longer fire their page shortcut twice.
3837
- AWS IAM connections no longer ask for a password on connect or reconnect. IAM supplies the credentials, so the prompt was never needed. The same now holds for any auth mode that replaces the password, such as a Postgres password file.
38+
- Oracle connection failures show the listener's actual reason (such as an unknown service name) instead of a generic "server closed the connection" message. (#483)
3939

4040
## [0.48.0] - 2026-06-02
4141

Plugins/OracleDriverPlugin/OracleConnection.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ final class OracleConnectionWrapper: @unchecked Sendable {
189189
let target = useSID ? "\(self.host):\(self.port):\(identifier)" : "\(self.host):\(self.port)/\(identifier)"
190190
osLogger.debug("Connected to Oracle \(target)")
191191
} catch let sqlError as OracleSQLError {
192-
let detail = sqlError.serverInfo?.message ?? sqlError.description
192+
let detail = Self.connectFailureDetail(sqlError)
193193
osLogger.error("Oracle connection failed: \(detail)")
194194
if let sslError = Self.classifySSLError(detail) {
195195
throw sslError
@@ -246,6 +246,13 @@ final class OracleConnectionWrapper: @unchecked Sendable {
246246
}
247247
}
248248

249+
private static func connectFailureDetail(_ error: OracleSQLError) -> String {
250+
if let refused = error.underlying as? OracleListenerRefusedError {
251+
return OracleListenerRefusal.detail(code: refused.code)
252+
}
253+
return error.serverInfo?.message ?? error.description
254+
}
255+
249256
private static func connectErrorMessage(
250257
for category: OracleError.Category,
251258
serverDetail: String
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import Foundation
2+
3+
enum OracleListenerRefusal {
4+
static func detail(code: Int?) -> String {
5+
guard let code else {
6+
return String(localized: "The Oracle listener refused the connection.")
7+
}
8+
if let reason = reason(forCode: code) {
9+
return String(format: String(localized: "%1$@ (ORA-%2$ld)."), reason, code)
10+
}
11+
return String(format: String(localized: "The Oracle listener refused the connection (ORA-%ld)."), code)
12+
}
13+
14+
static func reason(forCode code: Int) -> String? {
15+
switch code {
16+
case 12_514:
17+
return String(localized: "The listener does not know the requested service name")
18+
case 12_505:
19+
return String(localized: "The listener does not know the requested SID")
20+
case 12_516, 12_519, 12_520:
21+
return String(localized: "The listener has no handler available for the requested service")
22+
case 12_528:
23+
return String(localized: "The listener is blocking new connections to the requested service")
24+
default:
25+
return nil
26+
}
27+
}
28+
}

Plugins/OracleDriverPlugin/OraclePlugin.swift

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,9 @@ final class OraclePlugin: NSObject, TableProPlugin, DriverPlugin, PluginDiagnost
143143
title: String(localized: "Connection Dropped During Handshake"),
144144
message: oracleError.message,
145145
suggestedActions: [
146-
String(localized: "The server may require Native Network Encryption, which the pure-Swift driver cannot negotiate."),
147-
String(localized: "Configure the listener for TLS, or set SQLNET.ENCRYPTION_SERVER to ACCEPTED instead of REQUIRED."),
148-
String(localized: "If the same connection works in DBeaver or SQL Developer, they use Oracle's OCI client, which supports Native Network Encryption."),
149-
String(localized: "Check for a firewall or load balancer between the client and server that closes connections mid-handshake.")
146+
String(localized: "Check for a firewall, VPN, or load balancer between you and the server that closes connections mid-handshake."),
147+
String(localized: "If the listener endpoint is TLS-only (TCPS), set the SSL mode in the connection's SSL settings."),
148+
String(localized: "Confirm the host and port reach the database listener directly, not a proxy that resets unknown traffic.")
150149
],
151150
supportURL: URL(string: "https://github.com/TableProApp/TablePro/issues/483")
152151
)

TablePro.xcodeproj/project.pbxproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4451,7 +4451,7 @@
44514451
repositoryURL = "https://github.com/TableProApp/oracle-nio";
44524452
requirement = {
44534453
kind = revision;
4454-
revision = 254b72adfb6b527ac45895b42a38e60ba6c77a1f;
4454+
revision = 04a4e5967bf4d96cadf66081ea5a22133fe403ea;
44554455
};
44564456
};
44574457
/* End XCRemoteSwiftPackageReference section */

TablePro.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../Plugins/OracleDriverPlugin/OracleListenerRefusal.swift
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import Foundation
2+
import Testing
3+
4+
@testable import TablePro
5+
6+
@Suite("Oracle listener refusal detail")
7+
struct OracleListenerRefusalTests {
8+
@Test("Known listener codes map to a human reason with the ORA code")
9+
func knownCodes() {
10+
#expect(OracleListenerRefusal.detail(code: 12_514)
11+
== "The listener does not know the requested service name (ORA-12514).")
12+
#expect(OracleListenerRefusal.detail(code: 12_505)
13+
== "The listener does not know the requested SID (ORA-12505).")
14+
#expect(OracleListenerRefusal.detail(code: 12_528)
15+
== "The listener is blocking new connections to the requested service (ORA-12528).")
16+
}
17+
18+
@Test("Handler-unavailable codes share one reason")
19+
func handlerUnavailableCodes() {
20+
for code in [12_516, 12_519, 12_520] {
21+
#expect(OracleListenerRefusal.reason(forCode: code)
22+
== "The listener has no handler available for the requested service")
23+
}
24+
}
25+
26+
@Test("An unknown code falls back to the generic message with the code")
27+
func unknownCode() {
28+
#expect(OracleListenerRefusal.reason(forCode: 9_999) == nil)
29+
#expect(OracleListenerRefusal.detail(code: 9_999)
30+
== "The Oracle listener refused the connection (ORA-9999).")
31+
}
32+
33+
@Test("A missing code falls back to the generic message")
34+
func missingCode() {
35+
#expect(OracleListenerRefusal.detail(code: nil)
36+
== "The Oracle listener refused the connection.")
37+
}
38+
}

docs/databases/oracle.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ description: Connect to Oracle Database with TablePro
88
TablePro supports Oracle Database 11.1 and later. It speaks the Oracle TNS wire protocol directly in Swift, so no Oracle Instant Client or other external library is required. This covers Oracle Database instances running on-premises, in Docker, or Oracle Cloud.
99

1010
<Note>
11-
Oracle 10g and earlier are not supported: they use the older O3LOGON handshake that the pure-Swift driver does not implement. Servers that require Native Network Encryption also need Oracle's OCI client (use TLS instead).
11+
Oracle 10g and earlier are not supported: they use the older O3LOGON handshake that the pure-Swift driver does not implement.
1212
</Note>
1313

14+
Servers behind a RAC SCAN listener, shared server, or a load balancer are supported: TablePro follows the listener's redirect to the instance that serves the session. Servers that require Native Network Encryption (`SQLNET.ENCRYPTION_SERVER` or `SQLNET.CRYPTO_CHECKSUM_SERVER` set to REQUIRED) are also supported; TablePro negotiates AES encryption with a SHA crypto-checksum, the same as SQL Developer and DBeaver.
15+
1416
## Install Plugin
1517

1618
The Oracle driver is available as a downloadable plugin. When you select Oracle in the connection form, TablePro will prompt you to install it automatically. You can also install it manually:

0 commit comments

Comments
 (0)