use crate::context::RetryPredicate;
use crate::error::{Error, Result};
use serde_json::Value;
use std::future::Future;
use std::pin::Pin;
use std::time::Duration;
#[derive(Clone, Debug, PartialEq)]
pub enum Param {
Null,
Int(i64),
Float(f64),
Text(String),
Bool(bool),
Bytes(Vec<u8>),
}
impl From<i64> for Param {
fn from(v: i64) -> Self {
Param::Int(v)
}
}
impl From<i32> for Param {
fn from(v: i32) -> Self {
Param::Int(v as i64)
}
}
impl From<f64> for Param {
fn from(v: f64) -> Self {
Param::Float(v)
}
}
impl From<bool> for Param {
fn from(v: bool) -> Self {
Param::Bool(v)
}
}
impl From<&str> for Param {
fn from(v: &str) -> Self {
Param::Text(v.to_string())
}
}
impl From<String> for Param {
fn from(v: String) -> Self {
Param::Text(v)
}
}
impl From<Vec<u8>> for Param {
fn from(v: Vec<u8>) -> Self {
Param::Bytes(v)
}
}
impl<T: Into<Param>> From<Option<T>> for Param {
fn from(v: Option<T>) -> Self {
match v {
Some(x) => x.into(),
None => Param::Null,
}
}
}
#[macro_export]
macro_rules! params {
() => { ::std::vec::Vec::<$crate::Param>::new() };
($($x:expr),+ $(,)?) => { ::std::vec![$($crate::Param::from($x)),+] };
}
pub struct Tx<'c> {
inner: TxInner<'c>,
}
enum TxInner<'c> {
#[cfg(feature = "postgres")]
Postgres(&'c mut sqlx::PgConnection),
#[cfg(feature = "sqlite")]
Sqlite(&'c mut sqlx::SqliteConnection),
}
impl<'c> Tx<'c> {
#[cfg(feature = "postgres")]
pub(crate) fn postgres(conn: &'c mut sqlx::PgConnection) -> Self {
Tx {
inner: TxInner::Postgres(conn),
}
}
#[cfg(feature = "sqlite")]
pub(crate) fn sqlite(conn: &'c mut sqlx::SqliteConnection) -> Self {
Tx {
inner: TxInner::Sqlite(conn),
}
}
pub async fn execute(&mut self, sql: &str, params: &[Param]) -> Result<u64> {
match &mut self.inner {
#[cfg(feature = "postgres")]
TxInner::Postgres(conn) => {
let sql = to_pg_placeholders(sql);
let q = bind_pg(sqlx::query(&sql), params);
Ok(q.execute(&mut **conn).await?.rows_affected())
}
#[cfg(feature = "sqlite")]
TxInner::Sqlite(conn) => {
let q = bind_sqlite(sqlx::query(sql), params);
Ok(q.execute(&mut **conn).await?.rows_affected())
}
}
}
pub async fn query_all(&mut self, sql: &str, params: &[Param]) -> Result<Vec<Row>> {
match &mut self.inner {
#[cfg(feature = "postgres")]
TxInner::Postgres(conn) => {
let sql = to_pg_placeholders(sql);
let q = bind_pg(sqlx::query(&sql), params);
let rows = q.fetch_all(&mut **conn).await?;
Ok(rows
.into_iter()
.map(|r| Row {
inner: RowInner::Postgres(r),
})
.collect())
}
#[cfg(feature = "sqlite")]
TxInner::Sqlite(conn) => {
let q = bind_sqlite(sqlx::query(sql), params);
let rows = q.fetch_all(&mut **conn).await?;
Ok(rows
.into_iter()
.map(|r| Row {
inner: RowInner::Sqlite(r),
})
.collect())
}
}
}
pub async fn query_opt(&mut self, sql: &str, params: &[Param]) -> Result<Option<Row>> {
match &mut self.inner {
#[cfg(feature = "postgres")]
TxInner::Postgres(conn) => {
let sql = to_pg_placeholders(sql);
let q = bind_pg(sqlx::query(&sql), params);
Ok(q.fetch_optional(&mut **conn).await?.map(|r| Row {
inner: RowInner::Postgres(r),
}))
}
#[cfg(feature = "sqlite")]
TxInner::Sqlite(conn) => {
let q = bind_sqlite(sqlx::query(sql), params);
Ok(q.fetch_optional(&mut **conn).await?.map(|r| Row {
inner: RowInner::Sqlite(r),
}))
}
}
}
pub async fn query_one(&mut self, sql: &str, params: &[Param]) -> Result<Row> {
self.query_opt(sql, params)
.await?
.ok_or_else(|| Error::app("query_one: no rows returned"))
}
}
pub struct Row {
inner: RowInner,
}
enum RowInner {
#[cfg(feature = "postgres")]
Postgres(sqlx::postgres::PgRow),
#[cfg(feature = "sqlite")]
Sqlite(sqlx::sqlite::SqliteRow),
}
#[cfg(all(feature = "postgres", feature = "sqlite"))]
pub trait RowValue<'r>:
sqlx::Decode<'r, sqlx::Postgres>
+ sqlx::Type<sqlx::Postgres>
+ sqlx::Decode<'r, sqlx::Sqlite>
+ sqlx::Type<sqlx::Sqlite>
{
}
#[cfg(all(feature = "postgres", feature = "sqlite"))]
impl<'r, T> RowValue<'r> for T where
T: sqlx::Decode<'r, sqlx::Postgres>
+ sqlx::Type<sqlx::Postgres>
+ sqlx::Decode<'r, sqlx::Sqlite>
+ sqlx::Type<sqlx::Sqlite>
{
}
#[cfg(all(feature = "postgres", not(feature = "sqlite")))]
pub trait RowValue<'r>: sqlx::Decode<'r, sqlx::Postgres> + sqlx::Type<sqlx::Postgres> {}
#[cfg(all(feature = "postgres", not(feature = "sqlite")))]
impl<'r, T> RowValue<'r> for T where T: sqlx::Decode<'r, sqlx::Postgres> + sqlx::Type<sqlx::Postgres>
{}
#[cfg(all(feature = "sqlite", not(feature = "postgres")))]
pub trait RowValue<'r>: sqlx::Decode<'r, sqlx::Sqlite> + sqlx::Type<sqlx::Sqlite> {}
#[cfg(all(feature = "sqlite", not(feature = "postgres")))]
impl<'r, T> RowValue<'r> for T where T: sqlx::Decode<'r, sqlx::Sqlite> + sqlx::Type<sqlx::Sqlite> {}
impl Row {
pub fn get<'r, T>(&'r self, col: &str) -> T
where
T: RowValue<'r>,
{
use sqlx::Row as _;
match &self.inner {
#[cfg(feature = "postgres")]
RowInner::Postgres(r) => r.get::<T, _>(col),
#[cfg(feature = "sqlite")]
RowInner::Sqlite(r) => r.get::<T, _>(col),
}
}
pub fn try_get<'r, T>(&'r self, col: &str) -> Result<T>
where
T: RowValue<'r>,
{
use sqlx::Row as _;
match &self.inner {
#[cfg(feature = "postgres")]
RowInner::Postgres(r) => Ok(r.try_get::<T, _>(col)?),
#[cfg(feature = "sqlite")]
RowInner::Sqlite(r) => Ok(r.try_get::<T, _>(col)?),
}
}
}
pub type TxBody<'a> = Box<
dyn for<'t, 'c> Fn(&'t mut Tx<'c>) -> Pin<Box<dyn Future<Output = Result<Value>> + Send + 't>>
+ Send
+ Sync
+ 'a,
>;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum IsolationLevel {
#[default]
ReadCommitted,
RepeatableRead,
Serializable,
}
impl IsolationLevel {
#[cfg(feature = "postgres")]
pub(crate) fn pg_sql(self) -> &'static str {
match self {
IsolationLevel::ReadCommitted => "READ COMMITTED",
IsolationLevel::RepeatableRead => "REPEATABLE READ",
IsolationLevel::Serializable => "SERIALIZABLE",
}
}
}
#[derive(Clone)]
pub struct TransactionOptions {
pub name: String,
pub isolation: IsolationLevel,
pub read_only: bool,
pub max_retries: u32,
pub backoff_factor: f64,
pub base_interval: Duration,
pub max_interval: Duration,
pub retry_if: Option<RetryPredicate>,
}
impl TransactionOptions {
pub fn new(name: impl Into<String>) -> Self {
TransactionOptions {
name: name.into(),
isolation: IsolationLevel::default(),
read_only: false,
max_retries: 0,
backoff_factor: 2.0,
base_interval: Duration::from_millis(100),
max_interval: Duration::from_secs(5),
retry_if: None,
}
}
pub fn isolation(mut self, level: IsolationLevel) -> Self {
self.isolation = level;
self
}
pub fn read_only(mut self, read_only: bool) -> Self {
self.read_only = read_only;
self
}
pub fn max_retries(mut self, n: u32) -> Self {
self.max_retries = n;
self
}
pub fn backoff_factor(mut self, f: f64) -> Self {
self.backoff_factor = f;
self
}
pub fn base_interval(mut self, d: Duration) -> Self {
self.base_interval = d;
self
}
pub fn max_interval(mut self, d: Duration) -> Self {
self.max_interval = d;
self
}
pub fn retry_if<P>(mut self, predicate: P) -> Self
where
P: Fn(&Error) -> bool + Send + Sync + 'static,
{
self.retry_if = Some(std::sync::Arc::new(predicate));
self
}
pub(crate) fn user_retry_backoff(&self, attempt: u32) -> Duration {
let secs = self.base_interval.as_secs_f64() * self.backoff_factor.powi(attempt as i32);
Duration::from_secs_f64(secs).min(self.max_interval)
}
pub(crate) fn should_user_retry(&self, err: &Error, attempt: u32) -> bool {
!err.is_tx_conflict()
&& attempt < self.max_retries
&& self.retry_if.as_ref().is_none_or(|p| p(err))
}
}
#[cfg(feature = "postgres")]
fn to_pg_placeholders(sql: &str) -> String {
let mut out = String::with_capacity(sql.len() + 8);
let mut n = 0u32;
let mut in_str = false;
for c in sql.chars() {
match c {
'\'' => {
in_str = !in_str;
out.push(c);
}
'?' if !in_str => {
n += 1;
out.push('$');
out.push_str(&n.to_string());
}
_ => out.push(c),
}
}
out
}
#[cfg(feature = "postgres")]
fn bind_pg<'q>(
mut q: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
params: &'q [Param],
) -> sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments> {
for p in params {
q = match p {
Param::Null => q.bind(Option::<&str>::None),
Param::Int(v) => q.bind(*v),
Param::Float(v) => q.bind(*v),
Param::Text(v) => q.bind(v.as_str()),
Param::Bool(v) => q.bind(*v),
Param::Bytes(v) => q.bind(v.as_slice()),
};
}
q
}
#[cfg(feature = "sqlite")]
fn bind_sqlite<'q>(
mut q: sqlx::query::Query<'q, sqlx::Sqlite, sqlx::sqlite::SqliteArguments<'q>>,
params: &'q [Param],
) -> sqlx::query::Query<'q, sqlx::Sqlite, sqlx::sqlite::SqliteArguments<'q>> {
for p in params {
q = match p {
Param::Null => q.bind(Option::<&str>::None),
Param::Int(v) => q.bind(*v),
Param::Float(v) => q.bind(*v),
Param::Text(v) => q.bind(v.as_str()),
Param::Bool(v) => q.bind(*v),
Param::Bytes(v) => q.bind(v.as_slice()),
};
}
q
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "postgres")]
#[test]
fn placeholders_rewritten_outside_string_literals() {
assert_eq!(
to_pg_placeholders("UPDATE a SET b = ? WHERE id = ?"),
"UPDATE a SET b = $1 WHERE id = $2"
);
assert_eq!(
to_pg_placeholders("SELECT '? literal' , ? FROM t"),
"SELECT '? literal' , $1 FROM t"
);
}
#[test]
fn params_macro_builds_in_order() {
let p = params![1_i64, "x", true];
assert_eq!(
p,
vec![Param::Int(1), Param::Text("x".into()), Param::Bool(true)]
);
let empty = params![];
assert!(empty.is_empty());
}
}