camel_component_sql/
lib.rs1pub mod bundle;
2pub mod config;
3pub mod consumer;
4pub mod endpoint;
5pub mod headers;
6pub mod health;
7pub mod pool_factory;
8pub mod producer;
9pub mod query;
10pub(crate) mod utils;
11
12use std::sync::Arc;
13
14use camel_api::datasource::DatasourceCatalog;
15use camel_component_api::CamelError;
16use camel_component_api::UriConfig;
17use camel_component_api::{Component, Endpoint};
18use sqlx::AnyPool;
19use tokio::sync::OnceCell;
20
21pub use bundle::SqlBundle;
22pub use config::{
23 PollStrategy, ProcessingStrategy, SqlEndpointConfig, SqlGlobalConfig, SqlOutputType,
24 TransactionMode,
25};
26pub use health::SqlHealthCheck;
27
28type SharedPool = Arc<OnceCell<Arc<AnyPool>>>;
29
30pub struct SqlComponent {
31 config: Option<SqlGlobalConfig>,
32 catalog: Option<Arc<dyn DatasourceCatalog>>,
33}
34
35impl SqlComponent {
36 pub fn new() -> Self {
37 Self {
38 config: None,
39 catalog: None,
40 }
41 }
42
43 pub fn with_config(config: SqlGlobalConfig) -> Self {
44 Self {
45 config: Some(config),
46 catalog: None,
47 }
48 }
49
50 pub fn with_optional_config(config: Option<SqlGlobalConfig>) -> Self {
51 Self {
52 config,
53 catalog: None,
54 }
55 }
56
57 pub fn with_config_and_catalog(
58 config: SqlGlobalConfig,
59 catalog: Arc<dyn DatasourceCatalog>,
60 ) -> Self {
61 Self {
62 config: Some(config),
63 catalog: Some(catalog),
64 }
65 }
66
67 pub fn catalog(&self) -> Option<&Arc<dyn DatasourceCatalog>> {
68 self.catalog.as_ref()
69 }
70}
71
72impl Default for SqlComponent {
73 fn default() -> Self {
74 Self::new()
75 }
76}
77
78impl Component for SqlComponent {
79 fn scheme(&self) -> &str {
80 "sql"
81 }
82
83 fn create_endpoint(
84 &self,
85 uri: &str,
86 ctx: &dyn camel_component_api::ComponentContext,
87 ) -> Result<Box<dyn Endpoint>, CamelError> {
88 let mut config = SqlEndpointConfig::from_uri(uri)?;
89
90 if config.datasource_name.is_some() && self.catalog.is_none() {
91 return Err(CamelError::Config(
92 "datasource parameter requires catalog — no datasource catalog configured".into(),
93 ));
94 }
95
96 if let Some(ref ds_name) = config.datasource_name
97 && let Some(ref catalog) = self.catalog
98 {
99 let ds_config = catalog.get_config(ds_name).ok_or_else(|| {
100 CamelError::Config(format!("datasource '{}' not found in catalog", ds_name))
101 })?;
102 config.db_url = ds_config.db_url;
103 }
104
105 if let Some(ref global_config) = self.config {
106 config.apply_defaults(global_config);
107 }
108 config.resolve_defaults();
109 let pool: SharedPool = Arc::new(OnceCell::new());
110 let health_check = SqlHealthCheck::new(
111 Arc::clone(&pool),
112 self.catalog.clone(),
113 config.datasource_name.clone(),
114 );
115 ctx.register_current_route_health_check(Arc::new(health_check));
116
117 if config.datasource_name.is_some()
118 && let Some(ref catalog) = self.catalog
119 {
120 Ok(Box::new(endpoint::SqlEndpoint::new_with_pool_and_catalog(
121 uri.to_string(),
122 config,
123 pool,
124 Arc::clone(catalog),
125 )))
126 } else {
127 Ok(Box::new(endpoint::SqlEndpoint::new_with_pool(
128 uri.to_string(),
129 config,
130 pool,
131 )))
132 }
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use camel_component_api::Component;
140 use camel_component_api::NoOpComponentContext;
141
142 #[test]
143 fn test_component_scheme() {
144 let c = SqlComponent::new();
145 assert_eq!(c.scheme(), "sql");
146 }
147
148 #[test]
149 fn test_component_creates_endpoint() {
150 let c = SqlComponent::new();
151 let ctx = NoOpComponentContext;
152 let ep = c.create_endpoint("sql:select 1?db_url=postgres://localhost/test", &ctx);
153 assert!(ep.is_ok());
154 }
155
156 #[test]
157 fn test_component_rejects_wrong_scheme() {
158 let c = SqlComponent::new();
159 let ctx = NoOpComponentContext;
160 let ep = c.create_endpoint("redis://localhost", &ctx);
161 assert!(ep.is_err());
162 }
163
164 #[test]
165 fn test_endpoint_uri() {
166 let c = SqlComponent::new();
167 let ctx = NoOpComponentContext;
168 let ep = c
169 .create_endpoint("sql:select 1?db_url=postgres://localhost/test", &ctx)
170 .unwrap();
171 assert_eq!(ep.uri(), "sql:select 1?db_url=postgres://localhost/test");
172 }
173
174 #[test]
175 fn test_component_with_global_config() {
176 let global = SqlGlobalConfig::default().with_max_connections(20);
177 let c = SqlComponent::with_config(global);
178 let ctx = NoOpComponentContext;
179 assert_eq!(c.scheme(), "sql");
181 let ep = c.create_endpoint("sql:select 1?db_url=postgres://localhost/test", &ctx);
182 assert!(ep.is_ok());
183 }
184
185 #[test]
186 fn test_global_config_applied_to_endpoint() {
187 let global = SqlGlobalConfig::default()
190 .with_max_connections(20)
191 .with_min_connections(3)
192 .with_idle_timeout_secs(600)
193 .with_max_lifetime_secs(3600);
194 let mut cfg =
195 config::SqlEndpointConfig::from_uri("sql:select 1?db_url=postgres://localhost/test")
196 .unwrap();
197 cfg.apply_defaults(&global);
198 cfg.resolve_defaults();
199 assert_eq!(cfg.max_connections, Some(20));
200 assert_eq!(cfg.min_connections, Some(3));
201 assert_eq!(cfg.idle_timeout_secs, Some(600));
202 assert_eq!(cfg.max_lifetime_secs, Some(3600));
203 }
204
205 #[test]
206 fn test_uri_param_wins_over_global_config() {
207 let global = SqlGlobalConfig::default()
209 .with_max_connections(20)
210 .with_min_connections(3);
211 let mut cfg = config::SqlEndpointConfig::from_uri(
212 "sql:select 1?db_url=postgres://localhost/test&maxConnections=99&minConnections=7",
213 )
214 .unwrap();
215 cfg.apply_defaults(&global);
216 cfg.resolve_defaults();
217 assert_eq!(cfg.max_connections, Some(99)); assert_eq!(cfg.min_connections, Some(7)); }
220}