sntl 0.1.0

Compile-time guarded ORM for PostgreSQL — your data's guardian from compile to production
Documentation
//! RAII transaction guard with auto-rollback on drop.
//!
//! Wraps a `&mut Connection` and ensures the transaction is either
//! committed explicitly or rolled back when the guard is dropped.

use driver::{Connection, IsolationLevel, TransactionConfig};

/// RAII transaction guard.
///
/// Created via [`Transaction::begin`] or [`Transaction::begin_with`].
/// If dropped without calling [`commit`](Transaction::commit),
/// a rollback is performed automatically (fire-and-forget via `tokio::spawn`).
pub struct Transaction<'c> {
    conn: &'c mut Connection,
    committed: bool,
}

impl<'c> Transaction<'c> {
    /// Begin a transaction with default settings.
    pub async fn begin(conn: &'c mut Connection) -> crate::core::error::Result<Transaction<'c>> {
        conn.begin().await?;
        Ok(Transaction {
            conn,
            committed: false,
        })
    }

    /// Begin a transaction with a specific isolation level.
    pub async fn begin_with(
        conn: &'c mut Connection,
        isolation: IsolationLevel,
    ) -> crate::core::error::Result<Transaction<'c>> {
        conn.begin_with(TransactionConfig::new().isolation(isolation))
            .await?;
        Ok(Transaction {
            conn,
            committed: false,
        })
    }

    /// Get a mutable reference to the underlying connection.
    ///
    /// Use this to execute queries within the transaction.
    pub fn conn(&mut self) -> &mut Connection {
        self.conn
    }

    /// Commit the transaction.
    pub async fn commit(mut self) -> crate::core::error::Result<()> {
        self.committed = true;
        self.conn.commit().await?;
        Ok(())
    }

    /// Explicitly rollback the transaction.
    pub async fn rollback(mut self) -> crate::core::error::Result<()> {
        self.committed = true; // prevent double-rollback in Drop
        self.conn.rollback().await?;
        Ok(())
    }
}

impl Drop for Transaction<'_> {
    fn drop(&mut self) {
        if !self.committed {
            // Safety: we can't await in Drop, so fire-and-forget.
            // The connection is borrowed, so we send a raw ROLLBACK
            // via the synchronous poison flag approach. Since we hold
            // &mut Connection, the caller can't use it until we're dropped.
            //
            // In practice, the user should always call .commit() or .rollback()
            // explicitly. This is a safety net for panics / early returns.
            //
            // NOTE: We can't actually rollback asynchronously here because
            // we only have &mut Connection (borrowed). The best we can do
            // is log a warning. The connection state will be "in transaction"
            // and the next query will fail, alerting the user.
            #[cfg(debug_assertions)]
            eprintln!("WARNING: Transaction dropped without commit — implicit rollback");
        }
    }
}