Skip to main content

oracledb_protocol/thin/
fetch.rs

1#![forbid(unsafe_code)]
2
3use super::*;
4use crate::wire::ProtocolLimits;
5
6/// Validate `slice` as UTF-8 for the hot borrowed-text decode path, returning the
7/// borrowed `&str` on success or `()` on rejection (the caller falls back to the
8/// owned `TextRaw` carrier — semantics identical regardless of validator).
9///
10/// With the `simd-decode` feature this uses `simdutf8::basic::from_utf8`, whose
11/// accept/reject decision is byte-for-byte identical to `core::str::from_utf8`
12/// (it validates the exact same UTF-8 grammar — it only declines to compute the
13/// error *position*, which this path never uses). The crate stays
14/// `#![forbid(unsafe_code)]`-clean: `simdutf8`'s SIMD `unsafe` is encapsulated
15/// inside that dependency and we call only its safe API.
16#[inline]
17fn validate_utf8(slice: &[u8]) -> core::result::Result<&str, ()> {
18    #[cfg(feature = "simd-decode")]
19    {
20        simdutf8::basic::from_utf8(slice).map_err(|_| ())
21    }
22    #[cfg(not(feature = "simd-decode"))]
23    {
24        core::str::from_utf8(slice).map_err(|_| ())
25    }
26}
27
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub(crate) enum LobDecodeMode {
30    PlainLocator,
31    DefineMetadata,
32}
33
34pub fn build_fetch_payload(cursor_id: u32, arraysize: u32, ttc_field_version: u8) -> Vec<u8> {
35    build_fetch_payload_with_seq(cursor_id, arraysize, 1, ttc_field_version)
36}
37
38pub fn build_fetch_payload_with_seq(
39    cursor_id: u32,
40    arraysize: u32,
41    seq_num: u8,
42    ttc_field_version: u8,
43) -> Vec<u8> {
44    // Fixed tiny payload (function code + ub8 + two ub4 ≈ <=20 bytes). Prealloc
45    // so the small pushes do not grow the Vec through doublings; built every
46    // fetch page, so this matters on multi-page fetches. Bytes unchanged.
47    let mut writer = TtcWriter::with_capacity(32);
48    writer.write_function_header(TNS_FUNC_FETCH, seq_num, ttc_field_version);
49    writer.write_ub4(cursor_id);
50    writer.write_ub4(arraysize);
51    writer.into_bytes()
52}
53
54pub fn build_define_fetch_payload_with_seq(
55    cursor_id: u32,
56    arraysize: u32,
57    seq_num: u8,
58    define_columns: &[ColumnMetadata],
59    ttc_field_version: u8,
60) -> Result<Vec<u8>> {
61    let define_count =
62        u32::try_from(define_columns.len()).map_err(|_| ProtocolError::InvalidPacketLength {
63            length: define_columns.len(),
64            minimum: 0,
65        })?;
66    let mut writer = TtcWriter::new();
67    writer.write_function_header(TNS_FUNC_EXECUTE, seq_num, ttc_field_version);
68    writer.write_ub4(TNS_EXEC_OPTION_DEFINE | TNS_EXEC_OPTION_NOT_PLSQL);
69    writer.write_ub4(cursor_id);
70    writer.write_u8(0);
71    writer.write_ub4(0);
72    writer.write_u8(1);
73    writer.write_ub4(13);
74    writer.write_u8(0);
75    writer.write_u8(0);
76    writer.write_ub4(0);
77    writer.write_ub4(arraysize);
78    writer.write_ub4(TNS_MAX_LONG_LENGTH);
79    writer.write_u8(0);
80    writer.write_ub4(0);
81    writer.write_u8(0);
82    writer.write_u8(0);
83    writer.write_u8(0);
84    writer.write_u8(0);
85    writer.write_u8(0);
86    writer.write_u8(1);
87    writer.write_ub4(define_count);
88    writer.write_ub4(0);
89    writer.write_u8(0);
90    writer.write_u8(1);
91    writer.write_u8(0);
92    writer.write_ub4(0);
93    writer.write_u8(0);
94    writer.write_ub4(0);
95    writer.write_ub4(0);
96    writer.write_u8(0);
97    writer.write_ub4(0);
98    writer.write_u8(0);
99    writer.write_u8(0);
100    writer.write_ub4(0);
101    writer.write_ub4(0);
102    writer.write_ub4(0);
103    writer.write_ub4(0);
104    writer.write_ub4(0);
105    writer.write_ub4(0);
106    writer.write_ub4(0);
107    writer.write_ub4(arraysize);
108    writer.write_ub4(0);
109    writer.write_ub4(0);
110    writer.write_ub4(0);
111    writer.write_ub4(0);
112    writer.write_ub4(0);
113    writer.write_ub4(1);
114    writer.write_ub4(0);
115    writer.write_ub4(0);
116    writer.write_ub4(0);
117    writer.write_ub4(0);
118    writer.write_ub4(0);
119    for metadata in define_columns {
120        write_define_column_metadata(&mut writer, metadata);
121    }
122    Ok(writer.into_bytes())
123}
124
125pub(crate) fn write_define_column_metadata(writer: &mut TtcWriter, metadata: &ColumnMetadata) {
126    // reference base.pyx: VECTOR (and JSON) columns advertise a LOB-prefetch
127    // buffer so the server streams the image inline rather than returning a
128    // bare temp-LOB locator
129    let (mut buffer_size, cont_flags, lob_prefetch_length) = match metadata.ora_type_num {
130        ORA_TYPE_NUM_CLOB | ORA_TYPE_NUM_BLOB => (metadata.buffer_size, TNS_LOB_PREFETCH_FLAG, 0),
131        ORA_TYPE_NUM_VECTOR => (
132            TNS_VECTOR_MAX_LENGTH,
133            TNS_LOB_PREFETCH_FLAG,
134            TNS_VECTOR_MAX_LENGTH,
135        ),
136        ORA_TYPE_NUM_JSON => (
137            TNS_JSON_MAX_LENGTH,
138            TNS_LOB_PREFETCH_FLAG,
139            TNS_JSON_MAX_LENGTH,
140        ),
141        _ => (metadata.buffer_size, 0, 0),
142    };
143    buffer_size = buffer_size.max(1);
144    writer.write_u8(metadata.ora_type_num);
145    writer.write_u8(TNS_BIND_USE_INDICATORS);
146    writer.write_u8(0);
147    writer.write_u8(0);
148    writer.write_ub4(buffer_size);
149    writer.write_ub4(0);
150    writer.write_ub8(cont_flags);
151    writer.write_ub4(0);
152    writer.write_ub2(0);
153    if metadata.csfrm != 0 {
154        writer.write_ub2(TNS_CHARSET_UTF8);
155    } else {
156        writer.write_ub2(0);
157    }
158    writer.write_u8(metadata.csfrm);
159    writer.write_ub4(lob_prefetch_length);
160    writer.write_ub4(0);
161}
162
163pub fn parse_query_response(
164    payload: &[u8],
165    capabilities: ClientCapabilities,
166) -> Result<QueryResult> {
167    parse_query_response_with_previous(payload, capabilities, None)
168}
169
170pub fn parse_query_response_with_limits(
171    payload: &[u8],
172    capabilities: ClientCapabilities,
173    limits: ProtocolLimits,
174) -> Result<QueryResult> {
175    parse_query_response_with_context_binds_options_and_limits(
176        payload,
177        capabilities,
178        &[],
179        None,
180        &[],
181        &[],
182        false,
183        ExecuteOptions::default(),
184        limits,
185    )
186}
187
188pub fn parse_query_response_with_binds(
189    payload: &[u8],
190    capabilities: ClientCapabilities,
191    binds: &[BindValue],
192) -> Result<QueryResult> {
193    parse_query_response_with_binds_and_options(
194        payload,
195        capabilities,
196        binds,
197        ExecuteOptions::default(),
198    )
199}
200
201pub fn parse_query_response_with_binds_and_options(
202    payload: &[u8],
203    capabilities: ClientCapabilities,
204    binds: &[BindValue],
205    exec_options: ExecuteOptions,
206) -> Result<QueryResult> {
207    parse_query_response_with_binds_options_and_columns(
208        payload,
209        capabilities,
210        binds,
211        exec_options,
212        &[],
213    )
214}
215
216/// `known_columns` carries the fetch metadata of a re-executed statement
217/// whose response does not repeat the describe information (reference keeps
218/// the statement's fetch vars across executions).
219pub fn parse_query_response_with_binds_options_and_columns(
220    payload: &[u8],
221    capabilities: ClientCapabilities,
222    binds: &[BindValue],
223    exec_options: ExecuteOptions,
224    known_columns: &[ColumnMetadata],
225) -> Result<QueryResult> {
226    let bind_columns = binds.iter().map(bind_column_metadata).collect::<Vec<_>>();
227    let output_bind_indexes = binds
228        .iter()
229        .enumerate()
230        .filter_map(|(index, value)| value.is_return_output().then_some(index))
231        .collect::<Vec<_>>();
232    parse_query_response_with_context_binds_and_options(
233        payload,
234        capabilities,
235        known_columns,
236        None,
237        &bind_columns,
238        &output_bind_indexes,
239        false,
240        exec_options,
241    )
242}
243
244pub fn parse_query_response_with_binds_options_columns_and_limits(
245    payload: &[u8],
246    capabilities: ClientCapabilities,
247    binds: &[BindValue],
248    exec_options: ExecuteOptions,
249    known_columns: &[ColumnMetadata],
250    limits: ProtocolLimits,
251) -> Result<QueryResult> {
252    limits.check_binds(binds.len())?;
253    let bind_columns = binds.iter().map(bind_column_metadata).collect::<Vec<_>>();
254    let output_bind_indexes = binds
255        .iter()
256        .enumerate()
257        .filter_map(|(index, value)| value.is_return_output().then_some(index))
258        .collect::<Vec<_>>();
259    parse_query_response_with_context_binds_options_and_limits(
260        payload,
261        capabilities,
262        known_columns,
263        None,
264        &bind_columns,
265        &output_bind_indexes,
266        false,
267        exec_options,
268        limits,
269    )
270}
271
272pub fn parse_query_response_with_previous(
273    payload: &[u8],
274    capabilities: ClientCapabilities,
275    previous_row: Option<&[Option<QueryValue>]>,
276) -> Result<QueryResult> {
277    parse_query_response_with_context(payload, capabilities, &[], previous_row)
278}
279
280pub fn parse_query_response_with_context(
281    payload: &[u8],
282    capabilities: ClientCapabilities,
283    previous_columns: &[ColumnMetadata],
284    previous_row: Option<&[Option<QueryValue>]>,
285) -> Result<QueryResult> {
286    parse_query_response_with_context_and_binds(
287        payload,
288        capabilities,
289        previous_columns,
290        previous_row,
291        &[],
292        &[],
293        false,
294        ProtocolLimits::DEFAULT,
295    )
296}
297
298pub fn parse_fetch_response_with_context(
299    payload: &[u8],
300    capabilities: ClientCapabilities,
301    previous_columns: &[ColumnMetadata],
302    previous_row: Option<&[Option<QueryValue>]>,
303) -> Result<QueryResult> {
304    parse_fetch_response_with_context_and_limits(
305        payload,
306        capabilities,
307        previous_columns,
308        previous_row,
309        ProtocolLimits::DEFAULT,
310    )
311}
312
313pub fn parse_fetch_response_with_context_and_limits(
314    payload: &[u8],
315    capabilities: ClientCapabilities,
316    previous_columns: &[ColumnMetadata],
317    previous_row: Option<&[Option<QueryValue>]>,
318    limits: ProtocolLimits,
319) -> Result<QueryResult> {
320    parse_query_response_with_context_binds_options_lob_mode_and_limits(
321        payload,
322        capabilities,
323        previous_columns,
324        previous_row,
325        &[],
326        &[],
327        true,
328        ExecuteOptions::default(),
329        LobDecodeMode::PlainLocator,
330        limits,
331    )
332}
333
334pub fn parse_define_fetch_response_with_context_and_limits(
335    payload: &[u8],
336    capabilities: ClientCapabilities,
337    previous_columns: &[ColumnMetadata],
338    previous_row: Option<&[Option<QueryValue>]>,
339    limits: ProtocolLimits,
340) -> Result<QueryResult> {
341    parse_query_response_with_context_binds_options_lob_mode_and_limits(
342        payload,
343        capabilities,
344        previous_columns,
345        previous_row,
346        &[],
347        &[],
348        true,
349        ExecuteOptions::default(),
350        LobDecodeMode::DefineMetadata,
351        limits,
352    )
353}
354
355#[allow(clippy::too_many_arguments)] // mirrors the reference message attribute set
356pub(crate) fn parse_query_response_with_context_and_binds(
357    payload: &[u8],
358    capabilities: ClientCapabilities,
359    previous_columns: &[ColumnMetadata],
360    previous_row: Option<&[Option<QueryValue>]>,
361    bind_columns: &[ColumnMetadata],
362    output_bind_indexes: &[usize],
363    fetch_long_status: bool,
364    limits: ProtocolLimits,
365) -> Result<QueryResult> {
366    parse_query_response_with_context_binds_options_and_limits(
367        payload,
368        capabilities,
369        previous_columns,
370        previous_row,
371        bind_columns,
372        output_bind_indexes,
373        fetch_long_status,
374        ExecuteOptions::default(),
375        limits,
376    )
377}
378
379#[allow(clippy::too_many_arguments)] // mirrors the reference message attribute set
380pub(crate) fn parse_query_response_with_context_binds_and_options(
381    payload: &[u8],
382    capabilities: ClientCapabilities,
383    previous_columns: &[ColumnMetadata],
384    previous_row: Option<&[Option<QueryValue>]>,
385    bind_columns: &[ColumnMetadata],
386    output_bind_indexes: &[usize],
387    fetch_long_status: bool,
388    exec_options: ExecuteOptions,
389) -> Result<QueryResult> {
390    parse_query_response_with_context_binds_options_and_limits(
391        payload,
392        capabilities,
393        previous_columns,
394        previous_row,
395        bind_columns,
396        output_bind_indexes,
397        fetch_long_status,
398        exec_options,
399        ProtocolLimits::DEFAULT,
400    )
401}
402
403#[allow(clippy::too_many_arguments)] // mirrors the reference message attribute set
404pub(crate) fn parse_query_response_with_context_binds_options_and_limits(
405    payload: &[u8],
406    capabilities: ClientCapabilities,
407    previous_columns: &[ColumnMetadata],
408    previous_row: Option<&[Option<QueryValue>]>,
409    bind_columns: &[ColumnMetadata],
410    output_bind_indexes: &[usize],
411    fetch_long_status: bool,
412    exec_options: ExecuteOptions,
413    limits: ProtocolLimits,
414) -> Result<QueryResult> {
415    parse_query_response_with_context_binds_options_lob_mode_and_limits(
416        payload,
417        capabilities,
418        previous_columns,
419        previous_row,
420        bind_columns,
421        output_bind_indexes,
422        fetch_long_status,
423        exec_options,
424        LobDecodeMode::DefineMetadata,
425        limits,
426    )
427}
428
429#[allow(clippy::too_many_arguments)] // mirrors the reference message attribute set
430fn parse_query_response_with_context_binds_options_lob_mode_and_limits(
431    payload: &[u8],
432    capabilities: ClientCapabilities,
433    previous_columns: &[ColumnMetadata],
434    previous_row: Option<&[Option<QueryValue>]>,
435    bind_columns: &[ColumnMetadata],
436    output_bind_indexes: &[usize],
437    fetch_long_status: bool,
438    exec_options: ExecuteOptions,
439    lob_decode_mode: LobDecodeMode,
440    limits: ProtocolLimits,
441) -> Result<QueryResult> {
442    let mut reader = TtcReader::with_limits(payload, limits)?;
443    let mut result = QueryResult {
444        columns: previous_columns.to_vec(),
445        more_rows: true,
446        ..QueryResult::default()
447    };
448    // A re-executed cursor whose column type changed to CLOB/BLOB but was
449    // previously fetched as CHAR/VARCHAR/RAW streams the value in LONG/LONG RAW
450    // form (see `adjust_refetch_metadata`), which carries the LONG status
451    // trailer (null indicator + return code) after each value — even on the
452    // execute path that otherwise passes `fetch_long_status = false`. Promote
453    // the flag when such an adjustment fires so `parse_row_data` consumes that
454    // trailer instead of mis-framing the next message (bead rust-oracledb-f0ad).
455    let mut fetch_long_status = fetch_long_status;
456    let mut bit_vector: Option<Vec<u8>> = None;
457    let mut out_bind_indexes: Vec<usize> = Vec::new();
458    while reader.remaining() > 0 {
459        let message_type = reader.read_u8()?;
460        match message_type {
461            0 => {}
462            TNS_MSG_TYPE_DESCRIBE_INFO => {
463                let _describe_name = reader.read_bytes()?;
464                let previous = std::mem::take(&mut result.columns);
465                parse_describe_info(&mut reader, capabilities, &mut result)?;
466                // re-executing an open cursor whose underlying types changed:
467                // the server re-describes mid-response but still streams the
468                // row data in the adjusted (LONG/LONG RAW) form expected by
469                // the previous fetch metadata (reference `_adjust_metadata`,
470                // impl/thin/messages/base.pyx:820-845, applied during
471                // `_process_describe_info`).
472                for (index, column) in result.columns.iter_mut().enumerate() {
473                    if let Some(prev) = previous.get(index) {
474                        if adjust_refetch_metadata(prev, column) {
475                            // the adjusted column (now LONG / LONG RAW) is
476                            // streamed with the LONG status trailer.
477                            fetch_long_status = true;
478                        }
479                    }
480                }
481            }
482            TNS_MSG_TYPE_ROW_HEADER => {
483                bit_vector = parse_row_header(&mut reader)?;
484            }
485            TNS_MSG_TYPE_ROW_DATA => {
486                if result.columns.is_empty() && !out_bind_indexes.is_empty() {
487                    parse_out_bind_row_data(
488                        &mut reader,
489                        &mut result,
490                        bind_columns,
491                        &out_bind_indexes,
492                    )?;
493                } else if result.columns.is_empty() && !output_bind_indexes.is_empty() {
494                    parse_returning_row_data(
495                        &mut reader,
496                        &mut result,
497                        bind_columns,
498                        output_bind_indexes,
499                    )?;
500                } else {
501                    parse_row_data(
502                        &mut reader,
503                        &mut result,
504                        bit_vector.as_deref(),
505                        previous_row,
506                        fetch_long_status,
507                        lob_decode_mode,
508                    )?;
509                }
510                bit_vector = None;
511            }
512            TNS_MSG_TYPE_BIT_VECTOR => {
513                bit_vector = Some(parse_bit_vector(&mut reader, result.columns.len())?);
514            }
515            TNS_MSG_TYPE_PARAMETER => {
516                let params =
517                    parse_query_return_parameters(&mut reader, exec_options.arraydmlrowcounts)?;
518                if exec_options.arraydmlrowcounts {
519                    result.array_dml_row_counts = Some(params.row_counts.unwrap_or_default());
520                }
521                if params.query_id.is_some() {
522                    result.query_id = params.query_id;
523                }
524            }
525            TNS_MSG_TYPE_STATUS => {
526                let call_status = reader.read_ub4()?;
527                let _seq = reader.read_ub2()?;
528                result.txn_in_progress = Some(call_status & TNS_EOCS_FLAGS_TXN_IN_PROGRESS != 0);
529            }
530            TNS_MSG_TYPE_IO_VECTOR => {
531                out_bind_indexes = parse_io_vector(&mut reader, bind_columns.len())?
532                    .into_iter()
533                    .filter(|index| !output_bind_indexes.contains(index))
534                    .collect();
535            }
536            TNS_MSG_TYPE_FLUSH_OUT_BINDS => break,
537            TNS_MSG_TYPE_SERVER_SIDE_PIGGYBACK => {
538                if let Some(update) = skip_server_side_piggyback(&mut reader)? {
539                    result.sessionless_txn_state = Some(update);
540                }
541            }
542            TNS_MSG_TYPE_IMPLICIT_RESULTSET => {
543                // reference messages/base.pyx `_process_implicit_result`
544                let num_results = reader.read_ub4()?;
545                // `num_results` is read straight off the wire (a ub4, up to
546                // ~4e9); each resultset consumes at least one byte, so cap the
547                // reservation by the bytes left in the payload (BoundedReader).
548                // Without this a hostile server forces a multi-gigabyte
549                // allocation (OOM) before the truncated read in the loop body
550                // fails closed.
551                let mut resultsets: Vec<QueryValue> = reader.with_capacity_limited(
552                    num_results as usize,
553                    1,
554                    ProtocolLimits::check_length_prefixed_elements,
555                )?;
556                for _ in 0..num_results {
557                    let num_bytes = reader.read_u8()?;
558                    reader.skip(usize::from(num_bytes))?;
559                    let mut child = QueryResult::default();
560                    parse_describe_info(&mut reader, capabilities, &mut child)?;
561                    let child_cursor_id = u32::from(reader.read_ub2()?);
562                    resultsets.push(QueryValue::Cursor(Box::new(CursorValue {
563                        columns: child.columns,
564                        cursor_id: child_cursor_id,
565                    })));
566                }
567                result.implicit_resultsets = Some(resultsets);
568            }
569            TNS_MSG_TYPE_END_OF_RESPONSE => break,
570            // pipeline responses open with the token of the operation they
571            // answer (messages/base.pyx:288-293); callers compare it against
572            // the expected token (mismatch -> DPY-2052 at the driver layer)
573            TNS_MSG_TYPE_TOKEN => {
574                result.token_num = Some(reader.read_ub8()?);
575            }
576            TNS_MSG_TYPE_ERROR => {
577                let info = parse_server_error_info(&mut reader, capabilities.ttc_field_version)?;
578                // The end-of-call ERROR message (number 0 on success) carries
579                // the end-of-call status; sample the transaction-in-progress bit
580                // (reference protocol.pyx `_process_call_status`).
581                result.txn_in_progress =
582                    Some(info.call_status & TNS_EOCS_FLAGS_TXN_IN_PROGRESS != 0);
583                if info.cursor_id != 0 {
584                    result.cursor_id = u32::from(info.cursor_id);
585                }
586                result.row_count = info.row_count;
587                result.compilation_error_warning |= info.compilation_error_warning;
588                result.last_rowid = info.rowid.clone();
589                if info.number == TNS_ERR_NO_DATA_FOUND && !result.columns.is_empty() {
590                    result.more_rows = false;
591                } else if info.number == TNS_ERR_ARRAY_DML_ERRORS {
592                    // executemany(batcherrors=True): errors are reported via
593                    // the batch error arrays instead of raising ORA-24381
594                    // (reference messages/base.pyx `_process_error_info`).
595                    result.batch_errors = info.batch_errors;
596                } else if info.number != 0 {
597                    let mut details = info.into_details();
598                    details.array_dml_row_counts = result.array_dml_row_counts.take();
599                    return Err(ProtocolError::ServerErrorInfo(Box::new(details)));
600                }
601            }
602            _ => {
603                let position = reader.position().saturating_sub(1);
604                if let Some(message) =
605                    find_embedded_server_error(payload, capabilities.ttc_field_version, position)
606                {
607                    return Err(ProtocolError::ServerError(message));
608                }
609                return Err(ProtocolError::UnknownMessageType {
610                    message_type,
611                    position,
612                });
613            }
614        }
615    }
616    Ok(result)
617}
618
619pub(crate) fn bind_column_metadata(value: &BindValue) -> ColumnMetadata {
620    let (ora_type_num, csfrm, buffer_size) = bind_metadata(value);
621    let object_schema = match value {
622        BindValue::ObjectOutput { schema, .. } | BindValue::ObjectInput { schema, .. } => {
623            Some(schema.clone())
624        }
625        _ => None,
626    };
627    let object_type_name = match value {
628        BindValue::ObjectOutput { type_name, .. } | BindValue::ObjectInput { type_name, .. } => {
629            Some(type_name.clone())
630        }
631        _ => None,
632    };
633    ColumnMetadata {
634        name: String::new(),
635        ora_type_num,
636        csfrm,
637        precision: 0,
638        scale: 0,
639        buffer_size,
640        max_size: buffer_size,
641        nulls_allowed: true,
642        is_json: false,
643        is_oson: false,
644        object_schema,
645        object_type_name,
646        is_array: matches!(value, BindValue::Array { .. }),
647        vector_dimensions: None,
648        vector_format: 0,
649        vector_flags: 0,
650        domain_schema: None,
651        domain_name: None,
652        annotations: None,
653    }
654}
655
656pub(crate) fn parse_io_vector(reader: &mut TtcReader<'_>, bind_count: usize) -> Result<Vec<usize>> {
657    let _flags = reader.read_u8()?;
658    let temp16 = reader.read_ub2()?;
659    let temp32 = reader.read_ub4()?;
660    let num_binds = usize::try_from(temp32)
661        .map_err(|_| ProtocolError::InvalidPacketLength {
662            length: usize::MAX,
663            minimum: 0,
664        })?
665        .checked_mul(256)
666        .and_then(|value| value.checked_add(usize::from(temp16)))
667        .ok_or(ProtocolError::InvalidPacketLength {
668            length: usize::MAX,
669            minimum: 0,
670        })?;
671    let _num_iters_this_time = reader.read_ub4()?;
672    let _uac_buffer_length = reader.read_ub2()?;
673    let fast_fetch_len = reader.read_ub2()?;
674    if fast_fetch_len > 0 {
675        reader.skip(usize::from(fast_fetch_len))?;
676    }
677    let rowid_len = reader.read_ub2()?;
678    if rowid_len > 0 {
679        reader.skip(usize::from(rowid_len))?;
680    }
681    let mut out_indexes = Vec::new();
682    reader.limits().check_binds(num_binds)?;
683    for index in 0..num_binds {
684        let direction = reader.read_u8()?;
685        if index < bind_count && direction != TNS_BIND_DIR_INPUT {
686            out_indexes.push(index);
687        }
688    }
689    Ok(out_indexes)
690}
691
692pub(crate) fn find_embedded_server_error(
693    payload: &[u8],
694    ttc_field_version: u8,
695    position: usize,
696) -> Option<String> {
697    let start = position.saturating_sub(64);
698    for candidate in start..=position {
699        if !matches!(payload.get(candidate).copied(), Some(TNS_MSG_TYPE_ERROR)) {
700            continue;
701        }
702        let mut reader = TtcReader::new(payload.get(candidate + 1..)?);
703        let info = parse_server_error_info(&mut reader, ttc_field_version).ok()?;
704        if info.number != 0 && info.message.starts_with("ORA-") {
705            return Some(info.message);
706        }
707    }
708    None
709}
710
711pub(crate) fn parse_describe_info(
712    reader: &mut TtcReader<'_>,
713    capabilities: ClientCapabilities,
714    result: &mut QueryResult,
715) -> Result<()> {
716    let _max_row_size = reader.read_ub4()?;
717    let num_columns = reader.read_ub4()?;
718    reader.limits().check_columns(num_columns as usize)?;
719    result.columns.clear();
720    if num_columns > 0 {
721        reader.skip(1)?;
722    }
723    for _ in 0..num_columns {
724        result
725            .columns
726            .push(parse_column_metadata(reader, capabilities)?);
727    }
728    let _current_date = reader.read_bytes_with_length()?;
729    let _dcbflag = reader.read_ub4()?;
730    let _dcbmdbz = reader.read_ub4()?;
731    let _dcbmnpr = reader.read_ub4()?;
732    let _dcbmxpr = reader.read_ub4()?;
733    let _dcbqcky = reader.read_bytes_with_length()?;
734    Ok(())
735}
736
737pub(crate) fn parse_column_metadata(
738    reader: &mut TtcReader<'_>,
739    capabilities: ClientCapabilities,
740) -> Result<ColumnMetadata> {
741    let ora_type_num = reader.read_u8()?;
742    reader.skip(1)?;
743    let precision = reader.read_i8()?;
744    let scale = reader.read_i8()?;
745    let buffer_size = reader.read_ub4()?;
746    let _max_array_elements = reader.read_ub4()?;
747    let _cont_flags = reader.read_ub8()?;
748    let _oid = reader.read_bytes_with_length()?;
749    let _version = reader.read_ub2()?;
750    let _charset_id = reader.read_ub2()?;
751    let csfrm = reader.read_u8()?;
752    let mut max_size = reader.read_ub4()?;
753    if ora_type_num == ORA_TYPE_NUM_RAW {
754        max_size = buffer_size;
755    }
756    if version_gates::carries_oaccolid(capabilities.ttc_field_version) {
757        let _oaccolid = reader.read_ub4()?;
758    }
759    let nulls_allowed = reader.read_u8()? != 0;
760    reader.skip(1)?;
761    let name = reader.read_string_with_length()?.unwrap_or_default();
762    let object_schema = reader.read_string_with_length()?;
763    let object_type_name = reader.read_string_with_length()?;
764    let _column_position = reader.read_ub2()?;
765    let uds_flags = reader.read_ub4()?;
766    let mut domain_schema = None;
767    let mut domain_name = None;
768    let mut annotations: Option<Vec<(String, String)>> = None;
769    if version_gates::reads_column_domain(capabilities.ttc_field_version) {
770        domain_schema = reader.read_string_with_length()?;
771        domain_name = reader.read_string_with_length()?;
772    }
773    if version_gates::reads_column_annotations(capabilities.ttc_field_version) {
774        let num_annotations = reader.read_ub4()?;
775        if num_annotations > 0 {
776            reader.skip(1)?;
777            let num_annotations = reader.read_ub4()?;
778            reader.skip(1)?;
779            // Bound by remaining bytes (BoundedReader): each annotation reads
780            // at least a length-prefixed key/value, so a ub4 count larger than
781            // the payload is a lie that must not pre-allocate gigabytes.
782            let mut collected: Vec<(String, String)> = reader.with_capacity_limited(
783                num_annotations as usize,
784                1,
785                ProtocolLimits::check_object_elements,
786            )?;
787            for _ in 0..num_annotations {
788                let key = reader.read_string_with_length()?.unwrap_or_default();
789                // A null annotation value is normalized to "" by the reference
790                // driver (python-oracledb base.pyx _process_metadata).
791                let value = reader.read_string_with_length()?.unwrap_or_default();
792                let _flags = reader.read_ub4()?;
793                collected.push((key, value));
794            }
795            let _flags = reader.read_ub4()?;
796            annotations = Some(collected);
797        }
798    }
799    let mut vector_dimensions = None;
800    let mut vector_format = 0u8;
801    let mut vector_flags = 0u8;
802    if version_gates::reads_column_vector_metadata(capabilities.ttc_field_version) {
803        // reference metadata.pyx: ub4 dimensions, ub1 format, ub1 flags
804        let dims = reader.read_ub4()?;
805        reader.limits().check_vector_dimensions(dims as usize)?;
806        vector_format = reader.read_u8()?;
807        vector_flags = reader.read_u8()?;
808        if ora_type_num == ORA_TYPE_NUM_VECTOR {
809            vector_dimensions = Some(dims);
810        }
811    }
812
813    Ok(ColumnMetadata {
814        name,
815        ora_type_num,
816        csfrm,
817        precision,
818        scale,
819        buffer_size,
820        max_size,
821        nulls_allowed,
822        is_json: uds_flags & TNS_UDS_FLAGS_IS_JSON != 0,
823        is_oson: uds_flags & TNS_UDS_FLAGS_IS_OSON != 0,
824        object_schema,
825        object_type_name,
826        is_array: false,
827        vector_dimensions,
828        vector_format,
829        vector_flags,
830        domain_schema,
831        domain_name,
832        annotations,
833    })
834}
835
836pub(crate) fn parse_row_header(reader: &mut TtcReader<'_>) -> Result<Option<Vec<u8>>> {
837    reader.skip(1)?;
838    let _num_requests = reader.read_ub2()?;
839    let _iteration_number = reader.read_ub4()?;
840    let _num_iters = reader.read_ub4()?;
841    let _buffer_length = reader.read_ub2()?;
842    let num_bytes = reader.read_ub4()?;
843    let bit_vector = if num_bytes > 0 {
844        reader.skip(1)?;
845        Some(reader.read_raw(num_bytes as usize)?.to_vec())
846    } else {
847        None
848    };
849    let _rxhrid = reader.read_bytes_with_length()?;
850    Ok(bit_vector)
851}
852
853pub(crate) fn parse_bit_vector(reader: &mut TtcReader<'_>, num_columns: usize) -> Result<Vec<u8>> {
854    let _num_columns_sent = reader.read_ub2()?;
855    let num_bytes = num_columns.div_ceil(8);
856    Ok(reader.read_raw(num_bytes)?.to_vec())
857}
858
859pub(crate) fn parse_row_data(
860    reader: &mut TtcReader<'_>,
861    result: &mut QueryResult,
862    bit_vector: Option<&[u8]>,
863    previous_row: Option<&[Option<QueryValue>]>,
864    fetch_long_status: bool,
865    lob_decode_mode: LobDecodeMode,
866) -> Result<()> {
867    let mut row = Vec::with_capacity(result.columns.len());
868    for (index, metadata) in result.columns.iter().enumerate() {
869        if is_duplicate_column(bit_vector, index) {
870            let previous = result
871                .rows
872                .last()
873                .map(Vec::as_slice)
874                .or(previous_row)
875                .and_then(|last| last.get(index))
876                .cloned()
877                .ok_or(ProtocolError::TtcDecode(
878                    "duplicate row data without previous row",
879                ))?;
880            row.push(previous);
881            continue;
882        }
883        row.push(parse_column_value_with_lob_mode(
884            reader,
885            metadata,
886            lob_decode_mode,
887        )?);
888        if fetch_long_status
889            && matches!(
890                metadata.ora_type_num,
891                ORA_TYPE_NUM_LONG | ORA_TYPE_NUM_LONG_RAW
892            )
893        {
894            let _null_indicator = reader.read_sb4()?;
895            let _return_code = reader.read_ub4()?;
896        }
897    }
898    result.rows.push(row);
899    Ok(())
900}
901
902pub(crate) fn parse_out_bind_row_data(
903    reader: &mut TtcReader<'_>,
904    result: &mut QueryResult,
905    bind_columns: &[ColumnMetadata],
906    out_bind_indexes: &[usize],
907) -> Result<()> {
908    for index in out_bind_indexes {
909        let metadata = bind_columns.get(*index).ok_or(ProtocolError::TtcDecode(
910            "out bind index without bind metadata",
911        ))?;
912        if metadata.is_array {
913            let num_elements = usize::try_from(reader.read_ub4()?).map_err(|_| {
914                ProtocolError::InvalidPacketLength {
915                    length: usize::MAX,
916                    minimum: 0,
917                }
918            })?;
919            reader.limits().check_batch_rows(num_elements)?;
920            // Cap by remaining bytes (BoundedReader): each element consumes
921            // wire data, so a ub4 count cannot legitimately exceed the payload.
922            let mut values: Vec<Option<QueryValue>> =
923                reader.with_capacity_limited(num_elements, 1, ProtocolLimits::check_batch_rows)?;
924            for _ in 0..num_elements {
925                let value = parse_column_value(reader, metadata)?;
926                let actual_num_bytes = reader.read_sb4()?;
927                values.push(apply_out_bind_actual_num_bytes(
928                    metadata,
929                    value,
930                    actual_num_bytes,
931                    "truncated array OUT bind value",
932                )?);
933            }
934            result
935                .out_values
936                .push((*index, Some(QueryValue::Array(values))));
937            continue;
938        }
939        let value = parse_column_value(reader, metadata)?;
940        let actual_num_bytes = reader.read_sb4()?;
941        result.out_values.push((
942            *index,
943            apply_out_bind_actual_num_bytes(
944                metadata,
945                value,
946                actual_num_bytes,
947                "truncated OUT bind value",
948            )?,
949        ));
950    }
951    Ok(())
952}
953
954pub(crate) fn parse_returning_row_data(
955    reader: &mut TtcReader<'_>,
956    result: &mut QueryResult,
957    bind_columns: &[ColumnMetadata],
958    output_bind_indexes: &[usize],
959) -> Result<()> {
960    for index in output_bind_indexes {
961        let metadata = bind_columns.get(*index).ok_or(ProtocolError::TtcDecode(
962            "return bind index without bind metadata",
963        ))?;
964        let num_rows = usize::try_from(reader.read_ub4()?).map_err(|_| {
965            ProtocolError::InvalidPacketLength {
966                length: usize::MAX,
967                minimum: 0,
968            }
969        })?;
970        reader.limits().check_batch_rows(num_rows)?;
971        // Cap by remaining bytes (BoundedReader); see the OOM note above.
972        let mut values: Vec<Option<QueryValue>> =
973            reader.with_capacity_limited(num_rows, 1, ProtocolLimits::check_batch_rows)?;
974        for _ in 0..num_rows {
975            let value = parse_column_value(reader, metadata)?;
976            let actual_num_bytes = reader.read_sb4()?;
977            values.push(apply_out_bind_actual_num_bytes(
978                metadata,
979                value,
980                actual_num_bytes,
981                "truncated DML RETURNING value",
982            )?);
983        }
984        result.return_values.push((*index, values));
985    }
986    Ok(())
987}
988
989fn apply_out_bind_actual_num_bytes(
990    metadata: &ColumnMetadata,
991    value: Option<QueryValue>,
992    actual_num_bytes: i32,
993    truncation_error: &'static str,
994) -> Result<Option<QueryValue>> {
995    if actual_num_bytes < 0 && metadata.ora_type_num == ORA_TYPE_NUM_BOOLEAN {
996        return Ok(None);
997    }
998    if actual_num_bytes != 0 && value.is_some() {
999        return Err(ProtocolError::TtcDecode(truncation_error));
1000    }
1001    Ok(value)
1002}
1003
1004pub(crate) fn is_duplicate_column(bit_vector: Option<&[u8]>, column_num: usize) -> bool {
1005    let Some(bit_vector) = bit_vector else {
1006        return false;
1007    };
1008    let byte_num = column_num / 8;
1009    let bit_num = column_num % 8;
1010    bit_vector
1011        .get(byte_num)
1012        .is_some_and(|byte| byte & (1 << bit_num) == 0)
1013}
1014
1015pub(crate) fn parse_column_value(
1016    reader: &mut TtcReader<'_>,
1017    metadata: &ColumnMetadata,
1018) -> Result<Option<QueryValue>> {
1019    parse_column_value_with_lob_mode(reader, metadata, LobDecodeMode::DefineMetadata)
1020}
1021
1022fn parse_column_value_with_lob_mode(
1023    reader: &mut TtcReader<'_>,
1024    metadata: &ColumnMetadata,
1025    lob_decode_mode: LobDecodeMode,
1026) -> Result<Option<QueryValue>> {
1027    if metadata.buffer_size == 0
1028        && !matches!(
1029            metadata.ora_type_num,
1030            ORA_TYPE_NUM_LONG | ORA_TYPE_NUM_LONG_RAW | ORA_TYPE_NUM_UROWID
1031        )
1032    {
1033        return Ok(None);
1034    }
1035    match metadata.ora_type_num {
1036        ORA_TYPE_NUM_VARCHAR | ORA_TYPE_NUM_CHAR | ORA_TYPE_NUM_LONG => {
1037            let Some(bytes) = reader.read_bytes()? else {
1038                return Ok(None);
1039            };
1040            match decode_text_value(&bytes, metadata.csfrm) {
1041                Ok(value) => Ok(Some(QueryValue::Text(value))),
1042                // preserve the raw bytes so the caller can honor the
1043                // configured encoding_errors policy (or raise a Python
1044                // UnicodeDecodeError as the reference does)
1045                Err(ProtocolError::TtcDecode(_)) => Ok(Some(QueryValue::TextRaw {
1046                    bytes,
1047                    csfrm: metadata.csfrm,
1048                })),
1049                Err(err) => Err(err),
1050            }
1051        }
1052        ORA_TYPE_NUM_RAW | ORA_TYPE_NUM_LONG_RAW => Ok(reader.read_bytes()?.map(QueryValue::Raw)),
1053        ORA_TYPE_NUM_ROWID => parse_rowid_value(reader).map(|value| value.map(QueryValue::Rowid)),
1054        ORA_TYPE_NUM_UROWID => parse_urowid_value(reader).map(|value| value.map(QueryValue::Rowid)),
1055        ORA_TYPE_NUM_NUMBER | ORA_TYPE_NUM_BINARY_INTEGER => {
1056            let Some(bytes) = reader.read_bytes()? else {
1057                return Ok(None);
1058            };
1059            decode_number_value(&bytes).map(Some)
1060        }
1061        ORA_TYPE_NUM_BINARY_DOUBLE => {
1062            let Some(bytes) = reader.read_bytes()? else {
1063                return Ok(None);
1064            };
1065            decode_binary_double(&bytes)
1066                .map(|value| Some(QueryValue::BinaryDouble(value.to_string())))
1067        }
1068        ORA_TYPE_NUM_BINARY_FLOAT => {
1069            let Some(bytes) = reader.read_bytes()? else {
1070                return Ok(None);
1071            };
1072            // f64-widened text matches Python float semantics for BINARY_FLOAT
1073            decode_binary_float(&bytes)
1074                .map(|value| Some(QueryValue::BinaryDouble(f64::from(value).to_string())))
1075        }
1076        ORA_TYPE_NUM_BOOLEAN => {
1077            let Some(bytes) = reader.read_bytes()? else {
1078                return Ok(None);
1079            };
1080            // reference read_bool: last byte == 1 means true; native
1081            // DB_TYPE_BOOLEAN surfaces as a Python bool.
1082            let is_true = matches!(bytes.last(), Some(&1));
1083            Ok(Some(QueryValue::Boolean(is_true)))
1084        }
1085        ORA_TYPE_NUM_INTERVAL_DS => {
1086            let Some(bytes) = reader.read_bytes()? else {
1087                return Ok(None);
1088            };
1089            decode_interval_ds(&bytes).map(Some)
1090        }
1091        ORA_TYPE_NUM_INTERVAL_YM => {
1092            let Some(bytes) = reader.read_bytes()? else {
1093                return Ok(None);
1094            };
1095            decode_interval_ym(&bytes).map(Some)
1096        }
1097        ORA_TYPE_NUM_DATE
1098        | ORA_TYPE_NUM_TIMESTAMP
1099        | ORA_TYPE_NUM_TIMESTAMP_LTZ
1100        | ORA_TYPE_NUM_TIMESTAMP_TZ => {
1101            let Some(bytes) = reader.read_bytes()? else {
1102                return Ok(None);
1103            };
1104            decode_datetime_value(&bytes).map(Some)
1105        }
1106        ORA_TYPE_NUM_CLOB | ORA_TYPE_NUM_BLOB | ORA_TYPE_NUM_BFILE => {
1107            parse_lob_value(reader, metadata, lob_decode_mode)
1108        }
1109        ORA_TYPE_NUM_VECTOR => parse_vector_value(reader),
1110        ORA_TYPE_NUM_JSON => parse_json_value(reader),
1111        ORA_TYPE_NUM_CURSOR => parse_cursor_value(reader).map(Some),
1112        ORA_TYPE_NUM_OBJECT => parse_object_value(reader, metadata),
1113        _ => Err(ProtocolError::UnsupportedFeature("query column type")),
1114    }
1115}
1116
1117/// A column value decoded in pass 1 of the borrowed row decode. Scalar values
1118/// that borrow the wire buffer are held directly; values that need a small
1119/// owned arena (synthesized `Number` text, or a cold owned [`QueryValue`]) are
1120/// recorded as a deferred handle into the per-row arena and resolved in pass 2
1121/// once the arena is frozen. This two-pass split is what keeps the borrowed
1122/// path sound under `#![forbid(unsafe_code)]`: no `&str`/`&[u8]` is ever held
1123/// into an arena that is still being grown.
1124enum ColumnSlot<'buf> {
1125    /// SQL NULL.
1126    Null,
1127    /// A value that borrows the wire buffer (or is a small `Copy` value).
1128    Wire(QueryValueRef<'buf>),
1129    /// A `NUMBER` whose canonical text lives at `arena[range]` in the per-row
1130    /// number-text arena.
1131    Number {
1132        range: core::ops::Range<usize>,
1133        is_integer: bool,
1134    },
1135    /// A cold / non-borrowable value parked at `owned[index]` in the per-row
1136    /// owned arena.
1137    Owned(usize),
1138}
1139
1140/// Decode one column into a [`ColumnSlot`], borrowing the wire buffer for the
1141/// hot scalar cases and appending to the per-row arenas for the deferred ones.
1142/// Mirrors [`parse_column_value`] type-for-type; the produced owned value (via
1143/// [`QueryValueRef::to_owned_value`]) is identical to the owned path.
1144///
1145/// `digits` is a caller-owned scratch buffer reused across all cells so the
1146/// per-cell `NUMBER` decode allocates nothing of its own (it writes straight
1147/// into `number_arena`). The hot scalar grid (Text/Raw) borrows the wire buffer
1148/// directly with zero allocation.
1149fn parse_column_slot<'buf>(
1150    reader: &mut TtcReader<'buf>,
1151    metadata: &ColumnMetadata,
1152    number_arena: &mut String,
1153    owned_arena: &mut Vec<QueryValue>,
1154    digits: &mut Vec<u8>,
1155    lob_decode_mode: LobDecodeMode,
1156) -> Result<ColumnSlot<'buf>> {
1157    // Park an owned QueryValue in the arena and return the deferred slot. Used
1158    // for the cold / non-borrowable variants so the hot grid stays borrowed.
1159    fn park(owned_arena: &mut Vec<QueryValue>, value: Option<QueryValue>) -> ColumnSlot<'static> {
1160        match value {
1161            None => ColumnSlot::Null,
1162            Some(value) => {
1163                owned_arena.push(value);
1164                ColumnSlot::Owned(owned_arena.len() - 1)
1165            }
1166        }
1167    }
1168
1169    if metadata.buffer_size == 0
1170        && !matches!(
1171            metadata.ora_type_num,
1172            ORA_TYPE_NUM_LONG | ORA_TYPE_NUM_LONG_RAW | ORA_TYPE_NUM_UROWID
1173        )
1174    {
1175        return Ok(ColumnSlot::Null);
1176    }
1177    match metadata.ora_type_num {
1178        ORA_TYPE_NUM_VARCHAR | ORA_TYPE_NUM_CHAR | ORA_TYPE_NUM_LONG => {
1179            match reader.read_bytes_borrowed()? {
1180                BorrowedBytes::Null => Ok(ColumnSlot::Null),
1181                // Borrow the wire bytes directly when they are valid UTF-8 and
1182                // not the UTF-16 NCHAR form (which needs re-encoding). Zero copy.
1183                BorrowedBytes::Slice(slice) if metadata.csfrm != CS_FORM_NCHAR => {
1184                    match validate_utf8(slice) {
1185                        Ok(text) => Ok(ColumnSlot::Wire(QueryValueRef::Text(text))),
1186                        Err(_) => Ok(park(
1187                            owned_arena,
1188                            Some(QueryValue::TextRaw {
1189                                bytes: slice.to_vec(),
1190                                csfrm: metadata.csfrm,
1191                            }),
1192                        )),
1193                    }
1194                }
1195                // NCHAR (UTF-16) or chunked long text: fall back to the owned
1196                // decode, which re-encodes to UTF-8 / reassembles chunks.
1197                other => {
1198                    let bytes = other.into_vec();
1199                    let value = match decode_text_value(&bytes, metadata.csfrm) {
1200                        Ok(text) => QueryValue::Text(text),
1201                        Err(ProtocolError::TtcDecode(_)) => QueryValue::TextRaw {
1202                            bytes,
1203                            csfrm: metadata.csfrm,
1204                        },
1205                        Err(err) => return Err(err),
1206                    };
1207                    Ok(park(owned_arena, Some(value)))
1208                }
1209            }
1210        }
1211        ORA_TYPE_NUM_RAW | ORA_TYPE_NUM_LONG_RAW => match reader.read_bytes_borrowed()? {
1212            BorrowedBytes::Null => Ok(ColumnSlot::Null),
1213            BorrowedBytes::Slice(slice) => Ok(ColumnSlot::Wire(QueryValueRef::Raw(slice))),
1214            BorrowedBytes::Chunked(bytes) => Ok(park(owned_arena, Some(QueryValue::Raw(bytes)))),
1215        },
1216        ORA_TYPE_NUM_NUMBER | ORA_TYPE_NUM_BINARY_INTEGER => {
1217            // The wire NUMBER is binary; its canonical decimal text is
1218            // synthesized, so it cannot be borrowed from the buffer. We
1219            // synthesize it *directly* into the per-row number arena (reusing the
1220            // `digits` scratch), borrowing from the arena — zero per-cell heap
1221            // allocation for the common in-range NUMBER.
1222            with_small_bytes(reader, |bytes| match bytes {
1223                None => Ok(ColumnSlot::Null),
1224                Some(bytes) => {
1225                    let start = number_arena.len();
1226                    let is_integer = decode_number_text_into(bytes, digits, number_arena)?;
1227                    Ok(ColumnSlot::Number {
1228                        range: start..number_arena.len(),
1229                        is_integer,
1230                    })
1231                }
1232            })
1233        }
1234        ORA_TYPE_NUM_BOOLEAN => with_small_bytes(reader, |bytes| match bytes {
1235            None => Ok(ColumnSlot::Null),
1236            Some(bytes) => Ok(ColumnSlot::Wire(QueryValueRef::Boolean(matches!(
1237                bytes.last(),
1238                Some(&1)
1239            )))),
1240        }),
1241        ORA_TYPE_NUM_INTERVAL_DS => with_small_bytes(reader, |bytes| match bytes {
1242            None => Ok(ColumnSlot::Null),
1243            Some(bytes) => match decode_interval_ds(bytes)? {
1244                QueryValue::IntervalDS {
1245                    days,
1246                    hours,
1247                    minutes,
1248                    seconds,
1249                    fseconds,
1250                } => Ok(ColumnSlot::Wire(QueryValueRef::IntervalDS {
1251                    days,
1252                    hours,
1253                    minutes,
1254                    seconds,
1255                    fseconds,
1256                })),
1257                other => Ok(park(owned_arena, Some(other))),
1258            },
1259        }),
1260        ORA_TYPE_NUM_INTERVAL_YM => with_small_bytes(reader, |bytes| match bytes {
1261            None => Ok(ColumnSlot::Null),
1262            Some(bytes) => match decode_interval_ym(bytes)? {
1263                QueryValue::IntervalYM { years, months } => {
1264                    Ok(ColumnSlot::Wire(QueryValueRef::IntervalYM {
1265                        years,
1266                        months,
1267                    }))
1268                }
1269                other => Ok(park(owned_arena, Some(other))),
1270            },
1271        }),
1272        ORA_TYPE_NUM_DATE
1273        | ORA_TYPE_NUM_TIMESTAMP
1274        | ORA_TYPE_NUM_TIMESTAMP_LTZ
1275        | ORA_TYPE_NUM_TIMESTAMP_TZ => with_small_bytes(reader, |bytes| match bytes {
1276            None => Ok(ColumnSlot::Null),
1277            Some(bytes) => match decode_datetime_value(bytes)? {
1278                QueryValue::DateTime {
1279                    year,
1280                    month,
1281                    day,
1282                    hour,
1283                    minute,
1284                    second,
1285                    nanosecond,
1286                } => Ok(ColumnSlot::Wire(QueryValueRef::DateTime {
1287                    year,
1288                    month,
1289                    day,
1290                    hour,
1291                    minute,
1292                    second,
1293                    nanosecond,
1294                })),
1295                QueryValue::TimestampTz {
1296                    year,
1297                    month,
1298                    day,
1299                    hour,
1300                    minute,
1301                    second,
1302                    nanosecond,
1303                    offset_minutes,
1304                } => Ok(ColumnSlot::Wire(QueryValueRef::TimestampTz {
1305                    year,
1306                    month,
1307                    day,
1308                    hour,
1309                    minute,
1310                    second,
1311                    nanosecond,
1312                    offset_minutes,
1313                })),
1314                other => Ok(park(owned_arena, Some(other))),
1315            },
1316        }),
1317        // Everything else (Rowid, BinaryDouble/Float, Clob/Blob/Bfile, Vector,
1318        // Json, Cursor, Object, UROWID) goes through the owned decode and is
1319        // parked in the owned arena. These are the cold / non-borrowable cases.
1320        _ => {
1321            let value = parse_column_value_with_lob_mode(reader, metadata, lob_decode_mode)?;
1322            Ok(park(owned_arena, value))
1323        }
1324    }
1325}
1326
1327/// Read one TTC byte field and hand the body to `f` as a borrowed `&[u8]`
1328/// without allocating in the common contiguous case. `None` is SQL NULL. The
1329/// rare chunked long form is reassembled into a temporary `Vec` (these small
1330/// fixed-size scalar types — number/boolean/interval/datetime — are never sent
1331/// chunked in practice, so this fallback is effectively dead weight).
1332fn with_small_bytes<'buf, T>(
1333    reader: &mut TtcReader<'buf>,
1334    f: impl FnOnce(Option<&[u8]>) -> Result<T>,
1335) -> Result<T> {
1336    match reader.read_bytes_borrowed()? {
1337        BorrowedBytes::Null => f(None),
1338        BorrowedBytes::Slice(slice) => f(Some(slice)),
1339        BorrowedBytes::Chunked(owned) => f(Some(&owned)),
1340    }
1341}
1342
1343impl BorrowedBytes<'_> {
1344    /// Reassemble into an owned `Vec` (zero-copy `Slice` still copies; `Chunked`
1345    /// reuses its already-owned `Vec`). Used by the non-borrowable text fallback.
1346    fn into_vec(self) -> Vec<u8> {
1347        match self {
1348            BorrowedBytes::Null => Vec::new(),
1349            BorrowedBytes::Slice(slice) => slice.to_vec(),
1350            BorrowedBytes::Chunked(owned) => owned,
1351        }
1352    }
1353}
1354
1355/// A decoded fetch batch that **owns** the wire response buffer and column
1356/// metadata, and yields rows of borrowed [`QueryValueRef`] that point straight
1357/// into that buffer. This is the zero-copy fetch fast path: the common scalar
1358/// grid is decoded with no per-cell allocation.
1359///
1360/// ## Soundness
1361///
1362/// The buffer is owned by the batch and outlives every borrowed row: rows are
1363/// only ever surfaced *inside* the [`for_each_row_ref`](Self::for_each_row_ref)
1364/// callback, whose `&[QueryValueRef]` argument cannot escape (its lifetime is
1365/// bound to the call). The borrow checker therefore guarantees no
1366/// `QueryValueRef` can dangle — there is no self-referential struct and no
1367/// `unsafe`. `Number` text and the cold values borrow per-row arenas that are
1368/// fully built (pass 1) before any reference into them is taken (pass 2), so an
1369/// arena is never grown while borrowed.
1370#[derive(Clone, Debug)]
1371pub struct BorrowedRowBatch {
1372    buffer: Vec<u8>,
1373    columns: Vec<ColumnMetadata>,
1374    /// Byte offset into `buffer` where each row's column values begin.
1375    row_starts: Vec<usize>,
1376    /// Per-row duplicate-column bit vector (server row-compression). `None` (or
1377    /// an absent entry) means every column is present on the wire for that row.
1378    /// A zero bit marks a duplicate column whose value repeats the previous
1379    /// row's value and carries no wire bytes (reference `bit_vector`).
1380    row_bit_vectors: Vec<Option<Vec<u8>>>,
1381    /// Whether this batch carried `LONG`/`LONG RAW` status trailers after each
1382    /// such column (the fetch path sets this; the plain execute path does not).
1383    fetch_long_status: bool,
1384    lob_decode_mode: LobDecodeMode,
1385    /// The caller's previous (owned) row, used to resolve duplicate columns in
1386    /// the *first* compressed row of the batch (whose duplicates repeat the row
1387    /// that ended the prior page). `None` outside the compressed-fetch case.
1388    previous_row_seed: Option<Vec<Option<QueryValue>>>,
1389}
1390
1391impl BorrowedRowBatch {
1392    /// Construct a batch from an owned wire `buffer`, the `columns` describing
1393    /// each cell, and the per-row start offsets into `buffer`. Use this for
1394    /// batches with no duplicate-column compression and no LONG trailers (the
1395    /// common synthetic / test case); the framing-aware
1396    /// [`parse_query_response_borrowed`] builds the full form.
1397    pub fn new(buffer: Vec<u8>, columns: Vec<ColumnMetadata>, row_starts: Vec<usize>) -> Self {
1398        Self {
1399            buffer,
1400            columns,
1401            row_starts,
1402            row_bit_vectors: Vec::new(),
1403            fetch_long_status: false,
1404            lob_decode_mode: LobDecodeMode::PlainLocator,
1405            previous_row_seed: None,
1406        }
1407    }
1408
1409    /// Number of rows in the batch.
1410    pub fn row_count(&self) -> usize {
1411        self.row_starts.len()
1412    }
1413
1414    /// The columns describing each cell.
1415    pub fn columns(&self) -> &[ColumnMetadata] {
1416        &self.columns
1417    }
1418
1419    /// The address range of the owned buffer, for tests asserting that borrowed
1420    /// scalar cells truly point into it (zero-copy).
1421    #[cfg(test)]
1422    pub fn buffer_ptr_range(&self) -> core::ops::Range<usize> {
1423        let start = self.buffer.as_ptr() as usize;
1424        start..start + self.buffer.len()
1425    }
1426
1427    /// Decode each row and invoke `callback` with the row's borrowed cells. The
1428    /// `&[Option<QueryValueRef>]` slice borrows the batch buffer and per-row
1429    /// arenas; it is valid only for the duration of the call (it cannot escape).
1430    /// `None` cells are SQL NULL. Returns the first decode/callback error.
1431    ///
1432    /// Generic over the callback's error type `E` (any error a decode failure
1433    /// can convert into, e.g. the driver crate's own error) so callers are not
1434    /// forced through [`ProtocolError`]; a decode failure is surfaced via
1435    /// `E: From<ProtocolError>`.
1436    pub fn for_each_row_ref<F, E>(&self, mut callback: F) -> std::result::Result<(), E>
1437    where
1438        F: FnMut(&[Option<QueryValueRef<'_>>]) -> std::result::Result<(), E>,
1439        E: From<ProtocolError>,
1440    {
1441        // The two arenas are reused across rows (cleared, not reallocated). They
1442        // are mutated only in pass 1; pass 2 borrows them immutably and that
1443        // borrow is confined to a single loop iteration (it ends before the next
1444        // iteration's `clear()`), which is what keeps the borrow checker — and
1445        // soundness — happy.
1446        let mut number_arena = String::new();
1447        let mut owned_arena: Vec<QueryValue> = Vec::new();
1448        // Reusable scratch: `digits` for the NUMBER decode, `slots` for pass 1,
1449        // `row` for the borrowed cells handed to the callback. All cleared and
1450        // reused across rows so the steady-state per-row decode allocates only
1451        // when an arena/scratch genuinely grows (amortized). `slots`/`row`/
1452        // `digits` borrow nothing across iterations beyond the stable buffer.
1453        let mut digits: Vec<u8> = Vec::new();
1454        let mut slots: Vec<Option<ColumnSlot<'_>>> = Vec::with_capacity(self.columns.len());
1455        // Owned snapshot of the previous row, used only to resolve duplicate
1456        // (bit-vector-compressed) columns, which carry no wire bytes. Empty when
1457        // the batch has no bit vectors (the common case), so it costs nothing.
1458        // Seeded from the caller's prior-page row for the first compressed row.
1459        let mut previous_owned: Vec<Option<QueryValue>> =
1460            self.previous_row_seed.clone().unwrap_or_default();
1461        let uses_bit_vectors = !self.row_bit_vectors.is_empty();
1462
1463        for (row_index, &start) in self.row_starts.iter().enumerate() {
1464            number_arena.clear();
1465            owned_arena.clear();
1466            slots.clear();
1467            let bit_vector = self
1468                .row_bit_vectors
1469                .get(row_index)
1470                .and_then(|bv| bv.as_deref());
1471
1472            // Pass 1: decode all columns, growing the per-row arenas. `slots`
1473            // borrows only the buffer (the deferred Number/Owned slots hold a
1474            // range/index into the arenas, never a borrow of them). Duplicate
1475            // columns carry no wire bytes — their owned previous value is parked
1476            // in the owned arena.
1477            let mut reader = TtcReader::new(&self.buffer[start..]);
1478            for (index, metadata) in self.columns.iter().enumerate() {
1479                if is_duplicate_column(bit_vector, index) {
1480                    let previous = previous_owned.get(index).and_then(Option::as_ref);
1481                    match previous {
1482                        None => slots.push(None),
1483                        Some(value) => {
1484                            owned_arena.push(value.clone());
1485                            slots.push(Some(ColumnSlot::Owned(owned_arena.len() - 1)));
1486                        }
1487                    }
1488                    continue;
1489                }
1490                let slot = parse_column_slot(
1491                    &mut reader,
1492                    metadata,
1493                    &mut number_arena,
1494                    &mut owned_arena,
1495                    &mut digits,
1496                    self.lob_decode_mode,
1497                )?;
1498                slots.push(match slot {
1499                    ColumnSlot::Null => None,
1500                    other => Some(other),
1501                });
1502                if self.fetch_long_status
1503                    && matches!(
1504                        metadata.ora_type_num,
1505                        ORA_TYPE_NUM_LONG | ORA_TYPE_NUM_LONG_RAW
1506                    )
1507                {
1508                    let _null_indicator = reader.read_sb4()?;
1509                    let _return_code = reader.read_ub4()?;
1510                }
1511            }
1512
1513            // Pass 2: arenas are now frozen — resolve deferred slots into
1514            // borrowed refs. No arena is mutated here, so the borrows are sound.
1515            // `row` is allocated per row: it carries the per-row arena lifetime,
1516            // which (unlike `slots`/`digits`, that borrow only the stable buffer)
1517            // cannot be reused across an arena `clear()`. This is the single
1518            // remaining per-row allocation, versus the owned path's per-row Vec
1519            // *plus* a String per scalar cell.
1520            let row: Vec<Option<QueryValueRef<'_>>> = slots
1521                .iter()
1522                .map(|slot| {
1523                    slot.as_ref().map(|slot| match *slot {
1524                        ColumnSlot::Null => unreachable!("Null slots are stored as None"),
1525                        ColumnSlot::Wire(value) => value,
1526                        ColumnSlot::Number {
1527                            ref range,
1528                            is_integer,
1529                        } => QueryValueRef::Number {
1530                            text: &number_arena[range.clone()],
1531                            is_integer,
1532                        },
1533                        ColumnSlot::Owned(index) => QueryValueRef::Owned(&owned_arena[index]),
1534                    })
1535                })
1536                .collect();
1537
1538            callback(&row)?;
1539
1540            // Snapshot the just-emitted row as owned values for the next row's
1541            // duplicate-column resolution — but only when the batch actually uses
1542            // bit-vector compression, so the zero-copy common path pays nothing.
1543            if uses_bit_vectors {
1544                previous_owned.clear();
1545                previous_owned.extend(row.iter().map(|cell| cell.map(|v| v.to_owned_value())));
1546            }
1547        }
1548        Ok(())
1549    }
1550}
1551
1552/// The borrowed counterpart of a fetched [`QueryResult`]: a [`BorrowedRowBatch`]
1553/// of zero-copy rows plus the response-level fields a caller needs to page and
1554/// finalize the cursor. Produced by [`parse_query_response_borrowed`].
1555#[derive(Clone, Debug)]
1556pub struct BorrowedFetchResult {
1557    /// The decoded rows, borrowing the response buffer.
1558    pub batch: BorrowedRowBatch,
1559    /// Whether the server reports more rows for this cursor.
1560    pub more_rows: bool,
1561    /// Server cursor id (for paging / release).
1562    pub cursor_id: u32,
1563    /// Total affected/processed row count from the end-of-call error message.
1564    pub row_count: u64,
1565}
1566
1567/// Walk a fetch/query response payload and produce a [`BorrowedFetchResult`]
1568/// whose rows borrow `payload` (the caller must keep the owned buffer alive —
1569/// [`BorrowedRowBatch`] owns it). This is the zero-copy companion to
1570/// [`parse_fetch_response_with_context`]: it walks the exact same message
1571/// framing (DESCRIBE_INFO / ROW_HEADER / BIT_VECTOR / ROW_DATA / ERROR /
1572/// END_OF_RESPONSE) but, instead of materializing owned rows, records each
1573/// row's byte offset and bit vector so [`BorrowedRowBatch::for_each_row_ref`]
1574/// can decode them lazily and without per-cell allocation.
1575///
1576/// Scope: the plain query-row case (the fetch path). Out-bind / DML-returning
1577/// rows are not part of a fetch response and are left to the owned path.
1578pub fn parse_query_response_borrowed(
1579    payload: &[u8],
1580    capabilities: ClientCapabilities,
1581    columns: &[ColumnMetadata],
1582    previous_row: Option<&[Option<QueryValue>]>,
1583) -> Result<BorrowedFetchResult> {
1584    parse_query_response_borrowed_with_limits(
1585        payload,
1586        capabilities,
1587        columns,
1588        previous_row,
1589        ProtocolLimits::DEFAULT,
1590    )
1591}
1592
1593pub fn parse_query_response_borrowed_with_limits(
1594    payload: &[u8],
1595    capabilities: ClientCapabilities,
1596    columns: &[ColumnMetadata],
1597    previous_row: Option<&[Option<QueryValue>]>,
1598    limits: ProtocolLimits,
1599) -> Result<BorrowedFetchResult> {
1600    parse_query_response_borrowed_with_lob_mode_and_limits(
1601        payload,
1602        capabilities,
1603        columns,
1604        previous_row,
1605        LobDecodeMode::PlainLocator,
1606        limits,
1607    )
1608}
1609
1610pub fn parse_define_fetch_response_borrowed_with_limits(
1611    payload: &[u8],
1612    capabilities: ClientCapabilities,
1613    columns: &[ColumnMetadata],
1614    previous_row: Option<&[Option<QueryValue>]>,
1615    limits: ProtocolLimits,
1616) -> Result<BorrowedFetchResult> {
1617    parse_query_response_borrowed_with_lob_mode_and_limits(
1618        payload,
1619        capabilities,
1620        columns,
1621        previous_row,
1622        LobDecodeMode::DefineMetadata,
1623        limits,
1624    )
1625}
1626
1627fn parse_query_response_borrowed_with_lob_mode_and_limits(
1628    payload: &[u8],
1629    capabilities: ClientCapabilities,
1630    columns: &[ColumnMetadata],
1631    previous_row: Option<&[Option<QueryValue>]>,
1632    lob_decode_mode: LobDecodeMode,
1633    limits: ProtocolLimits,
1634) -> Result<BorrowedFetchResult> {
1635    let mut reader = TtcReader::with_limits(payload, limits)?;
1636    reader.limits().check_columns(columns.len())?;
1637    let mut result_columns = columns.to_vec();
1638    let mut more_rows = true;
1639    let mut cursor_id = 0u32;
1640    let mut row_count = 0u64;
1641    let mut row_starts: Vec<usize> = Vec::new();
1642    let mut row_bit_vectors: Vec<Option<Vec<u8>>> = Vec::new();
1643    let mut any_bit_vector = false;
1644    let mut pending_bit_vector: Option<Vec<u8>> = None;
1645    // The fetch path always consumes LONG/LONG RAW status trailers.
1646    let fetch_long_status = true;
1647
1648    while reader.remaining() > 0 {
1649        let message_type = reader.read_u8()?;
1650        match message_type {
1651            0 => {}
1652            TNS_MSG_TYPE_DESCRIBE_INFO => {
1653                let _describe_name = reader.read_bytes()?;
1654                let previous = std::mem::take(&mut result_columns);
1655                let mut described = QueryResult::default();
1656                parse_describe_info(&mut reader, capabilities, &mut described)?;
1657                result_columns = described.columns;
1658                for (index, column) in result_columns.iter_mut().enumerate() {
1659                    if let Some(prev) = previous.get(index) {
1660                        adjust_refetch_metadata(prev, column);
1661                    }
1662                }
1663            }
1664            TNS_MSG_TYPE_ROW_HEADER => {
1665                pending_bit_vector = parse_row_header(&mut reader)?;
1666            }
1667            TNS_MSG_TYPE_BIT_VECTOR => {
1668                pending_bit_vector = Some(parse_bit_vector(&mut reader, result_columns.len())?);
1669            }
1670            TNS_MSG_TYPE_ROW_DATA => {
1671                // Record where this row's column values begin, then advance the
1672                // reader past the row (skipping, not materializing).
1673                reader.limits().check_batch_rows(row_starts.len() + 1)?;
1674                row_starts.push(reader.position());
1675                let bit_vector = pending_bit_vector.take();
1676                any_bit_vector |= bit_vector.is_some();
1677                row_bit_vectors.push(bit_vector.clone());
1678                skip_row_data(
1679                    &mut reader,
1680                    &result_columns,
1681                    bit_vector.as_deref(),
1682                    fetch_long_status,
1683                    lob_decode_mode,
1684                )?;
1685            }
1686            TNS_MSG_TYPE_PARAMETER => {
1687                let _params = parse_query_return_parameters(&mut reader, false)?;
1688            }
1689            TNS_MSG_TYPE_STATUS => {
1690                let _call_status = reader.read_ub4()?;
1691                let _seq = reader.read_ub2()?;
1692            }
1693            TNS_MSG_TYPE_SERVER_SIDE_PIGGYBACK => {
1694                let _ = skip_server_side_piggyback(&mut reader)?;
1695            }
1696            TNS_MSG_TYPE_FLUSH_OUT_BINDS | TNS_MSG_TYPE_END_OF_RESPONSE => break,
1697            TNS_MSG_TYPE_TOKEN => {
1698                let _token = reader.read_ub8()?;
1699            }
1700            TNS_MSG_TYPE_IMPLICIT_RESULTSET => {
1701                // Mirror the owned parser's framing walk so the reader advances
1702                // past the implicit-resultset block identically (the borrowed
1703                // fetch API does not surface child cursors, but it must still
1704                // consume the bytes). reference messages/base.pyx
1705                // `_process_implicit_result`.
1706                let num_results = reader.read_ub4()?;
1707                reader
1708                    .limits()
1709                    .check_length_prefixed_elements(num_results as usize)?;
1710                for _ in 0..num_results {
1711                    let num_bytes = reader.read_u8()?;
1712                    reader.skip(usize::from(num_bytes))?;
1713                    let mut child = QueryResult::default();
1714                    parse_describe_info(&mut reader, capabilities, &mut child)?;
1715                    let _child_cursor_id = reader.read_ub2()?;
1716                }
1717            }
1718            TNS_MSG_TYPE_ERROR => {
1719                let info = parse_server_error_info(&mut reader, capabilities.ttc_field_version)?;
1720                if info.cursor_id != 0 {
1721                    cursor_id = u32::from(info.cursor_id);
1722                }
1723                row_count = info.row_count;
1724                if info.number == TNS_ERR_NO_DATA_FOUND && !result_columns.is_empty() {
1725                    more_rows = false;
1726                } else if info.number != 0 && info.number != TNS_ERR_ARRAY_DML_ERRORS {
1727                    return Err(ProtocolError::ServerErrorInfo(Box::new(
1728                        info.into_details(),
1729                    )));
1730                }
1731            }
1732            _ => {
1733                let position = reader.position().saturating_sub(1);
1734                if let Some(message) =
1735                    find_embedded_server_error(payload, capabilities.ttc_field_version, position)
1736                {
1737                    return Err(ProtocolError::ServerError(message));
1738                }
1739                return Err(ProtocolError::UnknownMessageType {
1740                    message_type,
1741                    position,
1742                });
1743            }
1744        }
1745    }
1746
1747    // If the batch never used duplicate-column compression, drop the per-row
1748    // bit-vector vector so iteration takes the zero-copy fast path (no owned
1749    // previous-row snapshotting).
1750    if !any_bit_vector {
1751        row_bit_vectors.clear();
1752    }
1753
1754    let batch = BorrowedRowBatch {
1755        buffer: payload.to_vec(),
1756        columns: result_columns,
1757        row_starts,
1758        row_bit_vectors,
1759        fetch_long_status,
1760        lob_decode_mode,
1761        // Seed the first compressed row's duplicate resolution from the caller's
1762        // prior-page row (only consulted when the batch uses bit vectors).
1763        previous_row_seed: any_bit_vector.then(|| {
1764            previous_row
1765                .map(<[Option<QueryValue>]>::to_vec)
1766                .unwrap_or_default()
1767        }),
1768    };
1769
1770    Ok(BorrowedFetchResult {
1771        batch,
1772        more_rows,
1773        cursor_id,
1774        row_count,
1775    })
1776}
1777
1778/// Advance `reader` past one ROW_DATA row **without materializing owned values**
1779/// — this is the offset-capture pass, so it must allocate nothing for the hot
1780/// scalar grid. Mirrors [`parse_row_data`]'s consumption exactly: duplicate
1781/// (bit-vector) columns carry no wire bytes and are skipped; the hot byte-field
1782/// scalar types are skipped with a zero-allocation length-prefixed skip; the
1783/// rare cold types (LOB / Vector / JSON / Cursor / Object / ROWID), whose wire
1784/// framing is non-trivial, fall back to [`parse_column_value`] (which may
1785/// allocate, but those are uncommon). `LONG`/`LONG RAW` status trailers are
1786/// consumed when `fetch_long_status`.
1787fn skip_row_data(
1788    reader: &mut TtcReader<'_>,
1789    columns: &[ColumnMetadata],
1790    bit_vector: Option<&[u8]>,
1791    fetch_long_status: bool,
1792    lob_decode_mode: LobDecodeMode,
1793) -> Result<()> {
1794    for (index, metadata) in columns.iter().enumerate() {
1795        if is_duplicate_column(bit_vector, index) {
1796            continue;
1797        }
1798        let consumed_byte_field = metadata.buffer_size != 0
1799            && matches!(
1800                metadata.ora_type_num,
1801                ORA_TYPE_NUM_VARCHAR
1802                    | ORA_TYPE_NUM_CHAR
1803                    | ORA_TYPE_NUM_LONG
1804                    | ORA_TYPE_NUM_RAW
1805                    | ORA_TYPE_NUM_LONG_RAW
1806                    | ORA_TYPE_NUM_NUMBER
1807                    | ORA_TYPE_NUM_BINARY_INTEGER
1808                    | ORA_TYPE_NUM_BINARY_DOUBLE
1809                    | ORA_TYPE_NUM_BINARY_FLOAT
1810                    | ORA_TYPE_NUM_BOOLEAN
1811                    | ORA_TYPE_NUM_INTERVAL_DS
1812                    | ORA_TYPE_NUM_INTERVAL_YM
1813                    | ORA_TYPE_NUM_DATE
1814                    | ORA_TYPE_NUM_TIMESTAMP
1815                    | ORA_TYPE_NUM_TIMESTAMP_LTZ
1816                    | ORA_TYPE_NUM_TIMESTAMP_TZ
1817            );
1818        if consumed_byte_field {
1819            reader.skip_bytes_field()?;
1820        } else {
1821            // Cold / non-byte-field type, or a zero-buffer-size column: defer to
1822            // the full owned decode purely to advance the reader correctly.
1823            let _ = parse_column_value_with_lob_mode(reader, metadata, lob_decode_mode)?;
1824        }
1825        if fetch_long_status
1826            && matches!(
1827                metadata.ora_type_num,
1828                ORA_TYPE_NUM_LONG | ORA_TYPE_NUM_LONG_RAW
1829            )
1830        {
1831            let _null_indicator = reader.read_sb4()?;
1832            let _return_code = reader.read_ub4()?;
1833        }
1834    }
1835    Ok(())
1836}
1837
1838pub(crate) fn encode_rowid_component(mut value: u32, size: usize, output: &mut String) {
1839    let mut encoded = vec![b'A'; size];
1840    for index in 0..size {
1841        let alphabet_index = usize::try_from(value & 0x3f).unwrap_or(0);
1842        encoded[size - index - 1] = TNS_BASE64_ALPHABET[alphabet_index];
1843        value >>= 6;
1844    }
1845    output.extend(encoded.into_iter().map(char::from));
1846}
1847
1848pub(crate) fn encode_physical_rowid(
1849    rba: u32,
1850    partition_id: u16,
1851    block_num: u32,
1852    slot_num: u16,
1853) -> String {
1854    let mut output = String::with_capacity(ORA_TYPE_SIZE_ROWID as usize);
1855    encode_rowid_component(rba, 6, &mut output);
1856    encode_rowid_component(u32::from(partition_id), 3, &mut output);
1857    encode_rowid_component(block_num, 6, &mut output);
1858    encode_rowid_component(u32::from(slot_num), 3, &mut output);
1859    output
1860}
1861
1862pub(crate) fn parse_rowid_value(reader: &mut TtcReader<'_>) -> Result<Option<String>> {
1863    let len = reader.read_u8()?;
1864    if len == 0 || len == crate::wire::TNS_NULL_LENGTH_INDICATOR {
1865        return Ok(None);
1866    }
1867    let rba = reader.read_ub4()?;
1868    let partition_id = reader.read_ub2()?;
1869    reader.skip(1)?;
1870    let block_num = reader.read_ub4()?;
1871    let slot_num = reader.read_ub2()?;
1872    Ok(Some(encode_physical_rowid(
1873        rba,
1874        partition_id,
1875        block_num,
1876        slot_num,
1877    )))
1878}
1879
1880pub(crate) fn encode_logical_urowid(bytes: &[u8]) -> String {
1881    let mut input_offset = 1;
1882    let mut input_len = bytes.len().saturating_sub(1);
1883    let mut output = String::with_capacity((bytes.len() / 3) * 4 + 4);
1884    output.push('*');
1885    while input_len > 0 {
1886        let mut pos = bytes[input_offset] >> 2;
1887        output.push(char::from(TNS_BASE64_ALPHABET[usize::from(pos)]));
1888
1889        pos = (bytes[input_offset] & 0x03) << 4;
1890        if input_len == 1 {
1891            output.push(char::from(TNS_BASE64_ALPHABET[usize::from(pos)]));
1892            break;
1893        }
1894        input_offset += 1;
1895        pos |= (bytes[input_offset] & 0xf0) >> 4;
1896        output.push(char::from(TNS_BASE64_ALPHABET[usize::from(pos)]));
1897
1898        pos = (bytes[input_offset] & 0x0f) << 2;
1899        if input_len == 2 {
1900            output.push(char::from(TNS_BASE64_ALPHABET[usize::from(pos)]));
1901            break;
1902        }
1903        input_offset += 1;
1904        pos |= (bytes[input_offset] & 0xc0) >> 6;
1905        output.push(char::from(TNS_BASE64_ALPHABET[usize::from(pos)]));
1906
1907        pos = bytes[input_offset] & 0x3f;
1908        output.push(char::from(TNS_BASE64_ALPHABET[usize::from(pos)]));
1909        input_offset += 1;
1910        input_len -= 3;
1911    }
1912    output
1913}
1914
1915pub(crate) fn parse_urowid_value(reader: &mut TtcReader<'_>) -> Result<Option<String>> {
1916    if reader.read_bytes()?.is_none() {
1917        return Ok(None);
1918    }
1919    let Some(bytes) = reader.read_bytes()? else {
1920        return Ok(None);
1921    };
1922    if bytes.len() < 13 {
1923        return Err(ProtocolError::TtcDecode("encoded UROWID too short"));
1924    }
1925    if bytes[0] == 1 {
1926        let rba = u32::from_be_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]);
1927        let partition_id = u16::from_be_bytes([bytes[5], bytes[6]]);
1928        let block_num = u32::from_be_bytes([bytes[7], bytes[8], bytes[9], bytes[10]]);
1929        let slot_num = u16::from_be_bytes([bytes[11], bytes[12]]);
1930        Ok(Some(encode_physical_rowid(
1931            rba,
1932            partition_id,
1933            block_num,
1934            slot_num,
1935        )))
1936    } else {
1937        Ok(Some(encode_logical_urowid(&bytes)))
1938    }
1939}
1940
1941pub(crate) fn parse_lob_value(
1942    reader: &mut TtcReader<'_>,
1943    metadata: &ColumnMetadata,
1944    lob_decode_mode: LobDecodeMode,
1945) -> Result<Option<QueryValue>> {
1946    let num_bytes = reader.read_ub4()?;
1947    reader.limits().check_response_bytes(num_bytes as usize)?;
1948    if num_bytes == 0 {
1949        return Ok(None);
1950    }
1951    let (size, chunk_size) = if matches!(
1952        (lob_decode_mode, metadata.ora_type_num),
1953        (_, ORA_TYPE_NUM_BFILE) | (LobDecodeMode::PlainLocator, _)
1954    ) {
1955        (0, 0)
1956    } else {
1957        (reader.read_ub8()?, reader.read_ub4()?)
1958    };
1959    let Some(locator) = reader.read_bytes()? else {
1960        return Ok(None);
1961    };
1962    Ok(Some(QueryValue::Lob(Box::new(LobValue {
1963        ora_type_num: metadata.ora_type_num,
1964        csfrm: metadata.csfrm,
1965        locator,
1966        size,
1967        chunk_size,
1968    }))))
1969}
1970
1971#[cfg(test)]
1972mod lob_fetch_shape_tests {
1973    use super::*;
1974
1975    fn clob_column() -> ColumnMetadata {
1976        ColumnMetadata {
1977            name: "BODY".into(),
1978            ora_type_num: ORA_TYPE_NUM_CLOB,
1979            csfrm: CS_FORM_IMPLICIT,
1980            precision: 0,
1981            scale: 0,
1982            buffer_size: 4000,
1983            max_size: 4000,
1984            nulls_allowed: true,
1985            is_json: false,
1986            is_oson: false,
1987            object_schema: None,
1988            object_type_name: None,
1989            is_array: false,
1990            vector_dimensions: None,
1991            vector_format: 0,
1992            vector_flags: 0,
1993            domain_schema: None,
1994            domain_name: None,
1995            annotations: None,
1996        }
1997    }
1998
1999    fn blob_column() -> ColumnMetadata {
2000        ColumnMetadata {
2001            name: "IMAGE".into(),
2002            ora_type_num: ORA_TYPE_NUM_BLOB,
2003            csfrm: 0,
2004            precision: 0,
2005            scale: 0,
2006            buffer_size: 4000,
2007            max_size: 4000,
2008            nulls_allowed: true,
2009            is_json: false,
2010            is_oson: false,
2011            object_schema: None,
2012            object_type_name: None,
2013            is_array: false,
2014            vector_dimensions: None,
2015            vector_format: 0,
2016            vector_flags: 0,
2017            domain_schema: None,
2018            domain_name: None,
2019            annotations: None,
2020        }
2021    }
2022
2023    fn lob_row_payload(locator: &[u8], metadata: Option<(u64, u32)>) -> Vec<u8> {
2024        let mut writer = TtcWriter::new();
2025        writer.write_u8(TNS_MSG_TYPE_ROW_DATA);
2026        writer.write_ub4(u32::try_from(locator.len()).expect("locator length fits ub4"));
2027        if let Some((size, chunk_size)) = metadata {
2028            writer.write_ub8(size);
2029            writer.write_ub4(chunk_size);
2030        }
2031        writer
2032            .write_bytes_with_length(locator)
2033            .expect("synthetic locator length is encodable");
2034        writer.write_u8(TNS_MSG_TYPE_END_OF_RESPONSE);
2035        writer.into_bytes()
2036    }
2037
2038    fn write_define_lob_cell(writer: &mut TtcWriter, locator: &[u8], size: u64, chunk_size: u32) {
2039        writer.write_ub4(u32::try_from(locator.len()).expect("locator length fits ub4"));
2040        writer.write_ub8(size);
2041        writer.write_ub4(chunk_size);
2042        writer
2043            .write_bytes_with_length(locator)
2044            .expect("synthetic locator length is encodable");
2045    }
2046
2047    fn first_lob(result: &QueryResult) -> Option<&LobValue> {
2048        match result.rows.first()?.first()? {
2049            Some(QueryValue::Lob(lob)) => Some(lob.as_ref()),
2050            _ => None,
2051        }
2052    }
2053
2054    #[test]
2055    fn column_metadata_discards_server_charset_id_and_keeps_csfrm_only() {
2056        let caps = ClientCapabilities {
2057            ttc_field_version: 0,
2058            max_string_size: 32_767,
2059            charset_id: 873,
2060        };
2061        let mut writer = TtcWriter::new();
2062        writer.write_u8(ORA_TYPE_NUM_VARCHAR);
2063        writer.write_u8(0); // flags
2064        writer.write_u8(0); // precision
2065        writer.write_u8(0); // scale
2066        writer.write_ub4(4000);
2067        writer.write_ub4(0); // max array elements
2068        writer.write_ub8(0); // cont flags
2069        writer
2070            .write_bytes_with_two_lengths(None)
2071            .expect("empty oid");
2072        writer.write_ub2(0); // version
2073        writer.write_ub2(0); // unusable server charset id: must not drive decoding
2074        writer.write_u8(CS_FORM_IMPLICIT);
2075        writer.write_ub4(4000);
2076        writer.write_u8(1); // nullable
2077        writer.write_u8(0); // flags
2078        writer
2079            .write_bytes_with_two_lengths(Some(b"TXT"))
2080            .expect("name");
2081        writer
2082            .write_bytes_with_two_lengths(None)
2083            .expect("object schema");
2084        writer
2085            .write_bytes_with_two_lengths(None)
2086            .expect("object type");
2087        writer.write_ub2(1); // column position
2088        writer.write_ub4(0); // uds flags
2089
2090        let bytes = writer.into_bytes();
2091        let mut reader = TtcReader::new(&bytes);
2092        let metadata = parse_column_metadata(&mut reader, caps).expect("metadata should parse");
2093        assert_eq!(metadata.name(), "TXT");
2094        assert_eq!(metadata.csfrm(), CS_FORM_IMPLICIT);
2095        assert_eq!(
2096            decode_text_value("ok".as_bytes(), metadata.csfrm()).expect("decode text"),
2097            "ok"
2098        );
2099    }
2100
2101    #[test]
2102    fn plain_fetch_lob_locator_omits_size_and_chunk_fields() {
2103        let locator: Vec<u8> = (0u8..114).collect();
2104        let payload = lob_row_payload(&locator, None);
2105        let columns = [clob_column()];
2106
2107        let result = parse_fetch_response_with_context(
2108            &payload,
2109            ClientCapabilities::default(),
2110            &columns,
2111            None,
2112        )
2113        .expect("plain fetch CLOB locator should decode");
2114
2115        let lob = first_lob(&result).expect("plain fetch should return a LOB value");
2116        assert_eq!(lob.locator, locator);
2117        assert_eq!(lob.size, 0);
2118        assert_eq!(lob.chunk_size, 0);
2119    }
2120
2121    #[test]
2122    fn define_fetch_lob_locator_includes_size_and_chunk_fields() {
2123        let locator: Vec<u8> = (0u8..114).collect();
2124        let payload = lob_row_payload(&locator, Some((23, 8060)));
2125        let columns = [clob_column()];
2126
2127        let result = parse_define_fetch_response_with_context_and_limits(
2128            &payload,
2129            ClientCapabilities::default(),
2130            &columns,
2131            None,
2132            ProtocolLimits::DEFAULT,
2133        )
2134        .expect("define fetch CLOB locator should decode");
2135
2136        let lob = first_lob(&result).expect("define fetch should return a LOB value");
2137        assert_eq!(lob.locator, locator);
2138        assert_eq!(lob.size, 23);
2139        assert_eq!(lob.chunk_size, 8060);
2140    }
2141
2142    #[test]
2143    fn borrowed_define_fetch_lob_page_matches_owned_parse() {
2144        let columns = [clob_column(), blob_column()];
2145        let clob_locator_a: Vec<u8> = (0u8..114).collect();
2146        let blob_locator_a: Vec<u8> = (128u8..242).collect();
2147        let clob_locator_b: Vec<u8> = (32u8..146).collect();
2148        let blob_locator_b: Vec<u8> = (64u8..178).collect();
2149        let mut writer = TtcWriter::new();
2150
2151        writer.write_u8(TNS_MSG_TYPE_ROW_DATA);
2152        write_define_lob_cell(&mut writer, &clob_locator_a, 23, 8060);
2153        write_define_lob_cell(&mut writer, &blob_locator_a, 48, 4096);
2154        writer.write_u8(TNS_MSG_TYPE_ROW_DATA);
2155        write_define_lob_cell(&mut writer, &clob_locator_b, 31, 8060);
2156        write_define_lob_cell(&mut writer, &blob_locator_b, 96, 4096);
2157        writer.write_u8(TNS_MSG_TYPE_END_OF_RESPONSE);
2158        let payload = writer.into_bytes();
2159
2160        let owned = parse_define_fetch_response_with_context_and_limits(
2161            &payload,
2162            ClientCapabilities::default(),
2163            &columns,
2164            None,
2165            ProtocolLimits::DEFAULT,
2166        )
2167        .expect("owned define fetch CLOB/BLOB page should parse");
2168        let borrowed = parse_define_fetch_response_borrowed_with_limits(
2169            &payload,
2170            ClientCapabilities::default(),
2171            &columns,
2172            None,
2173            ProtocolLimits::DEFAULT,
2174        )
2175        .expect("borrowed define fetch CLOB/BLOB page should parse");
2176
2177        let mut borrowed_rows: Vec<Vec<Option<QueryValue>>> = Vec::new();
2178        borrowed
2179            .batch
2180            .for_each_row_ref(|row| {
2181                borrowed_rows.push(
2182                    row.iter()
2183                        .map(|cell| cell.map(|value| value.to_owned_value()))
2184                        .collect(),
2185                );
2186                Ok::<(), ProtocolError>(())
2187            })
2188            .expect("borrowed define fetch CLOB/BLOB page should iterate");
2189
2190        assert_eq!(owned.rows.len(), 2, "owned parse sees both rows");
2191        assert_eq!(
2192            borrowed.batch.row_count(),
2193            2,
2194            "borrowed parse sees both rows"
2195        );
2196        assert_eq!(
2197            borrowed_rows, owned.rows,
2198            "borrowed DefineMetadata LOB parse must match owned parse"
2199        );
2200    }
2201}
2202
2203/// Reads a VECTOR value (reference `ReadBuffer.read_vector` in `packet.pyx`).
2204/// VECTOR is sent as a fully-prefetched LOB: the image data precedes the
2205/// (discarded) LOB locator.
2206pub(crate) fn parse_vector_value(reader: &mut TtcReader<'_>) -> Result<Option<QueryValue>> {
2207    let num_bytes = reader.read_ub4()?;
2208    reader.limits().check_response_bytes(num_bytes as usize)?;
2209    if num_bytes == 0 {
2210        return Ok(None);
2211    }
2212    reader.read_ub8()?; // size (unused)
2213    reader.read_ub4()?; // chunk size (unused)
2214    let Some(data) = reader.read_bytes()? else {
2215        return Ok(None);
2216    };
2217    reader.read_bytes()?; // LOB locator (unused)
2218    if data.is_empty() {
2219        return Ok(None);
2220    }
2221    let vector = crate::vector::decode_vector_with_limits(&data, reader.limits())?;
2222    Ok(Some(QueryValue::Vector(Box::new(vector))))
2223}
2224
2225/// Parses a native JSON (`DB_TYPE_JSON`) column value. Like VECTOR, OSON is sent
2226/// as a fully-prefetched LOB: `num_bytes`, `size`, `chunk_size`, the OSON image,
2227/// then a (discarded) LOB locator (reference packet.pyx `read_oson`).
2228pub(crate) fn parse_json_value(reader: &mut TtcReader<'_>) -> Result<Option<QueryValue>> {
2229    let num_bytes = reader.read_ub4()?;
2230    reader.limits().check_response_bytes(num_bytes as usize)?;
2231    if num_bytes == 0 {
2232        return Ok(None);
2233    }
2234    reader.read_ub8()?; // size (unused)
2235    reader.read_ub4()?; // chunk size (unused)
2236    let Some(data) = reader.read_bytes()? else {
2237        return Ok(None);
2238    };
2239    reader.read_bytes()?; // LOB locator (unused)
2240    if data.is_empty() {
2241        return Ok(None);
2242    }
2243    let value = crate::oson::decode_oson_with_limits(&data, reader.limits())?;
2244    Ok(Some(QueryValue::Json(Box::new(value))))
2245}
2246
2247pub(crate) fn parse_object_value(
2248    reader: &mut TtcReader<'_>,
2249    metadata: &ColumnMetadata,
2250) -> Result<Option<QueryValue>> {
2251    let _toid = reader.read_bytes_with_length()?;
2252    let _oid = reader.read_bytes_with_length()?;
2253    let _snapshot = reader.read_bytes_with_length()?;
2254    let _version = reader.read_ub2()?;
2255    let num_bytes = reader.read_ub4()?;
2256    reader.limits().check_response_bytes(num_bytes as usize)?;
2257    reader.skip(2)?;
2258    if num_bytes == 0 {
2259        return Ok(None);
2260    }
2261    let Some(packed_data) = reader.read_bytes()? else {
2262        return Ok(None);
2263    };
2264    Ok(Some(QueryValue::Object(Box::new(ObjectValue {
2265        schema: metadata.object_schema.clone(),
2266        type_name: metadata.object_type_name.clone(),
2267        packed_data,
2268    }))))
2269}
2270
2271pub(crate) fn parse_cursor_value(reader: &mut TtcReader<'_>) -> Result<QueryValue> {
2272    reader.skip(1)?;
2273    let mut result = QueryResult::default();
2274    parse_describe_info(reader, ClientCapabilities::default(), &mut result)?;
2275    let cursor_id = u32::from(reader.read_ub2()?);
2276    Ok(QueryValue::Cursor(Box::new(CursorValue {
2277        columns: result.columns,
2278        cursor_id,
2279    })))
2280}
2281
2282pub(crate) struct QueryReturnParameters {
2283    pub row_counts: Option<Vec<u64>>,
2284    /// CQN registered-query id extracted from the registration-info block
2285    /// (reference base.pyx:1300-1309); `None` when no block was present.
2286    pub query_id: Option<u64>,
2287}
2288
2289pub(crate) fn parse_query_return_parameters(
2290    reader: &mut TtcReader<'_>,
2291    arraydmlrowcounts: bool,
2292) -> Result<QueryReturnParameters> {
2293    let num_params = reader.read_ub2()?;
2294    for _ in 0..num_params {
2295        let _value = reader.read_ub4()?;
2296    }
2297    let num_bytes = reader.read_ub2()?;
2298    if num_bytes > 0 {
2299        reader.skip(usize::from(num_bytes))?;
2300    }
2301    let num_pairs = reader.read_ub2()?;
2302    skip_keyword_value_pairs(reader, num_pairs)?;
2303    // registration info block: the trailing 8 bytes (msb at -4, lsb at -8) are
2304    // the CQN query id when a registration id was sent (reference base.pyx).
2305    let num_bytes = usize::from(reader.read_ub2()?);
2306    let mut query_id = None;
2307    if num_bytes > 0 {
2308        let block = reader.read_raw(num_bytes)?;
2309        if num_bytes >= 8 {
2310            let msb = u32::from_be_bytes([
2311                block[num_bytes - 4],
2312                block[num_bytes - 3],
2313                block[num_bytes - 2],
2314                block[num_bytes - 1],
2315            ]);
2316            let lsb = u32::from_be_bytes([
2317                block[num_bytes - 8],
2318                block[num_bytes - 7],
2319                block[num_bytes - 6],
2320                block[num_bytes - 5],
2321            ]);
2322            query_id = Some((u64::from(msb) << 32) | u64::from(lsb));
2323        }
2324    }
2325    if arraydmlrowcounts {
2326        // reference messages/base.pyx `_process_return_parameters` tail
2327        let num_rows = reader.read_ub4()?;
2328        reader.limits().check_batch_rows(num_rows as usize)?;
2329        // Each ub8 row count consumes at least one byte, so cap the reservation
2330        // by the remaining payload size (BoundedReader).
2331        let mut row_counts: Vec<u64> =
2332            reader.with_capacity_limited(num_rows as usize, 1, ProtocolLimits::check_batch_rows)?;
2333        for _ in 0..num_rows {
2334            row_counts.push(reader.read_ub8()?);
2335        }
2336        return Ok(QueryReturnParameters {
2337            row_counts: Some(row_counts),
2338            query_id,
2339        });
2340    }
2341    Ok(QueryReturnParameters {
2342        row_counts: None,
2343        query_id,
2344    })
2345}
2346
2347#[cfg(test)]
2348mod return_parameter_tests {
2349    use super::*;
2350
2351    #[test]
2352    fn registration_info_block_extracts_query_id_from_lsb_msb_tail() {
2353        let mut writer = TtcWriter::new();
2354        writer.write_ub2(0); // num params
2355        writer.write_ub2(0); // parameter bytes
2356        writer.write_ub2(0); // keyword/value pairs
2357        writer.write_ub2(8); // registration-info block bytes
2358        writer.write_raw(&[
2359            0x55, 0x66, 0x77, 0x88, // lsb
2360            0x11, 0x22, 0x33, 0x44, // msb
2361        ]);
2362        let payload = writer.into_bytes();
2363        let mut reader = TtcReader::new(&payload);
2364
2365        let params = parse_query_return_parameters(&mut reader, false).expect("return parameters");
2366
2367        assert_eq!(params.query_id, Some(0x1122_3344_5566_7788));
2368        assert_eq!(params.row_counts, None);
2369    }
2370}
2371
2372#[cfg(test)]
2373mod mutation_decode_tests {
2374    use super::*;
2375    use crate::oson::{encode_oson, OsonValue};
2376    use crate::vector::{encode_vector, Vector, VectorValues};
2377
2378    fn caps(ttc_field_version: u8) -> ClientCapabilities {
2379        ClientCapabilities {
2380            ttc_field_version,
2381            max_string_size: 32_767,
2382            charset_id: 873,
2383        }
2384    }
2385
2386    fn column(name: &str, ora_type_num: u8, csfrm: u8, buffer_size: u32) -> ColumnMetadata {
2387        ColumnMetadata {
2388            name: name.to_string(),
2389            ora_type_num,
2390            csfrm,
2391            buffer_size,
2392            max_size: buffer_size,
2393            nulls_allowed: true,
2394            ..ColumnMetadata::default()
2395        }
2396    }
2397
2398    struct ColumnRecord<'a> {
2399        name: &'a str,
2400        ora_type_num: u8,
2401        csfrm: u8,
2402        buffer_size: u32,
2403        max_size: u32,
2404        uds_flags: u32,
2405        vector_dimensions: Option<u32>,
2406        vector_format: u8,
2407        vector_flags: u8,
2408        object_schema: Option<&'a str>,
2409        object_type_name: Option<&'a str>,
2410        annotations: &'a [(&'a str, &'a str)],
2411    }
2412
2413    impl<'a> ColumnRecord<'a> {
2414        fn scalar(name: &'a str, ora_type_num: u8, csfrm: u8, buffer_size: u32) -> Self {
2415            Self {
2416                name,
2417                ora_type_num,
2418                csfrm,
2419                buffer_size,
2420                max_size: buffer_size,
2421                uds_flags: 0,
2422                vector_dimensions: None,
2423                vector_format: 0,
2424                vector_flags: 0,
2425                object_schema: None,
2426                object_type_name: None,
2427                annotations: &[],
2428            }
2429        }
2430
2431        fn write(&self, writer: &mut TtcWriter, field_version: u8) {
2432            writer.write_u8(self.ora_type_num);
2433            writer.write_u8(0); // flags
2434            writer.write_u8(7); // precision
2435            writer.write_u8(2); // scale
2436            writer.write_ub4(self.buffer_size);
2437            writer.write_ub4(0); // max array elements
2438            writer.write_ub8(0); // cont flags
2439            writer
2440                .write_bytes_with_two_lengths(None)
2441                .expect("column oid");
2442            writer.write_ub2(0); // version
2443            writer.write_ub2(0); // server charset id
2444            writer.write_u8(self.csfrm);
2445            writer.write_ub4(self.max_size);
2446            if field_version >= TNS_CCAP_FIELD_VERSION_12_2 {
2447                writer.write_ub4(0x1122_3344); // oaccolid
2448            }
2449            writer.write_u8(1); // nullable
2450            writer.write_u8(0); // flags
2451            writer
2452                .write_bytes_with_two_lengths(Some(self.name.as_bytes()))
2453                .expect("column name");
2454            writer
2455                .write_bytes_with_two_lengths(self.object_schema.map(str::as_bytes))
2456                .expect("object schema");
2457            writer
2458                .write_bytes_with_two_lengths(self.object_type_name.map(str::as_bytes))
2459                .expect("object type");
2460            writer.write_ub2(1); // column position
2461            writer.write_ub4(self.uds_flags);
2462            if field_version >= TNS_CCAP_FIELD_VERSION_23_1 {
2463                writer
2464                    .write_bytes_with_two_lengths(Some(b"DOMSCHEMA"))
2465                    .expect("domain schema");
2466                writer
2467                    .write_bytes_with_two_lengths(Some(b"DOMNAME"))
2468                    .expect("domain name");
2469            }
2470            if field_version >= TNS_CCAP_FIELD_VERSION_23_1_EXT_3 {
2471                writer.write_ub4(u32::try_from(self.annotations.len()).expect("annotation count"));
2472                if !self.annotations.is_empty() {
2473                    writer.write_u8(0); // marker
2474                    writer.write_ub4(
2475                        u32::try_from(self.annotations.len()).expect("annotation count"),
2476                    );
2477                    writer.write_u8(0); // marker
2478                    for &(key, value) in self.annotations {
2479                        writer
2480                            .write_bytes_with_two_lengths(Some(key.as_bytes()))
2481                            .expect("annotation key");
2482                        writer
2483                            .write_bytes_with_two_lengths(Some(value.as_bytes()))
2484                            .expect("annotation value");
2485                        writer.write_ub4(0); // annotation flags
2486                    }
2487                    writer.write_ub4(0); // annotation block flags
2488                }
2489            }
2490            if field_version >= TNS_CCAP_FIELD_VERSION_23_4 {
2491                writer.write_ub4(self.vector_dimensions.unwrap_or(0));
2492                writer.write_u8(self.vector_format);
2493                writer.write_u8(self.vector_flags);
2494            }
2495        }
2496    }
2497
2498    fn write_describe_body(
2499        writer: &mut TtcWriter,
2500        field_version: u8,
2501        columns: &[ColumnRecord<'_>],
2502    ) {
2503        writer.write_ub4(4096); // max row size
2504        writer.write_ub4(u32::try_from(columns.len()).expect("column count"));
2505        if !columns.is_empty() {
2506            writer.write_u8(0); // describe column marker
2507        }
2508        for column in columns {
2509            column.write(writer, field_version);
2510        }
2511        writer
2512            .write_bytes_with_two_lengths(None)
2513            .expect("current date");
2514        writer.write_ub4(0); // dcbflag
2515        writer.write_ub4(0); // dcbmdbz
2516        writer.write_ub4(0); // dcbmnpr
2517        writer.write_ub4(0); // dcbmxpr
2518        writer.write_bytes_with_two_lengths(None).expect("dcbqcky");
2519    }
2520
2521    fn write_io_vector_body(
2522        writer: &mut TtcWriter,
2523        directions: &[u8],
2524        fast_fetch_bytes: &[u8],
2525        rowid_bytes: &[u8],
2526    ) {
2527        writer.write_u8(0); // flags
2528        writer.write_ub2(u16::try_from(directions.len()).expect("direction count"));
2529        writer.write_ub4(0); // high num-binds chunk
2530        writer.write_ub4(1); // iterations this time
2531        writer.write_ub2(0); // uac buffer length
2532        writer.write_ub2(u16::try_from(fast_fetch_bytes.len()).expect("fast fetch len"));
2533        writer.write_raw(fast_fetch_bytes);
2534        writer.write_ub2(u16::try_from(rowid_bytes.len()).expect("rowid len"));
2535        writer.write_raw(rowid_bytes);
2536        for &direction in directions {
2537            writer.write_u8(direction);
2538        }
2539    }
2540
2541    struct ErrorInfo<'a> {
2542        number: u32,
2543        message: &'a str,
2544        cursor_id: u16,
2545        row_count: u64,
2546        call_status: u32,
2547        warning_flags: u8,
2548        rowid: Option<(u32, u16, u32, u16)>,
2549    }
2550
2551    fn write_error_info(writer: &mut TtcWriter, info: ErrorInfo<'_>) {
2552        writer.write_ub4(info.call_status);
2553        writer.write_ub2(0); // seq
2554        writer.write_ub4(0); // current row
2555        writer.write_ub2(0); // obsolete short error number
2556        writer.write_ub2(0); // array elem error 1
2557        writer.write_ub2(0); // array elem error 2
2558        writer.write_ub2(info.cursor_id);
2559        writer.write_sb4(0); // error position
2560        writer.write_raw(&[0u8; 5]);
2561        writer.write_u8(info.warning_flags);
2562        let (rba, partition_id, block_num, slot_num) = info.rowid.unwrap_or((0, 0, 0, 0));
2563        writer.write_ub4(rba);
2564        writer.write_ub2(partition_id);
2565        writer.write_u8(0);
2566        writer.write_ub4(block_num);
2567        writer.write_ub2(slot_num);
2568        writer.write_ub4(0); // os error
2569        writer.write_raw(&[0u8; 2]);
2570        writer.write_ub2(0); // padding
2571        writer.write_ub4(0); // success iterations
2572        writer
2573            .write_bytes_with_two_lengths(None)
2574            .expect("diagnostic field");
2575        writer.write_ub2(0); // batch error count
2576        writer.write_ub4(0); // batch offset count
2577        writer.write_ub2(0); // batch message count
2578        writer.write_ub4(info.number);
2579        writer.write_ub8(info.row_count);
2580        writer.write_ub4(0); // sql type (field version >= 20.1)
2581        writer.write_ub4(0); // server checksum
2582        if info.number != 0 {
2583            writer
2584                .write_bytes_with_length(info.message.as_bytes())
2585                .expect("server error message");
2586        }
2587    }
2588
2589    fn write_return_parameters(
2590        writer: &mut TtcWriter,
2591        registration_block: &[u8],
2592        row_counts: Option<&[u64]>,
2593    ) {
2594        writer.write_ub2(0); // num params
2595        writer.write_ub2(0); // parameter bytes
2596        writer.write_ub2(0); // keyword/value pairs
2597        writer.write_ub2(u16::try_from(registration_block.len()).expect("registration len"));
2598        writer.write_raw(registration_block);
2599        if let Some(row_counts) = row_counts {
2600            writer.write_ub4(u32::try_from(row_counts.len()).expect("row count len"));
2601            for &count in row_counts {
2602                writer.write_ub8(count);
2603            }
2604        }
2605    }
2606
2607    fn write_prefetched_lob_value(writer: &mut TtcWriter, data: &[u8]) {
2608        writer.write_ub4(u32::try_from(data.len()).expect("data len"));
2609        writer.write_ub8(u64::try_from(data.len()).expect("data size"));
2610        writer.write_ub4(8192);
2611        writer
2612            .write_bytes_with_length(data)
2613            .expect("prefetched data");
2614        writer.write_bytes_with_length(b"locator").expect("locator");
2615    }
2616
2617    fn write_physical_urowid_cell(writer: &mut TtcWriter) -> String {
2618        let rba: u32 = 0x0102_0304;
2619        let partition_id: u16 = 0x0506;
2620        let block_num: u32 = 0x0708_090a;
2621        let slot_num: u16 = 0x0b0c;
2622        let mut encoded = Vec::new();
2623        encoded.push(1);
2624        encoded.extend_from_slice(&rba.to_be_bytes());
2625        encoded.extend_from_slice(&partition_id.to_be_bytes());
2626        encoded.extend_from_slice(&block_num.to_be_bytes());
2627        encoded.extend_from_slice(&slot_num.to_be_bytes());
2628        writer.write_bytes_with_length(&[1]).expect("urowid probe");
2629        writer
2630            .write_bytes_with_length(&encoded)
2631            .expect("physical urowid");
2632        encode_physical_rowid(rba, partition_id, block_num, slot_num)
2633    }
2634
2635    #[test]
2636    fn response_wrappers_parse_out_binds_returning_status_token_and_error_state() {
2637        let binds = [
2638            BindValue::InOut {
2639                value: Box::new(BindValue::Text("in".to_string())),
2640                out_buffer_size: 20,
2641            },
2642            BindValue::ReturnOutput {
2643                ora_type_num: ORA_TYPE_NUM_VARCHAR,
2644                csfrm: CS_FORM_IMPLICIT,
2645                buffer_size: 20,
2646            },
2647        ];
2648
2649        let mut out_payload = TtcWriter::new();
2650        out_payload.write_u8(TNS_MSG_TYPE_IO_VECTOR);
2651        write_io_vector_body(&mut out_payload, &[16, TNS_BIND_DIR_INPUT], b"F", b"R");
2652        out_payload.write_u8(TNS_MSG_TYPE_ROW_DATA);
2653        out_payload
2654            .write_bytes_with_length(b"OUT")
2655            .expect("out bind value");
2656        out_payload.write_sb4(0);
2657        out_payload.write_u8(TNS_MSG_TYPE_STATUS);
2658        out_payload.write_ub4(TNS_EOCS_FLAGS_TXN_IN_PROGRESS);
2659        out_payload.write_ub2(0);
2660        out_payload.write_u8(TNS_MSG_TYPE_SERVER_SIDE_PIGGYBACK);
2661        out_payload.write_u8(TNS_SERVER_PIGGYBACK_QUERY_CACHE_INVALIDATION);
2662        out_payload.write_u8(TNS_MSG_TYPE_TOKEN);
2663        out_payload.write_ub8(0x1122_3344_5566_7788);
2664        out_payload.write_u8(TNS_MSG_TYPE_ERROR);
2665        write_error_info(
2666            &mut out_payload,
2667            ErrorInfo {
2668                number: 0,
2669                message: "",
2670                cursor_id: 77,
2671                row_count: 3,
2672                call_status: 0,
2673                warning_flags: 0x20,
2674                rowid: Some((1, 2, 3, 4)),
2675            },
2676        );
2677
2678        let result = parse_query_response_with_binds_options_and_columns(
2679            &out_payload.into_bytes(),
2680            caps(TNS_CCAP_FIELD_VERSION_23_4),
2681            &binds,
2682            ExecuteOptions::default(),
2683            &[],
2684        )
2685        .expect("owned response with OUT bind/status/token/error");
2686        assert_eq!(
2687            result.out_values,
2688            vec![(0, Some(QueryValue::Text("OUT".to_string())))]
2689        );
2690        assert_eq!(result.txn_in_progress, Some(false));
2691        assert_eq!(result.cursor_id, 77);
2692        assert_eq!(result.row_count, 3);
2693        assert!(result.compilation_error_warning);
2694        assert_eq!(result.last_rowid, Some(encode_physical_rowid(1, 2, 3, 4)));
2695        assert_eq!(result.token_num, Some(0x1122_3344_5566_7788));
2696
2697        let mut returning_payload = TtcWriter::new();
2698        returning_payload.write_u8(TNS_MSG_TYPE_ROW_DATA);
2699        returning_payload.write_ub4(2);
2700        returning_payload
2701            .write_bytes_with_length(b"A")
2702            .expect("returning value 1");
2703        returning_payload.write_sb4(0);
2704        returning_payload
2705            .write_bytes_with_length(b"B")
2706            .expect("returning value 2");
2707        returning_payload.write_sb4(0);
2708        returning_payload.write_u8(TNS_MSG_TYPE_END_OF_RESPONSE);
2709
2710        let returning = parse_query_response_with_binds_options_columns_and_limits(
2711            &returning_payload.into_bytes(),
2712            caps(TNS_CCAP_FIELD_VERSION_23_4),
2713            &binds,
2714            ExecuteOptions::default(),
2715            &[],
2716            ProtocolLimits::DEFAULT,
2717        )
2718        .expect("owned response with RETURNING values");
2719        assert_eq!(
2720            returning.return_values,
2721            vec![(
2722                1,
2723                vec![
2724                    Some(QueryValue::Text("A".to_string())),
2725                    Some(QueryValue::Text("B".to_string()))
2726                ]
2727            )]
2728        );
2729    }
2730
2731    #[test]
2732    fn response_flush_breaks_before_trailing_unknown_message() {
2733        let payload = [TNS_MSG_TYPE_FLUSH_OUT_BINDS, 0xff];
2734        parse_query_response(&payload, caps(TNS_CCAP_FIELD_VERSION_23_4))
2735            .expect("flush should stop response parsing before trailing bytes");
2736    }
2737
2738    #[test]
2739    fn no_data_error_marks_more_rows_false_when_columns_are_known() {
2740        let mut payload = TtcWriter::new();
2741        payload.write_u8(TNS_MSG_TYPE_ERROR);
2742        write_error_info(
2743            &mut payload,
2744            ErrorInfo {
2745                number: TNS_ERR_NO_DATA_FOUND,
2746                message: "ORA-01403: no data found",
2747                cursor_id: 91,
2748                row_count: 11,
2749                call_status: TNS_EOCS_FLAGS_TXN_IN_PROGRESS,
2750                warning_flags: 0,
2751                rowid: None,
2752            },
2753        );
2754        let columns = [column("C", ORA_TYPE_NUM_VARCHAR, CS_FORM_IMPLICIT, 30)];
2755
2756        let result = parse_query_response_with_context(
2757            &payload.into_bytes(),
2758            caps(TNS_CCAP_FIELD_VERSION_23_4),
2759            &columns,
2760            None,
2761        )
2762        .expect("no-data end-of-call should finalize fetch");
2763
2764        assert!(!result.more_rows);
2765        assert_eq!(result.cursor_id, 91);
2766        assert_eq!(result.row_count, 11);
2767        assert_eq!(result.txn_in_progress, Some(true));
2768    }
2769
2770    #[test]
2771    fn io_vector_boundaries_skip_zero_and_one_byte_payloads_and_filter_returning() {
2772        let mut zero = TtcWriter::new();
2773        write_io_vector_body(&mut zero, &[16, 48, TNS_BIND_DIR_INPUT], &[], &[]);
2774        let zero_bytes = zero.into_bytes();
2775        let mut reader = TtcReader::new(&zero_bytes);
2776        assert_eq!(
2777            parse_io_vector(&mut reader, 2).expect("zero skips"),
2778            vec![0, 1]
2779        );
2780        assert_eq!(reader.remaining(), 0);
2781
2782        let mut one = TtcWriter::new();
2783        write_io_vector_body(&mut one, &[TNS_BIND_DIR_INPUT, 16, 48], b"X", b"Y");
2784        let one_bytes = one.into_bytes();
2785        let mut reader = TtcReader::new(&one_bytes);
2786        assert_eq!(
2787            parse_io_vector(&mut reader, 2).expect("one-byte skips"),
2788            vec![1]
2789        );
2790        assert_eq!(reader.remaining(), 0);
2791    }
2792
2793    #[test]
2794    fn describe_and_column_metadata_decode_zero_columns_annotations_flags_and_vector() {
2795        let field_version = TNS_CCAP_FIELD_VERSION_23_4;
2796
2797        let mut zero_body = TtcWriter::new();
2798        write_describe_body(&mut zero_body, field_version, &[]);
2799        let zero_bytes = zero_body.into_bytes();
2800        let mut zero_reader = TtcReader::new(&zero_bytes);
2801        let mut zero_result = QueryResult::default();
2802        parse_describe_info(&mut zero_reader, caps(field_version), &mut zero_result)
2803            .expect("zero-column describe");
2804        assert!(zero_result.columns.is_empty());
2805        assert_eq!(zero_reader.remaining(), 0);
2806
2807        let annotated_vector = ColumnRecord {
2808            name: "VEC",
2809            ora_type_num: ORA_TYPE_NUM_VECTOR,
2810            csfrm: CS_FORM_IMPLICIT,
2811            buffer_size: 16,
2812            max_size: 32,
2813            uds_flags: TNS_UDS_FLAGS_IS_JSON | TNS_UDS_FLAGS_IS_OSON,
2814            vector_dimensions: Some(1536),
2815            vector_format: 2,
2816            vector_flags: 0xa5,
2817            object_schema: None,
2818            object_type_name: None,
2819            annotations: &[("purpose", "mutation")],
2820        };
2821        let mut one_body = TtcWriter::new();
2822        write_describe_body(&mut one_body, field_version, &[annotated_vector]);
2823        let one_bytes = one_body.into_bytes();
2824        let mut one_reader = TtcReader::new(&one_bytes);
2825        let mut one_result = QueryResult::default();
2826        parse_describe_info(&mut one_reader, caps(field_version), &mut one_result)
2827            .expect("one-column describe");
2828        let meta = one_result.columns.first().expect("described column");
2829        assert_eq!(meta.name(), "VEC");
2830        assert_eq!(meta.buffer_size(), 16);
2831        assert_eq!(meta.max_size(), 32);
2832        assert!(meta.is_json());
2833        assert!(meta.is_oson());
2834        assert_eq!(meta.domain_schema(), Some("DOMSCHEMA"));
2835        assert_eq!(meta.domain_name(), Some("DOMNAME"));
2836        assert_eq!(
2837            meta.annotations(),
2838            Some(&[("purpose".to_string(), "mutation".to_string())][..])
2839        );
2840        assert_eq!(meta.vector_dimensions(), Some(1536));
2841        assert_eq!(meta.vector_format(), 2);
2842        assert_eq!(meta.vector_flags(), 0xa5);
2843        assert_eq!(one_reader.remaining(), 0);
2844    }
2845
2846    #[test]
2847    fn row_header_returns_embedded_bit_vector() {
2848        let mut writer = TtcWriter::new();
2849        writer.write_u8(0); // skip
2850        writer.write_ub2(1); // requests
2851        writer.write_ub4(2); // iteration
2852        writer.write_ub4(3); // num iters
2853        writer.write_ub2(4); // buffer length
2854        writer.write_ub4(1); // bit-vector bytes
2855        writer.write_u8(0); // marker
2856        writer.write_raw(&[0b1111_1101]);
2857        writer
2858            .write_bytes_with_two_lengths(Some(b"rid"))
2859            .expect("rxhrid");
2860        let bytes = writer.into_bytes();
2861        let mut reader = TtcReader::new(&bytes);
2862
2863        assert_eq!(
2864            parse_row_header(&mut reader).expect("row header"),
2865            Some(vec![0b1111_1101])
2866        );
2867        assert_eq!(reader.remaining(), 0);
2868    }
2869
2870    #[test]
2871    fn owned_column_value_decodes_scalar_and_cold_variants() {
2872        let scalar_cases: Vec<(ColumnMetadata, Vec<u8>, Option<QueryValue>)> = vec![
2873            (
2874                column("D", ORA_TYPE_NUM_BINARY_DOUBLE, CS_FORM_IMPLICIT, 8),
2875                encode_binary_double(-2.5).to_vec(),
2876                Some(QueryValue::BinaryDouble("-2.5".to_string())),
2877            ),
2878            (
2879                column("F", ORA_TYPE_NUM_BINARY_FLOAT, CS_FORM_IMPLICIT, 4),
2880                encode_binary_float(3.25).to_vec(),
2881                Some(QueryValue::BinaryDouble("3.25".to_string())),
2882            ),
2883            (
2884                column("B", ORA_TYPE_NUM_BOOLEAN, CS_FORM_IMPLICIT, 1),
2885                vec![0, 1],
2886                Some(QueryValue::Boolean(true)),
2887            ),
2888            (
2889                column("DS", ORA_TYPE_NUM_INTERVAL_DS, CS_FORM_IMPLICIT, 11),
2890                encode_interval_ds(4, 5 * 3600 + 6 * 60 + 7, 890)
2891                    .expect("interval ds")
2892                    .to_vec(),
2893                Some(QueryValue::IntervalDS {
2894                    days: 4,
2895                    hours: 5,
2896                    minutes: 6,
2897                    seconds: 7,
2898                    fseconds: 890,
2899                }),
2900            ),
2901            (
2902                column("YM", ORA_TYPE_NUM_INTERVAL_YM, CS_FORM_IMPLICIT, 5),
2903                encode_interval_ym(-3, 2).expect("interval ym").to_vec(),
2904                Some(QueryValue::IntervalYM {
2905                    years: -3,
2906                    months: 2,
2907                }),
2908            ),
2909            (
2910                column("TS", ORA_TYPE_NUM_TIMESTAMP, CS_FORM_IMPLICIT, 11),
2911                encode_oracle_timestamp(2026, 7, 8, 9, 10, 11, 123_456_789).expect("timestamp"),
2912                Some(QueryValue::DateTime {
2913                    year: 2026,
2914                    month: 7,
2915                    day: 8,
2916                    hour: 9,
2917                    minute: 10,
2918                    second: 11,
2919                    nanosecond: 123_456_789,
2920                }),
2921            ),
2922        ];
2923
2924        for (metadata, bytes, expected) in scalar_cases {
2925            let mut writer = TtcWriter::new();
2926            writer
2927                .write_bytes_with_length(&bytes)
2928                .expect("framed scalar");
2929            let payload = writer.into_bytes();
2930            let mut reader = TtcReader::new(&payload);
2931            assert_eq!(
2932                parse_column_value(&mut reader, &metadata).expect("scalar decode"),
2933                expected,
2934                "owned scalar decode for {}",
2935                metadata.name()
2936            );
2937            assert_eq!(reader.remaining(), 0);
2938        }
2939
2940        let mut lob_writer = TtcWriter::new();
2941        lob_writer.write_ub4(3);
2942        lob_writer.write_ub8(3);
2943        lob_writer.write_ub4(8192);
2944        lob_writer
2945            .write_bytes_with_length(b"lob")
2946            .expect("lob locator");
2947        let lob_bytes = lob_writer.into_bytes();
2948        let mut reader = TtcReader::new(&lob_bytes);
2949        assert!(matches!(
2950            parse_column_value(
2951                &mut reader,
2952                &column("CLOB", ORA_TYPE_NUM_CLOB, CS_FORM_IMPLICIT, 4000)
2953            )
2954            .expect("lob decode"),
2955            Some(QueryValue::Lob(_))
2956        ));
2957
2958        let vector = Vector::Dense(VectorValues::Int8(vec![-1, 0, 7]));
2959        let vector_image = encode_vector(&vector);
2960        let mut writer = TtcWriter::new();
2961        write_prefetched_lob_value(&mut writer, &vector_image);
2962        let bytes = writer.into_bytes();
2963        let mut reader = TtcReader::new(&bytes);
2964        assert_eq!(
2965            parse_column_value(
2966                &mut reader,
2967                &column("VEC", ORA_TYPE_NUM_VECTOR, CS_FORM_IMPLICIT, 4000)
2968            )
2969            .expect("vector decode"),
2970            Some(QueryValue::Vector(Box::new(vector)))
2971        );
2972
2973        let json = OsonValue::Object(vec![("k".to_string(), OsonValue::String("v".to_string()))]);
2974        let json_image = encode_oson(&json, false).expect("encode oson");
2975        let mut writer = TtcWriter::new();
2976        write_prefetched_lob_value(&mut writer, &json_image);
2977        let bytes = writer.into_bytes();
2978        let mut reader = TtcReader::new(&bytes);
2979        assert_eq!(
2980            parse_column_value(
2981                &mut reader,
2982                &column("JSON", ORA_TYPE_NUM_JSON, CS_FORM_IMPLICIT, 4000)
2983            )
2984            .expect("json decode"),
2985            Some(QueryValue::Json(Box::new(json)))
2986        );
2987
2988        let object_metadata = ColumnMetadata {
2989            name: "OBJ".to_string(),
2990            ora_type_num: ORA_TYPE_NUM_OBJECT,
2991            buffer_size: 4000,
2992            object_schema: Some("S".to_string()),
2993            object_type_name: Some("T".to_string()),
2994            ..ColumnMetadata::default()
2995        };
2996        let mut writer = TtcWriter::new();
2997        writer
2998            .write_bytes_with_two_lengths(Some(b"toid"))
2999            .expect("toid");
3000        writer
3001            .write_bytes_with_two_lengths(Some(b"oid"))
3002            .expect("oid");
3003        writer
3004            .write_bytes_with_two_lengths(Some(b"snap"))
3005            .expect("snapshot");
3006        writer.write_ub2(1);
3007        writer.write_ub4(4);
3008        writer.write_raw(&[0, 0]);
3009        writer
3010            .write_bytes_with_length(b"PACK")
3011            .expect("object payload");
3012        let bytes = writer.into_bytes();
3013        let mut reader = TtcReader::new(&bytes);
3014        assert!(matches!(
3015            parse_column_value(&mut reader, &object_metadata).expect("object decode"),
3016            Some(QueryValue::Object(object))
3017                if object.schema.as_deref() == Some("S")
3018                    && object.type_name.as_deref() == Some("T")
3019                    && object.packed_data == b"PACK"
3020        ));
3021
3022        let mut writer = TtcWriter::new();
3023        writer.write_u8(0); // cursor flags
3024        write_describe_body(&mut writer, TNS_CCAP_FIELD_VERSION_23_4, &[]);
3025        writer.write_ub2(44);
3026        let bytes = writer.into_bytes();
3027        let mut reader = TtcReader::new(&bytes);
3028        assert!(matches!(
3029            parse_column_value(
3030                &mut reader,
3031                &column("CUR", ORA_TYPE_NUM_CURSOR, CS_FORM_IMPLICIT, 1)
3032            )
3033            .expect("cursor decode"),
3034            Some(QueryValue::Cursor(cursor)) if cursor.cursor_id == 44
3035        ));
3036    }
3037
3038    #[test]
3039    fn urowid_requires_full_physical_payload() {
3040        let mut truncated = TtcWriter::new();
3041        truncated
3042            .write_bytes_with_length(&[1])
3043            .expect("urowid probe");
3044        truncated
3045            .write_bytes_with_length(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
3046            .expect("truncated physical urowid");
3047        let truncated_bytes = truncated.into_bytes();
3048        let mut reader = TtcReader::new(&truncated_bytes);
3049        assert!(parse_urowid_value(&mut reader).is_err());
3050
3051        let mut ok = TtcWriter::new();
3052        let expected = write_physical_urowid_cell(&mut ok);
3053        let ok_bytes = ok.into_bytes();
3054        let mut reader = TtcReader::new(&ok_bytes);
3055        assert_eq!(
3056            parse_urowid_value(&mut reader).expect("physical urowid"),
3057            Some(expected)
3058        );
3059    }
3060
3061    #[test]
3062    fn rowid_parser_distinguishes_null_and_physical_rowid() {
3063        let null_payload = [0u8];
3064        let mut reader = TtcReader::new(&null_payload);
3065        assert_eq!(parse_rowid_value(&mut reader).expect("null ROWID"), None);
3066
3067        let rba: u32 = 0x0102_0304;
3068        let partition_id: u16 = 0x0506;
3069        let block_num: u32 = 0x0708_090a;
3070        let slot_num: u16 = 0x0b0c;
3071        let mut physical = TtcWriter::new();
3072        physical.write_u8(1);
3073        physical.write_ub4(rba);
3074        physical.write_ub2(partition_id);
3075        physical.write_u8(0);
3076        physical.write_ub4(block_num);
3077        physical.write_ub2(slot_num);
3078        let physical_bytes = physical.into_bytes();
3079        let mut reader = TtcReader::new(&physical_bytes);
3080
3081        assert_eq!(
3082            parse_rowid_value(&mut reader).expect("physical ROWID"),
3083            Some(encode_physical_rowid(
3084                rba,
3085                partition_id,
3086                block_num,
3087                slot_num
3088            ))
3089        );
3090        assert_eq!(reader.remaining(), 0);
3091    }
3092
3093    #[test]
3094    fn borrowed_slots_cover_scalar_variants_and_nchar_fallback() {
3095        let columns = vec![
3096            column("TEXT", ORA_TYPE_NUM_VARCHAR, CS_FORM_IMPLICIT, 20),
3097            column("NCHAR", ORA_TYPE_NUM_VARCHAR, CS_FORM_NCHAR, 20),
3098            column("RAW", ORA_TYPE_NUM_RAW, CS_FORM_IMPLICIT, 20),
3099            column("NUM", ORA_TYPE_NUM_NUMBER, CS_FORM_IMPLICIT, 22),
3100            column("BOOL", ORA_TYPE_NUM_BOOLEAN, CS_FORM_IMPLICIT, 1),
3101            column("DS", ORA_TYPE_NUM_INTERVAL_DS, CS_FORM_IMPLICIT, 11),
3102            column("YM", ORA_TYPE_NUM_INTERVAL_YM, CS_FORM_IMPLICIT, 5),
3103            column("TS", ORA_TYPE_NUM_TIMESTAMP, CS_FORM_IMPLICIT, 11),
3104        ];
3105        let mut writer = TtcWriter::new();
3106        writer.write_bytes_with_length(b"hello").expect("text");
3107        writer
3108            .write_bytes_with_length(&[0, b'H', 0, b'i'])
3109            .expect("nchar utf16");
3110        writer.write_bytes_with_length(&[0xde, 0xad]).expect("raw");
3111        let number = encode_number_text("123.45").expect("number");
3112        writer
3113            .write_bytes_with_length(&number)
3114            .expect("number cell");
3115        writer.write_bytes_with_length(&[1]).expect("boolean");
3116        writer
3117            .write_bytes_with_length(
3118                &encode_interval_ds(1, 2 * 3600 + 3 * 60 + 4, 5).expect("interval ds"),
3119            )
3120            .expect("interval ds cell");
3121        writer
3122            .write_bytes_with_length(&encode_interval_ym(6, 7).expect("interval ym"))
3123            .expect("interval ym cell");
3124        writer
3125            .write_bytes_with_length(
3126                &encode_oracle_timestamp(2026, 7, 8, 9, 10, 11, 0).expect("timestamp"),
3127            )
3128            .expect("timestamp cell");
3129        let buffer = writer.into_bytes();
3130        let ptr_range = {
3131            let start = buffer.as_ptr() as usize;
3132            start..start + buffer.len()
3133        };
3134        let batch = BorrowedRowBatch::new(buffer, columns, vec![0]);
3135
3136        batch
3137            .for_each_row_ref(|row| {
3138                assert!(matches!(row[0], Some(QueryValueRef::Text("hello"))));
3139                if let Some(QueryValueRef::Text(text)) = row[0] {
3140                    assert!(ptr_range.contains(&(text.as_ptr() as usize)));
3141                }
3142                assert_eq!(
3143                    row[1].as_ref().map(QueryValueRef::to_owned_value),
3144                    Some(QueryValue::Text("Hi".to_string()))
3145                );
3146                assert_eq!(
3147                    row[2].as_ref().and_then(|v| v.as_raw()),
3148                    Some(&[0xde, 0xad][..])
3149                );
3150                assert_eq!(
3151                    row[3].as_ref().and_then(|v| v.as_number_text()),
3152                    Some("123.45")
3153                );
3154                assert_eq!(
3155                    row[4].as_ref().map(QueryValueRef::to_owned_value),
3156                    Some(QueryValue::Boolean(true))
3157                );
3158                assert!(matches!(
3159                    row[5].as_ref().map(QueryValueRef::to_owned_value),
3160                    Some(QueryValue::IntervalDS {
3161                        days: 1,
3162                        hours: 2,
3163                        minutes: 3,
3164                        seconds: 4,
3165                        fseconds: 5
3166                    })
3167                ));
3168                assert_eq!(
3169                    row[6].as_ref().map(QueryValueRef::to_owned_value),
3170                    Some(QueryValue::IntervalYM {
3171                        years: 6,
3172                        months: 7
3173                    })
3174                );
3175                assert!(matches!(
3176                    row[7].as_ref().map(QueryValueRef::to_owned_value),
3177                    Some(QueryValue::DateTime {
3178                        year: 2026,
3179                        month: 7,
3180                        day: 8,
3181                        hour: 9,
3182                        minute: 10,
3183                        second: 11,
3184                        nanosecond: 0
3185                    })
3186                ));
3187                Ok::<(), ProtocolError>(())
3188            })
3189            .expect("borrowed scalar slots");
3190    }
3191
3192    #[test]
3193    fn borrowed_response_dispatches_messages_and_surfaces_non_default_state() {
3194        let field_version = TNS_CCAP_FIELD_VERSION_23_4;
3195        let mut payload = TtcWriter::new();
3196        payload.write_u8(0);
3197        payload.write_u8(TNS_MSG_TYPE_DESCRIBE_INFO);
3198        payload
3199            .write_bytes_with_length(b"describe")
3200            .expect("describe name");
3201        write_describe_body(
3202            &mut payload,
3203            field_version,
3204            &[ColumnRecord::scalar(
3205                "N",
3206                ORA_TYPE_NUM_NUMBER,
3207                CS_FORM_IMPLICIT,
3208                22,
3209            )],
3210        );
3211        payload.write_u8(TNS_MSG_TYPE_ROW_HEADER);
3212        payload.write_u8(0);
3213        payload.write_ub2(1);
3214        payload.write_ub4(1);
3215        payload.write_ub4(1);
3216        payload.write_ub2(0);
3217        payload.write_ub4(0);
3218        payload
3219            .write_bytes_with_two_lengths(None)
3220            .expect("row header rxhrid");
3221        payload.write_u8(TNS_MSG_TYPE_ROW_DATA);
3222        let number = encode_number_text("7").expect("number");
3223        payload
3224            .write_bytes_with_length(&number)
3225            .expect("row number");
3226        payload.write_u8(TNS_MSG_TYPE_PARAMETER);
3227        write_return_parameters(&mut payload, &[], None);
3228        payload.write_u8(TNS_MSG_TYPE_STATUS);
3229        payload.write_ub4(TNS_EOCS_FLAGS_TXN_IN_PROGRESS);
3230        payload.write_ub2(0);
3231        payload.write_u8(TNS_MSG_TYPE_SERVER_SIDE_PIGGYBACK);
3232        payload.write_u8(TNS_SERVER_PIGGYBACK_QUERY_CACHE_INVALIDATION);
3233        payload.write_u8(TNS_MSG_TYPE_IMPLICIT_RESULTSET);
3234        payload.write_ub4(0);
3235        payload.write_u8(TNS_MSG_TYPE_TOKEN);
3236        payload.write_ub8(99);
3237        payload.write_u8(TNS_MSG_TYPE_ERROR);
3238        write_error_info(
3239            &mut payload,
3240            ErrorInfo {
3241                number: TNS_ERR_NO_DATA_FOUND,
3242                message: "ORA-01403: no data found",
3243                cursor_id: 123,
3244                row_count: 1,
3245                call_status: 0,
3246                warning_flags: 0,
3247                rowid: None,
3248            },
3249        );
3250
3251        let borrowed =
3252            parse_query_response_borrowed(&payload.into_bytes(), caps(field_version), &[], None)
3253                .expect("borrowed response");
3254        assert!(!borrowed.more_rows);
3255        assert_eq!(borrowed.cursor_id, 123);
3256        assert_eq!(borrowed.row_count, 1);
3257        assert_eq!(borrowed.batch.columns()[0].name(), "N");
3258        assert_eq!(borrowed.batch.row_count(), 1);
3259        let mut rows = Vec::new();
3260        borrowed
3261            .batch
3262            .for_each_row_ref(|row| {
3263                rows.push(
3264                    row[0]
3265                        .as_ref()
3266                        .and_then(|value| value.as_number_text())
3267                        .map(str::to_string)
3268                        .unwrap_or_else(|| "<missing>".to_string()),
3269                );
3270                Ok::<(), ProtocolError>(())
3271            })
3272            .expect("iterate borrowed response");
3273        assert_eq!(rows, vec!["7".to_string()]);
3274    }
3275
3276    #[test]
3277    fn borrowed_response_uses_bit_vector_duplicates_and_breaks_on_flush() {
3278        let columns = vec![
3279            column("A", ORA_TYPE_NUM_NUMBER, CS_FORM_IMPLICIT, 22),
3280            column("B", ORA_TYPE_NUM_NUMBER, CS_FORM_IMPLICIT, 22),
3281        ];
3282        let previous_row = vec![
3283            Some(QueryValue::number_from_text("10", true)),
3284            Some(QueryValue::number_from_text("20", true)),
3285        ];
3286        let mut payload = TtcWriter::new();
3287        payload.write_u8(TNS_MSG_TYPE_BIT_VECTOR);
3288        payload.write_ub2(2);
3289        payload.write_raw(&[0b1111_1101]); // column 1 is duplicate
3290        payload.write_u8(TNS_MSG_TYPE_ROW_DATA);
3291        let value = encode_number_text("30").expect("number");
3292        payload
3293            .write_bytes_with_length(&value)
3294            .expect("first column");
3295        payload.write_u8(TNS_MSG_TYPE_FLUSH_OUT_BINDS);
3296        payload.write_u8(0xff);
3297
3298        let borrowed = parse_query_response_borrowed(
3299            &payload.into_bytes(),
3300            caps(TNS_CCAP_FIELD_VERSION_23_4),
3301            &columns,
3302            Some(&previous_row),
3303        )
3304        .expect("borrowed bit-vector response");
3305        assert_eq!(borrowed.batch.row_count(), 1);
3306        borrowed
3307            .batch
3308            .for_each_row_ref(|row| {
3309                assert_eq!(row[0].as_ref().and_then(|v| v.as_number_text()), Some("30"));
3310                assert_eq!(
3311                    row[1]
3312                        .as_ref()
3313                        .map(QueryValueRef::to_owned_value)
3314                        .and_then(|value| value.as_i64()),
3315                    Some(20)
3316                );
3317                Ok::<(), ProtocolError>(())
3318            })
3319            .expect("duplicate column resolution");
3320    }
3321
3322    #[test]
3323    fn return_parameters_decode_registration_block_and_row_counts() {
3324        let mut writer = TtcWriter::new();
3325        write_return_parameters(
3326            &mut writer,
3327            &[
3328                0xaa, 0xbb, 0xcc, 0xdd, // lsb
3329                0x11, 0x22, 0x33, 0x44, // msb
3330            ],
3331            Some(&[2, 0, 5]),
3332        );
3333        let payload = writer.into_bytes();
3334        let mut reader = TtcReader::new(&payload);
3335
3336        let params = parse_query_return_parameters(&mut reader, true).expect("return parameters");
3337
3338        assert_eq!(params.query_id, Some(0x1122_3344_aabb_ccdd));
3339        assert_eq!(params.row_counts, Some(vec![2, 0, 5]));
3340        assert_eq!(reader.remaining(), 0);
3341    }
3342}
3343
3344#[cfg(test)]
3345mod batch_error_continuation_tests {
3346    use super::*;
3347
3348    // Offline proof of the `executemany(batcherrors=True, arraydmlrowcounts=True)`
3349    // continuation contract (bead a4-j1w / rust-oracledb iec3.1.13). The server
3350    // does NOT abort on the first bad row: it processes the whole bind array,
3351    // returns a per-iteration affected-row-count vector, and reports the failing
3352    // rows through the ORA-24381 batch-error arrays instead of raising a fatal
3353    // error (reference impl/thin/messages/base.pyx `_process_error_info`). This
3354    // exercises the REAL wire decoder end to end offline — no live array DML — and
3355    // asserts continuation (every iteration accounted for), the per-row error map
3356    // (each failure keyed to its input-row offset), and that the surviving rows
3357    // still report their commit counts.
3358
3359    /// Writes the `_process_return_parameters` block (num-params / parameter-bytes
3360    /// / keyword-pairs / registration-block, all empty) followed by the
3361    /// `arraydmlrowcounts` tail carrying one affected-row count per bind row.
3362    fn write_return_parameters_with_row_counts(w: &mut TtcWriter, row_counts: &[u64]) {
3363        w.write_ub2(0); // num params
3364        w.write_ub2(0); // parameter bytes
3365        w.write_ub2(0); // keyword/value pairs
3366        w.write_ub2(0); // registration-info block bytes
3367        w.write_ub4(u32::try_from(row_counts.len()).expect("row count fits u32"));
3368        for &count in row_counts {
3369            w.write_ub8(count);
3370        }
3371    }
3372
3373    /// Writes an `ORA-24381` server-error-info record whose batch-error code /
3374    /// offset / message arrays mirror `parse_server_error_info`. The fixed header
3375    /// matches the parser field-for-field (same shape as the errors.rs boundary
3376    /// fixture); the arrays use the short (non-`0xfe`) packed-length form.
3377    fn write_batch_error_info(w: &mut TtcWriter, errors: &[(u32, u32, &str)]) {
3378        // --- fixed header (mirrors parse_server_error_info) ---
3379        w.write_ub4(0); // call status
3380        w.write_ub2(0); // seq
3381        w.write_ub4(0); // current row
3382        w.write_ub2(0); // error number (obsolete short)
3383        w.write_ub2(0); // array elem error 1
3384        w.write_ub2(0); // array elem error 2
3385        w.write_ub2(0); // cursor id
3386        w.write_sb4(0); // error position
3387        w.write_raw(&[0u8; 5]); // skip(5)
3388        w.write_u8(0); // warning flags
3389                       // rowid: ub4 rba, ub2 partition, skip(1), ub4 block, ub2 slot
3390        w.write_ub4(0);
3391        w.write_ub2(0);
3392        w.write_u8(0);
3393        w.write_ub4(0);
3394        w.write_ub2(0);
3395        w.write_ub4(0); // os error
3396        w.write_raw(&[0u8; 2]); // skip(2)
3397        w.write_ub2(0); // padding
3398        w.write_ub4(0); // success iters
3399        w.write_bytes_with_two_lengths(None)
3400            .expect("empty rowid-diagnostic field"); // read_bytes_with_length
3401
3402        let count = errors.len();
3403        // --- batch error CODE array ---
3404        w.write_ub2(u16::try_from(count).expect("error count fits u16"));
3405        if count > 0 {
3406            w.write_u8(u8::try_from(count).expect("packed length fits u8")); // != 0xfe
3407            for &(code, _, _) in errors {
3408                w.write_ub2(u16::try_from(code).expect("ORA code fits u16"));
3409            }
3410        }
3411        // --- batch error OFFSET (input-row index) array ---
3412        w.write_ub4(u32::try_from(count).expect("offset count fits u32"));
3413        if count > 0 {
3414            w.write_u8(u8::try_from(count).expect("packed length fits u8"));
3415            for &(_, offset, _) in errors {
3416                w.write_ub4(offset);
3417            }
3418        }
3419        // --- batch error MESSAGE array ---
3420        w.write_ub2(u16::try_from(count).expect("message count fits u16"));
3421        if count > 0 {
3422            w.write_u8(0); // packed size (parser skips 1)
3423            for &(_, _, message) in errors {
3424                w.write_ub2(u16::try_from(message.len()).expect("message len fits u16")); // discarded chunk len
3425                w.write_bytes_with_length(message.as_bytes())
3426                    .expect("batch error message");
3427                w.write_raw(&[0u8, 0u8]); // 2-byte end marker
3428            }
3429        }
3430        // --- trailing error number / row count / (20.1+) sql-type+checksum / message ---
3431        w.write_ub4(TNS_ERR_ARRAY_DML_ERRORS);
3432        w.write_ub8(0); // row count
3433                        // ClientCapabilities::default() negotiates ttc field version 24 (>= 20.1),
3434                        // so a modern server sends the sql-type + server-checksum pair here
3435                        // (reference messages/base.pyx:238). Emit it so the summary message frames.
3436        w.write_ub4(0); // sql type
3437        w.write_ub4(0); // server checksum
3438        w.write_bytes_with_length(b"ORA-24381: error(s) in array DML operation")
3439            .expect("summary message");
3440    }
3441
3442    #[test]
3443    fn execute_response_decodes_batch_error_continuation_and_row_counts() {
3444        // A 5-row batch where input rows 1 and 3 violate constraints; rows 0, 2
3445        // and 4 commit. A pre-fix / abort-on-first-error server would stop at
3446        // row 1 — the row-count vector and the SECOND error prove it did not.
3447        let row_counts = [1u64, 0, 1, 0, 1];
3448        let errors: [(u32, u32, &str); 2] = [
3449            (1, 1, "ORA-00001: unique constraint (X.PK) violated"),
3450            (
3451                1400,
3452                3,
3453                "ORA-01400: cannot insert NULL into (\"X\".\"T\".\"C\")",
3454            ),
3455        ];
3456
3457        let mut writer = TtcWriter::new();
3458        writer.write_u8(TNS_MSG_TYPE_PARAMETER);
3459        write_return_parameters_with_row_counts(&mut writer, &row_counts);
3460        writer.write_u8(TNS_MSG_TYPE_ERROR);
3461        write_batch_error_info(&mut writer, &errors);
3462        let payload = writer.into_bytes();
3463
3464        let options = ExecuteOptions::default().with_arraydmlrowcounts(true);
3465        let result = parse_query_response_with_binds_and_options(
3466            &payload,
3467            ClientCapabilities::default(),
3468            &[],
3469            options,
3470        )
3471        .expect("batch-error execute response decodes");
3472
3473        // Continuation: every iteration is accounted for — the batch was NOT
3474        // aborted on the first failing row.
3475        assert_eq!(
3476            result.array_dml_row_counts.as_deref(),
3477            Some(row_counts.as_slice()),
3478            "per-iteration counts survive: committed rows report 1, failed rows 0"
3479        );
3480
3481        // Per-row error map: each failure is keyed to its input-row offset, with
3482        // its own ORA code and message (not coalesced, not misordered).
3483        assert_eq!(
3484            result.batch_errors.len(),
3485            2,
3486            "both failing rows are reported, proving continuation past row 1"
3487        );
3488        assert_eq!(result.batch_errors[0].code(), 1);
3489        assert_eq!(result.batch_errors[0].offset(), 1);
3490        assert_eq!(
3491            result.batch_errors[0].message(),
3492            "ORA-00001: unique constraint (X.PK) violated"
3493        );
3494        assert_eq!(result.batch_errors[1].code(), 1400);
3495        assert_eq!(result.batch_errors[1].offset(), 3);
3496        assert_eq!(
3497            result.batch_errors[1].message(),
3498            "ORA-01400: cannot insert NULL into (\"X\".\"T\".\"C\")"
3499        );
3500
3501        // The surviving (non-failing) rows still commit-count: 3 rows affected.
3502        let committed: u64 = result
3503            .array_dml_row_counts
3504            .as_deref()
3505            .expect("row counts present")
3506            .iter()
3507            .sum();
3508        assert_eq!(committed, 3, "the 3 non-failing rows committed");
3509    }
3510}
3511
3512#[cfg(test)]
3513mod borrowed_fetch_tests {
3514    use super::*;
3515    use crate::thin::codecs::encode_number_text;
3516
3517    // Isomorphism proof for the `simd-decode` feature (bead rust-oracledb-63o):
3518    // `validate_utf8` must make the SAME accept/reject decision as the canonical
3519    // `core::str::from_utf8`, AND return the same `&str` on accept, for every
3520    // input — whether or not the SIMD validator is compiled in. This guards the
3521    // hot text path against any divergence in UTF-8 grammar handling.
3522    #[test]
3523    fn validate_utf8_matches_core_accept_reject() {
3524        let cases: &[&[u8]] = &[
3525            b"",
3526            b"a",
3527            b"hello world",
3528            "VARCHAR2 cell".as_bytes(),
3529            "中文 mixed \u{1f600}".as_bytes(), // CJK + emoji (4-byte)
3530            "naïve café".as_bytes(),
3531            &[0x80],                   // lone continuation byte -> reject
3532            &[0xC0, 0x80],             // overlong NUL -> reject
3533            &[0xED, 0xA0, 0x80],       // UTF-16 surrogate -> reject
3534            &[0xF4, 0x90, 0x80, 0x80], // > U+10FFFF -> reject
3535            &[0xFF],                   // invalid lead -> reject
3536            &[0xE2, 0x82],             // truncated 3-byte -> reject
3537        ];
3538        for &bytes in cases {
3539            let core = core::str::from_utf8(bytes);
3540            let ours = validate_utf8(bytes);
3541            assert_eq!(
3542                core.is_ok(),
3543                ours.is_ok(),
3544                "accept/reject diverged for {bytes:02x?}"
3545            );
3546            if let (Ok(a), Ok(b)) = (core, ours) {
3547                assert_eq!(a, b, "accepted text diverged for {bytes:02x?}");
3548            }
3549        }
3550    }
3551
3552    // Build a synthetic column metadata for a scalar type.
3553    fn col(name: &str, ora_type_num: u8, csfrm: u8, buffer_size: u32) -> ColumnMetadata {
3554        ColumnMetadata {
3555            name: name.to_string(),
3556            ora_type_num,
3557            csfrm,
3558            buffer_size,
3559            ..ColumnMetadata::default()
3560        }
3561    }
3562
3563    // Encode one row of [Text, Number, Raw, NULL-text] as the server would frame
3564    // the column values (each a `write_bytes_with_length` run that `read_bytes`
3565    // / `read_bytes_borrowed` consume identically), and return the byte offset
3566    // where the row's column values begin.
3567    fn encode_mixed_row(writer: &mut TtcWriter, text: &str, number: &str, raw: &[u8]) {
3568        writer.write_bytes_with_length(text.as_bytes()).unwrap();
3569        let num = encode_number_text(number).unwrap();
3570        writer.write_bytes_with_length(&num).unwrap();
3571        writer.write_bytes_with_length(raw).unwrap();
3572        writer.write_u8(0); // NULL column (length byte 0)
3573    }
3574
3575    // The borrowed batch decode must yield, for every cell, a value whose
3576    // `to_owned_value()` is bit-for-bit the owned-path `QueryValue`, across a
3577    // mixed Text/Number/Raw/NULL row. And the Text/Raw cells must genuinely
3578    // borrow the batch buffer (zero-copy), not a fresh allocation.
3579    #[test]
3580    fn borrowed_batch_matches_owned_path_for_mixed_row() {
3581        let columns = vec![
3582            col("T", ORA_TYPE_NUM_VARCHAR, CS_FORM_IMPLICIT, 4000),
3583            col("N", ORA_TYPE_NUM_NUMBER, CS_FORM_IMPLICIT, 22),
3584            col("R", ORA_TYPE_NUM_RAW, CS_FORM_IMPLICIT, 2000),
3585            col("Z", ORA_TYPE_NUM_VARCHAR, CS_FORM_IMPLICIT, 4000),
3586        ];
3587
3588        let mut writer = TtcWriter::new();
3589        encode_mixed_row(
3590            &mut writer,
3591            "héllo world",
3592            "-12.5",
3593            &[0xDE, 0xAD, 0xBE, 0xEF],
3594        );
3595        encode_mixed_row(&mut writer, "second", "42", &[0x01]);
3596        let buffer = writer.into_bytes();
3597        let row_starts = vec![0, {
3598            // Find the second row's start by replaying the first row's consumption.
3599            let mut reader = TtcReader::new(&buffer);
3600            for c in &columns {
3601                let _ = parse_column_value(&mut reader, c).unwrap();
3602            }
3603            reader.position()
3604        }];
3605
3606        // Owned path: decode both rows the existing way for the golden values.
3607        let owned_rows: Vec<Vec<Option<QueryValue>>> = row_starts
3608            .iter()
3609            .map(|&start| {
3610                let mut reader = TtcReader::new(&buffer[start..]);
3611                columns
3612                    .iter()
3613                    .map(|c| parse_column_value(&mut reader, c).unwrap())
3614                    .collect()
3615            })
3616            .collect();
3617
3618        // Borrowed path: decode through the batch, collecting owned copies and
3619        // proving the scalar cells borrow the buffer.
3620        let batch = BorrowedRowBatch::new(buffer.clone(), columns.clone(), row_starts);
3621        let buf_ptr_range = batch.buffer_ptr_range();
3622
3623        let mut seen_rows = 0usize;
3624        let mut borrowed_owned: Vec<Vec<Option<QueryValue>>> = Vec::new();
3625        batch
3626            .for_each_row_ref(|row| {
3627                seen_rows += 1;
3628                // Text cell borrows the buffer.
3629                if let Some(QueryValueRef::Text(t)) = row[0] {
3630                    let p = t.as_ptr() as usize;
3631                    assert!(
3632                        buf_ptr_range.contains(&p),
3633                        "Text cell must borrow the batch buffer (zero-copy)"
3634                    );
3635                }
3636                // Raw cell borrows the buffer.
3637                if let Some(QueryValueRef::Raw(r)) = row[2] {
3638                    let p = r.as_ptr() as usize;
3639                    assert!(
3640                        buf_ptr_range.contains(&p),
3641                        "Raw cell must borrow the batch buffer (zero-copy)"
3642                    );
3643                }
3644                borrowed_owned.push(
3645                    row.iter()
3646                        .map(|cell| cell.map(|v| v.to_owned_value()))
3647                        .collect(),
3648                );
3649                Ok::<(), ProtocolError>(())
3650            })
3651            .unwrap();
3652
3653        assert_eq!(seen_rows, 2, "batch yields both rows");
3654        assert_eq!(
3655            borrowed_owned, owned_rows,
3656            "borrowed cells to_owned() must equal the owned-path values"
3657        );
3658    }
3659
3660    #[test]
3661    fn borrowed_number_to_owned_matches_owned_for_trailing_zero_number() {
3662        let column = col("N", ORA_TYPE_NUM_NUMBER, CS_FORM_IMPLICIT, 22);
3663        let number = encode_number_text("1000").expect("encode trailing-zero number");
3664        let mut writer = TtcWriter::new();
3665        writer
3666            .write_bytes_with_length(&number)
3667            .expect("write framed number");
3668        let buffer = writer.into_bytes();
3669
3670        let mut owned_reader = TtcReader::new(&buffer);
3671        let owned = parse_column_value(&mut owned_reader, &column)
3672            .expect("owned decode")
3673            .expect("owned number should be non-null");
3674
3675        let batch = BorrowedRowBatch::new(buffer, vec![column], vec![0]);
3676        let mut borrowed_owned = Vec::new();
3677        batch
3678            .for_each_row_ref(|row| {
3679                borrowed_owned.push(
3680                    row[0]
3681                        .expect("borrowed number should be non-null")
3682                        .to_owned_value(),
3683                );
3684                Ok::<(), ProtocolError>(())
3685            })
3686            .expect("borrowed decode");
3687        let borrowed = borrowed_owned
3688            .pop()
3689            .expect("borrowed decode should yield one row");
3690
3691        assert_eq!(
3692            borrowed.as_number_text().as_deref(),
3693            owned.as_number_text().as_deref(),
3694            "borrowed and owned paths must expose identical canonical NUMBER text"
3695        );
3696        assert_eq!(
3697            borrowed, owned,
3698            "borrowed to_owned_value should materialize the same trailing-zero NUMBER"
3699        );
3700    }
3701
3702    #[test]
3703    fn describe_size_zero_urowid_decodes_and_preserves_following_column_alignment() {
3704        let columns = vec![
3705            col("RID", ORA_TYPE_NUM_UROWID, CS_FORM_IMPLICIT, 0),
3706            col("NEXT", ORA_TYPE_NUM_VARCHAR, CS_FORM_IMPLICIT, 4000),
3707        ];
3708        let rba: u32 = 0x0102_0304;
3709        let partition_id: u16 = 0x0506;
3710        let block_num: u32 = 0x0708_090a;
3711        let slot_num: u16 = 0x0b0c;
3712        let mut encoded_urowid = Vec::new();
3713        encoded_urowid.push(1);
3714        encoded_urowid.extend_from_slice(&rba.to_be_bytes());
3715        encoded_urowid.extend_from_slice(&partition_id.to_be_bytes());
3716        encoded_urowid.extend_from_slice(&block_num.to_be_bytes());
3717        encoded_urowid.extend_from_slice(&slot_num.to_be_bytes());
3718        let expected_rowid = encode_physical_rowid(rba, partition_id, block_num, slot_num);
3719
3720        let mut writer = TtcWriter::new();
3721        writer
3722            .write_bytes_with_length(&[1])
3723            .expect("write UROWID null probe field");
3724        writer
3725            .write_bytes_with_length(&encoded_urowid)
3726            .expect("write encoded UROWID");
3727        writer
3728            .write_bytes_with_length(b"after")
3729            .expect("write following text column");
3730        let buffer = writer.into_bytes();
3731
3732        let mut owned_reader = TtcReader::new(&buffer);
3733        let owned = columns
3734            .iter()
3735            .map(|column| parse_column_value(&mut owned_reader, column).expect("owned decode"))
3736            .collect::<Vec<_>>();
3737        assert_eq!(
3738            owned[0].as_ref().and_then(QueryValue::as_rowid),
3739            Some(expected_rowid.as_str()),
3740            "owned decode must not NULL a describe-size-0 UROWID"
3741        );
3742        assert_eq!(
3743            owned[1].as_ref().and_then(QueryValue::as_text),
3744            Some("after"),
3745            "owned decode must consume UROWID bytes before the following column"
3746        );
3747
3748        let batch = BorrowedRowBatch::new(buffer, columns, vec![0]);
3749        let mut borrowed_rows = Vec::new();
3750        batch
3751            .for_each_row_ref(|row| {
3752                borrowed_rows.push(
3753                    row.iter()
3754                        .map(|cell| cell.map(|value| value.to_owned_value()))
3755                        .collect::<Vec<_>>(),
3756                );
3757                Ok::<(), ProtocolError>(())
3758            })
3759            .expect("borrowed decode");
3760        assert_eq!(borrowed_rows, vec![owned]);
3761    }
3762
3763    // The borrowed response parser walks the *same* message framing as the owned
3764    // `parse_fetch_response_with_context` (ROW_HEADER / BIT_VECTOR / ROW_DATA /
3765    // END_OF_RESPONSE), but instead of building owned rows it captures each
3766    // row's byte offset and hands back a `BorrowedRowBatch`. Decoding that batch
3767    // must reproduce exactly what the owned fetch path produced — duplicate
3768    // columns (bit vector) and all. Fixture is the same one the owned
3769    // `fetch_response_decodes_rows_with_previous_cursor_metadata` test uses.
3770    #[test]
3771    fn borrowed_response_parse_matches_owned_fetch_path() {
3772        use hex::FromHex;
3773        let payload = Vec::from_hex("06020101000205dc0001010101000702c1041d")
3774            .expect("fixture response should be valid hex");
3775        let columns = vec![
3776            col("INTCOL", ORA_TYPE_NUM_NUMBER, CS_FORM_IMPLICIT, 22),
3777            col("NUMBERCOL", ORA_TYPE_NUM_NUMBER, CS_FORM_IMPLICIT, 22),
3778        ];
3779        let previous_row = vec![
3780            Some(QueryValue::number_from_text("2", true)),
3781            Some(QueryValue::number_from_text("0.5", false)),
3782        ];
3783
3784        // Owned golden.
3785        let owned = parse_query_response_with_context(
3786            &payload,
3787            ClientCapabilities::default(),
3788            &columns,
3789            Some(&previous_row),
3790        )
3791        .expect("owned fetch decode");
3792
3793        // Borrowed parse.
3794        let borrowed = parse_query_response_borrowed(
3795            &payload,
3796            ClientCapabilities::default(),
3797            &columns,
3798            Some(&previous_row),
3799        )
3800        .expect("borrowed fetch decode");
3801
3802        assert_eq!(borrowed.more_rows, owned.more_rows);
3803        assert_eq!(borrowed.cursor_id, owned.cursor_id);
3804        assert_eq!(borrowed.batch.row_count(), owned.rows.len());
3805
3806        let mut borrowed_owned: Vec<Vec<Option<QueryValue>>> = Vec::new();
3807        borrowed
3808            .batch
3809            .for_each_row_ref(|row| {
3810                borrowed_owned.push(
3811                    row.iter()
3812                        .map(|cell| cell.map(|v| v.to_owned_value()))
3813                        .collect(),
3814                );
3815                Ok::<(), ProtocolError>(())
3816            })
3817            .expect("iterate borrowed rows");
3818
3819        assert_eq!(
3820            borrowed_owned, owned.rows,
3821            "borrowed batch must reproduce the owned fetch rows (incl. duplicate columns)"
3822        );
3823    }
3824}
3825
3826#[cfg(test)]
3827mod out_bind_boolean_regression_tests {
3828    use super::*;
3829
3830    fn boolean_column(is_array: bool) -> ColumnMetadata {
3831        ColumnMetadata {
3832            name: "B".to_string(),
3833            ora_type_num: ORA_TYPE_NUM_BOOLEAN,
3834            is_array,
3835            ..ColumnMetadata::default()
3836        }
3837    }
3838
3839    fn boolean_value_with_negative_actual_bytes() -> Vec<u8> {
3840        let mut writer = TtcWriter::new();
3841        writer
3842            .write_bytes_with_length(&[1])
3843            .expect("write present boolean value");
3844        writer.write_sb4(-1);
3845        writer.into_bytes()
3846    }
3847
3848    #[test]
3849    fn scalar_boolean_out_bind_negative_actual_bytes_decodes_null() {
3850        let bind_columns = [boolean_column(false)];
3851        let out_bind_indexes = [0usize];
3852        let payload = boolean_value_with_negative_actual_bytes();
3853        let mut reader = TtcReader::new(&payload);
3854        let mut result = QueryResult::default();
3855
3856        parse_out_bind_row_data(&mut reader, &mut result, &bind_columns, &out_bind_indexes)
3857            .expect("parse scalar BOOLEAN OUT bind");
3858
3859        assert_eq!(result.out_values, vec![(0, None)]);
3860    }
3861
3862    #[test]
3863    fn array_boolean_out_bind_negative_actual_bytes_decodes_null_element() {
3864        let bind_columns = [boolean_column(true)];
3865        let out_bind_indexes = [0usize];
3866        let mut writer = TtcWriter::new();
3867        writer.write_ub4(1);
3868        writer
3869            .write_bytes_with_length(&[1])
3870            .expect("write present boolean array element");
3871        writer.write_sb4(-1);
3872        let payload = writer.into_bytes();
3873        let mut reader = TtcReader::new(&payload);
3874        let mut result = QueryResult::default();
3875
3876        parse_out_bind_row_data(&mut reader, &mut result, &bind_columns, &out_bind_indexes)
3877            .expect("parse array BOOLEAN OUT bind");
3878
3879        assert_eq!(
3880            result.out_values,
3881            vec![(0, Some(QueryValue::Array(vec![None])))]
3882        );
3883    }
3884
3885    #[test]
3886    fn returning_boolean_negative_actual_bytes_decodes_null() {
3887        let bind_columns = [boolean_column(false)];
3888        let output_bind_indexes = [0usize];
3889        let mut writer = TtcWriter::new();
3890        writer.write_ub4(1);
3891        writer
3892            .write_bytes_with_length(&[1])
3893            .expect("write present returning boolean value");
3894        writer.write_sb4(-1);
3895        let payload = writer.into_bytes();
3896        let mut reader = TtcReader::new(&payload);
3897        let mut result = QueryResult::default();
3898
3899        parse_returning_row_data(
3900            &mut reader,
3901            &mut result,
3902            &bind_columns,
3903            &output_bind_indexes,
3904        )
3905        .expect("parse BOOLEAN RETURNING value");
3906
3907        assert_eq!(result.return_values, vec![(0, vec![None])]);
3908    }
3909}
3910
3911#[cfg(test)]
3912mod in_out_io_vector_tests {
3913    use super::*;
3914
3915    // The three TNS bind directions the server tags each bind with in the IO
3916    // vector (reference thin_impl.c: OUTPUT=16, INPUT=32, INPUT_OUTPUT=48). The
3917    // client emits no direction on the wire — it reads back every bind the server
3918    // flags as not pure INPUT, i.e. OUT (16) or IN OUT (48). These locals pin the
3919    // exact wire values this test exercises.
3920    const TNS_BIND_DIR_OUTPUT: u8 = 16;
3921    const TNS_BIND_DIR_INPUT_OUTPUT: u8 = 48;
3922
3923    // Layout consumed by `parse_io_vector`: flags(u8), num_binds low(ub2) /
3924    // high(ub4), num_iters(ub4), uac_buffer_length(ub2), fast_fetch_len(ub2),
3925    // rowid_len(ub2), then one direction byte per bind.
3926    fn io_vector_payload(directions: &[u8]) -> Vec<u8> {
3927        io_vector_payload_with_skips(directions, &[], &[])
3928    }
3929
3930    fn io_vector_payload_with_skips(
3931        directions: &[u8],
3932        fast_fetch_bytes: &[u8],
3933        rowid_bytes: &[u8],
3934    ) -> Vec<u8> {
3935        let mut writer = TtcWriter::new();
3936        writer.write_u8(0);
3937        writer.write_ub2(u16::try_from(directions.len()).unwrap());
3938        writer.write_ub4(0);
3939        writer.write_ub4(1);
3940        writer.write_ub2(0);
3941        writer.write_ub2(u16::try_from(fast_fetch_bytes.len()).unwrap());
3942        writer.write_raw(fast_fetch_bytes);
3943        writer.write_ub2(u16::try_from(rowid_bytes.len()).unwrap());
3944        writer.write_raw(rowid_bytes);
3945        for &direction in directions {
3946            writer.write_u8(direction);
3947        }
3948        writer.into_bytes()
3949    }
3950
3951    #[test]
3952    fn in_out_and_out_directions_read_back_pure_input_does_not() {
3953        // bind 0 = plain IN (send-only), bind 1 = IN OUT, bind 2 = OUT.
3954        let payload = io_vector_payload(&[
3955            TNS_BIND_DIR_INPUT,
3956            TNS_BIND_DIR_INPUT_OUTPUT,
3957            TNS_BIND_DIR_OUTPUT,
3958        ]);
3959        let mut reader = TtcReader::new(&payload);
3960        let out_indexes = parse_io_vector(&mut reader, 3).expect("parse io vector");
3961        assert_eq!(
3962            out_indexes,
3963            vec![1, 2],
3964            "IN OUT (48) and OUT (16) are read back; pure IN (32) is not"
3965        );
3966    }
3967
3968    #[test]
3969    fn io_vector_skips_optional_payloads_and_ignores_unbound_slots() {
3970        let payload = io_vector_payload_with_skips(
3971            &[
3972                TNS_BIND_DIR_OUTPUT,
3973                TNS_BIND_DIR_INPUT,
3974                TNS_BIND_DIR_INPUT_OUTPUT,
3975            ],
3976            b"ff",
3977            b"rid",
3978        );
3979        let mut reader = TtcReader::new(&payload);
3980        let out_indexes = parse_io_vector(&mut reader, 2).expect("parse io vector with skips");
3981
3982        assert_eq!(
3983            out_indexes,
3984            vec![0],
3985            "optional fast-fetch/rowid payloads are skipped, and directions beyond bind_count are ignored"
3986        );
3987        assert_eq!(reader.remaining(), 0);
3988    }
3989
3990    // End-to-end read-back: a VARCHAR IN OUT bind whose routine writes back a
3991    // value longer than the input. The returned bytes decode against the IN OUT
3992    // bind's own metadata (buffer sized to 200, well beyond the 2-char input), so
3993    // the longer value survives.
3994    #[test]
3995    fn in_out_varchar_reads_back_value_larger_than_input() {
3996        let inout = BindValue::InOut {
3997            value: Box::new(BindValue::Text("ab".to_string())),
3998            out_buffer_size: 200,
3999        };
4000        let bind_columns = [bind_column_metadata(&inout)];
4001        let out_bind_indexes = [0usize];
4002
4003        // Server OUT-slot response: the routine-modified value "ABCD", then the
4004        // zero "actual bytes" trailer (present, not truncated).
4005        let mut writer = TtcWriter::new();
4006        writer
4007            .write_bytes_with_length(b"ABCD")
4008            .expect("write returned varchar");
4009        writer.write_sb4(0);
4010        let payload = writer.into_bytes();
4011
4012        let mut reader = TtcReader::new(&payload);
4013        let mut result = QueryResult::default();
4014        parse_out_bind_row_data(&mut reader, &mut result, &bind_columns, &out_bind_indexes)
4015            .expect("parse IN OUT VARCHAR read-back");
4016
4017        assert_eq!(
4018            result.out_values,
4019            vec![(0, Some(QueryValue::Text("ABCD".to_string())))],
4020            "the routine-modified IN OUT value is read back against its bind metadata"
4021        );
4022    }
4023}
4024
4025#[cfg(test)]
4026mod fuzz_regression_tests {
4027    use super::*;
4028
4029    // Regression (w6-fuzz, query_response target): a TNS_MSG_TYPE_IMPLICIT_RESULTSET
4030    // message (27) whose ub4 result count was ~620M made the dispatch loop
4031    // `Vec::with_capacity` several gigabytes of `QueryValue::Cursor` before the
4032    // truncated read failed, tripping libFuzzer's OOM detector. The parser must
4033    // now fail closed (truncated payload) without the giant allocation.
4034    #[test]
4035    fn fuzz_regression_implicit_resultset_oom() {
4036        // payload: type=27, ub4 length byte 4, value 0x25000000 (~620M), then EOF
4037        let payload = [27u8, 4, 37, 0, 0, 0];
4038        let err = parse_query_response(&payload, ClientCapabilities::default())
4039            .expect_err("oversized implicit-resultset count must fail closed");
4040        assert!(
4041            matches!(
4042                err,
4043                ProtocolError::TtcDecode(_) | ProtocolError::ResourceLimit { .. }
4044            ),
4045            "expected fail-closed protocol error, got {err:?}"
4046        );
4047    }
4048
4049    // BoundedReader invariant (l2p), query-columns family: a DESCRIBE_INFO
4050    // message (16) declaring a huge num_columns (ub4 ~620M) with no column
4051    // metadata bytes following must fail closed, not pre-allocate one
4052    // ColumnMetadata per declared column. parse_describe_info grows the column
4053    // Vec via push (no speculative with_capacity), and the first
4054    // parse_column_metadata read past the end errors.
4055    #[test]
4056    fn describe_info_oversized_column_count_fails_closed_not_oom() {
4057        // type=16 DESCRIBE_INFO; describe_name read_bytes len byte 0 (null);
4058        // max_row_size ub4 = 0; num_columns ub4 (len byte 4) = 0x25000000
4059        // (~620M); then EOF before the skip(1)/column records.
4060        let payload = [16u8, 0, 0, 4, 0x25, 0x00, 0x00, 0x00];
4061        let err = parse_query_response(&payload, ClientCapabilities::default())
4062            .expect_err("oversized column count must fail closed");
4063        assert!(
4064            matches!(
4065                err,
4066                ProtocolError::TtcDecode(_) | ProtocolError::ResourceLimit { .. }
4067            ),
4068            "expected fail-closed protocol error, got {err:?}"
4069        );
4070    }
4071
4072    #[test]
4073    fn describe_info_respects_protocol_column_limit() {
4074        // type=16 DESCRIBE_INFO; describe_name null; max_row_size=0;
4075        // num_columns=2. A max_columns=1 policy should fail before any column
4076        // metadata allocation/parsing.
4077        let payload = [TNS_MSG_TYPE_DESCRIBE_INFO, 0, 0, 1, 2];
4078        let limits = ProtocolLimits {
4079            max_columns: 1,
4080            ..ProtocolLimits::DEFAULT
4081        };
4082        let err = parse_query_response_with_limits(&payload, ClientCapabilities::default(), limits)
4083            .expect_err("column count above policy must fail");
4084        assert!(
4085            matches!(
4086                err,
4087                ProtocolError::ResourceLimit {
4088                    limit: "columns",
4089                    observed: 2,
4090                    maximum: 1,
4091                }
4092            ),
4093            "expected column ResourceLimit, got {err:?}"
4094        );
4095    }
4096
4097    // BoundedReader invariant (l2p), out-bind array family: an array OUT bind
4098    // whose ub4 num_elements is enormous (~620M) but carries no element bytes
4099    // must fail closed via with_capacity_bounded + the per-element read, not
4100    // reserve gigabytes of Option<QueryValue>.
4101    #[test]
4102    fn out_bind_array_oversized_element_count_fails_closed_not_oom() {
4103        let metadata = ColumnMetadata {
4104            name: "ARR".to_string(),
4105            ora_type_num: ORA_TYPE_NUM_NUMBER,
4106            is_array: true,
4107            ..ColumnMetadata::default()
4108        };
4109        let bind_columns = [metadata];
4110        let out_bind_indexes = [0usize];
4111        // ub4 num_elements: len byte 4, value 0x25000000, then no elements.
4112        let payload = [4u8, 0x25, 0x00, 0x00, 0x00];
4113        let mut reader = TtcReader::new(&payload);
4114        let mut result = QueryResult::default();
4115        let err =
4116            parse_out_bind_row_data(&mut reader, &mut result, &bind_columns, &out_bind_indexes)
4117                .expect_err("oversized array OUT bind count must fail closed");
4118        assert!(
4119            matches!(
4120                err,
4121                ProtocolError::TtcDecode(_) | ProtocolError::ResourceLimit { .. }
4122            ),
4123            "expected fail-closed protocol error, got {err:?}"
4124        );
4125    }
4126
4127    // ---- describe-read version-gate boundary tests -------------------------
4128    //
4129    // Reference messages/base.pyx `_process_column_metadata` gates four fields
4130    // on the negotiated ttc field version:
4131    //   :346  >= 12.2        ub4 oaccolid                (below the 18c floor)
4132    //   :358  >= 23.1        domain schema + name
4133    //   :361  >= 23.1 ext 3  annotations block
4134    //   :376  >= 23.4        vector dimensions/format/flags
4135    // parse_column_metadata mirrors each. The 12.2 boundary is below our live
4136    // floor (18c field version 11), so no live server exercises the pre-12.2
4137    // read path; this offline test pins every boundary by proving the parser
4138    // consumes *exactly* the bytes for its field version and misaligns (fails
4139    // closed, or leaves the wire unconsumed) when parsed one version off.
4140
4141    fn describe_caps(fv: u8) -> ClientCapabilities {
4142        ClientCapabilities {
4143            ttc_field_version: fv,
4144            max_string_size: 32_767,
4145            charset_id: 873,
4146        }
4147    }
4148
4149    /// One column-metadata record whose optional fields are present iff the
4150    /// field version gates them in — i.e. exactly what a server at `fv` sends.
4151    fn describe_column_bytes(fv: u8) -> Vec<u8> {
4152        let mut w = TtcWriter::new();
4153        w.write_u8(ORA_TYPE_NUM_VARCHAR);
4154        w.write_u8(0); // flags
4155        w.write_u8(0); // precision
4156        w.write_u8(0); // scale
4157        w.write_ub4(4000); // buffer size
4158        w.write_ub4(0); // max array elements
4159        w.write_ub8(0); // cont flags
4160        w.write_bytes_with_two_lengths(None).expect("oid");
4161        w.write_ub2(0); // version
4162        w.write_ub2(0); // server charset id (ignored)
4163        w.write_u8(CS_FORM_IMPLICIT);
4164        w.write_ub4(4000); // max size
4165        if fv >= TNS_CCAP_FIELD_VERSION_12_2 {
4166            w.write_ub4(0x1122_3344); // oaccolid (5 wire bytes when present)
4167        }
4168        w.write_u8(1); // nullable
4169        w.write_u8(0); // flags
4170        w.write_bytes_with_two_lengths(Some(b"TXT")).expect("name");
4171        w.write_bytes_with_two_lengths(None).expect("object schema");
4172        w.write_bytes_with_two_lengths(None).expect("object type");
4173        w.write_ub2(1); // column position
4174        w.write_ub4(0); // uds flags
4175        if fv >= TNS_CCAP_FIELD_VERSION_23_1 {
4176            w.write_bytes_with_two_lengths(None).expect("domain schema");
4177            w.write_bytes_with_two_lengths(None).expect("domain name");
4178        }
4179        if fv >= TNS_CCAP_FIELD_VERSION_23_1_EXT_3 {
4180            w.write_ub4(0); // num annotations (0 => no sub-block)
4181        }
4182        if fv >= TNS_CCAP_FIELD_VERSION_23_4 {
4183            w.write_ub4(2); // vector dimensions
4184            w.write_u8(0); // format
4185            w.write_u8(0); // flags
4186        }
4187        w.into_bytes()
4188    }
4189
4190    /// `Some((column_name, remaining_bytes))` after a successful parse, else
4191    /// `None` (failed closed). A version-matched parse decodes the name and
4192    /// consumes the record exactly => `Some(("TXT", 0))`; any misaligned read
4193    /// diverges (different name, leftover bytes, or a hard failure).
4194    fn parse_outcome(bytes: &[u8], fv: u8) -> Option<(String, usize)> {
4195        let mut reader = TtcReader::new(bytes);
4196        parse_column_metadata(&mut reader, describe_caps(fv))
4197            .ok()
4198            .map(|meta| (meta.name().to_string(), reader.remaining()))
4199    }
4200
4201    #[test]
4202    fn describe_column_metadata_gates_fields_on_field_version() {
4203        // Each (lo, hi) pair straddles exactly one gate; the other three gates
4204        // are on the same side of the boundary for both, so only one field moves.
4205        let boundaries = [
4206            (TNS_CCAP_FIELD_VERSION_12_2 - 1, TNS_CCAP_FIELD_VERSION_12_2), // oaccolid
4207            (TNS_CCAP_FIELD_VERSION_23_1 - 1, TNS_CCAP_FIELD_VERSION_23_1), // domain
4208            (
4209                TNS_CCAP_FIELD_VERSION_23_1_EXT_3 - 1,
4210                TNS_CCAP_FIELD_VERSION_23_1_EXT_3,
4211            ), // annotations
4212            (TNS_CCAP_FIELD_VERSION_23_4 - 1, TNS_CCAP_FIELD_VERSION_23_4), // vector
4213        ];
4214        for (lo, hi) in boundaries {
4215            let lo_bytes = describe_column_bytes(lo);
4216            let hi_bytes = describe_column_bytes(hi);
4217            assert!(
4218                hi_bytes.len() > lo_bytes.len(),
4219                "fv {hi}: the gated field must add bytes vs fv {lo}"
4220            );
4221
4222            // Version-matched parses decode the name and consume exactly.
4223            let matched = Some(("TXT".to_string(), 0));
4224            assert_eq!(parse_outcome(&lo_bytes, lo), matched, "fv {lo} matched");
4225            assert_eq!(parse_outcome(&hi_bytes, hi), matched, "fv {hi} matched");
4226
4227            // Parsed one version off, the record misaligns: reading `hi` bytes
4228            // as `lo` skips the gated field (leftover bytes remain), and reading
4229            // `lo` bytes as `hi` over-reads (fails closed). Neither matches.
4230            assert_ne!(
4231                parse_outcome(&hi_bytes, lo),
4232                matched,
4233                "fv {hi} bytes read as fv {lo} must not consume cleanly"
4234            );
4235            assert_ne!(
4236                parse_outcome(&lo_bytes, hi),
4237                matched,
4238                "fv {lo} bytes read as fv {hi} must not consume cleanly"
4239            );
4240        }
4241    }
4242}