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