#![forbid(unsafe_code)]
use super::*;
pub fn build_execute_query_payload(sql: &str, prefetch_rows: u32) -> Result<Vec<u8>> {
build_execute_query_payload_with_seq(sql, prefetch_rows, 1)
}
pub fn build_execute_query_payload_with_seq(
sql: &str,
prefetch_rows: u32,
seq_num: u8,
) -> Result<Vec<u8>> {
build_execute_payload_with_seq(sql, prefetch_rows, seq_num, true)
}
pub fn build_execute_payload_with_seq(
sql: &str,
prefetch_rows: u32,
seq_num: u8,
is_query: bool,
) -> Result<Vec<u8>> {
build_execute_payload_with_binds_with_seq(sql, prefetch_rows, seq_num, is_query, &[])
}
pub fn build_execute_payload_with_binds_with_seq(
sql: &str,
prefetch_rows: u32,
seq_num: u8,
is_query: bool,
binds: &[BindValue],
) -> Result<Vec<u8>> {
let bind_rows = if binds.is_empty() {
Vec::new()
} else {
vec![binds.to_vec()]
};
build_execute_payload_with_bind_rows_with_seq(sql, prefetch_rows, seq_num, is_query, &bind_rows)
}
pub fn build_execute_payload_with_bind_rows_with_seq(
sql: &str,
prefetch_rows: u32,
seq_num: u8,
is_query: bool,
bind_rows: &[Vec<BindValue>],
) -> Result<Vec<u8>> {
build_execute_payload_with_bind_rows_and_options_with_seq(
sql,
prefetch_rows,
seq_num,
is_query,
bind_rows,
ExecuteOptions::default(),
)
}
pub fn build_execute_payload_with_bind_rows_with_seq_and_token(
sql: &str,
prefetch_rows: u32,
seq_num: u8,
is_query: bool,
bind_rows: &[Vec<BindValue>],
token_num: u64,
) -> Result<Vec<u8>> {
build_execute_payload_with_bind_rows_and_options_with_seq(
sql,
prefetch_rows,
seq_num,
is_query,
bind_rows,
ExecuteOptions {
token_num,
..ExecuteOptions::default()
},
)
}
pub fn build_close_cursors_piggyback(cursor_ids: &[u32], seq_num: u8) -> Vec<u8> {
let mut writer = TtcWriter::new();
writer.write_u8(TNS_MSG_TYPE_PIGGYBACK);
writer.write_u8(TNS_FUNC_CLOSE_CURSORS);
writer.write_u8(seq_num);
writer.write_ub8(0); writer.write_u8(1); writer.write_ub4(u32::try_from(cursor_ids.len()).unwrap_or(u32::MAX));
for cursor_id in cursor_ids {
writer.write_ub4(*cursor_id);
}
writer.into_bytes()
}
pub fn build_execute_payload_with_bind_rows_and_options_with_seq(
sql: &str,
prefetch_rows: u32,
seq_num: u8,
is_query: bool,
bind_rows: &[Vec<BindValue>],
exec_options: ExecuteOptions,
) -> Result<Vec<u8>> {
let sql_bytes = sql.as_bytes();
let sql_len =
u32::try_from(sql_bytes.len()).map_err(|_| ProtocolError::InvalidPacketLength {
length: sql_bytes.len(),
minimum: 0,
})?;
let bind_count = bind_rows.first().map_or(0, Vec::len);
for row in bind_rows {
if row.len() != bind_count {
return Err(ProtocolError::TtcDecode("inconsistent bind row width"));
}
}
let bind_count = u32::try_from(bind_count).map_err(|_| ProtocolError::InvalidPacketLength {
length: bind_count,
minimum: 0,
})?;
let bind_row_count =
u32::try_from(bind_rows.len()).map_err(|_| ProtocolError::InvalidPacketLength {
length: bind_rows.len(),
minimum: 0,
})?;
let writer_capacity = 96 + sql_bytes.len();
let mut writer = TtcWriter::with_capacity(writer_capacity);
writer.write_function_code_with_seq(TNS_FUNC_EXECUTE, seq_num);
writer.write_ub8(exec_options.token_num);
let is_plsql = statement_is_plsql(sql);
let parse_only = exec_options.parse_only;
let needs_parse = exec_options.cursor_id == 0 || crate::sql::statement_is_ddl(sql);
let scroll_operation = exec_options.scroll_operation;
let mut options = 0;
if needs_parse {
options |= TNS_EXEC_OPTION_PARSE;
}
if !parse_only && !scroll_operation {
options |= TNS_EXEC_OPTION_EXECUTE;
}
if is_query {
if parse_only {
options |= TNS_EXEC_OPTION_DESCRIBE;
} else if !exec_options.no_prefetch {
options |= TNS_EXEC_OPTION_FETCH;
}
}
if bind_count > 0 && !scroll_operation {
options |= TNS_EXEC_OPTION_BIND;
}
if is_plsql {
if bind_count > 0 {
options |= TNS_EXEC_OPTION_PLSQL_BIND;
}
} else if !parse_only {
options |= TNS_EXEC_OPTION_NOT_PLSQL;
}
if exec_options.batcherrors {
options |= TNS_EXEC_OPTION_BATCH_ERRORS;
}
let num_iters = if is_query && !parse_only {
prefetch_rows
} else {
1
};
let exec_count = if parse_only {
0
} else if is_query {
if exec_options.cursor_id == 0 {
0
} else {
num_iters
}
} else {
bind_row_count.max(1)
};
let query_flag = u32::from(is_query);
let mut exec_flags = if parse_only {
0
} else {
TNS_EXEC_FLAGS_IMPLICIT_RESULTSET
};
if exec_options.arraydmlrowcounts {
exec_flags |= TNS_EXEC_FLAGS_DML_ROWCOUNTS;
}
if exec_options.scrollable && !parse_only {
exec_flags |= TNS_EXEC_FLAGS_SCROLLABLE;
exec_flags |= TNS_EXEC_FLAGS_NO_CANCEL_ON_EOF;
}
writer.write_ub4(options);
writer.write_ub4(exec_options.cursor_id);
if needs_parse {
writer.write_u8(1); writer.write_ub4(sql_len);
} else {
writer.write_u8(0); writer.write_ub4(0);
}
writer.write_u8(1);
writer.write_ub4(13);
writer.write_u8(0);
writer.write_u8(0);
writer.write_ub4(0);
writer.write_ub4(num_iters);
writer.write_ub4(TNS_MAX_LONG_LENGTH);
if bind_count == 0 {
writer.write_u8(0);
writer.write_ub4(0);
} else {
writer.write_u8(1);
writer.write_ub4(bind_count);
}
let registration_id_lsb = (exec_options.registration_id & 0xffff_ffff) as u32;
let registration_id_msb = ((exec_options.registration_id >> 32) & 0xffff_ffff) as u32;
writer.write_u8(0);
writer.write_u8(0);
writer.write_u8(0);
writer.write_u8(0);
writer.write_u8(0);
writer.write_u8(0);
writer.write_ub4(0);
writer.write_ub4(registration_id_lsb); writer.write_u8(0); writer.write_u8(1); writer.write_u8(0); writer.write_ub4(0); writer.write_u8(0); writer.write_ub4(0); writer.write_ub4(registration_id_msb); if exec_options.arraydmlrowcounts {
writer.write_u8(1); writer.write_ub4(exec_count); writer.write_u8(1); } else {
writer.write_u8(0); writer.write_ub4(0); writer.write_u8(0); }
writer.write_u8(0); writer.write_ub4(0); writer.write_u8(0); writer.write_ub4(0); writer.write_u8(0); writer.write_u8(0); writer.write_ub4(0);
if needs_parse {
writer.write_bytes_with_length(sql_bytes)?;
writer.write_ub4(1); } else {
writer.write_ub4(0); }
writer.write_ub4(exec_count);
writer.write_ub4(0);
writer.write_ub4(0);
writer.write_ub4(0);
writer.write_ub4(0);
writer.write_ub4(0);
writer.write_ub4(query_flag); writer.write_ub4(0); writer.write_ub4(exec_flags); writer.write_ub4(exec_options.fetch_orientation); writer.write_ub4(exec_options.fetch_pos); writer.write_ub4(0); if !bind_rows.is_empty() && !scroll_operation {
write_bind_params(
&mut writer,
bind_rows,
is_plsql,
exec_options.max_string_size,
)?;
}
Ok(writer.into_bytes())
}
pub(crate) fn write_bind_params(
writer: &mut TtcWriter,
bind_rows: &[Vec<BindValue>],
is_plsql: bool,
max_string_size: u32,
) -> Result<()> {
let Some(first_row) = bind_rows.first() else {
return Ok(());
};
let mut bind_metadata = Vec::with_capacity(first_row.len());
for index in 0..first_row.len() {
bind_metadata.push(write_bind_metadata_for_rows(writer, bind_rows, index)?);
}
for row in bind_rows {
if !is_plsql && row.iter().all(BindValue::is_output_only) {
continue;
}
writer.write_u8(TNS_MSG_TYPE_ROW_DATA);
for index in bind_row_value_order(row, &bind_metadata, is_plsql, max_string_size) {
let value = &row[index];
let (_ora_type_num, csfrm, _buffer_size) = bind_metadata
.get(index)
.copied()
.unwrap_or((ORA_TYPE_NUM_VARCHAR, CS_FORM_IMPLICIT, 1));
write_bind_value(writer, value, csfrm)?;
}
}
Ok(())
}
pub(crate) fn bind_row_value_order(
row: &[BindValue],
bind_metadata: &[(u8, u8, u32)],
is_plsql: bool,
max_string_size: u32,
) -> Vec<usize> {
let mut non_long = Vec::with_capacity(row.len());
let mut long = Vec::new();
for (index, value) in row.iter().enumerate() {
if !is_plsql && value.is_output_only() {
continue;
}
if !is_plsql
&& bind_metadata
.get(index)
.is_some_and(|(ora_type_num, _, buffer_size)| {
matches!(*ora_type_num, ORA_TYPE_NUM_LONG | ORA_TYPE_NUM_LONG_RAW)
|| *buffer_size > max_string_size
})
{
long.push(index);
} else {
non_long.push(index);
}
}
non_long.extend(long);
non_long
}
pub(crate) fn write_bind_metadata_for_rows(
writer: &mut TtcWriter,
bind_rows: &[Vec<BindValue>],
index: usize,
) -> Result<(u8, u8, u32)> {
let Some(first_row) = bind_rows.first() else {
return Ok((ORA_TYPE_NUM_VARCHAR, CS_FORM_IMPLICIT, 1));
};
let Some(first_value) = first_row.get(index) else {
return Ok((ORA_TYPE_NUM_VARCHAR, CS_FORM_IMPLICIT, 1));
};
let mut metadata_value = first_value;
let (mut ora_type_num, mut csfrm, mut buffer_size) = bind_metadata(first_value);
let mut needs_type_inference = matches!(first_value, BindValue::Null);
for row in bind_rows.iter().skip(1) {
let Some(value) = row.get(index) else {
continue;
};
if needs_type_inference {
if matches!(value, BindValue::Null) {
continue;
}
metadata_value = value;
(ora_type_num, csfrm, buffer_size) = bind_metadata(value);
needs_type_inference = false;
continue;
}
let (row_ora_type_num, row_csfrm, row_buffer_size) = bind_metadata(value);
if row_csfrm == csfrm && bind_metadata_types_are_compatible(ora_type_num, row_ora_type_num)
{
ora_type_num = promoted_bind_metadata_type(ora_type_num, row_ora_type_num);
buffer_size = buffer_size.max(row_buffer_size);
}
}
write_bind_metadata_with_type(writer, metadata_value, ora_type_num, csfrm, buffer_size)?;
Ok((ora_type_num, csfrm, buffer_size))
}
pub(crate) fn bind_metadata_types_are_compatible(left: u8, right: u8) -> bool {
left == right
|| (matches!(
left,
ORA_TYPE_NUM_CHAR | ORA_TYPE_NUM_VARCHAR | ORA_TYPE_NUM_LONG
) && matches!(
right,
ORA_TYPE_NUM_CHAR | ORA_TYPE_NUM_VARCHAR | ORA_TYPE_NUM_LONG
))
|| (matches!(left, ORA_TYPE_NUM_RAW | ORA_TYPE_NUM_LONG_RAW)
&& matches!(right, ORA_TYPE_NUM_RAW | ORA_TYPE_NUM_LONG_RAW))
}
pub(crate) fn promoted_bind_metadata_type(left: u8, right: u8) -> u8 {
if matches!(left, ORA_TYPE_NUM_LONG | ORA_TYPE_NUM_LONG_RAW) {
left
} else if matches!(right, ORA_TYPE_NUM_LONG | ORA_TYPE_NUM_LONG_RAW) {
right
} else {
left
}
}
#[cfg(test)]
mod tests {
use super::*;
fn find_subslice(haystack: &[u8], needle: &[u8], label: &str) -> usize {
haystack
.windows(needle.len())
.position(|window| window == needle)
.unwrap_or_else(|| panic!("{label} not found in execute payload"))
}
#[test]
fn register_query_execute_payload_splits_registration_id() {
let registration_id = 0x1122_3344_5566_7788_u64;
let payload = build_execute_payload_with_bind_rows_and_options_with_seq(
"select * from rust_register_query_t",
0,
7,
true,
&[],
ExecuteOptions {
registration_id,
..ExecuteOptions::default()
},
)
.expect("execute payload");
let lsb = [0x55, 0x66, 0x77, 0x88];
let msb = [0x11, 0x22, 0x33, 0x44];
let lsb_pos = payload
.windows(lsb.len())
.position(|window| window == lsb)
.expect("registration id lsb is encoded");
let msb_pos = payload
.windows(msb.len())
.position(|window| window == msb)
.expect("registration id msb is encoded");
assert!(
lsb_pos < msb_pos,
"execute payload writes registration id lsb before msb"
);
}
#[test]
fn long_bind_split_uses_negotiated_max_string_size() {
let row = vec![
BindValue::Raw(vec![b'A'; 3_999]),
BindValue::Raw(vec![b'B'; 4_001]),
BindValue::Raw(vec![b'C'; 32_767]),
BindValue::Text("ZMARK".to_string()),
];
let metadata = row.iter().map(bind_metadata).collect::<Vec<_>>();
assert_eq!(
bind_row_value_order(&row, &metadata, false, 4_000),
vec![0, 3, 1, 2],
"STANDARD max_string_size=4000 writes >4000-byte binds in the long section"
);
assert_eq!(
bind_row_value_order(&row, &metadata, false, 32_767),
vec![0, 1, 2, 3],
"32K-capable connections keep <=32767-byte binds in ordinary order"
);
assert_eq!(
bind_row_value_order(&row, &metadata, true, 4_000),
vec![0, 1, 2, 3],
"PL/SQL bind order is unchanged"
);
let standard = build_execute_payload_with_bind_rows_and_options_with_seq(
"insert into t values (:1, :2, :3, :4)",
1,
7,
false,
&[row.clone()],
ExecuteOptions::default().with_max_string_size(4_000),
)
.expect("STANDARD execute payload");
let standard_a = find_subslice(&standard, &[b'A'; 32], "STANDARD A value");
let standard_b = find_subslice(&standard, &[b'B'; 32], "STANDARD B value");
let standard_c = find_subslice(&standard, &[b'C'; 32], "STANDARD C value");
let standard_z = find_subslice(&standard, b"ZMARK", "STANDARD raw marker");
assert!(
standard_a < standard_z && standard_z < standard_b && standard_b < standard_c,
"STANDARD payload must write non-long values before >4000-byte long values"
);
let extended = build_execute_payload_with_bind_rows_and_options_with_seq(
"insert into t values (:1, :2, :3, :4)",
1,
8,
false,
&[row],
ExecuteOptions::default().with_max_string_size(32_767),
)
.expect("32K execute payload");
let extended_a = find_subslice(&extended, &[b'A'; 32], "32K A value");
let extended_b = find_subslice(&extended, &[b'B'; 32], "32K B value");
let extended_c = find_subslice(&extended, &[b'C'; 32], "32K C value");
let extended_z = find_subslice(&extended, b"ZMARK", "32K raw marker");
assert!(
extended_a < extended_b && extended_b < extended_c && extended_c < extended_z,
"32K payload must keep <=32767-byte values in bind order"
);
}
}