floz-orm 0.1.1

A lightweight, typesafe Rust ORM — unifying DAO and DSL from a single schema
Documentation
//! `Db` — polymorphic database connection pool.
//!
//! Wraps either a PostgreSQL or SQLite pool behind a single type.
//! The backend is auto-detected from the `DATABASE_URL` scheme.
//!
//! ```ignore
//! let db = Db::connect("postgres://user:pass@localhost/mydb").await?;
//! let db = Db::connect("sqlite::memory:").await?;
//! ```

#[cfg(feature = "postgres")]
use sqlx::postgres::{PgPool, PgPoolOptions};
#[cfg(feature = "sqlite")]
use sqlx::sqlite::{SqlitePool, SqlitePoolOptions};

use tokio::sync::Mutex;

use crate::error::FlozError;
use super::placeholder::replace_pg_placeholders;
use super::tx::{Tx, TxInner};

/// The inner pool enum — one variant per enabled backend.
#[derive(Debug, Clone)]
pub(crate) enum DbInner {
    #[cfg(feature = "postgres")]
    Postgres(PgPool),
    #[cfg(feature = "sqlite")]
    Sqlite(SqlitePool),
}

/// A database connection pool that auto-selects the backend based on
/// the connection URL scheme.
///
/// `Db` is cheap to clone (internally reference-counted) and safe to share
/// across tokio tasks.
#[derive(Debug, Clone)]
pub struct Db {
    pub(crate) inner: DbInner,
}

impl Db {
    /// Connect to a database, auto-detecting the backend from the URL scheme.
    ///
    /// - `postgres://...` or `postgresql://...` → PostgreSQL
    /// - `sqlite://...` or `sqlite::memory:` → SQLite
    pub async fn connect(url: &str) -> Result<Self, FlozError> {
        #[cfg(feature = "sqlite")]
        if url.starts_with("sqlite:") {
            let pool = SqlitePoolOptions::new()
                .max_connections(5)
                .connect(url)
                .await?;
            return Ok(Self {
                inner: DbInner::Sqlite(pool),
            });
        }

        #[cfg(feature = "postgres")]
        if url.starts_with("postgres:") || url.starts_with("postgresql:") {
            let pool = PgPoolOptions::new()
                .max_connections(10)
                .connect(url)
                .await?;
            return Ok(Self {
                inner: DbInner::Postgres(pool),
            });
        }

        Err(FlozError::Database(sqlx::Error::Configuration(
            format!(
                "Unsupported database URL scheme: {}. Enable the `postgres` or `sqlite` feature.",
                url
            )
            .into(),
        )))
    }

    /// Connect using an environment variable, falling back to a default URL.
    pub async fn connect_env(env_var: &str, default_url: &str) -> Result<Self, FlozError> {
        let url = std::env::var(env_var).unwrap_or_else(|_| default_url.to_string());
        Self::connect(&url).await
    }

    /// Wrap an existing PostgreSQL pool.
    #[cfg(feature = "postgres")]
    pub fn from_pg_pool(pool: PgPool) -> Self {
        Self {
            inner: DbInner::Postgres(pool),
        }
    }

    /// Wrap an existing SQLite pool.
    #[cfg(feature = "sqlite")]
    pub fn from_sqlite_pool(pool: SqlitePool) -> Self {
        Self {
            inner: DbInner::Sqlite(pool),
        }
    }

    /// Begin a new transaction.
    #[allow(unreachable_patterns)]
    pub async fn begin(&self) -> Result<Tx, FlozError> {
        match &self.inner {
            #[cfg(feature = "postgres")]
            DbInner::Postgres(pool) => {
                let mut conn = pool.acquire().await?;
                sqlx::query("BEGIN").execute(&mut *conn).await?;
                Ok(Tx {
                    inner: TxInner::Postgres(Mutex::new(conn)),
                    committed: false,
                })
            }
            #[cfg(feature = "sqlite")]
            DbInner::Sqlite(pool) => {
                let mut conn = pool.acquire().await?;
                sqlx::query("BEGIN").execute(&mut *conn).await?;
                Ok(Tx {
                    inner: TxInner::Sqlite(Mutex::new(conn)),
                    committed: false,
                })
            }
            _ => unreachable!("no database backend enabled"),
        }
    }

    /// Adjust SQL placeholder syntax for the current backend.
    #[allow(unreachable_patterns)]
    pub(crate) fn adjust_sql(&self, sql: &str) -> String {
        match &self.inner {
            #[cfg(feature = "postgres")]
            DbInner::Postgres(_) => sql.to_string(),
            #[cfg(feature = "sqlite")]
            DbInner::Sqlite(_) => replace_pg_placeholders(sql),
            _ => unreachable!("no database backend enabled"),
        }
    }
}