Skip to main content

oracledb_protocol/thin/
mod.rs

1#![forbid(unsafe_code)]
2
3//! TTC thin-protocol wire codecs, split across cohesive submodules.
4//! `mod.rs` wires the submodules and re-exports their items so every
5//! `oracledb_protocol::thin::*` path that downstream crates depend on stays
6//! reachable, and so submodules see each other via `use super::*`.
7
8pub(crate) use std::collections::BTreeMap;
9
10pub(crate) use crate::sql::statement_is_plsql;
11pub(crate) use crate::wire::{BorrowedBytes, BoundedReader, TtcReader, TtcWriter};
12pub(crate) use crate::{ProtocolError, Result, TNS_VERSION_DESIRED, TNS_VERSION_MIN};
13pub(crate) use hex::FromHex;
14
15pub mod aq;
16mod auth;
17mod bind;
18mod codecs;
19mod connect;
20mod constants;
21mod dbobject;
22mod errors;
23mod execute;
24mod fetch;
25mod lob;
26mod number;
27mod sessionless;
28mod subscr;
29mod types;
30
31// Property / metamorphic / boundary suites for the `pub(crate)` scalar codecs
32// (NUMBER, DATE/TIMESTAMP/TSTZ, INTERVAL YM/DS, BINARY_FLOAT/DOUBLE, text).
33// Compiled only under `cfg(test)`; reaches the crate-internal encoders that the
34// integration `tests/` directory cannot see. See `proptests.rs`.
35#[cfg(test)]
36mod proptests;
37
38pub use auth::*;
39pub use bind::*;
40pub use codecs::*;
41pub use connect::*;
42pub use constants::*;
43pub use dbobject::*;
44pub use execute::*;
45pub use fetch::*;
46pub use lob::*;
47pub use number::*;
48pub use sessionless::*;
49pub use subscr::*;
50pub use types::*;
51// `errors` holds only crate-internal items (ServerErrorInfo + parse/skip helpers);
52// re-export at crate visibility so the glob has something to re-export.
53pub(crate) use errors::*;
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn connect_payload_matches_reference_shape() {
61        let payload = build_connect_packet_payload(
62            "(DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=localhost)(PORT=1522))(CONNECT_DATA=(SERVICE_NAME=FREEPDB1)))",
63            8192,
64        )
65        .expect("connect payload should encode");
66        assert_eq!(&payload[..8], &[0x01, 0x3f, 0x01, 0x2c, 0, 1, 0x20, 0]);
67    }
68
69    #[test]
70    fn auth_phase_one_contains_identity_keys() {
71        let payload = build_fast_auth_phase_one_payload("u", "p", "m", "o", "t", 42)
72            .expect("auth packet should encode");
73        let text = String::from_utf8_lossy(&payload);
74        assert!(text.contains("AUTH_PROGRAM_NM"));
75        assert!(text.contains("AUTH_MACHINE"));
76        assert!(text.contains("AUTH_SID"));
77    }
78
79    #[test]
80    fn nchar_bind_text_uses_utf16be() {
81        assert_eq!(encode_text_value("Aあ", CS_FORM_IMPLICIT), b"A\xE3\x81\x82");
82        assert_eq!(
83            encode_text_value("Aあ", CS_FORM_NCHAR),
84            vec![0x00, 0x41, 0x30, 0x42]
85        );
86    }
87
88    #[test]
89    fn query_response_decodes_prefetched_text_row_with_no_data_eof() {
90        let payload = Vec::from_hex(concat!(
91            "101710740fb986350b6010fbcb6e06a74ed0787e060a110328014001018201800000",
92            "014000000000020369010140023ffe010501050556414c554500000000000000000000",
93            "010707787e060a110b1000021fe8010a010a00062201010001020000000708414c33",
94            "32555446380801060323a4d500010100000000000004010102013b010102057b0000",
95            "01010003000000000000000000000000030001010000000002057b0101010300194f",
96            "52412d30313430333a206e6f206461746120666f756e640a1d",
97        ))
98        .expect("fixture response should be valid hex");
99
100        let parsed = parse_query_response(&payload, ClientCapabilities::default())
101            .expect("accepted execute response should decode");
102
103        assert_eq!(parsed.columns.len(), 1);
104        assert_eq!(parsed.columns[0].name, "VALUE");
105        assert_eq!(
106            parsed.rows,
107            vec![vec![Some(QueryValue::Text("AL32UTF8".into()))]]
108        );
109        assert!(!parsed.more_rows);
110    }
111
112    // Regression for bead rust-oracledb-f0ad. A cached query cursor
113    // (`select :1 as v from dual`) is re-executed after the column's type
114    // changed CHAR -> CLOB. The server re-describes the column as CLOB but, per
115    // `adjust_refetch_metadata`, streams the (null) value in LONG form WITH the
116    // LONG status trailer (null indicator + ORA-01405 return code). The execute
117    // path runs with `fetch_long_status = false`, so before the fix the 5-byte
118    // trailer was left unconsumed and the following message byte (0x81) was
119    // mis-read as "unknown TTC message type 129 at position 95". The fix
120    // promotes `fetch_long_status` when an adjustment folds a column to
121    // LONG/LONG RAW. Bytes are a real Oracle Free 23ai wire capture.
122    #[test]
123    fn query_response_consumes_long_trailer_for_clob_refetched_as_long() {
124        let payload = Vec::from_hex(concat!(
125            "1017d2695e29222bd76b27853dff436b866a787e06170608290001018270800000",
126            "020fa0000000000203690100023ffe010101010156000000000000000000000107",
127            "07787e061706082a00021fe8010201020006220101000164000000070081010205",
128            "7d0801060371a8380001010000000000000401010211fe010102057b0000010101",
129            "1403000000000000000000000000210001010000000002057b0101010300194f52",
130            "412d30313430333a206e6f206461746120666f756e640a1d",
131        ))
132        .expect("fixture response should be valid hex");
133
134        // The cached cursor's previous fetch saw column V as CHAR; the
135        // re-execute describes it as CLOB, which adjust_refetch_metadata folds
136        // to LONG, so the wire value carries the LONG status trailer.
137        let previous = vec![char_column("V")];
138        let parsed = parse_query_response_with_context(
139            &payload,
140            ClientCapabilities::default(),
141            &previous,
142            None,
143        )
144        .expect("CLOB-refetched-as-LONG must decode, not desync on the LONG status trailer");
145
146        assert_eq!(parsed.columns.len(), 1);
147        assert_eq!(parsed.columns[0].ora_type_num, ORA_TYPE_NUM_LONG);
148        assert_eq!(parsed.rows, vec![vec![None]]);
149        assert!(!parsed.more_rows);
150    }
151
152    #[test]
153    fn fetch_response_decodes_rows_with_previous_cursor_metadata() {
154        let payload = Vec::from_hex("06020101000205dc0001010101000702c1041d")
155            .expect("fixture response should be valid hex");
156        let columns = vec![number_column("INTCOL"), number_column("NUMBERCOL")];
157        let previous_row = vec![
158            Some(QueryValue::number_from_text("2", true)),
159            Some(QueryValue::number_from_text("0.5", false)),
160        ];
161
162        let parsed = parse_query_response_with_context(
163            &payload,
164            ClientCapabilities::default(),
165            &columns,
166            Some(&previous_row),
167        )
168        .expect("fetch response should decode using cached cursor metadata");
169
170        assert_eq!(parsed.columns, columns);
171        assert_eq!(
172            parsed.rows,
173            vec![vec![
174                Some(QueryValue::number_from_text("3", true)),
175                previous_row[1].clone(),
176            ]]
177        );
178    }
179
180    #[test]
181    fn fetch_response_skips_long_status_fields() {
182        let payload =
183            Vec::from_hex("07036162638101001d").expect("fixture response should be valid hex");
184        let columns = vec![long_column("LONGCOL")];
185
186        let parsed = parse_fetch_response_with_context(
187            &payload,
188            ClientCapabilities::default(),
189            &columns,
190            None,
191        )
192        .expect("fetch response should consume LONG status fields");
193
194        assert_eq!(
195            parsed.rows,
196            vec![vec![Some(QueryValue::Text("abc".into()))]]
197        );
198    }
199
200    #[test]
201    fn fetch_response_decodes_mid_row_oracle_error() {
202        let payload = Vec::from_hex(concat!(
203            "150101010703c20401010205100205db0205c400000106018f030000000000",
204            "0301214d0118000293b60201c60000080000000000000205db0205c40103",
205            "00244f52412d30313437363a2064697669736f7220697320657175616c20",
206            "746f207a65726f0a1d",
207        ))
208        .expect("fixture response should be valid hex");
209        let columns = vec![number_column("INTCOL"), number_column("NUMBERCOL")];
210        let previous_row = vec![
211            Some(QueryValue::number_from_text("1499", true)),
212            Some(QueryValue::number_from_text("0.5", false)),
213        ];
214
215        let err = parse_query_response_with_context(
216            &payload,
217            ClientCapabilities::default(),
218            &columns,
219            Some(&previous_row),
220        )
221        .expect_err("mid-row error info should surface as a server error");
222
223        assert_eq!(
224            err.to_string(),
225            "server returned Oracle error: ORA-01476: divisor is equal to zero"
226        );
227    }
228
229    #[test]
230    fn lob_read_payload_writes_modern_token_field() {
231        let locator = [0x00, 0x70, 0xaa];
232        let modern =
233            build_lob_read_payload_with_seq(&locator, 1, 5, 8, TNS_CCAP_FIELD_VERSION_23_1_EXT_1)
234                .expect("LOB read payload should encode");
235        assert_eq!(
236            &modern[..7],
237            &[TNS_MSG_TYPE_FUNCTION, TNS_FUNC_LOB_OP, 8, 0, 1, 1, 3]
238        );
239
240        let legacy =
241            build_lob_read_payload_with_seq(&locator, 1, 5, 8, TNS_CCAP_FIELD_VERSION_23_1)
242                .expect("LOB read payload should encode");
243        assert_eq!(
244            &legacy[..6],
245            &[TNS_MSG_TYPE_FUNCTION, TNS_FUNC_LOB_OP, 8, 1, 1, 3]
246        );
247    }
248
249    #[test]
250    fn lob_locator_temporary_flags_match_reference_offsets() {
251        let mut locator = vec![0; 40];
252        assert!(!lob_locator_is_temporary(&locator));
253
254        locator[TNS_LOB_LOC_OFFSET_FLAG_1] = TNS_LOB_LOC_FLAGS_ABSTRACT;
255        assert!(lob_locator_is_temporary(&locator));
256
257        locator[TNS_LOB_LOC_OFFSET_FLAG_1] = 0;
258        locator[TNS_LOB_LOC_OFFSET_FLAG_4] = TNS_LOB_LOC_FLAGS_TEMP;
259        assert!(lob_locator_is_temporary(&locator));
260    }
261
262    #[test]
263    fn lob_free_temp_payload_writes_array_free_operation() {
264        let locator = vec![0xaa; 40];
265        let payload = build_lob_free_temp_payload_with_seq(
266            std::slice::from_ref(&locator),
267            9,
268            TNS_CCAP_FIELD_VERSION_23_1_EXT_1,
269        )
270        .expect("LOB free-temp payload should encode");
271
272        assert_eq!(
273            &payload[..19],
274            &[
275                TNS_MSG_TYPE_FUNCTION,
276                TNS_FUNC_LOB_OP,
277                9,
278                0,
279                1,
280                1,
281                40,
282                0,
283                0,
284                0,
285                0,
286                0,
287                0,
288                0,
289                4,
290                0,
291                8,
292                1,
293                0x11,
294            ]
295        );
296        assert!(payload.ends_with(&locator));
297    }
298
299    #[test]
300    fn lob_free_temp_response_skips_returned_locator_parameter() {
301        let payload = Vec::from_hex(concat!(
302            "0800260000020080000002ee5500000044000000030369000a000000000002",
303            "5295f656000000010000040101021a390000000000000000000000000000",
304            "00000000000a000000000000000000001d",
305        ))
306        .expect("fixture response should be valid hex");
307
308        parse_lob_free_temp_response(&payload, ClientCapabilities::default(), 40)
309            .expect("free-temp response should consume returned locator");
310    }
311
312    #[test]
313    fn rowid_value_decodes_physical_rowid() {
314        let mut reader = TtcReader::new(&[
315            13, // non-null rowid marker
316            1, 1, // rba
317            1, 2, // partition id
318            0, // ignored padding byte
319            1, 3, // block number
320            1, 4, // slot number
321        ]);
322
323        let value = parse_rowid_value(&mut reader).expect("physical rowid should decode");
324
325        assert_eq!(value.as_deref(), Some("AAAAABAACAAAAADAAE"));
326        assert_eq!(reader.remaining(), 0);
327    }
328
329    #[test]
330    fn urowid_value_decodes_physical_rowid() {
331        let mut reader = TtcReader::new(&[
332            1, 13, // ignored first length buffer
333            13, // second buffer length
334            1,  // physical rowid marker
335            0, 0, 0, 1, // rba
336            0, 2, // partition id
337            0, 0, 0, 3, // block number
338            0, 4, // slot number
339        ]);
340
341        let value = parse_urowid_value(&mut reader).expect("physical urowid should decode");
342
343        assert_eq!(value.as_deref(), Some("AAAAABAACAAAAADAAE"));
344        assert_eq!(reader.remaining(), 0);
345    }
346
347    #[test]
348    fn urowid_value_decodes_logical_rowid() {
349        let mut reader = TtcReader::new(&[
350            1, 13, // ignored first length buffer
351            13, // second buffer length
352            0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
353        ]);
354
355        let value = parse_urowid_value(&mut reader).expect("logical urowid should decode");
356
357        assert_eq!(value.as_deref(), Some("*AQIDBAUGBwgJCgsM"));
358        assert_eq!(reader.remaining(), 0);
359    }
360
361    #[test]
362    fn binary_double_round_trips_oracle_canonical_bytes() {
363        for value in [0.0, -0.0, 1.5, -2.25, f64::INFINITY, f64::NEG_INFINITY] {
364            let decoded = decode_binary_double(&encode_binary_double(value))
365                .expect("BINARY_DOUBLE should round trip");
366            assert_eq!(decoded.to_bits(), value.to_bits());
367        }
368
369        let decoded =
370            decode_binary_double(&encode_binary_double(f64::NAN)).expect("NaN should decode");
371        assert!(decoded.is_nan());
372    }
373
374    #[test]
375    fn bind_value_type_info_reports_protocol_metadata() {
376        assert_eq!(bind_value_type_info(&BindValue::Null), None);
377        assert_eq!(
378            bind_value_type_info(&BindValue::Text("abc".into())),
379            Some(BindTypeInfo {
380                ora_type_num: ORA_TYPE_NUM_VARCHAR,
381                csfrm: CS_FORM_IMPLICIT,
382                buffer_size: 12,
383            })
384        );
385        assert_eq!(
386            bind_value_type_info(&BindValue::BinaryDouble(1.25)),
387            Some(BindTypeInfo {
388                ora_type_num: ORA_TYPE_NUM_BINARY_DOUBLE,
389                csfrm: 0,
390                buffer_size: ORA_TYPE_SIZE_BINARY_DOUBLE,
391            })
392        );
393    }
394
395    #[test]
396    fn adjust_refetch_metadata_follows_reference_rules() {
397        let column = |ora_type_num: u8, csfrm: u8| ColumnMetadata {
398            name: "VALUE".to_string(),
399            ora_type_num,
400            csfrm,
401            precision: 0,
402            scale: 0,
403            buffer_size: 4000,
404            max_size: 1000,
405            nulls_allowed: true,
406            is_json: false,
407            is_oson: false,
408            object_schema: None,
409            object_type_name: None,
410            is_array: false,
411            vector_dimensions: None,
412            vector_format: 0,
413            vector_flags: 0,
414            ..Default::default()
415        };
416
417        // VARCHAR -> CLOB fetches as LONG keeping the previous csfrm
418        let mut current = column(ORA_TYPE_NUM_CLOB, CS_FORM_IMPLICIT);
419        assert!(adjust_refetch_metadata(
420            &column(ORA_TYPE_NUM_VARCHAR, CS_FORM_IMPLICIT),
421            &mut current
422        ));
423        assert_eq!(current.ora_type_num, ORA_TYPE_NUM_LONG);
424        assert_eq!(current.csfrm, CS_FORM_IMPLICIT);
425        assert_eq!(current.buffer_size, TNS_MAX_LONG_LENGTH);
426        assert_eq!(current.max_size, 0);
427
428        // NVARCHAR -> NCLOB keeps the NCHAR character set form
429        let mut current = column(ORA_TYPE_NUM_CLOB, CS_FORM_NCHAR);
430        assert!(adjust_refetch_metadata(
431            &column(ORA_TYPE_NUM_VARCHAR, CS_FORM_NCHAR),
432            &mut current
433        ));
434        assert_eq!(current.ora_type_num, ORA_TYPE_NUM_LONG);
435        assert_eq!(current.csfrm, CS_FORM_NCHAR);
436
437        // RAW -> BLOB fetches as LONG RAW
438        let mut current = column(ORA_TYPE_NUM_BLOB, 0);
439        assert!(adjust_refetch_metadata(
440            &column(ORA_TYPE_NUM_RAW, 0),
441            &mut current
442        ));
443        assert_eq!(current.ora_type_num, ORA_TYPE_NUM_LONG_RAW);
444        assert_eq!(current.csfrm, 0);
445
446        // unrelated type changes are untouched
447        let mut current = column(ORA_TYPE_NUM_CLOB, CS_FORM_IMPLICIT);
448        assert!(!adjust_refetch_metadata(
449            &column(ORA_TYPE_NUM_NUMBER, 0),
450            &mut current
451        ));
452        assert_eq!(current.ora_type_num, ORA_TYPE_NUM_CLOB);
453        let mut current = column(ORA_TYPE_NUM_VARCHAR, CS_FORM_IMPLICIT);
454        assert!(!adjust_refetch_metadata(
455            &column(ORA_TYPE_NUM_CLOB, CS_FORM_IMPLICIT),
456            &mut current
457        ));
458        assert_eq!(current.ora_type_num, ORA_TYPE_NUM_VARCHAR);
459    }
460
461    #[test]
462    fn row_bind_metadata_keeps_raw_type_with_promoted_buffer_size() {
463        // bytes values stay RAW regardless of size (reference
464        // OracleMetadata.from_value never switches str/bytes binds to
465        // LONG/LONG_RAW); only the buffer size grows to the largest row
466        let rows = vec![
467            vec![BindValue::Raw(vec![0; 25_000])],
468            vec![BindValue::Raw(vec![0; 40_000])],
469        ];
470        let mut writer = TtcWriter::new();
471
472        let (ora_type_num, csfrm, buffer_size) =
473            write_bind_metadata_for_rows(&mut writer, &rows, 0).expect("metadata writes");
474
475        assert_eq!(ora_type_num, ORA_TYPE_NUM_RAW);
476        assert_eq!(csfrm, 0);
477        assert_eq!(buffer_size, 40_000);
478    }
479
480    #[test]
481    fn non_plsql_bind_rows_emit_long_values_last() {
482        let row = vec![
483            BindValue::Number("1".into()),
484            BindValue::Raw(vec![0; 40_000]),
485            BindValue::Number("8".into()),
486            BindValue::Text("A".repeat(40_000)),
487        ];
488        let metadata = row.iter().map(bind_metadata).collect::<Vec<_>>();
489
490        assert_eq!(
491            bind_row_value_order(&row, &metadata, false, 32_767),
492            vec![0, 2, 1, 3]
493        );
494        assert_eq!(
495            bind_row_value_order(&row, &metadata, true, 32_767),
496            vec![0, 1, 2, 3]
497        );
498    }
499
500    #[test]
501    fn lob_bind_metadata_sets_prefetch_continuation_flag() {
502        let mut writer = TtcWriter::new();
503        write_bind_metadata_with_type(
504            &mut writer,
505            &BindValue::Lob {
506                ora_type_num: ORA_TYPE_NUM_CLOB,
507                csfrm: CS_FORM_IMPLICIT,
508                locator: vec![0; 40],
509            },
510            ORA_TYPE_NUM_CLOB,
511            CS_FORM_IMPLICIT,
512            1,
513        )
514        .expect("CLOB bind metadata should encode");
515        let encoded = writer.into_bytes();
516        let mut reader = TtcReader::new(&encoded);
517
518        assert_eq!(reader.read_u8().expect("type"), ORA_TYPE_NUM_CLOB);
519        reader.skip(3).expect("flags, precision, scale");
520        assert_eq!(reader.read_ub4().expect("buffer size"), 1);
521        assert_eq!(reader.read_ub4().expect("max elements"), 0);
522        assert_eq!(
523            reader.read_ub8().expect("cont flags"),
524            TNS_LOB_PREFETCH_FLAG
525        );
526    }
527
528    #[test]
529    fn define_metadata_from_bind_preserves_clob_long_define_semantics() {
530        let mut source = number_column("VALUE");
531        source.ora_type_num = ORA_TYPE_NUM_CLOB;
532        source.csfrm = CS_FORM_NCHAR;
533        let metadata = define_metadata_from_bind(
534            &source,
535            &BindValue::TypedNull {
536                ora_type_num: ORA_TYPE_NUM_VARCHAR,
537                csfrm: CS_FORM_IMPLICIT,
538                buffer_size: 128,
539            },
540        );
541
542        assert_eq!(metadata.ora_type_num, ORA_TYPE_NUM_LONG);
543        assert_eq!(metadata.csfrm, CS_FORM_NCHAR);
544        assert_eq!(metadata.buffer_size, TNS_MAX_LONG_LENGTH);
545        assert_eq!(metadata.max_size, 0);
546    }
547
548    #[test]
549    fn output_bind_normalizes_type_metadata() {
550        assert_eq!(
551            output_bind(BindValue::Text("abc".into())),
552            BindValue::Output {
553                ora_type_num: ORA_TYPE_NUM_VARCHAR,
554                csfrm: CS_FORM_IMPLICIT,
555                buffer_size: 12,
556            }
557        );
558        assert_eq!(
559            returning_output_bind(BindValue::Null),
560            BindValue::ReturnOutput {
561                ora_type_num: ORA_TYPE_NUM_VARCHAR,
562                csfrm: CS_FORM_IMPLICIT,
563                buffer_size: 1,
564            }
565        );
566        assert!(is_cursor_bind_template(&cursor_bind_template()));
567    }
568
569    #[test]
570    fn public_dbtype_names_come_from_protocol_metadata() {
571        assert_eq!(
572            public_dbtype_name_from_type_name("NATIVE_FLOAT"),
573            "DB_TYPE_BINARY_DOUBLE"
574        );
575        assert_eq!(
576            public_dbtype_name_from_bind(&BindValue::BinaryDouble(1.25)),
577            "DB_TYPE_BINARY_DOUBLE"
578        );
579        assert_eq!(
580            public_dbtype_name_from_bind(&BindValue::TypedNull {
581                ora_type_num: ORA_TYPE_NUM_VARCHAR,
582                csfrm: CS_FORM_NCHAR,
583                buffer_size: 16,
584            }),
585            "DB_TYPE_NVARCHAR"
586        );
587    }
588
589    #[test]
590    fn public_dbtype_names_from_column_metadata_preserve_fetch_semantics() {
591        let mut metadata = number_column("VALUE");
592        assert_eq!(
593            public_dbtype_name_from_column_metadata(&metadata),
594            "DB_TYPE_NUMBER"
595        );
596
597        metadata.ora_type_num = ORA_TYPE_NUM_CHAR;
598        metadata.csfrm = CS_FORM_NCHAR;
599        assert_eq!(
600            public_dbtype_name_from_column_metadata(&metadata),
601            "DB_TYPE_NCHAR"
602        );
603
604        metadata.ora_type_num = ORA_TYPE_NUM_VARCHAR;
605        assert_eq!(
606            public_dbtype_name_from_column_metadata(&metadata),
607            "DB_TYPE_NVARCHAR"
608        );
609
610        metadata.ora_type_num = ORA_TYPE_NUM_CLOB;
611        assert_eq!(
612            public_dbtype_name_from_column_metadata(&metadata),
613            "DB_TYPE_NCLOB"
614        );
615
616        metadata.csfrm = CS_FORM_IMPLICIT;
617        for (ora_type_num, expected) in [
618            (ORA_TYPE_NUM_LONG, "DB_TYPE_LONG"),
619            (ORA_TYPE_NUM_LONG_RAW, "DB_TYPE_LONG_RAW"),
620            (ORA_TYPE_NUM_ROWID, "DB_TYPE_ROWID"),
621            (ORA_TYPE_NUM_UROWID, "DB_TYPE_UROWID"),
622            (ORA_TYPE_NUM_TIMESTAMP, "DB_TYPE_TIMESTAMP"),
623            (ORA_TYPE_NUM_TIMESTAMP_LTZ, "DB_TYPE_TIMESTAMP_LTZ"),
624            (ORA_TYPE_NUM_TIMESTAMP_TZ, "DB_TYPE_TIMESTAMP_TZ"),
625            (ORA_TYPE_NUM_BFILE, "DB_TYPE_BFILE"),
626        ] {
627            metadata.ora_type_num = ora_type_num;
628            assert_eq!(public_dbtype_name_from_column_metadata(&metadata), expected);
629        }
630
631        metadata.ora_type_num = ORA_TYPE_NUM_OBJECT;
632        metadata.object_schema = Some("SYS".into());
633        metadata.object_type_name = Some("XMLTYPE".into());
634        assert!(column_metadata_is_xmltype(&metadata));
635        assert_eq!(
636            public_dbtype_name_from_column_metadata(&metadata),
637            "DB_TYPE_XMLTYPE"
638        );
639    }
640
641    #[test]
642    fn oracle_dictionary_type_metadata_is_protocol_owned() {
643        assert_eq!(
644            public_dbtype_name_from_oracle_type_name("timestamp with local time zone"),
645            "DB_TYPE_TIMESTAMP_LTZ"
646        );
647        assert_eq!(
648            public_dbtype_name_from_oracle_type_name("TIMESTAMP WITH TZ"),
649            "DB_TYPE_TIMESTAMP_TZ"
650        );
651        assert_eq!(
652            public_dbtype_name_from_oracle_type_name("BINARY_FLOAT"),
653            "DB_TYPE_BINARY_FLOAT"
654        );
655        assert_eq!(
656            public_dbtype_name_from_oracle_type_name("UDT_OBJECT"),
657            "DB_TYPE_OBJECT"
658        );
659        // PL/SQL scalar attribute/element type names must NOT fall through to
660        // the DB_TYPE_OBJECT ADT fallback (Wave 3 BUG 1).
661        for (name, expected) in [
662            ("BOOLEAN", "DB_TYPE_BOOLEAN"),
663            ("PL/SQL BOOLEAN", "DB_TYPE_BOOLEAN"),
664            ("PL/SQL PLS INTEGER", "DB_TYPE_BINARY_INTEGER"),
665            ("PL/SQL BINARY INTEGER", "DB_TYPE_BINARY_INTEGER"),
666            ("BINARY_INTEGER", "DB_TYPE_BINARY_INTEGER"),
667            ("PLS_INTEGER", "DB_TYPE_BINARY_INTEGER"),
668            ("INTERVAL DAY TO SECOND", "DB_TYPE_INTERVAL_DS"),
669            ("INTERVAL YEAR TO MONTH", "DB_TYPE_INTERVAL_YM"),
670        ] {
671            assert_eq!(public_dbtype_name_from_oracle_type_name(name), expected);
672        }
673
674        assert_eq!(
675            dbobject_attr_precision_scale("NUMBER", None, Some(0)),
676            (38, 0)
677        );
678        assert_eq!(
679            dbobject_attr_precision_scale("NUMBER", None, None),
680            (0, -127)
681        );
682        assert_eq!(
683            dbobject_attr_precision_scale("DOUBLE PRECISION", None, None),
684            (126, -127)
685        );
686        assert_eq!(dbobject_attr_max_size("NVARCHAR2", Some(10)), 20);
687        assert_eq!(
688            dbobject_rowtype_attr_max_size("NVARCHAR2", Some(40), Some(7)),
689            14
690        );
691        assert_eq!(
692            dbobject_rowtype_attr_max_size("NVARCHAR2", Some(40), Some(0)),
693            80
694        );
695        assert_eq!(dbobject_rowtype_attr_max_size("NUMBER", Some(22), None), 0);
696    }
697
698    #[test]
699    fn bind_templates_are_protocol_owned() {
700        assert_eq!(
701            bind_template_from_type_name("DB_TYPE_NCLOB", 0),
702            BindValue::TypedNull {
703                ora_type_num: ORA_TYPE_NUM_LONG,
704                csfrm: CS_FORM_NCHAR,
705                buffer_size: TNS_MAX_LONG_LENGTH,
706            }
707        );
708        assert_eq!(
709            bind_template_from_type_name("DB_TYPE_BLOB", 0),
710            BindValue::TypedNull {
711                ora_type_num: ORA_TYPE_NUM_LONG_RAW,
712                csfrm: 0,
713                buffer_size: TNS_MAX_LONG_LENGTH,
714            }
715        );
716        assert_eq!(
717            dbobject_element_bind_type_info("DB_TYPE_NCHAR", 12),
718            BindTypeInfo {
719                ora_type_num: ORA_TYPE_NUM_VARCHAR,
720                csfrm: CS_FORM_NCHAR,
721                buffer_size: 4000,
722            }
723        );
724    }
725
726    #[test]
727    fn dbobject_packed_reader_decodes_header_lengths_and_nulls() {
728        let bytes = [
729            TNS_OBJ_NO_PREFIX_SEG,
730            1,
731            0,
732            4,
733            b't',
734            b'e',
735            b's',
736            b't',
737            TNS_OBJ_ATOMIC_NULL,
738        ];
739        let mut reader = DbObjectPackedReader::new(&bytes);
740        reader.read_header().expect("header should decode");
741        assert_eq!(
742            reader
743                .read_value_bytes()
744                .expect("value bytes should decode"),
745            Some(b"test".to_vec())
746        );
747        assert!(reader
748            .read_atomic_null(false)
749            .expect("atomic null should decode"));
750    }
751
752    #[test]
753    fn dbobject_scalar_decoders_match_oracle_canonical_data() {
754        assert_eq!(
755            decode_dbobject_text(&[0, b'A'], "DB_TYPE_NCHAR").expect("nchar text"),
756            "A"
757        );
758        assert_eq!(
759            decode_dbobject_xmltype_text(&[
760                TNS_OBJ_NO_PREFIX_SEG,
761                1,
762                0,
763                0,
764                0,
765                0,
766                0,
767                TNS_XML_TYPE_STRING as u8,
768                b'<',
769                b'x',
770                b'/',
771                b'>',
772            ])
773            .expect("XMLTYPE text should decode"),
774            Some("<x/>".to_string())
775        );
776        assert_eq!(
777            decode_dbobject_binary_float(&[0xbf, 0x80, 0, 0]).expect("binary float"),
778            1.0
779        );
780        assert_eq!(
781            decode_dbobject_binary_double(&[0xbf, 0xf0, 0, 0, 0, 0, 0, 0]).expect("binary double"),
782            1.0
783        );
784    }
785
786    #[test]
787    fn lob_text_encoding_uses_csfrm_and_locator_flags() {
788        assert_eq!(
789            decode_lob_text(b"Plain", CS_FORM_IMPLICIT, None).expect("utf8 lob"),
790            "Plain"
791        );
792        assert_eq!(
793            encode_lob_text("Text", CS_FORM_IMPLICIT, None),
794            b"Text".to_vec()
795        );
796        assert_eq!(
797            encode_lob_text("AB", CS_FORM_NCHAR, None),
798            vec![0, b'A', 0, b'B']
799        );
800        assert_eq!(
801            decode_lob_text(&[0, b'A', 0, b'B'], CS_FORM_NCHAR, None).expect("nchar lob"),
802            "AB"
803        );
804
805        let mut locator = vec![0; 8];
806        locator[TNS_LOB_LOC_OFFSET_FLAG_3] = TNS_LOB_LOC_FLAGS_VAR_LENGTH_CHARSET;
807        locator[TNS_LOB_LOC_OFFSET_FLAG_4] = TNS_LOB_LOC_FLAGS_LITTLE_ENDIAN;
808        assert_eq!(
809            encode_lob_text("AB", CS_FORM_IMPLICIT, Some(&locator)),
810            vec![b'A', 0, b'B', 0]
811        );
812        assert_eq!(
813            decode_lob_text(&[b'A', 0, b'B', 0], CS_FORM_IMPLICIT, Some(&locator))
814                .expect("locator utf16 lob"),
815            "AB"
816        );
817    }
818
819    #[test]
820    fn bfile_locator_name_decodes_directory_and_file_tail() {
821        let locator = Vec::from_hex(
822            "0808000000010000000000000015544553545f313933365f4d495353494e475f444952\
823             001a746573745f313933365f6d697373696e675f66696c652e747874",
824        )
825        .expect("BFILE locator fixture should be valid hex");
826
827        assert_eq!(
828            decode_bfile_locator_name(&locator),
829            Some((
830                "TEST_1936_MISSING_DIR".to_string(),
831                "test_1936_missing_file.txt".to_string()
832            ))
833        );
834    }
835
836    fn number_column(name: &str) -> ColumnMetadata {
837        ColumnMetadata {
838            name: name.into(),
839            ora_type_num: ORA_TYPE_NUM_NUMBER,
840            csfrm: CS_FORM_IMPLICIT,
841            precision: 0,
842            scale: 0,
843            buffer_size: ORA_TYPE_SIZE_NUMBER,
844            max_size: ORA_TYPE_SIZE_NUMBER,
845            nulls_allowed: true,
846            is_json: false,
847            is_oson: false,
848            object_schema: None,
849            object_type_name: None,
850            is_array: false,
851            vector_dimensions: None,
852            vector_format: 0,
853            vector_flags: 0,
854            ..Default::default()
855        }
856    }
857
858    fn char_column(name: &str) -> ColumnMetadata {
859        ColumnMetadata {
860            name: name.into(),
861            ora_type_num: ORA_TYPE_NUM_CHAR,
862            csfrm: CS_FORM_IMPLICIT,
863            precision: 0,
864            scale: 0,
865            buffer_size: 2000,
866            max_size: 2000,
867            nulls_allowed: true,
868            is_json: false,
869            is_oson: false,
870            object_schema: None,
871            object_type_name: None,
872            is_array: false,
873            vector_dimensions: None,
874            vector_format: 0,
875            vector_flags: 0,
876            ..Default::default()
877        }
878    }
879
880    fn long_column(name: &str) -> ColumnMetadata {
881        ColumnMetadata {
882            name: name.into(),
883            ora_type_num: ORA_TYPE_NUM_LONG,
884            csfrm: CS_FORM_IMPLICIT,
885            precision: 0,
886            scale: 0,
887            buffer_size: TNS_MAX_LONG_LENGTH,
888            max_size: 0,
889            nulls_allowed: true,
890            is_json: false,
891            is_oson: false,
892            object_schema: None,
893            object_type_name: None,
894            is_array: false,
895            vector_dimensions: None,
896            vector_format: 0,
897            vector_flags: 0,
898            ..Default::default()
899        }
900    }
901}