rustrails-record 0.1.2

ORM layer (ActiveRecord equivalent)
Documentation
use rustrails_support::runtime;
use sea_orm::{ConnectOptions, Database, DatabaseConnection};

/// Errors returned while establishing or managing database connections.
#[derive(Debug, thiserror::Error)]
pub enum ConnectionError {
    /// The database connection could not be established.
    #[error("connection failed: {0}")]
    ConnectionFailed(String),
    /// No database connection is available.
    #[error("not connected")]
    NotConnected,
}

/// Wrapper around a SeaORM database connection.
#[derive(Clone, Debug)]
pub struct ConnectionPool {
    db: DatabaseConnection,
}

impl ConnectionPool {
    /// Establishes a database connection from a URL.
    pub async fn connect(url: &str) -> Result<Self, ConnectionError> {
        let db = Database::connect(url)
            .await
            .map_err(|error| ConnectionError::ConnectionFailed(error.to_string()))?;
        Ok(Self { db })
    }

    /// Synchronous wrapper for [`Self::connect`].
    pub fn connect_sync(url: &str) -> Result<Self, ConnectionError> {
        runtime::block_on(Self::connect(url))
    }

    /// Establishes a database connection using explicit connection options.
    pub async fn connect_with_options(options: ConnectOptions) -> Result<Self, ConnectionError> {
        let db = Database::connect(options)
            .await
            .map_err(|error| ConnectionError::ConnectionFailed(error.to_string()))?;
        Ok(Self { db })
    }

    /// Synchronous wrapper for [`Self::connect_with_options`].
    pub fn connect_with_options_sync(options: ConnectOptions) -> Result<Self, ConnectionError> {
        runtime::block_on(Self::connect_with_options(options))
    }

    /// Returns the underlying SeaORM connection.
    pub fn connection(&self) -> &DatabaseConnection {
        &self.db
    }

    /// Executes a simple query to confirm the connection is healthy.
    pub async fn ping(&self) -> Result<(), ConnectionError> {
        self.db
            .ping()
            .await
            .map_err(|error| ConnectionError::ConnectionFailed(error.to_string()))
    }

    /// Synchronous wrapper for [`Self::ping`].
    pub fn ping_sync(&self) -> Result<(), ConnectionError> {
        runtime::block_on(self.ping())
    }

    /// Closes the underlying database connection.
    pub async fn close(self) -> Result<(), ConnectionError> {
        self.db
            .close()
            .await
            .map_err(|error| ConnectionError::ConnectionFailed(error.to_string()))
    }

    /// Synchronous wrapper for [`Self::close`].
    pub fn close_sync(self) -> Result<(), ConnectionError> {
        runtime::block_on(self.close())
    }
}

/// Establishes a database connection from a URL string.
pub async fn establish(url: &str) -> Result<ConnectionPool, ConnectionError> {
    ConnectionPool::connect(url).await
}

/// Synchronous wrapper for [`establish`].
pub fn establish_sync(url: &str) -> Result<ConnectionPool, ConnectionError> {
    runtime::block_on(establish(url))
}

#[cfg(test)]
mod tests {
    use rustrails_support::{database, runtime};
    use sea_orm::{ConnectOptions, DatabaseBackend};

    use super::{ConnectionError, ConnectionPool, establish, establish_sync};

    fn run_sync_connection_test(test: impl FnOnce() + Send + 'static) {
        std::thread::spawn(move || {
            let _rt = runtime::init_runtime();
            database::establish("sqlite::memory:")
                .expect("sqlite in-memory connection should succeed");
            test();
        })
        .join()
        .unwrap();
    }

    #[tokio::test]
    async fn connect_to_in_memory_sqlite() {
        let pool = ConnectionPool::connect("sqlite::memory:")
            .await
            .expect("sqlite in-memory connection should succeed");

        assert_eq!(
            pool.connection().get_database_backend(),
            DatabaseBackend::Sqlite
        );
    }

    #[tokio::test]
    async fn connect_with_options_uses_same_backend() {
        let mut options = ConnectOptions::new("sqlite::memory:");
        options.sqlx_logging(false);

        let pool = ConnectionPool::connect_with_options(options)
            .await
            .expect("sqlite in-memory connection should succeed");

        assert_eq!(
            pool.connection().get_database_backend(),
            DatabaseBackend::Sqlite
        );
    }

    #[tokio::test]
    async fn ping_succeeds_for_live_connection() {
        let pool = establish("sqlite::memory:")
            .await
            .expect("sqlite in-memory connection should succeed");

        pool.ping().await.expect("ping should succeed");
    }

    #[tokio::test]
    async fn close_succeeds_for_live_connection() {
        let pool = establish("sqlite::memory:")
            .await
            .expect("sqlite in-memory connection should succeed");

        pool.close().await.expect("close should succeed");
    }

    #[test]
    fn connect_sync_to_in_memory_sqlite() {
        run_sync_connection_test(|| {
            let pool = ConnectionPool::connect_sync("sqlite::memory:")
                .expect("sqlite in-memory connection should succeed");

            assert_eq!(
                pool.connection().get_database_backend(),
                DatabaseBackend::Sqlite
            );
        });
    }

    #[test]
    fn connect_with_options_sync_uses_same_backend() {
        run_sync_connection_test(|| {
            let mut options = ConnectOptions::new("sqlite::memory:");
            options.sqlx_logging(false);

            let pool = ConnectionPool::connect_with_options_sync(options)
                .expect("sqlite in-memory connection should succeed");

            assert_eq!(
                pool.connection().get_database_backend(),
                DatabaseBackend::Sqlite
            );
        });
    }

    #[test]
    fn ping_sync_succeeds_for_live_connection() {
        run_sync_connection_test(|| {
            let pool = establish_sync("sqlite::memory:")
                .expect("sqlite in-memory connection should succeed");

            pool.ping_sync().expect("ping should succeed");
        });
    }

    #[test]
    fn close_sync_succeeds_for_live_connection() {
        run_sync_connection_test(|| {
            let pool = establish_sync("sqlite::memory:")
                .expect("sqlite in-memory connection should succeed");

            pool.close_sync().expect("close should succeed");
        });
    }

    #[test]
    fn establish_sync_creates_connection_pool() {
        run_sync_connection_test(|| {
            let pool = establish_sync("sqlite::memory:")
                .expect("sqlite in-memory connection should succeed");

            assert_eq!(
                pool.connection().get_database_backend(),
                DatabaseBackend::Sqlite
            );
        });
    }

    #[tokio::test]
    async fn invalid_url_returns_connection_error() {
        let result = ConnectionPool::connect("not-a-valid-database-url").await;

        assert!(matches!(result, Err(ConnectionError::ConnectionFailed(_))));
    }
}