Skip to main content

prax_sqlite/
pool.rs

1//! Connection pool for SQLite.
2//!
3//! This module provides an optimized connection pool for SQLite databases.
4//! Unlike PostgreSQL/MySQL, SQLite has unique characteristics:
5//!
6//! - In-memory databases: Each connection has its own isolated database
7//! - File-based databases: Connections share the same database file
8//!
9//! **In-memory caveat:** because every in-memory connection is its own
10//! isolated database, an in-memory pool isolates every statement on its
11//! own database. Use `transaction()` to pin a single connection, or a
12//! tempfile/shared-cache URI, for multi-statement workflows.
13//!
14//! For file-based databases, this pool reuses connections to avoid the
15//! overhead of opening new connections (~200µs per open).
16
17use std::collections::VecDeque;
18use std::sync::Arc;
19use std::time::Duration;
20
21use parking_lot::Mutex;
22use tokio::sync::Semaphore;
23use tokio_rusqlite::Connection;
24use tracing::{debug, info, trace, warn};
25
26use crate::config::SqliteConfig;
27use crate::connection::{PooledConnection, SqliteConnection};
28use crate::error::{SqliteError, SqliteResult};
29
30/// A connection pool for SQLite.
31///
32/// This pool provides connection reuse for file-based SQLite databases,
33/// significantly improving performance by avoiding repeated connection opens.
34///
35/// # Example
36///
37/// ```rust,ignore
38/// use prax_sqlite::{SqlitePool, SqliteConfig};
39///
40/// let pool = SqlitePool::new(SqliteConfig::file("data.db")).await?;
41/// let conn = pool.get().await?;
42/// // Use connection...
43/// // Connection is returned to pool when dropped
44/// ```
45#[derive(Clone)]
46pub struct SqlitePool {
47    config: Arc<SqliteConfig>,
48    /// Semaphore to limit concurrent connections.
49    semaphore: Arc<Semaphore>,
50    /// Pool of idle connections (only for file-based databases).
51    idle_connections: Arc<Mutex<VecDeque<PooledConnection>>>,
52    pool_config: Arc<PoolConfig>,
53    /// Statistics about pool usage.
54    stats: Arc<Mutex<PoolStats>>,
55}
56
57/// Statistics about pool usage.
58#[derive(Debug, Default, Clone)]
59pub struct PoolStats {
60    /// Number of connection reuses.
61    pub reuses: u64,
62    /// Number of new connections opened.
63    pub opens: u64,
64    /// Number of connections closed due to expiration.
65    pub expirations: u64,
66    /// Total number of connection checkouts since the pool was created
67    /// (or since the last [`SqlitePool::reset_stats`] call).
68    ///
69    /// This counter is cumulative: it is incremented on every
70    /// [`SqlitePool::get`] but never decremented, because
71    /// `SqliteConnection` holds no handle back to the pool's stats to
72    /// decrement on drop. Read it as total checkouts, not as the number
73    /// of connections currently checked out.
74    pub in_use: usize,
75}
76
77impl SqlitePool {
78    /// Create a new connection pool from configuration.
79    pub async fn new(config: SqliteConfig) -> SqliteResult<Self> {
80        Self::with_pool_config(config, PoolConfig::default()).await
81    }
82
83    /// Create a new connection pool with custom pool configuration.
84    pub async fn with_pool_config(
85        config: SqliteConfig,
86        pool_config: PoolConfig,
87    ) -> SqliteResult<Self> {
88        info!(
89            path = %config.path_str(),
90            max_connections = %pool_config.max_connections,
91            "SQLite connection pool created"
92        );
93
94        // Verify we can open at least one connection
95        let test_conn = Self::open_connection(&config).await?;
96        drop(test_conn);
97
98        let pool = Self {
99            config: Arc::new(config),
100            semaphore: Arc::new(Semaphore::new(pool_config.max_connections)),
101            idle_connections: Arc::new(Mutex::new(VecDeque::with_capacity(
102                pool_config.max_connections,
103            ))),
104            pool_config: Arc::new(pool_config),
105            stats: Arc::new(Mutex::new(PoolStats::default())),
106        };
107
108        // Pre-warm the pool with min_connections
109        if !pool.config.path.is_memory() && pool.pool_config.min_connections > 0 {
110            debug!(
111                "Pre-warming pool with {} connections",
112                pool.pool_config.min_connections
113            );
114            for _ in 0..pool.pool_config.min_connections {
115                match Self::open_connection(&pool.config).await {
116                    Ok(conn) => {
117                        let mut idle = pool.idle_connections.lock();
118                        idle.push_back(PooledConnection::new(conn));
119                    }
120                    Err(e) => {
121                        warn!(
122                            error = %e,
123                            "Failed to pre-warm a pooled connection; \
124                             continuing with a smaller pool"
125                        );
126                    }
127                }
128            }
129        }
130
131        Ok(pool)
132    }
133
134    /// Open a new connection with the given configuration.
135    async fn open_connection(config: &SqliteConfig) -> SqliteResult<Connection> {
136        let path = config.path_str().to_string();
137        let init_sql = config.init_sql();
138
139        let conn = if config.path.is_memory() {
140            Connection::open_in_memory().await?
141        } else {
142            Connection::open(&path).await?
143        };
144
145        // Run initialization SQL
146        conn.call(move |conn| {
147            conn.execute_batch(&init_sql)?;
148            Ok(())
149        })
150        .await?;
151
152        // Register the vector extension on every connection when the `vector`
153        // feature is enabled. Registration is idempotent and per-connection
154        // (every rusqlite Connection is a separate SQLite handle; for
155        // in-memory databases the handle has its own isolated database).
156        //
157        // Soft-fails: if the shared library cannot be located, pool creation
158        // still succeeds. Vector SQL functions will then be unavailable on
159        // that connection and fail at query time with a clear SQLite error.
160        //
161        // To avoid log spam on every new connection once the library is
162        // known to be missing, the warning is emitted at most once per
163        // process via a Once guard. Re-running with the library on disk
164        // will still produce functional connections.
165        #[cfg(feature = "vector")]
166        {
167            use std::sync::Once;
168            static WARN_ONCE: Once = Once::new();
169
170            let _ = conn
171                .call(|conn| {
172                    if let Err(e) = crate::vector::register_vector_extension(conn) {
173                        WARN_ONCE.call_once(|| {
174                            tracing::warn!(
175                                error = %e,
176                                "sqlite-vector-rs extension could not be registered; \
177                                 vector SQL functions will be unavailable on this connection. \
178                                 Build libsqlite_vector_rs.so and set SQLITE_VECTOR_RS_LIB \
179                                 or place it alongside the test/binary. \
180                                 (This warning is emitted once per process.)"
181                            );
182                        });
183                    }
184                    Ok(())
185                })
186                .await;
187        }
188
189        Ok(conn)
190    }
191
192    /// Get a connection from the pool.
193    ///
194    /// For file-based databases, this will try to reuse an idle connection
195    /// before opening a new one. For in-memory databases, always opens a new
196    /// connection (since each connection has its own database).
197    pub async fn get(&self) -> SqliteResult<SqliteConnection> {
198        trace!("Acquiring connection from pool");
199
200        // Wait for a permit (limits concurrent connections), honoring the
201        // configured connection timeout so a saturated pool fails fast
202        // instead of hanging forever.
203        let acquire = self.semaphore.clone().acquire_owned();
204        let permit = match self.pool_config.connection_timeout {
205            Some(timeout) => tokio::time::timeout(timeout, acquire)
206                .await
207                .map_err(|_| {
208                    SqliteError::timeout(format!(
209                        "timed out after {:?} waiting for a connection from the pool",
210                        timeout
211                    ))
212                })?
213                .map_err(|e| SqliteError::pool(format!("failed to acquire permit: {}", e)))?,
214            None => acquire
215                .await
216                .map_err(|e| SqliteError::pool(format!("failed to acquire permit: {}", e)))?,
217        };
218
219        // Update stats
220        {
221            let mut stats = self.stats.lock();
222            stats.in_use += 1;
223        }
224
225        // For in-memory databases, always create a new connection
226        // (each connection has its own separate database)
227        if self.config.path.is_memory() {
228            let conn = Self::open_connection(&self.config).await?;
229            {
230                let mut stats = self.stats.lock();
231                stats.opens += 1;
232            }
233            return Ok(SqliteConnection::new_pooled(
234                conn, permit, None, // No return channel for in-memory
235            ));
236        }
237
238        // Try to get an idle connection
239        let conn: Option<Connection> = {
240            let mut idle = self.idle_connections.lock();
241
242            // Clean up expired connections while searching
243            while let Some(pooled) = idle.pop_front() {
244                let is_expired = if let Some(lifetime) = self.pool_config.max_lifetime {
245                    pooled.created_at.elapsed() > lifetime
246                } else {
247                    false
248                };
249                let is_idle_expired = if let Some(timeout) = self.pool_config.idle_timeout {
250                    pooled.last_used.elapsed() > timeout
251                } else {
252                    false
253                };
254
255                if is_expired || is_idle_expired {
256                    let mut stats = self.stats.lock();
257                    stats.expirations += 1;
258                    // Connection will be dropped
259                    continue;
260                }
261                // Found a valid connection
262                let mut stats = self.stats.lock();
263                stats.reuses += 1;
264                return Ok(SqliteConnection::new_pooled(
265                    pooled.conn,
266                    permit,
267                    Some(self.idle_connections.clone()),
268                ));
269            }
270            None
271        };
272
273        // No idle connection available, open a new one
274        if conn.is_none() {
275            debug!("No idle connections, opening new connection");
276            let new_conn = Self::open_connection(&self.config).await?;
277            {
278                let mut stats = self.stats.lock();
279                stats.opens += 1;
280            }
281            return Ok(SqliteConnection::new_pooled(
282                new_conn,
283                permit,
284                Some(self.idle_connections.clone()),
285            ));
286        }
287
288        unreachable!()
289    }
290
291    /// Get the pool configuration.
292    pub fn config(&self) -> &SqliteConfig {
293        &self.config
294    }
295
296    /// Get the pool settings.
297    pub fn pool_config(&self) -> &PoolConfig {
298        &self.pool_config
299    }
300
301    /// Get pool statistics.
302    pub fn stats(&self) -> PoolStats {
303        self.stats.lock().clone()
304    }
305
306    /// Reset pool statistics.
307    pub fn reset_stats(&self) {
308        let mut stats = self.stats.lock();
309        *stats = PoolStats::default();
310    }
311
312    /// Check if the pool is healthy by attempting to get a connection.
313    pub async fn is_healthy(&self) -> bool {
314        match Self::open_connection(&self.config).await {
315            Ok(conn) => {
316                let result = conn
317                    .call(|conn| {
318                        conn.execute("SELECT 1", [])?;
319                        Ok(())
320                    })
321                    .await;
322                result.is_ok()
323            }
324            Err(_) => false,
325        }
326    }
327
328    /// Get the number of available permits (potential concurrent connections).
329    pub fn available_permits(&self) -> usize {
330        self.semaphore.available_permits()
331    }
332
333    /// Get the number of idle connections in the pool.
334    pub fn idle_count(&self) -> usize {
335        self.idle_connections.lock().len()
336    }
337
338    /// Create a builder for configuring the pool.
339    pub fn builder() -> SqlitePoolBuilder {
340        SqlitePoolBuilder::new()
341    }
342}
343
344/// Configuration for the connection pool.
345#[derive(Debug, Clone)]
346pub struct PoolConfig {
347    /// Maximum number of concurrent connections.
348    pub max_connections: usize,
349    /// Minimum number of connections to keep in the pool.
350    pub min_connections: usize,
351    /// Connection timeout.
352    pub connection_timeout: Option<Duration>,
353    /// Maximum idle time before a connection is closed.
354    pub idle_timeout: Option<Duration>,
355    /// Maximum lifetime of a connection before it's recycled.
356    pub max_lifetime: Option<Duration>,
357}
358
359impl Default for PoolConfig {
360    fn default() -> Self {
361        Self {
362            max_connections: 5, // SQLite benefits from fewer connections
363            min_connections: 1,
364            connection_timeout: Some(Duration::from_secs(30)),
365            idle_timeout: Some(Duration::from_secs(300)), // 5 minutes
366            max_lifetime: Some(Duration::from_secs(1800)), // 30 minutes
367        }
368    }
369}
370
371/// Builder for creating a connection pool.
372#[derive(Debug, Default)]
373pub struct SqlitePoolBuilder {
374    config: Option<SqliteConfig>,
375    url: Option<String>,
376    pool_config: PoolConfig,
377}
378
379impl SqlitePoolBuilder {
380    /// Create a new pool builder.
381    pub fn new() -> Self {
382        Self {
383            config: None,
384            url: None,
385            pool_config: PoolConfig::default(),
386        }
387    }
388
389    /// Set the database URL.
390    pub fn url(mut self, url: impl Into<String>) -> Self {
391        self.url = Some(url.into());
392        self
393    }
394
395    /// Set the configuration.
396    pub fn config(mut self, config: SqliteConfig) -> Self {
397        self.config = Some(config);
398        self
399    }
400
401    /// Set the maximum number of connections.
402    pub fn max_connections(mut self, n: usize) -> Self {
403        self.pool_config.max_connections = n;
404        self
405    }
406
407    /// Set the connection timeout.
408    pub fn connection_timeout(mut self, timeout: Duration) -> Self {
409        self.pool_config.connection_timeout = Some(timeout);
410        self
411    }
412
413    /// Set the idle timeout.
414    pub fn idle_timeout(mut self, timeout: Duration) -> Self {
415        self.pool_config.idle_timeout = Some(timeout);
416        self
417    }
418
419    /// Build the connection pool.
420    pub async fn build(self) -> SqliteResult<SqlitePool> {
421        let config = if let Some(config) = self.config {
422            config
423        } else if let Some(url) = self.url {
424            SqliteConfig::from_url(url)?
425        } else {
426            return Err(SqliteError::config("no database URL or config provided"));
427        };
428
429        SqlitePool::with_pool_config(config, self.pool_config).await
430    }
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436
437    #[test]
438    fn test_pool_config_default() {
439        let config = PoolConfig::default();
440        assert_eq!(config.max_connections, 5);
441    }
442
443    #[test]
444    fn test_pool_builder() {
445        let builder = SqlitePoolBuilder::new()
446            .url("sqlite::memory:")
447            .max_connections(10);
448
449        assert!(builder.url.is_some());
450        assert_eq!(builder.pool_config.max_connections, 10);
451    }
452
453    #[tokio::test]
454    async fn test_pool_memory() {
455        let pool = SqlitePool::new(SqliteConfig::memory()).await.unwrap();
456        // For in-memory databases, is_healthy opens a new database
457        // so we just verify the pool was created successfully
458        assert!(pool.available_permits() > 0);
459    }
460
461    #[tokio::test]
462    async fn test_pool_get_connection() {
463        let pool = SqlitePool::new(SqliteConfig::memory()).await.unwrap();
464        let conn = pool.get().await;
465        assert!(conn.is_ok());
466    }
467}