Skip to main content

rust_ef_sqlite/
provider.rs

1use crate::pool_strategy::{
2    SqliteConnectionCustomizer, SqliteProviderInner, SQLITE_DEFAULT_POOL_SIZE,
3};
4use crate::sql_generator::SqliteSqlGenerator;
5use async_trait::async_trait;
6use r2d2_sqlite::SqliteConnectionManager;
7use rust_ef::error::{EFError, EFResult};
8use rust_ef::provider::{IDatabaseProvider, ISqlGenerator};
9use std::sync::Arc;
10use tokio::sync::Mutex;
11
12pub struct SqliteProvider {
13    inner: SqliteProviderInner,
14    #[cfg(feature = "tracing")]
15    slow_query_threshold_ms: std::sync::atomic::AtomicU64,
16}
17
18impl SqliteProvider {
19    /// Creates a provider for a file-based SQLite database with a connection
20    /// pool (default 8 connections). WAL mode and a 5s busy timeout are
21    /// applied to every pooled connection.
22    ///
23    /// Note: `:memory:` (without URI) is special-cased to delegate to
24    /// [`new_in_memory`](Self::new_in_memory) because SQLite `:memory:`
25    /// databases are per-connection — a pool would give each connection its
26    /// own isolated database. URI-form shared in-memory DBs (e.g.
27    /// `file::memory:?cache=shared`) intentionally use `Pooled` mode.
28    pub fn new(path: impl AsRef<std::path::Path>) -> EFResult<Self> {
29        let path_ref = path.as_ref();
30        if path_ref == std::path::Path::new(":memory:") {
31            return Self::new_in_memory();
32        }
33        let manager = SqliteConnectionManager::file(path_ref);
34        let pool = r2d2::Pool::builder()
35            .max_size(SQLITE_DEFAULT_POOL_SIZE)
36            .connection_customizer(Box::new(SqliteConnectionCustomizer))
37            .build(manager)
38            .map_err(|e| EFError::connection(format!("SQLite pool creation failed: {}", e)))?;
39        Ok(Self {
40            inner: SqliteProviderInner::Pooled(pool),
41            #[cfg(feature = "tracing")]
42            slow_query_threshold_ms: std::sync::atomic::AtomicU64::new(0),
43        })
44    }
45
46    /// Creates a provider for an in-memory SQLite database.
47    ///
48    /// Uses a single shared connection (`Arc<Mutex<Connection>>`) because
49    /// SQLite `:memory:` databases are per-connection — a pool would give
50    /// each connection its own isolated database. This preserves the
51    /// pre-v1.4 behavior: each `new_in_memory()` call creates a fresh,
52    /// independent database with full test isolation.
53    pub fn new_in_memory() -> EFResult<Self> {
54        let conn = rusqlite::Connection::open_in_memory()
55            .map_err(|e| EFError::connection(format!("SQLite in-memory open failed: {}", e)))?;
56        // WAL isn't supported for in-memory databases (SQLite silently
57        // ignores the PRAGMA), but busy_timeout still applies.
58        conn.execute_batch("PRAGMA busy_timeout=5000;")
59            .map_err(|e| EFError::connection(format!("SQLite pragma setup failed: {}", e)))?;
60        Ok(Self {
61            inner: SqliteProviderInner::Single(Arc::new(Mutex::new(conn))),
62            #[cfg(feature = "tracing")]
63            slow_query_threshold_ms: std::sync::atomic::AtomicU64::new(0),
64        })
65    }
66}
67
68#[async_trait]
69impl IDatabaseProvider for SqliteProvider {
70    fn sql_generator(&self) -> &'static dyn ISqlGenerator {
71        // Stateless generator: rvalue static promotion gives `&'static`.
72        &SqliteSqlGenerator
73    }
74
75    async fn get_connection(&self) -> EFResult<Box<dyn rust_ef::provider::IAsyncConnection>> {
76        let _guard = rust_ef::observability::PoolAcquireGuard::new("SQLite");
77        #[cfg_attr(not(feature = "tracing"), allow(unused_mut))]
78        let mut conn = self.inner.get_connection().await?;
79        #[cfg(feature = "tracing")]
80        {
81            let ms = self
82                .slow_query_threshold_ms
83                .load(std::sync::atomic::Ordering::Relaxed);
84            if ms > 0 {
85                conn.set_slow_query_threshold(std::time::Duration::from_millis(ms));
86            }
87        }
88        Ok(conn)
89    }
90
91    async fn execute_migration_command(&self, sql: &str) -> EFResult<()> {
92        self.inner.execute_migration_command(sql).await
93    }
94
95    fn name(&self) -> &str {
96        "SQLite"
97    }
98
99    fn migration_dialect(&self) -> rust_ef::migration::MigrationDialect {
100        rust_ef::migration::MigrationDialect::Sqlite
101    }
102
103    #[cfg(feature = "tracing")]
104    fn set_slow_query_threshold(&self, threshold: std::time::Duration) {
105        self.slow_query_threshold_ms.store(
106            threshold.as_millis() as u64,
107            std::sync::atomic::Ordering::Relaxed,
108        );
109    }
110}
111
112impl std::fmt::Debug for SqliteProvider {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        f.debug_struct("SqliteProvider")
115            .field("name", &self.name())
116            .finish()
117    }
118}