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