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
36 changes: 36 additions & 0 deletions Plugins/OracleDriverPlugin/OracleConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,39 @@ struct OracleQueryResult {
let isTruncated: Bool
}

// MARK: - Query Serialization

/// OracleNIO does not support concurrent queries on a single connection.
/// Sending a second statement while the first stream is active corrupts the
/// state machine. This actor serializes all executeQuery calls.
private actor QueryGate {
private var busy = false
private var waiters: [CheckedContinuation<Void, Never>] = []

func acquire() async {
if !busy {
busy = true
return
}
await withCheckedContinuation { waiters.append($0) }
}

func release() {
if !waiters.isEmpty {
waiters.removeFirst().resume()
} else {
busy = false
}
}
}

// MARK: - Connection Class

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

private static let connectionCounter = OSAllocatedUnfairLock(initialState: 0)
private let queryGate = QueryGate()

private let host: String
private let port: Int
Expand Down Expand Up @@ -143,6 +170,10 @@ final class OracleConnectionWrapper: @unchecked Sendable {
}
lock.unlock()

// OracleNIO does not support concurrent queries on a single connection.
// Serialize all queries to prevent state-machine corruption.
await queryGate.acquire()

do {
let statement = OracleStatement(stringLiteral: query)
let stream = try await connection.execute(statement, logger: nioLogger)
Expand Down Expand Up @@ -183,6 +214,7 @@ final class OracleConnectionWrapper: @unchecked Sendable {
columnTypeNames = Array(repeating: "unknown", count: columns.count)
}

await queryGate.release()
return OracleQueryResult(
columns: columns,
columnTypeNames: columnTypeNames,
Expand All @@ -192,12 +224,16 @@ final class OracleConnectionWrapper: @unchecked Sendable {
)
} catch let sqlError as OracleSQLError {
let detail = sqlError.serverInfo?.message ?? sqlError.description
await queryGate.release()
throw OracleError(message: detail)
} catch let error as OracleError {
await queryGate.release()
throw error
} catch is CancellationError {
await queryGate.release()
throw CancellationError()
} catch {
await queryGate.release()
throw OracleError(message: "Query execution failed: \(String(describing: error))")
}
}
Expand Down
26 changes: 10 additions & 16 deletions Plugins/OracleDriverPlugin/OraclePlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,6 @@ final class OraclePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
c.DATA_PRECISION,
c.DATA_SCALE,
c.NULLABLE,
c.DATA_DEFAULT,
CASE WHEN cc.COLUMN_NAME IS NOT NULL THEN 'Y' ELSE 'N' END AS IS_PK
FROM ALL_TAB_COLUMNS c
LEFT JOIN (
Expand All @@ -424,8 +423,7 @@ final class OraclePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
let precision = row[safe: 4] ?? nil
let scale = row[safe: 5] ?? nil
let isNullable = (row[safe: 6] ?? nil) == "Y"
let defaultValue = (row[safe: 7] ?? nil)?.trimmingCharacters(in: .whitespacesAndNewlines)
let isPk = (row[safe: 8] ?? nil) == "Y"
let isPk = (row[safe: 7] ?? nil) == "Y"

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

Expand All @@ -434,7 +432,7 @@ final class OraclePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
dataType: fullType,
isNullable: isNullable,
isPrimaryKey: isPk,
defaultValue: defaultValue
defaultValue: nil
)
columnsByTable[tableName, default: []].append(col)
}
Expand Down Expand Up @@ -509,15 +507,10 @@ final class OraclePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
func fetchTableDDL(table: String, schema: String?) async throws -> String {
let escapedTable = table.replacingOccurrences(of: "'", with: "''")
let escaped = effectiveSchemaEscaped(schema)
let sql = "SELECT DBMS_METADATA.GET_DDL('TABLE', '\(escapedTable)', '\(escaped)') FROM DUAL"
do {
let result = try await execute(query: sql)
if let row = result.rows.first, let ddl = row.first ?? nil {
return ddl
}
} catch {
Self.logger.debug("DBMS_METADATA failed, building DDL manually: \(error.localizedDescription)")
}

// Do NOT use DBMS_METADATA.GET_DDL — if the object type is wrong
// (view, materialized view, etc.), Oracle returns ORA-31603 which
// corrupts OracleNIO's connection state machine. Build DDL manually.

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