use super::build::{self, Oracle, OracleCompat, Sql};
use super::decode::{Arg, Cell, Col, decode_row};
use super::{OnError, PoolLimits};
use crate::action::{Error, Result};
use crate::model::Entity;
use crate::value::Value;
use oracle::pool::{GetMode, Pool, PoolBuilder};
use oracle::sql_type::{OracleType, ToSql};
use oracle::{Connection, Row};
use std::time::Duration;
const PREFETCH: u32 = 128;
#[derive(Clone)]
pub(crate) struct OraPool {
pool: Pool,
statement_timeout: Option<Duration>,
}
#[derive(Debug, PartialEq)]
struct Target {
user: String,
password: String,
connect: String,
compat: Option<OracleCompat>,
}
fn percent_decode(s: &str) -> String {
let bytes = s.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
let hex = |b: u8| (b as char).to_digit(16);
match (bytes[i], bytes.get(i + 1).copied().and_then(hex), bytes.get(i + 2).copied().and_then(hex)) {
(b'%', Some(h), Some(l)) => {
out.push((h * 16 + l) as u8);
i += 3;
}
(b, ..) => {
out.push(b);
i += 1;
}
}
}
String::from_utf8_lossy(&out).into_owned()
}
fn parse_url(url: &str) -> Result<Target> {
let bad =
|| Error::Internal("database.url is not a valid Oracle URL (oracle://user:password@host:1521/service)".into());
let rest = url.strip_prefix("oracle://").ok_or_else(bad)?;
let (creds, rest) = rest.rsplit_once('@').ok_or_else(bad)?;
let (user, password) = creds.split_once(':').ok_or_else(bad)?;
let (connect, query) = rest.split_once('?').unwrap_or((rest, ""));
let mut compat = None;
for pair in query.split('&').filter(|p| !p.is_empty()) {
compat = Some(match pair {
"compat=11" => OracleCompat::V11,
"compat=12" => OracleCompat::V12,
_ => {
return Err(Error::Internal(format!(
"unknown Oracle URL option `{pair}`; expected compat=11 or compat=12"
)));
}
});
}
if user.is_empty() || connect.is_empty() {
return Err(bad());
}
Ok(Target { user: percent_decode(user), password: percent_decode(password), connect: connect.to_string(), compat })
}
async fn blocking<T: Send + 'static>(f: impl FnOnce() -> Result<T> + Send + 'static) -> Result<T> {
tokio::task::spawn_blocking(f).await.map_err(|e| Error::Internal(format!("database task: {e}")))?
}
fn internal(e: oracle::Error) -> Error {
Error::Internal(e.to_string())
}
fn error(e: oracle::Error, on: OnError) -> Error {
if let Some(db) = e.db_error()
&& db.code() == 1
{
let name = db
.message()
.split_once('(')
.and_then(|(_, r)| r.split_once(')'))
.map(|(n, _)| n.rsplit('.').next().unwrap_or(n));
return on(Some(name.unwrap_or_default()), e.to_string());
}
on(None, e.to_string())
}
pub(crate) async fn connect(url: &str, limits: &PoolLimits) -> Result<(OraPool, Oracle)> {
let target = parse_url(url)?;
let limits = limits.clone();
blocking(move || {
if let Ok(dir) = std::env::var("ZEYN_ORACLE_CLIENT_DIR")
&& !dir.is_empty()
{
oracle::InitParams::new().oracle_client_lib_dir(dir).and_then(|p| p.init()).map_err(internal)?;
}
let mut builder = PoolBuilder::new(&target.user, &target.password, &target.connect);
builder
.max_connections(limits.max_connections)
.min_connections(limits.min_connections)
.get_mode(GetMode::TimedWait(limits.acquire_timeout))
.stmt_cache_size(64);
builder.timeout(limits.idle_timeout).map_err(internal)?;
let pool = builder.build().map_err(|e| Error::Internal(format!("database connection: {e}")))?;
let compat = match target.compat {
Some(c) => c,
None => {
let conn = pool.get().map_err(internal)?;
let (version, _) = conn.server_version().map_err(internal)?;
if version.major() < 12 { OracleCompat::V11 } else { OracleCompat::V12 }
}
};
Ok((OraPool { pool, statement_timeout: limits.statement_timeout }, Oracle { compat }))
})
.await
}
pub(crate) struct OraLink {
conn: Option<Connection>,
tx: bool,
compat: OracleCompat,
}
impl OraLink {
pub(crate) async fn acquire(pool: &OraPool, compat: OracleCompat, tx: bool) -> Result<OraLink> {
let pool = pool.clone();
let conn = blocking(move || {
let mut conn = pool.pool.get().map_err(|e| Error::Internal(format!("database connection: {e}")))?;
conn.set_autocommit(!tx);
conn.set_call_timeout(pool.statement_timeout).map_err(internal)?;
Ok(conn)
})
.await?;
Ok(OraLink { conn: Some(conn), tx, compat })
}
pub(crate) fn compat(&self) -> OracleCompat {
self.compat
}
async fn with<T: Send + 'static>(
&mut self,
f: impl FnOnce(&Connection) -> Result<T> + Send + 'static,
) -> Result<T> {
let conn = self.conn.take().ok_or_else(|| Error::Internal("the database connection was lost".into()))?;
let (conn, result) = tokio::task::spawn_blocking(move || {
let result = f(&conn);
(conn, result)
})
.await
.map_err(|e| Error::Internal(format!("database task: {e}")))?;
self.conn = Some(conn);
result
}
pub(crate) async fn fetch(
&mut self,
sql: Sql,
on: OnError,
each: &mut (dyn FnMut(&mut Vec<Cell>) -> Result<()> + Send),
) -> Result<()> {
let rows = self.with(move |c| query_rows(c, sql, on)).await?;
for mut cells in rows {
each(&mut cells)?;
}
Ok(())
}
pub(crate) async fn execute(&mut self, sql: Sql, on: OnError) -> Result<u64> {
self.with(move |c| execute(c, sql, on)).await
}
pub(crate) async fn insert<E: Entity>(&mut self, d: &Oracle, row: &E, on: OnError) -> Result<Value> {
let (ki, kf) = E::fields()
.iter()
.enumerate()
.find(|(_, f)| f.key)
.ok_or_else(|| Error::Internal("entity has no key".into()))?;
let key = row.value_at(ki).unwrap_or(Value::Null);
if !build::is_new_key(&key) {
self.execute(build::plan_insert(d, row, None, false)?, on).await?;
return Ok(key);
}
match self.compat {
OracleCompat::V11 => {
let seq = build::ident(d, &Oracle::sequence(E::TABLE))?;
let next = Sql {
text: format!("SELECT {seq}.NEXTVAL AS c0 FROM DUAL"),
args: Vec::new(),
cols: vec![Col::Int],
};
let mut id = None;
self.fetch(next, on, &mut |cells| {
if let Some(Cell::Int(n)) = cells.drain(..).next() {
id = Some(n);
}
Ok(())
})
.await?;
let id = id.ok_or_else(|| Error::Internal("the key sequence returned nothing".into()))?;
self.execute(build::plan_insert(d, row, Some(Arg::Int(id)), false)?, on).await?;
Ok(Value::Int(id))
}
OracleCompat::V12 => {
let mut sql = build::plan_insert(d, row, None, false)?;
sql.push(" RETURNING ");
sql.push(&build::ident(d, kf.name)?);
sql.push(" INTO :");
let out = sql.args.len() + 1;
sql.push(&out.to_string());
let id = self
.with(move |c| {
let Sql { text, args, .. } = sql;
let mut values = params(args);
values.push(Box::new(OracleType::Int64));
let refs: Vec<&dyn ToSql> = values.iter().map(|v| &**v).collect();
let stmt = c.execute(&text, &refs).map_err(|e| error(e, on))?;
let ids: Vec<i64> = stmt.returned_values(out).map_err(internal)?;
ids.first().copied().ok_or_else(|| Error::Internal("insert returned no key".into()))
})
.await?;
Ok(Value::Int(id))
}
}
}
pub(crate) async fn commit(mut self) -> Result<()> {
self.tx = false;
self.with(|c| c.commit().map_err(|e| Error::Internal(format!("commit: {e}")))).await
}
pub(crate) async fn rollback(mut self) -> Result<()> {
self.tx = false;
self.with(|c| c.rollback().map_err(|e| Error::Internal(format!("rollback: {e}")))).await
}
}
impl Drop for OraLink {
fn drop(&mut self) {
if !self.tx {
return;
}
if let Some(conn) = self.conn.take() {
let rollback = move || {
let _ = conn.rollback();
};
match tokio::runtime::Handle::try_current() {
Ok(rt) => {
rt.spawn_blocking(rollback);
}
Err(_) => rollback(),
}
}
}
}
fn params(args: Vec<Arg>) -> Vec<Box<dyn ToSql>> {
args.into_iter()
.map(|a| -> Box<dyn ToSql> {
match a {
Arg::Int(n) => Box::new(n),
Arg::Text(s) => Box::new(s),
Arg::Bool(b) => Box::new(i64::from(b)),
Arg::Null(Col::Int | Col::Bool) => Box::new(None::<i64>),
Arg::Null(Col::Float) => Box::new(None::<f64>),
Arg::Null(Col::Text) => Box::new(None::<String>),
}
})
.collect()
}
fn read(row: &Row, cols: &[Col], out: &mut Vec<Cell>) -> Result<()> {
for (i, col) in cols.iter().enumerate() {
let cell = match col {
Col::Int => row.get::<usize, Option<i64>>(i).map(|v| v.map_or(Cell::Null, Cell::Int)),
Col::Float => row.get::<usize, Option<f64>>(i).map(|v| v.map_or(Cell::Null, Cell::Float)),
Col::Text => row.get::<usize, Option<String>>(i).map(|v| v.map_or(Cell::Null, Cell::Text)),
Col::Bool => row.get::<usize, Option<i64>>(i).map(|v| v.map_or(Cell::Null, |n| Cell::Bool(n != 0))),
};
out.push(cell.map_err(|e| Error::Internal(format!("reading column {i}: {e}")))?);
}
Ok(())
}
fn query_rows(conn: &Connection, sql: Sql, on: OnError) -> Result<Vec<Vec<Cell>>> {
let Sql { text, args, cols } = sql;
let values = params(args);
let refs: Vec<&dyn ToSql> = values.iter().map(|v| &**v).collect();
let mut stmt = conn.statement(&text).prefetch_rows(PREFETCH).build().map_err(|e| error(e, on))?;
let rows = stmt.query(&refs).map_err(|e| error(e, on))?;
let mut out = Vec::new();
for row in rows {
let row = row.map_err(|e| error(e, on))?;
let mut cells = Vec::with_capacity(cols.len());
read(&row, &cols, &mut cells)?;
out.push(cells);
}
Ok(out)
}
fn execute(conn: &Connection, sql: Sql, on: OnError) -> Result<u64> {
let Sql { text, args, .. } = sql;
let values = params(args);
let refs: Vec<&dyn ToSql> = values.iter().map(|v| &**v).collect();
let stmt = conn.execute(&text, &refs).map_err(|e| error(e, on))?;
stmt.row_count().map_err(internal)
}
pub(crate) fn stream<E: Entity>(pool: OraPool, sql: Sql, tx: tokio::sync::mpsc::Sender<Result<E>>) {
tokio::task::spawn_blocking(move || {
let run = || -> Result<()> {
let mut conn = pool.pool.get().map_err(|e| Error::Internal(format!("database connection: {e}")))?;
conn.set_autocommit(true);
let Sql { text, args, cols } = sql;
let values = params(args);
let refs: Vec<&dyn ToSql> = values.iter().map(|v| &**v).collect();
let mut stmt =
conn.statement(&text).prefetch_rows(PREFETCH).fetch_array_size(PREFETCH).build().map_err(internal)?;
let rows = stmt.query(&refs).map_err(internal)?;
let mut cells = Vec::with_capacity(cols.len());
for row in rows {
cells.clear();
let item = row
.map_err(internal)
.and_then(|row| read(&row, &cols, &mut cells))
.and_then(|_| decode_row::<E>(&mut cells));
let failed = item.is_err();
if tx.blocking_send(item).is_err() || failed {
return Ok(());
}
}
Ok(())
};
if let Err(e) = run() {
let _ = tx.blocking_send(Err(e));
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn urls_are_read_with_encoded_passwords_and_compat() {
let t = parse_url("oracle://app:p%40ss%3Aw@db.local:1521/XEPDB1?compat=11").unwrap();
assert_eq!(
t,
Target {
user: "app".into(),
password: "p@ss:w".into(),
connect: "db.local:1521/XEPDB1".into(),
compat: Some(OracleCompat::V11)
}
);
assert_eq!(parse_url("oracle://a:b@h/XE").unwrap().compat, None);
assert!(parse_url("oracle://h/XE").is_err());
assert!(parse_url("oracle://a:b@h/XE?compat=9").is_err());
assert!(parse_url("postgres://a:b@h/x").is_err());
}
}