use std::borrow::Cow;
use oracledb_protocol::thin::{
BindValue, QueryValue, CS_FORM_IMPLICIT, ORA_TYPE_NUM_NUMBER, ORA_TYPE_NUM_VARCHAR,
};
use asupersync::Cx;
use crate::{block_on_io, BlockingConnection, Connection, ExecuteOutcome, FromSql, Params, Result};
const NUMBER_BUFFER_SIZE: u32 = 22;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum OutType {
Varchar { buffer_size: u32 },
Number,
}
impl OutType {
fn placeholder(self, is_return: bool) -> BindValue {
let (ora_type_num, csfrm, buffer_size) = match self {
OutType::Varchar { buffer_size } => {
(ORA_TYPE_NUM_VARCHAR, CS_FORM_IMPLICIT, buffer_size)
}
OutType::Number => (ORA_TYPE_NUM_NUMBER, 0, NUMBER_BUFFER_SIZE),
};
if is_return {
BindValue::ReturnOutput {
ora_type_num,
csfrm,
buffer_size,
}
} else {
BindValue::Output {
ora_type_num,
csfrm,
buffer_size,
}
}
}
}
#[derive(Clone, Debug)]
enum RoutineArg {
In(BindValue),
Out(OutType),
}
#[derive(Clone, Debug)]
pub struct RoutineCall {
name: String,
return_type: Option<OutType>,
args: Vec<RoutineArg>,
}
impl RoutineCall {
pub fn procedure(name: impl Into<String>) -> Self {
Self {
name: name.into(),
return_type: None,
args: Vec::new(),
}
}
pub fn function(name: impl Into<String>, returns: OutType) -> Self {
Self {
name: name.into(),
return_type: Some(returns),
args: Vec::new(),
}
}
#[must_use]
pub fn arg_in(mut self, value: impl crate::ToSql) -> Self {
self.args.push(RoutineArg::In(value.to_sql()));
self
}
#[must_use]
pub fn arg_out(mut self, out: OutType) -> Self {
self.args.push(RoutineArg::Out(out));
self
}
fn is_function(&self) -> bool {
self.return_type.is_some()
}
fn build(&self) -> (String, Vec<BindValue>) {
let mut binds = Vec::with_capacity(self.args.len() + usize::from(self.is_function()));
let mut next: u32 = 1;
let return_placeholder = self.return_type.map(|ret| {
binds.push(ret.placeholder(true));
let placeholder = next;
next += 1;
placeholder
});
let mut arg_placeholders = Vec::with_capacity(self.args.len());
for arg in &self.args {
match arg {
RoutineArg::In(value) => binds.push(value.clone()),
RoutineArg::Out(out) => binds.push(out.placeholder(false)),
}
arg_placeholders.push(next);
next += 1;
}
let call_expr = if arg_placeholders.is_empty() {
self.name.clone()
} else {
let list = arg_placeholders
.iter()
.map(|placeholder| format!(":{placeholder}"))
.collect::<Vec<_>>()
.join(", ");
format!("{}({list})", self.name)
};
let block = match return_placeholder {
Some(placeholder) => format!("BEGIN :{placeholder} := {call_expr}; END;"),
None => format!("BEGIN {call_expr}; END;"),
};
(block, binds)
}
}
#[derive(Clone, Debug)]
pub struct RoutineOutcome {
outputs: Vec<Option<QueryValue>>,
has_return: bool,
}
impl RoutineOutcome {
fn from_outputs(outputs: Vec<Option<QueryValue>>, has_return: bool) -> Self {
Self {
outputs,
has_return,
}
}
fn from_execute(outcome: &ExecuteOutcome, has_return: bool) -> Self {
let outputs = outcome
.out_binds()
.values()
.iter()
.map(|(_, value)| value.clone())
.collect();
Self::from_outputs(outputs, has_return)
}
pub fn returned(&self) -> Option<&QueryValue> {
if self.has_return {
self.outputs.first().and_then(Option::as_ref)
} else {
None
}
}
pub fn out(&self, index: usize) -> Option<&QueryValue> {
let offset = usize::from(self.has_return);
self.outputs.get(index + offset).and_then(Option::as_ref)
}
pub fn returned_as<T: FromSql>(&self) -> Result<Option<T>> {
self.returned()
.map(|value| T::from_sql(value).map_err(crate::Error::from))
.transpose()
}
pub fn out_as<T: FromSql>(&self, index: usize) -> Result<Option<T>> {
self.out(index)
.map(|value| T::from_sql(value).map_err(crate::Error::from))
.transpose()
}
}
impl Connection {
pub async fn call_routine(&mut self, cx: &Cx, call: RoutineCall) -> Result<RoutineOutcome> {
let has_return = call.is_function();
let (block, binds) = call.build();
let outcome = self
.execute(cx, &block, Params::Positional(Cow::Owned(binds)))
.await?;
Ok(RoutineOutcome::from_execute(&outcome, has_return))
}
}
impl BlockingConnection {
pub fn call_routine(connection: &mut Connection, call: RoutineCall) -> Result<RoutineOutcome> {
block_on_io(|cx| async move { connection.call_routine(&cx, call).await })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn procedure_block_positional_in_args() {
let (block, binds) = RoutineCall::procedure("pkg.do_thing")
.arg_in(1i64)
.arg_in("x")
.build();
assert_eq!(block, "BEGIN pkg.do_thing(:1, :2); END;");
assert_eq!(binds.len(), 2);
assert!(matches!(binds[0], BindValue::Number(_)));
assert!(matches!(binds[1], BindValue::Text(_)));
}
#[test]
fn procedure_no_args_omits_parens() {
let (block, binds) = RoutineCall::procedure("housekeeping").build();
assert_eq!(block, "BEGIN housekeeping; END;");
assert!(binds.is_empty());
}
#[test]
fn procedure_with_out_registers_output_placeholder() {
let (block, binds) = RoutineCall::procedure("get_count")
.arg_out(OutType::Number)
.build();
assert_eq!(block, "BEGIN get_count(:1); END;");
assert_eq!(binds.len(), 1);
assert!(matches!(
binds[0],
BindValue::Output {
ora_type_num: ORA_TYPE_NUM_NUMBER,
..
}
));
}
#[test]
fn function_block_return_takes_first_placeholder() {
let (block, binds) = RoutineCall::function("pkg.add", OutType::Number)
.arg_in(2i64)
.arg_in(3i64)
.build();
assert_eq!(block, "BEGIN :1 := pkg.add(:2, :3); END;");
assert_eq!(binds.len(), 3);
assert!(matches!(
binds[0],
BindValue::ReturnOutput {
ora_type_num: ORA_TYPE_NUM_NUMBER,
..
}
));
assert!(matches!(binds[1], BindValue::Number(_)));
assert!(matches!(binds[2], BindValue::Number(_)));
}
#[test]
fn function_no_args_returns_bare_call() {
let (block, binds) =
RoutineCall::function("current_ts", OutType::Varchar { buffer_size: 64 }).build();
assert_eq!(block, "BEGIN :1 := current_ts; END;");
assert_eq!(binds.len(), 1);
assert!(matches!(
binds[0],
BindValue::ReturnOutput {
ora_type_num: ORA_TYPE_NUM_VARCHAR,
buffer_size: 64,
..
}
));
}
#[test]
fn out_varchar_uses_implicit_charset_form() {
assert!(matches!(
OutType::Varchar { buffer_size: 200 }.placeholder(false),
BindValue::Output {
ora_type_num: ORA_TYPE_NUM_VARCHAR,
csfrm: CS_FORM_IMPLICIT,
buffer_size: 200,
}
));
}
#[test]
fn outcome_maps_return_then_out_args_by_declaration_order() {
let outputs = vec![
Some(QueryValue::Text("ret".into())),
Some(QueryValue::Text("a".into())),
Some(QueryValue::Text("b".into())),
];
let outcome = RoutineOutcome::from_outputs(outputs, true);
assert_eq!(outcome.returned(), Some(&QueryValue::Text("ret".into())));
assert_eq!(outcome.out(0), Some(&QueryValue::Text("a".into())));
assert_eq!(outcome.out(1), Some(&QueryValue::Text("b".into())));
assert_eq!(outcome.out(2), None);
}
#[test]
fn procedure_outcome_has_no_return_and_out_starts_at_zero() {
let outputs = vec![
Some(QueryValue::Text("first".into())),
Some(QueryValue::Text("second".into())),
];
let outcome = RoutineOutcome::from_outputs(outputs, false);
assert_eq!(outcome.returned(), None);
assert_eq!(outcome.out(0), Some(&QueryValue::Text("first".into())));
assert_eq!(outcome.out(1), Some(&QueryValue::Text("second".into())));
}
}