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 Some("sql-producer"),
203 || {
204 async {
205 PoolOptions::new()
206 .max_connections(max_conn)
207 .min_connections(min_conn)
208 .idle_timeout(Duration::from_secs(idle_timeout))
209 .max_lifetime(Duration::from_secs(max_lifetime))
210 .connect(&db_url)
211 .await
212 }
213 },
214 is_retryable_sqlx_error,
215 )
216 .await
217 .map_err(|e| {
218 runtime.health().force_unhealthy_for_route(
219 &route_id,
220 "g:sql:producer-pool-init",
221 &e.to_string(),
222 );
223 error!(error = %e, db_url = %redact_db_url(&config.db_url), "Failed to connect to database");
225 CamelError::EndpointCreationFailed(format!(
226 "Failed to connect to database: {}",
227 e
228 ))
229 })?;
230 Ok(Arc::new(pool))
231 })
232 .await
233 .map_err(|e: CamelError| e.clone())?;
234
235 let query_str = Self::resolve_query_source(&exchange, &config);
237
238 if config.transaction_mode == crate::config::TransactionMode::Managed {
240 warn!("transactionManager not yet implemented; using Auto mode");
241 }
242
243 debug!(
244 query = %query_str,
245 "executing SQL query"
246 );
247
248 if config.batch {
250 execute_batch(pool.as_ref(), &config, &mut exchange).await?;
252 } else if config.use_placeholder {
253 let template = parse_query_template(&query_str, config.placeholder)?;
255 let mut prepared = resolve_params(&template, &exchange, &config.in_separator)?;
256
257 if let Some(params_value) = exchange.input.header(headers::PARAMETERS) {
259 if let Some(arr) = params_value.as_array() {
260 if arr.len() != prepared.bindings.len() {
261 warn!(
262 expected = prepared.bindings.len(),
263 got = arr.len(),
264 header = headers::PARAMETERS,
265 "Parameter count mismatch — SQL has {} placeholders but header provides {} values",
266 prepared.bindings.len(),
267 arr.len()
268 );
269 }
270 debug!(
271 "Overriding bindings from {} header with {} parameters",
272 headers::PARAMETERS,
273 arr.len()
274 );
275 prepared.bindings = arr.clone();
276 } else {
277 warn!(
278 header = headers::PARAMETERS,
279 "Header is present but not a JSON array — ignoring parameter override"
280 );
281 }
282 }
283
284 debug!(
285 "Executing prepared SQL ({} bindings)",
286 prepared.bindings.len()
287 );
288
289 if is_select_query(&prepared.sql) {
290 execute_select(pool.as_ref(), &prepared, &config, &mut exchange).await?;
291 } else {
292 execute_modify(pool.as_ref(), &prepared, &config, &mut exchange).await?;
293 }
294 } else {
295 debug!("Executing raw SQL (placeholder processing disabled)");
297 let prepared = PreparedQuery {
298 sql: query_str,
299 bindings: vec![],
300 };
301
302 if is_select_query(&prepared.sql) {
303 execute_select(pool.as_ref(), &prepared, &config, &mut exchange).await?;
304 } else {
305 execute_modify(pool.as_ref(), &prepared, &config, &mut exchange).await?;
306 }
307 }
308
309 Ok(exchange)
310 })
311 }
312}
313
314async fn execute_select(
316 pool: &AnyPool,
317 prepared: &PreparedQuery,
318 config: &SqlEndpointConfig,
319 exchange: &mut Exchange,
320) -> Result<(), CamelError> {
321 match config.output_type {
322 SqlOutputType::SelectOne => {
323 let mut query = sqlx::query(&prepared.sql);
325 query = bind_json_values(query, &prepared.bindings);
326
327 let rows: Vec<AnyRow> = query.fetch_all(pool).await.map_err(|e| {
328 warn!(error = %e, "SQL query failed");
329 CamelError::ProcessorError(format!("Query execution failed: {}", e))
330 })?;
331
332 let count = rows.len();
333 debug!(rows = count, "SQL query completed");
334 let json_rows: Vec<serde_json::Value> = rows
335 .iter()
336 .map(row_to_json)
337 .collect::<Result<Vec<_>, _>>()?;
338
339 if let Some(first_row) = json_rows.into_iter().next() {
340 exchange.input.body = Body::Json(first_row);
341 } else {
342 exchange.input.body = Body::Empty;
343 }
344 debug!("SelectOne returned {} row", if count > 0 { 1 } else { 0 });
345 exchange
346 .input
347 .set_header(headers::ROW_COUNT, serde_json::json!(count));
348 }
349 SqlOutputType::SelectList => {
350 let mut query = sqlx::query(&prepared.sql);
352 query = bind_json_values(query, &prepared.bindings);
353
354 let rows: Vec<AnyRow> = query.fetch_all(pool).await.map_err(|e| {
355 warn!(error = %e, "SQL query failed");
356 CamelError::ProcessorError(format!("Query execution failed: {}", e))
357 })?;
358
359 let count = rows.len();
360 debug!(rows = count, "SQL query completed");
361 let json_rows: Vec<serde_json::Value> = rows
362 .iter()
363 .map(row_to_json)
364 .collect::<Result<Vec<_>, _>>()?;
365
366 exchange.input.body = Body::Json(serde_json::Value::Array(json_rows));
367 debug!("SelectList returned {} rows", count);
368 exchange
369 .input
370 .set_header(headers::ROW_COUNT, serde_json::json!(count));
371 }
372 SqlOutputType::StreamList => {
373 use futures::TryStreamExt;
375
376 let pool_clone = pool.clone();
377 let sql_str = prepared.sql.clone();
378 let bindings = prepared.bindings.clone();
379
380 let byte_stream = async_stream::try_stream! {
382 let mut q = sqlx::query(&sql_str);
383 q = bind_json_values(q, &bindings);
384 let mut rows = q.fetch(&pool_clone);
385 while let Some(row) = rows.try_next().await.map_err(|e| {
386 CamelError::ProcessorError(format!("Query execution failed: {}", e))
387 })? {
388 let json_val = row_to_json(&row).map_err(|e| {
389 CamelError::ProcessorError(format!("JSON serialization failed: {}", e))
390 })?;
391 let mut bytes = serde_json::to_vec(&json_val)
392 .map_err(|e| CamelError::ProcessorError(format!("JSON serialization failed: {}", e)))?;
393 bytes.push(b'\n');
394 yield Bytes::from(bytes);
395 }
396 };
397
398 exchange.input.body = Body::Stream(StreamBody {
399 stream: Arc::new(tokio::sync::Mutex::new(Some(Box::pin(byte_stream)))),
400 metadata: StreamMetadata {
401 content_type: Some("application/x-ndjson".to_string()),
402 size_hint: None,
403 origin: None,
404 },
405 });
406 debug!("StreamList: created lazy stream (rows fetched on demand)");
407 }
409 }
410
411 Ok(())
412}
413
414async fn execute_modify(
416 pool: &AnyPool,
417 prepared: &PreparedQuery,
418 config: &SqlEndpointConfig,
419 exchange: &mut Exchange,
420) -> Result<(), CamelError> {
421 let mut query = sqlx::query(&prepared.sql);
422 query = bind_json_values(query, &prepared.bindings);
423
424 let result = query.execute(pool).await.map_err(|e| {
425 warn!(error = %e, "SQL query failed");
426 CamelError::ProcessorError(format!("Query execution failed: {}", e))
427 })?;
428
429 let rows_affected = result.rows_affected();
430
431 if let Some(expected) = config.expected_update_count
433 && rows_affected as i64 != expected
434 {
435 warn!(expected, actual = rows_affected, "Row count mismatch");
436 return Err(CamelError::ProcessorError(format!(
437 "Expected {} rows affected, got {}",
438 expected, rows_affected
439 )));
440 }
441
442 exchange
443 .input
444 .set_header(headers::UPDATE_COUNT, serde_json::json!(rows_affected));
445
446 if config.noop {
447 } else {
449 exchange.input.body = Body::Json(json!({ "rowsAffected": rows_affected }));
450 }
451
452 debug!(rows = rows_affected, "SQL modify query completed");
453
454 Ok(())
455}
456
457async fn execute_batch(
459 pool: &AnyPool,
460 config: &SqlEndpointConfig,
461 exchange: &mut Exchange,
462) -> Result<(), CamelError> {
463 let body_json = match &exchange.input.body {
465 Body::Json(val) => val,
466 _ => {
467 return Err(CamelError::ProcessorError(
468 "Batch mode requires body to be a JSON array of arrays".to_string(),
469 ));
470 }
471 };
472
473 let batch_data = body_json
474 .as_array()
475 .ok_or_else(|| {
476 CamelError::ProcessorError("Batch mode requires body to be a JSON array".to_string())
477 })?
478 .clone();
479
480 let template = parse_query_template(&config.query, config.placeholder)?;
482
483 let mut tx = pool.begin().await.map_err(|e| {
485 warn!(error = %e, "Failed to begin transaction");
488 CamelError::ProcessorError(format!("Failed to begin transaction: {}", e))
489 })?;
490
491 let mut total_rows_affected: u64 = 0;
492
493 for (batch_idx, params_array) in batch_data.into_iter().enumerate() {
494 params_array.as_array().ok_or_else(|| {
496 CamelError::ProcessorError(format!(
497 "Batch item at index {} must be a JSON array of parameters",
498 batch_idx
499 ))
500 })?;
501
502 let temp_msg = Message::new(Body::Json(params_array.clone()));
504 let temp_exchange = Exchange::new(temp_msg);
505
506 let prepared = resolve_params(&template, &temp_exchange, &config.in_separator)?;
508
509 let mut query = sqlx::query(&prepared.sql);
511 query = bind_json_values(query, &prepared.bindings);
512
513 let result = query.execute(&mut *tx).await.map_err(|e| {
514 warn!("Batch query execution failed at index {}: {}", batch_idx, e);
517 CamelError::ProcessorError(format!("Batch query execution failed: {}", e))
518 })?;
519
520 if let Some(expected) = config.expected_update_count
522 && result.rows_affected() as i64 != expected
523 {
524 warn!(
527 "Batch item {}: expected {} rows affected, got {}",
528 batch_idx,
529 expected,
530 result.rows_affected()
531 );
532 return Err(CamelError::ProcessorError(format!(
533 "Batch item {}: expected {} rows affected, got {}",
534 batch_idx,
535 expected,
536 result.rows_affected()
537 )));
538 }
539
540 total_rows_affected += result.rows_affected();
541 }
542
543 tx.commit().await.map_err(|e| {
545 warn!(error = %e, "Failed to commit transaction");
548 CamelError::ProcessorError(format!("Failed to commit transaction: {}", e))
549 })?;
550
551 exchange.input.set_header(
552 headers::UPDATE_COUNT,
553 serde_json::json!(total_rows_affected),
554 );
555
556 debug!(
557 "Batch execution completed, total rows affected: {}",
558 total_rows_affected
559 );
560
561 Ok(())
562}
563
564#[cfg(test)]
565mod tests {
566 use super::*;
567 use camel_api::MetricsCollector;
568 use camel_component_api::HealthCheckRegistry;
569 use camel_component_api::test_support::PanicRuntimeObservability;
570 fn test_rt() -> std::sync::Arc<dyn camel_component_api::RuntimeObservability> {
571 std::sync::Arc::new(PanicRuntimeObservability)
572 }
573 use camel_component_api::Message;
574 use camel_component_api::UriConfig;
575 use sqlx::any::AnyPoolOptions;
576 use std::sync::Arc;
577 use std::sync::Mutex;
578 use tokio::sync::OnceCell;
579
580 #[derive(Debug, Default)]
583 struct RecordingHealth {
584 forced: Mutex<Vec<(String, String, String)>>,
585 }
586
587 impl HealthCheckRegistry for RecordingHealth {
588 fn force_unhealthy_for_route(&self, route_id: &str, name: &str, reason: &str) {
589 self.forced.lock().unwrap().push((
590 route_id.to_string(),
591 name.to_string(),
592 reason.to_string(),
593 ));
594 }
595 }
596
597 struct NoopMetrics;
598
599 impl MetricsCollector for NoopMetrics {
600 fn record_exchange_duration(&self, _: &str, _: Duration) {}
601 fn increment_errors(&self, _: &str, _: &str) {}
602 fn increment_exchanges(&self, _: &str) {}
603 fn set_queue_depth(&self, _: &str, _: usize) {}
604 fn record_circuit_breaker_change(&self, _: &str, _: &str, _: &str) {}
605 }
606
607 struct RecordingRuntime {
608 health: Arc<RecordingHealth>,
609 }
610
611 impl RuntimeObservability for RecordingRuntime {
612 fn metrics(&self) -> Arc<dyn MetricsCollector> {
613 Arc::new(NoopMetrics)
614 }
615 fn health(&self) -> Arc<dyn HealthCheckRegistry> {
616 self.health.clone()
617 }
618 }
619
620 async fn sqlite_pool() -> AnyPool {
621 sqlx::any::install_default_drivers();
622 AnyPoolOptions::new()
623 .max_connections(1)
624 .connect("sqlite::memory:")
625 .await
626 .expect("sqlite pool")
627 }
628
629 async fn seed_items_table(pool: &AnyPool) {
630 sqlx::query(
631 "CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT, done INTEGER DEFAULT 0)",
632 )
633 .execute(pool)
634 .await
635 .expect("create table");
636 sqlx::query("INSERT INTO items (id, name, done) VALUES (1, 'a', 0), (2, 'b', 0)")
637 .execute(pool)
638 .await
639 .expect("seed rows");
640 }
641
642 fn config() -> SqlEndpointConfig {
643 let mut c =
644 SqlEndpointConfig::from_uri("sql:select 1?db_url=postgres://localhost/test").unwrap();
645 c.resolve_defaults();
646 c
647 }
648
649 #[test]
650 fn producer_clone_shares_pool() {
651 let p1 = SqlProducer::new(
652 config(),
653 Arc::new(OnceCell::new()),
654 None,
655 test_rt(),
656 "sql-producer-test-route",
657 );
658 let p2 = p1.clone();
659 assert!(Arc::ptr_eq(&p1.pool, &p2.pool));
660 assert!(Arc::ptr_eq(&p1.stopped, &p2.stopped));
661 }
662
663 #[test]
664 fn resolve_query_from_config() {
665 let config = config();
666 let ex = Exchange::new(Message::default());
667 let q = SqlProducer::resolve_query_source(&ex, &config);
668 assert_eq!(q, "select 1");
669 }
670
671 #[test]
672 fn resolve_query_from_header() {
673 let mut config = config();
674 config.allow_dynamic_query = true;
675 let mut msg = Message::default();
676 msg.set_header(headers::QUERY, serde_json::json!("select 2"));
677 let ex = Exchange::new(msg);
678 let q = SqlProducer::resolve_query_source(&ex, &config);
679 assert_eq!(q, "select 2");
680 }
681
682 #[test]
683 fn resolve_query_from_body() {
684 let mut config = config();
685 config.use_message_body_for_sql = true;
686 config.allow_dynamic_query = true;
687 let msg = Message::new(Body::Text("select 3".to_string()));
688 let ex = Exchange::new(msg);
689 let q = SqlProducer::resolve_query_source(&ex, &config);
690 assert_eq!(q, "select 3");
691 }
692
693 #[test]
694 fn resolve_query_header_priority_over_body() {
695 let mut config = config();
696 config.use_message_body_for_sql = true;
697 config.allow_dynamic_query = true;
698 let mut msg = Message::new(Body::Text("select from body".to_string()));
699 msg.set_header(headers::QUERY, serde_json::json!("select from header"));
700 let ex = Exchange::new(msg);
701 let q = SqlProducer::resolve_query_source(&ex, &config);
702 assert_eq!(q, "select from header");
703 }
704
705 #[test]
706 fn resolve_query_body_priority_over_config() {
707 let mut config = config();
708 config.use_message_body_for_sql = true;
709 config.allow_dynamic_query = true;
710 let msg = Message::new(Body::Text("select from body".to_string()));
711 let ex = Exchange::new(msg);
712 let q = SqlProducer::resolve_query_source(&ex, &config);
713 assert_eq!(q, "select from body");
714 }
715
716 #[test]
720 fn dynamic_query_denied_by_default() {
721 let mut msg = Message::default();
722 msg.set_header(headers::QUERY, "DROP TABLE users; --");
723 let ex = Exchange::new(msg);
724
725 let mut config = config();
726 config.query = "SELECT 1".to_string();
727 let q = SqlProducer::resolve_query_source(&ex, &config);
730 assert_eq!(q, "SELECT 1");
732 }
733
734 #[test]
736 fn dynamic_query_allowed_with_opt_in() {
737 let mut msg = Message::default();
738 msg.set_header(headers::QUERY, "SELECT 1 FROM users WHERE id = :#id");
739 let ex = Exchange::new(msg);
740
741 let mut config = config();
742 config.query = "SELECT 1".to_string();
743 config.allow_dynamic_query = true;
744
745 let q = SqlProducer::resolve_query_source(&ex, &config);
746 assert_eq!(q, "SELECT 1 FROM users WHERE id = :#id");
748 }
749
750 #[test]
751 fn bind_json_null() {
752 let query = sqlx::query("SELECT ?");
753 let values = vec![serde_json::Value::Null];
754 let _bound = bind_json_values(query, &values);
755 }
757
758 #[test]
759 fn bind_json_bool() {
760 let query = sqlx::query("SELECT ?");
761 let values = vec![serde_json::Value::Bool(true)];
762 let _bound = bind_json_values(query, &values);
763 }
764
765 #[test]
766 fn bind_json_number_i64() {
767 let query = sqlx::query("SELECT ?");
768 let values = vec![serde_json::json!(42)];
769 let _bound = bind_json_values(query, &values);
770 }
771
772 #[test]
773 fn bind_json_number_f64() {
774 let query = sqlx::query("SELECT ?");
775 let values = vec![serde_json::json!(std::f64::consts::PI)];
776 let _bound = bind_json_values(query, &values);
777 }
778
779 #[test]
780 fn bind_json_string() {
781 let query = sqlx::query("SELECT ?");
782 let values = vec![serde_json::json!("hello world")];
783 let _bound = bind_json_values(query, &values);
784 }
785
786 #[test]
787 fn bind_json_array() {
788 let query = sqlx::query("SELECT ?");
789 let values = vec![serde_json::json!([1, 2, 3])];
790 let _bound = bind_json_values(query, &values);
791 }
792
793 #[test]
794 fn bind_json_object() {
795 let query = sqlx::query("SELECT ?");
796 let values = vec![serde_json::json!({"key": "value"})];
797 let _bound = bind_json_values(query, &values);
798 }
799
800 #[test]
801 fn bind_multiple_values() {
802 let query = sqlx::query("SELECT ?, ?, ?");
803 let values = vec![
804 serde_json::json!(1),
805 serde_json::json!("test"),
806 serde_json::Value::Null,
807 ];
808 let _bound = bind_json_values(query, &values);
809 }
810
811 #[test]
813 fn expected_update_count_validation() {
814 let config = SqlEndpointConfig::from_uri(
816 "sql:update t set x=1?db_url=postgres://localhost/test&expectedUpdateCount=5",
817 )
818 .unwrap();
819 assert_eq!(config.expected_update_count, Some(5));
820
821 let config_default = self::config();
823 assert_eq!(config_default.expected_update_count, None);
824
825 let config_neg = SqlEndpointConfig::from_uri(
827 "sql:update t set x=1?db_url=postgres://localhost/test&expectedUpdateCount=-1",
828 )
829 .unwrap();
830 assert_eq!(config_neg.expected_update_count, Some(-1));
831 }
832
833 #[test]
835 fn parameters_header_override_logic() {
836 let mut prepared = PreparedQuery {
838 sql: "SELECT * FROM t WHERE id = $1".to_string(),
839 bindings: vec![serde_json::json!(42)],
840 };
841
842 let header_params = serde_json::json!([99, "extra"]);
844 if let Some(arr) = header_params.as_array() {
845 prepared.bindings = arr.clone();
846 }
847
848 assert_eq!(prepared.bindings.len(), 2);
850 assert_eq!(prepared.bindings[0], serde_json::json!(99));
851 assert_eq!(prepared.bindings[1], serde_json::json!("extra"));
852
853 let mut prepared2 = PreparedQuery {
855 sql: "SELECT * FROM t WHERE id = $1".to_string(),
856 bindings: vec![serde_json::json!(42)],
857 };
858 let header_non_array = serde_json::json!({"not": "an array"});
859 if let Some(arr) = header_non_array.as_array() {
860 prepared2.bindings = arr.clone();
861 }
862 assert_eq!(prepared2.bindings.len(), 1);
864 assert_eq!(prepared2.bindings[0], serde_json::json!(42));
865 }
866
867 #[tokio::test]
868 async fn execute_select_one_sets_body_and_row_count() {
869 let pool = sqlite_pool().await;
870 seed_items_table(&pool).await;
871
872 let mut config = SqlEndpointConfig::from_uri(
873 "sql:select id, name from items order by id?db_url=sqlite::memory:&outputType=SelectOne",
874 )
875 .unwrap();
876 config.resolve_defaults();
877
878 let prepared = PreparedQuery {
879 sql: "select id, name from items order by id".to_string(),
880 bindings: vec![],
881 };
882 let mut exchange = Exchange::new(Message::default());
883
884 execute_select(&pool, &prepared, &config, &mut exchange)
885 .await
886 .expect("select one");
887
888 assert_eq!(exchange.input.header(headers::ROW_COUNT), Some(&json!(2)));
889 assert_eq!(
890 exchange.input.body,
891 Body::Json(json!({"id": 1, "name": "a"}))
892 );
893 }
894
895 #[tokio::test]
896 async fn execute_stream_list_materializes_ndjson() {
897 let pool = sqlite_pool().await;
898 seed_items_table(&pool).await;
899
900 let mut config = SqlEndpointConfig::from_uri(
901 "sql:select id from items order by id?db_url=sqlite::memory:&outputType=StreamList",
902 )
903 .unwrap();
904 config.resolve_defaults();
905
906 let prepared = PreparedQuery {
907 sql: "select id from items order by id".to_string(),
908 bindings: vec![],
909 };
910 let mut exchange = Exchange::new(Message::default());
911
912 execute_select(&pool, &prepared, &config, &mut exchange)
913 .await
914 .expect("stream list");
915
916 let bytes = exchange
917 .input
918 .body
919 .clone()
920 .into_bytes(1024)
921 .await
922 .expect("stream bytes");
923 let text = String::from_utf8(bytes.to_vec()).expect("utf8");
924 assert!(text.contains("{\"id\":1}"));
925 assert!(text.contains("{\"id\":2}"));
926 assert_eq!(exchange.input.header(headers::ROW_COUNT), None);
927 }
928
929 #[tokio::test]
930 async fn execute_modify_expected_update_count_mismatch_returns_error() {
931 let pool = sqlite_pool().await;
932 seed_items_table(&pool).await;
933
934 let mut config = SqlEndpointConfig::from_uri(
935 "sql:update items set done=1 where id = #?db_url=sqlite::memory:&expectedUpdateCount=2",
936 )
937 .unwrap();
938 config.resolve_defaults();
939
940 let prepared = PreparedQuery {
941 sql: "update items set done=1 where id = $1".to_string(),
942 bindings: vec![json!(1)],
943 };
944 let mut exchange = Exchange::new(Message::default());
945
946 let err = execute_modify(&pool, &prepared, &config, &mut exchange)
947 .await
948 .expect_err("must fail due expected row count mismatch");
949 assert!(err.to_string().contains("Expected 2 rows affected, got 1"));
950 }
951
952 #[tokio::test]
953 async fn execute_batch_rollback_when_any_item_fails_expected_count() {
954 let pool = sqlite_pool().await;
955 seed_items_table(&pool).await;
956
957 let mut config = SqlEndpointConfig::from_uri(
958 "sql:update items set done=1 where id = #?db_url=sqlite::memory:&batch=true&expectedUpdateCount=1",
959 )
960 .unwrap();
961 config.resolve_defaults();
962
963 let mut exchange = Exchange::new(Message::new(Body::Json(json!([[1], [999]]))));
964
965 let err = execute_batch(&pool, &config, &mut exchange)
966 .await
967 .expect_err("second batch item should fail expectedUpdateCount");
968 assert!(
969 err.to_string()
970 .contains("Batch item 1: expected 1 rows affected, got 0")
971 );
972
973 let row = sqlx::query("select done from items where id = 1")
974 .fetch_one(&pool)
975 .await
976 .expect("query row");
977 let done: i64 = sqlx::Row::try_get(&row, 0).expect("done column");
978 assert_eq!(done, 0, "transaction must rollback first update");
979 }
980
981 #[tokio::test]
987 async fn producer_no_panic_without_prior_resolve_defaults() {
988 let config = SqlEndpointConfig::from_uri("sql:select 1?db_url=sqlite::memory:").unwrap();
990 assert!(config.max_connections.is_none());
991
992 let mut producer = SqlProducer::new(
993 config,
994 Arc::new(OnceCell::new()),
995 None,
996 test_rt(),
997 "sql-producer-test-route",
998 );
999 let exchange = Exchange::new(Message::default());
1000
1001 let result = producer.call(exchange).await;
1003 assert!(
1004 result.is_ok(),
1005 "Producer should initialize pool without panic, got: {:?}",
1006 result
1007 );
1008 }
1009
1010 #[tokio::test]
1012 async fn producer_pool_init_returns_config_error_for_invalid_db() {
1013 let mut config = SqlEndpointConfig::from_uri(
1017 "sql:select 1?db_url=postgres://nonexistent-host:5432/nonexistent_db&retryEnabled=false",
1018 )
1019 .unwrap();
1020 config.max_connections = Some(1);
1022 config.min_connections = Some(0);
1023 config.idle_timeout_secs = Some(300);
1024 config.max_lifetime_secs = Some(1800);
1025
1026 let health = Arc::new(RecordingHealth::default());
1027 let rt: Arc<dyn RuntimeObservability> = Arc::new(RecordingRuntime {
1028 health: health.clone(),
1029 });
1030 let mut producer = SqlProducer::new(
1031 config,
1032 Arc::new(OnceCell::new()),
1033 None,
1034 rt,
1035 "sql-producer-test-route",
1036 );
1037 let exchange = Exchange::new(Message::default());
1038
1039 let result = producer.call(exchange).await;
1040 assert!(result.is_err());
1041 let err_msg = result.unwrap_err().to_string();
1043 assert!(
1044 err_msg.contains("Failed to connect") || err_msg.contains("database"),
1045 "Expected connection error, got: {}",
1046 err_msg
1047 );
1048 let forced = health.forced.lock().unwrap();
1050 assert_eq!(
1051 forced.len(),
1052 1,
1053 "expected one force_unhealthy_for_route call"
1054 );
1055 assert_eq!(forced[0].0, "sql-producer-test-route");
1056 assert_eq!(forced[0].1, "g:sql:producer-pool-init");
1057 }
1058
1059 #[test]
1061 fn poll_ready_returns_ready_for_uninitialized_pool() {
1062 let config = {
1063 let mut c = SqlEndpointConfig::from_uri("sql:select 1?db_url=sqlite::memory:").unwrap();
1064 c.resolve_defaults();
1065 c
1066 };
1067 let mut producer = SqlProducer::new(
1068 config,
1069 Arc::new(OnceCell::new()),
1070 None,
1071 test_rt(),
1072 "sql-producer-test-route",
1073 );
1074 let mut cx = Context::from_waker(futures::task::noop_waker_ref());
1075 let result = producer.poll_ready(&mut cx);
1076 assert!(matches!(result, Poll::Ready(Ok(()))));
1077 }
1078
1079 #[test]
1081 fn poll_ready_returns_error_when_stopped() {
1082 let config = {
1083 let mut c = SqlEndpointConfig::from_uri("sql:select 1?db_url=sqlite::memory:").unwrap();
1084 c.resolve_defaults();
1085 c
1086 };
1087 let mut producer = SqlProducer::new(
1088 config,
1089 Arc::new(OnceCell::new()),
1090 None,
1091 test_rt(),
1092 "sql-producer-test-route",
1093 );
1094 producer.stop();
1095 let mut cx = Context::from_waker(futures::task::noop_waker_ref());
1096 let result = producer.poll_ready(&mut cx);
1097 assert!(matches!(result, Poll::Ready(Err(_))));
1098 let err_msg = match result {
1099 Poll::Ready(Err(e)) => e.to_string(),
1100 _ => unreachable!(),
1101 };
1102 assert!(err_msg.contains("SQL producer stopped"));
1103 }
1104
1105 #[tokio::test]
1107 async fn poll_ready_returns_error_when_pool_closed() {
1108 let pool = sqlite_pool().await;
1109 pool.close().await;
1110
1111 let config = {
1112 let mut c = SqlEndpointConfig::from_uri("sql:select 1?db_url=sqlite::memory:").unwrap();
1113 c.resolve_defaults();
1114 c
1115 };
1116 let pool_cell = Arc::new(OnceCell::new());
1117 pool_cell.set(Arc::new(pool)).unwrap();
1118
1119 let mut producer = SqlProducer::new(
1120 config,
1121 pool_cell,
1122 None,
1123 test_rt(),
1124 "sql-producer-test-route",
1125 );
1126 let mut cx = Context::from_waker(futures::task::noop_waker_ref());
1127 let result = producer.poll_ready(&mut cx);
1128 assert!(matches!(result, Poll::Ready(Err(_))));
1129 let err_msg = match result {
1130 Poll::Ready(Err(e)) => e.to_string(),
1131 _ => unreachable!(),
1132 };
1133 assert!(err_msg.contains("SQL connection pool is closed"));
1134 }
1135
1136 #[tokio::test]
1138 async fn poll_ready_returns_ok_for_healthy_pool() {
1139 let pool = sqlite_pool().await;
1140
1141 let config = {
1142 let mut c = SqlEndpointConfig::from_uri("sql:select 1?db_url=sqlite::memory:").unwrap();
1143 c.resolve_defaults();
1144 c
1145 };
1146 let pool_cell = Arc::new(OnceCell::new());
1147 pool_cell.set(Arc::new(pool)).unwrap();
1148
1149 let mut producer = SqlProducer::new(
1150 config,
1151 pool_cell,
1152 None,
1153 test_rt(),
1154 "sql-producer-test-route",
1155 );
1156 let mut cx = Context::from_waker(futures::task::noop_waker_ref());
1157 let result = producer.poll_ready(&mut cx);
1158 assert!(matches!(result, Poll::Ready(Ok(()))));
1159 }
1160
1161 #[tokio::test]
1163 async fn test_sql_stop_closes_pool() {
1164 let pool = sqlite_pool().await;
1165
1166 let config = {
1167 let mut c = SqlEndpointConfig::from_uri("sql:select 1?db_url=sqlite::memory:").unwrap();
1168 c.resolve_defaults();
1169 c
1170 };
1171 let pool_cell = Arc::new(OnceCell::new());
1172 pool_cell.set(Arc::new(pool.clone())).unwrap();
1173
1174 let producer = SqlProducer::new(
1175 config,
1176 pool_cell.clone(),
1177 None,
1178 test_rt(),
1179 "sql-producer-test-route",
1180 );
1181 assert!(!pool.is_closed(), "Pool should be open before stop");
1182
1183 producer.stop();
1184
1185 tokio::time::sleep(Duration::from_millis(100)).await;
1187
1188 assert!(
1189 pool.is_closed(),
1190 "Pool should be closed after producer.stop()"
1191 );
1192
1193 let mut producer2 = SqlProducer::new(
1195 {
1196 let mut c =
1197 SqlEndpointConfig::from_uri("sql:select 1?db_url=sqlite::memory:").unwrap();
1198 c.resolve_defaults();
1199 c
1200 },
1201 pool_cell.clone(),
1202 None,
1203 test_rt(),
1204 "sql-producer-test-route",
1205 );
1206 let mut cx = Context::from_waker(futures::task::noop_waker_ref());
1207 let result = producer2.poll_ready(&mut cx);
1208 assert!(
1209 matches!(result, Poll::Ready(Err(_))),
1210 "poll_ready should fail after pool closed"
1211 );
1212 }
1213
1214 #[tokio::test]
1216 async fn use_placeholder_false_executes_raw_sql() {
1217 let pool = sqlite_pool().await;
1218 seed_items_table(&pool).await;
1219
1220 let mut config = SqlEndpointConfig::from_uri(
1221 "sql:select id, name from items order by id?db_url=sqlite::memory:&usePlaceholder=false",
1222 )
1223 .unwrap();
1224 config.resolve_defaults();
1225 assert!(!config.use_placeholder);
1226
1227 let mut producer = SqlProducer::new(
1228 config,
1229 Arc::new(OnceCell::new()),
1230 None,
1231 test_rt(),
1232 "sql-producer-test-route",
1233 );
1234 producer.pool.set(Arc::new(pool.clone())).unwrap();
1236
1237 let exchange = Exchange::new(Message::default());
1238 let result = producer.call(exchange).await;
1239 assert!(result.is_ok());
1240 let exchange = result.unwrap();
1241 assert!(matches!(exchange.input.body, Body::Json(_)));
1243 }
1244
1245 #[tokio::test]
1247 async fn use_placeholder_true_processes_placeholders() {
1248 let pool = sqlite_pool().await;
1249 seed_items_table(&pool).await;
1250
1251 let mut config = SqlEndpointConfig::from_uri(
1252 "sql:select id, name from items where id = #?db_url=sqlite::memory:",
1253 )
1254 .unwrap();
1255 config.resolve_defaults();
1256 assert!(config.use_placeholder);
1257
1258 let mut producer = SqlProducer::new(
1259 config,
1260 Arc::new(OnceCell::new()),
1261 None,
1262 test_rt(),
1263 "sql-producer-test-route",
1264 );
1265 producer.pool.set(Arc::new(pool.clone())).unwrap();
1266
1267 let msg = Message::new(Body::Json(json!([1])));
1268 let exchange = Exchange::new(msg);
1269 let result = producer.call(exchange).await;
1270 assert!(result.is_ok());
1271 }
1272
1273 #[tokio::test]
1277 async fn retry_loop_invokes_operation_exactly_max_attempts_times() {
1278 use camel_component_api::NetworkRetryPolicy;
1279 use std::sync::Arc;
1280 use std::sync::atomic::{AtomicU32, Ordering};
1281
1282 let policy = NetworkRetryPolicy {
1283 max_attempts: 3,
1284 initial_delay: Duration::from_millis(1),
1285 max_delay: Duration::from_millis(1),
1286 multiplier: 1.0,
1287 ..NetworkRetryPolicy::default()
1288 };
1289
1290 let calls = Arc::new(AtomicU32::new(0));
1291 let calls_clone = Arc::clone(&calls);
1292
1293 let mut attempt: u32 = 0;
1294 let _result: Result<(), ()> = loop {
1295 attempt += 1;
1296 calls_clone.fetch_add(1, Ordering::SeqCst);
1297 let op_result: Result<(), ()> = Err(());
1298 match op_result {
1299 Ok(v) => break Ok(v),
1300 Err(_) if policy.should_retry(attempt) => {
1301 let delay = policy.delay_for(attempt - 1);
1302 tokio::time::sleep(delay).await;
1303 continue;
1304 }
1305 Err(_) => break Err(()),
1306 }
1307 };
1308
1309 assert_eq!(
1310 calls.load(Ordering::SeqCst),
1311 3,
1312 "max_attempts=3 must yield exactly 3 invocations"
1313 );
1314 }
1315
1316 #[tokio::test]
1321 async fn producer_pool_init_failure_calls_force_unhealthy_for_route() {
1322 let health = Arc::new(RecordingHealth::default());
1323 let rt: Arc<dyn RuntimeObservability> = Arc::new(RecordingRuntime {
1324 health: health.clone(),
1325 });
1326
1327 let mut config = SqlEndpointConfig::from_uri(
1328 "sql:select 1?db_url=postgres://nonexistent-host:5432/nonexistent_db&retryEnabled=false",
1329 )
1330 .unwrap();
1331 config.max_connections = Some(1);
1332 config.min_connections = Some(0);
1333 config.idle_timeout_secs = Some(300);
1334 config.max_lifetime_secs = Some(1800);
1335
1336 let mut producer = SqlProducer::new(
1337 config,
1338 Arc::new(OnceCell::new()),
1339 None,
1340 rt,
1341 "sql-producer-test-route",
1342 );
1343 let exchange = Exchange::new(Message::default());
1344
1345 let result = producer.call(exchange).await;
1346 assert!(result.is_err());
1347
1348 let forced = health.forced.lock().unwrap();
1349 assert_eq!(
1350 forced.len(),
1351 1,
1352 "expected one force_unhealthy_for_route call"
1353 );
1354 assert_eq!(forced[0].0, "sql-producer-test-route");
1355 assert_eq!(forced[0].1, "g:sql:producer-pool-init");
1356 assert!(!forced[0].2.is_empty(), "reason should be non-empty");
1357 }
1358}