use crate::{Dialect, RelError, RelationalBackend, RelationalTx, Row, Value};
use std::future::Future;
use unisql_embed::{Binding, Query, Target};
pub type Sql = Query;
fn target_of(dialect: &Dialect) -> Option<Target> {
match dialect {
Dialect::Postgres => Some(Target::Postgres),
Dialect::Sqlite => Some(Target::Sqlite),
Dialect::MySql => Some(Target::Mysql),
Dialect::MariaDb => Some(Target::Mariadb),
Dialect::Other(_) => None,
}
}
fn prepare(
sql: &Sql,
dialect: &Dialect,
params: &[Value],
) -> Result<(&'static str, Vec<Value>), RelError> {
let rendered = target_of(dialect)
.and_then(|t| sql.for_target(t))
.ok_or_else(|| {
RelError::Unsupported(format!(
"the statement was not translated for {dialect}: name it in the macro's `targets`"
))
})?;
if params.len() != sql.params as usize {
return Err(RelError::Backend(format!(
"the statement takes {} parameters, got {}",
sql.params,
params.len()
)));
}
if let Some(n) = rendered
.param_bindings
.iter()
.position(|b| *b != Binding::Scalar)
{
return Err(RelError::Unsupported(format!(
"parameter ${} is an array on {dialect}, which `Value` does not carry yet",
n + 1
)));
}
let ordered = rendered
.param_order
.iter()
.map(|n| params[(*n - 1) as usize].clone())
.collect();
Ok((rendered.sql, ordered))
}
pub trait RelationalSqlExt: RelationalBackend {
fn execute_sql<'a>(
&'a self,
sql: &Sql,
params: &[Value],
) -> impl Future<Output = Result<u64, RelError>> + Send + 'a {
let prepared = prepare(sql, &self.dialect(), params);
async move {
let (text, ordered) = prepared?;
self.execute(text, &ordered).await
}
}
fn query_sql<'a>(
&'a self,
sql: &Sql,
params: &[Value],
) -> impl Future<Output = Result<Vec<Row>, RelError>> + Send + 'a {
let prepared = prepare(sql, &self.dialect(), params);
async move {
let (text, ordered) = prepared?;
self.query(text, &ordered).await
}
}
}
impl<T: RelationalBackend + ?Sized> RelationalSqlExt for T {}
pub trait RelationalTxSqlExt: RelationalTx {
fn execute_sql<'a>(
&'a mut self,
dialect: &Dialect,
sql: &Sql,
params: &[Value],
) -> impl Future<Output = Result<u64, RelError>> + Send + 'a {
let prepared = prepare(sql, dialect, params);
async move {
let (text, ordered) = prepared?;
self.execute(text, &ordered).await
}
}
fn query_sql<'a>(
&'a mut self,
dialect: &Dialect,
sql: &Sql,
params: &[Value],
) -> impl Future<Output = Result<Vec<Row>, RelError>> + Send + 'a {
let prepared = prepare(sql, dialect, params);
async move {
let (text, ordered) = prepared?;
self.query(text, &ordered).await
}
}
}
impl<T: RelationalTx + ?Sized> RelationalTxSqlExt for T {}
#[cfg(test)]
mod tests {
use super::*;
use crate::Isolation;
use std::sync::{Arc, Mutex};
use unisql_embed::Rendered;
const Q: Sql = Query {
params: 2,
constructs: &[],
rendered: &[
Rendered {
target: Target::Sqlite,
sql: "SELECT ?2, ?1, ?1",
param_order: &[2, 1, 1],
param_bindings: &[Binding::Scalar, Binding::Scalar],
},
Rendered {
target: Target::Mysql,
sql: "SELECT ?, ?, ?",
param_order: &[2, 1, 1],
param_bindings: &[Binding::Scalar, Binding::Scalar],
},
Rendered {
target: Target::Postgres,
sql: "SELECT $2, $1, $1",
param_order: &[1, 2],
param_bindings: &[Binding::Scalar, Binding::NativeArray],
},
],
};
type Call = (String, Vec<Value>);
#[derive(Clone, Default)]
struct Seen(Arc<Mutex<Vec<Call>>>);
struct Fake {
dialect: Dialect,
seen: Seen,
}
struct FakeTx(Seen);
impl RelationalBackend for Fake {
type Tx<'a> = FakeTx;
fn backend(&self) -> &str {
"fake"
}
fn version(&self) -> &str {
"0"
}
fn dialect(&self) -> Dialect {
self.dialect.clone()
}
async fn execute(&self, sql: &str, params: &[Value]) -> Result<u64, RelError> {
self.seen
.0
.lock()
.unwrap()
.push((sql.into(), params.to_vec()));
Ok(1)
}
async fn query(&self, sql: &str, params: &[Value]) -> Result<Vec<Row>, RelError> {
self.seen
.0
.lock()
.unwrap()
.push((sql.into(), params.to_vec()));
Ok(Vec::new())
}
async fn begin(&self, _: Isolation) -> Result<FakeTx, RelError> {
Ok(FakeTx(self.seen.clone()))
}
}
impl RelationalTx for FakeTx {
async fn execute(&mut self, sql: &str, params: &[Value]) -> Result<u64, RelError> {
self.0.0.lock().unwrap().push((sql.into(), params.to_vec()));
Ok(1)
}
async fn query(&mut self, sql: &str, params: &[Value]) -> Result<Vec<Row>, RelError> {
self.0.0.lock().unwrap().push((sql.into(), params.to_vec()));
Ok(Vec::new())
}
async fn commit(self) -> Result<(), RelError> {
Ok(())
}
async fn rollback(self) -> Result<(), RelError> {
Ok(())
}
}
fn fake(dialect: Dialect) -> Fake {
Fake {
dialect,
seen: Seen::default(),
}
}
fn block<T>(f: impl Future<Output = T>) -> T {
let mut f = std::pin::pin!(f);
let waker = std::task::Waker::noop();
match f.as_mut().poll(&mut std::task::Context::from_waker(waker)) {
std::task::Poll::Ready(v) => v,
std::task::Poll::Pending => panic!("the fake backend waited"),
}
}
#[test]
fn the_backends_own_text_and_parameter_order_are_used() {
let a = Value::Int(10);
let b = Value::Text("x".into());
let db = fake(Dialect::Sqlite);
block(db.execute_sql(&Q, &[a.clone(), b.clone()])).unwrap();
let db2 = fake(Dialect::MySql);
block(db2.query_sql(&Q, &[a.clone(), b.clone()])).unwrap();
let want = vec![b.clone(), a.clone(), a.clone()];
assert_eq!(
db.seen.0.lock().unwrap()[0],
("SELECT ?2, ?1, ?1".to_string(), want.clone())
);
assert_eq!(
db2.seen.0.lock().unwrap()[0],
("SELECT ?, ?, ?".to_string(), want)
);
}
#[test]
fn a_dialect_the_call_did_not_name_is_unsupported_not_guessed() {
let q: Sql = Query {
params: 0,
constructs: &[],
rendered: &[Rendered {
target: Target::Sqlite,
sql: "SELECT 1",
param_order: &[],
param_bindings: &[],
}],
};
assert!(block(fake(Dialect::Sqlite).execute_sql(&q, &[])).is_ok());
for d in [
Dialect::Postgres,
Dialect::MariaDb,
Dialect::Other("x".into()),
] {
match block(fake(d).execute_sql(&q, &[])) {
Err(RelError::Unsupported(m)) => assert!(m.contains("targets"), "{m}"),
other => panic!("expected Unsupported, got {other:?}"),
}
}
}
#[test]
fn the_wrong_number_of_parameters_is_an_error() {
let db = fake(Dialect::Sqlite);
let one = [Value::Int(1)];
assert!(matches!(
block(db.execute_sql(&Q, &one)),
Err(RelError::Backend(m)) if m.contains("takes 2 parameters, got 1")
));
assert!(db.seen.0.lock().unwrap().is_empty(), "nothing ran");
assert!(block(db.execute_sql(&Q, &[Value::Int(1), Value::Int(2)])).is_ok());
}
#[test]
fn an_array_parameter_is_refused_until_value_carries_one() {
let db = fake(Dialect::Postgres);
match block(db.query_sql(&Q, &[Value::Int(1), Value::Int(2)])) {
Err(RelError::Unsupported(m)) => assert!(m.contains("$2"), "{m}"),
other => panic!("expected Unsupported, got {other:?}"),
}
assert!(db.seen.0.lock().unwrap().is_empty());
}
#[test]
fn a_transaction_takes_the_dialect_from_the_caller() {
let db = fake(Dialect::MySql);
let mut tx = block(db.begin(Isolation::ReadCommitted)).unwrap();
block(tx.execute_sql(&db.dialect(), &Q, &[Value::Int(1), Value::Int(2)])).unwrap();
assert_eq!(db.seen.0.lock().unwrap()[0].0, "SELECT ?, ?, ?");
assert!(matches!(
block(tx.query_sql(
&Dialect::Other("x".into()),
&Q,
&[Value::Int(1), Value::Int(2)]
)),
Err(RelError::Unsupported(_))
));
}
}