Skip to main content

database_mcp_sqlite/
connection.rs

1//! `SQLite` connection: pool ownership, initialization, and [`Connection`] impl.
2//!
3//! Owns the single lazy [`SqlitePool`] used by [`SqliteHandler`](crate::SqliteHandler).
4//! `SQLite` is a single-file, single-writer backend; the pool is fixed
5//! at one connection.
6
7use std::time::Duration;
8
9use database_mcp_config::DatabaseConfig;
10use database_mcp_sql::Connection;
11use database_mcp_sql::SqlError;
12use sqlx::sqlite::{SqliteConnectOptions, SqlitePool};
13use tracing::info;
14
15/// Owns the lazy `SQLite` pool and the logic that builds it.
16#[derive(Clone)]
17pub(crate) struct SqliteConnection {
18    config: DatabaseConfig,
19    pool: SqlitePool,
20}
21
22impl std::fmt::Debug for SqliteConnection {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        f.debug_struct("SqliteConnection").finish_non_exhaustive()
25    }
26}
27
28impl SqliteConnection {
29    /// Builds the connection and its lazy pool.
30    pub(crate) fn new(config: &DatabaseConfig) -> Self {
31        info!("SQLite lazy connection pool created");
32
33        Self {
34            config: config.clone(),
35            pool: create_lazy_pool(config),
36        }
37    }
38
39    /// Returns the single pool. Target is ignored (`SQLite` is single-file).
40    ///
41    /// Crate-private so every tool path goes through the unified
42    /// [`Connection`] methods and cannot bypass timeout / error capture.
43    #[allow(clippy::unused_async)]
44    pub(crate) async fn pool(&self, _target: Option<&str>) -> Result<SqlitePool, SqlError> {
45        Ok(self.pool.clone())
46    }
47}
48
49impl Connection for SqliteConnection {
50    type DB = sqlx::Sqlite;
51
52    async fn pool(&self, target: Option<&str>) -> Result<sqlx::Pool<Self::DB>, SqlError> {
53        self.pool(target).await
54    }
55
56    fn query_timeout(&self) -> Option<u64> {
57        self.config.query_timeout
58    }
59}
60
61/// Creates a lazy `SQLite` pool from a [`DatabaseConfig`].
62///
63/// Forces `max_connections` to 1 — `SQLite` is a single-writer backend.
64fn create_lazy_pool(config: &DatabaseConfig) -> SqlitePool {
65    let conn_ops = SqliteConnectOptions::new().filename(config.name.as_deref().unwrap_or_default());
66    let mut pool_opts = sqlx::pool::PoolOptions::new()
67        .max_connections(1)
68        .min_connections(DatabaseConfig::DEFAULT_MIN_CONNECTIONS)
69        .idle_timeout(Duration::from_secs(DatabaseConfig::DEFAULT_IDLE_TIMEOUT_SECS))
70        .max_lifetime(Duration::from_secs(DatabaseConfig::DEFAULT_MAX_LIFETIME_SECS));
71
72    if let Some(timeout) = config.connection_timeout {
73        pool_opts = pool_opts.acquire_timeout(Duration::from_secs(timeout));
74    }
75
76    pool_opts.connect_lazy_with(conn_ops)
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use database_mcp_config::DatabaseBackend;
83
84    fn base_config() -> DatabaseConfig {
85        DatabaseConfig {
86            backend: DatabaseBackend::Sqlite,
87            name: Some("test.db".into()),
88            ..DatabaseConfig::default()
89        }
90    }
91
92    #[tokio::test]
93    async fn create_lazy_pool_returns_idle_pool() {
94        let pool = create_lazy_pool(&base_config());
95        assert_eq!(pool.size(), 0, "pool should be lazy (no connections yet)");
96    }
97
98    #[tokio::test]
99    async fn create_lazy_pool_without_name() {
100        let pool = create_lazy_pool(&DatabaseConfig {
101            name: None,
102            ..base_config()
103        });
104        assert_eq!(pool.size(), 0);
105    }
106
107    #[tokio::test]
108    async fn new_creates_lazy_pool() {
109        let connection = SqliteConnection::new(&base_config());
110        assert_eq!(connection.pool.size(), 0, "pool should be lazy");
111    }
112
113    #[tokio::test]
114    async fn pool_returns_single_pool() {
115        let connection = SqliteConnection::new(&base_config());
116        connection.pool(None).await.expect("None target should succeed");
117        connection
118            .pool(Some("anything"))
119            .await
120            .expect("any target should return the same single pool");
121    }
122}