Skip to main content

database_mcp_sqlite/
connection.rs

1//! `SQLite` connection configuration and backend definition.
2
3use database_mcp_backend::error::AppError;
4use database_mcp_config::DatabaseConfig;
5use sqlx::SqlitePool;
6use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
7use tracing::info;
8
9/// `SQLite` file-based database backend.
10#[derive(Clone)]
11pub struct SqliteBackend {
12    pub(crate) pool: SqlitePool,
13    pub read_only: bool,
14}
15
16impl std::fmt::Debug for SqliteBackend {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        f.debug_struct("SqliteBackend")
19            .field("read_only", &self.read_only)
20            .finish_non_exhaustive()
21    }
22}
23
24impl SqliteBackend {
25    /// Creates a new `SQLite` backend from configuration.
26    ///
27    /// # Errors
28    ///
29    /// Returns [`AppError::Connection`] if the database file cannot be opened.
30    pub async fn new(config: &DatabaseConfig) -> Result<Self, AppError> {
31        let name = config.name.as_deref().unwrap_or_default();
32        let pool = SqlitePoolOptions::new()
33            .max_connections(1) // SQLite is single-writer
34            .connect_with(connect_options(config))
35            .await
36            .map_err(|e| AppError::Connection(format!("Failed to open SQLite: {e}")))?;
37
38        info!("SQLite connection initialized: {name}");
39
40        Ok(Self {
41            pool,
42            read_only: config.read_only,
43        })
44    }
45
46    /// Wraps `name` in double quotes for safe use in `SQLite` SQL statements.
47    pub(crate) fn quote_identifier(name: &str) -> String {
48        database_mcp_backend::identifier::quote_identifier(name, '"')
49    }
50}
51
52/// Builds [`SqliteConnectOptions`] from a [`DatabaseConfig`].
53fn connect_options(config: &DatabaseConfig) -> SqliteConnectOptions {
54    let name = config.name.as_deref().unwrap_or_default();
55    SqliteConnectOptions::new().filename(name)
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use database_mcp_config::DatabaseBackend;
62
63    #[test]
64    fn try_from_sets_filename() {
65        let config = DatabaseConfig {
66            backend: DatabaseBackend::Sqlite,
67            name: Some("test.db".into()),
68            ..DatabaseConfig::default()
69        };
70        let opts = connect_options(&config);
71
72        assert_eq!(opts.get_filename().to_str().expect("valid path"), "test.db");
73    }
74
75    #[test]
76    fn try_from_empty_name_defaults() {
77        let config = DatabaseConfig {
78            backend: DatabaseBackend::Sqlite,
79            name: None,
80            ..DatabaseConfig::default()
81        };
82        let opts = connect_options(&config);
83
84        // Empty string filename — validated elsewhere by Config::validate()
85        assert_eq!(opts.get_filename().to_str().expect("valid path"), "");
86    }
87}