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