rustorm-core 0.1.1

Core traits, types and utilities for RustORM
Documentation
use crate::error::{OrmError, OrmResult};
use sqlx::{PgPool, Postgres, Transaction};
use std::future::Future;

/// Выполняет асинхронную функцию внутри транзакции.
/// При ошибке откатывает, при успехе — коммитит.
///
/// ```rust
/// let (user, profile) = orm::transaction(&pool, |tx| async move {
///     let user = User::create(data, tx).await?;
///     let profile = Profile::create(NewProfile { user_id: user.id }, tx).await?;
///     Ok((user, profile))
/// }).await?;
/// ```
pub async fn transaction<F, Fut, R>(pool: &PgPool, f: F) -> OrmResult<R>
where
    F: FnOnce(Transaction<'static, Postgres>) -> Fut,
    Fut: Future<Output = OrmResult<R>>,
{
    let tx = pool.begin().await.map_err(OrmError::from_sqlx)?;

    match f(tx).await {
        Ok(result) => Ok(result),
        Err(err) => {
            // Транзакция автоматически откатывается при дропе
            Err(err)
        }
    }
}

/// Уровни изоляции транзакции.
#[derive(Debug, Clone, Copy, Default)]
pub enum IsolationLevel {
    #[default]
    ReadCommitted,
    RepeatableRead,
    Serializable,
    ReadUncommitted,
}

impl IsolationLevel {
    pub fn as_sql(&self) -> &'static str {
        match self {
            IsolationLevel::ReadCommitted => "READ COMMITTED",
            IsolationLevel::RepeatableRead => "REPEATABLE READ",
            IsolationLevel::Serializable => "SERIALIZABLE",
            IsolationLevel::ReadUncommitted => "READ UNCOMMITTED",
        }
    }
}

#[derive(Debug, Clone, Default)]
pub struct TransactionOptions {
    pub isolation: IsolationLevel,
    pub read_only: bool,
}

/// Транзакция с явными опциями.
pub async fn transaction_with<F, Fut, R>(
    pool: &PgPool,
    opts: TransactionOptions,
    f: F,
) -> OrmResult<R>
where
    F: FnOnce(Transaction<'static, Postgres>) -> Fut,
    Fut: Future<Output = OrmResult<R>>,
{
    let mut tx = pool.begin().await.map_err(OrmError::from_sqlx)?;

    // Установить уровень изоляции
    let iso_sql = format!(
        "SET TRANSACTION ISOLATION LEVEL {}",
        opts.isolation.as_sql()
    );
    sqlx::query(&iso_sql)
        .execute(&mut *tx)
        .await
        .map_err(OrmError::from_sqlx)?;

    if opts.read_only {
        sqlx::query("SET TRANSACTION READ ONLY")
            .execute(&mut *tx)
            .await
            .map_err(OrmError::from_sqlx)?;
    }

    match f(tx).await {
        Ok(result) => Ok(result),
        Err(err) => Err(err),
    }
}