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        // Timestamp/interval attributes also carry precision/scale in the data
675        // dictionary (leading-field precision + fractional-seconds scale).
676        // Propagate them with upstream's defaults instead of discarding them
677        // (reference impl commit 6cfd00aa642e).
678        "TIMESTAMP" | "TIMESTAMP WITH TIME ZONE" | "TIMESTAMP WITH LOCAL TIME ZONE" => {
679            (precision.unwrap_or(0), scale.unwrap_or(6))
680        }
681        "INTERVAL DAY TO SECOND" => (precision.unwrap_or(2), scale.unwrap_or(6)),
682        "INTERVAL YEAR TO MONTH" => (precision.unwrap_or(2), scale.unwrap_or(0)),
683        _ => (0, 0),
684    }
685}
686
687pub fn dbobject_attr_max_size(type_name: &str, length: Option<u32>) -> u32 {
688    let length = length.unwrap_or(0);
689    match type_name.to_ascii_uppercase().as_str() {
690        "NCHAR" | "NVARCHAR2" | "NVARCHAR" => length.saturating_mul(2),
691        _ => length,
692    }
693}
694
695pub fn dbobject_rowtype_attr_max_size(
696    type_name: &str,
697    data_length: Option<u32>,
698    char_length: Option<u32>,
699) -> u32 {
700    match type_name.to_ascii_uppercase().as_str() {
701        "CHAR" | "VARCHAR" | "VARCHAR2" | "RAW" => data_length.unwrap_or(0),
702        "NCHAR" | "NVARCHAR" | "NVARCHAR2" => dbobject_attr_max_size(
703            type_name,
704            char_length.filter(|length| *length > 0).or(data_length),
705        ),
706        _ => 0,
707    }
708}
709
710pub fn public_dbtype_name_from_bind(value: &BindValue) -> &'static str {
711    match value {
712        BindValue::TypedNull {
713            ora_type_num,
714            csfrm,
715            ..
716        }
717        | BindValue::Output {
718            ora_type_num,
719            csfrm,
720            ..
721        }
722        | BindValue::ReturnOutput {
723            ora_type_num,
724            csfrm,
725            ..
726        }
727        | BindValue::Array {
728            ora_type_num,
729            csfrm,
730            ..
731        } => public_dbtype_name_from_type_info(*ora_type_num, *csfrm),
732        BindValue::ObjectOutput { .. } | BindValue::ObjectInput { .. } => "DB_TYPE_OBJECT",
733        // An IN OUT bind's public type is that of its input value.
734        BindValue::InOut { value, .. } => public_dbtype_name_from_bind(value),
735        BindValue::Text(_) => "DB_TYPE_VARCHAR",
736        BindValue::Raw(_) => "DB_TYPE_RAW",
737        BindValue::Lob {
738            ora_type_num,
739            csfrm,
740            ..
741        } => match (*ora_type_num, *csfrm) {
742            (ORA_TYPE_NUM_BLOB, _) => "DB_TYPE_BLOB",
743            (ORA_TYPE_NUM_CLOB, CS_FORM_NCHAR) => "DB_TYPE_NCLOB",
744            (ORA_TYPE_NUM_CLOB, _) => "DB_TYPE_CLOB",
745            _ => "DB_TYPE_CLOB",
746        },
747        BindValue::Number(_) => "DB_TYPE_NUMBER",
748        BindValue::BinaryInteger(_) => "DB_TYPE_BINARY_INTEGER",
749        BindValue::BinaryDouble(_) => "DB_TYPE_BINARY_DOUBLE",
750        BindValue::BinaryFloat(_) => "DB_TYPE_BINARY_FLOAT",
751        BindValue::Boolean(_) => "DB_TYPE_BOOLEAN",
752        BindValue::IntervalDS { .. } => "DB_TYPE_INTERVAL_DS",
753        BindValue::IntervalYM { .. } => "DB_TYPE_INTERVAL_YM",
754        BindValue::DateTime { .. } => "DB_TYPE_DATE",
755        BindValue::Timestamp { ora_type_num, .. } => match *ora_type_num {
756            ORA_TYPE_NUM_TIMESTAMP_LTZ => "DB_TYPE_TIMESTAMP_LTZ",
757            ORA_TYPE_NUM_TIMESTAMP_TZ => "DB_TYPE_TIMESTAMP_TZ",
758            _ => "DB_TYPE_TIMESTAMP",
759        },
760        BindValue::TimestampTz { .. } => "DB_TYPE_TIMESTAMP_TZ",
761        BindValue::Vector(_) => "DB_TYPE_VECTOR",
762        BindValue::Json(_) => "DB_TYPE_JSON",
763        BindValue::Cursor { .. } => "DB_TYPE_CURSOR",
764        BindValue::Null => "DB_TYPE_VARCHAR",
765    }
766}
767
768pub fn bind_template_from_type_name(type_name: &str, size: u32) -> BindValue {
769    let text_buffer_size = if size == 0 { 4000 } else { size.max(1) };
770    let nchar_buffer_size = text_buffer_size.saturating_mul(4);
771    match type_name {
772        "NUMBER" | "DB_TYPE_NUMBER" | "int" | "float" | "Decimal" => BindValue::TypedNull {
773            ora_type_num: ORA_TYPE_NUM_NUMBER,
774            csfrm: 0,
775            buffer_size: ORA_TYPE_SIZE_NUMBER,
776        },
777        "NATIVE_INT" | "DB_TYPE_BINARY_INTEGER" => BindValue::TypedNull {
778            ora_type_num: ORA_TYPE_NUM_BINARY_INTEGER,
779            csfrm: 0,
780            buffer_size: ORA_TYPE_SIZE_NUMBER,
781        },
782        "NATIVE_FLOAT" | "DB_TYPE_BINARY_DOUBLE" => BindValue::TypedNull {
783            ora_type_num: ORA_TYPE_NUM_BINARY_DOUBLE,
784            csfrm: 0,
785            buffer_size: ORA_TYPE_SIZE_BINARY_DOUBLE,
786        },
787        "DB_TYPE_BINARY_FLOAT" | "BINARY_FLOAT" => BindValue::TypedNull {
788            ora_type_num: ORA_TYPE_NUM_BINARY_FLOAT,
789            csfrm: 0,
790            buffer_size: ORA_TYPE_SIZE_BINARY_FLOAT,
791        },
792        "DB_TYPE_BOOLEAN" | "BOOLEAN" | "bool" => BindValue::TypedNull {
793            ora_type_num: ORA_TYPE_NUM_BOOLEAN,
794            csfrm: 0,
795            buffer_size: ORA_TYPE_SIZE_BOOLEAN,
796        },
797        "DB_TYPE_INTERVAL_DS" | "INTERVAL DAY TO SECOND" | "timedelta" => BindValue::TypedNull {
798            ora_type_num: ORA_TYPE_NUM_INTERVAL_DS,
799            csfrm: 0,
800            buffer_size: ORA_TYPE_SIZE_INTERVAL_DS,
801        },
802        "DB_TYPE_INTERVAL_YM" | "INTERVAL YEAR TO MONTH" | "IntervalYM" => BindValue::TypedNull {
803            ora_type_num: ORA_TYPE_NUM_INTERVAL_YM,
804            csfrm: 0,
805            buffer_size: ORA_TYPE_SIZE_INTERVAL_YM,
806        },
807        "STRING" | "DB_TYPE_VARCHAR" | "DB_TYPE_CHAR" | "str" => BindValue::TypedNull {
808            ora_type_num: ORA_TYPE_NUM_VARCHAR,
809            csfrm: CS_FORM_IMPLICIT,
810            buffer_size: text_buffer_size,
811        },
812        "DB_TYPE_NCHAR" | "DB_TYPE_NVARCHAR" => BindValue::TypedNull {
813            ora_type_num: ORA_TYPE_NUM_VARCHAR,
814            csfrm: CS_FORM_NCHAR,
815            buffer_size: nchar_buffer_size,
816        },
817        "DB_TYPE_CLOB" | "CLOB" => BindValue::TypedNull {
818            ora_type_num: ORA_TYPE_NUM_LONG,
819            csfrm: CS_FORM_IMPLICIT,
820            buffer_size: TNS_MAX_LONG_LENGTH,
821        },
822        "DB_TYPE_NCLOB" | "NCLOB" => BindValue::TypedNull {
823            ora_type_num: ORA_TYPE_NUM_LONG,
824            csfrm: CS_FORM_NCHAR,
825            buffer_size: TNS_MAX_LONG_LENGTH,
826        },
827        "DB_TYPE_BLOB" | "BLOB" => BindValue::TypedNull {
828            ora_type_num: ORA_TYPE_NUM_LONG_RAW,
829            csfrm: 0,
830            buffer_size: TNS_MAX_LONG_LENGTH,
831        },
832        "DB_TYPE_LONG" | "LONG" | "LONG_STRING" => BindValue::TypedNull {
833            ora_type_num: ORA_TYPE_NUM_LONG,
834            csfrm: CS_FORM_IMPLICIT,
835            buffer_size: TNS_MAX_LONG_LENGTH,
836        },
837        "DB_TYPE_LONG_NVARCHAR" | "LONG NVARCHAR" => BindValue::TypedNull {
838            ora_type_num: ORA_TYPE_NUM_LONG,
839            csfrm: CS_FORM_NCHAR,
840            buffer_size: TNS_MAX_LONG_LENGTH,
841        },
842        "DB_TYPE_LONG_RAW" | "LONG RAW" | "LONG_BINARY" => BindValue::TypedNull {
843            ora_type_num: ORA_TYPE_NUM_LONG_RAW,
844            csfrm: 0,
845            buffer_size: TNS_MAX_LONG_LENGTH,
846        },
847        "DB_TYPE_RAW" | "BINARY" | "bytes" => BindValue::TypedNull {
848            ora_type_num: ORA_TYPE_NUM_RAW,
849            csfrm: 0,
850            buffer_size: size.max(1).max(4000),
851        },
852        "ROWID" | "DB_TYPE_ROWID" | "DB_TYPE_UROWID" => BindValue::TypedNull {
853            ora_type_num: ORA_TYPE_NUM_VARCHAR,
854            csfrm: CS_FORM_IMPLICIT,
855            buffer_size: 5267,
856        },
857        "DATETIME" | "DB_TYPE_DATE" | "date" | "datetime" => BindValue::TypedNull {
858            ora_type_num: ORA_TYPE_NUM_DATE,
859            csfrm: 0,
860            buffer_size: ORA_TYPE_SIZE_DATE,
861        },
862        "DB_TYPE_TIMESTAMP" | "TIMESTAMP" => BindValue::TypedNull {
863            ora_type_num: ORA_TYPE_NUM_TIMESTAMP,
864            csfrm: 0,
865            buffer_size: ORA_TYPE_SIZE_TIMESTAMP,
866        },
867        "DB_TYPE_TIMESTAMP_LTZ" | "TIMESTAMP WITH LOCAL TIME ZONE" => BindValue::TypedNull {
868            ora_type_num: ORA_TYPE_NUM_TIMESTAMP_LTZ,
869            csfrm: 0,
870            buffer_size: ORA_TYPE_SIZE_TIMESTAMP,
871        },
872        "DB_TYPE_TIMESTAMP_TZ" | "TIMESTAMP WITH TIME ZONE" => BindValue::TypedNull {
873            ora_type_num: ORA_TYPE_NUM_TIMESTAMP_TZ,
874            csfrm: 0,
875            buffer_size: ORA_TYPE_SIZE_TIMESTAMP_TZ,
876        },
877        "DB_TYPE_CURSOR" | "CURSOR" => cursor_bind_template(),
878        "DB_TYPE_VECTOR" | "VECTOR" => BindValue::TypedNull {
879            ora_type_num: ORA_TYPE_NUM_VECTOR,
880            csfrm: 0,
881            buffer_size: TNS_VECTOR_MAX_LENGTH,
882        },
883        "DB_TYPE_JSON" | "JSON" => BindValue::TypedNull {
884            ora_type_num: ORA_TYPE_NUM_JSON,
885            csfrm: 0,
886            buffer_size: TNS_VECTOR_MAX_LENGTH,
887        },
888        _ => BindValue::Null,
889    }
890}
891
892pub fn dbobject_element_bind_type_info(dbtype_name: &str, max_size: u32) -> BindTypeInfo {
893    let buffer_size = max_size.max(1);
894    let (ora_type_num, csfrm, buffer_size) = match dbtype_name {
895        "DB_TYPE_NUMBER" => (ORA_TYPE_NUM_NUMBER, 0, ORA_TYPE_SIZE_NUMBER),
896        "DB_TYPE_RAW" | "DB_TYPE_BLOB" => (ORA_TYPE_NUM_RAW, 0, buffer_size.max(4000)),
897        "DB_TYPE_NCHAR" | "DB_TYPE_NVARCHAR" | "DB_TYPE_NCLOB" => {
898            (ORA_TYPE_NUM_VARCHAR, CS_FORM_NCHAR, buffer_size.max(4000))
899        }
900        "DB_TYPE_DATE" => (ORA_TYPE_NUM_DATE, 0, ORA_TYPE_SIZE_DATE),
901        "DB_TYPE_TIMESTAMP" => (ORA_TYPE_NUM_TIMESTAMP, 0, ORA_TYPE_SIZE_TIMESTAMP),
902        "DB_TYPE_TIMESTAMP_LTZ" => (ORA_TYPE_NUM_TIMESTAMP_LTZ, 0, ORA_TYPE_SIZE_TIMESTAMP),
903        "DB_TYPE_TIMESTAMP_TZ" => (ORA_TYPE_NUM_TIMESTAMP_TZ, 0, ORA_TYPE_SIZE_TIMESTAMP_TZ),
904        _ => (
905            ORA_TYPE_NUM_VARCHAR,
906            CS_FORM_IMPLICIT,
907            buffer_size.max(4000),
908        ),
909    };
910    BindTypeInfo {
911        ora_type_num,
912        csfrm,
913        buffer_size,
914    }
915}
916
917pub(crate) fn public_dbtype_name_from_type_info(ora_type_num: u8, csfrm: u8) -> &'static str {
918    match (ora_type_num, csfrm) {
919        (ORA_TYPE_NUM_BINARY_DOUBLE, _) => "DB_TYPE_BINARY_DOUBLE",
920        (ORA_TYPE_NUM_BINARY_FLOAT, _) => "DB_TYPE_BINARY_FLOAT",
921        (ORA_TYPE_NUM_INTERVAL_DS, _) => "DB_TYPE_INTERVAL_DS",
922        (ORA_TYPE_NUM_INTERVAL_YM, _) => "DB_TYPE_INTERVAL_YM",
923        (ORA_TYPE_NUM_BOOLEAN, _) => "DB_TYPE_BOOLEAN",
924        (ORA_TYPE_NUM_BINARY_INTEGER, _) => "DB_TYPE_BINARY_INTEGER",
925        (ORA_TYPE_NUM_NUMBER, _) => "DB_TYPE_NUMBER",
926        (ORA_TYPE_NUM_CHAR, CS_FORM_NCHAR) | (ORA_TYPE_NUM_VARCHAR, CS_FORM_NCHAR) => {
927            "DB_TYPE_NVARCHAR"
928        }
929        (ORA_TYPE_NUM_CHAR, _) => "DB_TYPE_CHAR",
930        (ORA_TYPE_NUM_VARCHAR, _) => "DB_TYPE_VARCHAR",
931        (ORA_TYPE_NUM_LONG, CS_FORM_NCHAR) => "DB_TYPE_LONG_NVARCHAR",
932        (ORA_TYPE_NUM_LONG, _) => "DB_TYPE_LONG",
933        (ORA_TYPE_NUM_LONG_RAW, _) => "DB_TYPE_LONG_RAW",
934        (ORA_TYPE_NUM_RAW, _) => "DB_TYPE_RAW",
935        (ORA_TYPE_NUM_DATE, _) => "DB_TYPE_DATE",
936        (ORA_TYPE_NUM_TIMESTAMP, _) => "DB_TYPE_TIMESTAMP",
937        (ORA_TYPE_NUM_TIMESTAMP_LTZ, _) => "DB_TYPE_TIMESTAMP_LTZ",
938        (ORA_TYPE_NUM_TIMESTAMP_TZ, _) => "DB_TYPE_TIMESTAMP_TZ",
939        (ORA_TYPE_NUM_CURSOR, _) => "DB_TYPE_CURSOR",
940        (ORA_TYPE_NUM_OBJECT, _) => "DB_TYPE_OBJECT",
941        (ORA_TYPE_NUM_VECTOR, _) => "DB_TYPE_VECTOR",
942        (ORA_TYPE_NUM_JSON, _) => "DB_TYPE_JSON",
943        _ => "DB_TYPE_VARCHAR",
944    }
945}
946
947pub(crate) fn bind_metadata(value: &BindValue) -> (u8, u8, u32) {
948    bind_value_type_info(value)
949        .map(|info| (info.ora_type_num, info.csfrm, info.buffer_size))
950        .unwrap_or((ORA_TYPE_NUM_VARCHAR, CS_FORM_IMPLICIT, 1))
951}
952
953pub(crate) fn write_bind_value(writer: &mut TtcWriter, value: &BindValue, csfrm: u8) -> Result<()> {
954    match value {
955        BindValue::TypedNull {
956            ora_type_num: ORA_TYPE_NUM_CURSOR,
957            ..
958        } => {
959            writer.write_u8(1);
960            writer.write_u8(0);
961            Ok(())
962        }
963        // A NULL BOOLEAN bind is encoded as the two raw bytes
964        // [TNS_ESCAPE_CHAR, 1], not the usual single 0 null indicator; sending
965        // a plain 0 makes the server reject a PL/SQL BOOLEAN parameter with
966        // PLS-00306 (reference messages/base.pyx _write_bind_params_column).
967        BindValue::TypedNull {
968            ora_type_num: ORA_TYPE_NUM_BOOLEAN,
969            ..
970        } => {
971            writer.write_u8(TNS_ESCAPE_CHAR);
972            writer.write_u8(1);
973            Ok(())
974        }
975        BindValue::Null | BindValue::TypedNull { .. } => {
976            writer.write_u8(0);
977            Ok(())
978        }
979        BindValue::Output { .. } | BindValue::ReturnOutput { .. } => {
980            writer.write_u8(0);
981            Ok(())
982        }
983        // An IN OUT bind sends its input value bytes (never a null indicator);
984        // the read-back is driven by the server's IO-vector direction, not by
985        // anything written here. Recurse with the resolved csfrm (which the
986        // metadata derived from this same inner value).
987        BindValue::InOut { value, .. } => write_bind_value(writer, value, csfrm),
988        BindValue::ObjectOutput { .. } => {
989            // NULL object image (empty OUT bind): reference messages/base.pyx
990            // 1462-1468.
991            writer.write_ub4(0);
992            writer.write_ub4(0);
993            writer.write_ub4(0);
994            writer.write_ub2(0);
995            writer.write_ub4(0);
996            writer.write_ub4(TNS_OBJ_TOP_LEVEL);
997            Ok(())
998        }
999        BindValue::ObjectInput { oid, image, .. } => write_dbobject_bind(writer, oid, image),
1000        BindValue::Text(value) => {
1001            // The common implicit/single-byte-charset case writes the &str bytes
1002            // straight through (no throwaway Vec); only NCHAR re-encodes to UTF-16
1003            // and needs the owned buffer. Byte-identical to encode_text_value,
1004            // which for the non-NCHAR path is exactly `value.as_bytes().to_vec()`.
1005            if csfrm == CS_FORM_NCHAR {
1006                let bytes = encode_text_value(value, csfrm);
1007                writer.write_bytes_with_length(&bytes)
1008            } else {
1009                writer.write_bytes_with_length(value.as_bytes())
1010            }
1011        }
1012        BindValue::Raw(value) => writer.write_bytes_with_length(value),
1013        BindValue::Lob { locator, .. } => writer.write_bytes_with_two_lengths(Some(locator)),
1014        BindValue::Number(value) | BindValue::BinaryInteger(value) => {
1015            let bytes = encode_number_text(value)?;
1016            writer.write_bytes_with_length(&bytes)
1017        }
1018        // reference encode_boolean (impl/base/encoders.pyx:99-111): true is
1019        // the two bytes [1, 1]; false is the single byte [0]
1020        BindValue::Boolean(value) => {
1021            let bytes: &[u8] = if *value { &[1, 1] } else { &[0] };
1022            writer.write_bytes_with_length(bytes)
1023        }
1024        BindValue::BinaryDouble(value) => {
1025            let bytes = encode_binary_double(*value);
1026            writer.write_bytes_with_length(&bytes)
1027        }
1028        BindValue::BinaryFloat(value) => {
1029            let bytes = encode_binary_float(*value as f32);
1030            writer.write_bytes_with_length(&bytes)
1031        }
1032        BindValue::IntervalDS {
1033            days,
1034            seconds,
1035            microseconds,
1036        } => {
1037            let nanoseconds = microseconds
1038                .checked_mul(1000)
1039                .ok_or(ProtocolError::TtcDecode(
1040                    "INTERVAL DS fractional seconds out of range",
1041                ))?;
1042            let bytes = encode_interval_ds(*days, *seconds, nanoseconds)?;
1043            writer.write_bytes_with_length(&bytes)
1044        }
1045        BindValue::IntervalYM { years, months } => {
1046            let bytes = encode_interval_ym(*years, *months)?;
1047            writer.write_bytes_with_length(&bytes)
1048        }
1049        BindValue::DateTime {
1050            year,
1051            month,
1052            day,
1053            hour,
1054            minute,
1055            second,
1056        } => {
1057            let bytes = encode_oracle_date(*year, *month, *day, *hour, *minute, *second)?;
1058            writer.write_bytes_with_length(&bytes)
1059        }
1060        BindValue::Timestamp {
1061            year,
1062            month,
1063            day,
1064            hour,
1065            minute,
1066            second,
1067            nanosecond,
1068            ora_type_num,
1069        } => {
1070            let bytes = if matches!(*ora_type_num, ORA_TYPE_NUM_TIMESTAMP_TZ) {
1071                encode_oracle_timestamp_tz(
1072                    *year,
1073                    *month,
1074                    *day,
1075                    *hour,
1076                    *minute,
1077                    *second,
1078                    *nanosecond,
1079                )?
1080            } else {
1081                encode_oracle_timestamp(*year, *month, *day, *hour, *minute, *second, *nanosecond)?
1082            };
1083            writer.write_bytes_with_length(&bytes)
1084        }
1085        BindValue::TimestampTz {
1086            year,
1087            month,
1088            day,
1089            hour,
1090            minute,
1091            second,
1092            nanosecond,
1093            offset_minutes,
1094        } => {
1095            let bytes = encode_oracle_timestamp_tz_with_offset(
1096                *year,
1097                *month,
1098                *day,
1099                *hour,
1100                *minute,
1101                *second,
1102                *nanosecond,
1103                *offset_minutes,
1104            )?;
1105            writer.write_bytes_with_length(&bytes)
1106        }
1107        BindValue::Array {
1108            values,
1109            csfrm: array_csfrm,
1110            ..
1111        } => {
1112            writer.write_ub4(u32::try_from(values.len()).map_err(|_| {
1113                ProtocolError::InvalidPacketLength {
1114                    length: values.len(),
1115                    minimum: 0,
1116                }
1117            })?);
1118            for value in values {
1119                match value {
1120                    Some(value) => write_bind_value(writer, value, *array_csfrm)?,
1121                    None => writer.write_u8(0),
1122                }
1123            }
1124            Ok(())
1125        }
1126        // reference WriteBuffer.write_vector: a QLocator carrying the image
1127        // length, then the image bytes-with-length
1128        BindValue::Vector(vector) => {
1129            let image = crate::vector::encode_vector_checked(vector)?;
1130            crate::vector::write_vector_image(writer, &image)
1131        }
1132        // reference WriteBuffer.write_oson: a QLocator carrying the OSON image
1133        // length, then the image bytes-with-length (same framing as VECTOR).
1134        BindValue::Json(image) => crate::vector::write_vector_image(writer, image),
1135        BindValue::Cursor { cursor_id } => {
1136            if *cursor_id == 0 {
1137                writer.write_u8(1);
1138                writer.write_u8(0);
1139            } else {
1140                writer.write_ub4(1);
1141                writer.write_ub4(*cursor_id);
1142            }
1143            Ok(())
1144        }
1145    }
1146}
1147
1148pub(crate) fn encode_text_value(value: &str, csfrm: u8) -> Vec<u8> {
1149    if csfrm == CS_FORM_NCHAR {
1150        let mut bytes = Vec::with_capacity(value.len().saturating_mul(2));
1151        for unit in value.encode_utf16() {
1152            bytes.extend_from_slice(&unit.to_be_bytes());
1153        }
1154        bytes
1155    } else {
1156        value.as_bytes().to_vec()
1157    }
1158}
1159
1160#[cfg(test)]
1161mod in_out_bind_tests {
1162    use super::*;
1163
1164    fn written_bytes(value: &BindValue, csfrm: u8) -> Vec<u8> {
1165        let mut writer = TtcWriter::new();
1166        write_bind_value(&mut writer, value, csfrm).expect("write bind value");
1167        writer.into_bytes()
1168    }
1169
1170    // An IN OUT NUMBER bind sends its input value (not a null indicator) and its
1171    // metadata reserves an output slot. NUMBER is a fixed 22-byte type, so the
1172    // input and output sizes coincide; the point here is that the value bytes
1173    // are on the wire exactly as a plain IN bind writes them.
1174    #[test]
1175    fn in_out_number_writes_input_and_reserves_output_slot() {
1176        let inout = BindValue::InOut {
1177            value: Box::new(BindValue::Number("21".to_string())),
1178            out_buffer_size: ORA_TYPE_SIZE_NUMBER,
1179        };
1180        // Not an output-only placeholder: the input value is sent, never a null.
1181        assert!(!inout.is_output_only(), "IN OUT carries an input value");
1182        assert!(!inout.is_return_output(), "IN OUT is not a function RETURN");
1183
1184        let info = bind_value_type_info(&inout).expect("IN OUT NUMBER has type info");
1185        assert_eq!(info.ora_type_num, ORA_TYPE_NUM_NUMBER);
1186        assert_eq!(info.csfrm, 0);
1187        assert_eq!(info.buffer_size, ORA_TYPE_SIZE_NUMBER);
1188
1189        // The bytes on the wire are byte-identical to a plain IN NUMBER(21):
1190        // an IN OUT bind is encoded as an input bind.
1191        assert_eq!(
1192            written_bytes(&inout, 0),
1193            written_bytes(&BindValue::Number("21".to_string()), 0),
1194            "IN OUT sends the input value, same bytes as a plain IN bind"
1195        );
1196    }
1197
1198    // An IN OUT VARCHAR must size its bind buffer for the *returned* value, which
1199    // is typically larger than the (possibly short) input. The metadata takes the
1200    // max of the input's natural size and the requested output size; the value
1201    // bytes are still just the input text.
1202    #[test]
1203    fn in_out_varchar_sizes_output_slot_and_writes_input_text() {
1204        // input "ab" natural size = 2 chars * 4 = 8; the OUT slot wants 200.
1205        let inout = BindValue::InOut {
1206            value: Box::new(BindValue::Text("ab".to_string())),
1207            out_buffer_size: 200,
1208        };
1209        let info = bind_value_type_info(&inout).expect("IN OUT VARCHAR has type info");
1210        assert_eq!(info.ora_type_num, ORA_TYPE_NUM_VARCHAR);
1211        assert_eq!(info.csfrm, CS_FORM_IMPLICIT);
1212        assert_eq!(
1213            info.buffer_size, 200,
1214            "the OUT slot is sized for the returned value, not the short input"
1215        );
1216
1217        // The written bytes are exactly the input text (same as a plain IN Text).
1218        assert_eq!(
1219            written_bytes(&inout, CS_FORM_IMPLICIT),
1220            written_bytes(&BindValue::Text("ab".to_string()), CS_FORM_IMPLICIT),
1221        );
1222
1223        // When the input is the larger side, its natural size wins.
1224        let big_input = BindValue::InOut {
1225            value: Box::new(BindValue::Text("abcdefghij".to_string())), // 10 * 4 = 40
1226            out_buffer_size: 4,
1227        };
1228        assert_eq!(
1229            bind_value_type_info(&big_input).unwrap().buffer_size,
1230            40,
1231            "buffer is max(input natural size, requested output size)"
1232        );
1233    }
1234
1235    // The read-back metadata a server response is decoded against
1236    // (`bind_column_metadata`) must carry the IN OUT bind's Oracle type and the
1237    // reserved output buffer, so the returned value parses correctly.
1238    #[test]
1239    fn in_out_read_back_metadata_matches_output_slot() {
1240        let inout = BindValue::InOut {
1241            value: Box::new(BindValue::Text("ab".to_string())),
1242            out_buffer_size: 200,
1243        };
1244        let column = crate::thin::bind_column_metadata(&inout);
1245        assert_eq!(column.ora_type_num, ORA_TYPE_NUM_VARCHAR);
1246        assert_eq!(column.csfrm, CS_FORM_IMPLICIT);
1247        assert_eq!(column.buffer_size, 200);
1248        assert_eq!(column.max_size, 200);
1249        assert!(!column.is_array);
1250    }
1251}