use std::collections::VecDeque;
use std::sync::Arc;
use std::time::Duration;
use parking_lot::Mutex;
use tokio::sync::Semaphore;
use tokio_rusqlite::Connection;
use tracing::{debug, info, trace};
use crate::config::SqliteConfig;
use crate::connection::{PooledConnection, SqliteConnection};
use crate::error::{SqliteError, SqliteResult};
#[derive(Clone)]
pub struct SqlitePool {
config: Arc<SqliteConfig>,
semaphore: Arc<Semaphore>,
idle_connections: Arc<Mutex<VecDeque<PooledConnection>>>,
pool_config: Arc<PoolConfig>,
stats: Arc<Mutex<PoolStats>>,
}
#[derive(Debug, Default, Clone)]
pub struct PoolStats {
pub reuses: u64,
pub opens: u64,
pub expirations: u64,
pub in_use: usize,
}
impl SqlitePool {
pub async fn new(config: SqliteConfig) -> SqliteResult<Self> {
Self::with_pool_config(config, PoolConfig::default()).await
}
pub async fn with_pool_config(
config: SqliteConfig,
pool_config: PoolConfig,
) -> SqliteResult<Self> {
info!(
path = %config.path_str(),
max_connections = %pool_config.max_connections,
"SQLite connection pool created"
);
let test_conn = Self::open_connection(&config).await?;
drop(test_conn);
let pool = Self {
config: Arc::new(config),
semaphore: Arc::new(Semaphore::new(pool_config.max_connections)),
idle_connections: Arc::new(Mutex::new(VecDeque::with_capacity(
pool_config.max_connections,
))),
pool_config: Arc::new(pool_config),
stats: Arc::new(Mutex::new(PoolStats::default())),
};
if !pool.config.path.is_memory() && pool.pool_config.min_connections > 0 {
debug!(
"Pre-warming pool with {} connections",
pool.pool_config.min_connections
);
for _ in 0..pool.pool_config.min_connections {
if let Ok(conn) = Self::open_connection(&pool.config).await {
let mut idle = pool.idle_connections.lock();
idle.push_back(PooledConnection::new(conn));
}
}
}
Ok(pool)
}
async fn open_connection(config: &SqliteConfig) -> SqliteResult<Connection> {
let path = config.path_str().to_string();
let init_sql = config.init_sql();
let conn = if config.path.is_memory() {
Connection::open_in_memory().await?
} else {
Connection::open(&path).await?
};
conn.call(move |conn| {
conn.execute_batch(&init_sql)?;
Ok(())
})
.await?;
#[cfg(feature = "vector")]
{
use std::sync::Once;
static WARN_ONCE: Once = Once::new();
let _ = conn
.call(|conn| {
if let Err(e) = crate::vector::register_vector_extension(conn) {
WARN_ONCE.call_once(|| {
tracing::warn!(
error = %e,
"sqlite-vector-rs extension could not be registered; \
vector SQL functions will be unavailable on this connection. \
Build libsqlite_vector_rs.so and set SQLITE_VECTOR_RS_LIB \
or place it alongside the test/binary. \
(This warning is emitted once per process.)"
);
});
}
Ok(())
})
.await;
}
Ok(conn)
}
pub async fn get(&self) -> SqliteResult<SqliteConnection> {
trace!("Acquiring connection from pool");
let permit = self
.semaphore
.clone()
.acquire_owned()
.await
.map_err(|e| SqliteError::pool(format!("failed to acquire permit: {}", e)))?;
{
let mut stats = self.stats.lock();
stats.in_use += 1;
}
if self.config.path.is_memory() {
let conn = Self::open_connection(&self.config).await?;
{
let mut stats = self.stats.lock();
stats.opens += 1;
}
return Ok(SqliteConnection::new_pooled(
conn, permit, None, ));
}
let conn: Option<Connection> = {
let mut idle = self.idle_connections.lock();
while let Some(pooled) = idle.pop_front() {
let is_expired = if let Some(lifetime) = self.pool_config.max_lifetime {
pooled.created_at.elapsed() > lifetime
} else {
false
};
let is_idle_expired = if let Some(timeout) = self.pool_config.idle_timeout {
pooled.last_used.elapsed() > timeout
} else {
false
};
if is_expired || is_idle_expired {
let mut stats = self.stats.lock();
stats.expirations += 1;
continue;
}
let mut stats = self.stats.lock();
stats.reuses += 1;
return Ok(SqliteConnection::new_pooled(
pooled.conn,
permit,
Some(self.idle_connections.clone()),
));
}
None
};
if conn.is_none() {
debug!("No idle connections, opening new connection");
let new_conn = Self::open_connection(&self.config).await?;
{
let mut stats = self.stats.lock();
stats.opens += 1;
}
return Ok(SqliteConnection::new_pooled(
new_conn,
permit,
Some(self.idle_connections.clone()),
));
}
unreachable!()
}
pub fn config(&self) -> &SqliteConfig {
&self.config
}
pub fn pool_config(&self) -> &PoolConfig {
&self.pool_config
}
pub fn stats(&self) -> PoolStats {
self.stats.lock().clone()
}
pub fn reset_stats(&self) {
let mut stats = self.stats.lock();
*stats = PoolStats::default();
}
pub async fn is_healthy(&self) -> bool {
match Self::open_connection(&self.config).await {
Ok(conn) => {
let result = conn
.call(|conn| {
conn.execute("SELECT 1", [])?;
Ok(())
})
.await;
result.is_ok()
}
Err(_) => false,
}
}
pub fn available_permits(&self) -> usize {
self.semaphore.available_permits()
}
pub fn idle_count(&self) -> usize {
self.idle_connections.lock().len()
}
pub fn builder() -> SqlitePoolBuilder {
SqlitePoolBuilder::new()
}
}
#[derive(Debug, Clone)]
pub struct PoolConfig {
pub max_connections: usize,
pub min_connections: usize,
pub connection_timeout: Option<Duration>,
pub idle_timeout: Option<Duration>,
pub max_lifetime: Option<Duration>,
}
impl Default for PoolConfig {
fn default() -> Self {
Self {
max_connections: 5, min_connections: 1,
connection_timeout: Some(Duration::from_secs(30)),
idle_timeout: Some(Duration::from_secs(300)), max_lifetime: Some(Duration::from_secs(1800)), }
}
}
#[derive(Debug, Default)]
pub struct SqlitePoolBuilder {
config: Option<SqliteConfig>,
url: Option<String>,
pool_config: PoolConfig,
}
impl SqlitePoolBuilder {
pub fn new() -> Self {
Self {
config: None,
url: None,
pool_config: PoolConfig::default(),
}
}
pub fn url(mut self, url: impl Into<String>) -> Self {
self.url = Some(url.into());
self
}
pub fn config(mut self, config: SqliteConfig) -> Self {
self.config = Some(config);
self
}
pub fn max_connections(mut self, n: usize) -> Self {
self.pool_config.max_connections = n;
self
}
pub fn connection_timeout(mut self, timeout: Duration) -> Self {
self.pool_config.connection_timeout = Some(timeout);
self
}
pub fn idle_timeout(mut self, timeout: Duration) -> Self {
self.pool_config.idle_timeout = Some(timeout);
self
}
pub async fn build(self) -> SqliteResult<SqlitePool> {
let config = if let Some(config) = self.config {
config
} else if let Some(url) = self.url {
SqliteConfig::from_url(url)?
} else {
return Err(SqliteError::config("no database URL or config provided"));
};
SqlitePool::with_pool_config(config, self.pool_config).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pool_config_default() {
let config = PoolConfig::default();
assert_eq!(config.max_connections, 5);
}
#[test]
fn test_pool_builder() {
let builder = SqlitePoolBuilder::new()
.url("sqlite::memory:")
.max_connections(10);
assert!(builder.url.is_some());
assert_eq!(builder.pool_config.max_connections, 10);
}
#[tokio::test]
async fn test_pool_memory() {
let pool = SqlitePool::new(SqliteConfig::memory()).await.unwrap();
assert!(pool.available_permits() > 0);
}
#[tokio::test]
async fn test_pool_get_connection() {
let pool = SqlitePool::new(SqliteConfig::memory()).await.unwrap();
let conn = pool.get().await;
assert!(conn.is_ok());
}
}