From 403af849818204c5ab953c0cffcc0ee889dff24d Mon Sep 17 00:00:00 2001 From: Timo <38291523+lovetodream@users.noreply.github.com> Date: Fri, 5 Jul 2024 16:53:27 +0200 Subject: [PATCH] refactor: cleanup previous impl --- .../ConnectionStateMachine.swift | 27 - .../StatementStateMachine.swift | 588 +----------------- .../Coding/MissingDataDecodingError.swift | 2 +- .../Coding/OracleBackendMessageDecoder.swift | 18 +- .../OracleBackendMessage+RowData.swift | 78 ++- .../Messages/OracleBackendMessage.swift | 42 +- Sources/OracleNIO/OracleChannelHandler.swift | 19 +- .../OracleNIO/Utilities/OracleHexDump.swift | 9 +- Tests/IntegrationTests/OracleNIOTests.swift | 1 - .../StatementStateMachineTests.swift | 215 ++----- .../Messages/RowDataTests.swift | 121 ++++ .../ConnectionAction+TestUtils.swift | 2 - ...racleBackendMessageDecoder+TestUtils.swift | 46 ++ .../Utilities/OracleHexDumpTests.swift | 4 + 14 files changed, 324 insertions(+), 848 deletions(-) create mode 100644 Tests/OracleNIOTests/Messages/RowDataTests.swift create mode 100644 Tests/OracleNIOTests/TestUtils/OracleBackendMessageDecoder+TestUtils.swift diff --git a/Sources/OracleNIO/ConnectionStateMachine/ConnectionStateMachine.swift b/Sources/OracleNIO/ConnectionStateMachine/ConnectionStateMachine.swift index 1beb0fc..0d33e87 100644 --- a/Sources/OracleNIO/ConnectionStateMachine/ConnectionStateMachine.swift +++ b/Sources/OracleNIO/ConnectionStateMachine/ConnectionStateMachine.swift @@ -134,7 +134,6 @@ struct ConnectionStateMachine { EventLoopPromise, StatementResult ) - case needMoreData // Statement streaming case forwardRows([DataRow]) @@ -753,29 +752,6 @@ struct ConnectionStateMachine { } } - mutating func chunkReceived( - _ buffer: ByteBuffer, capabilities: Capabilities - ) -> ConnectionAction { - switch self.state { - case .statement(var statement): - return self.avoidingStateMachineCoW { machine in - let action = statement.chunkReceived( - buffer, capabilities: capabilities - ) - machine.state = .statement(statement) - return machine.modify(with: action) - } - - case .loggingOff, .closing: - // Might happen if an error is thrown in row decoding and the - // connection is closed immediately after. - return .wait - - default: - preconditionFailure("Invalid state: \(self.state)") - } - } - mutating func ioVectorReceived( _ vector: OracleBackendMessage.InOutVector ) -> ConnectionAction { @@ -921,7 +897,6 @@ struct ConnectionStateMachine { .sendFetch, .sendFlushOutBinds, .succeedStatement, - .needMoreData, .forwardRows, .forwardStreamComplete, .forwardCancelComplete, @@ -1172,8 +1147,6 @@ extension ConnectionStateMachine { return .failStatement(promise, with: error, cleanupContext: nil) case .succeedStatement(let promise, let columns): return .succeedStatement(promise, columns) - case .needMoreData: - return .needMoreData case .forwardRows(let rows): return .forwardRows(rows) case .forwardStreamComplete(let rows, let cursorID): diff --git a/Sources/OracleNIO/ConnectionStateMachine/StatementStateMachine.swift b/Sources/OracleNIO/ConnectionStateMachine/StatementStateMachine.swift index 0531b82..33c1aa7 100644 --- a/Sources/OracleNIO/ConnectionStateMachine/StatementStateMachine.swift +++ b/Sources/OracleNIO/ConnectionStateMachine/StatementStateMachine.swift @@ -25,13 +25,6 @@ struct StatementStateMachine { OracleBackendMessage.RowHeader, RowStreamStateMachine ) - case streamingAndWaiting( - StatementContext, - DescribeInfo, - OracleBackendMessage.RowHeader, - RowStreamStateMachine, - partial: ByteBuffer - ) /// Indicates that the current statement was cancelled and we want to drain /// rows from the connection ASAP. case drain([OracleColumn]) @@ -53,8 +46,6 @@ struct StatementStateMachine { case evaluateErrorAtConnectionLevel(OracleSQLError) - case needMoreData - case forwardRows([DataRow]) case forwardStreamComplete([DataRow], cursorID: UInt16) /// Error payload and a optional cursor ID, which should be closed in a future roundtrip. @@ -70,11 +61,6 @@ struct StatementStateMachine { case wait } - private enum DataRowResult { - case row(DataRow) - case notEnoughData - } - private var state: State private var isCancelled: Bool @@ -120,10 +106,7 @@ struct StatementStateMachine { return .failStatement(promise, with: .statementCancelled) } - case .streaming(_, let describeInfo, _, var streamStateMachine), - .streamingAndWaiting( - _, let describeInfo, _, var streamStateMachine, _ - ): + case .streaming(_, let describeInfo, _, var streamStateMachine): precondition(!self.isCancelled) self.isCancelled = true self.state = .drain(describeInfo.columns) @@ -200,10 +183,9 @@ struct StatementStateMachine { // This state might occur, if the client cancelled the statement, // but the server did not yet receive/process the cancellation // marker. Due to that it might send more data without knowing yet. - // TODO: check if we need to forward row header here too return .wait - case .initialized, .streamingAndWaiting, .error, .commandComplete: + case .initialized, .error, .commandComplete: preconditionFailure("Invalid state: \(self.state)") case .modifying: @@ -217,65 +199,16 @@ struct StatementStateMachine { ) -> Action { switch self.state { case .initialized(let context): -// let outBinds = context.statement.binds.metadata.compactMap(\.outContainer) -// guard !outBinds.isEmpty else { preconditionFailure() } -// var buffer = rowData.slice -// if context.isReturning { -// for outBind in outBinds { -// outBind.storage.withLockedValue { $0 = nil } -// let rowCount = buffer.readUB4() ?? 0 -// guard rowCount > 0 else { -// continue -// } -// -// do { -// for _ in 0.. Action { - switch self.state { - case .drain: - // Could happen if we cancelled the statement while the database is - // still sending stuff - return .wait - - case .streamingAndWaiting( - let statementContext, - let describeInfo, - let rowHeader, - let streamState, - var partial - ): - partial.writeImmutableBuffer(buffer) - self.avoidingStateMachineCoWVoid { state in - state = .streaming( - statementContext, describeInfo, rowHeader, streamState - ) - } - return self.moreDataReceived( - &partial, - capabilities: capabilities, - context: statementContext, - describeInfo: describeInfo - ) - - default: - preconditionFailure("Invalid state: \(self.state)") - } - } - mutating func ioVectorReceived( _ vector: OracleBackendMessage.InOutVector ) -> Action { @@ -614,7 +499,6 @@ struct StatementStateMachine { case .describeInfoReceived, .streaming, - .streamingAndWaiting, .drain, .commandComplete, .error: @@ -638,7 +522,6 @@ struct StatementStateMachine { case .describeInfoReceived, .streaming, - .streamingAndWaiting, .drain, .commandComplete, .error: @@ -657,8 +540,7 @@ struct StatementStateMachine { switch self.state { case .initialized(let context), .describeInfoReceived(let context, _), - .streaming(let context, _, _, _), - .streamingAndWaiting(let context, _, _, _, _): + .streaming(let context, _, _, _): return .sendFetch(context) case .drain, .commandComplete, @@ -692,7 +574,7 @@ struct StatementStateMachine { } } - case .streamingAndWaiting, .drain, .describeInfoReceived: + case .drain, .describeInfoReceived: return .wait case .initialized: @@ -738,23 +620,6 @@ struct StatementStateMachine { return .wait } } - case .streamingAndWaiting( - let context, let describeInfo, let header, var demandStateMachine, - let partial - ): - return self.avoidingStateMachineCoW { state in - let rows = demandStateMachine.channelReadComplete() - state = .streamingAndWaiting( - context, describeInfo, header, demandStateMachine, - partial: partial - ) - switch rows { - case .some(let rows): - return .forwardRows(rows) - case .none: - return .wait - } - } case .modifying: preconditionFailure("Invalid state: \(self.state)") @@ -779,24 +644,6 @@ struct StatementStateMachine { return .read } } - case .streamingAndWaiting( - let context, let describeInfo, let header, var demandStateMachine, - let partial - ): - precondition(!self.isCancelled) - return self.avoidingStateMachineCoW { state in - let action = demandStateMachine.read() - state = .streamingAndWaiting( - context, describeInfo, header, demandStateMachine, - partial: partial - ) - switch action { - case .wait: - return .wait - case .read: - return .read - } - } case .initialized, .commandComplete, .drain, @@ -813,148 +660,6 @@ struct StatementStateMachine { // MARK: Private Methods - private mutating func moreDataReceived( - _ buffer: inout ByteBuffer, - capabilities: Capabilities, - context: StatementContext, - describeInfo: DescribeInfo? - ) -> Action { - while buffer.readableBytes > 0 { - // This is not ideal, but still not as bad as passing potentially - // huge buffers around as associated values in enums and causing - // potential stack overflows when having big array sizes configured. - // Reason for this even existing is that the `row data` response - // from the Oracle database doesn't contain a length field you can - // read without parsing all the values of said row. And to parse the - // values you have to have contextual information from - // `DescribeInfo`. So, because Oracle is sending messages in bulk, - // we don't really have another choice. - var slice = buffer.slice() - let startReaderIndex = slice.readerIndex - do { - let decodingContext = OracleBackendMessageDecoder.Context( - capabilities: capabilities) - // TODO: move this out of here - decodingContext.statementContext = context - var messages: TinySequence = [] - try OracleBackendMessage.decodeData( - from: &slice, - into: &messages, - context: decodingContext - ) - - for message in messages { - let action: Action - switch message { - case .bitVector(let bitVector): - action = self.bitVectorReceived(bitVector) - case .describeInfo(let describeInfo): - action = self.describeInfoReceived(describeInfo) - case .rowHeader(let rowHeader): - action = self.rowHeaderReceived(rowHeader) - case .rowData(let rowData): - fatalError("TODO") - buffer = ByteBuffer() - action = self.rowDataReceived0( - buffer: &buffer, capabilities: capabilities - ) - case .queryParameter: - // query parameters can be safely ignored - action = .wait - case .error(let error): - action = self.errorReceived(error) - case .flushOutBinds: - action = self.flushOutBindsReceived() - default: - preconditionFailure("Invalid state: \(self.state)") - } - // If action is anything other than wait, we will have to - // return it. This should be fine, because messages with - // an action should only occur at the end of a packet. - // At least thats what I (@lovetodream) know from testing. - if case .wait = action { - continue - } - return action - } - - continue - } catch let error as OraclePartialDecodingError { - slice.moveReaderIndex(to: startReaderIndex) - let completeMessage = slice.slice() - let error = OracleSQLError.messageDecodingFailure( - .withPartialError( - error, - packetID: OracleBackendMessage.ID.data.rawValue, - messageBytes: completeMessage - ) - ) - return self.errorHappened(error) - } catch { - preconditionFailure( - "Expected to only see `OraclePartialDecodingError`s here." - ) - } - } - - return .wait - } - - private mutating func rowDataReceived0( - buffer: inout ByteBuffer, capabilities: Capabilities - ) -> Action { - switch self.state { - case .streaming( - let context, let describeInfo, var rowHeader, var demandStateMachine - ): - let readerIndex = buffer.readerIndex - do { - switch try self.rowDataReceived( - buffer: &buffer, - describeInfo: describeInfo, - rowHeader: &rowHeader, - capabilities: capabilities, - demandStateMachine: &demandStateMachine - ) { - case .row(let row): - demandStateMachine.receivedRow(row) - self.avoidingStateMachineCoWVoid { state in - state = .streaming( - context, describeInfo, rowHeader, demandStateMachine - ) - } - case .notEnoughData: - buffer.moveReaderIndex(to: readerIndex) - // prepend message id prefix again - var partial = ByteBuffer( - bytes: [OracleBackendMessage.MessageID.rowData.rawValue] - ) - partial.writeImmutableBuffer(buffer.slice()) - - self.avoidingStateMachineCoWVoid { state in - state = .streamingAndWaiting( - context, - describeInfo, - rowHeader, - demandStateMachine, - partial: partial - ) - } - return .needMoreData - } - } catch let error as OracleSQLError { - return self.setAndFireError(error) - } catch { - preconditionFailure("Unexpected error: \(error)") - } - - default: - preconditionFailure("Invalid state: \(self.state)") - } - - return .wait - } - private mutating func setAndFireError(_ error: OracleSQLError) -> Action { switch self.state { case .initialized(let context), @@ -978,8 +683,7 @@ struct StatementStateMachine { self.state = .error(error) return .evaluateErrorAtConnectionLevel(error) - case .streaming(_, _, _, var streamState), - .streamingAndWaiting(_, _, _, var streamState, _): + case .streaming(_, _, _, var streamState): self.state = .error(error) switch streamState.fail() { case .read: @@ -1001,255 +705,11 @@ struct StatementStateMachine { } } - // MARK: - Private helper methods - - - private func isDuplicateData( - columnNumber: UInt32, bitVector: [UInt8]? - ) -> Bool { - guard let bitVector else { return false } - let byteNumber = columnNumber / 8 - let bitNumber = columnNumber % 8 - return bitVector[Int(byteNumber)] & (1 << bitNumber) == 0 - } - - private func processBindData( - from buffer: inout ByteBuffer, - outBind: OracleRef, - capabilities: Capabilities - ) throws { - let metadata = outBind.metadata.withLockedValue { $0 } - guard - var columnData = try self.processColumnData( - from: &buffer, - oracleType: metadata.dataType._oracleType, - csfrm: metadata.dataType.csfrm, - bufferSize: metadata.bufferSize, - capabilities: capabilities - ) - else { - preconditionFailure( - """ - unhandled need more data in bind processing: please file a issue \ - on https://github.com/lovetodream/oracle-nio/issues with steps to \ - reproduce the crash - """) - } - - let actualBytesCount = buffer.readSB4() ?? 0 - if actualBytesCount < 0 && metadata.dataType._oracleType == .boolean { - return - } else if actualBytesCount != 0 && !columnData.oracleColumnIsEmpty { - // TODO: throw this as error? - preconditionFailure("column truncated, length: \(actualBytesCount)") - } - - outBind.storage.withLockedValue { storage in - if storage == nil { - storage = columnData - } else { - storage!.writeBuffer(&columnData) - } - } - } - - /* private */ func processColumnData( - from buffer: inout ByteBuffer, - oracleType: _TNSDataType?, - csfrm: UInt8, - bufferSize: UInt32, - capabilities: Capabilities - ) throws -> ByteBuffer? { - var columnValue: ByteBuffer - if bufferSize == 0 && ![.long, .longRAW, .uRowID].contains(oracleType) { - columnValue = ByteBuffer(bytes: [0]) // NULL indicator - return columnValue - } - - if [.varchar, .char, .long].contains(oracleType) { - if csfrm == Constants.TNS_CS_NCHAR { - try capabilities.checkNCharsetID() - } - // if we need capabilities during decoding in the future, we should - // move this to decoding too - } - - switch oracleType { - case .varchar, .char, .long, .raw, .longRAW, .number, .date, .timestamp, - .timestampLTZ, .timestampTZ, .binaryDouble, .binaryFloat, - .binaryInteger, .boolean, .intervalDS: - switch buffer.readOracleSlice() { - case .some(let slice): - columnValue = slice - case .none: - return nil // need more data - } - case .rowID: - // length is not the actual length of row ids - let length = try buffer.throwingReadInteger(as: UInt8.self) - if length == 0 || length == Constants.TNS_NULL_LENGTH_INDICATOR { - columnValue = ByteBuffer(bytes: [0]) // NULL indicator - } else { - columnValue = ByteBuffer() - try columnValue.writeLengthPrefixed(as: UInt8.self) { - let start = buffer.readerIndex - _ = try RowID(from: &buffer, type: .rowID, context: .default) - let end = buffer.readerIndex - buffer.moveReaderIndex(to: start) - return $0.writeImmutableBuffer(buffer.readSlice(length: end - start)!) - } - } - case .cursor: - buffer.moveReaderIndex(forwardBy: 1) // length (fixed value) - - let readerIndex = buffer.readerIndex - _ = try DescribeInfo._decode( - from: &buffer, context: .init(capabilities: capabilities) - ) - buffer.skipUB2() // cursor id - let length = buffer.readerIndex - readerIndex - buffer.moveReaderIndex(to: readerIndex) - columnValue = ByteBuffer(integer: Constants.TNS_LONG_LENGTH_INDICATOR) - try columnValue.writeLengthPrefixed(as: UInt32.self) { base in - let start = base.writerIndex - try capabilities.encode(into: &base) - base.writeImmutableBuffer(buffer.readSlice(length: length)!) - return base.writerIndex - start - } - columnValue.writeInteger(0, as: UInt32.self) // chunk length of zero - case .clob, .blob: - - // LOB has a UB4 length indicator instead of the usual UInt8 - let length = try buffer.throwingReadUB4() - if length > 0 { - let size = try buffer.throwingReadUB8() - let chunkSize = try buffer.throwingReadUB4() - var locator: ByteBuffer - switch buffer.readOracleSlice() { - case .some(let slice): - locator = slice - case .none: - return nil // need more data - } - columnValue = ByteBuffer() - try columnValue.writeLengthPrefixed(as: UInt8.self) { - $0.writeInteger(size) + $0.writeInteger(chunkSize) + $0.writeBuffer(&locator) - } - } else { - columnValue = .init(bytes: [0]) // empty buffer - } - case .json: - // TODO: OSON - // OSON has a UB4 length indicator instead of the usual UInt8 - fatalError("OSON is not yet implemented, will be added in the future") - case .vector: - let length = try buffer.throwingReadUB4() - if length > 0 { - buffer.skipUB8() // size (unused) - buffer.skipUB4() // chunk size (unused) - switch buffer.readOracleSlice() { - case .some(let slice): - columnValue = slice - case .none: - return nil // need more data - } - if !buffer.skipRawBytesChunked() { // LOB locator (unused) - return nil // need more data - } - } else { - columnValue = .init(bytes: [0]) // empty buffer - } - case .intNamed: - let startIndex = buffer.readerIndex - if try buffer.throwingReadUB4() > 0 { - if !buffer.skipRawBytesChunked() { // type oid - return nil // need more data - } - } - if try buffer.throwingReadUB4() > 0 { - if !buffer.skipRawBytesChunked() { // oid - return nil // need more data - } - } - if try buffer.throwingReadUB4() > 0 { - if !buffer.skipRawBytesChunked() { // snapshot - return nil // need more data - } - } - buffer.skipUB2() // version - let dataLength = try buffer.throwingReadUB4() - buffer.skipUB2() // flags - if dataLength > 0 { - if !buffer.skipRawBytesChunked() { // data - return nil // need more data - } - } - let endIndex = buffer.readerIndex - buffer.moveReaderIndex(to: startIndex) - columnValue = ByteBuffer(integer: Constants.TNS_LONG_LENGTH_INDICATOR) - let length = (endIndex - startIndex) + (MemoryLayout.size * 2) - columnValue.reserveCapacity(minimumWritableBytes: length) - try columnValue.writeLengthPrefixed(as: UInt32.self) { - $0.writeImmutableBuffer(buffer.readSlice(length: endIndex - startIndex)!) - } - columnValue.writeInteger(0, as: UInt32.self) // chunk length of zero - default: - fatalError( - "\(String(reflecting: oracleType)) is not implemented, please file a bug report") - } - - if [.long, .longRAW].contains(oracleType) { - buffer.skipSB4() // null indicator - buffer.skipUB4() // return code - } - - return columnValue - } - - private mutating func rowDataReceived( - buffer: inout ByteBuffer, - describeInfo: DescribeInfo, - rowHeader: inout OracleBackendMessage.RowHeader, - capabilities: Capabilities, - demandStateMachine: inout RowStreamStateMachine - ) throws -> DataRowResult { - var out = ByteBuffer() - for (index, column) in describeInfo.columns.enumerated() { - if self.isDuplicateData( - columnNumber: UInt32(index), bitVector: rowHeader.bitVector - ) { - var data = demandStateMachine.receivedDuplicate(at: index) - // write data with length, because demandStateMachine doesn't - // return the length field - try out.writeLengthPrefixed(as: UInt8.self) { buffer in - buffer.writeBuffer(&data) - } - } else if var data = try self.processColumnData( - from: &buffer, - oracleType: column.dataType._oracleType, - csfrm: column.dataType.csfrm, - bufferSize: column.bufferSize, - capabilities: capabilities - ) { - out.writeBuffer(&data) - } else { - return .notEnoughData - } - } - - let data = DataRow( - columnCount: describeInfo.columns.count, bytes: out - ) - rowHeader.bitVector = nil // reset bit vector after usage - - return .row(data) - } - var isComplete: Bool { switch self.state { case .initialized, .describeInfoReceived, .streaming, - .streamingAndWaiting, .drain: return false case .commandComplete, .error: diff --git a/Sources/OracleNIO/Messages/Coding/MissingDataDecodingError.swift b/Sources/OracleNIO/Messages/Coding/MissingDataDecodingError.swift index 2fb9a3d..4c0e463 100644 --- a/Sources/OracleNIO/Messages/Coding/MissingDataDecodingError.swift +++ b/Sources/OracleNIO/Messages/Coding/MissingDataDecodingError.swift @@ -20,5 +20,5 @@ struct MissingDataDecodingError: Error { let decodedMessages: TinySequence let resetToReaderIndex: Int - struct Trigger: Error {} + struct Trigger: Error, Equatable {} } diff --git a/Sources/OracleNIO/Messages/Coding/OracleBackendMessageDecoder.swift b/Sources/OracleNIO/Messages/Coding/OracleBackendMessageDecoder.swift index acd38dc..ea613d8 100644 --- a/Sources/OracleNIO/Messages/Coding/OracleBackendMessageDecoder.swift +++ b/Sources/OracleNIO/Messages/Coding/OracleBackendMessageDecoder.swift @@ -35,9 +35,23 @@ struct OracleBackendMessageDecoder: ByteToMessageDecoder { final class Context { var capabilities: Capabilities - var performingChunkedRead = false // TODO: remove - var statementContext: StatementContext? + private var _statementContext: StatementContext? + var statementContext: StatementContext? { + get { + self._statementContext + } + set { + self._statementContext = newValue + switch newValue?.type { + case .cursor(let cursor, _): + if cursor.describeInfo != self.describeInfo { + self.describeInfo = cursor.describeInfo + } + default: break + } + } + } var bitVector: [UInt8]? var describeInfo: DescribeInfo? diff --git a/Sources/OracleNIO/Messages/OracleBackendMessage+RowData.swift b/Sources/OracleNIO/Messages/OracleBackendMessage+RowData.swift index 6de7270..ddac971 100644 --- a/Sources/OracleNIO/Messages/OracleBackendMessage+RowData.swift +++ b/Sources/OracleNIO/Messages/OracleBackendMessage+RowData.swift @@ -33,8 +33,15 @@ extension OracleBackendMessage { ) } + let describeInfo = switch context.statementContext?.type { + case .cursor(let cursor, _): + cursor.describeInfo + default: + context.describeInfo + } + let columns: [ColumnStorage] - if let describeInfo = context.describeInfo { + if let describeInfo { columns = try self.processRowData( from: &buffer, describeInfo: describeInfo, @@ -73,23 +80,15 @@ extension OracleBackendMessage { bitVector: context.bitVector ) { columns.append(.duplicate(index)) - } else if let data = try self.processColumnData( - from: &buffer, - oracleType: column.dataType._oracleType, - csfrm: column.dataType.csfrm, - bufferSize: column.bufferSize, - capabilities: context.capabilities - ) { - if index == 0 { - var data = data.getSlice(at: 1, length: data.readableBytes - 1)! - print(try Int(from: &data, type: column.dataType, context: .default)) - } else { - var data = data.getSlice(at: 1, length: data.readableBytes - 1)! - print(try String(from: &data, type: column.dataType, context: .default)) - } - columns.append(.data(data)) } else { - throw MissingDataDecodingError.Trigger() + let data = try self.processColumnData( + from: &buffer, + oracleType: column.dataType._oracleType, + csfrm: column.dataType.csfrm, + bufferSize: column.bufferSize, + capabilities: context.capabilities + ) + columns.append(.data(data)) } } @@ -104,7 +103,7 @@ extension OracleBackendMessage { csfrm: UInt8, bufferSize: UInt32, capabilities: Capabilities - ) throws -> ByteBuffer? { + ) throws -> ByteBuffer { var columnValue: ByteBuffer if bufferSize == 0 && ![.long, .longRAW, .uRowID].contains(oracleType) { columnValue = ByteBuffer(bytes: [0]) // NULL indicator @@ -127,7 +126,7 @@ extension OracleBackendMessage { case .some(let slice): columnValue = slice case .none: - return nil // need more data + throw MissingDataDecodingError.Trigger() } case .rowID: // length is not the actual length of row ids @@ -174,7 +173,7 @@ extension OracleBackendMessage { case .some(let slice): locator = slice case .none: - return nil // need more data + throw MissingDataDecodingError.Trigger() } columnValue = ByteBuffer() try columnValue.writeLengthPrefixed(as: UInt8.self) { @@ -196,10 +195,10 @@ extension OracleBackendMessage { case .some(let slice): columnValue = slice case .none: - return nil // need more data + throw MissingDataDecodingError.Trigger() } if !buffer.skipRawBytesChunked() { // LOB locator (unused) - return nil // need more data + throw MissingDataDecodingError.Trigger() } } else { columnValue = .init(bytes: [0]) // empty buffer @@ -208,17 +207,17 @@ extension OracleBackendMessage { let startIndex = buffer.readerIndex if try buffer.throwingReadUB4() > 0 { if !buffer.skipRawBytesChunked() { // type oid - return nil // need more data + throw MissingDataDecodingError.Trigger() } } if try buffer.throwingReadUB4() > 0 { if !buffer.skipRawBytesChunked() { // oid - return nil // need more data + throw MissingDataDecodingError.Trigger() } } if try buffer.throwingReadUB4() > 0 { if !buffer.skipRawBytesChunked() { // snapshot - return nil // need more data + throw MissingDataDecodingError.Trigger() } } buffer.skipUB2() // version @@ -226,7 +225,7 @@ extension OracleBackendMessage { buffer.skipUB2() // flags if dataLength > 0 { if !buffer.skipRawBytesChunked() { // data - return nil // need more data + throw MissingDataDecodingError.Trigger() } } let endIndex = buffer.readerIndex @@ -262,16 +261,17 @@ extension OracleBackendMessage { if statementContext.isReturning { for outBind in outBinds { let rowCount = buffer.readUB4() ?? 0 - guard rowCount > 0 else { - continue - } - - for _ in 0.. 0 { + for _ in 0.. ByteBuffer { - guard let columnData = try self.processColumnData( + let columnData = try self.processColumnData( from: &buffer, oracleType: metadata.dataType._oracleType, csfrm: metadata.dataType.csfrm, bufferSize: metadata.bufferSize, capabilities: capabilities - ) else { - throw MissingDataDecodingError.Trigger() - } + ) let actualBytesCount = buffer.readSB4() ?? 0 if actualBytesCount < 0 && metadata.dataType._oracleType == .boolean { diff --git a/Sources/OracleNIO/Messages/OracleBackendMessage.swift b/Sources/OracleNIO/Messages/OracleBackendMessage.swift index 466fdd1..58963a4 100644 --- a/Sources/OracleNIO/Messages/OracleBackendMessage.swift +++ b/Sources/OracleNIO/Messages/OracleBackendMessage.swift @@ -56,8 +56,6 @@ enum OracleBackendMessage: Sendable, Hashable { case warning(BackendError) case ioVector(InOutVector) case flushOutBinds - - case chunk(ByteBuffer) } extension OracleBackendMessage { @@ -95,10 +93,6 @@ extension OracleBackendMessage { of packetID: ID, context: OracleBackendMessageDecoder.Context ) throws -> (TinySequence, lastPacket: Bool) { - // previous chunked read is definitely over! - if packetID != .data && context.performingChunkedRead { - context.performingChunkedRead = false - } switch packetID { case .resend: return (.init(element: .resend), true) @@ -114,11 +108,7 @@ extension OracleBackendMessage { var messages: TinySequence = [] let flags = try buffer.throwingReadInteger(as: UInt16.self) let lastPacket = (flags & Constants.TNS_DATA_FLAGS_END_OF_REQUEST) != 0 - if context.performingChunkedRead { - messages.append(.chunk(buffer.slice())) - } else { - try self.decodeData(from: &buffer, into: &messages, context: context) - } + try self.decodeData(from: &buffer, into: &messages, context: context) return (messages, lastPacket) } } @@ -149,32 +139,34 @@ extension OracleBackendMessage { break readLoop } case .error: - messages.append( - try .error(.decode(from: &buffer, context: context)) - ) + let error = try BackendError.decode(from: &buffer, context: context) + // not ideal but the most reliable way I could come up with + if error.number != 0 { context.clearStatementContext() } + messages.append(.error(error)) // error marks the end of response if no explicit end of // response is available if !context.capabilities.supportsEndOfRequest { break readLoop } case .parameter: - switch context.statementContext { - case .some: + if context.lobContext != nil { messages.append( - try .queryParameter(.decode(from: &buffer, context: context)) + try .lobParameter(.decode(from: &buffer, context: context)) ) - case .none: - if context.lobContext != nil { + } else { + switch context.statementContext { + case .some: messages.append( - try .lobParameter(.decode(from: &buffer, context: context)) + try .queryParameter(.decode(from: &buffer, context: context)) ) - } else { + case .none: messages.append( try .parameter(.decode(from: &buffer, context: context)) ) break readLoop } } + case .status: messages.append( try .status(.decode(from: &buffer, context: context)) @@ -200,11 +192,7 @@ extension OracleBackendMessage { messages.append( try .rowData(.decode(from: &buffer, context: context)) ) - // Until we handled the current rowData on - // OracleChannelHandler, we are performing a chunked - // read on all upcoming data packets, because we are - // "blind" and don't know what we might get until then. - context.performingChunkedRead = true // TODO: remove this + case .bitVector: let bitVector = try BitVector.decode(from: &buffer, context: context) context.bitVector = bitVector.bitVector @@ -272,8 +260,6 @@ extension OracleBackendMessage: CustomDebugStringConvertible { return ".lobParameter(\(String(reflecting: parameter)))" case .warning(let warning): return ".warning(\(String(reflecting: warning))" - case .chunk(let buffer): - return ".chunk(\(String(reflecting: buffer)))" case .serverSidePiggyback(let piggyback): return ".serverSidePiggyback(\(String(reflecting: piggyback)))" case .lobData(let data): diff --git a/Sources/OracleNIO/OracleChannelHandler.swift b/Sources/OracleNIO/OracleChannelHandler.swift index bdffe91..25ed882 100644 --- a/Sources/OracleNIO/OracleChannelHandler.swift +++ b/Sources/OracleNIO/OracleChannelHandler.swift @@ -156,7 +156,6 @@ final class OracleChannelHandler: ChannelDuplexHandler { metadata: [ .message: "\(message)" ]) - self.decoderContext.performingChunkedRead = false let action: ConnectionStateMachine.ConnectionAction switch message { @@ -218,11 +217,6 @@ final class OracleChannelHandler: ChannelDuplexHandler { action = self.state.lobDataReceived(lobData: lobData) case .lobParameter(let parameter): action = self.state.lobParameterReceived(parameter: parameter) - - case .chunk(let buffer): - action = self.state.chunkReceived( - buffer, capabilities: self.capabilities - ) } self.run(action, flags: flags, with: context) @@ -402,13 +396,8 @@ final class OracleChannelHandler: ChannelDuplexHandler { if let cleanupContext { self.closeConnectionAndCleanup(cleanupContext, context: context) } - self.decoderContext.clearStatementContext() self.run(self.state.readyForStatementReceived(), with: context) - case .needMoreData: - self.decoderContext.performingChunkedRead = true - context.read() - case .forwardRows(let rows): self.rowStream!.receive(rows) case .forwardStreamComplete(let buffer, let cursorID): @@ -422,8 +411,6 @@ final class OracleChannelHandler: ChannelDuplexHandler { } rowStream.receive(completion: .success(())) - self.decoderContext.clearStatementContext() - if cursorID != 0 { self.cleanupContext.cursorsToClose.insert(cursorID) } @@ -441,8 +428,6 @@ final class OracleChannelHandler: ChannelDuplexHandler { context.read() } - self.decoderContext.clearStatementContext() - if clientCancelled { self.run(self.state.statementStreamCancelled(), with: context) } else { @@ -668,6 +653,9 @@ final class OracleChannelHandler: ChannelDuplexHandler { ) self.decoderContext.statementContext = statementContext + if let describeInfo, self.decoderContext.describeInfo != describeInfo { + self.decoderContext.describeInfo = describeInfo + } context.writeAndFlush( self.wrapOutboundOut(self.encoder.flush()), promise: nil @@ -725,7 +713,6 @@ final class OracleChannelHandler: ChannelDuplexHandler { logger: result.logger ) promise.succeed(rows) - self.decoderContext.clearStatementContext() self.run(self.state.readyForStatementReceived(), with: context) } diff --git a/Sources/OracleNIO/Utilities/OracleHexDump.swift b/Sources/OracleNIO/Utilities/OracleHexDump.swift index 01c9c75..990f3ec 100644 --- a/Sources/OracleNIO/Utilities/OracleHexDump.swift +++ b/Sources/OracleNIO/Utilities/OracleHexDump.swift @@ -102,10 +102,15 @@ extension String { /// /// - parameters: /// - radix: radix base to use for conversion. - /// - padding: the desired lenght of the resulting string. + /// - padding: the desired length of the resulting string. @inlinable internal init(_ value: Value, radix: Int, padding: Int) where Value: BinaryInteger { let formatted = String(value, radix: radix, uppercase: true) - self = String(repeating: "0", count: padding - formatted.count) + formatted + let padding = padding - formatted.count + if padding < 0 { + self = String(formatted.dropFirst(abs(padding))) + } else { + self = String(repeating: "0", count: padding) + formatted + } } } diff --git a/Tests/IntegrationTests/OracleNIOTests.swift b/Tests/IntegrationTests/OracleNIOTests.swift index a97dcc3..16be19e 100644 --- a/Tests/IntegrationTests/OracleNIOTests.swift +++ b/Tests/IntegrationTests/OracleNIOTests.swift @@ -233,7 +233,6 @@ final class OracleNIOTests: XCTestCase { ) var index = 0 for try await row in rows.decode((Int, String).self) { - print(row) XCTAssertEqual(index + 1, row.0) index = row.0 switch index { diff --git a/Tests/OracleNIOTests/ConnectionStateMachine/StatementStateMachineTests.swift b/Tests/OracleNIOTests/ConnectionStateMachine/StatementStateMachineTests.swift index b11ff9f..8750fae 100644 --- a/Tests/OracleNIOTests/ConnectionStateMachine/StatementStateMachineTests.swift +++ b/Tests/OracleNIOTests/ConnectionStateMachine/StatementStateMachineTests.swift @@ -64,10 +64,6 @@ final class StatementStateMachineTests: XCTestCase { ]) let rowHeader = OracleBackendMessage.RowHeader() let result = StatementResult(value: .describeInfo(describeInfo.columns)) - let rowData = try Array( - hexString: - "02 c1 02 08 01 06 03 24 13 32 00 01 01 00 00 00 00 01 01 00 01 0b 0b 80 00 00 00 3d 3c 3c 80 00 00 00 01 a3 00 04 01 01 01 37 01 01 02 05 7b 00 00 01 01 01 14 03 00 00 00 00 00 00 00 00 00 00 00 00 03 00 01 01 00 00 00 00 02 05 7b 01 01 00 00 19 4f 52 41 2d 30 31 34 30 33 3a 20 6e 6f 20 64 61 74 61 20 66 6f 75 6e 64 0a" - .replacingOccurrences(of: " ", with: "")) var state = ConnectionStateMachine.readyForStatement() XCTAssertEqual( @@ -75,9 +71,9 @@ final class StatementStateMachineTests: XCTestCase { XCTAssertEqual(state.describeInfoReceived(describeInfo), .wait) XCTAssertEqual(state.rowHeaderReceived(rowHeader), .succeedStatement(promise, result)) let row1: DataRow = .makeTestDataRow(1) - XCTAssertEqual( - state.rowDataReceived(.init(columns: []), capabilities: .init()), - .forwardStreamComplete([row1], cursorID: 1)) + XCTAssertEqual(state.rowDataReceived(.init(1), capabilities: .init()), .wait) + XCTAssertEqual(state.queryParameterReceived(.init()), .wait) + XCTAssertEqual(state.backendErrorReceived(.noData), .forwardStreamComplete([row1], cursorID: 1)) } func testCancellationCompletesQueryOnlyOnce() throws { @@ -106,10 +102,6 @@ final class StatementStateMachineTests: XCTestCase { ]) let rowHeader = OracleBackendMessage.RowHeader() let result = StatementResult(value: .describeInfo(describeInfo.columns)) - let rowData = try Array( - hexString: - "05 c4 02 03 31 23 07 05 c4 02 03 31 23 08 01 06 04 bd 33 f6 cf 01 0f 01 03 00 00 00 00 01 01 00 01 0b 0b 80 00 00 00 3d 3c 3c 80 00 00 00 01 a3 00 04 01 01 01 04 01 02 00 00 00 01 03 00 03 00 00 00 00 00 00 00 00 00 00 00 00 03 00 01 01 00 00 00 00 00 01 02" - .replacingOccurrences(of: " ", with: "")) let backendError = OracleBackendMessage.BackendError( number: 1013, cursorID: 3, position: 0, rowCount: 2, isWarning: false, message: "ORA-01013: user requested cancel of current operation\n", rowID: nil, @@ -121,8 +113,13 @@ final class StatementStateMachineTests: XCTestCase { XCTAssertEqual(state.describeInfoReceived(describeInfo), .wait) XCTAssertEqual(state.rowHeaderReceived(rowHeader), .succeedStatement(promise, result)) XCTAssertEqual( - state.rowDataReceived(.init(columns: []), capabilities: .init()), - .sendFetch(queryContext)) + state.rowDataReceived(.init(1024834), capabilities: .init()), + .wait) + XCTAssertEqual( + state.rowDataReceived(.init(1024834), capabilities: .init()), + .wait) + XCTAssertEqual(state.queryParameterReceived(.init()), .wait) + XCTAssertEqual(state.backendErrorReceived(.sendFetch), .sendFetch(queryContext)) XCTAssertEqual( state.cancelStatementStream(), .forwardStreamError( @@ -157,10 +154,6 @@ final class StatementStateMachineTests: XCTestCase { ]) let rowHeader = OracleBackendMessage.RowHeader() let result = StatementResult(value: .describeInfo(describeInfo.columns)) - let rowData = try Array( - hexString: - "05 c4 02 03 31 23 07 05 c4 02 03 31 23 08 01 06 04 bd 33 f6 cf 01 0f 01 03 00 00 00 00 01 01 00 01 0b 0b 80 00 00 00 3d 3c 3c 80 00 00 00 01 a3 00 04 01 01 01 04 01 02 00 00 00 01 03 00 03 00 00 00 00 00 00 00 00 00 00 00 00 03 00 01 01 00 00 00 00 00 01 02" - .replacingOccurrences(of: " ", with: "")) var state = ConnectionStateMachine.readyForStatement() XCTAssertEqual( @@ -168,161 +161,53 @@ final class StatementStateMachineTests: XCTestCase { XCTAssertEqual(state.describeInfoReceived(describeInfo), .wait) XCTAssertEqual(state.rowHeaderReceived(rowHeader), .succeedStatement(promise, result)) XCTAssertEqual( - state.rowDataReceived(.init(columns: []), capabilities: .init()), - .sendFetch(queryContext)) + state.rowDataReceived(.init(1024834), capabilities: .init()), + .wait) + XCTAssertEqual( + state.rowDataReceived(.init(1024834), capabilities: .init()), + .wait) + XCTAssertEqual(state.queryParameterReceived(.init()), .wait) + XCTAssertEqual(state.backendErrorReceived(.sendFetch), .sendFetch(queryContext)) XCTAssertEqual( state.cancelStatementStream(), .forwardStreamError( .statementCancelled, read: false, cursorID: nil, clientCancelled: true)) XCTAssertEqual(state.statementStreamCancelled(), .sendMarker(read: true)) } +} - func testProcessVectorColumnDataRequestsMissingData() { - let state = StatementStateMachine( - statementContext: .init(statement: "") - ) - let type = OracleDataType.vector - - var buffer = ByteBuffer(bytes: [ - 1, 1, // length - 0, // size - 0, // chunk size - 1, // value (partial) - ]) - try XCTAssertNil( - state.processColumnData( - from: &buffer, - oracleType: type._oracleType, - csfrm: type.csfrm, - bufferSize: 1, - capabilities: .init() - )) - - buffer = ByteBuffer(bytes: [ - 1, 1, // length - 0, // size - 0, // chunk size - 1, 1, // value - 1, // locator (partial) - ]) - try XCTAssertNil( - state.processColumnData( - from: &buffer, - oracleType: type._oracleType, - csfrm: type.csfrm, - bufferSize: 1, - capabilities: .init() - )) - } - - func testProcessObjectColumnDataRequestsMissingData() throws { - let state = StatementStateMachine( - statementContext: .init(statement: "") - ) - let type = OracleDataType.object - - var buffer = ByteBuffer(bytes: [1, 1]) // type oid - try XCTAssertNil( - state.processColumnData( - from: &buffer, - oracleType: type._oracleType, - csfrm: type.csfrm, - bufferSize: 1, - capabilities: .init() - )) - - buffer = ByteBuffer(bytes: [ - 1, 1, 0, // type oid - 1, 1, // oid - ]) - try XCTAssertNil( - state.processColumnData( - from: &buffer, - oracleType: type._oracleType, - csfrm: type.csfrm, - bufferSize: 1, - capabilities: .init() - )) - - buffer = ByteBuffer(bytes: [ - 1, 1, 0, // type oid - 1, 1, 0, // oid - 1, 1, // snapshot - ]) - try XCTAssertNil( - state.processColumnData( - from: &buffer, - oracleType: type._oracleType, - csfrm: type.csfrm, - bufferSize: 1, - capabilities: .init() - )) - - buffer = ByteBuffer(bytes: [ - 1, 1, 0, // type oid - 1, 1, 0, // oid - 1, 1, 0, // snapshot - 0, // version - 0, // data length - 0, // flags - ]) - try XCTAssertNotNil( - state.processColumnData( - from: &buffer, - oracleType: type._oracleType, - csfrm: type.csfrm, - bufferSize: 1, - capabilities: .init() - )) +extension OracleBackendMessage.RowData { + init(_ elements: T...) { + var columns: [OracleBackendMessage.RowData.ColumnStorage] = [] + for element in elements { + var buffer = ByteBuffer() + element._encodeRaw(into: &buffer, context: .default) + columns.append(.data(buffer)) + } + self.init(columns: columns) } +} - func testProcessLOBColumnDataRequestsMissingData() throws { - let state = StatementStateMachine( - statementContext: .init(statement: "") - ) - let type = OracleDataType.blob - - var buffer = ByteBuffer(bytes: [ - 1, 1, // length - 1, 1, // size - 1, 1, // chunk size - 2, 0, // locator (partial) - ]) - try XCTAssertNil( - state.processColumnData( - from: &buffer, - oracleType: type._oracleType, - csfrm: type.csfrm, - bufferSize: 1, - capabilities: .init() - ) - ) - - buffer = ByteBuffer(bytes: [ - 1, 1, // length - 1, 1, // size - 1, 1, // chunk size - 1, 0, // locator - ]) - try XCTAssertNotNil( - state.processColumnData( - from: &buffer, - oracleType: type._oracleType, - csfrm: type.csfrm, - bufferSize: 1, - capabilities: .init() - ) - ) - - buffer = ByteBuffer(bytes: [0]) - try XCTAssertNotNil( - state.processColumnData( - from: &buffer, - oracleType: type._oracleType, - csfrm: type.csfrm, - bufferSize: 1, - capabilities: .init() - ) - ) - } +extension OracleBackendMessage.BackendError { + static let noData = OracleBackendMessage.BackendError( + number: 1403, + cursorID: 1, + position: 20, + rowCount: 1, + isWarning: false, + message: "ORA-01403: no data found\n", + rowID: nil, + batchErrors: [] + ) + + static let sendFetch = OracleBackendMessage.BackendError( + number: 0, + cursorID: 3, + position: 0, + rowCount: 2, + isWarning: false, + message: nil, + rowID: nil, + batchErrors: [] + ) } diff --git a/Tests/OracleNIOTests/Messages/RowDataTests.swift b/Tests/OracleNIOTests/Messages/RowDataTests.swift new file mode 100644 index 0000000..8b0bff4 --- /dev/null +++ b/Tests/OracleNIOTests/Messages/RowDataTests.swift @@ -0,0 +1,121 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the OracleNIO open source project +// +// Copyright (c) 2024 Timo Zacherl and the OracleNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE for license information +// See CONTRIBUTORS.md for the list of OracleNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore +import XCTest + +@testable import OracleNIO + +private typealias RowData = OracleBackendMessage.RowData + +final class RowDataTests: XCTestCase { + + func testProcessVectorColumnDataRequestsMissingData() { + let type = OracleDataType.vector + + var buffer = ByteBuffer(bytes: [ + 1, 1, // length + 0, // size + 0, // chunk size + 1, // value (partial) + ]) + XCTAssertThrowsError( + try RowData.decode(from: &buffer, context: .init(columns: type)), + expected: MissingDataDecodingError.Trigger() + ) + + buffer = ByteBuffer(bytes: [ + 1, 1, // length + 0, // size + 0, // chunk size + 1, 1, // value + 1, // locator (partial) + ]) + XCTAssertThrowsError( + try RowData.decode(from: &buffer, context: .init(columns: type)), + expected: MissingDataDecodingError.Trigger() + ) + } + + func testProcessObjectColumnDataRequestsMissingData() throws { + let type = OracleDataType.object + + var buffer = ByteBuffer(bytes: [1, 1]) // type oid + XCTAssertThrowsError( + try RowData.decode(from: &buffer, context: .init(columns: type)), + expected: MissingDataDecodingError.Trigger() + ) + + buffer = ByteBuffer(bytes: [ + 1, 1, 0, // type oid + 1, 1, // oid + ]) + XCTAssertThrowsError( + try RowData.decode(from: &buffer, context: .init(columns: type)), + expected: MissingDataDecodingError.Trigger() + ) + + buffer = ByteBuffer(bytes: [ + 1, 1, 0, // type oid + 1, 1, 0, // oid + 1, 1, // snapshot + ]) + XCTAssertThrowsError( + try RowData.decode(from: &buffer, context: .init(columns: type)), + expected: MissingDataDecodingError.Trigger() + ) + + buffer = ByteBuffer(bytes: [ + 1, 1, 0, // type oid + 1, 1, 0, // oid + 1, 1, 0, // snapshot + 0, // version + 0, // data length + 0, // flags + ]) + XCTAssertNoThrow( + try RowData.decode(from: &buffer, context: .init(columns: type)) + ) + } + + func testProcessLOBColumnDataRequestsMissingData() throws { + let type = OracleDataType.blob + + var buffer = ByteBuffer(bytes: [ + 1, 1, // length + 1, 1, // size + 1, 1, // chunk size + 2, 0, // locator (partial) + ]) + XCTAssertThrowsError( + try RowData.decode(from: &buffer, context: .init(columns: type)), + expected: MissingDataDecodingError.Trigger() + ) + + buffer = ByteBuffer(bytes: [ + 1, 1, // length + 1, 1, // size + 1, 1, // chunk size + 1, 0, // locator + ]) + XCTAssertNoThrow( + try RowData.decode(from: &buffer, context: .init(columns: type)) + ) + + buffer = ByteBuffer(bytes: [0]) + XCTAssertNoThrow( + try RowData.decode(from: &buffer, context: .init(columns: type)) + ) + } +} diff --git a/Tests/OracleNIOTests/TestUtils/ConnectionAction+TestUtils.swift b/Tests/OracleNIOTests/TestUtils/ConnectionAction+TestUtils.swift index 1d1df25..ae7ca4c 100644 --- a/Tests/OracleNIOTests/TestUtils/ConnectionAction+TestUtils.swift +++ b/Tests/OracleNIOTests/TestUtils/ConnectionAction+TestUtils.swift @@ -127,8 +127,6 @@ extension ConnectionStateMachine.ConnectionAction { ): return lhsPromise.futureResult === rhsPromise.futureResult && lhsResult.value == rhsResult.value - case (.needMoreData, .needMoreData): - return true case (.forwardRows(let lhs), .forwardRows(let rhs)): return lhs == rhs diff --git a/Tests/OracleNIOTests/TestUtils/OracleBackendMessageDecoder+TestUtils.swift b/Tests/OracleNIOTests/TestUtils/OracleBackendMessageDecoder+TestUtils.swift new file mode 100644 index 0000000..dfe92eb --- /dev/null +++ b/Tests/OracleNIOTests/TestUtils/OracleBackendMessageDecoder+TestUtils.swift @@ -0,0 +1,46 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the OracleNIO open source project +// +// Copyright (c) 2024 Timo Zacherl and the OracleNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE for license information +// See CONTRIBUTORS.md for the list of OracleNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@testable import OracleNIO + +extension OracleBackendMessageDecoder.Context { + convenience init( + capabilities: Capabilities = Capabilities(), + statementContext: StatementContext = StatementContext(statement: ""), + columns: OracleDataType... + ) { + self.init(capabilities: capabilities) + self.statementContext = statementContext + var describeInfo = DescribeInfo(columns: []) + for column in columns { + describeInfo.columns.append(.init( + name: "", + dataType: column, + dataTypeSize: UInt32(column.defaultSize), + precision: 0, + scale: 0, + bufferSize: 1, + nullsAllowed: true, + typeScheme: nil, + typeName: nil, + domainSchema: nil, + domainName: nil, + annotations: [:], + vectorDimensions: nil, + vectorFormat: nil + )) + } + self.describeInfo = describeInfo + } +} diff --git a/Tests/OracleNIOTests/Utilities/OracleHexDumpTests.swift b/Tests/OracleNIOTests/Utilities/OracleHexDumpTests.swift index 1234eff..be140a3 100644 --- a/Tests/OracleNIOTests/Utilities/OracleHexDumpTests.swift +++ b/Tests/OracleNIOTests/Utilities/OracleHexDumpTests.swift @@ -38,4 +38,8 @@ final class OracleHexDumpTests: XCTestCase { let buffer = ByteBuffer() XCTAssertEqual(buffer.oracleHexDump(), "") } + + func testStringInitializationIsTruncatedIfNeeded() { + XCTAssertEqual(String(10_000, radix: 10, padding: 4), "0000") + } }