1use std::future::Future;
2use std::pin::Pin;
3use std::sync::Arc;
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::task::{Context, Poll};
6use std::time::Duration;
7
8use bytes::Bytes;
9use camel_api::datasource::DatasourceCatalog;
10use serde_json::json;
11use sqlx::AnyPool;
12use sqlx::any::AnyRow;
13use sqlx::pool::PoolOptions;
14use tokio::sync::OnceCell;
15use tower::Service;
16use tracing::{debug, error, info, warn};
17
18use crate::config::{SqlEndpointConfig, SqlOutputType, enrich_db_url_with_ssl, redact_db_url};
19use crate::headers;
20use crate::query::{PreparedQuery, is_select_query, parse_query_template, resolve_params};
21use crate::utils::{bind_json_values, is_retryable_sqlx_error, row_to_json};
22use camel_component_api::retry_async;
23use camel_component_api::{
24 Body, CamelError, Exchange, Message, RuntimeObservability, StreamBody, StreamMetadata,
25};
26
27#[derive(Clone)]
28pub struct SqlProducer {
29 pub(crate) config: SqlEndpointConfig,
30 pub(crate) pool: Arc<OnceCell<Arc<AnyPool>>>,
31 pub(crate) catalog: Option<Arc<dyn DatasourceCatalog>>,
32 pub(crate) stopped: Arc<AtomicBool>,
33 pub(crate) runtime: Arc<dyn RuntimeObservability>,
34 pub(crate) route_id: String,
36}
37
38impl SqlProducer {
39 pub fn new(
40 config: SqlEndpointConfig,
41 pool: Arc<OnceCell<Arc<AnyPool>>>,
42 catalog: Option<Arc<dyn DatasourceCatalog>>,
43 runtime: Arc<dyn RuntimeObservability>,
44 route_id: impl Into<String>,
45 ) -> Self {
46 Self {
47 config,
48 pool,
49 catalog,
50 stopped: Arc::new(AtomicBool::new(false)),
51 runtime,
52 route_id: route_id.into(),
53 }
54 }
55
56 pub fn stop(&self) {
57 self.stopped.store(true, Ordering::Relaxed);
58 if let Some(pool) = self.pool.get() {
60 let pool = pool.clone();
61 tokio::spawn(async move {
62 if tokio::time::timeout(Duration::from_secs(5), pool.close())
63 .await
64 .is_err()
65 {
66 tracing::warn!("SQL producer pool did not close within 5s");
67 }
68 });
69 }
70 }
71
72 pub(crate) fn resolve_query_source(exchange: &Exchange, config: &SqlEndpointConfig) -> String {
81 if config.allow_dynamic_query {
82 if let Some(query_value) = exchange.input.header(headers::QUERY)
84 && let Some(query_str) = query_value.as_str()
85 {
86 return query_str.to_string();
87 }
88
89 if config.use_message_body_for_sql
91 && let Some(body_text) = exchange.input.body.as_text()
92 {
93 return body_text.to_string();
94 }
95 }
96
97 config.query.clone()
99 }
100
101 pub async fn check_connection(&self) -> Result<(), CamelError> {
107 let pool =
108 if let (Some(catalog), Some(ds_name)) = (&self.catalog, &self.config.datasource_name) {
109 let handle = catalog.get_pool(ds_name).await?;
110 let arc_pool: Arc<AnyPool> = handle.downcast()?;
111 arc_pool
113 } else {
114 self.pool
115 .get()
116 .ok_or_else(|| {
117 CamelError::ProcessorError("SQL connection pool not initialized".into())
118 })?
119 .clone()
120 };
121
122 debug!("Running health check: SELECT 1");
123 sqlx::query("SELECT 1").execute(&*pool).await.map_err(|e| {
124 warn!(error = %e, "SQL health check failed");
125 CamelError::ProcessorError(format!("SQL health check failed: {}", e))
126 })?;
127
128 debug!("SQL health check passed");
129 Ok(())
130 }
131}
132
133impl Service<Exchange> for SqlProducer {
134 type Response = Exchange;
135 type Error = CamelError;
136 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
137
138 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
139 if self.stopped.load(Ordering::Relaxed) {
140 return Poll::Ready(Err(CamelError::ProcessorError(
141 "SQL producer stopped".into(),
142 )));
143 }
144 if let Some(pool) = self.pool.get()
145 && pool.is_closed()
146 {
147 return Poll::Ready(Err(CamelError::ProcessorError(
148 "SQL connection pool is closed".into(),
149 )));
150 }
151 Poll::Ready(Ok(()))
152 }
153
154 fn call(&mut self, mut exchange: Exchange) -> Self::Future {
155 let mut config = self.config.clone();
156 let pool_cell = Arc::clone(&self.pool);
157 let catalog = self.catalog.clone();
158 let runtime = Arc::clone(&self.runtime);
159 let route_id = self.route_id.clone();
160
161 Box::pin(async move {
162 config.resolve_defaults();
164 config.resolve_file_query().await?;
165
166 let pool: &Arc<AnyPool> = pool_cell
168 .get_or_try_init(|| async {
169 let ds_name = config.datasource_name.clone();
171 if let (Some(ref cat), Some(ref name)) = (catalog, ds_name) {
172 let handle = cat.get_pool(name).await?;
173 return handle.downcast::<AnyPool>();
174 }
175
176 let db_url = enrich_db_url_with_ssl(&config.db_url, &config)?;
177
178 sqlx::any::install_default_drivers();
181
182 let max_conn = config.max_connections.ok_or_else(|| {
183 CamelError::Config("max_connections not resolved for SQL pool".into())
184 })?;
185 let min_conn = config.min_connections.ok_or_else(|| {
186 CamelError::Config("min_connections not resolved for SQL pool".into())
187 })?;
188 let idle_timeout = config.idle_timeout_secs.ok_or_else(|| {
189 CamelError::Config("idle_timeout_secs not resolved for SQL pool".into())
190 })?;
191 let max_lifetime = config.max_lifetime_secs.ok_or_else(|| {
192 CamelError::Config("max_lifetime_secs not resolved for SQL pool".into())
193 })?;
194
195 info!(
196 db_url = %redact_db_url(&config.db_url),
197 "SQL producer pool initializing"
198 );
199 let retry_policy = &config.retry;
200 let pool = retry_async::<_, _, _, _, sqlx::Error>(
201 retry_policy,
202 "sql",
203 "producer-pool-init",
204 || {
205 async {
206 PoolOptions::new()
207 .max_connections(max_conn)
208 .min_connections(min_conn)
209 .idle_timeout(Duration::from_secs(idle_timeout))
210 .max_lifetime(Duration::from_secs(max_lifetime))
211 .connect(&db_url)
212 .await
213 }
214 },
215 is_retryable_sqlx_error,
216 Some(runtime.metrics().as_ref()),
217 )
218 .await
219 .map_err(|e| {
220 runtime.health().force_unhealthy_for_route(
221 &route_id,
222 "g:sql:producer-pool-init",
223 &e.to_string(),
224 );
225 error!(error = %e, db_url = %redact_db_url(&config.db_url), "Failed to connect to database");
227 CamelError::EndpointCreationFailed(format!(
228 "Failed to connect to database: {}",
229 e
230 ))
231 })?;
232 Ok(Arc::new(pool))
233 })
234 .await
235 .map_err(|e: CamelError| e.clone())?;
236
237 let query_str = Self::resolve_query_source(&exchange, &config);
239
240 if config.transaction_mode == crate::config::TransactionMode::Managed {
242 warn!("transactionManager not yet implemented; using Auto mode");
243 }
244
245 debug!(
246 query = %query_str,
247 "executing SQL query"
248 );
249
250 if config.batch {
252 execute_batch(pool.as_ref(), &config, &mut exchange).await?;
254 } else if config.use_placeholder {
255 let template = parse_query_template(&query_str, config.placeholder)?;
257 let mut prepared = resolve_params(&template, &exchange, &config.in_separator)?;
258
259 if let Some(params_value) = exchange.input.header(headers::PARAMETERS) {
261 if let Some(arr) = params_value.as_array() {
262 if arr.len() != prepared.bindings.len() {
263 warn!(
264 expected = prepared.bindings.len(),
265 got = arr.len(),
266 header = headers::PARAMETERS,
267 "Parameter count mismatch — SQL has {} placeholders but header provides {} values",
268 prepared.bindings.len(),
269 arr.len()
270 );
271 }
272 debug!(
273 "Overriding bindings from {} header with {} parameters",
274 headers::PARAMETERS,
275 arr.len()
276 );
277 prepared.bindings = arr.clone();
278 } else {
279 warn!(
280 header = headers::PARAMETERS,
281 "Header is present but not a JSON array — ignoring parameter override"
282 );
283 }
284 }
285
286 debug!(
287 "Executing prepared SQL ({} bindings)",
288 prepared.bindings.len()
289 );
290
291 if is_select_query(&prepared.sql) {
292 execute_select(pool.as_ref(), &prepared, &config, &mut exchange).await?;
293 } else {
294 execute_modify(pool.as_ref(), &prepared, &config, &mut exchange).await?;
295 }
296 } else {
297 debug!("Executing raw SQL (placeholder processing disabled)");
299 let prepared = PreparedQuery {
300 sql: query_str,
301 bindings: vec![],
302 };
303
304 if is_select_query(&prepared.sql) {
305 execute_select(pool.as_ref(), &prepared, &config, &mut exchange).await?;
306 } else {
307 execute_modify(pool.as_ref(), &prepared, &config, &mut exchange).await?;
308 }
309 }
310
311 Ok(exchange)
312 })
313 }
314}
315
316async fn execute_select(
318 pool: &AnyPool,
319 prepared: &PreparedQuery,
320 config: &SqlEndpointConfig,
321 exchange: &mut Exchange,
322) -> Result<(), CamelError> {
323 match config.output_type {
324 SqlOutputType::SelectOne => {
325 let mut query = sqlx::query(&prepared.sql);
327 query = bind_json_values(query, &prepared.bindings);
328
329 let rows: Vec<AnyRow> = query.fetch_all(pool).await.map_err(|e| {
330 warn!(error = %e, "SQL query failed");
331 CamelError::ProcessorError(format!("Query execution failed: {}", e))
332 })?;
333
334 let count = rows.len();
335 debug!(rows = count, "SQL query completed");
336 let json_rows: Vec<serde_json::Value> = rows
337 .iter()
338 .map(row_to_json)
339 .collect::<Result<Vec<_>, _>>()?;
340
341 if let Some(first_row) = json_rows.into_iter().next() {
342 exchange.input.body = Body::Json(first_row);
343 } else {
344 exchange.input.body = Body::Empty;
345 }
346 debug!("SelectOne returned {} row", if count > 0 { 1 } else { 0 });
347 exchange
348 .input
349 .set_header(headers::ROW_COUNT, serde_json::json!(count));
350 }
351 SqlOutputType::SelectList => {
352 let mut query = sqlx::query(&prepared.sql);
354 query = bind_json_values(query, &prepared.bindings);
355
356 let rows: Vec<AnyRow> = query.fetch_all(pool).await.map_err(|e| {
357 warn!(error = %e, "SQL query failed");
358 CamelError::ProcessorError(format!("Query execution failed: {}", e))
359 })?;
360
361 let count = rows.len();
362 debug!(rows = count, "SQL query completed");
363 let json_rows: Vec<serde_json::Value> = rows
364 .iter()
365 .map(row_to_json)
366 .collect::<Result<Vec<_>, _>>()?;
367
368 exchange.input.body = Body::Json(serde_json::Value::Array(json_rows));
369 debug!("SelectList returned {} rows", count);
370 exchange
371 .input
372 .set_header(headers::ROW_COUNT, serde_json::json!(count));
373 }
374 SqlOutputType::StreamList => {
375 use futures::TryStreamExt;
377
378 let pool_clone = pool.clone();
379 let sql_str = prepared.sql.clone();
380 let bindings = prepared.bindings.clone();
381
382 let byte_stream = async_stream::try_stream! {
384 let mut q = sqlx::query(&sql_str);
385 q = bind_json_values(q, &bindings);
386 let mut rows = q.fetch(&pool_clone);
387 while let Some(row) = rows.try_next().await.map_err(|e| {
388 CamelError::ProcessorError(format!("Query execution failed: {}", e))
389 })? {
390 let json_val = row_to_json(&row).map_err(|e| {
391 CamelError::ProcessorError(format!("JSON serialization failed: {}", e))
392 })?;
393 let mut bytes = serde_json::to_vec(&json_val)
394 .map_err(|e| CamelError::ProcessorError(format!("JSON serialization failed: {}", e)))?;
395 bytes.push(b'\n');
396 yield Bytes::from(bytes);
397 }
398 };
399
400 exchange.input.body = Body::Stream(StreamBody {
401 stream: Arc::new(tokio::sync::Mutex::new(Some(Box::pin(byte_stream)))),
402 metadata: StreamMetadata {
403 content_type: Some("application/x-ndjson".to_string()),
404 size_hint: None,
405 origin: None,
406 },
407 });
408 debug!("StreamList: created lazy stream (rows fetched on demand)");
409 }
411 }
412
413 Ok(())
414}
415
416async fn execute_modify(
418 pool: &AnyPool,
419 prepared: &PreparedQuery,
420 config: &SqlEndpointConfig,
421 exchange: &mut Exchange,
422) -> Result<(), CamelError> {
423 let mut query = sqlx::query(&prepared.sql);
424 query = bind_json_values(query, &prepared.bindings);
425
426 let result = query.execute(pool).await.map_err(|e| {
427 warn!(error = %e, "SQL query failed");
428 CamelError::ProcessorError(format!("Query execution failed: {}", e))
429 })?;
430
431 let rows_affected = result.rows_affected();
432
433 if let Some(expected) = config.expected_update_count
435 && rows_affected as i64 != expected
436 {
437 warn!(expected, actual = rows_affected, "Row count mismatch");
438 return Err(CamelError::ProcessorError(format!(
439 "Expected {} rows affected, got {}",
440 expected, rows_affected
441 )));
442 }
443
444 exchange
445 .input
446 .set_header(headers::UPDATE_COUNT, serde_json::json!(rows_affected));
447
448 if config.noop {
449 } else {
451 exchange.input.body = Body::Json(json!({ "rowsAffected": rows_affected }));
452 }
453
454 debug!(rows = rows_affected, "SQL modify query completed");
455
456 Ok(())
457}
458
459async fn execute_batch(
461 pool: &AnyPool,
462 config: &SqlEndpointConfig,
463 exchange: &mut Exchange,
464) -> Result<(), CamelError> {
465 let body_json = match &exchange.input.body {
467 Body::Json(val) => val,
468 _ => {
469 return Err(CamelError::ProcessorError(
470 "Batch mode requires body to be a JSON array of arrays".to_string(),
471 ));
472 }
473 };
474
475 let batch_data = body_json
476 .as_array()
477 .ok_or_else(|| {
478 CamelError::ProcessorError("Batch mode requires body to be a JSON array".to_string())
479 })?
480 .clone();
481
482 let template = parse_query_template(&config.query, config.placeholder)?;
484
485 let mut tx = pool.begin().await.map_err(|e| {
487 warn!(error = %e, "Failed to begin transaction");
490 CamelError::ProcessorError(format!("Failed to begin transaction: {}", e))
491 })?;
492
493 let mut total_rows_affected: u64 = 0;
494
495 for (batch_idx, params_array) in batch_data.into_iter().enumerate() {
496 params_array.as_array().ok_or_else(|| {
498 CamelError::ProcessorError(format!(
499 "Batch item at index {} must be a JSON array of parameters",
500 batch_idx
501 ))
502 })?;
503
504 let temp_msg = Message::new(Body::Json(params_array.clone()));
506 let temp_exchange = Exchange::new(temp_msg);
507
508 let prepared = resolve_params(&template, &temp_exchange, &config.in_separator)?;
510
511 let mut query = sqlx::query(&prepared.sql);
513 query = bind_json_values(query, &prepared.bindings);
514
515 let result = query.execute(&mut *tx).await.map_err(|e| {
516 warn!("Batch query execution failed at index {}: {}", batch_idx, e);
519 CamelError::ProcessorError(format!("Batch query execution failed: {}", e))
520 })?;
521
522 if let Some(expected) = config.expected_update_count
524 && result.rows_affected() as i64 != expected
525 {
526 warn!(
529 "Batch item {}: expected {} rows affected, got {}",
530 batch_idx,
531 expected,
532 result.rows_affected()
533 );
534 return Err(CamelError::ProcessorError(format!(
535 "Batch item {}: expected {} rows affected, got {}",
536 batch_idx,
537 expected,
538 result.rows_affected()
539 )));
540 }
541
542 total_rows_affected += result.rows_affected();
543 }
544
545 tx.commit().await.map_err(|e| {
547 warn!(error = %e, "Failed to commit transaction");
550 CamelError::ProcessorError(format!("Failed to commit transaction: {}", e))
551 })?;
552
553 exchange.input.set_header(
554 headers::UPDATE_COUNT,
555 serde_json::json!(total_rows_affected),
556 );
557
558 debug!(
559 "Batch execution completed, total rows affected: {}",
560 total_rows_affected
561 );
562
563 Ok(())
564}
565
566#[cfg(test)]
567mod tests {
568 use super::*;
569 use camel_api::MetricsCollector;
570 use camel_component_api::HealthCheckRegistry;
571 use camel_component_api::test_support::PanicRuntimeObservability;
572 fn test_rt() -> std::sync::Arc<dyn camel_component_api::RuntimeObservability> {
573 std::sync::Arc::new(PanicRuntimeObservability)
574 }
575 use camel_component_api::Message;
576 use camel_component_api::UriConfig;
577 use sqlx::any::AnyPoolOptions;
578 use std::sync::Arc;
579 use std::sync::Mutex;
580 use tokio::sync::OnceCell;
581
582 #[derive(Debug, Default)]
585 struct RecordingHealth {
586 forced: Mutex<Vec<(String, String, String)>>,
587 }
588
589 impl HealthCheckRegistry for RecordingHealth {
590 fn force_unhealthy_for_route(&self, route_id: &str, name: &str, reason: &str) {
591 self.forced.lock().unwrap().push((
592 route_id.to_string(),
593 name.to_string(),
594 reason.to_string(),
595 ));
596 }
597 }
598
599 struct NoopMetrics;
600
601 impl MetricsCollector for NoopMetrics {
602 fn record_exchange_duration(&self, _: &str, _: Duration) {}
603 fn increment_errors(&self, _: &str, _: &str) {}
604 fn increment_exchanges(&self, _: &str) {}
605 fn set_queue_depth(&self, _: &str, _: usize) {}
606 fn record_circuit_breaker_change(&self, _: &str, _: &str, _: &str) {}
607 }
608
609 struct RecordingRuntime {
610 health: Arc<RecordingHealth>,
611 }
612
613 impl RuntimeObservability for RecordingRuntime {
614 fn metrics(&self) -> Arc<dyn MetricsCollector> {
615 Arc::new(NoopMetrics)
616 }
617 fn health(&self) -> Arc<dyn HealthCheckRegistry> {
618 self.health.clone()
619 }
620 }
621
622 async fn sqlite_pool() -> AnyPool {
623 sqlx::any::install_default_drivers();
624 tokio::time::timeout(
627 std::time::Duration::from_secs(10),
628 AnyPoolOptions::new()
629 .max_connections(1)
630 .connect("sqlite::memory:"),
631 )
632 .await
633 .expect("sqlite pool connect timed out after 10s")
634 .expect("sqlite pool")
635 }
636
637 async fn seed_items_table(pool: &AnyPool) {
638 sqlx::query(
639 "CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT, done INTEGER DEFAULT 0)",
640 )
641 .execute(pool)
642 .await
643 .expect("create table");
644 sqlx::query("INSERT INTO items (id, name, done) VALUES (1, 'a', 0), (2, 'b', 0)")
645 .execute(pool)
646 .await
647 .expect("seed rows");
648 }
649
650 fn config() -> SqlEndpointConfig {
651 let mut c =
652 SqlEndpointConfig::from_uri("sql:select 1?db_url=postgres://localhost/test").unwrap();
653 c.resolve_defaults();
654 c
655 }
656
657 #[test]
658 fn producer_clone_shares_pool() {
659 let p1 = SqlProducer::new(
660 config(),
661 Arc::new(OnceCell::new()),
662 None,
663 test_rt(),
664 "sql-producer-test-route",
665 );
666 let p2 = p1.clone();
667 assert!(Arc::ptr_eq(&p1.pool, &p2.pool));
668 assert!(Arc::ptr_eq(&p1.stopped, &p2.stopped));
669 }
670
671 #[test]
672 fn resolve_query_from_config() {
673 let config = config();
674 let ex = Exchange::new(Message::default());
675 let q = SqlProducer::resolve_query_source(&ex, &config);
676 assert_eq!(q, "select 1");
677 }
678
679 #[test]
680 fn resolve_query_from_header() {
681 let mut config = config();
682 config.allow_dynamic_query = true;
683 let mut msg = Message::default();
684 msg.set_header(headers::QUERY, serde_json::json!("select 2"));
685 let ex = Exchange::new(msg);
686 let q = SqlProducer::resolve_query_source(&ex, &config);
687 assert_eq!(q, "select 2");
688 }
689
690 #[test]
691 fn resolve_query_from_body() {
692 let mut config = config();
693 config.use_message_body_for_sql = true;
694 config.allow_dynamic_query = true;
695 let msg = Message::new(Body::Text("select 3".to_string()));
696 let ex = Exchange::new(msg);
697 let q = SqlProducer::resolve_query_source(&ex, &config);
698 assert_eq!(q, "select 3");
699 }
700
701 #[test]
702 fn resolve_query_header_priority_over_body() {
703 let mut config = config();
704 config.use_message_body_for_sql = true;
705 config.allow_dynamic_query = true;
706 let mut msg = Message::new(Body::Text("select from body".to_string()));
707 msg.set_header(headers::QUERY, serde_json::json!("select from header"));
708 let ex = Exchange::new(msg);
709 let q = SqlProducer::resolve_query_source(&ex, &config);
710 assert_eq!(q, "select from header");
711 }
712
713 #[test]
714 fn resolve_query_body_priority_over_config() {
715 let mut config = config();
716 config.use_message_body_for_sql = true;
717 config.allow_dynamic_query = true;
718 let msg = Message::new(Body::Text("select from body".to_string()));
719 let ex = Exchange::new(msg);
720 let q = SqlProducer::resolve_query_source(&ex, &config);
721 assert_eq!(q, "select from body");
722 }
723
724 #[test]
728 fn dynamic_query_denied_by_default() {
729 let mut msg = Message::default();
730 msg.set_header(headers::QUERY, "DROP TABLE users; --");
731 let ex = Exchange::new(msg);
732
733 let mut config = config();
734 config.query = "SELECT 1".to_string();
735 let q = SqlProducer::resolve_query_source(&ex, &config);
738 assert_eq!(q, "SELECT 1");
740 }
741
742 #[test]
744 fn dynamic_query_allowed_with_opt_in() {
745 let mut msg = Message::default();
746 msg.set_header(headers::QUERY, "SELECT 1 FROM users WHERE id = :#id");
747 let ex = Exchange::new(msg);
748
749 let mut config = config();
750 config.query = "SELECT 1".to_string();
751 config.allow_dynamic_query = true;
752
753 let q = SqlProducer::resolve_query_source(&ex, &config);
754 assert_eq!(q, "SELECT 1 FROM users WHERE id = :#id");
756 }
757
758 #[test]
759 fn bind_json_null() {
760 let query = sqlx::query("SELECT ?");
761 let values = vec![serde_json::Value::Null];
762 let _bound = bind_json_values(query, &values);
763 }
765
766 #[test]
767 fn bind_json_bool() {
768 let query = sqlx::query("SELECT ?");
769 let values = vec![serde_json::Value::Bool(true)];
770 let _bound = bind_json_values(query, &values);
771 }
772
773 #[test]
774 fn bind_json_number_i64() {
775 let query = sqlx::query("SELECT ?");
776 let values = vec![serde_json::json!(42)];
777 let _bound = bind_json_values(query, &values);
778 }
779
780 #[test]
781 fn bind_json_number_f64() {
782 let query = sqlx::query("SELECT ?");
783 let values = vec![serde_json::json!(std::f64::consts::PI)];
784 let _bound = bind_json_values(query, &values);
785 }
786
787 #[test]
788 fn bind_json_string() {
789 let query = sqlx::query("SELECT ?");
790 let values = vec![serde_json::json!("hello world")];
791 let _bound = bind_json_values(query, &values);
792 }
793
794 #[test]
795 fn bind_json_array() {
796 let query = sqlx::query("SELECT ?");
797 let values = vec![serde_json::json!([1, 2, 3])];
798 let _bound = bind_json_values(query, &values);
799 }
800
801 #[test]
802 fn bind_json_object() {
803 let query = sqlx::query("SELECT ?");
804 let values = vec![serde_json::json!({"key": "value"})];
805 let _bound = bind_json_values(query, &values);
806 }
807
808 #[test]
809 fn bind_multiple_values() {
810 let query = sqlx::query("SELECT ?, ?, ?");
811 let values = vec![
812 serde_json::json!(1),
813 serde_json::json!("test"),
814 serde_json::Value::Null,
815 ];
816 let _bound = bind_json_values(query, &values);
817 }
818
819 #[test]
821 fn expected_update_count_validation() {
822 let config = SqlEndpointConfig::from_uri(
824 "sql:update t set x=1?db_url=postgres://localhost/test&expectedUpdateCount=5",
825 )
826 .unwrap();
827 assert_eq!(config.expected_update_count, Some(5));
828
829 let config_default = self::config();
831 assert_eq!(config_default.expected_update_count, None);
832
833 let config_neg = SqlEndpointConfig::from_uri(
835 "sql:update t set x=1?db_url=postgres://localhost/test&expectedUpdateCount=-1",
836 )
837 .unwrap();
838 assert_eq!(config_neg.expected_update_count, Some(-1));
839 }
840
841 #[test]
843 fn parameters_header_override_logic() {
844 let mut prepared = PreparedQuery {
846 sql: "SELECT * FROM t WHERE id = $1".to_string(),
847 bindings: vec![serde_json::json!(42)],
848 };
849
850 let header_params = serde_json::json!([99, "extra"]);
852 if let Some(arr) = header_params.as_array() {
853 prepared.bindings = arr.clone();
854 }
855
856 assert_eq!(prepared.bindings.len(), 2);
858 assert_eq!(prepared.bindings[0], serde_json::json!(99));
859 assert_eq!(prepared.bindings[1], serde_json::json!("extra"));
860
861 let mut prepared2 = PreparedQuery {
863 sql: "SELECT * FROM t WHERE id = $1".to_string(),
864 bindings: vec![serde_json::json!(42)],
865 };
866 let header_non_array = serde_json::json!({"not": "an array"});
867 if let Some(arr) = header_non_array.as_array() {
868 prepared2.bindings = arr.clone();
869 }
870 assert_eq!(prepared2.bindings.len(), 1);
872 assert_eq!(prepared2.bindings[0], serde_json::json!(42));
873 }
874
875 #[tokio::test]
876 async fn execute_select_one_sets_body_and_row_count() {
877 let pool = sqlite_pool().await;
878 seed_items_table(&pool).await;
879
880 let mut config = SqlEndpointConfig::from_uri(
881 "sql:select id, name from items order by id?db_url=sqlite::memory:&outputType=SelectOne",
882 )
883 .unwrap();
884 config.resolve_defaults();
885
886 let prepared = PreparedQuery {
887 sql: "select id, name from items order by id".to_string(),
888 bindings: vec![],
889 };
890 let mut exchange = Exchange::new(Message::default());
891
892 execute_select(&pool, &prepared, &config, &mut exchange)
893 .await
894 .expect("select one");
895
896 assert_eq!(exchange.input.header(headers::ROW_COUNT), Some(&json!(2)));
897 assert_eq!(
898 exchange.input.body,
899 Body::Json(json!({"id": 1, "name": "a"}))
900 );
901 }
902
903 #[tokio::test]
904 async fn execute_stream_list_materializes_ndjson() {
905 let pool = sqlite_pool().await;
906 seed_items_table(&pool).await;
907
908 let mut config = SqlEndpointConfig::from_uri(
909 "sql:select id from items order by id?db_url=sqlite::memory:&outputType=StreamList",
910 )
911 .unwrap();
912 config.resolve_defaults();
913
914 let prepared = PreparedQuery {
915 sql: "select id from items order by id".to_string(),
916 bindings: vec![],
917 };
918 let mut exchange = Exchange::new(Message::default());
919
920 execute_select(&pool, &prepared, &config, &mut exchange)
921 .await
922 .expect("stream list");
923
924 let bytes = exchange
925 .input
926 .body
927 .clone()
928 .into_bytes(1024)
929 .await
930 .expect("stream bytes");
931 let text = String::from_utf8(bytes.to_vec()).expect("utf8");
932 assert!(text.contains("{\"id\":1}"));
933 assert!(text.contains("{\"id\":2}"));
934 assert_eq!(exchange.input.header(headers::ROW_COUNT), None);
935 }
936
937 #[tokio::test]
938 async fn execute_modify_expected_update_count_mismatch_returns_error() {
939 let pool = sqlite_pool().await;
940 seed_items_table(&pool).await;
941
942 let mut config = SqlEndpointConfig::from_uri(
943 "sql:update items set done=1 where id = #?db_url=sqlite::memory:&expectedUpdateCount=2",
944 )
945 .unwrap();
946 config.resolve_defaults();
947
948 let prepared = PreparedQuery {
949 sql: "update items set done=1 where id = $1".to_string(),
950 bindings: vec![json!(1)],
951 };
952 let mut exchange = Exchange::new(Message::default());
953
954 let err = execute_modify(&pool, &prepared, &config, &mut exchange)
955 .await
956 .expect_err("must fail due expected row count mismatch");
957 assert!(err.to_string().contains("Expected 2 rows affected, got 1"));
958 }
959
960 #[tokio::test]
961 async fn execute_batch_rollback_when_any_item_fails_expected_count() {
962 let pool = sqlite_pool().await;
963 seed_items_table(&pool).await;
964
965 let mut config = SqlEndpointConfig::from_uri(
966 "sql:update items set done=1 where id = #?db_url=sqlite::memory:&batch=true&expectedUpdateCount=1",
967 )
968 .unwrap();
969 config.resolve_defaults();
970
971 let mut exchange = Exchange::new(Message::new(Body::Json(json!([[1], [999]]))));
972
973 let err = execute_batch(&pool, &config, &mut exchange)
974 .await
975 .expect_err("second batch item should fail expectedUpdateCount");
976 assert!(
977 err.to_string()
978 .contains("Batch item 1: expected 1 rows affected, got 0")
979 );
980
981 let row = sqlx::query("select done from items where id = 1")
982 .fetch_one(&pool)
983 .await
984 .expect("query row");
985 let done: i64 = sqlx::Row::try_get(&row, 0).expect("done column");
986 assert_eq!(done, 0, "transaction must rollback first update");
987 }
988
989 #[tokio::test]
995 async fn producer_no_panic_without_prior_resolve_defaults() {
996 let config = SqlEndpointConfig::from_uri("sql:select 1?db_url=sqlite::memory:").unwrap();
998 assert!(config.max_connections.is_none());
999
1000 let mut producer = SqlProducer::new(
1001 config,
1002 Arc::new(OnceCell::new()),
1003 None,
1004 std::sync::Arc::new(camel_component_api::test_support::NoopRuntimeObservability),
1007 "sql-producer-test-route",
1008 );
1009 let exchange = Exchange::new(Message::default());
1010
1011 let result = producer.call(exchange).await;
1013 assert!(
1014 result.is_ok(),
1015 "Producer should initialize pool without panic, got: {:?}",
1016 result
1017 );
1018 }
1019
1020 #[tokio::test]
1022 async fn producer_pool_init_returns_config_error_for_invalid_db() {
1023 let mut config = SqlEndpointConfig::from_uri(
1027 "sql:select 1?db_url=postgres://nonexistent-host:5432/nonexistent_db&retryEnabled=false",
1028 )
1029 .unwrap();
1030 config.max_connections = Some(1);
1032 config.min_connections = Some(0);
1033 config.idle_timeout_secs = Some(300);
1034 config.max_lifetime_secs = Some(1800);
1035
1036 let health = Arc::new(RecordingHealth::default());
1037 let rt: Arc<dyn RuntimeObservability> = Arc::new(RecordingRuntime {
1038 health: health.clone(),
1039 });
1040 let mut producer = SqlProducer::new(
1041 config,
1042 Arc::new(OnceCell::new()),
1043 None,
1044 rt,
1045 "sql-producer-test-route",
1046 );
1047 let exchange = Exchange::new(Message::default());
1048
1049 let result = producer.call(exchange).await;
1050 assert!(result.is_err());
1051 let err_msg = result.unwrap_err().to_string();
1053 assert!(
1054 err_msg.contains("Failed to connect") || err_msg.contains("database"),
1055 "Expected connection error, got: {}",
1056 err_msg
1057 );
1058 let forced = health.forced.lock().unwrap();
1060 assert_eq!(
1061 forced.len(),
1062 1,
1063 "expected one force_unhealthy_for_route call"
1064 );
1065 assert_eq!(forced[0].0, "sql-producer-test-route");
1066 assert_eq!(forced[0].1, "g:sql:producer-pool-init");
1067 }
1068
1069 #[test]
1071 fn poll_ready_returns_ready_for_uninitialized_pool() {
1072 let config = {
1073 let mut c = SqlEndpointConfig::from_uri("sql:select 1?db_url=sqlite::memory:").unwrap();
1074 c.resolve_defaults();
1075 c
1076 };
1077 let mut producer = SqlProducer::new(
1078 config,
1079 Arc::new(OnceCell::new()),
1080 None,
1081 test_rt(),
1082 "sql-producer-test-route",
1083 );
1084 let mut cx = Context::from_waker(futures::task::noop_waker_ref());
1085 let result = producer.poll_ready(&mut cx);
1086 assert!(matches!(result, Poll::Ready(Ok(()))));
1087 }
1088
1089 #[test]
1091 fn poll_ready_returns_error_when_stopped() {
1092 let config = {
1093 let mut c = SqlEndpointConfig::from_uri("sql:select 1?db_url=sqlite::memory:").unwrap();
1094 c.resolve_defaults();
1095 c
1096 };
1097 let mut producer = SqlProducer::new(
1098 config,
1099 Arc::new(OnceCell::new()),
1100 None,
1101 test_rt(),
1102 "sql-producer-test-route",
1103 );
1104 producer.stop();
1105 let mut cx = Context::from_waker(futures::task::noop_waker_ref());
1106 let result = producer.poll_ready(&mut cx);
1107 assert!(matches!(result, Poll::Ready(Err(_))));
1108 let err_msg = match result {
1109 Poll::Ready(Err(e)) => e.to_string(),
1110 _ => unreachable!(),
1111 };
1112 assert!(err_msg.contains("SQL producer stopped"));
1113 }
1114
1115 #[tokio::test]
1117 async fn poll_ready_returns_error_when_pool_closed() {
1118 let pool = sqlite_pool().await;
1119 pool.close().await;
1120
1121 let config = {
1122 let mut c = SqlEndpointConfig::from_uri("sql:select 1?db_url=sqlite::memory:").unwrap();
1123 c.resolve_defaults();
1124 c
1125 };
1126 let pool_cell = Arc::new(OnceCell::new());
1127 pool_cell.set(Arc::new(pool)).unwrap();
1128
1129 let mut producer = SqlProducer::new(
1130 config,
1131 pool_cell,
1132 None,
1133 test_rt(),
1134 "sql-producer-test-route",
1135 );
1136 let mut cx = Context::from_waker(futures::task::noop_waker_ref());
1137 let result = producer.poll_ready(&mut cx);
1138 assert!(matches!(result, Poll::Ready(Err(_))));
1139 let err_msg = match result {
1140 Poll::Ready(Err(e)) => e.to_string(),
1141 _ => unreachable!(),
1142 };
1143 assert!(err_msg.contains("SQL connection pool is closed"));
1144 }
1145
1146 #[tokio::test]
1148 async fn poll_ready_returns_ok_for_healthy_pool() {
1149 let pool = sqlite_pool().await;
1150
1151 let config = {
1152 let mut c = SqlEndpointConfig::from_uri("sql:select 1?db_url=sqlite::memory:").unwrap();
1153 c.resolve_defaults();
1154 c
1155 };
1156 let pool_cell = Arc::new(OnceCell::new());
1157 pool_cell.set(Arc::new(pool)).unwrap();
1158
1159 let mut producer = SqlProducer::new(
1160 config,
1161 pool_cell,
1162 None,
1163 test_rt(),
1164 "sql-producer-test-route",
1165 );
1166 let mut cx = Context::from_waker(futures::task::noop_waker_ref());
1167 let result = producer.poll_ready(&mut cx);
1168 assert!(matches!(result, Poll::Ready(Ok(()))));
1169 }
1170
1171 #[tokio::test]
1173 async fn test_sql_stop_closes_pool() {
1174 let pool = sqlite_pool().await;
1175
1176 let config = {
1177 let mut c = SqlEndpointConfig::from_uri("sql:select 1?db_url=sqlite::memory:").unwrap();
1178 c.resolve_defaults();
1179 c
1180 };
1181 let pool_cell = Arc::new(OnceCell::new());
1182 pool_cell.set(Arc::new(pool.clone())).unwrap();
1183
1184 let producer = SqlProducer::new(
1185 config,
1186 pool_cell.clone(),
1187 None,
1188 test_rt(),
1189 "sql-producer-test-route",
1190 );
1191 assert!(!pool.is_closed(), "Pool should be open before stop");
1192
1193 producer.stop();
1194
1195 tokio::time::sleep(Duration::from_millis(100)).await;
1197
1198 assert!(
1199 pool.is_closed(),
1200 "Pool should be closed after producer.stop()"
1201 );
1202
1203 let mut producer2 = SqlProducer::new(
1205 {
1206 let mut c =
1207 SqlEndpointConfig::from_uri("sql:select 1?db_url=sqlite::memory:").unwrap();
1208 c.resolve_defaults();
1209 c
1210 },
1211 pool_cell.clone(),
1212 None,
1213 test_rt(),
1214 "sql-producer-test-route",
1215 );
1216 let mut cx = Context::from_waker(futures::task::noop_waker_ref());
1217 let result = producer2.poll_ready(&mut cx);
1218 assert!(
1219 matches!(result, Poll::Ready(Err(_))),
1220 "poll_ready should fail after pool closed"
1221 );
1222 }
1223
1224 #[tokio::test]
1226 async fn use_placeholder_false_executes_raw_sql() {
1227 let pool = sqlite_pool().await;
1228 seed_items_table(&pool).await;
1229
1230 let mut config = SqlEndpointConfig::from_uri(
1231 "sql:select id, name from items order by id?db_url=sqlite::memory:&usePlaceholder=false",
1232 )
1233 .unwrap();
1234 config.resolve_defaults();
1235 assert!(!config.use_placeholder);
1236
1237 let mut producer = SqlProducer::new(
1238 config,
1239 Arc::new(OnceCell::new()),
1240 None,
1241 test_rt(),
1242 "sql-producer-test-route",
1243 );
1244 producer.pool.set(Arc::new(pool.clone())).unwrap();
1246
1247 let exchange = Exchange::new(Message::default());
1248 let result = producer.call(exchange).await;
1249 assert!(result.is_ok());
1250 let exchange = result.unwrap();
1251 assert!(matches!(exchange.input.body, Body::Json(_)));
1253 }
1254
1255 #[tokio::test]
1257 async fn use_placeholder_true_processes_placeholders() {
1258 let pool = sqlite_pool().await;
1259 seed_items_table(&pool).await;
1260
1261 let mut config = SqlEndpointConfig::from_uri(
1262 "sql:select id, name from items where id = #?db_url=sqlite::memory:",
1263 )
1264 .unwrap();
1265 config.resolve_defaults();
1266 assert!(config.use_placeholder);
1267
1268 let mut producer = SqlProducer::new(
1269 config,
1270 Arc::new(OnceCell::new()),
1271 None,
1272 test_rt(),
1273 "sql-producer-test-route",
1274 );
1275 producer.pool.set(Arc::new(pool.clone())).unwrap();
1276
1277 let msg = Message::new(Body::Json(json!([1])));
1278 let exchange = Exchange::new(msg);
1279 let result = producer.call(exchange).await;
1280 assert!(result.is_ok());
1281 }
1282
1283 #[tokio::test]
1287 async fn retry_loop_invokes_operation_exactly_max_attempts_times() {
1288 use camel_component_api::NetworkRetryPolicy;
1289 use std::sync::Arc;
1290 use std::sync::atomic::{AtomicU32, Ordering};
1291
1292 let policy = NetworkRetryPolicy {
1293 max_attempts: 3,
1294 initial_delay: Duration::from_millis(1),
1295 max_delay: Duration::from_millis(1),
1296 multiplier: 1.0,
1297 ..NetworkRetryPolicy::default()
1298 };
1299
1300 let calls = Arc::new(AtomicU32::new(0));
1301 let calls_clone = Arc::clone(&calls);
1302
1303 let mut attempt: u32 = 0;
1304 let _result: Result<(), ()> = tokio::time::timeout(Duration::from_secs(30), async {
1305 loop {
1306 attempt += 1;
1307 calls_clone.fetch_add(1, Ordering::SeqCst);
1308 let op_result: Result<(), ()> = Err(());
1309 match op_result {
1310 Ok(v) => break Ok(v),
1311 Err(_) if policy.should_retry(attempt) => {
1312 let delay = policy.delay_for(attempt - 1);
1313 tokio::time::sleep(delay).await;
1314 continue;
1315 }
1316 Err(_) => break Err(()),
1317 }
1318 }
1319 })
1320 .await
1321 .expect("retry loop must finish within 30s");
1322
1323 assert_eq!(
1324 calls.load(Ordering::SeqCst),
1325 3,
1326 "max_attempts=3 must yield exactly 3 invocations"
1327 );
1328 }
1329
1330 #[tokio::test]
1335 async fn producer_pool_init_failure_calls_force_unhealthy_for_route() {
1336 let health = Arc::new(RecordingHealth::default());
1337 let rt: Arc<dyn RuntimeObservability> = Arc::new(RecordingRuntime {
1338 health: health.clone(),
1339 });
1340
1341 let mut config = SqlEndpointConfig::from_uri(
1342 "sql:select 1?db_url=postgres://nonexistent-host:5432/nonexistent_db&retryEnabled=false",
1343 )
1344 .unwrap();
1345 config.max_connections = Some(1);
1346 config.min_connections = Some(0);
1347 config.idle_timeout_secs = Some(300);
1348 config.max_lifetime_secs = Some(1800);
1349
1350 let mut producer = SqlProducer::new(
1351 config,
1352 Arc::new(OnceCell::new()),
1353 None,
1354 rt,
1355 "sql-producer-test-route",
1356 );
1357 let exchange = Exchange::new(Message::default());
1358
1359 let result = producer.call(exchange).await;
1360 assert!(result.is_err());
1361
1362 let forced = health.forced.lock().unwrap();
1363 assert_eq!(
1364 forced.len(),
1365 1,
1366 "expected one force_unhealthy_for_route call"
1367 );
1368 assert_eq!(forced[0].0, "sql-producer-test-route");
1369 assert_eq!(forced[0].1, "g:sql:producer-pool-init");
1370 assert!(!forced[0].2.is_empty(), "reason should be non-empty");
1371 }
1372
1373 #[test]
1376 fn sql_allow_dynamic_query_defaults_false_from_uri() {
1377 let config =
1378 SqlEndpointConfig::from_uri("sql:query?datasource=mydb&query=SELECT+1").unwrap();
1379 assert!(!config.allow_dynamic_query);
1380 }
1381}