database_mcp_sqlite/
adapter.rs1use std::time::Duration;
7
8use database_mcp_config::DatabaseConfig;
9use sqlx::SqlitePool;
10use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
11use tracing::info;
12
13#[derive(Clone)]
19pub struct SqliteAdapter {
20 pub(crate) config: DatabaseConfig,
21 pub(crate) pool: SqlitePool,
22}
23
24impl std::fmt::Debug for SqliteAdapter {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 f.debug_struct("SqliteAdapter")
27 .field("read_only", &self.config.read_only)
28 .finish_non_exhaustive()
29 }
30}
31
32impl SqliteAdapter {
33 #[must_use]
38 pub fn new(config: &DatabaseConfig) -> Self {
39 let pool = pool_options(config).connect_lazy_with(connect_options(config));
40 let name = config.name.as_deref().unwrap_or_default();
41 info!("SQLite lazy connection pool created: {name}");
42 Self {
43 config: config.clone(),
44 pool,
45 }
46 }
47
48 pub(crate) fn quote_identifier(name: &str) -> String {
50 database_mcp_sql::identifier::quote_identifier(name, '"')
51 }
52}
53
54fn pool_options(config: &DatabaseConfig) -> SqlitePoolOptions {
56 let mut opts = SqlitePoolOptions::new()
57 .max_connections(1) .min_connections(DatabaseConfig::DEFAULT_MIN_CONNECTIONS)
59 .idle_timeout(Duration::from_secs(DatabaseConfig::DEFAULT_IDLE_TIMEOUT_SECS))
60 .max_lifetime(Duration::from_secs(DatabaseConfig::DEFAULT_MAX_LIFETIME_SECS));
61
62 if let Some(timeout) = config.connection_timeout {
63 opts = opts.acquire_timeout(Duration::from_secs(timeout));
64 }
65
66 opts
67}
68
69fn connect_options(config: &DatabaseConfig) -> SqliteConnectOptions {
71 let name = config.name.as_deref().unwrap_or_default();
72 SqliteConnectOptions::new().filename(name)
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use database_mcp_config::DatabaseBackend;
79
80 fn base_config() -> DatabaseConfig {
81 DatabaseConfig {
82 backend: DatabaseBackend::Sqlite,
83 name: Some("test.db".into()),
84 ..DatabaseConfig::default()
85 }
86 }
87
88 #[test]
89 fn pool_options_applies_defaults() {
90 let config = base_config();
91 let opts = pool_options(&config);
92
93 assert_eq!(opts.get_max_connections(), 1, "SQLite must be single-writer");
94 assert_eq!(opts.get_min_connections(), DatabaseConfig::DEFAULT_MIN_CONNECTIONS);
95 assert_eq!(
96 opts.get_idle_timeout(),
97 Some(Duration::from_secs(DatabaseConfig::DEFAULT_IDLE_TIMEOUT_SECS))
98 );
99 assert_eq!(
100 opts.get_max_lifetime(),
101 Some(Duration::from_secs(DatabaseConfig::DEFAULT_MAX_LIFETIME_SECS))
102 );
103 }
104
105 #[test]
106 fn pool_options_applies_connection_timeout() {
107 let config = DatabaseConfig {
108 connection_timeout: Some(7),
109 ..base_config()
110 };
111 let opts = pool_options(&config);
112
113 assert_eq!(opts.get_acquire_timeout(), Duration::from_secs(7));
114 }
115
116 #[test]
117 fn pool_options_without_connection_timeout_uses_sqlx_default() {
118 let config = base_config();
119 let opts = pool_options(&config);
120
121 assert_eq!(opts.get_acquire_timeout(), Duration::from_secs(30));
122 }
123
124 #[test]
125 fn pool_options_ignores_max_pool_size() {
126 let config = DatabaseConfig {
127 max_pool_size: 20,
128 ..base_config()
129 };
130 let opts = pool_options(&config);
131
132 assert_eq!(opts.get_max_connections(), 1, "SQLite must always be single-writer");
133 }
134
135 #[test]
136 fn try_from_sets_filename() {
137 let opts = connect_options(&base_config());
138
139 assert_eq!(opts.get_filename().to_str().expect("valid path"), "test.db");
140 }
141
142 #[test]
143 fn try_from_empty_name_defaults() {
144 let config = DatabaseConfig {
145 name: None,
146 ..base_config()
147 };
148 let opts = connect_options(&config);
149
150 assert_eq!(opts.get_filename().to_str().expect("valid path"), "");
152 }
153
154 #[tokio::test]
155 async fn new_creates_lazy_pool() {
156 let config = base_config();
157 let adapter = SqliteAdapter::new(&config);
158 assert_eq!(adapter.pool.size(), 0);
160 }
161}