use crate::error::{SqlxError, SqlxResult};
use crate::pool::SqlxPool;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IsolationLevel {
ReadUncommitted,
ReadCommitted,
RepeatableRead,
Serializable,
}
impl IsolationLevel {
pub fn as_sql(&self) -> &'static str {
match self {
IsolationLevel::ReadUncommitted => "READ UNCOMMITTED",
IsolationLevel::ReadCommitted => "READ COMMITTED",
IsolationLevel::RepeatableRead => "REPEATABLE READ",
IsolationLevel::Serializable => "SERIALIZABLE",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccessMode {
ReadWrite,
ReadOnly,
}
impl AccessMode {
pub fn as_sql(&self) -> &'static str {
match self {
AccessMode::ReadWrite => "READ WRITE",
AccessMode::ReadOnly => "READ ONLY",
}
}
}
#[derive(Debug, Clone, Default)]
pub struct TransactionOptions {
pub isolation_level: Option<IsolationLevel>,
pub access_mode: Option<AccessMode>,
pub deferrable: Option<bool>,
}
impl TransactionOptions {
pub fn new() -> Self {
Self::default()
}
pub fn isolation_level(mut self, level: IsolationLevel) -> Self {
self.isolation_level = Some(level);
self
}
pub fn access_mode(mut self, mode: AccessMode) -> Self {
self.access_mode = Some(mode);
self
}
pub fn deferrable(mut self, deferrable: bool) -> Self {
self.deferrable = Some(deferrable);
self
}
pub fn read_only() -> Self {
Self::new().access_mode(AccessMode::ReadOnly)
}
pub fn serializable() -> Self {
Self::new().isolation_level(IsolationLevel::Serializable)
}
}
#[cfg(feature = "postgres")]
pub async fn with_transaction_pg<F, T>(pool: &sqlx::PgPool, f: F) -> SqlxResult<T>
where
F: for<'c> FnOnce(
&'c mut sqlx::Transaction<'static, sqlx::Postgres>,
) -> futures::future::BoxFuture<'c, SqlxResult<T>>,
{
let mut tx = pool.begin().await?;
match f(&mut tx).await {
Ok(value) => {
tx.commit().await?;
Ok(value)
}
Err(err) => {
if let Err(rb) = tx.rollback().await {
tracing::warn!(error = %rb, "transaction rollback failed; connection drop will abort server-side");
}
Err(err)
}
}
}
#[cfg(feature = "mysql")]
pub async fn with_transaction_mysql<F, T>(pool: &sqlx::MySqlPool, f: F) -> SqlxResult<T>
where
F: for<'c> FnOnce(
&'c mut sqlx::Transaction<'static, sqlx::MySql>,
) -> futures::future::BoxFuture<'c, SqlxResult<T>>,
{
let mut tx = pool.begin().await?;
match f(&mut tx).await {
Ok(value) => {
tx.commit().await?;
Ok(value)
}
Err(err) => {
if let Err(rb) = tx.rollback().await {
tracing::warn!(error = %rb, "transaction rollback failed; connection drop will abort server-side");
}
Err(err)
}
}
}
#[cfg(feature = "sqlite")]
pub async fn with_transaction_sqlite<F, T>(pool: &sqlx::SqlitePool, f: F) -> SqlxResult<T>
where
F: for<'c> FnOnce(
&'c mut sqlx::Transaction<'static, sqlx::Sqlite>,
) -> futures::future::BoxFuture<'c, SqlxResult<T>>,
{
let mut tx = pool.begin().await?;
match f(&mut tx).await {
Ok(value) => {
tx.commit().await?;
Ok(value)
}
Err(err) => {
if let Err(rb) = tx.rollback().await {
tracing::warn!(error = %rb, "transaction rollback failed; connection drop will abort server-side");
}
Err(err)
}
}
}
pub async fn with_transaction<F, T>(pool: &SqlxPool, f: F) -> SqlxResult<T>
where
F: FnOnce(&SqlxPool) -> futures::future::BoxFuture<'_, Result<T, SqlxError>>,
{
f(pool).await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_isolation_level_sql() {
assert_eq!(IsolationLevel::ReadUncommitted.as_sql(), "READ UNCOMMITTED");
assert_eq!(IsolationLevel::ReadCommitted.as_sql(), "READ COMMITTED");
assert_eq!(IsolationLevel::RepeatableRead.as_sql(), "REPEATABLE READ");
assert_eq!(IsolationLevel::Serializable.as_sql(), "SERIALIZABLE");
}
#[test]
fn test_access_mode_sql() {
assert_eq!(AccessMode::ReadWrite.as_sql(), "READ WRITE");
assert_eq!(AccessMode::ReadOnly.as_sql(), "READ ONLY");
}
#[test]
fn test_transaction_options_builder() {
let opts = TransactionOptions::new()
.isolation_level(IsolationLevel::Serializable)
.access_mode(AccessMode::ReadOnly)
.deferrable(true);
assert_eq!(opts.isolation_level, Some(IsolationLevel::Serializable));
assert_eq!(opts.access_mode, Some(AccessMode::ReadOnly));
assert_eq!(opts.deferrable, Some(true));
}
#[test]
fn test_transaction_options_presets() {
let read_only = TransactionOptions::read_only();
assert_eq!(read_only.access_mode, Some(AccessMode::ReadOnly));
let serializable = TransactionOptions::serializable();
assert_eq!(
serializable.isolation_level,
Some(IsolationLevel::Serializable)
);
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_with_transaction_sqlite_commits_on_ok() {
let pool = sqlx::sqlite::SqlitePoolOptions::new()
.max_connections(1)
.connect("sqlite::memory:")
.await
.expect("in-memory sqlite needs no server");
sqlx::query("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")
.execute(&pool)
.await
.unwrap();
with_transaction_sqlite(&pool, |tx| {
Box::pin(async move {
sqlx::query("INSERT INTO t (name) VALUES ('a')")
.execute(&mut **tx)
.await?;
Ok::<_, SqlxError>(())
})
})
.await
.expect("transaction commits");
let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM t")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(count, 1, "committed row must be visible after commit");
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn test_with_transaction_sqlite_rolls_back_on_err() {
let pool = sqlx::sqlite::SqlitePoolOptions::new()
.max_connections(1)
.connect("sqlite::memory:")
.await
.expect("in-memory sqlite needs no server");
sqlx::query("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")
.execute(&pool)
.await
.unwrap();
let result: SqlxResult<()> = with_transaction_sqlite(&pool, |tx| {
Box::pin(async move {
sqlx::query("INSERT INTO t (name) VALUES ('a')")
.execute(&mut **tx)
.await?;
Err(SqlxError::Internal("boom".into()))
})
})
.await;
assert!(result.is_err(), "closure error must propagate");
let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM t")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(count, 0, "rolled-back write must not persist");
}
}