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