Skip to main content

oracledb_protocol/thin/
execute.rs

1#![forbid(unsafe_code)]
2
3use super::*;
4
5pub fn build_execute_query_payload(
6    sql: &str,
7    prefetch_rows: u32,
8    ttc_field_version: u8,
9) -> Result<Vec<u8>> {
10    build_execute_query_payload_with_seq(sql, prefetch_rows, 1, ttc_field_version)
11}
12
13pub fn build_execute_query_payload_with_seq(
14    sql: &str,
15    prefetch_rows: u32,
16    seq_num: u8,
17    ttc_field_version: u8,
18) -> Result<Vec<u8>> {
19    build_execute_payload_with_seq(sql, prefetch_rows, seq_num, true, ttc_field_version)
20}
21
22pub fn build_execute_payload_with_seq(
23    sql: &str,
24    prefetch_rows: u32,
25    seq_num: u8,
26    is_query: bool,
27    ttc_field_version: u8,
28) -> Result<Vec<u8>> {
29    build_execute_payload_with_binds_with_seq(
30        sql,
31        prefetch_rows,
32        seq_num,
33        is_query,
34        &[],
35        ttc_field_version,
36    )
37}
38
39pub fn build_execute_payload_with_binds_with_seq(
40    sql: &str,
41    prefetch_rows: u32,
42    seq_num: u8,
43    is_query: bool,
44    binds: &[BindValue],
45    ttc_field_version: u8,
46) -> Result<Vec<u8>> {
47    let bind_rows = if binds.is_empty() {
48        Vec::new()
49    } else {
50        vec![binds.to_vec()]
51    };
52    build_execute_payload_with_bind_rows_with_seq(
53        sql,
54        prefetch_rows,
55        seq_num,
56        is_query,
57        &bind_rows,
58        ttc_field_version,
59    )
60}
61
62pub fn build_execute_payload_with_bind_rows_with_seq(
63    sql: &str,
64    prefetch_rows: u32,
65    seq_num: u8,
66    is_query: bool,
67    bind_rows: &[Vec<BindValue>],
68    ttc_field_version: u8,
69) -> Result<Vec<u8>> {
70    build_execute_payload_with_bind_rows_and_options_with_seq(
71        sql,
72        prefetch_rows,
73        seq_num,
74        is_query,
75        bind_rows,
76        ExecuteOptions::default(),
77        ttc_field_version,
78    )
79}
80
81/// Execute message with an explicit pipeline token; pipelined operations
82/// carry tokens 1..N (impl/thin/connection.pyx `_create_messages_for_pipeline`),
83/// everything else carries 0.
84pub fn build_execute_payload_with_bind_rows_with_seq_and_token(
85    sql: &str,
86    prefetch_rows: u32,
87    seq_num: u8,
88    is_query: bool,
89    bind_rows: &[Vec<BindValue>],
90    token_num: u64,
91    ttc_field_version: u8,
92) -> Result<Vec<u8>> {
93    build_execute_payload_with_bind_rows_and_options_with_seq(
94        sql,
95        prefetch_rows,
96        seq_num,
97        is_query,
98        bind_rows,
99        ExecuteOptions {
100            token_num,
101            ..ExecuteOptions::default()
102        },
103        ttc_field_version,
104    )
105}
106
107/// Builds a close-cursors piggyback message (reference
108/// `_write_close_cursors_piggyback` + `write_cursors_to_close`); it is
109/// prepended to the next regular message in the same data packet and
110/// consumes a TTC sequence number of its own.
111pub fn build_close_cursors_piggyback(
112    cursor_ids: &[u32],
113    seq_num: u8,
114    ttc_field_version: u8,
115) -> Vec<u8> {
116    let mut writer = TtcWriter::new();
117    writer.write_piggyback_header(TNS_FUNC_CLOSE_CURSORS, seq_num, ttc_field_version);
118    writer.write_u8(1); // pointer
119    writer.write_ub4(u32::try_from(cursor_ids.len()).unwrap_or(u32::MAX));
120    for cursor_id in cursor_ids {
121        writer.write_ub4(*cursor_id);
122    }
123    writer.into_bytes()
124}
125
126pub fn build_execute_payload_with_bind_rows_and_options_with_seq(
127    sql: &str,
128    prefetch_rows: u32,
129    seq_num: u8,
130    is_query: bool,
131    bind_rows: &[Vec<BindValue>],
132    exec_options: ExecuteOptions,
133    ttc_field_version: u8,
134) -> Result<Vec<u8>> {
135    let sql_bytes = sql.as_bytes();
136    let sql_len =
137        u32::try_from(sql_bytes.len()).map_err(|_| ProtocolError::InvalidPacketLength {
138            length: sql_bytes.len(),
139            minimum: 0,
140        })?;
141    let bind_count = bind_rows.first().map_or(0, Vec::len);
142    for row in bind_rows {
143        if row.len() != bind_count {
144            return Err(ProtocolError::TtcDecode("inconsistent bind row width"));
145        }
146    }
147    let bind_count = u32::try_from(bind_count).map_err(|_| ProtocolError::InvalidPacketLength {
148        length: bind_count,
149        minimum: 0,
150    })?;
151    let bind_row_count =
152        u32::try_from(bind_rows.len()).map_err(|_| ProtocolError::InvalidPacketLength {
153            length: bind_rows.len(),
154            minimum: 0,
155        })?;
156    // Preallocate the writer so the small per-field `write_*` pushes do not grow
157    // the backing `Vec` through several doublings (each a heap allocation). The
158    // fixed message header + the inline SQL bytes dominate the no-bind/small-bind
159    // common case (e.g. `select 1 from dual` is 87 bytes total); bind columns add
160    // their own bytes and may still grow the buffer, but the hot small-statement
161    // path now builds in a single allocation. The written bytes are unchanged —
162    // this is a pure allocation optimization (see `TtcWriter::with_capacity`).
163    let writer_capacity = 96 + sql_bytes.len();
164    let mut writer = TtcWriter::with_capacity(writer_capacity);
165    writer.write_function_code_with_seq(TNS_FUNC_EXECUTE, seq_num);
166    // The ub8 pipeline token exists only when the negotiated field version is
167    // >= 23.1 ext 1 (reference messages/base.pyx `_write_function_code`); a
168    // pre-23ai server parses a stray token as message content (ORA-03120).
169    // Pipelining only happens on 23ai-negotiated connections, so a nonzero
170    // token is never dropped here.
171    if version_gates::writes_pipeline_token(ttc_field_version) {
172        writer.write_ub8(exec_options.token_num);
173    } else {
174        debug_assert_eq!(
175            exec_options.token_num, 0,
176            "pipeline tokens require a 23ai-negotiated connection"
177        );
178    }
179
180    let is_plsql = statement_is_plsql(sql);
181    let parse_only = exec_options.parse_only;
182    // a fresh parse is required when the statement has no open server cursor
183    // or is DDL (reference execute.pyx:88-89)
184    let needs_parse = exec_options.cursor_id == 0 || crate::sql::statement_is_ddl(sql);
185    // a scroll request only repositions the open cursor and fetches; the
186    // EXECUTE/BIND options are suppressed (reference execute.pyx:82-84,105)
187    let scroll_operation = exec_options.scroll_operation;
188    let mut options = 0;
189    if needs_parse {
190        options |= TNS_EXEC_OPTION_PARSE;
191    }
192    if !parse_only && !scroll_operation {
193        options |= TNS_EXEC_OPTION_EXECUTE;
194    }
195    if is_query {
196        if parse_only {
197            options |= TNS_EXEC_OPTION_DESCRIBE;
198        } else if !exec_options.no_prefetch {
199            // reference execute.pyx:99 gates FETCH on `not stmt._no_prefetch`;
200            // a no-prefetch statement (VECTOR columns) leaves the rows to be
201            // retrieved by the follow-up define-fetch instead.
202            options |= TNS_EXEC_OPTION_FETCH;
203        }
204    }
205    if bind_count > 0 && !scroll_operation {
206        options |= TNS_EXEC_OPTION_BIND;
207    }
208    if is_plsql {
209        if bind_count > 0 {
210            options |= TNS_EXEC_OPTION_PLSQL_BIND;
211        }
212    } else if !parse_only {
213        options |= TNS_EXEC_OPTION_NOT_PLSQL;
214    }
215    if exec_options.batcherrors {
216        options |= TNS_EXEC_OPTION_BATCH_ERRORS;
217    }
218    let num_iters = if is_query && !parse_only {
219        prefetch_rows
220    } else {
221        1
222    };
223    // al8i4[1]: queries report 0 on first execute and the iteration count on
224    // re-execute of an open cursor (execute.pyx:187-193)
225    let exec_count = if parse_only {
226        0
227    } else if is_query {
228        if exec_options.cursor_id == 0 {
229            0
230        } else {
231            num_iters
232        }
233    } else {
234        bind_row_count.max(1)
235    };
236    let query_flag = u32::from(is_query);
237    // reference sets the implicit-resultset flag on every full execute with
238    // SQL (execute.pyx:81-82); anonymous PL/SQL blocks need it for
239    // dbms_sql.return_result (ORA-29481 otherwise)
240    let mut exec_flags = if parse_only {
241        0
242    } else {
243        TNS_EXEC_FLAGS_IMPLICIT_RESULTSET
244    };
245    if exec_options.arraydmlrowcounts {
246        exec_flags |= TNS_EXEC_FLAGS_DML_ROWCOUNTS;
247    }
248    // scrollable cursors keep the result set open across fetches and avoid the
249    // server cancelling on end-of-fetch (reference execute.pyx:85-87)
250    if exec_options.scrollable && !parse_only {
251        exec_flags |= TNS_EXEC_FLAGS_SCROLLABLE;
252        exec_flags |= TNS_EXEC_FLAGS_NO_CANCEL_ON_EOF;
253    }
254    writer.write_ub4(options);
255    writer.write_ub4(exec_options.cursor_id);
256    if needs_parse {
257        writer.write_u8(1); // pointer (cursor id)
258        writer.write_ub4(sql_len);
259    } else {
260        writer.write_u8(0); // pointer (cursor id)
261        writer.write_ub4(0);
262    }
263    writer.write_u8(1);
264    writer.write_ub4(13);
265    writer.write_u8(0);
266    writer.write_u8(0);
267    writer.write_ub4(0);
268    writer.write_ub4(num_iters);
269    writer.write_ub4(TNS_MAX_LONG_LENGTH);
270    if bind_count == 0 {
271        writer.write_u8(0);
272        writer.write_ub4(0);
273    } else {
274        writer.write_u8(1);
275        writer.write_ub4(bind_count);
276    }
277    // CQN registration id (registerquery) split lsb/msb across the al8i4 slots
278    // (reference execute.pyx:116-119,156,163). Zero for ordinary executes.
279    let registration_id_lsb = (exec_options.registration_id & 0xffff_ffff) as u32;
280    let registration_id_msb = ((exec_options.registration_id >> 32) & 0xffff_ffff) as u32;
281    writer.write_u8(0);
282    writer.write_u8(0);
283    writer.write_u8(0);
284    writer.write_u8(0);
285    writer.write_u8(0);
286    writer.write_u8(0);
287    writer.write_ub4(0);
288    writer.write_ub4(registration_id_lsb); // registration id (lsb)
289    writer.write_u8(0); // pointer (al8objlist)
290    writer.write_u8(1); // pointer (al8objlen)
291    writer.write_u8(0); // pointer (al8blv)
292    writer.write_ub4(0); // al8blvl
293    writer.write_u8(0); // pointer (al8dnam)
294    writer.write_ub4(0); // al8dnaml
295    writer.write_ub4(registration_id_msb); // registration id (msb)
296    if exec_options.arraydmlrowcounts {
297        writer.write_u8(1); // pointer (al8pidmlrc)
298        writer.write_ub4(exec_count); // al8pidmlrcbl
299        writer.write_u8(1); // pointer (al8pidmlrcl)
300    } else {
301        writer.write_u8(0); // pointer (al8pidmlrc)
302        writer.write_ub4(0); // al8pidmlrcbl
303        writer.write_u8(0); // pointer (al8pidmlrcl)
304    }
305    // The al8sqlsig block (SQL signature + SQL ID pointers) is gated on the
306    // negotiated field version >= 12.2, and the chunk-ids block on >= 12.2 ext1
307    // (reference messages/execute.pyx:172-180). A pre-12.2 server (ttc field
308    // version 7, Oracle 12.1 — still above our accept floor of 315) never reads
309    // these fields, so writing them unconditionally shifts every following byte
310    // and corrupts the EXECUTE. Our live matrix floor is 18c (field version 11),
311    // so both branches always fired there and the miss stayed invisible.
312    if version_gates::writes_al8sqlsig(ttc_field_version) {
313        writer.write_u8(0); // pointer (al8sqlsig)
314        writer.write_ub4(0); // SQL signature length
315        writer.write_u8(0); // pointer (SQL ID)
316        writer.write_ub4(0); // allocated size of SQL ID
317        writer.write_u8(0); // pointer (length of SQL ID)
318        if version_gates::writes_execute_chunk_ids(ttc_field_version) {
319            writer.write_u8(0); // pointer (chunk ids)
320            writer.write_ub4(0); // number of chunk ids
321        }
322    }
323
324    if needs_parse {
325        writer.write_bytes_with_length(sql_bytes)?;
326        writer.write_ub4(1); // al8i4[0] parse
327    } else {
328        writer.write_ub4(0); // al8i4[0] parse
329    }
330    writer.write_ub4(exec_count);
331    writer.write_ub4(0);
332    writer.write_ub4(0);
333    writer.write_ub4(0);
334    writer.write_ub4(0);
335    writer.write_ub4(0);
336    writer.write_ub4(query_flag); // al8i4[7] is query
337    writer.write_ub4(0); // al8i4[8]
338    writer.write_ub4(exec_flags); // al8i4[9] execute flags
339    writer.write_ub4(exec_options.fetch_orientation); // al8i4[10] fetch orientation
340    writer.write_ub4(exec_options.fetch_pos); // al8i4[11] fetch pos
341    writer.write_ub4(0); // al8i4[12]
342                         // a scroll request carries no bind parameters (reference suppresses the
343                         // BIND option and never writes bind params for scroll_operation)
344    if !bind_rows.is_empty() && !scroll_operation {
345        write_bind_params(
346            &mut writer,
347            bind_rows,
348            is_plsql,
349            exec_options.max_string_size,
350            ttc_field_version,
351        )?;
352    }
353    Ok(writer.into_bytes())
354}
355
356pub(crate) fn write_bind_params(
357    writer: &mut TtcWriter,
358    bind_rows: &[Vec<BindValue>],
359    is_plsql: bool,
360    max_string_size: u32,
361    ttc_field_version: u8,
362) -> Result<()> {
363    let Some(first_row) = bind_rows.first() else {
364        return Ok(());
365    };
366    let mut bind_metadata = Vec::with_capacity(first_row.len());
367    for index in 0..first_row.len() {
368        bind_metadata.push(write_bind_metadata_for_rows(
369            writer,
370            bind_rows,
371            index,
372            ttc_field_version,
373        )?);
374    }
375    for row in bind_rows {
376        if !is_plsql && row.iter().all(BindValue::is_output_only) {
377            continue;
378        }
379        writer.write_u8(TNS_MSG_TYPE_ROW_DATA);
380        for index in bind_row_value_order(row, &bind_metadata, is_plsql, max_string_size) {
381            let value = &row[index];
382            let (_ora_type_num, csfrm, _buffer_size) = bind_metadata
383                .get(index)
384                .copied()
385                .unwrap_or((ORA_TYPE_NUM_VARCHAR, CS_FORM_IMPLICIT, 1));
386            write_bind_value(writer, value, csfrm)?;
387        }
388    }
389    Ok(())
390}
391
392pub(crate) fn bind_row_value_order(
393    row: &[BindValue],
394    bind_metadata: &[(u8, u8, u32)],
395    is_plsql: bool,
396    max_string_size: u32,
397) -> Vec<usize> {
398    let mut non_long = Vec::with_capacity(row.len());
399    let mut long = Vec::new();
400    for (index, value) in row.iter().enumerate() {
401        if !is_plsql && value.is_output_only() {
402            continue;
403        }
404        // non-LONG values are written first followed by any LONG values; a
405        // value is "long" when its buffer size exceeds the maximum string
406        // size (reference messages/base.pyx:1529-1565 keys this off
407        // `metadata.buffer_size > buf._caps.max_string_size`)
408        if !is_plsql
409            && bind_metadata
410                .get(index)
411                .is_some_and(|(ora_type_num, _, buffer_size)| {
412                    matches!(*ora_type_num, ORA_TYPE_NUM_LONG | ORA_TYPE_NUM_LONG_RAW)
413                        || *buffer_size > max_string_size
414                })
415        {
416            long.push(index);
417        } else {
418            non_long.push(index);
419        }
420    }
421    non_long.extend(long);
422    non_long
423}
424
425pub(crate) fn write_bind_metadata_for_rows(
426    writer: &mut TtcWriter,
427    bind_rows: &[Vec<BindValue>],
428    index: usize,
429    ttc_field_version: u8,
430) -> Result<(u8, u8, u32)> {
431    let Some(first_row) = bind_rows.first() else {
432        return Ok((ORA_TYPE_NUM_VARCHAR, CS_FORM_IMPLICIT, 1));
433    };
434    let Some(first_value) = first_row.get(index) else {
435        return Ok((ORA_TYPE_NUM_VARCHAR, CS_FORM_IMPLICIT, 1));
436    };
437    let mut metadata_value = first_value;
438    let (mut ora_type_num, mut csfrm, mut buffer_size) = bind_metadata(first_value);
439    let mut needs_type_inference = matches!(first_value, BindValue::Null);
440    for row in bind_rows.iter().skip(1) {
441        let Some(value) = row.get(index) else {
442            continue;
443        };
444        if needs_type_inference {
445            if matches!(value, BindValue::Null) {
446                continue;
447            }
448            metadata_value = value;
449            (ora_type_num, csfrm, buffer_size) = bind_metadata(value);
450            needs_type_inference = false;
451            continue;
452        }
453        let (row_ora_type_num, row_csfrm, row_buffer_size) = bind_metadata(value);
454        if row_csfrm == csfrm && bind_metadata_types_are_compatible(ora_type_num, row_ora_type_num)
455        {
456            ora_type_num = promoted_bind_metadata_type(ora_type_num, row_ora_type_num);
457            buffer_size = buffer_size.max(row_buffer_size);
458        }
459    }
460    write_bind_metadata_with_type(
461        writer,
462        metadata_value,
463        ora_type_num,
464        csfrm,
465        buffer_size,
466        ttc_field_version,
467    )?;
468    Ok((ora_type_num, csfrm, buffer_size))
469}
470
471pub(crate) fn bind_metadata_types_are_compatible(left: u8, right: u8) -> bool {
472    left == right
473        || (matches!(
474            left,
475            ORA_TYPE_NUM_CHAR | ORA_TYPE_NUM_VARCHAR | ORA_TYPE_NUM_LONG
476        ) && matches!(
477            right,
478            ORA_TYPE_NUM_CHAR | ORA_TYPE_NUM_VARCHAR | ORA_TYPE_NUM_LONG
479        ))
480        || (matches!(left, ORA_TYPE_NUM_RAW | ORA_TYPE_NUM_LONG_RAW)
481            && matches!(right, ORA_TYPE_NUM_RAW | ORA_TYPE_NUM_LONG_RAW))
482}
483
484pub(crate) fn promoted_bind_metadata_type(left: u8, right: u8) -> u8 {
485    if matches!(left, ORA_TYPE_NUM_LONG | ORA_TYPE_NUM_LONG_RAW) {
486        left
487    } else if matches!(right, ORA_TYPE_NUM_LONG | ORA_TYPE_NUM_LONG_RAW) {
488        right
489    } else {
490        left
491    }
492}
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497
498    fn find_subslice(haystack: &[u8], needle: &[u8], label: &str) -> usize {
499        haystack
500            .windows(needle.len())
501            .position(|window| window == needle)
502            .unwrap_or_else(|| panic!("{label} not found in execute payload"))
503    }
504
505    fn execute_payload_for_version(field_version: u8) -> Vec<u8> {
506        build_execute_payload_with_bind_rows_and_options_with_seq(
507            "select 1 from dual",
508            0,
509            7,
510            true,
511            &[],
512            ExecuteOptions::default(),
513            field_version,
514        )
515        .expect("execute payload")
516    }
517
518    // Reference messages/execute.pyx:172-180 gates the al8sqlsig block on
519    // ttc field version >= 12.2 and the chunk-ids block on >= 12.2 ext1. A
520    // pre-12.2 server (field version 7, Oracle 12.1) must not receive those
521    // bytes; writing them unconditionally shifts the SQL text and following
522    // al8i4 slots (the classic missing-version-gate corruption).
523    #[test]
524    fn execute_gates_al8sqlsig_and_chunk_ids_on_field_version() {
525        let v7 = execute_payload_for_version(TNS_CCAP_FIELD_VERSION_12_2 - 1); // 12.1
526        let v8 = execute_payload_for_version(TNS_CCAP_FIELD_VERSION_12_2); // 12.2
527        let v9 = execute_payload_for_version(TNS_CCAP_FIELD_VERSION_12_2_EXT1); // 12.2 ext1
528        let v17 = execute_payload_for_version(TNS_CCAP_FIELD_VERSION_23_1); // 23.1 (< token gate)
529
530        // al8sqlsig block = u8 + ub4(0) + u8 + ub4(0) + u8 = 5 bytes.
531        assert_eq!(
532            v8.len() - v7.len(),
533            5,
534            "the al8sqlsig block appears only at field version >= 12.2"
535        );
536        // chunk-ids block = u8 + ub4(0) = 2 bytes.
537        assert_eq!(
538            v9.len() - v8.len(),
539            2,
540            "the chunk-ids block appears only at field version >= 12.2 ext1"
541        );
542        // Nothing else on the EXECUTE write path changes between 12.2 ext1 and
543        // 23.1 (the next write gate, the ub8 pipeline token, is at 23.1 ext1),
544        // so the al8sqlsig/chunk-ids blocks are the only version-dependent bytes
545        // in this band — the fix is inert for every 12.2 ext1+ server.
546        assert_eq!(
547            v9, v17,
548            "field versions in [12.2 ext1, 23.1) produce identical EXECUTE bytes"
549        );
550    }
551
552    // Reference messages/base.pyx:1429 gates the per-column oaccolid ub4 on
553    // field version >= 12.2. The symmetric describe read (fetch.rs) already
554    // gated it; the bind write side had not.
555    #[test]
556    fn bind_column_metadata_gates_oaccolid_on_field_version() {
557        let row = vec![BindValue::Text("x".to_string())];
558        let build = |field_version: u8| {
559            build_execute_payload_with_bind_rows_and_options_with_seq(
560                "insert into t values (:1)",
561                1,
562                7,
563                false,
564                &[row.clone()],
565                ExecuteOptions::default(),
566                field_version,
567            )
568            .expect("execute payload")
569        };
570        let v7 = build(TNS_CCAP_FIELD_VERSION_12_2 - 1);
571        let v8 = build(TNS_CCAP_FIELD_VERSION_12_2);
572        // One bind column: the al8sqlsig block (5) + oaccolid (1) = 6 extra
573        // bytes at 12.2 vs 12.1 (chunk-ids still absent at exactly 12.2).
574        assert_eq!(
575            v8.len() - v7.len(),
576            6,
577            "12.2 adds the al8sqlsig block (5) and one per-column oaccolid (1)"
578        );
579    }
580
581    #[test]
582    fn register_query_execute_payload_splits_registration_id() {
583        let registration_id = 0x1122_3344_5566_7788_u64;
584        let payload = build_execute_payload_with_bind_rows_and_options_with_seq(
585            "select * from rust_register_query_t",
586            0,
587            7,
588            true,
589            &[],
590            ExecuteOptions {
591                registration_id,
592                ..ExecuteOptions::default()
593            },
594            ClientCapabilities::default().ttc_field_version,
595        )
596        .expect("execute payload");
597
598        let lsb = [0x55, 0x66, 0x77, 0x88];
599        let msb = [0x11, 0x22, 0x33, 0x44];
600        let lsb_pos = payload
601            .windows(lsb.len())
602            .position(|window| window == lsb)
603            .expect("registration id lsb is encoded");
604        let msb_pos = payload
605            .windows(msb.len())
606            .position(|window| window == msb)
607            .expect("registration id msb is encoded");
608
609        assert!(
610            lsb_pos < msb_pos,
611            "execute payload writes registration id lsb before msb"
612        );
613    }
614
615    #[test]
616    fn long_bind_split_uses_negotiated_max_string_size() {
617        let row = vec![
618            BindValue::Raw(vec![b'A'; 3_999]),
619            BindValue::Raw(vec![b'B'; 4_001]),
620            BindValue::Raw(vec![b'C'; 32_767]),
621            BindValue::Text("ZMARK".to_string()),
622        ];
623        let metadata = row.iter().map(bind_metadata).collect::<Vec<_>>();
624
625        assert_eq!(
626            bind_row_value_order(&row, &metadata, false, 4_000),
627            vec![0, 3, 1, 2],
628            "STANDARD max_string_size=4000 writes >4000-byte binds in the long section"
629        );
630        assert_eq!(
631            bind_row_value_order(&row, &metadata, false, 32_767),
632            vec![0, 1, 2, 3],
633            "32K-capable connections keep <=32767-byte binds in ordinary order"
634        );
635        assert_eq!(
636            bind_row_value_order(&row, &metadata, true, 4_000),
637            vec![0, 1, 2, 3],
638            "PL/SQL bind order is unchanged"
639        );
640
641        let standard = build_execute_payload_with_bind_rows_and_options_with_seq(
642            "insert into t values (:1, :2, :3, :4)",
643            1,
644            7,
645            false,
646            &[row.clone()],
647            ExecuteOptions::default().with_max_string_size(4_000),
648            ClientCapabilities::default().ttc_field_version,
649        )
650        .expect("STANDARD execute payload");
651        let standard_a = find_subslice(&standard, &[b'A'; 32], "STANDARD A value");
652        let standard_b = find_subslice(&standard, &[b'B'; 32], "STANDARD B value");
653        let standard_c = find_subslice(&standard, &[b'C'; 32], "STANDARD C value");
654        let standard_z = find_subslice(&standard, b"ZMARK", "STANDARD raw marker");
655        assert!(
656            standard_a < standard_z && standard_z < standard_b && standard_b < standard_c,
657            "STANDARD payload must write non-long values before >4000-byte long values"
658        );
659
660        let extended = build_execute_payload_with_bind_rows_and_options_with_seq(
661            "insert into t values (:1, :2, :3, :4)",
662            1,
663            8,
664            false,
665            &[row],
666            ExecuteOptions::default().with_max_string_size(32_767),
667            ClientCapabilities::default().ttc_field_version,
668        )
669        .expect("32K execute payload");
670        let extended_a = find_subslice(&extended, &[b'A'; 32], "32K A value");
671        let extended_b = find_subslice(&extended, &[b'B'; 32], "32K B value");
672        let extended_c = find_subslice(&extended, &[b'C'; 32], "32K C value");
673        let extended_z = find_subslice(&extended, b"ZMARK", "32K raw marker");
674        assert!(
675            extended_a < extended_b && extended_b < extended_c && extended_c < extended_z,
676            "32K payload must keep <=32767-byte values in bind order"
677        );
678    }
679}