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::component_metadata::ComponentMetadata;
15use camel_api::datasource::DatasourceCatalog;
16use camel_component_api::CamelError;
17use camel_component_api::UriConfig;
18use camel_component_api::{Component, Endpoint};
19use sqlx::AnyPool;
20use tokio::sync::OnceCell;
21
22pub use bundle::SqlBundle;
23pub use config::{
24 PollStrategy, ProcessingStrategy, SqlEndpointConfig, SqlGlobalConfig, SqlOutputType,
25 TransactionMode,
26};
27pub use health::SqlHealthCheck;
28
29type SharedPool = Arc<OnceCell<Arc<AnyPool>>>;
30
31pub struct SqlComponent {
32 config: Option<SqlGlobalConfig>,
33 catalog: Option<Arc<dyn DatasourceCatalog>>,
34}
35
36impl SqlComponent {
37 pub fn new() -> Self {
38 Self {
39 config: None,
40 catalog: None,
41 }
42 }
43
44 pub fn with_config(config: SqlGlobalConfig) -> Self {
45 Self {
46 config: Some(config),
47 catalog: None,
48 }
49 }
50
51 pub fn with_optional_config(config: Option<SqlGlobalConfig>) -> Self {
52 Self {
53 config,
54 catalog: None,
55 }
56 }
57
58 pub fn with_config_and_catalog(
59 config: SqlGlobalConfig,
60 catalog: Arc<dyn DatasourceCatalog>,
61 ) -> Self {
62 Self {
63 config: Some(config),
64 catalog: Some(catalog),
65 }
66 }
67
68 pub fn catalog(&self) -> Option<&Arc<dyn DatasourceCatalog>> {
69 self.catalog.as_ref()
70 }
71}
72
73impl Default for SqlComponent {
74 fn default() -> Self {
75 Self::new()
76 }
77}
78
79impl Component for SqlComponent {
80 fn scheme(&self) -> &str {
81 "sql"
82 }
83
84 fn metadata(&self) -> ComponentMetadata {
85 SqlEndpointConfig::metadata()
86 }
87
88 fn create_endpoint(
89 &self,
90 uri: &str,
91 ctx: &dyn camel_component_api::ComponentContext,
92 ) -> Result<Box<dyn Endpoint>, CamelError> {
93 let mut config = SqlEndpointConfig::from_uri(uri)?;
94
95 if config.datasource_name.is_some() && self.catalog.is_none() {
96 return Err(CamelError::Config(
97 "datasource parameter requires catalog — no datasource catalog configured".into(),
98 ));
99 }
100
101 if let Some(ref ds_name) = config.datasource_name
102 && let Some(ref catalog) = self.catalog
103 {
104 let ds_config = catalog.get_config(ds_name).ok_or_else(|| {
105 CamelError::Config(format!("datasource '{}' not found in catalog", ds_name))
106 })?;
107 config.db_url = ds_config.db_url;
108 }
109
110 if let Some(ref global_config) = self.config {
111 config.apply_defaults(global_config);
112 }
113 config.resolve_defaults();
114 let pool: SharedPool = Arc::new(OnceCell::new());
115 let health_check = SqlHealthCheck::new(
116 Arc::clone(&pool),
117 self.catalog.clone(),
118 config.datasource_name.clone(),
119 );
120 ctx.register_current_route_health_check(Arc::new(health_check));
121
122 if config.datasource_name.is_some()
123 && let Some(ref catalog) = self.catalog
124 {
125 Ok(Box::new(endpoint::SqlEndpoint::new_with_pool_and_catalog(
126 uri.to_string(),
127 config,
128 pool,
129 Arc::clone(catalog),
130 )))
131 } else {
132 Ok(Box::new(endpoint::SqlEndpoint::new_with_pool(
133 uri.to_string(),
134 config,
135 pool,
136 )))
137 }
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use camel_component_api::Component;
145 use camel_component_api::NoOpComponentContext;
146
147 #[test]
148 fn test_component_scheme() {
149 let c = SqlComponent::new();
150 assert_eq!(c.scheme(), "sql");
151 }
152
153 #[test]
154 fn test_component_metadata_nonempty() {
155 let c = SqlComponent::new();
156 let meta = c.metadata();
157 assert_eq!(meta.scheme, "sql");
158 assert!(!meta.description.is_empty());
159 assert!(
160 !meta.uri_options.is_empty(),
161 "uri_options must be non-empty"
162 );
163 let names: Vec<&str> = meta.uri_options.iter().map(|o| o.name.as_str()).collect();
164 assert!(
165 names.contains(&"db_url"),
166 "expected db_url in uri_options, got: {names:?}"
167 );
168 assert!(
169 names.contains(&"outputType"),
170 "expected outputType in uri_options, got: {names:?}"
171 );
172 let db_url_opt = meta
174 .uri_options
175 .iter()
176 .find(|o| o.name == "db_url")
177 .unwrap();
178 assert!(db_url_opt.secret, "db_url must be marked secret");
179 }
180
181 #[test]
182 fn test_config_uri_options_nonempty() {
183 let opts = SqlEndpointConfig::uri_options();
184 assert!(!opts.is_empty(), "uri_options() must be non-empty");
185 let names: Vec<&str> = opts.iter().map(|o| o.name.as_str()).collect();
186 assert!(names.contains(&"db_url"));
187 assert!(names.contains(&"delay"));
188 assert!(names.contains(&"batch"));
189 }
190
191 #[test]
192 fn test_config_metadata_capabilities() {
193 let meta = SqlEndpointConfig::metadata();
194 assert!(meta.capabilities.supports_producer);
195 assert!(meta.capabilities.supports_consumer);
196 assert!(!meta.capabilities.supports_polling_consumer);
197 assert!(!meta.capabilities.supports_streaming);
198 }
199
200 #[test]
201 fn test_component_creates_endpoint() {
202 let c = SqlComponent::new();
203 let ctx = NoOpComponentContext;
204 let ep = c.create_endpoint("sql:select 1?db_url=postgres://localhost/test", &ctx);
205 assert!(ep.is_ok());
206 }
207
208 #[test]
209 fn test_component_rejects_wrong_scheme() {
210 let c = SqlComponent::new();
211 let ctx = NoOpComponentContext;
212 let ep = c.create_endpoint("redis://localhost", &ctx);
213 assert!(ep.is_err());
214 }
215
216 #[test]
217 fn test_endpoint_uri() {
218 let c = SqlComponent::new();
219 let ctx = NoOpComponentContext;
220 let ep = c
221 .create_endpoint("sql:select 1?db_url=postgres://localhost/test", &ctx)
222 .unwrap();
223 assert_eq!(ep.uri(), "sql:select 1?db_url=postgres://localhost/test");
224 }
225
226 #[test]
227 fn test_component_with_global_config() {
228 let global = SqlGlobalConfig::default().with_max_connections(20);
229 let c = SqlComponent::with_config(global);
230 let ctx = NoOpComponentContext;
231 assert_eq!(c.scheme(), "sql");
233 let ep = c.create_endpoint("sql:select 1?db_url=postgres://localhost/test", &ctx);
234 assert!(ep.is_ok());
235 }
236
237 #[test]
238 fn test_global_config_applied_to_endpoint() {
239 let global = SqlGlobalConfig::default()
242 .with_max_connections(20)
243 .with_min_connections(3)
244 .with_idle_timeout_secs(600)
245 .with_max_lifetime_secs(3600);
246 let mut cfg =
247 config::SqlEndpointConfig::from_uri("sql:select 1?db_url=postgres://localhost/test")
248 .unwrap();
249 cfg.apply_defaults(&global);
250 cfg.resolve_defaults();
251 assert_eq!(cfg.max_connections, Some(20));
252 assert_eq!(cfg.min_connections, Some(3));
253 assert_eq!(cfg.idle_timeout_secs, Some(600));
254 assert_eq!(cfg.max_lifetime_secs, Some(3600));
255 }
256
257 #[test]
258 fn test_uri_param_wins_over_global_config() {
259 let global = SqlGlobalConfig::default()
261 .with_max_connections(20)
262 .with_min_connections(3);
263 let mut cfg = config::SqlEndpointConfig::from_uri(
264 "sql:select 1?db_url=postgres://localhost/test&maxConnections=99&minConnections=7",
265 )
266 .unwrap();
267 cfg.apply_defaults(&global);
268 cfg.resolve_defaults();
269 assert_eq!(cfg.max_connections, Some(99)); assert_eq!(cfg.min_connections, Some(7)); }
272}