1use crate::thin::{
17 encode_binary_double, encode_binary_float, encode_number_text, encode_oracle_date,
18 encode_oracle_timestamp, encode_oracle_timestamp_tz, parse_column_metadata,
19 parse_server_error_info, skip_server_side_piggyback, ClientCapabilities, ColumnMetadata,
20 CS_FORM_IMPLICIT, CS_FORM_NCHAR, ORA_TYPE_NUM_BINARY_DOUBLE, ORA_TYPE_NUM_BINARY_FLOAT,
21 ORA_TYPE_NUM_BINARY_INTEGER, ORA_TYPE_NUM_BLOB, ORA_TYPE_NUM_BOOLEAN, ORA_TYPE_NUM_CHAR,
22 ORA_TYPE_NUM_CLOB, ORA_TYPE_NUM_DATE, ORA_TYPE_NUM_LONG, ORA_TYPE_NUM_LONG_RAW,
23 ORA_TYPE_NUM_NUMBER, ORA_TYPE_NUM_RAW, ORA_TYPE_NUM_TIMESTAMP, ORA_TYPE_NUM_TIMESTAMP_LTZ,
24 ORA_TYPE_NUM_TIMESTAMP_TZ, ORA_TYPE_NUM_VARCHAR, TNS_MSG_TYPE_END_OF_RESPONSE,
25 TNS_MSG_TYPE_ERROR, TNS_MSG_TYPE_PARAMETER, TNS_MSG_TYPE_SERVER_SIDE_PIGGYBACK,
26 TNS_MSG_TYPE_STATUS,
27};
28use crate::wire::{BoundedReader, ProtocolLimits, TtcReader, TtcWriter};
29use crate::{ProtocolError, Result};
30
31pub const TNS_FUNC_DIRECT_PATH_PREPARE: u8 = 128;
32pub const TNS_FUNC_DIRECT_PATH_LOAD_STREAM: u8 = 129;
33pub const TNS_FUNC_DIRECT_PATH_OP: u8 = 130;
34
35pub const TNS_DP_INTERFACE_VERSION: u32 = 400;
36pub const TNS_DP_STREAM_VERSION: u32 = 400;
37
38pub const TNS_DPP_OP_CODE_LOAD: u32 = 1;
39
40pub const TNS_DP_OP_ABORT: u32 = 1;
41pub const TNS_DP_OP_FINISH: u32 = 2;
42
43const TNS_DPP_IN_INDEX_INTERFACE_VERSION: usize = 0;
44const TNS_DPP_IN_INDEX_STREAM_VERSION: usize = 1;
45const TNS_DPP_IN_INDEX_LOCK_WAIT: usize = 14;
46const TNS_DPP_KW_INDEX_OBJECT_NAME: u16 = 1;
47const TNS_DPP_KW_INDEX_SCHEMA_NAME: u16 = 3;
48const TNS_DPP_KW_INDEX_COLUMN_NAME: u16 = 4;
49const TNS_DPP_KW_INDEX_NFOBJ_OID_POS: usize = 11;
50const TNS_DPP_OUT_INDEX_CURSOR: usize = 3;
51const TNS_DPP_IN_VALUES_SENT: usize = TNS_DPP_IN_INDEX_LOCK_WAIT + 1;
55
56pub const TNS_DPLS_ROW_HEADER_FAST_PIECE: u8 = 0x10;
57pub const TNS_DPLS_ROW_HEADER_FAST_ROW: u8 = 0x20;
58pub const TNS_DPLS_ROW_HEADER_FIRST: u8 = 0x08;
59pub const TNS_DPLS_ROW_HEADER_LAST: u8 = 0x04;
60pub const TNS_DPLS_ROW_HEADER_SPLIT_WITH_PREV: u8 = 0x02;
61pub const TNS_DPLS_ROW_HEADER_SPLIT_WITH_NEXT: u8 = 0x01;
62
63pub const TNS_DPLS_MAX_MESSAGE_SIZE: u64 = 1_073_728_895;
64pub const TNS_DPLS_MAX_SHORT_LENGTH: usize = 0xfa;
65pub const TNS_DPLS_MAX_PIECE_SIZE: usize = 0xfff0;
66
67const TNS_DPLS_LONG_LENGTH_INDICATOR: u8 = 0xfe;
68const TNS_NULL_LENGTH_INDICATOR: u8 = 0xff;
69
70pub fn build_direct_path_prepare_payload(
74 schema_name: &str,
75 table_name: &str,
76 column_names: &[String],
77 seq_num: u8,
78) -> Result<Vec<u8>> {
79 build_direct_path_prepare_payload_with_version(
80 schema_name,
81 table_name,
82 column_names,
83 seq_num,
84 crate::thin::TNS_CCAP_FIELD_VERSION_23_1_EXT_1,
85 )
86}
87
88pub fn build_direct_path_prepare_payload_with_version(
92 schema_name: &str,
93 table_name: &str,
94 column_names: &[String],
95 seq_num: u8,
96 ttc_field_version: u8,
97) -> Result<Vec<u8>> {
98 let keyword_parameters_length =
99 u32::try_from(column_names.len() + 2).map_err(|_| ProtocolError::InvalidPacketLength {
100 length: column_names.len(),
101 minimum: 0,
102 })?;
103
104 let mut in_values = [0u32; TNS_DPP_IN_VALUES_SENT];
105 in_values[TNS_DPP_IN_INDEX_INTERFACE_VERSION] = TNS_DP_INTERFACE_VERSION;
106 in_values[TNS_DPP_IN_INDEX_STREAM_VERSION] = TNS_DP_STREAM_VERSION;
107 in_values[TNS_DPP_KW_INDEX_NFOBJ_OID_POS] = 0xffff;
108 in_values[TNS_DPP_IN_INDEX_LOCK_WAIT] = 1;
109
110 let mut writer = TtcWriter::new();
111 writer.write_function_header(TNS_FUNC_DIRECT_PATH_PREPARE, seq_num, ttc_field_version);
112 writer.write_ub4(TNS_DPP_OP_CODE_LOAD);
113 writer.write_u8(1); writer.write_ub4(keyword_parameters_length);
115 writer.write_u8(1); writer.write_ub2(TNS_DPP_IN_VALUES_SENT as u16);
117 writer.write_u8(1); writer.write_u8(1); writer.write_u8(1); writer.write_u8(1); writer.write_u8(1); writer.write_u8(1); write_keyword_param(&mut writer, TNS_DPP_KW_INDEX_SCHEMA_NAME, schema_name)?;
124 write_keyword_param(&mut writer, TNS_DPP_KW_INDEX_OBJECT_NAME, table_name)?;
125 for name in column_names {
126 write_keyword_param(&mut writer, TNS_DPP_KW_INDEX_COLUMN_NAME, name)?;
127 }
128 for value in in_values {
129 writer.write_ub4(value);
130 }
131 Ok(writer.into_bytes())
132}
133
134fn write_keyword_param(writer: &mut TtcWriter, index: u16, value: &str) -> Result<()> {
135 let bytes = value.as_bytes();
136 let len = u16::try_from(bytes.len()).map_err(|_| ProtocolError::InvalidPacketLength {
137 length: bytes.len(),
138 minimum: 0,
139 })?;
140 writer.write_ub2(0); writer.write_ub2(len);
142 writer.write_bytes_with_length(bytes)?;
143 writer.write_ub2(index);
144 Ok(())
145}
146
147#[derive(Clone, Debug, Default, Eq, PartialEq)]
148pub struct DirectPathPrepareResult {
149 pub column_metadata: Vec<ColumnMetadata>,
150 pub cursor_id: u16,
151 pub out_values: Vec<u32>,
152}
153
154pub fn parse_direct_path_prepare_response(
161 payload: &[u8],
162 capabilities: ClientCapabilities,
163) -> Result<DirectPathPrepareResult> {
164 parse_direct_path_prepare_response_with_limits(payload, capabilities, ProtocolLimits::DEFAULT)
165}
166
167pub fn parse_direct_path_prepare_response_with_limits(
168 payload: &[u8],
169 capabilities: ClientCapabilities,
170 limits: ProtocolLimits,
171) -> Result<DirectPathPrepareResult> {
172 let mut reader = TtcReader::with_limits(payload, limits)?;
173 let mut result: Option<DirectPathPrepareResult> = None;
174 while reader.remaining() > 0 {
175 let message_type = reader.read_u8()?;
176 match message_type {
177 0 => {}
178 TNS_MSG_TYPE_PARAMETER => {
179 result = Some(parse_prepare_return_parameters(&mut reader, capabilities)?);
180 }
181 TNS_MSG_TYPE_STATUS => {
182 let _call_status = reader.read_ub4()?;
183 let _seq = reader.read_ub2()?;
184 }
185 TNS_MSG_TYPE_SERVER_SIDE_PIGGYBACK => {
186 let _ = skip_server_side_piggyback(&mut reader)?;
187 }
188 TNS_MSG_TYPE_END_OF_RESPONSE => break,
189 TNS_MSG_TYPE_ERROR => {
190 let info = parse_server_error_info(&mut reader, capabilities.ttc_field_version)?;
191 if info.number != 0 {
192 return Err(ProtocolError::ServerError(info.message));
193 }
194 }
195 _ => {
196 return Err(ProtocolError::UnknownMessageType {
197 message_type,
198 position: reader.position().saturating_sub(1),
199 })
200 }
201 }
202 }
203 result.ok_or(ProtocolError::TtcDecode(
204 "direct path prepare response did not contain return parameters",
205 ))
206}
207
208fn parse_prepare_return_parameters(
209 reader: &mut TtcReader<'_>,
210 capabilities: ClientCapabilities,
211) -> Result<DirectPathPrepareResult> {
212 let num_columns = reader.read_ub4()?;
213 reader.limits().check_columns(num_columns as usize)?;
214 let mut column_metadata: Vec<ColumnMetadata> =
218 reader.with_capacity_limited(num_columns as usize, 1, ProtocolLimits::check_columns)?;
219 for _ in 0..num_columns {
220 let mut metadata = parse_column_metadata(reader, capabilities)?;
221 apply_direct_path_metadata_overrides(&mut metadata, capabilities.charset_id);
222 column_metadata.push(metadata);
223 }
224 let num_params = reader.read_ub2()?;
225 if num_params != 0 {
226 return Err(ProtocolError::TtcDecode(
227 "unexpected parameters in direct path prepare response",
228 ));
229 }
230 let out_values_length = reader.read_ub2()?;
231 reader
232 .limits()
233 .check_length_prefixed_elements(usize::from(out_values_length))?;
234 let mut out_values: Vec<u32> = reader.with_capacity_limited(
236 usize::from(out_values_length),
237 1,
238 ProtocolLimits::check_length_prefixed_elements,
239 )?;
240 for _ in 0..out_values_length {
241 out_values.push(reader.read_ub4()?);
242 }
243 let cursor_id =
244 out_values
245 .get(TNS_DPP_OUT_INDEX_CURSOR)
246 .copied()
247 .ok_or(ProtocolError::TtcDecode(
248 "direct path prepare response missing cursor id",
249 ))?;
250 let cursor_id = u16::try_from(cursor_id)
251 .map_err(|_| ProtocolError::TtcDecode("direct path cursor id out of range"))?;
252 Ok(DirectPathPrepareResult {
253 column_metadata,
254 cursor_id,
255 out_values,
256 })
257}
258
259fn apply_direct_path_metadata_overrides(metadata: &mut ColumnMetadata, charset_id: u16) {
263 if metadata.ora_type_num == ORA_TYPE_NUM_CLOB {
264 if metadata.csfrm == CS_FORM_IMPLICIT && charset_id >= 800 {
265 metadata.csfrm = CS_FORM_NCHAR;
266 }
267 metadata.ora_type_num = ORA_TYPE_NUM_LONG;
268 } else if metadata.ora_type_num == ORA_TYPE_NUM_BLOB {
269 metadata.ora_type_num = ORA_TYPE_NUM_LONG_RAW;
270 metadata.csfrm = 0;
271 }
272}
273
274pub fn build_direct_path_op_payload(cursor_id: u16, op_code: u32, seq_num: u8) -> Vec<u8> {
279 build_direct_path_op_payload_with_version(
280 cursor_id,
281 op_code,
282 seq_num,
283 crate::thin::TNS_CCAP_FIELD_VERSION_23_1_EXT_1,
284 )
285}
286
287pub fn build_direct_path_op_payload_with_version(
294 cursor_id: u16,
295 op_code: u32,
296 seq_num: u8,
297 ttc_field_version: u8,
298) -> Vec<u8> {
299 let mut writer = TtcWriter::new();
300 writer.write_function_header(TNS_FUNC_DIRECT_PATH_OP, seq_num, ttc_field_version);
301 writer.write_ub4(op_code);
302 writer.write_ub2(cursor_id);
303 writer.write_u8(0); writer.write_ub4(0); writer.write_u8(1); writer.write_u8(1); writer.into_bytes()
308}
309
310pub fn parse_direct_path_simple_response(
313 payload: &[u8],
314 capabilities: ClientCapabilities,
315) -> Result<()> {
316 parse_direct_path_simple_response_with_limits(payload, capabilities, ProtocolLimits::DEFAULT)
317}
318
319pub fn parse_direct_path_simple_response_with_limits(
320 payload: &[u8],
321 capabilities: ClientCapabilities,
322 limits: ProtocolLimits,
323) -> Result<()> {
324 let mut reader = TtcReader::with_limits(payload, limits)?;
325 while reader.remaining() > 0 {
326 let message_type = reader.read_u8()?;
327 match message_type {
328 0 => {}
329 TNS_MSG_TYPE_PARAMETER => {
330 let num_out_values = reader.read_ub2()?;
331 for _ in 0..num_out_values {
332 let _value = reader.read_ub4()?;
333 }
334 }
335 TNS_MSG_TYPE_STATUS => {
336 let _call_status = reader.read_ub4()?;
337 let _seq = reader.read_ub2()?;
338 }
339 TNS_MSG_TYPE_SERVER_SIDE_PIGGYBACK => {
340 let _ = skip_server_side_piggyback(&mut reader)?;
341 }
342 TNS_MSG_TYPE_END_OF_RESPONSE => break,
343 TNS_MSG_TYPE_ERROR => {
344 let info = parse_server_error_info(&mut reader, capabilities.ttc_field_version)?;
345 if info.number != 0 {
346 return Err(ProtocolError::ServerError(info.message));
347 }
348 }
349 _ => {
350 return Err(ProtocolError::UnknownMessageType {
351 message_type,
352 position: reader.position().saturating_sub(1),
353 })
354 }
355 }
356 }
357 Ok(())
358}
359
360pub use parse_direct_path_simple_response as parse_direct_path_load_stream_response;
361pub use parse_direct_path_simple_response as parse_direct_path_op_response;
362pub use parse_direct_path_simple_response_with_limits as parse_direct_path_load_stream_response_with_limits;
363pub use parse_direct_path_simple_response_with_limits as parse_direct_path_op_response_with_limits;
364
365#[derive(Clone, Debug, PartialEq)]
371pub enum DirectPathColumnValue {
372 Null,
373 Bytes(Vec<u8>),
374 Number(String),
375 BinaryDouble(f64),
376 BinaryFloat(f32),
377 DateTime {
378 year: i32,
379 month: u8,
380 day: u8,
381 hour: u8,
382 minute: u8,
383 second: u8,
384 nanosecond: u32,
385 },
386 Boolean(bool),
387}
388
389#[derive(Clone, Debug, Eq, PartialEq)]
392pub struct DirectPathPiece {
393 flags: u8,
394 num_segments: u8,
395 data: Vec<u8>,
396}
397
398impl DirectPathPiece {
399 pub fn flags(&self) -> u8 {
400 self.flags
401 }
402
403 pub fn num_segments(&self) -> u8 {
404 self.num_segments
405 }
406
407 pub fn data(&self) -> &[u8] {
408 &self.data
409 }
410
411 fn is_fast_row(&self) -> bool {
412 self.flags & TNS_DPLS_ROW_HEADER_FAST_ROW != 0
413 }
414
415 fn header_length(&self) -> u64 {
416 if self.is_fast_row() {
417 4
418 } else {
419 2
420 }
421 }
422
423 fn write_to(&self, writer: &mut TtcWriter) -> Result<()> {
424 writer.write_u8(self.flags);
425 if self.is_fast_row() {
426 let total = self.data.len() as u64 + self.header_length();
427 let total = u16::try_from(total).map_err(|_| {
428 ProtocolError::TtcDecode("direct path fast piece exceeds 16-bit length")
429 })?;
430 writer.write_u16be(total);
431 }
432 writer.write_u8(self.num_segments);
433 writer.write_raw(&self.data);
434 Ok(())
435 }
436}
437
438#[derive(Clone, Copy, Debug, Default)]
439struct PieceState {
440 is_first: bool,
441 is_last: bool,
442 is_split_with_prev: bool,
443 is_split_with_next: bool,
444 is_fast: bool,
445 num_segments: u16,
446}
447
448#[derive(Debug, Default)]
454pub(crate) struct DirectPathPieceBuffer {
455 pieces: Vec<DirectPathPiece>,
456 total_piece_length: u64,
457 data: Vec<u8>,
458 current: Option<PieceState>,
459}
460
461impl DirectPathPieceBuffer {
462 pub fn new() -> Self {
463 Self::default()
464 }
465
466 pub fn start_row(&mut self) -> Result<()> {
467 if self.current.is_some() {
468 return Err(ProtocolError::TtcDecode(
469 "direct path row started before previous row was finished",
470 ));
471 }
472 self.current = Some(PieceState {
473 is_first: true,
474 is_fast: true,
475 ..PieceState::default()
476 });
477 Ok(())
478 }
479
480 pub fn finish_row(&mut self) -> Result<()> {
481 let Some(state) = self.current.as_mut() else {
482 return Err(ProtocolError::TtcDecode(
483 "direct path row finished without being started",
484 ));
485 };
486 state.is_last = true;
487 self.finalize_piece()?;
488 self.current = None;
489 Ok(())
490 }
491
492 pub fn add_column_value(
493 &mut self,
494 metadata: &ColumnMetadata,
495 value: &DirectPathColumnValue,
496 row_num: u64,
497 ) -> Result<()> {
498 let Some(state) = self.current.as_mut() else {
499 return Err(ProtocolError::TtcDecode(
500 "direct path column value added outside of a row",
501 ));
502 };
503
504 if state.num_segments == 255 {
506 self.finalize_piece()?;
507 self.current = Some(PieceState::default());
508 }
509
510 if !is_fast_dbtype(metadata) {
511 if let Some(state) = self.current.as_mut() {
512 state.is_fast = false;
513 }
514 }
515
516 match value {
517 DirectPathColumnValue::Null => {
518 if !metadata.nulls_allowed {
519 return Err(ProtocolError::NullsNotAllowed {
520 column_name: metadata.name.clone(),
521 row_num,
522 });
523 }
524 self.write_u8_in_piece(TNS_NULL_LENGTH_INDICATOR)?;
525 self.bump_segments();
526 Ok(())
527 }
528 DirectPathColumnValue::Bytes(bytes) => {
529 if !matches!(
530 metadata.ora_type_num,
531 ORA_TYPE_NUM_VARCHAR
532 | ORA_TYPE_NUM_CHAR
533 | ORA_TYPE_NUM_LONG
534 | ORA_TYPE_NUM_RAW
535 | ORA_TYPE_NUM_LONG_RAW
536 ) {
537 return Err(ProtocolError::TtcDecode(
538 "direct path byte value sent for non-character column",
539 ));
540 }
541 if metadata.max_size > 0 && bytes.len() as u64 > u64::from(metadata.max_size) {
542 return Err(ProtocolError::ValueTooLarge {
543 actual_size: bytes.len(),
544 max_size: metadata.max_size,
545 column_name: metadata.name.clone(),
546 row_num,
547 });
548 }
549 self.write_raw_bytes_and_length(bytes)
550 }
551 DirectPathColumnValue::Number(text) => {
552 if !matches!(
553 metadata.ora_type_num,
554 ORA_TYPE_NUM_NUMBER | ORA_TYPE_NUM_BINARY_INTEGER
555 ) {
556 return Err(ProtocolError::TtcDecode(
557 "direct path number value sent for non-number column",
558 ));
559 }
560 let encoded = encode_number_text(text)?;
561 self.write_raw_bytes_and_length(&encoded)
562 }
563 DirectPathColumnValue::BinaryDouble(value) => {
564 if metadata.ora_type_num != ORA_TYPE_NUM_BINARY_DOUBLE {
565 return Err(ProtocolError::TtcDecode(
566 "direct path binary double sent for other column type",
567 ));
568 }
569 let encoded = encode_binary_double(*value);
570 self.write_raw_bytes_and_length(&encoded)
571 }
572 DirectPathColumnValue::BinaryFloat(value) => {
573 if metadata.ora_type_num != ORA_TYPE_NUM_BINARY_FLOAT {
574 return Err(ProtocolError::TtcDecode(
575 "direct path binary float sent for other column type",
576 ));
577 }
578 let encoded = encode_binary_float(*value);
579 self.write_raw_bytes_and_length(&encoded)
580 }
581 DirectPathColumnValue::DateTime {
582 year,
583 month,
584 day,
585 hour,
586 minute,
587 second,
588 nanosecond,
589 } => {
590 let encoded = match metadata.ora_type_num {
591 ORA_TYPE_NUM_DATE => {
592 if *nanosecond != 0 {
593 return Err(ProtocolError::TtcDecode(
594 "direct path DATE value has fractional seconds",
595 ));
596 }
597 encode_oracle_date(*year, *month, *day, *hour, *minute, *second)?.to_vec()
598 }
599 ORA_TYPE_NUM_TIMESTAMP | ORA_TYPE_NUM_TIMESTAMP_LTZ => encode_oracle_timestamp(
602 *year,
603 *month,
604 *day,
605 *hour,
606 *minute,
607 *second,
608 *nanosecond,
609 )?,
610 ORA_TYPE_NUM_TIMESTAMP_TZ => encode_oracle_timestamp_tz(
611 *year,
612 *month,
613 *day,
614 *hour,
615 *minute,
616 *second,
617 *nanosecond,
618 )?,
619 _ => {
620 return Err(ProtocolError::TtcDecode(
621 "direct path datetime sent for non-datetime column",
622 ))
623 }
624 };
625 self.write_raw_bytes_and_length(&encoded)
626 }
627 DirectPathColumnValue::Boolean(value) => {
628 if metadata.ora_type_num != ORA_TYPE_NUM_BOOLEAN {
629 return Err(ProtocolError::TtcDecode(
630 "direct path boolean sent for non-boolean column",
631 ));
632 }
633 let encoded: &[u8] = if *value { &[1, 1] } else { &[0] };
634 self.write_raw_bytes_and_length(encoded)
635 }
636 }
637 }
638
639 pub fn finish(self) -> Result<(Vec<DirectPathPiece>, u32)> {
642 if self.current.is_some() {
643 return Err(ProtocolError::TtcDecode(
644 "direct path stream finished mid-row",
645 ));
646 }
647 let total = u32::try_from(self.total_piece_length)
648 .map_err(|_| ProtocolError::DirectPathLoadTooMuchData)?;
649 Ok((self.pieces, total))
650 }
651
652 fn bump_segments(&mut self) {
653 if let Some(state) = self.current.as_mut() {
654 state.num_segments = state.num_segments.saturating_add(1);
655 }
656 }
657
658 fn space_left(&self) -> usize {
659 TNS_DPLS_MAX_PIECE_SIZE.saturating_sub(self.data.len())
660 }
661
662 fn write_u8_in_piece(&mut self, value: u8) -> Result<()> {
663 if self.space_left() < 1 {
664 self.finalize_piece()?;
665 self.current = Some(PieceState::default());
666 }
667 self.data.push(value);
668 Ok(())
669 }
670
671 fn write_raw_bytes_and_length(&mut self, bytes: &[u8]) -> Result<()> {
676 if bytes.len() <= TNS_DPLS_MAX_SHORT_LENGTH {
677 if bytes.len() + 1 > self.space_left() {
678 self.finalize_piece()?;
679 self.current = Some(PieceState::default());
680 }
681 self.data.push(bytes.len() as u8);
682 self.data.extend_from_slice(bytes);
683 self.bump_segments();
684 return Ok(());
685 }
686
687 let mut remaining = bytes;
688 while remaining.len() + 3 > self.space_left() {
689 if self.space_left() < 4 {
693 self.finalize_piece()?;
694 self.current = Some(PieceState::default());
695 continue;
696 }
697 let chunk_len = self.space_left() - 3;
698 let (chunk, rest) = remaining.split_at(chunk_len.min(remaining.len()));
699 self.data.push(TNS_DPLS_LONG_LENGTH_INDICATOR);
700 self.data
701 .extend_from_slice(&(chunk.len() as u16).to_be_bytes());
702 self.data.extend_from_slice(chunk);
703 remaining = rest;
704 if let Some(state) = self.current.as_mut() {
705 state.is_split_with_next = true;
706 }
707 self.bump_segments();
708 self.finalize_piece()?;
709 self.current = Some(PieceState {
710 is_split_with_prev: !remaining.is_empty(),
711 ..PieceState::default()
712 });
713 }
714 if !remaining.is_empty() {
715 self.bump_segments();
716 self.data.push(TNS_DPLS_LONG_LENGTH_INDICATOR);
717 self.data
718 .extend_from_slice(&(remaining.len() as u16).to_be_bytes());
719 self.data.extend_from_slice(remaining);
720 }
721 Ok(())
722 }
723
724 fn finalize_piece(&mut self) -> Result<()> {
725 let Some(state) = self.current.take() else {
726 return Err(ProtocolError::TtcDecode(
727 "direct path piece finalized without an active piece",
728 ));
729 };
730 let mut flags = 0u8;
731 if state.is_first {
732 flags |= TNS_DPLS_ROW_HEADER_FIRST;
733 } else if state.is_split_with_prev {
734 flags |= TNS_DPLS_ROW_HEADER_SPLIT_WITH_PREV;
735 }
736 if state.is_last {
737 flags |= TNS_DPLS_ROW_HEADER_LAST;
738 } else if state.is_split_with_next {
739 flags |= TNS_DPLS_ROW_HEADER_SPLIT_WITH_NEXT;
740 }
741 let is_fast_row = state.is_first && state.is_last && state.is_fast;
742 if is_fast_row {
743 flags |= TNS_DPLS_ROW_HEADER_FAST_ROW | TNS_DPLS_ROW_HEADER_FAST_PIECE;
744 }
745 let num_segments = u8::try_from(state.num_segments)
746 .map_err(|_| ProtocolError::TtcDecode("direct path piece segment count overflow"))?;
747 let piece = DirectPathPiece {
748 flags,
749 num_segments,
750 data: std::mem::take(&mut self.data),
751 };
752 let new_length = self.total_piece_length + piece.data.len() as u64 + piece.header_length();
753 if new_length > TNS_DPLS_MAX_MESSAGE_SIZE {
754 return Err(ProtocolError::DirectPathLoadTooMuchData);
755 }
756 self.total_piece_length = new_length;
757 self.pieces.push(piece);
758 Ok(())
760 }
761}
762
763fn is_fast_dbtype(metadata: &ColumnMetadata) -> bool {
766 matches!(
767 metadata.ora_type_num,
768 ORA_TYPE_NUM_VARCHAR
769 | ORA_TYPE_NUM_NUMBER
770 | ORA_TYPE_NUM_BINARY_INTEGER
771 | ORA_TYPE_NUM_CHAR
772 | ORA_TYPE_NUM_DATE
773 | ORA_TYPE_NUM_RAW
774 | ORA_TYPE_NUM_BINARY_FLOAT
775 | ORA_TYPE_NUM_BINARY_DOUBLE
776 | ORA_TYPE_NUM_BOOLEAN
777 | ORA_TYPE_NUM_TIMESTAMP
778 | ORA_TYPE_NUM_TIMESTAMP_TZ
779 | ORA_TYPE_NUM_TIMESTAMP_LTZ
780 )
781}
782
783#[derive(Clone, Debug, Eq, PartialEq)]
785pub struct DirectPathStream {
786 pub(crate) pieces: Vec<DirectPathPiece>,
787 pub(crate) total_piece_length: u32,
788}
789
790pub fn encode_direct_path_rows(
796 column_metadata: &[ColumnMetadata],
797 rows: &[Vec<DirectPathColumnValue>],
798 first_row_num: u64,
799) -> Result<DirectPathStream> {
800 let mut buffer = DirectPathPieceBuffer::new();
801 for (row_index, row) in rows.iter().enumerate() {
802 if row.len() != column_metadata.len() {
803 return Err(ProtocolError::TtcDecode(
804 "direct path row width does not match column metadata",
805 ));
806 }
807 let row_num = first_row_num + row_index as u64;
808 buffer.start_row()?;
809 for (metadata, value) in column_metadata.iter().zip(row) {
810 buffer.add_column_value(metadata, value, row_num)?;
811 }
812 buffer.finish_row()?;
813 }
814 let (pieces, total_piece_length) = buffer.finish()?;
815 Ok(DirectPathStream {
816 pieces,
817 total_piece_length,
818 })
819}
820
821pub fn build_direct_path_load_stream_payload(
825 cursor_id: u16,
826 stream: &DirectPathStream,
827 seq_num: u8,
828) -> Result<Vec<u8>> {
829 build_direct_path_load_stream_payload_with_version(
830 cursor_id,
831 stream,
832 seq_num,
833 crate::thin::TNS_CCAP_FIELD_VERSION_23_1_EXT_1,
834 )
835}
836
837pub fn build_direct_path_load_stream_payload_with_version(
841 cursor_id: u16,
842 stream: &DirectPathStream,
843 seq_num: u8,
844 ttc_field_version: u8,
845) -> Result<Vec<u8>> {
846 let mut writer = TtcWriter::new();
847 writer.write_function_header(TNS_FUNC_DIRECT_PATH_LOAD_STREAM, seq_num, ttc_field_version);
848 writer.write_ub2(cursor_id);
849 writer.write_u8(1); writer.write_ub4(stream.total_piece_length);
851 writer.write_ub4(TNS_DP_STREAM_VERSION);
852 writer.write_u8(0); writer.write_ub4(0); writer.write_u8(1); writer.write_u8(1); for piece in &stream.pieces {
857 piece.write_to(&mut writer)?;
858 }
859 Ok(writer.into_bytes())
860}
861
862#[derive(Clone, Debug, Eq, PartialEq)]
871pub struct BatchLoadState {
872 chunk_lengths: Vec<u64>,
873 batch_size: u32,
874 chunk_index: usize,
875 offset: u64,
876 message_offset: u64,
877 num_rows: u32,
878}
879
880impl BatchLoadState {
881 pub fn new(chunk_lengths: Vec<u64>, batch_size: u32) -> Result<Self> {
882 if batch_size == 0 {
883 return Err(ProtocolError::TtcDecode(
884 "batch_size must be a positive integer",
885 ));
886 }
887 let mut state = Self {
888 chunk_lengths,
889 batch_size,
890 chunk_index: 0,
891 offset: 0,
892 message_offset: 0,
893 num_rows: 0,
894 };
895 state.advance_batch();
896 Ok(state)
897 }
898
899 pub fn for_rows(total_rows: u64, batch_size: u32) -> Result<Self> {
902 Self::new(vec![total_rows], batch_size)
903 }
904
905 pub fn num_rows(&self) -> u32 {
907 self.num_rows
908 }
909
910 pub fn offset(&self) -> u64 {
912 self.offset
913 }
914
915 pub fn message_offset(&self) -> u64 {
918 self.message_offset
919 }
920
921 pub fn chunk_index(&self) -> usize {
923 self.chunk_index
924 }
925
926 pub fn is_done(&self) -> bool {
927 self.num_rows == 0
928 }
929
930 pub fn next_batch(&mut self) {
932 self.offset += u64::from(self.num_rows);
933 self.advance_batch();
934 }
935
936 fn rows_in_current_chunk(&self) -> u64 {
937 self.chunk_lengths
938 .get(self.chunk_index)
939 .copied()
940 .unwrap_or(0)
941 }
942
943 fn calculate_num_rows_in_batch(&mut self) {
944 let remaining = self.rows_in_current_chunk().saturating_sub(self.offset);
945 self.num_rows = u32::try_from(remaining.min(u64::from(self.batch_size))).unwrap_or(0);
946 }
947
948 fn advance_batch(&mut self) {
949 self.message_offset = self.offset;
950 self.calculate_num_rows_in_batch();
951 if self.num_rows == 0 {
952 self.advance_chunk();
953 }
954 }
955
956 fn advance_chunk(&mut self) {
957 while self.chunk_index + 1 < self.chunk_lengths.len() {
958 self.offset = 0;
959 self.message_offset = 0;
960 self.chunk_index += 1;
961 self.calculate_num_rows_in_batch();
962 if self.num_rows > 0 {
963 break;
964 }
965 }
966 }
967}
968
969#[cfg(test)]
970mod tests {
971 use super::*;
972
973 #[test]
979 fn direct_path_oversized_column_count_fails_closed_not_oom() {
980 let payload = [TNS_MSG_TYPE_PARAMETER, 4, 0x25, 0x00, 0x00, 0x00];
982 let err = parse_direct_path_prepare_response(&payload, ClientCapabilities::default())
983 .expect_err("oversized direct-path column count must fail closed");
984 assert!(
985 matches!(
986 err,
987 ProtocolError::TtcDecode(_) | ProtocolError::ResourceLimit { .. }
988 ),
989 "got {err:?}"
990 );
991 }
992
993 #[test]
994 fn direct_path_prepare_respects_protocol_column_limit() {
995 let payload = [TNS_MSG_TYPE_PARAMETER, 1, 2];
996 let limits = ProtocolLimits {
997 max_columns: 1,
998 ..ProtocolLimits::DEFAULT
999 };
1000 let err = parse_direct_path_prepare_response_with_limits(
1001 &payload,
1002 ClientCapabilities::default(),
1003 limits,
1004 )
1005 .expect_err("column count above policy must fail");
1006 assert!(
1007 matches!(
1008 err,
1009 ProtocolError::ResourceLimit {
1010 limit: "columns",
1011 observed: 2,
1012 maximum: 1,
1013 }
1014 ),
1015 "got {err:?}"
1016 );
1017 }
1018
1019 fn column(name: &str, ora_type_num: u8, max_size: u32, nulls_allowed: bool) -> ColumnMetadata {
1020 ColumnMetadata {
1021 name: name.to_string(),
1022 ora_type_num,
1023 csfrm: if matches!(
1024 ora_type_num,
1025 ORA_TYPE_NUM_VARCHAR | ORA_TYPE_NUM_CHAR | ORA_TYPE_NUM_LONG
1026 ) {
1027 CS_FORM_IMPLICIT
1028 } else {
1029 0
1030 },
1031 precision: 0,
1032 scale: 0,
1033 buffer_size: max_size,
1034 max_size,
1035 nulls_allowed,
1036 is_json: false,
1037 is_oson: false,
1038 object_schema: None,
1039 object_type_name: None,
1040 is_array: false,
1041 vector_dimensions: None,
1042 vector_format: 0,
1043 vector_flags: 0,
1044 ..Default::default()
1045 }
1046 }
1047
1048 #[test]
1049 fn prepare_payload_matches_reference_layout() {
1050 let payload = build_direct_path_prepare_payload(
1051 "pythontest",
1052 "dpl_golden",
1053 &["id".to_string(), "name".to_string()],
1054 10,
1055 )
1056 .expect("payload should build");
1057 assert_eq!(&payload[..4], &[3, 128, 10, 0]);
1059 let mut expected = vec![
1060 1, 1, 1, 1, 4, 1, 1, 15, 1, 1, 1, 1, 1, 1, ];
1067 expected.extend_from_slice(&[0, 1, 10]);
1069 expected.extend_from_slice(&[10]);
1070 expected.extend_from_slice(b"pythontest");
1071 expected.extend_from_slice(&[1, 3]);
1072 expected.extend_from_slice(&[0, 1, 10]);
1074 expected.extend_from_slice(&[10]);
1075 expected.extend_from_slice(b"dpl_golden");
1076 expected.extend_from_slice(&[1, 1]);
1077 expected.extend_from_slice(&[0, 1, 2, 2]);
1079 expected.extend_from_slice(b"id");
1080 expected.extend_from_slice(&[1, 4]);
1081 expected.extend_from_slice(&[0, 1, 4, 4]);
1082 expected.extend_from_slice(b"name");
1083 expected.extend_from_slice(&[1, 4]);
1084 expected.extend_from_slice(&[2, 0x01, 0x90, 2, 0x01, 0x90]);
1086 expected.extend_from_slice(&[0; 9]);
1087 expected.extend_from_slice(&[2, 0xff, 0xff, 0, 0, 1, 1]);
1088 assert_eq!(&payload[4..], expected.as_slice());
1089 }
1090
1091 #[test]
1092 fn op_payload_matches_reference_layout() {
1093 let payload = build_direct_path_op_payload(1, TNS_DP_OP_FINISH, 12);
1094 assert_eq!(
1095 payload,
1096 vec![3, 130, 12, 0, 1, 2, 1, 1, 0, 0, 1, 1],
1097 "fn code, seq, token, ub4 op, ub2 cursor, ptr 0, ub4 0, ptr 1, ptr 1"
1098 );
1099 }
1100
1101 #[test]
1102 fn single_fast_row_produces_one_fast_piece() {
1103 let columns = vec![
1104 column("ID", ORA_TYPE_NUM_NUMBER, 0, false),
1105 column("NAME", ORA_TYPE_NUM_VARCHAR, 100, false),
1106 ];
1107 let rows = vec![vec![
1108 DirectPathColumnValue::Number("1".into()),
1109 DirectPathColumnValue::Bytes(b"alpha".to_vec()),
1110 ]];
1111 let stream = encode_direct_path_rows(&columns, &rows, 1).expect("stream should encode");
1112 assert_eq!(stream.pieces.len(), 1);
1113 let piece = &stream.pieces[0];
1114 assert_eq!(
1115 piece.flags(),
1116 TNS_DPLS_ROW_HEADER_FIRST
1117 | TNS_DPLS_ROW_HEADER_LAST
1118 | TNS_DPLS_ROW_HEADER_FAST_ROW
1119 | TNS_DPLS_ROW_HEADER_FAST_PIECE
1120 );
1121 assert_eq!(piece.num_segments(), 2);
1122 assert_eq!(
1124 piece.data(),
1125 &[2, 0xc1, 0x02, 5, b'a', b'l', b'p', b'h', b'a']
1126 );
1127 assert_eq!(stream.total_piece_length, piece.data().len() as u32 + 4);
1129 }
1130
1131 #[test]
1132 fn long_column_clears_fast_flag() {
1133 let columns = vec![column("WIDE", ORA_TYPE_NUM_LONG, 0, false)];
1134 let rows = vec![vec![DirectPathColumnValue::Bytes(vec![b'x'; 10])]];
1135 let stream = encode_direct_path_rows(&columns, &rows, 1).expect("stream should encode");
1136 assert_eq!(stream.pieces.len(), 1);
1137 assert_eq!(
1138 stream.pieces[0].flags(),
1139 TNS_DPLS_ROW_HEADER_FIRST | TNS_DPLS_ROW_HEADER_LAST
1140 );
1141 assert_eq!(stream.total_piece_length, 11 + 2);
1143 }
1144
1145 #[test]
1146 fn null_values_encode_as_null_indicator() {
1147 let columns = vec![column("SALARY", ORA_TYPE_NUM_NUMBER, 0, true)];
1148 let rows = vec![vec![DirectPathColumnValue::Null]];
1149 let stream = encode_direct_path_rows(&columns, &rows, 1).expect("stream should encode");
1150 assert_eq!(stream.pieces[0].data(), &[0xff]);
1151 assert_eq!(stream.pieces[0].num_segments(), 1);
1152 }
1153
1154 #[test]
1155 fn null_into_not_null_column_raises_dpy_8001() {
1156 let columns = vec![column("NAME", ORA_TYPE_NUM_VARCHAR, 100, false)];
1157 let rows = vec![vec![DirectPathColumnValue::Null]];
1158 let err = encode_direct_path_rows(&columns, &rows, 1).expect_err("nulls must be rejected");
1159 assert!(
1160 err.to_string().starts_with("DPY-8001:"),
1161 "unexpected error: {err}"
1162 );
1163 assert!(err.to_string().contains("\"NAME\""), "{err}");
1164 assert!(err.to_string().contains("row 1"), "{err}");
1165 }
1166
1167 #[test]
1168 fn oversized_value_raises_dpy_8000() {
1169 let columns = vec![column("NAME", ORA_TYPE_NUM_VARCHAR, 4, false)];
1170 let rows = vec![vec![DirectPathColumnValue::Bytes(b"toolong".to_vec())]];
1171 let err = encode_direct_path_rows(&columns, &rows, 3).expect_err("size must be enforced");
1172 assert!(
1173 err.to_string().starts_with("DPY-8000:"),
1174 "unexpected error: {err}"
1175 );
1176 assert!(err.to_string().contains("row 3"), "{err}");
1177 }
1178
1179 #[test]
1180 fn long_values_use_fe_chunked_segments() {
1181 let columns = vec![column("WIDE", ORA_TYPE_NUM_VARCHAR, 1000, false)];
1183 let value = vec![b'q'; 600];
1184 let rows = vec![vec![DirectPathColumnValue::Bytes(value.clone())]];
1185 let stream = encode_direct_path_rows(&columns, &rows, 1).expect("stream should encode");
1186 assert_eq!(stream.pieces.len(), 1);
1187 let piece = &stream.pieces[0];
1188 assert_eq!(piece.num_segments(), 1);
1189 let mut expected = vec![0xfe, 0x02, 0x58];
1190 expected.extend_from_slice(&value);
1191 assert_eq!(piece.data(), expected.as_slice());
1192 }
1193
1194 #[test]
1195 fn values_larger_than_piece_split_across_pieces_with_split_flags() {
1196 let columns = vec![column("WIDE", ORA_TYPE_NUM_LONG, 0, false)];
1197 let total = TNS_DPLS_MAX_PIECE_SIZE + 100;
1198 let rows = vec![vec![DirectPathColumnValue::Bytes(vec![b'z'; total])]];
1199 let stream = encode_direct_path_rows(&columns, &rows, 1).expect("stream should encode");
1200 assert_eq!(stream.pieces.len(), 2);
1201 let first = &stream.pieces[0];
1202 let second = &stream.pieces[1];
1203 assert_eq!(
1204 first.flags(),
1205 TNS_DPLS_ROW_HEADER_FIRST | TNS_DPLS_ROW_HEADER_SPLIT_WITH_NEXT
1206 );
1207 assert_eq!(
1208 second.flags(),
1209 TNS_DPLS_ROW_HEADER_SPLIT_WITH_PREV | TNS_DPLS_ROW_HEADER_LAST
1210 );
1211 assert_eq!(first.data().len(), TNS_DPLS_MAX_PIECE_SIZE);
1213 assert_eq!(first.data()[0], 0xfe);
1214 let first_chunk = usize::from(u16::from_be_bytes([first.data()[1], first.data()[2]]));
1215 assert_eq!(first_chunk, TNS_DPLS_MAX_PIECE_SIZE - 3);
1216 let second_chunk = usize::from(u16::from_be_bytes([second.data()[1], second.data()[2]]));
1217 assert_eq!(first_chunk + second_chunk, total);
1218 assert_eq!(
1219 stream.total_piece_length as usize,
1220 first.data().len() + second.data().len() + 2 + 2
1221 );
1222 }
1223
1224 #[test]
1225 fn segment_count_caps_at_255_per_piece() {
1226 let columns: Vec<ColumnMetadata> = (0..300)
1227 .map(|i| column(&format!("C{i}"), ORA_TYPE_NUM_NUMBER, 0, true))
1228 .collect();
1229 let row: Vec<DirectPathColumnValue> =
1230 (0..300).map(|_| DirectPathColumnValue::Null).collect();
1231 let stream = encode_direct_path_rows(&columns, &[row], 1).expect("stream should encode");
1232 assert_eq!(stream.pieces.len(), 2);
1233 assert_eq!(stream.pieces[0].num_segments(), 255);
1234 assert_eq!(stream.pieces[1].num_segments(), 45);
1235 assert_eq!(stream.pieces[0].flags(), TNS_DPLS_ROW_HEADER_FIRST);
1238 assert_eq!(stream.pieces[1].flags(), TNS_DPLS_ROW_HEADER_LAST);
1239 }
1240
1241 #[test]
1242 fn timestamp_with_zero_fraction_collapses_to_seven_bytes() {
1243 let columns = vec![column("TS", ORA_TYPE_NUM_TIMESTAMP, 0, true)];
1244 let rows = vec![vec![DirectPathColumnValue::DateTime {
1245 year: 2024,
1246 month: 1,
1247 day: 2,
1248 hour: 3,
1249 minute: 4,
1250 second: 5,
1251 nanosecond: 0,
1252 }]];
1253 let stream = encode_direct_path_rows(&columns, &rows, 1).expect("stream should encode");
1254 assert_eq!(
1255 stream.pieces[0].data(),
1256 &[7, 120, 124, 1, 2, 4, 5, 6],
1257 "7-byte date form expected when fractional seconds are zero"
1258 );
1259 }
1260
1261 #[test]
1262 fn boolean_values_encode_per_reference() {
1263 let columns = vec![column("FLAG", ORA_TYPE_NUM_BOOLEAN, 0, true)];
1264 let rows = vec![
1265 vec![DirectPathColumnValue::Boolean(true)],
1266 vec![DirectPathColumnValue::Boolean(false)],
1267 ];
1268 let stream = encode_direct_path_rows(&columns, &rows, 1).expect("stream should encode");
1269 assert_eq!(stream.pieces[0].data(), &[2, 1, 1]);
1270 assert_eq!(stream.pieces[1].data(), &[1, 0]);
1271 }
1272
1273 #[test]
1274 fn row_width_mismatch_is_rejected() {
1275 let columns = vec![
1276 column("A", ORA_TYPE_NUM_NUMBER, 0, true),
1277 column("B", ORA_TYPE_NUM_NUMBER, 0, true),
1278 ];
1279 let rows = vec![vec![DirectPathColumnValue::Null]];
1280 assert!(encode_direct_path_rows(&columns, &rows, 1).is_err());
1281 }
1282
1283 #[test]
1284 fn metadata_overrides_inline_lobs() {
1285 let mut clob = column("DOC", ORA_TYPE_NUM_CLOB, 0, true);
1286 clob.csfrm = CS_FORM_IMPLICIT;
1287 apply_direct_path_metadata_overrides(&mut clob, 873);
1288 assert_eq!(clob.ora_type_num, ORA_TYPE_NUM_LONG);
1289 assert_eq!(clob.csfrm, CS_FORM_NCHAR, "multi-byte charset uses NCHAR");
1290
1291 let mut clob = column("DOC", ORA_TYPE_NUM_CLOB, 0, true);
1292 clob.csfrm = CS_FORM_IMPLICIT;
1293 apply_direct_path_metadata_overrides(&mut clob, 178);
1294 assert_eq!(
1295 clob.csfrm, CS_FORM_IMPLICIT,
1296 "single-byte charset keeps form"
1297 );
1298
1299 let mut blob = column("BIN", ORA_TYPE_NUM_BLOB, 0, true);
1300 apply_direct_path_metadata_overrides(&mut blob, 873);
1301 assert_eq!(blob.ora_type_num, ORA_TYPE_NUM_LONG_RAW);
1302 assert_eq!(blob.csfrm, 0);
1303 }
1304
1305 #[test]
1306 fn batch_state_single_chunk_splits_by_batch_size() {
1307 let mut state = BatchLoadState::for_rows(5, 2).expect("state should build");
1308 assert_eq!(
1309 (state.num_rows(), state.offset(), state.message_offset()),
1310 (2, 0, 0)
1311 );
1312 state.next_batch();
1313 assert_eq!(
1314 (state.num_rows(), state.offset(), state.message_offset()),
1315 (2, 2, 2)
1316 );
1317 state.next_batch();
1318 assert_eq!(
1319 (state.num_rows(), state.offset(), state.message_offset()),
1320 (1, 4, 4)
1321 );
1322 state.next_batch();
1323 assert!(state.is_done());
1324 }
1325
1326 #[test]
1327 fn batch_state_never_spans_chunks() {
1328 let mut state = BatchLoadState::new(vec![3, 2], 2).expect("state should build");
1330 assert_eq!(
1331 (
1332 state.chunk_index(),
1333 state.num_rows(),
1334 state.message_offset()
1335 ),
1336 (0, 2, 0)
1337 );
1338 state.next_batch();
1339 assert_eq!(
1340 (
1341 state.chunk_index(),
1342 state.num_rows(),
1343 state.message_offset()
1344 ),
1345 (0, 1, 2)
1346 );
1347 state.next_batch();
1348 assert_eq!(
1349 (
1350 state.chunk_index(),
1351 state.num_rows(),
1352 state.message_offset()
1353 ),
1354 (1, 2, 0)
1355 );
1356 state.next_batch();
1357 assert!(state.is_done());
1358 }
1359
1360 #[test]
1361 fn batch_state_skips_empty_chunks() {
1362 let mut state = BatchLoadState::new(vec![0, 0, 3], 10).expect("state should build");
1363 assert_eq!((state.chunk_index(), state.num_rows()), (2, 3));
1364 state.next_batch();
1365 assert!(state.is_done());
1366 }
1367
1368 #[test]
1369 fn batch_state_rejects_zero_batch_size() {
1370 assert!(BatchLoadState::for_rows(5, 0).is_err());
1371 }
1372
1373 #[test]
1374 fn batch_state_empty_source_is_done_immediately() {
1375 let state = BatchLoadState::for_rows(0, 10).expect("state should build");
1376 assert!(state.is_done());
1377 }
1378
1379 #[test]
1380 fn load_stream_payload_header_matches_reference_layout() {
1381 let columns = vec![column("ID", ORA_TYPE_NUM_NUMBER, 0, false)];
1382 let rows = vec![vec![DirectPathColumnValue::Number("1".into())]];
1383 let stream = encode_direct_path_rows(&columns, &rows, 1).expect("stream should encode");
1384 let payload =
1385 build_direct_path_load_stream_payload(1, &stream, 11).expect("payload should build");
1386 let mut expected = vec![
1387 3, 129, 11, 0, 1, 1, 1, 1, 7, 2, 0x01, 0x90, 0, 0, 1, 1, 0x3c, 0, 7, 1, 2, 0xc1, 0x02, ];
1399 assert_eq!(payload, std::mem::take(&mut expected));
1400 }
1401}