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