Skip to content

Commit a542476

Browse files
authored
fix: prevent Oracle view crash and connection hang (#564) (#569)
Three root causes fixed: 1. Query serialization (OracleConnection.swift) OracleNIO doesn't support concurrent queries on a single connection. Add QueryGate actor to serialize all executeQuery calls, preventing state-machine corruption when fetchColumns/fetchForeignKeys run in parallel with the main query. 2. Remove DBMS_METADATA.GET_DDL (OraclePlugin.swift) GET_DDL with wrong object type (e.g. 'TABLE' for a materialized view) triggers ORA-31603, which corrupts OracleNIO's channel handler state. Build DDL manually from column info instead. 3. Remove DATA_DEFAULT from column queries (OraclePlugin.swift) DATA_DEFAULT in ALL_TAB_COLUMNS is LONG type. OracleNIO crashes when decoding non-NULL LONG values. Also use DBMS_METADATA.GET_DDL('VIEW') for fetchViewDefinition, and add view fallback in fetchTableMetadata. Note: OracleNIO 1.0.0-rc.4 has a force-unwrap bug at OracleChannelHandler.swift:441 (self.rowStream!) that requires a library-level patch — tracked separately for upstream fix.
1 parent 674944a commit a542476

File tree

2 files changed

+46
-16
lines changed

2 files changed

+46
-16
lines changed

Plugins/OracleDriverPlugin/OracleConnection.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,39 @@ struct OracleQueryResult {
3939
let isTruncated: Bool
4040
}
4141

42+
// MARK: - Query Serialization
43+
44+
/// OracleNIO does not support concurrent queries on a single connection.
45+
/// Sending a second statement while the first stream is active corrupts the
46+
/// state machine. This actor serializes all executeQuery calls.
47+
private actor QueryGate {
48+
private var busy = false
49+
private var waiters: [CheckedContinuation<Void, Never>] = []
50+
51+
func acquire() async {
52+
if !busy {
53+
busy = true
54+
return
55+
}
56+
await withCheckedContinuation { waiters.append($0) }
57+
}
58+
59+
func release() {
60+
if !waiters.isEmpty {
61+
waiters.removeFirst().resume()
62+
} else {
63+
busy = false
64+
}
65+
}
66+
}
67+
4268
// MARK: - Connection Class
4369

4470
final class OracleConnectionWrapper: @unchecked Sendable {
4571
// MARK: - Properties
4672

4773
private static let connectionCounter = OSAllocatedUnfairLock(initialState: 0)
74+
private let queryGate = QueryGate()
4875

4976
private let host: String
5077
private let port: Int
@@ -143,6 +170,10 @@ final class OracleConnectionWrapper: @unchecked Sendable {
143170
}
144171
lock.unlock()
145172

173+
// OracleNIO does not support concurrent queries on a single connection.
174+
// Serialize all queries to prevent state-machine corruption.
175+
await queryGate.acquire()
176+
146177
do {
147178
let statement = OracleStatement(stringLiteral: query)
148179
let stream = try await connection.execute(statement, logger: nioLogger)
@@ -183,6 +214,7 @@ final class OracleConnectionWrapper: @unchecked Sendable {
183214
columnTypeNames = Array(repeating: "unknown", count: columns.count)
184215
}
185216

217+
await queryGate.release()
186218
return OracleQueryResult(
187219
columns: columns,
188220
columnTypeNames: columnTypeNames,
@@ -192,12 +224,16 @@ final class OracleConnectionWrapper: @unchecked Sendable {
192224
)
193225
} catch let sqlError as OracleSQLError {
194226
let detail = sqlError.serverInfo?.message ?? sqlError.description
227+
await queryGate.release()
195228
throw OracleError(message: detail)
196229
} catch let error as OracleError {
230+
await queryGate.release()
197231
throw error
198232
} catch is CancellationError {
233+
await queryGate.release()
199234
throw CancellationError()
200235
} catch {
236+
await queryGate.release()
201237
throw OracleError(message: "Query execution failed: \(String(describing: error))")
202238
}
203239
}

Plugins/OracleDriverPlugin/OraclePlugin.swift

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,6 @@ final class OraclePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
401401
c.DATA_PRECISION,
402402
c.DATA_SCALE,
403403
c.NULLABLE,
404-
c.DATA_DEFAULT,
405404
CASE WHEN cc.COLUMN_NAME IS NOT NULL THEN 'Y' ELSE 'N' END AS IS_PK
406405
FROM ALL_TAB_COLUMNS c
407406
LEFT JOIN (
@@ -424,8 +423,7 @@ final class OraclePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
424423
let precision = row[safe: 4] ?? nil
425424
let scale = row[safe: 5] ?? nil
426425
let isNullable = (row[safe: 6] ?? nil) == "Y"
427-
let defaultValue = (row[safe: 7] ?? nil)?.trimmingCharacters(in: .whitespacesAndNewlines)
428-
let isPk = (row[safe: 8] ?? nil) == "Y"
426+
let isPk = (row[safe: 7] ?? nil) == "Y"
429427

430428
let fullType = buildOracleFullType(dataType: dataType, dataLength: dataLength, precision: precision, scale: scale)
431429

@@ -434,7 +432,7 @@ final class OraclePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
434432
dataType: fullType,
435433
isNullable: isNullable,
436434
isPrimaryKey: isPk,
437-
defaultValue: defaultValue
435+
defaultValue: nil
438436
)
439437
columnsByTable[tableName, default: []].append(col)
440438
}
@@ -509,15 +507,10 @@ final class OraclePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
509507
func fetchTableDDL(table: String, schema: String?) async throws -> String {
510508
let escapedTable = table.replacingOccurrences(of: "'", with: "''")
511509
let escaped = effectiveSchemaEscaped(schema)
512-
let sql = "SELECT DBMS_METADATA.GET_DDL('TABLE', '\(escapedTable)', '\(escaped)') FROM DUAL"
513-
do {
514-
let result = try await execute(query: sql)
515-
if let row = result.rows.first, let ddl = row.first ?? nil {
516-
return ddl
517-
}
518-
} catch {
519-
Self.logger.debug("DBMS_METADATA failed, building DDL manually: \(error.localizedDescription)")
520-
}
510+
511+
// Do NOT use DBMS_METADATA.GET_DDL — if the object type is wrong
512+
// (view, materialized view, etc.), Oracle returns ORA-31603 which
513+
// corrupts OracleNIO's connection state machine. Build DDL manually.
521514

522515
let cols = try await fetchColumns(table: table, schema: schema)
523516
var ddl = "CREATE TABLE \"\(escaped)\".\"\(escapedTable)\" (\n"
@@ -535,9 +528,10 @@ final class OraclePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
535528
func fetchViewDefinition(view: String, schema: String?) async throws -> String {
536529
let escapedView = view.replacingOccurrences(of: "'", with: "''")
537530
let escaped = effectiveSchemaEscaped(schema)
538-
// Use DBMS_METADATA.GET_DDL instead of ALL_VIEWS.TEXT to avoid LONG column type
539-
// that crashes OracleNIO's decoder
540-
let sql = "SELECT DBMS_METADATA.GET_DDL('VIEW', '\(escapedView)', '\(escaped)') FROM DUAL"
531+
// ALL_VIEWS.TEXT is LONG (crashes OracleNIO). TEXT_VC is VARCHAR2(4000), safe.
532+
// Do NOT use DBMS_METADATA.GET_DDL — wrong object type triggers ORA-31603
533+
// which corrupts OracleNIO's connection state machine.
534+
let sql = "SELECT TEXT_VC FROM ALL_VIEWS WHERE VIEW_NAME = '\(escapedView)' AND OWNER = '\(escaped)'"
541535
let result = try await execute(query: sql)
542536
return result.rows.first?.first?.flatMap { $0 } ?? ""
543537
}

0 commit comments

Comments
 (0)