Skip to main content

oracledb_protocol/thin/
bind.rs

1#![forbid(unsafe_code)]
2
3use super::*;
4
5impl BindValue {
6    pub(crate) fn is_output_only(&self) -> bool {
7        matches!(self, BindValue::Output { .. })
8            || matches!(self, BindValue::ReturnOutput { .. })
9            || matches!(self, BindValue::ObjectOutput { .. })
10            || matches!(self, BindValue::Array { values, .. } if values.is_empty())
11    }
12
13    pub(crate) fn is_return_output(&self) -> bool {
14        matches!(self, BindValue::ReturnOutput { .. })
15            || matches!(
16                self,
17                BindValue::ObjectOutput {
18                    is_return: true,
19                    ..
20                }
21            )
22    }
23}
24
25pub(crate) fn write_bind_metadata_with_type(
26    writer: &mut TtcWriter,
27    value: &BindValue,
28    ora_type_num: u8,
29    csfrm: u8,
30    buffer_size: u32,
31) -> Result<()> {
32    let (flags, max_elements) = match value {
33        BindValue::Array { max_elements, .. } => {
34            (TNS_BIND_USE_INDICATORS | TNS_BIND_ARRAY, *max_elements)
35        }
36        _ => (TNS_BIND_USE_INDICATORS, 0),
37    };
38    // JSON binds advertise a TNS_JSON_MAX_LENGTH prefetch buffer (reference
39    // base.pyx:1398-1400) so a returned/out OSON image streams inline.
40    let buffer_size = if ora_type_num == ORA_TYPE_NUM_JSON {
41        TNS_JSON_MAX_LENGTH
42    } else {
43        buffer_size
44    };
45    writer.write_u8(ora_type_num);
46    writer.write_u8(flags);
47    writer.write_u8(0);
48    writer.write_u8(0);
49    writer.write_ub4(buffer_size);
50    writer.write_ub4(max_elements);
51    let cont_flags = if matches!(
52        ora_type_num,
53        ORA_TYPE_NUM_CLOB | ORA_TYPE_NUM_BLOB | ORA_TYPE_NUM_VECTOR | ORA_TYPE_NUM_JSON
54    ) {
55        TNS_LOB_PREFETCH_FLAG
56    } else {
57        0
58    };
59    writer.write_ub8(cont_flags);
60    if let BindValue::ObjectOutput { oid, version, .. }
61    | BindValue::ObjectInput { oid, version, .. } = value
62    {
63        writer.write_bytes_with_two_lengths(Some(oid))?;
64        writer.write_ub4(*version);
65    } else {
66        writer.write_ub4(0);
67        writer.write_ub2(0);
68    }
69    if csfrm != 0 {
70        writer.write_ub2(TNS_CHARSET_UTF8);
71    } else {
72        writer.write_ub2(0);
73    }
74    writer.write_u8(csfrm);
75    // max chars (LOB prefetch length): VECTOR advertises TNS_VECTOR_MAX_LENGTH
76    // so the server prefetches the image inline (reference base.pyx)
77    let lob_prefetch_length = match ora_type_num {
78        ORA_TYPE_NUM_VECTOR => TNS_VECTOR_MAX_LENGTH,
79        ORA_TYPE_NUM_JSON => TNS_JSON_MAX_LENGTH,
80        _ => 0,
81    };
82    writer.write_ub4(lob_prefetch_length);
83    writer.write_ub4(0);
84    Ok(())
85}
86
87pub fn bind_value_type_info(value: &BindValue) -> Option<BindTypeInfo> {
88    let (ora_type_num, csfrm, buffer_size) = match value {
89        BindValue::Null => return None,
90        BindValue::TypedNull {
91            ora_type_num,
92            csfrm,
93            buffer_size,
94        }
95        | BindValue::Output {
96            ora_type_num,
97            csfrm,
98            buffer_size,
99        }
100        | BindValue::ReturnOutput {
101            ora_type_num,
102            csfrm,
103            buffer_size,
104        } => (*ora_type_num, *csfrm, (*buffer_size).max(1)),
105        BindValue::ObjectOutput { buffer_size, .. }
106        | BindValue::ObjectInput { buffer_size, .. } => {
107            (ORA_TYPE_NUM_OBJECT, 0, (*buffer_size).max(1))
108        }
109        // values larger than 32767 bytes keep the VARCHAR/RAW bind type with
110        // a large buffer size; the chunked length encoding carries the data
111        // (reference always derives VARCHAR/RAW from str/bytes values —
112        // metadata.pyx from_value — and never switches the bind type to LONG,
113        // which the server rejects for PL/SQL LOB parameters with ORA-01460)
114        BindValue::Text(value) => {
115            let buffer_size = u32::try_from(value.chars().count())
116                .unwrap_or(u32::MAX)
117                .saturating_mul(4)
118                .max(1);
119            (ORA_TYPE_NUM_VARCHAR, CS_FORM_IMPLICIT, buffer_size)
120        }
121        BindValue::Raw(value) => {
122            let buffer_size = u32::try_from(value.len()).unwrap_or(u32::MAX).max(1);
123            (ORA_TYPE_NUM_RAW, 0, buffer_size)
124        }
125        BindValue::Lob {
126            ora_type_num,
127            csfrm,
128            ..
129        } => (*ora_type_num, *csfrm, 1),
130        BindValue::Number(_) => (ORA_TYPE_NUM_NUMBER, 0, ORA_TYPE_SIZE_NUMBER),
131        BindValue::BinaryInteger(_) => (ORA_TYPE_NUM_BINARY_INTEGER, 0, ORA_TYPE_SIZE_NUMBER),
132        BindValue::Boolean(_) => (ORA_TYPE_NUM_BOOLEAN, 0, ORA_TYPE_SIZE_BOOLEAN),
133        BindValue::BinaryDouble(_) => (ORA_TYPE_NUM_BINARY_DOUBLE, 0, ORA_TYPE_SIZE_BINARY_DOUBLE),
134        BindValue::BinaryFloat(_) => (ORA_TYPE_NUM_BINARY_FLOAT, 0, ORA_TYPE_SIZE_BINARY_FLOAT),
135        BindValue::IntervalDS { .. } => (ORA_TYPE_NUM_INTERVAL_DS, 0, ORA_TYPE_SIZE_INTERVAL_DS),
136        BindValue::IntervalYM { .. } => (ORA_TYPE_NUM_INTERVAL_YM, 0, ORA_TYPE_SIZE_INTERVAL_YM),
137        BindValue::DateTime { .. } => (ORA_TYPE_NUM_DATE, 0, ORA_TYPE_SIZE_DATE),
138        BindValue::Timestamp { ora_type_num, .. } => (
139            *ora_type_num,
140            0,
141            if *ora_type_num == ORA_TYPE_NUM_TIMESTAMP_TZ {
142                ORA_TYPE_SIZE_TIMESTAMP_TZ
143            } else {
144                ORA_TYPE_SIZE_TIMESTAMP
145            },
146        ),
147        BindValue::TimestampTz { .. } => (ORA_TYPE_NUM_TIMESTAMP_TZ, 0, ORA_TYPE_SIZE_TIMESTAMP_TZ),
148        BindValue::Array {
149            ora_type_num,
150            csfrm,
151            buffer_size,
152            ..
153        } => (*ora_type_num, *csfrm, (*buffer_size).max(1)),
154        // reference base.pyx _write_column_metadata: VECTOR binds advertise a
155        // TNS_VECTOR_MAX_LENGTH prefetch buffer and the LOB-prefetch cont flag
156        BindValue::Vector(_) => (ORA_TYPE_NUM_VECTOR, 0, TNS_VECTOR_MAX_LENGTH),
157        // JSON binds: the reference DB_TYPE_JSON var has buffer_size_factor 0, so
158        // its metadata buffer_size is small and the OSON value is written inline
159        // (not deferred to the "long" bind section). The TNS_JSON_MAX_LENGTH
160        // prefetch buffer is applied only in the wire metadata writer, not here,
161        // so the long/non-long bind-data ordering matches the reference.
162        BindValue::Json(_) => (ORA_TYPE_NUM_JSON, 0, 1),
163        BindValue::Cursor { .. } => (ORA_TYPE_NUM_CURSOR, 0, 4),
164    };
165    Some(BindTypeInfo {
166        ora_type_num,
167        csfrm,
168        buffer_size,
169    })
170}
171
172pub fn define_metadata_from_bind(source: &ColumnMetadata, value: &BindValue) -> ColumnMetadata {
173    let Some(mut info) = bind_value_type_info(value) else {
174        return source.clone();
175    };
176    if source.ora_type_num == ORA_TYPE_NUM_CLOB
177        && matches!(
178            info.ora_type_num,
179            ORA_TYPE_NUM_CHAR | ORA_TYPE_NUM_LONG | ORA_TYPE_NUM_VARCHAR
180        )
181    {
182        info.ora_type_num = ORA_TYPE_NUM_LONG;
183        if source.csfrm != 0 {
184            info.csfrm = source.csfrm;
185        }
186    }
187    let mut metadata = source.clone();
188    metadata.ora_type_num = info.ora_type_num;
189    metadata.csfrm = info.csfrm;
190    if info.ora_type_num == ORA_TYPE_NUM_LONG {
191        metadata.buffer_size = TNS_MAX_LONG_LENGTH;
192        metadata.max_size = 0;
193    } else {
194        metadata.buffer_size = info.buffer_size.max(1);
195        metadata.max_size = info.buffer_size.max(1);
196    }
197    metadata
198}
199
200/// When the same query is re-executed after a column's data type changed to
201/// CLOB/BLOB but the previous execution fetched the column as a char/raw
202/// type, the server streams the data as LONG/LONG RAW (same as a define of
203/// CLOB/BLOB as string/bytes); the fetch metadata must follow (reference
204/// impl/thin/messages/base.pyx:820-845 `_adjust_metadata`). Returns `true`
205/// when the metadata was adjusted.
206pub fn adjust_refetch_metadata(previous: &ColumnMetadata, current: &mut ColumnMetadata) -> bool {
207    if current.ora_type_num == ORA_TYPE_NUM_CLOB
208        && matches!(
209            previous.ora_type_num,
210            ORA_TYPE_NUM_CHAR | ORA_TYPE_NUM_LONG | ORA_TYPE_NUM_VARCHAR
211        )
212    {
213        current.ora_type_num = ORA_TYPE_NUM_LONG;
214        current.csfrm = previous.csfrm;
215        current.buffer_size = TNS_MAX_LONG_LENGTH;
216        current.max_size = 0;
217        return true;
218    }
219    if current.ora_type_num == ORA_TYPE_NUM_BLOB
220        && matches!(
221            previous.ora_type_num,
222            ORA_TYPE_NUM_RAW | ORA_TYPE_NUM_LONG_RAW
223        )
224    {
225        current.ora_type_num = ORA_TYPE_NUM_LONG_RAW;
226        current.csfrm = 0;
227        current.buffer_size = TNS_MAX_LONG_LENGTH;
228        current.max_size = 0;
229        return true;
230    }
231    false
232}
233
234pub fn output_bind(value: BindValue) -> BindValue {
235    match value {
236        BindValue::ObjectOutput {
237            schema,
238            type_name,
239            oid,
240            version,
241            buffer_size,
242            ..
243        } => BindValue::ObjectOutput {
244            schema,
245            type_name,
246            oid,
247            version,
248            buffer_size: buffer_size.max(1),
249            is_return: false,
250        },
251        value => {
252            let info = bind_value_type_info(&value).unwrap_or(BindTypeInfo {
253                ora_type_num: ORA_TYPE_NUM_VARCHAR,
254                csfrm: CS_FORM_IMPLICIT,
255                buffer_size: 1,
256            });
257            BindValue::Output {
258                ora_type_num: info.ora_type_num,
259                csfrm: info.csfrm,
260                buffer_size: info.buffer_size,
261            }
262        }
263    }
264}
265
266pub fn returning_output_bind(value: BindValue) -> BindValue {
267    match value {
268        BindValue::ObjectOutput {
269            schema,
270            type_name,
271            oid,
272            version,
273            buffer_size,
274            ..
275        } => BindValue::ObjectOutput {
276            schema,
277            type_name,
278            oid,
279            version,
280            buffer_size: buffer_size.max(1),
281            is_return: true,
282        },
283        value => {
284            let info = bind_value_type_info(&value).unwrap_or(BindTypeInfo {
285                ora_type_num: ORA_TYPE_NUM_VARCHAR,
286                csfrm: CS_FORM_IMPLICIT,
287                buffer_size: 1,
288            });
289            BindValue::ReturnOutput {
290                ora_type_num: info.ora_type_num,
291                csfrm: info.csfrm,
292                buffer_size: info.buffer_size,
293            }
294        }
295    }
296}
297
298pub fn cursor_bind_template() -> BindValue {
299    BindValue::TypedNull {
300        ora_type_num: ORA_TYPE_NUM_CURSOR,
301        csfrm: 0,
302        buffer_size: 4,
303    }
304}
305
306pub fn is_cursor_bind_template(value: &BindValue) -> bool {
307    matches!(
308        value,
309        BindValue::TypedNull {
310            ora_type_num: ORA_TYPE_NUM_CURSOR,
311            ..
312        }
313    )
314}
315
316pub fn public_dbtype_name_from_type_name(type_name: &str) -> &'static str {
317    match type_name {
318        "NUMBER" | "DB_TYPE_NUMBER" | "int" | "float" | "Decimal" => "DB_TYPE_NUMBER",
319        "NATIVE_INT" | "DB_TYPE_BINARY_INTEGER" => "DB_TYPE_BINARY_INTEGER",
320        "NATIVE_FLOAT" | "DB_TYPE_BINARY_DOUBLE" => "DB_TYPE_BINARY_DOUBLE",
321        "DB_TYPE_BINARY_FLOAT" | "BINARY_FLOAT" => "DB_TYPE_BINARY_FLOAT",
322        "DB_TYPE_BOOLEAN" | "BOOLEAN" | "bool" => "DB_TYPE_BOOLEAN",
323        "DB_TYPE_INTERVAL_DS" | "INTERVAL DAY TO SECOND" | "timedelta" => "DB_TYPE_INTERVAL_DS",
324        "DB_TYPE_INTERVAL_YM" | "INTERVAL YEAR TO MONTH" | "IntervalYM" => "DB_TYPE_INTERVAL_YM",
325        "DB_TYPE_BFILE" | "BFILE" => "DB_TYPE_BFILE",
326        "DB_TYPE_JSON" | "JSON" => "DB_TYPE_JSON",
327        "STRING" | "DB_TYPE_VARCHAR" | "str" => "DB_TYPE_VARCHAR",
328        "DB_TYPE_CHAR" => "DB_TYPE_CHAR",
329        "DB_TYPE_NCHAR" => "DB_TYPE_NCHAR",
330        "DB_TYPE_NVARCHAR" => "DB_TYPE_NVARCHAR",
331        "DB_TYPE_CLOB" | "CLOB" => "DB_TYPE_CLOB",
332        "DB_TYPE_NCLOB" | "NCLOB" => "DB_TYPE_NCLOB",
333        "DB_TYPE_BLOB" | "BLOB" => "DB_TYPE_BLOB",
334        "DB_TYPE_LONG" | "LONG" | "LONG_STRING" => "DB_TYPE_LONG",
335        "DB_TYPE_LONG_NVARCHAR" | "LONG NVARCHAR" => "DB_TYPE_LONG_NVARCHAR",
336        "DB_TYPE_LONG_RAW" | "LONG RAW" | "LONG_BINARY" => "DB_TYPE_LONG_RAW",
337        "DB_TYPE_RAW" | "BINARY" | "bytes" => "DB_TYPE_RAW",
338        "ROWID" | "DB_TYPE_ROWID" => "DB_TYPE_ROWID",
339        "DB_TYPE_UROWID" => "DB_TYPE_UROWID",
340        "DATETIME" | "DB_TYPE_DATE" | "date" | "datetime" => "DB_TYPE_DATE",
341        "DB_TYPE_TIMESTAMP" | "TIMESTAMP" => "DB_TYPE_TIMESTAMP",
342        "DB_TYPE_TIMESTAMP_LTZ" | "TIMESTAMP WITH LOCAL TIME ZONE" => "DB_TYPE_TIMESTAMP_LTZ",
343        "DB_TYPE_TIMESTAMP_TZ" | "TIMESTAMP WITH TIME ZONE" => "DB_TYPE_TIMESTAMP_TZ",
344        "DB_TYPE_CURSOR" | "CURSOR" => "DB_TYPE_CURSOR",
345        "DB_TYPE_VECTOR" | "VECTOR" => "DB_TYPE_VECTOR",
346        _ => "DB_TYPE_VARCHAR",
347    }
348}
349
350pub fn column_metadata_is_xmltype(metadata: &ColumnMetadata) -> bool {
351    metadata
352        .object_schema
353        .as_deref()
354        .is_some_and(|schema| schema.eq_ignore_ascii_case("SYS"))
355        && metadata
356            .object_type_name
357            .as_deref()
358            .is_some_and(|name| name.eq_ignore_ascii_case("XMLTYPE"))
359}
360
361pub fn public_dbtype_name_from_column_metadata(metadata: &ColumnMetadata) -> &'static str {
362    if column_metadata_is_xmltype(metadata) {
363        return "DB_TYPE_XMLTYPE";
364    }
365    match (metadata.ora_type_num, metadata.csfrm) {
366        (ORA_TYPE_NUM_LONG, CS_FORM_NCHAR) => "DB_TYPE_LONG_NVARCHAR",
367        (ORA_TYPE_NUM_LONG, _) => "DB_TYPE_LONG",
368        (ORA_TYPE_NUM_LONG_RAW, _) => "DB_TYPE_LONG_RAW",
369        (ORA_TYPE_NUM_VARCHAR, CS_FORM_NCHAR) => "DB_TYPE_NVARCHAR",
370        (ORA_TYPE_NUM_CHAR, CS_FORM_NCHAR) => "DB_TYPE_NCHAR",
371        (ORA_TYPE_NUM_CHAR, _) => "DB_TYPE_CHAR",
372        (ORA_TYPE_NUM_VARCHAR, _) => "DB_TYPE_VARCHAR",
373        (ORA_TYPE_NUM_RAW, _) => "DB_TYPE_RAW",
374        (ORA_TYPE_NUM_ROWID, _) => "DB_TYPE_ROWID",
375        (ORA_TYPE_NUM_UROWID, _) => "DB_TYPE_UROWID",
376        (ORA_TYPE_NUM_BINARY_DOUBLE, _) => "DB_TYPE_BINARY_DOUBLE",
377        (ORA_TYPE_NUM_BINARY_FLOAT, _) => "DB_TYPE_BINARY_FLOAT",
378        (ORA_TYPE_NUM_BINARY_INTEGER, _) => "DB_TYPE_BINARY_INTEGER",
379        (ORA_TYPE_NUM_NUMBER, _) => "DB_TYPE_NUMBER",
380        (ORA_TYPE_NUM_CURSOR, _) => "DB_TYPE_CURSOR",
381        (ORA_TYPE_NUM_OBJECT, _) => "DB_TYPE_OBJECT",
382        (ORA_TYPE_NUM_CLOB, CS_FORM_NCHAR) => "DB_TYPE_NCLOB",
383        (ORA_TYPE_NUM_CLOB, _) => "DB_TYPE_CLOB",
384        (ORA_TYPE_NUM_BLOB, _) => "DB_TYPE_BLOB",
385        (ORA_TYPE_NUM_BFILE, _) => "DB_TYPE_BFILE",
386        (ORA_TYPE_NUM_DATE, _) => "DB_TYPE_DATE",
387        (ORA_TYPE_NUM_TIMESTAMP, _) => "DB_TYPE_TIMESTAMP",
388        (ORA_TYPE_NUM_TIMESTAMP_LTZ, _) => "DB_TYPE_TIMESTAMP_LTZ",
389        (ORA_TYPE_NUM_TIMESTAMP_TZ, _) => "DB_TYPE_TIMESTAMP_TZ",
390        (ORA_TYPE_NUM_INTERVAL_DS, _) => "DB_TYPE_INTERVAL_DS",
391        (ORA_TYPE_NUM_INTERVAL_YM, _) => "DB_TYPE_INTERVAL_YM",
392        (ORA_TYPE_NUM_BOOLEAN, _) => "DB_TYPE_BOOLEAN",
393        (ORA_TYPE_NUM_VECTOR, _) => "DB_TYPE_VECTOR",
394        (ORA_TYPE_NUM_JSON, _) => "DB_TYPE_JSON",
395        _ => "DB_TYPE_VARCHAR",
396    }
397}
398
399/// Mirrors the reference `DbType.default_size` / `_buffer_size_factor` table
400/// (reference impl/base/types.pyx:120-440). Returns
401/// `(default_size, buffer_size_factor)` for a public database type name.
402pub fn public_dbtype_size_info(dbtype_name: &str) -> (u32, u32) {
403    match dbtype_name {
404        "DB_TYPE_BFILE" => (0, 4000),
405        "DB_TYPE_BINARY_DOUBLE" => (0, ORA_TYPE_SIZE_BINARY_DOUBLE),
406        "DB_TYPE_BINARY_FLOAT" => (0, ORA_TYPE_SIZE_BINARY_FLOAT),
407        "DB_TYPE_BINARY_INTEGER" | "DB_TYPE_NUMBER" => (0, ORA_TYPE_SIZE_NUMBER),
408        "DB_TYPE_BLOB" | "DB_TYPE_CLOB" | "DB_TYPE_NCLOB" => (0, 112),
409        "DB_TYPE_BOOLEAN" => (0, ORA_TYPE_SIZE_BOOLEAN),
410        "DB_TYPE_CHAR" | "DB_TYPE_NCHAR" => (2000, 4),
411        "DB_TYPE_CURSOR" => (0, 4),
412        "DB_TYPE_DATE" => (0, ORA_TYPE_SIZE_DATE),
413        "DB_TYPE_INTERVAL_DS" => (0, ORA_TYPE_SIZE_INTERVAL_DS),
414        "DB_TYPE_INTERVAL_YM" => (0, 5),
415        "DB_TYPE_LONG" | "DB_TYPE_LONG_NVARCHAR" | "DB_TYPE_LONG_RAW" => (0, TNS_MAX_LONG_LENGTH),
416        "DB_TYPE_NVARCHAR" | "DB_TYPE_VARCHAR" => (4000, 4),
417        "DB_TYPE_RAW" => (4000, 1),
418        "DB_TYPE_ROWID" => (0, ORA_TYPE_SIZE_ROWID),
419        "DB_TYPE_TIMESTAMP" | "DB_TYPE_TIMESTAMP_LTZ" => (0, ORA_TYPE_SIZE_TIMESTAMP),
420        "DB_TYPE_TIMESTAMP_TZ" => (0, ORA_TYPE_SIZE_TIMESTAMP_TZ),
421        "DB_TYPE_JSON" | "DB_TYPE_VECTOR" => (0, 1),
422        _ => (0, 0),
423    }
424}
425
426/// Mirrors the reference fetch-conversion legality matrix
427/// (reference impl/base/var.pyx:113-248 `_check_fetch_conversion`). Given the
428/// metadata of the column being fetched and the Oracle type requested by an
429/// output type handler variable, returns the metadata that should be used for
430/// the wire define. Conversions that only affect the Python materialization
431/// keep the original wire metadata; LOB and JSON sources adjust the define so
432/// the server sends inline data. Unsupported pairs return `None` and the
433/// caller is expected to raise `DPY-4007`.
434pub fn check_fetch_conversion(
435    source: &ColumnMetadata,
436    to_ora_type_num: u8,
437    to_csfrm: u8,
438) -> Option<ColumnMetadata> {
439    const CHAR_TYPES: [u8; 3] = [ORA_TYPE_NUM_CHAR, ORA_TYPE_NUM_LONG, ORA_TYPE_NUM_VARCHAR];
440    let from = source.ora_type_num;
441    let to = to_ora_type_num;
442    if from == to {
443        return Some(source.clone());
444    }
445    let supported = match from {
446        ORA_TYPE_NUM_BINARY_DOUBLE | ORA_TYPE_NUM_BINARY_FLOAT => {
447            matches!(
448                to,
449                ORA_TYPE_NUM_BINARY_INTEGER
450                    | ORA_TYPE_NUM_BINARY_DOUBLE
451                    | ORA_TYPE_NUM_BINARY_FLOAT
452                    | ORA_TYPE_NUM_NUMBER
453            ) || CHAR_TYPES.contains(&to)
454        }
455        ORA_TYPE_NUM_BINARY_INTEGER => to == ORA_TYPE_NUM_NUMBER || CHAR_TYPES.contains(&to),
456        ORA_TYPE_NUM_BLOB => {
457            if matches!(to, ORA_TYPE_NUM_RAW | ORA_TYPE_NUM_LONG_RAW) {
458                let mut metadata = source.clone();
459                metadata.ora_type_num = ORA_TYPE_NUM_LONG_RAW;
460                metadata.csfrm = 0;
461                metadata.buffer_size = TNS_MAX_LONG_LENGTH;
462                metadata.max_size = 0;
463                return Some(metadata);
464            }
465            false
466        }
467        ORA_TYPE_NUM_CHAR | ORA_TYPE_NUM_LONG | ORA_TYPE_NUM_VARCHAR => {
468            matches!(
469                to,
470                ORA_TYPE_NUM_BINARY_DOUBLE
471                    | ORA_TYPE_NUM_BINARY_FLOAT
472                    | ORA_TYPE_NUM_NUMBER
473                    | ORA_TYPE_NUM_BINARY_INTEGER
474            ) || CHAR_TYPES.contains(&to)
475        }
476        ORA_TYPE_NUM_CLOB => {
477            if CHAR_TYPES.contains(&to) {
478                let mut metadata = source.clone();
479                metadata.ora_type_num = ORA_TYPE_NUM_LONG;
480                metadata.buffer_size = TNS_MAX_LONG_LENGTH;
481                metadata.max_size = 0;
482                return Some(metadata);
483            }
484            false
485        }
486        ORA_TYPE_NUM_DATE
487        | ORA_TYPE_NUM_TIMESTAMP
488        | ORA_TYPE_NUM_TIMESTAMP_LTZ
489        | ORA_TYPE_NUM_TIMESTAMP_TZ => {
490            matches!(
491                to,
492                ORA_TYPE_NUM_DATE
493                    | ORA_TYPE_NUM_TIMESTAMP
494                    | ORA_TYPE_NUM_TIMESTAMP_LTZ
495                    | ORA_TYPE_NUM_TIMESTAMP_TZ
496            ) || CHAR_TYPES.contains(&to)
497        }
498        ORA_TYPE_NUM_INTERVAL_DS | ORA_TYPE_NUM_INTERVAL_YM | ORA_TYPE_NUM_ROWID => {
499            CHAR_TYPES.contains(&to)
500        }
501        ORA_TYPE_NUM_NUMBER => {
502            matches!(
503                to,
504                ORA_TYPE_NUM_BINARY_INTEGER
505                    | ORA_TYPE_NUM_BINARY_DOUBLE
506                    | ORA_TYPE_NUM_BINARY_FLOAT
507            ) || CHAR_TYPES.contains(&to)
508        }
509        ORA_TYPE_NUM_JSON => {
510            // Native JSON (DB_TYPE_JSON) fetched as a character type via an
511            // output type handler. The reference defines the column to the
512            // server as VARCHAR but decodes the returned bytes as LONG: "the
513            // server won't accept LONG being defined but even so it still sends
514            // back LONG data" (reference impl/base/var.pyx:208-215, where
515            // `_fetch_metadata.dbtype = DB_TYPE_LONG` and `return
516            // DB_TYPE_VARCHAR`).
517            //
518            // Our wire define writer keys the VARCHAR ora_type_num off this
519            // metadata, so the server accepts the define and streams the OSON
520            // image inline as text. The returned data is then decoded through
521            // the same `read_bytes` path used for VARCHAR/CHAR/LONG (all three
522            // share identical framing in `parse_column_value`), and the
523            // non-zero LONG-sized `buffer_size` set here keeps the
524            // null-by-describe shortcut from firing — exactly the effect the
525            // reference obtains by decoding as LONG. The handler's outconverter
526            // (e.g. `json.loads`) then materializes the Python value.
527            if matches!(to, ORA_TYPE_NUM_CHAR | ORA_TYPE_NUM_VARCHAR) {
528                let mut metadata = source.clone();
529                metadata.ora_type_num = ORA_TYPE_NUM_VARCHAR;
530                metadata.csfrm = CS_FORM_IMPLICIT;
531                metadata.buffer_size = TNS_MAX_LONG_LENGTH;
532                metadata.max_size = 0;
533                return Some(metadata);
534            }
535            // JSON fetched as RAW/bytes decodes the OSON image bytes directly
536            // (reference var.pyx:216-218 sets `_fetch_metadata.dbtype =
537            // DB_TYPE_RAW`).
538            if to == ORA_TYPE_NUM_RAW {
539                let mut metadata = source.clone();
540                metadata.ora_type_num = ORA_TYPE_NUM_RAW;
541                metadata.csfrm = 0;
542                metadata.buffer_size = TNS_MAX_LONG_LENGTH;
543                metadata.max_size = 0;
544                return Some(metadata);
545            }
546            false
547        }
548        ORA_TYPE_NUM_VECTOR => {
549            // VECTOR fetched as a character type streams its JSON text via a
550            // LONG wire define; VECTOR fetched as a CLOB streams via a CLOB
551            // locator (reference var.pyx:234-243).
552            if CHAR_TYPES.contains(&to) {
553                let mut metadata = source.clone();
554                metadata.ora_type_num = ORA_TYPE_NUM_LONG;
555                metadata.csfrm = CS_FORM_IMPLICIT;
556                metadata.buffer_size = TNS_MAX_LONG_LENGTH;
557                metadata.max_size = 0;
558                return Some(metadata);
559            }
560            if to == ORA_TYPE_NUM_CLOB {
561                let mut metadata = source.clone();
562                metadata.ora_type_num = ORA_TYPE_NUM_CLOB;
563                return Some(metadata);
564            }
565            false
566        }
567        _ => false,
568    };
569    let _ = to_csfrm;
570    if supported {
571        Some(source.clone())
572    } else {
573        None
574    }
575}
576
577pub fn public_dbtype_name_from_oracle_type_name(type_name: &str) -> &'static str {
578    let upper = type_name.to_ascii_uppercase();
579    if upper.starts_with("TIMESTAMP") {
580        if upper.contains("LOCAL TIME ZONE") || upper.contains("LOCAL TZ") {
581            return "DB_TYPE_TIMESTAMP_LTZ";
582        }
583        if upper.contains("TIME ZONE") || upper.contains("WITH TZ") {
584            return "DB_TYPE_TIMESTAMP_TZ";
585        }
586        return "DB_TYPE_TIMESTAMP";
587    }
588    match upper.as_str() {
589        "CHAR" => "DB_TYPE_CHAR",
590        "NCHAR" => "DB_TYPE_NCHAR",
591        "VARCHAR2" | "VARCHAR" => "DB_TYPE_VARCHAR",
592        "NVARCHAR2" | "NVARCHAR" => "DB_TYPE_NVARCHAR",
593        "RAW" => "DB_TYPE_RAW",
594        "DATE" => "DB_TYPE_DATE",
595        "TIMESTAMP" => "DB_TYPE_TIMESTAMP",
596        "TIMESTAMP WITH TIME ZONE" | "TIMESTAMP WITH TZ" => "DB_TYPE_TIMESTAMP_TZ",
597        "TIMESTAMP WITH LOCAL TIME ZONE" | "TIMESTAMP WITH LOCAL TZ" => "DB_TYPE_TIMESTAMP_LTZ",
598        "CLOB" => "DB_TYPE_CLOB",
599        "NCLOB" => "DB_TYPE_NCLOB",
600        "BLOB" => "DB_TYPE_BLOB",
601        "XMLTYPE" => "DB_TYPE_XMLTYPE",
602        "BINARY_FLOAT" => "DB_TYPE_BINARY_FLOAT",
603        "BINARY_DOUBLE" => "DB_TYPE_BINARY_DOUBLE",
604        "NUMBER" | "INTEGER" | "SMALLINT" | "REAL" | "DOUBLE PRECISION" | "FLOAT" => {
605            "DB_TYPE_NUMBER"
606        }
607        // PL/SQL scalar attribute/element type names returned verbatim by the
608        // type catalog. Without these arms they would fall through to the ADT
609        // fallback below and be misclassified as nested objects (reference
610        // impl/base/types.pyx:154-175,451-455 db_type_by_ora_name).
611        "BOOLEAN" | "PL/SQL BOOLEAN" => "DB_TYPE_BOOLEAN",
612        "BINARY_INTEGER" | "PLS_INTEGER" | "PL/SQL BINARY INTEGER" | "PL/SQL PLS INTEGER" => {
613            "DB_TYPE_BINARY_INTEGER"
614        }
615        "LONG" => "DB_TYPE_LONG",
616        "LONG RAW" => "DB_TYPE_LONG_RAW",
617        "ROWID" => "DB_TYPE_ROWID",
618        "UROWID" => "DB_TYPE_UROWID",
619        "BFILE" => "DB_TYPE_BFILE",
620        "JSON" => "DB_TYPE_JSON",
621        "VECTOR" => "DB_TYPE_VECTOR",
622        "INTERVAL DAY TO SECOND" => "DB_TYPE_INTERVAL_DS",
623        "INTERVAL YEAR TO MONTH" => "DB_TYPE_INTERVAL_YM",
624        // An unknown name IS a nested object type (mirrors reference
625        // _create_attr only calling get_type_for_info when type_owner is set).
626        _ => "DB_TYPE_OBJECT",
627    }
628}
629
630pub fn dbobject_attr_precision_scale(
631    type_name: &str,
632    precision: Option<i8>,
633    scale: Option<i8>,
634) -> (i8, i8) {
635    match type_name.to_ascii_uppercase().as_str() {
636        "NUMBER" => (
637            precision.unwrap_or(if scale == Some(0) { 38 } else { 0 }),
638            scale.unwrap_or(-127),
639        ),
640        "INTEGER" | "SMALLINT" => (precision.unwrap_or(38), scale.unwrap_or(0)),
641        "REAL" => (precision.unwrap_or(63), scale.unwrap_or(-127)),
642        "DOUBLE PRECISION" | "FLOAT" => (precision.unwrap_or(126), scale.unwrap_or(-127)),
643        _ => (0, 0),
644    }
645}
646
647pub fn dbobject_attr_max_size(type_name: &str, length: Option<u32>) -> u32 {
648    let length = length.unwrap_or(0);
649    match type_name.to_ascii_uppercase().as_str() {
650        "NCHAR" | "NVARCHAR2" | "NVARCHAR" => length.saturating_mul(2),
651        _ => length,
652    }
653}
654
655pub fn dbobject_rowtype_attr_max_size(
656    type_name: &str,
657    data_length: Option<u32>,
658    char_length: Option<u32>,
659) -> u32 {
660    match type_name.to_ascii_uppercase().as_str() {
661        "CHAR" | "VARCHAR" | "VARCHAR2" | "RAW" => data_length.unwrap_or(0),
662        "NCHAR" | "NVARCHAR" | "NVARCHAR2" => dbobject_attr_max_size(
663            type_name,
664            char_length.filter(|length| *length > 0).or(data_length),
665        ),
666        _ => 0,
667    }
668}
669
670pub fn public_dbtype_name_from_bind(value: &BindValue) -> &'static str {
671    match value {
672        BindValue::TypedNull {
673            ora_type_num,
674            csfrm,
675            ..
676        }
677        | BindValue::Output {
678            ora_type_num,
679            csfrm,
680            ..
681        }
682        | BindValue::ReturnOutput {
683            ora_type_num,
684            csfrm,
685            ..
686        }
687        | BindValue::Array {
688            ora_type_num,
689            csfrm,
690            ..
691        } => public_dbtype_name_from_type_info(*ora_type_num, *csfrm),
692        BindValue::ObjectOutput { .. } | BindValue::ObjectInput { .. } => "DB_TYPE_OBJECT",
693        BindValue::Text(_) => "DB_TYPE_VARCHAR",
694        BindValue::Raw(_) => "DB_TYPE_RAW",
695        BindValue::Lob {
696            ora_type_num,
697            csfrm,
698            ..
699        } => match (*ora_type_num, *csfrm) {
700            (ORA_TYPE_NUM_BLOB, _) => "DB_TYPE_BLOB",
701            (ORA_TYPE_NUM_CLOB, CS_FORM_NCHAR) => "DB_TYPE_NCLOB",
702            (ORA_TYPE_NUM_CLOB, _) => "DB_TYPE_CLOB",
703            _ => "DB_TYPE_CLOB",
704        },
705        BindValue::Number(_) => "DB_TYPE_NUMBER",
706        BindValue::BinaryInteger(_) => "DB_TYPE_BINARY_INTEGER",
707        BindValue::BinaryDouble(_) => "DB_TYPE_BINARY_DOUBLE",
708        BindValue::BinaryFloat(_) => "DB_TYPE_BINARY_FLOAT",
709        BindValue::Boolean(_) => "DB_TYPE_BOOLEAN",
710        BindValue::IntervalDS { .. } => "DB_TYPE_INTERVAL_DS",
711        BindValue::IntervalYM { .. } => "DB_TYPE_INTERVAL_YM",
712        BindValue::DateTime { .. } => "DB_TYPE_DATE",
713        BindValue::Timestamp { ora_type_num, .. } => match *ora_type_num {
714            ORA_TYPE_NUM_TIMESTAMP_LTZ => "DB_TYPE_TIMESTAMP_LTZ",
715            ORA_TYPE_NUM_TIMESTAMP_TZ => "DB_TYPE_TIMESTAMP_TZ",
716            _ => "DB_TYPE_TIMESTAMP",
717        },
718        BindValue::TimestampTz { .. } => "DB_TYPE_TIMESTAMP_TZ",
719        BindValue::Vector(_) => "DB_TYPE_VECTOR",
720        BindValue::Json(_) => "DB_TYPE_JSON",
721        BindValue::Cursor { .. } => "DB_TYPE_CURSOR",
722        BindValue::Null => "DB_TYPE_VARCHAR",
723    }
724}
725
726pub fn bind_template_from_type_name(type_name: &str, size: u32) -> BindValue {
727    let text_buffer_size = if size == 0 { 4000 } else { size.max(1) };
728    let nchar_buffer_size = text_buffer_size.saturating_mul(4);
729    match type_name {
730        "NUMBER" | "DB_TYPE_NUMBER" | "int" | "float" | "Decimal" => BindValue::TypedNull {
731            ora_type_num: ORA_TYPE_NUM_NUMBER,
732            csfrm: 0,
733            buffer_size: ORA_TYPE_SIZE_NUMBER,
734        },
735        "NATIVE_INT" | "DB_TYPE_BINARY_INTEGER" => BindValue::TypedNull {
736            ora_type_num: ORA_TYPE_NUM_BINARY_INTEGER,
737            csfrm: 0,
738            buffer_size: ORA_TYPE_SIZE_NUMBER,
739        },
740        "NATIVE_FLOAT" | "DB_TYPE_BINARY_DOUBLE" => BindValue::TypedNull {
741            ora_type_num: ORA_TYPE_NUM_BINARY_DOUBLE,
742            csfrm: 0,
743            buffer_size: ORA_TYPE_SIZE_BINARY_DOUBLE,
744        },
745        "DB_TYPE_BINARY_FLOAT" | "BINARY_FLOAT" => BindValue::TypedNull {
746            ora_type_num: ORA_TYPE_NUM_BINARY_FLOAT,
747            csfrm: 0,
748            buffer_size: ORA_TYPE_SIZE_BINARY_FLOAT,
749        },
750        "DB_TYPE_BOOLEAN" | "BOOLEAN" | "bool" => BindValue::TypedNull {
751            ora_type_num: ORA_TYPE_NUM_BOOLEAN,
752            csfrm: 0,
753            buffer_size: ORA_TYPE_SIZE_BOOLEAN,
754        },
755        "DB_TYPE_INTERVAL_DS" | "INTERVAL DAY TO SECOND" | "timedelta" => BindValue::TypedNull {
756            ora_type_num: ORA_TYPE_NUM_INTERVAL_DS,
757            csfrm: 0,
758            buffer_size: ORA_TYPE_SIZE_INTERVAL_DS,
759        },
760        "DB_TYPE_INTERVAL_YM" | "INTERVAL YEAR TO MONTH" | "IntervalYM" => BindValue::TypedNull {
761            ora_type_num: ORA_TYPE_NUM_INTERVAL_YM,
762            csfrm: 0,
763            buffer_size: ORA_TYPE_SIZE_INTERVAL_YM,
764        },
765        "STRING" | "DB_TYPE_VARCHAR" | "DB_TYPE_CHAR" | "str" => BindValue::TypedNull {
766            ora_type_num: ORA_TYPE_NUM_VARCHAR,
767            csfrm: CS_FORM_IMPLICIT,
768            buffer_size: text_buffer_size,
769        },
770        "DB_TYPE_NCHAR" | "DB_TYPE_NVARCHAR" => BindValue::TypedNull {
771            ora_type_num: ORA_TYPE_NUM_VARCHAR,
772            csfrm: CS_FORM_NCHAR,
773            buffer_size: nchar_buffer_size,
774        },
775        "DB_TYPE_CLOB" | "CLOB" => BindValue::TypedNull {
776            ora_type_num: ORA_TYPE_NUM_LONG,
777            csfrm: CS_FORM_IMPLICIT,
778            buffer_size: TNS_MAX_LONG_LENGTH,
779        },
780        "DB_TYPE_NCLOB" | "NCLOB" => BindValue::TypedNull {
781            ora_type_num: ORA_TYPE_NUM_LONG,
782            csfrm: CS_FORM_NCHAR,
783            buffer_size: TNS_MAX_LONG_LENGTH,
784        },
785        "DB_TYPE_BLOB" | "BLOB" => BindValue::TypedNull {
786            ora_type_num: ORA_TYPE_NUM_LONG_RAW,
787            csfrm: 0,
788            buffer_size: TNS_MAX_LONG_LENGTH,
789        },
790        "DB_TYPE_LONG" | "LONG" | "LONG_STRING" => BindValue::TypedNull {
791            ora_type_num: ORA_TYPE_NUM_LONG,
792            csfrm: CS_FORM_IMPLICIT,
793            buffer_size: TNS_MAX_LONG_LENGTH,
794        },
795        "DB_TYPE_LONG_NVARCHAR" | "LONG NVARCHAR" => BindValue::TypedNull {
796            ora_type_num: ORA_TYPE_NUM_LONG,
797            csfrm: CS_FORM_NCHAR,
798            buffer_size: TNS_MAX_LONG_LENGTH,
799        },
800        "DB_TYPE_LONG_RAW" | "LONG RAW" | "LONG_BINARY" => BindValue::TypedNull {
801            ora_type_num: ORA_TYPE_NUM_LONG_RAW,
802            csfrm: 0,
803            buffer_size: TNS_MAX_LONG_LENGTH,
804        },
805        "DB_TYPE_RAW" | "BINARY" | "bytes" => BindValue::TypedNull {
806            ora_type_num: ORA_TYPE_NUM_RAW,
807            csfrm: 0,
808            buffer_size: size.max(1).max(4000),
809        },
810        "ROWID" | "DB_TYPE_ROWID" | "DB_TYPE_UROWID" => BindValue::TypedNull {
811            ora_type_num: ORA_TYPE_NUM_VARCHAR,
812            csfrm: CS_FORM_IMPLICIT,
813            buffer_size: 5267,
814        },
815        "DATETIME" | "DB_TYPE_DATE" | "date" | "datetime" => BindValue::TypedNull {
816            ora_type_num: ORA_TYPE_NUM_DATE,
817            csfrm: 0,
818            buffer_size: ORA_TYPE_SIZE_DATE,
819        },
820        "DB_TYPE_TIMESTAMP" | "TIMESTAMP" => BindValue::TypedNull {
821            ora_type_num: ORA_TYPE_NUM_TIMESTAMP,
822            csfrm: 0,
823            buffer_size: ORA_TYPE_SIZE_TIMESTAMP,
824        },
825        "DB_TYPE_TIMESTAMP_LTZ" | "TIMESTAMP WITH LOCAL TIME ZONE" => BindValue::TypedNull {
826            ora_type_num: ORA_TYPE_NUM_TIMESTAMP_LTZ,
827            csfrm: 0,
828            buffer_size: ORA_TYPE_SIZE_TIMESTAMP,
829        },
830        "DB_TYPE_TIMESTAMP_TZ" | "TIMESTAMP WITH TIME ZONE" => BindValue::TypedNull {
831            ora_type_num: ORA_TYPE_NUM_TIMESTAMP_TZ,
832            csfrm: 0,
833            buffer_size: ORA_TYPE_SIZE_TIMESTAMP_TZ,
834        },
835        "DB_TYPE_CURSOR" | "CURSOR" => cursor_bind_template(),
836        "DB_TYPE_VECTOR" | "VECTOR" => BindValue::TypedNull {
837            ora_type_num: ORA_TYPE_NUM_VECTOR,
838            csfrm: 0,
839            buffer_size: TNS_VECTOR_MAX_LENGTH,
840        },
841        "DB_TYPE_JSON" | "JSON" => BindValue::TypedNull {
842            ora_type_num: ORA_TYPE_NUM_JSON,
843            csfrm: 0,
844            buffer_size: TNS_VECTOR_MAX_LENGTH,
845        },
846        _ => BindValue::Null,
847    }
848}
849
850pub fn dbobject_element_bind_type_info(dbtype_name: &str, max_size: u32) -> BindTypeInfo {
851    let buffer_size = max_size.max(1);
852    let (ora_type_num, csfrm, buffer_size) = match dbtype_name {
853        "DB_TYPE_NUMBER" => (ORA_TYPE_NUM_NUMBER, 0, ORA_TYPE_SIZE_NUMBER),
854        "DB_TYPE_RAW" | "DB_TYPE_BLOB" => (ORA_TYPE_NUM_RAW, 0, buffer_size.max(4000)),
855        "DB_TYPE_NCHAR" | "DB_TYPE_NVARCHAR" | "DB_TYPE_NCLOB" => {
856            (ORA_TYPE_NUM_VARCHAR, CS_FORM_NCHAR, buffer_size.max(4000))
857        }
858        "DB_TYPE_DATE" => (ORA_TYPE_NUM_DATE, 0, ORA_TYPE_SIZE_DATE),
859        "DB_TYPE_TIMESTAMP" => (ORA_TYPE_NUM_TIMESTAMP, 0, ORA_TYPE_SIZE_TIMESTAMP),
860        "DB_TYPE_TIMESTAMP_LTZ" => (ORA_TYPE_NUM_TIMESTAMP_LTZ, 0, ORA_TYPE_SIZE_TIMESTAMP),
861        "DB_TYPE_TIMESTAMP_TZ" => (ORA_TYPE_NUM_TIMESTAMP_TZ, 0, ORA_TYPE_SIZE_TIMESTAMP_TZ),
862        _ => (
863            ORA_TYPE_NUM_VARCHAR,
864            CS_FORM_IMPLICIT,
865            buffer_size.max(4000),
866        ),
867    };
868    BindTypeInfo {
869        ora_type_num,
870        csfrm,
871        buffer_size,
872    }
873}
874
875pub(crate) fn public_dbtype_name_from_type_info(ora_type_num: u8, csfrm: u8) -> &'static str {
876    match (ora_type_num, csfrm) {
877        (ORA_TYPE_NUM_BINARY_DOUBLE, _) => "DB_TYPE_BINARY_DOUBLE",
878        (ORA_TYPE_NUM_BINARY_FLOAT, _) => "DB_TYPE_BINARY_FLOAT",
879        (ORA_TYPE_NUM_INTERVAL_DS, _) => "DB_TYPE_INTERVAL_DS",
880        (ORA_TYPE_NUM_INTERVAL_YM, _) => "DB_TYPE_INTERVAL_YM",
881        (ORA_TYPE_NUM_BOOLEAN, _) => "DB_TYPE_BOOLEAN",
882        (ORA_TYPE_NUM_BINARY_INTEGER, _) => "DB_TYPE_BINARY_INTEGER",
883        (ORA_TYPE_NUM_NUMBER, _) => "DB_TYPE_NUMBER",
884        (ORA_TYPE_NUM_CHAR, CS_FORM_NCHAR) | (ORA_TYPE_NUM_VARCHAR, CS_FORM_NCHAR) => {
885            "DB_TYPE_NVARCHAR"
886        }
887        (ORA_TYPE_NUM_CHAR, _) => "DB_TYPE_CHAR",
888        (ORA_TYPE_NUM_VARCHAR, _) => "DB_TYPE_VARCHAR",
889        (ORA_TYPE_NUM_LONG, CS_FORM_NCHAR) => "DB_TYPE_LONG_NVARCHAR",
890        (ORA_TYPE_NUM_LONG, _) => "DB_TYPE_LONG",
891        (ORA_TYPE_NUM_LONG_RAW, _) => "DB_TYPE_LONG_RAW",
892        (ORA_TYPE_NUM_RAW, _) => "DB_TYPE_RAW",
893        (ORA_TYPE_NUM_DATE, _) => "DB_TYPE_DATE",
894        (ORA_TYPE_NUM_TIMESTAMP, _) => "DB_TYPE_TIMESTAMP",
895        (ORA_TYPE_NUM_TIMESTAMP_LTZ, _) => "DB_TYPE_TIMESTAMP_LTZ",
896        (ORA_TYPE_NUM_TIMESTAMP_TZ, _) => "DB_TYPE_TIMESTAMP_TZ",
897        (ORA_TYPE_NUM_CURSOR, _) => "DB_TYPE_CURSOR",
898        (ORA_TYPE_NUM_OBJECT, _) => "DB_TYPE_OBJECT",
899        (ORA_TYPE_NUM_VECTOR, _) => "DB_TYPE_VECTOR",
900        (ORA_TYPE_NUM_JSON, _) => "DB_TYPE_JSON",
901        _ => "DB_TYPE_VARCHAR",
902    }
903}
904
905pub(crate) fn bind_metadata(value: &BindValue) -> (u8, u8, u32) {
906    bind_value_type_info(value)
907        .map(|info| (info.ora_type_num, info.csfrm, info.buffer_size))
908        .unwrap_or((ORA_TYPE_NUM_VARCHAR, CS_FORM_IMPLICIT, 1))
909}
910
911pub(crate) fn write_bind_value(writer: &mut TtcWriter, value: &BindValue, csfrm: u8) -> Result<()> {
912    match value {
913        BindValue::TypedNull {
914            ora_type_num: ORA_TYPE_NUM_CURSOR,
915            ..
916        } => {
917            writer.write_u8(1);
918            writer.write_u8(0);
919            Ok(())
920        }
921        // A NULL BOOLEAN bind is encoded as the two raw bytes
922        // [TNS_ESCAPE_CHAR, 1], not the usual single 0 null indicator; sending
923        // a plain 0 makes the server reject a PL/SQL BOOLEAN parameter with
924        // PLS-00306 (reference messages/base.pyx _write_bind_params_column).
925        BindValue::TypedNull {
926            ora_type_num: ORA_TYPE_NUM_BOOLEAN,
927            ..
928        } => {
929            writer.write_u8(TNS_ESCAPE_CHAR);
930            writer.write_u8(1);
931            Ok(())
932        }
933        BindValue::Null | BindValue::TypedNull { .. } => {
934            writer.write_u8(0);
935            Ok(())
936        }
937        BindValue::Output { .. } | BindValue::ReturnOutput { .. } => {
938            writer.write_u8(0);
939            Ok(())
940        }
941        BindValue::ObjectOutput { .. } => {
942            // NULL object image (empty OUT bind): reference messages/base.pyx
943            // 1462-1468.
944            writer.write_ub4(0);
945            writer.write_ub4(0);
946            writer.write_ub4(0);
947            writer.write_ub2(0);
948            writer.write_ub4(0);
949            writer.write_ub4(TNS_OBJ_TOP_LEVEL);
950            Ok(())
951        }
952        BindValue::ObjectInput { oid, image, .. } => write_dbobject_bind(writer, oid, image),
953        BindValue::Text(value) => {
954            // The common implicit/single-byte-charset case writes the &str bytes
955            // straight through (no throwaway Vec); only NCHAR re-encodes to UTF-16
956            // and needs the owned buffer. Byte-identical to encode_text_value,
957            // which for the non-NCHAR path is exactly `value.as_bytes().to_vec()`.
958            if csfrm == CS_FORM_NCHAR {
959                let bytes = encode_text_value(value, csfrm);
960                writer.write_bytes_with_length(&bytes)
961            } else {
962                writer.write_bytes_with_length(value.as_bytes())
963            }
964        }
965        BindValue::Raw(value) => writer.write_bytes_with_length(value),
966        BindValue::Lob { locator, .. } => writer.write_bytes_with_two_lengths(Some(locator)),
967        BindValue::Number(value) | BindValue::BinaryInteger(value) => {
968            let bytes = encode_number_text(value)?;
969            writer.write_bytes_with_length(&bytes)
970        }
971        // reference encode_boolean (impl/base/encoders.pyx:99-111): true is
972        // the two bytes [1, 1]; false is the single byte [0]
973        BindValue::Boolean(value) => {
974            let bytes: &[u8] = if *value { &[1, 1] } else { &[0] };
975            writer.write_bytes_with_length(bytes)
976        }
977        BindValue::BinaryDouble(value) => {
978            let bytes = encode_binary_double(*value);
979            writer.write_bytes_with_length(&bytes)
980        }
981        BindValue::BinaryFloat(value) => {
982            let bytes = encode_binary_float(*value as f32);
983            writer.write_bytes_with_length(&bytes)
984        }
985        BindValue::IntervalDS {
986            days,
987            seconds,
988            microseconds,
989        } => {
990            let nanoseconds = microseconds
991                .checked_mul(1000)
992                .ok_or(ProtocolError::TtcDecode(
993                    "INTERVAL DS fractional seconds out of range",
994                ))?;
995            let bytes = encode_interval_ds(*days, *seconds, nanoseconds)?;
996            writer.write_bytes_with_length(&bytes)
997        }
998        BindValue::IntervalYM { years, months } => {
999            let bytes = encode_interval_ym(*years, *months)?;
1000            writer.write_bytes_with_length(&bytes)
1001        }
1002        BindValue::DateTime {
1003            year,
1004            month,
1005            day,
1006            hour,
1007            minute,
1008            second,
1009        } => {
1010            let bytes = encode_oracle_date(*year, *month, *day, *hour, *minute, *second)?;
1011            writer.write_bytes_with_length(&bytes)
1012        }
1013        BindValue::Timestamp {
1014            year,
1015            month,
1016            day,
1017            hour,
1018            minute,
1019            second,
1020            nanosecond,
1021            ora_type_num,
1022        } => {
1023            let bytes = if matches!(*ora_type_num, ORA_TYPE_NUM_TIMESTAMP_TZ) {
1024                encode_oracle_timestamp_tz(
1025                    *year,
1026                    *month,
1027                    *day,
1028                    *hour,
1029                    *minute,
1030                    *second,
1031                    *nanosecond,
1032                )?
1033            } else {
1034                encode_oracle_timestamp(*year, *month, *day, *hour, *minute, *second, *nanosecond)?
1035            };
1036            writer.write_bytes_with_length(&bytes)
1037        }
1038        BindValue::TimestampTz {
1039            year,
1040            month,
1041            day,
1042            hour,
1043            minute,
1044            second,
1045            nanosecond,
1046            offset_minutes,
1047        } => {
1048            let bytes = encode_oracle_timestamp_tz_with_offset(
1049                *year,
1050                *month,
1051                *day,
1052                *hour,
1053                *minute,
1054                *second,
1055                *nanosecond,
1056                *offset_minutes,
1057            )?;
1058            writer.write_bytes_with_length(&bytes)
1059        }
1060        BindValue::Array {
1061            values,
1062            csfrm: array_csfrm,
1063            ..
1064        } => {
1065            writer.write_ub4(u32::try_from(values.len()).map_err(|_| {
1066                ProtocolError::InvalidPacketLength {
1067                    length: values.len(),
1068                    minimum: 0,
1069                }
1070            })?);
1071            for value in values {
1072                match value {
1073                    Some(value) => write_bind_value(writer, value, *array_csfrm)?,
1074                    None => writer.write_u8(0),
1075                }
1076            }
1077            Ok(())
1078        }
1079        // reference WriteBuffer.write_vector: a QLocator carrying the image
1080        // length, then the image bytes-with-length
1081        BindValue::Vector(vector) => {
1082            let image = crate::vector::encode_vector_checked(vector)?;
1083            crate::vector::write_vector_image(writer, &image)
1084        }
1085        // reference WriteBuffer.write_oson: a QLocator carrying the OSON image
1086        // length, then the image bytes-with-length (same framing as VECTOR).
1087        BindValue::Json(image) => crate::vector::write_vector_image(writer, image),
1088        BindValue::Cursor { cursor_id } => {
1089            if *cursor_id == 0 {
1090                writer.write_u8(1);
1091                writer.write_u8(0);
1092            } else {
1093                writer.write_ub4(1);
1094                writer.write_ub4(*cursor_id);
1095            }
1096            Ok(())
1097        }
1098    }
1099}
1100
1101pub(crate) fn encode_text_value(value: &str, csfrm: u8) -> Vec<u8> {
1102    if csfrm == CS_FORM_NCHAR {
1103        let mut bytes = Vec::with_capacity(value.len().saturating_mul(2));
1104        for unit in value.encode_utf16() {
1105            bytes.extend_from_slice(&unit.to_be_bytes());
1106        }
1107        bytes
1108    } else {
1109        value.as_bytes().to_vec()
1110    }
1111}