1use std::any::Any;
2use std::sync::Arc;
3use std::time::Duration;
4
5use camel_api::datasource::{CheckFuture, CloseFuture, CreatePoolFuture};
6use camel_api::datasource::{DatasourceConfig, DatasourceHandle, PoolFactory};
7use camel_api::error::CamelError;
8use camel_api::lifecycle::HealthStatus;
9use sqlx::AnyPool;
10use sqlx::any::AnyPoolOptions;
11
12use crate::config::{enrich_db_url_with_ssl_params, redact_db_url};
13
14fn is_sqlite_memory_url(url: &str) -> bool {
18 let lowered = url.to_lowercase();
19 lowered.starts_with("sqlite::memory:")
20 || lowered.starts_with("sqlite://:memory:")
21 || (lowered.starts_with("sqlite:") && lowered.contains("mode=memory"))
22}
23
24const IN_FLIGHT_DRAIN_WAIT: Duration = Duration::from_secs(10);
28const IN_FLIGHT_DRAIN_POLL: Duration = Duration::from_millis(5);
30
31pub struct SqlPoolFactory;
32
33impl PoolFactory for SqlPoolFactory {
34 fn create<'a>(&'a self, config: &'a DatasourceConfig) -> CreatePoolFuture<'a> {
35 Box::pin(async move {
36 sqlx::any::install_default_drivers();
39
40 let max_conn = config.max_connections.unwrap_or(5);
41 let min_conn = if is_sqlite_memory_url(&config.db_url) {
50 if config.min_connections.is_some_and(|m| m > 0) {
51 tracing::info!("datasource pool: min_connections ignored for in-memory sqlite");
53 }
54 0
55 } else {
56 config.min_connections.unwrap_or(1)
57 };
58 let idle_timeout = Duration::from_secs(config.idle_timeout_secs.unwrap_or(300));
59 let max_lifetime = Duration::from_secs(config.max_lifetime_secs.unwrap_or(1800));
60
61 let db_url = enrich_db_url_with_ssl_params(
62 &config.db_url,
63 config.ssl_mode.as_deref(),
64 config.ssl_root_cert.as_deref(),
65 config.ssl_cert.as_deref(),
66 config.ssl_key.as_deref(),
67 )?;
68
69 let pool = AnyPoolOptions::new()
70 .max_connections(max_conn)
71 .min_connections(min_conn)
72 .idle_timeout(idle_timeout)
73 .max_lifetime(max_lifetime)
74 .connect(&db_url)
75 .await
76 .map_err(|e| {
77 CamelError::ProcessorError(format!(
78 "failed to create datasource pool ({}): {}",
79 redact_db_url(&config.db_url),
80 e
81 ))
82 })?;
83
84 tracing::info!("datasource pool created: max_connections={}", max_conn);
85 Ok(Arc::new(pool) as Arc<dyn Any + Send + Sync>)
86 })
87 }
88
89 fn check<'a>(&'a self, handle: &'a DatasourceHandle) -> CheckFuture<'a> {
90 Box::pin(async move {
91 match handle.downcast::<AnyPool>() {
92 Ok(pool) => match sqlx::query("SELECT 1").execute(&*pool).await {
93 Ok(_) => HealthStatus::Healthy,
94 Err(e) => {
95 tracing::warn!("datasource '{}' health check failed: {}", handle.name, e);
97 HealthStatus::Unhealthy
98 }
99 },
100 Err(e) => {
101 tracing::warn!(
103 "datasource '{}' health check failed: pool downcast error: {}",
104 handle.name,
105 e
106 );
107 HealthStatus::Unhealthy
108 }
109 }
110 })
111 }
112
113 fn close<'a>(&'a self, handle: &'a DatasourceHandle) -> CloseFuture<'a> {
114 Box::pin(async move {
115 let pool = handle.downcast::<AnyPool>().map_err(|e| {
116 CamelError::ProcessorError(format!(
117 "datasource '{}': pool close downcast failed: {}",
118 handle.name, e
119 ))
120 })?;
121 pool.close().await;
124 let drain_deadline = std::time::Instant::now() + IN_FLIGHT_DRAIN_WAIT;
138 while pool.size() > 0 {
139 if std::time::Instant::now() >= drain_deadline {
140 return Err(CamelError::ProcessorError(format!(
148 "datasource '{}': pool did not drain within {}s ({} connection(s) \
149 still open) — the database may outlive its boot",
150 handle.name,
151 IN_FLIGHT_DRAIN_WAIT.as_secs(),
152 pool.size()
153 )));
154 }
155 tokio::time::sleep(IN_FLIGHT_DRAIN_POLL).await;
161 pool.close().await;
162 }
163 Ok(())
164 })
165 }
166
167 fn supported_schemes(&self) -> &[&str] {
168 &["postgres", "postgresql", "mysql", "sqlite"]
169 }
170
171 fn name(&self) -> &'static str {
172 "sqlx"
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use camel_component_api::test_support::timeout_wait;
180
181 #[test]
182 fn sql_pool_factory_name() {
183 let f = SqlPoolFactory;
184 assert_eq!(f.name(), "sqlx");
185 }
186
187 #[test]
188 fn sql_pool_factory_supported_schemes() {
189 let f = SqlPoolFactory;
190 assert!(f.supported_schemes().contains(&"postgres"));
191 assert!(f.supported_schemes().contains(&"mysql"));
192 assert!(f.supported_schemes().contains(&"sqlite"));
193 }
194
195 #[test]
196 fn sql_pool_factory_matches_postgres() {
197 let f = SqlPoolFactory;
198 let cfg = DatasourceConfig {
199 db_url: "postgres://localhost/test".into(),
200 provider: None,
201 max_connections: None,
202 min_connections: None,
203 idle_timeout_secs: None,
204 max_lifetime_secs: None,
205 ssl_mode: None,
206 ssl_root_cert: None,
207 ssl_cert: None,
208 ssl_key: None,
209 extra: std::collections::HashMap::new(),
210 };
211 assert!(f.matches(&cfg));
212 }
213
214 #[tokio::test]
215 async fn sql_pool_factory_close_closes_the_pool() {
216 let f = SqlPoolFactory;
217 let cfg = DatasourceConfig {
218 db_url: "sqlite::memory:?cache=shared".into(),
219 provider: None,
220 max_connections: None,
221 min_connections: None,
222 idle_timeout_secs: None,
223 max_lifetime_secs: None,
224 ssl_mode: None,
225 ssl_root_cert: None,
226 ssl_cert: None,
227 ssl_key: None,
228 extra: std::collections::HashMap::new(),
229 };
230 let inner = f.create(&cfg).await.unwrap();
231 let pool = Arc::downcast::<AnyPool>(Arc::clone(&inner)).unwrap();
232 let handle = DatasourceHandle::new("appdb".into(), f.name().into(), Arc::clone(&inner));
233
234 f.close(&handle).await.unwrap();
235 assert!(
236 pool.is_closed(),
237 "factory close must drain the sqlx pool (bd rc-25lup.4)"
238 );
239 }
240
241 #[tokio::test]
245 async fn sql_pool_factory_close_drains_pool_to_zero() {
246 let f = SqlPoolFactory;
247 let cfg = DatasourceConfig {
248 db_url: "sqlite:file:memdb_drain_zero?mode=memory&cache=shared".into(),
249 provider: None,
250 max_connections: Some(3),
251 min_connections: None,
252 idle_timeout_secs: None,
253 max_lifetime_secs: None,
254 ssl_mode: None,
255 ssl_root_cert: None,
256 ssl_cert: None,
257 ssl_key: None,
258 extra: std::collections::HashMap::new(),
259 };
260 let inner = f.create(&cfg).await.unwrap();
261 let pool = Arc::downcast::<AnyPool>(Arc::clone(&inner)).unwrap();
262
263 let mut conn1 = timeout_wait(
266 "first pool acquire",
267 Duration::from_secs(30),
268 pool.acquire(),
269 )
270 .await
271 .expect("first connection");
272 let mut conn2 = timeout_wait(
273 "second pool acquire",
274 Duration::from_secs(30),
275 pool.acquire(),
276 )
277 .await
278 .expect("second connection");
279 sqlx::query("SELECT 1")
280 .execute(&mut *conn1)
281 .await
282 .expect("SELECT 1 through first connection");
283 sqlx::query("SELECT 1")
284 .execute(&mut *conn2)
285 .await
286 .expect("SELECT 1 through second connection");
287 drop(conn1);
288 drop(conn2);
289
290 let handle = DatasourceHandle::new("appdb".into(), f.name().into(), inner);
291 f.close(&handle).await.expect("factory close must succeed");
292 assert!(pool.is_closed(), "factory close must leave the pool closed");
293 assert_eq!(
294 pool.size(),
295 0,
296 "factory close must drain the pool to zero connections"
297 );
298 }
299
300 #[tokio::test]
305 async fn named_shared_memory_uri_probe() {
306 use sqlx::Row;
307
308 let f = SqlPoolFactory;
309 let cfg = DatasourceConfig {
310 db_url: "sqlite:file:memdb_probe?mode=memory&cache=shared".into(),
311 provider: None,
312 max_connections: Some(3),
313 min_connections: None,
314 idle_timeout_secs: None,
315 max_lifetime_secs: None,
316 ssl_mode: None,
317 ssl_root_cert: None,
318 ssl_cert: None,
319 ssl_key: None,
320 extra: std::collections::HashMap::new(),
321 };
322 let inner = f.create(&cfg).await.unwrap();
323 let pool = Arc::downcast::<AnyPool>(Arc::clone(&inner)).unwrap();
324
325 sqlx::query("CREATE TABLE probe (v TEXT)")
326 .execute(&*pool)
327 .await
328 .expect("create");
329 let conn1 = timeout_wait(
332 "probe pool acquire",
333 Duration::from_secs(30),
334 pool.acquire(),
335 )
336 .await
337 .expect("conn1");
338 sqlx::query("INSERT INTO probe VALUES ('x')")
339 .execute(&*pool)
340 .await
341 .expect("insert on a second connection");
342 drop(conn1);
343
344 let row = sqlx::query("SELECT COUNT(*) FROM probe")
345 .fetch_one(&*pool)
346 .await
347 .expect("count");
348 let n = row.try_get::<i64, usize>(0).expect("count i64");
349 assert_eq!(
350 n, 1,
351 "named shared memory URI must share across pool connections"
352 );
353 }
354
355 #[test]
359 fn sqlite_memory_url_classifier_table() {
360 let cases: &[(&str, bool)] = &[
361 ("sqlite::memory:", true),
363 ("sqlite://:memory:", true),
364 ("sqlite:file:memdb1?mode=memory&cache=shared", true),
365 ("SQLITE::MEMORY:", true),
367 ("Sqlite:file:MEMDB2?MODE=MEMORY", true),
368 ("sqlite:file:demo_mode=memory.db", true),
370 ("sqlite:data.db", false),
372 ("sqlite:file:memory.db", false),
373 ("postgres://host/db?mode=memory", false),
374 ("sqlite:file::memory:?cache=shared", false),
377 ];
378 for (url, expected) in cases {
379 assert_eq!(
380 is_sqlite_memory_url(url),
381 *expected,
382 "classifier mismatch for {url:?}"
383 );
384 }
385 }
386}