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    #[test]
113    fn fetch_response_decodes_rows_with_previous_cursor_metadata() {
114        let payload = Vec::from_hex("06020101000205dc0001010101000702c1041d")
115            .expect("fixture response should be valid hex");
116        let columns = vec![number_column("INTCOL"), number_column("NUMBERCOL")];
117        let previous_row = vec![
118            Some(QueryValue::number_from_text("2", true)),
119            Some(QueryValue::number_from_text("0.5", false)),
120        ];
121
122        let parsed = parse_query_response_with_context(
123            &payload,
124            ClientCapabilities::default(),
125            &columns,
126            Some(&previous_row),
127        )
128        .expect("fetch response should decode using cached cursor metadata");
129
130        assert_eq!(parsed.columns, columns);
131        assert_eq!(
132            parsed.rows,
133            vec![vec![
134                Some(QueryValue::number_from_text("3", true)),
135                previous_row[1].clone(),
136            ]]
137        );
138    }
139
140    #[test]
141    fn fetch_response_skips_long_status_fields() {
142        let payload =
143            Vec::from_hex("07036162638101001d").expect("fixture response should be valid hex");
144        let columns = vec![long_column("LONGCOL")];
145
146        let parsed = parse_fetch_response_with_context(
147            &payload,
148            ClientCapabilities::default(),
149            &columns,
150            None,
151        )
152        .expect("fetch response should consume LONG status fields");
153
154        assert_eq!(
155            parsed.rows,
156            vec![vec![Some(QueryValue::Text("abc".into()))]]
157        );
158    }
159
160    #[test]
161    fn fetch_response_decodes_mid_row_oracle_error() {
162        let payload = Vec::from_hex(concat!(
163            "150101010703c20401010205100205db0205c400000106018f030000000000",
164            "0301214d0118000293b60201c60000080000000000000205db0205c40103",
165            "00244f52412d30313437363a2064697669736f7220697320657175616c20",
166            "746f207a65726f0a1d",
167        ))
168        .expect("fixture response should be valid hex");
169        let columns = vec![number_column("INTCOL"), number_column("NUMBERCOL")];
170        let previous_row = vec![
171            Some(QueryValue::number_from_text("1499", true)),
172            Some(QueryValue::number_from_text("0.5", false)),
173        ];
174
175        let err = parse_query_response_with_context(
176            &payload,
177            ClientCapabilities::default(),
178            &columns,
179            Some(&previous_row),
180        )
181        .expect_err("mid-row error info should surface as a server error");
182
183        assert_eq!(
184            err.to_string(),
185            "server returned Oracle error: ORA-01476: divisor is equal to zero"
186        );
187    }
188
189    #[test]
190    fn lob_read_payload_writes_modern_token_field() {
191        let locator = [0x00, 0x70, 0xaa];
192        let modern =
193            build_lob_read_payload_with_seq(&locator, 1, 5, 8, TNS_CCAP_FIELD_VERSION_23_1_EXT_1)
194                .expect("LOB read payload should encode");
195        assert_eq!(
196            &modern[..7],
197            &[TNS_MSG_TYPE_FUNCTION, TNS_FUNC_LOB_OP, 8, 0, 1, 1, 3]
198        );
199
200        let legacy =
201            build_lob_read_payload_with_seq(&locator, 1, 5, 8, TNS_CCAP_FIELD_VERSION_23_1)
202                .expect("LOB read payload should encode");
203        assert_eq!(
204            &legacy[..6],
205            &[TNS_MSG_TYPE_FUNCTION, TNS_FUNC_LOB_OP, 8, 1, 1, 3]
206        );
207    }
208
209    #[test]
210    fn lob_locator_temporary_flags_match_reference_offsets() {
211        let mut locator = vec![0; 40];
212        assert!(!lob_locator_is_temporary(&locator));
213
214        locator[TNS_LOB_LOC_OFFSET_FLAG_1] = TNS_LOB_LOC_FLAGS_ABSTRACT;
215        assert!(lob_locator_is_temporary(&locator));
216
217        locator[TNS_LOB_LOC_OFFSET_FLAG_1] = 0;
218        locator[TNS_LOB_LOC_OFFSET_FLAG_4] = TNS_LOB_LOC_FLAGS_TEMP;
219        assert!(lob_locator_is_temporary(&locator));
220    }
221
222    #[test]
223    fn lob_free_temp_payload_writes_array_free_operation() {
224        let locator = vec![0xaa; 40];
225        let payload = build_lob_free_temp_payload_with_seq(
226            std::slice::from_ref(&locator),
227            9,
228            TNS_CCAP_FIELD_VERSION_23_1_EXT_1,
229        )
230        .expect("LOB free-temp payload should encode");
231
232        assert_eq!(
233            &payload[..19],
234            &[
235                TNS_MSG_TYPE_FUNCTION,
236                TNS_FUNC_LOB_OP,
237                9,
238                0,
239                1,
240                1,
241                40,
242                0,
243                0,
244                0,
245                0,
246                0,
247                0,
248                0,
249                4,
250                0,
251                8,
252                1,
253                0x11,
254            ]
255        );
256        assert!(payload.ends_with(&locator));
257    }
258
259    #[test]
260    fn lob_free_temp_response_skips_returned_locator_parameter() {
261        let payload = Vec::from_hex(concat!(
262            "0800260000020080000002ee5500000044000000030369000a000000000002",
263            "5295f656000000010000040101021a390000000000000000000000000000",
264            "00000000000a000000000000000000001d",
265        ))
266        .expect("fixture response should be valid hex");
267
268        parse_lob_free_temp_response(&payload, ClientCapabilities::default(), 40)
269            .expect("free-temp response should consume returned locator");
270    }
271
272    #[test]
273    fn rowid_value_decodes_physical_rowid() {
274        let mut reader = TtcReader::new(&[
275            13, // non-null rowid marker
276            1, 1, // rba
277            1, 2, // partition id
278            0, // ignored padding byte
279            1, 3, // block number
280            1, 4, // slot number
281        ]);
282
283        let value = parse_rowid_value(&mut reader).expect("physical rowid should decode");
284
285        assert_eq!(value.as_deref(), Some("AAAAABAACAAAAADAAE"));
286        assert_eq!(reader.remaining(), 0);
287    }
288
289    #[test]
290    fn urowid_value_decodes_physical_rowid() {
291        let mut reader = TtcReader::new(&[
292            1, 13, // ignored first length buffer
293            13, // second buffer length
294            1,  // physical rowid marker
295            0, 0, 0, 1, // rba
296            0, 2, // partition id
297            0, 0, 0, 3, // block number
298            0, 4, // slot number
299        ]);
300
301        let value = parse_urowid_value(&mut reader).expect("physical urowid should decode");
302
303        assert_eq!(value.as_deref(), Some("AAAAABAACAAAAADAAE"));
304        assert_eq!(reader.remaining(), 0);
305    }
306
307    #[test]
308    fn urowid_value_decodes_logical_rowid() {
309        let mut reader = TtcReader::new(&[
310            1, 13, // ignored first length buffer
311            13, // second buffer length
312            0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
313        ]);
314
315        let value = parse_urowid_value(&mut reader).expect("logical urowid should decode");
316
317        assert_eq!(value.as_deref(), Some("*AQIDBAUGBwgJCgsM"));
318        assert_eq!(reader.remaining(), 0);
319    }
320
321    #[test]
322    fn binary_double_round_trips_oracle_canonical_bytes() {
323        for value in [0.0, -0.0, 1.5, -2.25, f64::INFINITY, f64::NEG_INFINITY] {
324            let decoded = decode_binary_double(&encode_binary_double(value))
325                .expect("BINARY_DOUBLE should round trip");
326            assert_eq!(decoded.to_bits(), value.to_bits());
327        }
328
329        let decoded =
330            decode_binary_double(&encode_binary_double(f64::NAN)).expect("NaN should decode");
331        assert!(decoded.is_nan());
332    }
333
334    #[test]
335    fn bind_value_type_info_reports_protocol_metadata() {
336        assert_eq!(bind_value_type_info(&BindValue::Null), None);
337        assert_eq!(
338            bind_value_type_info(&BindValue::Text("abc".into())),
339            Some(BindTypeInfo {
340                ora_type_num: ORA_TYPE_NUM_VARCHAR,
341                csfrm: CS_FORM_IMPLICIT,
342                buffer_size: 12,
343            })
344        );
345        assert_eq!(
346            bind_value_type_info(&BindValue::BinaryDouble(1.25)),
347            Some(BindTypeInfo {
348                ora_type_num: ORA_TYPE_NUM_BINARY_DOUBLE,
349                csfrm: 0,
350                buffer_size: ORA_TYPE_SIZE_BINARY_DOUBLE,
351            })
352        );
353    }
354
355    #[test]
356    fn adjust_refetch_metadata_follows_reference_rules() {
357        let column = |ora_type_num: u8, csfrm: u8| ColumnMetadata {
358            name: "VALUE".to_string(),
359            ora_type_num,
360            csfrm,
361            precision: 0,
362            scale: 0,
363            buffer_size: 4000,
364            max_size: 1000,
365            nulls_allowed: true,
366            is_json: false,
367            is_oson: false,
368            object_schema: None,
369            object_type_name: None,
370            is_array: false,
371            vector_dimensions: None,
372            vector_format: 0,
373            vector_flags: 0,
374            ..Default::default()
375        };
376
377        // VARCHAR -> CLOB fetches as LONG keeping the previous csfrm
378        let mut current = column(ORA_TYPE_NUM_CLOB, CS_FORM_IMPLICIT);
379        assert!(adjust_refetch_metadata(
380            &column(ORA_TYPE_NUM_VARCHAR, CS_FORM_IMPLICIT),
381            &mut current
382        ));
383        assert_eq!(current.ora_type_num, ORA_TYPE_NUM_LONG);
384        assert_eq!(current.csfrm, CS_FORM_IMPLICIT);
385        assert_eq!(current.buffer_size, TNS_MAX_LONG_LENGTH);
386        assert_eq!(current.max_size, 0);
387
388        // NVARCHAR -> NCLOB keeps the NCHAR character set form
389        let mut current = column(ORA_TYPE_NUM_CLOB, CS_FORM_NCHAR);
390        assert!(adjust_refetch_metadata(
391            &column(ORA_TYPE_NUM_VARCHAR, CS_FORM_NCHAR),
392            &mut current
393        ));
394        assert_eq!(current.ora_type_num, ORA_TYPE_NUM_LONG);
395        assert_eq!(current.csfrm, CS_FORM_NCHAR);
396
397        // RAW -> BLOB fetches as LONG RAW
398        let mut current = column(ORA_TYPE_NUM_BLOB, 0);
399        assert!(adjust_refetch_metadata(
400            &column(ORA_TYPE_NUM_RAW, 0),
401            &mut current
402        ));
403        assert_eq!(current.ora_type_num, ORA_TYPE_NUM_LONG_RAW);
404        assert_eq!(current.csfrm, 0);
405
406        // unrelated type changes are untouched
407        let mut current = column(ORA_TYPE_NUM_CLOB, CS_FORM_IMPLICIT);
408        assert!(!adjust_refetch_metadata(
409            &column(ORA_TYPE_NUM_NUMBER, 0),
410            &mut current
411        ));
412        assert_eq!(current.ora_type_num, ORA_TYPE_NUM_CLOB);
413        let mut current = column(ORA_TYPE_NUM_VARCHAR, CS_FORM_IMPLICIT);
414        assert!(!adjust_refetch_metadata(
415            &column(ORA_TYPE_NUM_CLOB, CS_FORM_IMPLICIT),
416            &mut current
417        ));
418        assert_eq!(current.ora_type_num, ORA_TYPE_NUM_VARCHAR);
419    }
420
421    #[test]
422    fn row_bind_metadata_keeps_raw_type_with_promoted_buffer_size() {
423        // bytes values stay RAW regardless of size (reference
424        // OracleMetadata.from_value never switches str/bytes binds to
425        // LONG/LONG_RAW); only the buffer size grows to the largest row
426        let rows = vec![
427            vec![BindValue::Raw(vec![0; 25_000])],
428            vec![BindValue::Raw(vec![0; 40_000])],
429        ];
430        let mut writer = TtcWriter::new();
431
432        let (ora_type_num, csfrm, buffer_size) =
433            write_bind_metadata_for_rows(&mut writer, &rows, 0).expect("metadata writes");
434
435        assert_eq!(ora_type_num, ORA_TYPE_NUM_RAW);
436        assert_eq!(csfrm, 0);
437        assert_eq!(buffer_size, 40_000);
438    }
439
440    #[test]
441    fn non_plsql_bind_rows_emit_long_values_last() {
442        let row = vec![
443            BindValue::Number("1".into()),
444            BindValue::Raw(vec![0; 40_000]),
445            BindValue::Number("8".into()),
446            BindValue::Text("A".repeat(40_000)),
447        ];
448        let metadata = row.iter().map(bind_metadata).collect::<Vec<_>>();
449
450        assert_eq!(
451            bind_row_value_order(&row, &metadata, false),
452            vec![0, 2, 1, 3]
453        );
454        assert_eq!(
455            bind_row_value_order(&row, &metadata, true),
456            vec![0, 1, 2, 3]
457        );
458    }
459
460    #[test]
461    fn lob_bind_metadata_sets_prefetch_continuation_flag() {
462        let mut writer = TtcWriter::new();
463        write_bind_metadata_with_type(
464            &mut writer,
465            &BindValue::Lob {
466                ora_type_num: ORA_TYPE_NUM_CLOB,
467                csfrm: CS_FORM_IMPLICIT,
468                locator: vec![0; 40],
469            },
470            ORA_TYPE_NUM_CLOB,
471            CS_FORM_IMPLICIT,
472            1,
473        )
474        .expect("CLOB bind metadata should encode");
475        let encoded = writer.into_bytes();
476        let mut reader = TtcReader::new(&encoded);
477
478        assert_eq!(reader.read_u8().expect("type"), ORA_TYPE_NUM_CLOB);
479        reader.skip(3).expect("flags, precision, scale");
480        assert_eq!(reader.read_ub4().expect("buffer size"), 1);
481        assert_eq!(reader.read_ub4().expect("max elements"), 0);
482        assert_eq!(
483            reader.read_ub8().expect("cont flags"),
484            TNS_LOB_PREFETCH_FLAG
485        );
486    }
487
488    #[test]
489    fn define_metadata_from_bind_preserves_clob_long_define_semantics() {
490        let mut source = number_column("VALUE");
491        source.ora_type_num = ORA_TYPE_NUM_CLOB;
492        source.csfrm = CS_FORM_NCHAR;
493        let metadata = define_metadata_from_bind(
494            &source,
495            &BindValue::TypedNull {
496                ora_type_num: ORA_TYPE_NUM_VARCHAR,
497                csfrm: CS_FORM_IMPLICIT,
498                buffer_size: 128,
499            },
500        );
501
502        assert_eq!(metadata.ora_type_num, ORA_TYPE_NUM_LONG);
503        assert_eq!(metadata.csfrm, CS_FORM_NCHAR);
504        assert_eq!(metadata.buffer_size, TNS_MAX_LONG_LENGTH);
505        assert_eq!(metadata.max_size, 0);
506    }
507
508    #[test]
509    fn output_bind_normalizes_type_metadata() {
510        assert_eq!(
511            output_bind(BindValue::Text("abc".into())),
512            BindValue::Output {
513                ora_type_num: ORA_TYPE_NUM_VARCHAR,
514                csfrm: CS_FORM_IMPLICIT,
515                buffer_size: 12,
516            }
517        );
518        assert_eq!(
519            returning_output_bind(BindValue::Null),
520            BindValue::ReturnOutput {
521                ora_type_num: ORA_TYPE_NUM_VARCHAR,
522                csfrm: CS_FORM_IMPLICIT,
523                buffer_size: 1,
524            }
525        );
526        assert!(is_cursor_bind_template(&cursor_bind_template()));
527    }
528
529    #[test]
530    fn public_dbtype_names_come_from_protocol_metadata() {
531        assert_eq!(
532            public_dbtype_name_from_type_name("NATIVE_FLOAT"),
533            "DB_TYPE_BINARY_DOUBLE"
534        );
535        assert_eq!(
536            public_dbtype_name_from_bind(&BindValue::BinaryDouble(1.25)),
537            "DB_TYPE_BINARY_DOUBLE"
538        );
539        assert_eq!(
540            public_dbtype_name_from_bind(&BindValue::TypedNull {
541                ora_type_num: ORA_TYPE_NUM_VARCHAR,
542                csfrm: CS_FORM_NCHAR,
543                buffer_size: 16,
544            }),
545            "DB_TYPE_NVARCHAR"
546        );
547    }
548
549    #[test]
550    fn public_dbtype_names_from_column_metadata_preserve_fetch_semantics() {
551        let mut metadata = number_column("VALUE");
552        assert_eq!(
553            public_dbtype_name_from_column_metadata(&metadata),
554            "DB_TYPE_NUMBER"
555        );
556
557        metadata.ora_type_num = ORA_TYPE_NUM_CHAR;
558        metadata.csfrm = CS_FORM_NCHAR;
559        assert_eq!(
560            public_dbtype_name_from_column_metadata(&metadata),
561            "DB_TYPE_NCHAR"
562        );
563
564        metadata.ora_type_num = ORA_TYPE_NUM_VARCHAR;
565        assert_eq!(
566            public_dbtype_name_from_column_metadata(&metadata),
567            "DB_TYPE_NVARCHAR"
568        );
569
570        metadata.ora_type_num = ORA_TYPE_NUM_CLOB;
571        assert_eq!(
572            public_dbtype_name_from_column_metadata(&metadata),
573            "DB_TYPE_NCLOB"
574        );
575
576        metadata.csfrm = CS_FORM_IMPLICIT;
577        for (ora_type_num, expected) in [
578            (ORA_TYPE_NUM_LONG, "DB_TYPE_LONG"),
579            (ORA_TYPE_NUM_LONG_RAW, "DB_TYPE_LONG_RAW"),
580            (ORA_TYPE_NUM_ROWID, "DB_TYPE_ROWID"),
581            (ORA_TYPE_NUM_UROWID, "DB_TYPE_UROWID"),
582            (ORA_TYPE_NUM_TIMESTAMP, "DB_TYPE_TIMESTAMP"),
583            (ORA_TYPE_NUM_TIMESTAMP_LTZ, "DB_TYPE_TIMESTAMP_LTZ"),
584            (ORA_TYPE_NUM_TIMESTAMP_TZ, "DB_TYPE_TIMESTAMP_TZ"),
585            (ORA_TYPE_NUM_BFILE, "DB_TYPE_BFILE"),
586        ] {
587            metadata.ora_type_num = ora_type_num;
588            assert_eq!(public_dbtype_name_from_column_metadata(&metadata), expected);
589        }
590
591        metadata.ora_type_num = ORA_TYPE_NUM_OBJECT;
592        metadata.object_schema = Some("SYS".into());
593        metadata.object_type_name = Some("XMLTYPE".into());
594        assert!(column_metadata_is_xmltype(&metadata));
595        assert_eq!(
596            public_dbtype_name_from_column_metadata(&metadata),
597            "DB_TYPE_XMLTYPE"
598        );
599    }
600
601    #[test]
602    fn oracle_dictionary_type_metadata_is_protocol_owned() {
603        assert_eq!(
604            public_dbtype_name_from_oracle_type_name("timestamp with local time zone"),
605            "DB_TYPE_TIMESTAMP_LTZ"
606        );
607        assert_eq!(
608            public_dbtype_name_from_oracle_type_name("TIMESTAMP WITH TZ"),
609            "DB_TYPE_TIMESTAMP_TZ"
610        );
611        assert_eq!(
612            public_dbtype_name_from_oracle_type_name("BINARY_FLOAT"),
613            "DB_TYPE_BINARY_FLOAT"
614        );
615        assert_eq!(
616            public_dbtype_name_from_oracle_type_name("UDT_OBJECT"),
617            "DB_TYPE_OBJECT"
618        );
619        // PL/SQL scalar attribute/element type names must NOT fall through to
620        // the DB_TYPE_OBJECT ADT fallback (Wave 3 BUG 1).
621        for (name, expected) in [
622            ("BOOLEAN", "DB_TYPE_BOOLEAN"),
623            ("PL/SQL BOOLEAN", "DB_TYPE_BOOLEAN"),
624            ("PL/SQL PLS INTEGER", "DB_TYPE_BINARY_INTEGER"),
625            ("PL/SQL BINARY INTEGER", "DB_TYPE_BINARY_INTEGER"),
626            ("BINARY_INTEGER", "DB_TYPE_BINARY_INTEGER"),
627            ("PLS_INTEGER", "DB_TYPE_BINARY_INTEGER"),
628            ("INTERVAL DAY TO SECOND", "DB_TYPE_INTERVAL_DS"),
629            ("INTERVAL YEAR TO MONTH", "DB_TYPE_INTERVAL_YM"),
630        ] {
631            assert_eq!(public_dbtype_name_from_oracle_type_name(name), expected);
632        }
633
634        assert_eq!(
635            dbobject_attr_precision_scale("NUMBER", None, Some(0)),
636            (38, 0)
637        );
638        assert_eq!(
639            dbobject_attr_precision_scale("NUMBER", None, None),
640            (0, -127)
641        );
642        assert_eq!(
643            dbobject_attr_precision_scale("DOUBLE PRECISION", None, None),
644            (126, -127)
645        );
646        assert_eq!(dbobject_attr_max_size("NVARCHAR2", Some(10)), 20);
647        assert_eq!(
648            dbobject_rowtype_attr_max_size("NVARCHAR2", Some(40), Some(7)),
649            14
650        );
651        assert_eq!(
652            dbobject_rowtype_attr_max_size("NVARCHAR2", Some(40), Some(0)),
653            80
654        );
655        assert_eq!(dbobject_rowtype_attr_max_size("NUMBER", Some(22), None), 0);
656    }
657
658    #[test]
659    fn bind_templates_are_protocol_owned() {
660        assert_eq!(
661            bind_template_from_type_name("DB_TYPE_NCLOB", 0),
662            BindValue::TypedNull {
663                ora_type_num: ORA_TYPE_NUM_LONG,
664                csfrm: CS_FORM_NCHAR,
665                buffer_size: TNS_MAX_LONG_LENGTH,
666            }
667        );
668        assert_eq!(
669            bind_template_from_type_name("DB_TYPE_BLOB", 0),
670            BindValue::TypedNull {
671                ora_type_num: ORA_TYPE_NUM_LONG_RAW,
672                csfrm: 0,
673                buffer_size: TNS_MAX_LONG_LENGTH,
674            }
675        );
676        assert_eq!(
677            dbobject_element_bind_type_info("DB_TYPE_NCHAR", 12),
678            BindTypeInfo {
679                ora_type_num: ORA_TYPE_NUM_VARCHAR,
680                csfrm: CS_FORM_NCHAR,
681                buffer_size: 4000,
682            }
683        );
684    }
685
686    #[test]
687    fn dbobject_packed_reader_decodes_header_lengths_and_nulls() {
688        let bytes = [
689            TNS_OBJ_NO_PREFIX_SEG,
690            1,
691            0,
692            4,
693            b't',
694            b'e',
695            b's',
696            b't',
697            TNS_OBJ_ATOMIC_NULL,
698        ];
699        let mut reader = DbObjectPackedReader::new(&bytes);
700        reader.read_header().expect("header should decode");
701        assert_eq!(
702            reader
703                .read_value_bytes()
704                .expect("value bytes should decode"),
705            Some(b"test".to_vec())
706        );
707        assert!(reader
708            .read_atomic_null(false)
709            .expect("atomic null should decode"));
710    }
711
712    #[test]
713    fn dbobject_scalar_decoders_match_oracle_canonical_data() {
714        assert_eq!(
715            decode_dbobject_text(&[0, b'A'], "DB_TYPE_NCHAR").expect("nchar text"),
716            "A"
717        );
718        assert_eq!(
719            decode_dbobject_xmltype_text(&[
720                TNS_OBJ_NO_PREFIX_SEG,
721                1,
722                0,
723                0,
724                0,
725                0,
726                0,
727                TNS_XML_TYPE_STRING as u8,
728                b'<',
729                b'x',
730                b'/',
731                b'>',
732            ])
733            .expect("XMLTYPE text should decode"),
734            Some("<x/>".to_string())
735        );
736        assert_eq!(
737            decode_dbobject_binary_float(&[0xbf, 0x80, 0, 0]).expect("binary float"),
738            1.0
739        );
740        assert_eq!(
741            decode_dbobject_binary_double(&[0xbf, 0xf0, 0, 0, 0, 0, 0, 0]).expect("binary double"),
742            1.0
743        );
744    }
745
746    #[test]
747    fn lob_text_encoding_uses_csfrm_and_locator_flags() {
748        assert_eq!(
749            decode_lob_text(b"Plain", CS_FORM_IMPLICIT, None).expect("utf8 lob"),
750            "Plain"
751        );
752        assert_eq!(
753            encode_lob_text("Text", CS_FORM_IMPLICIT, None),
754            b"Text".to_vec()
755        );
756        assert_eq!(
757            encode_lob_text("AB", CS_FORM_NCHAR, None),
758            vec![0, b'A', 0, b'B']
759        );
760        assert_eq!(
761            decode_lob_text(&[0, b'A', 0, b'B'], CS_FORM_NCHAR, None).expect("nchar lob"),
762            "AB"
763        );
764
765        let mut locator = vec![0; 8];
766        locator[TNS_LOB_LOC_OFFSET_FLAG_3] = TNS_LOB_LOC_FLAGS_VAR_LENGTH_CHARSET;
767        locator[TNS_LOB_LOC_OFFSET_FLAG_4] = TNS_LOB_LOC_FLAGS_LITTLE_ENDIAN;
768        assert_eq!(
769            encode_lob_text("AB", CS_FORM_IMPLICIT, Some(&locator)),
770            vec![b'A', 0, b'B', 0]
771        );
772        assert_eq!(
773            decode_lob_text(&[b'A', 0, b'B', 0], CS_FORM_IMPLICIT, Some(&locator))
774                .expect("locator utf16 lob"),
775            "AB"
776        );
777    }
778
779    #[test]
780    fn bfile_locator_name_decodes_directory_and_file_tail() {
781        let locator = Vec::from_hex(
782            "0808000000010000000000000015544553545f313933365f4d495353494e475f444952\
783             001a746573745f313933365f6d697373696e675f66696c652e747874",
784        )
785        .expect("BFILE locator fixture should be valid hex");
786
787        assert_eq!(
788            decode_bfile_locator_name(&locator),
789            Some((
790                "TEST_1936_MISSING_DIR".to_string(),
791                "test_1936_missing_file.txt".to_string()
792            ))
793        );
794    }
795
796    fn number_column(name: &str) -> ColumnMetadata {
797        ColumnMetadata {
798            name: name.into(),
799            ora_type_num: ORA_TYPE_NUM_NUMBER,
800            csfrm: CS_FORM_IMPLICIT,
801            precision: 0,
802            scale: 0,
803            buffer_size: ORA_TYPE_SIZE_NUMBER,
804            max_size: ORA_TYPE_SIZE_NUMBER,
805            nulls_allowed: true,
806            is_json: false,
807            is_oson: false,
808            object_schema: None,
809            object_type_name: None,
810            is_array: false,
811            vector_dimensions: None,
812            vector_format: 0,
813            vector_flags: 0,
814            ..Default::default()
815        }
816    }
817
818    fn long_column(name: &str) -> ColumnMetadata {
819        ColumnMetadata {
820            name: name.into(),
821            ora_type_num: ORA_TYPE_NUM_LONG,
822            csfrm: CS_FORM_IMPLICIT,
823            precision: 0,
824            scale: 0,
825            buffer_size: TNS_MAX_LONG_LENGTH,
826            max_size: 0,
827            nulls_allowed: true,
828            is_json: false,
829            is_oson: false,
830            object_schema: None,
831            object_type_name: None,
832            is_array: false,
833            vector_dimensions: None,
834            vector_format: 0,
835            vector_flags: 0,
836            ..Default::default()
837        }
838    }
839}