1use camel_api::ExchangeLookupPath;
13use camel_component_api::{Body, CamelError, Exchange};
14
15#[derive(Debug, Clone, PartialEq)]
17pub enum ParamSlot {
18 Positional(usize),
20 Named(String),
22 InClause(String),
24 Expression(String),
26}
27
28#[derive(Debug, Clone)]
30pub struct QueryTemplate {
31 pub fragments: Vec<String>,
33 pub params: Vec<ParamSlot>,
35}
36
37#[derive(Debug, Clone)]
39pub struct PreparedQuery {
40 pub sql: String,
42 pub bindings: Vec<serde_json::Value>,
44}
45
46pub fn parse_query_template(
61 template: &str,
62 placeholder: char,
63) -> Result<QueryTemplate, CamelError> {
64 let mut fragments = Vec::new();
65 let mut params = Vec::new();
66 let mut positional_index = 0usize;
67
68 let chars: Vec<char> = template.chars().collect();
69 let mut i = 0usize;
70 let mut last_param_end = 0usize;
71 let mut in_literal = false;
72
73 while i < chars.len() {
74 if chars[i] == '\'' {
76 if in_literal && i + 1 < chars.len() && chars[i + 1] == '\'' {
78 i += 2;
79 continue;
80 }
81 in_literal = !in_literal;
82 i += 1;
83 continue;
84 }
85
86 if !in_literal && chars[i] == placeholder {
88 if i > 0 && chars[i - 1] == ':' {
90 let is_in_clause = check_in_prefix(&chars, i + 1);
96 let is_expression =
101 i + 2 < chars.len() && chars[i + 1] == '$' && chars[i + 2] == '{';
102
103 if is_in_clause {
104 let name_start = i + 4;
107 let (name, name_end) = extract_param_name(&chars, name_start);
108
109 if name.is_empty() {
110 return Err(CamelError::ProcessorError(format!(
111 "Empty IN clause parameter name at position {}",
112 i
113 )));
114 }
115
116 fragments.push(chars[last_param_end..(i - 1)].iter().collect());
118
119 params.push(ParamSlot::InClause(name));
120 last_param_end = name_end;
121 i = name_end;
122 } else if is_expression {
123 let brace_start = i + 2;
126 if let Some(brace_end) = find_matching_brace(&chars, brace_start) {
127 let expr_content: String =
129 chars[(brace_start + 1)..brace_end].iter().collect();
130
131 if expr_content.is_empty() {
132 return Err(CamelError::ProcessorError(format!(
133 "Empty expression at position {}",
134 i
135 )));
136 }
137
138 fragments.push(chars[last_param_end..(i - 1)].iter().collect());
140
141 params.push(ParamSlot::Expression(expr_content));
142 last_param_end = brace_end + 1;
143 i = brace_end + 1;
144 } else {
145 return Err(CamelError::ProcessorError(format!(
146 "Unclosed expression at position {}",
147 i
148 )));
149 }
150 } else {
151 let name_start = i + 1;
153 let (name, name_end) = extract_param_name(&chars, name_start);
154
155 if name.is_empty() {
156 return Err(CamelError::ProcessorError(format!(
157 "Empty named parameter name at position {}",
158 i
159 )));
160 }
161
162 fragments.push(chars[last_param_end..(i - 1)].iter().collect());
164
165 params.push(ParamSlot::Named(name));
166 last_param_end = name_end;
167 i = name_end;
168 }
169 } else {
170 fragments.push(chars[last_param_end..i].iter().collect());
173
174 params.push(ParamSlot::Positional(positional_index));
175 positional_index += 1;
176 last_param_end = i + 1;
177 i += 1;
178 }
179 } else {
180 i += 1;
181 }
182 }
183
184 fragments.push(chars[last_param_end..].iter().collect());
186
187 Ok(QueryTemplate { fragments, params })
188}
189
190fn check_in_prefix(chars: &[char], start: usize) -> bool {
192 let in_prefix: Vec<char> = "in:".chars().collect();
193 if start + in_prefix.len() > chars.len() {
194 return false;
195 }
196 chars[start..start + in_prefix.len()] == in_prefix[..]
197}
198
199fn extract_param_name(chars: &[char], start: usize) -> (String, usize) {
211 let mut name = String::new();
212 let mut i = start;
213 while i < chars.len() {
214 let c = chars[i];
215 if is_name_terminator(c) {
216 break;
217 }
218 name.push(c);
219 i += 1;
220 }
221 (name, i)
222}
223
224fn is_name_terminator(c: char) -> bool {
226 c.is_whitespace()
227 || matches!(
228 c,
229 '?' | '(' | ')' | ',' | ';' | '=' | '<' | '>' | '!' | '\'' | '"' | '`' | '/' | ':'
230 )
231}
232
233fn find_matching_brace(chars: &[char], start: usize) -> Option<usize> {
236 chars[start..]
238 .iter()
239 .position(|&c| c == '}')
240 .map(|p| start + p)
241}
242
243pub fn resolve_params(
262 tpl: &QueryTemplate,
263 exchange: &Exchange,
264 in_separator: &str,
265) -> Result<PreparedQuery, CamelError> {
266 let mut sql_parts = Vec::new();
267 let mut bindings = Vec::new();
268 let mut placeholder_num = 1usize;
269
270 let body_array = match &exchange.input.body {
273 Body::Json(val) => val.as_array(),
274 _ => None,
275 };
276
277 for (i, param) in tpl.params.iter().enumerate() {
278 sql_parts.push(tpl.fragments[i].clone());
280
281 match param {
282 ParamSlot::Positional(idx) => {
283 let arr = body_array.ok_or_else(|| {
284 CamelError::ProcessorError(
285 "Positional parameter requires body to be a JSON array".to_string(),
286 )
287 })?;
288
289 let value = arr.get(*idx).ok_or_else(|| {
290 CamelError::ProcessorError(format!(
291 "Positional parameter index {} out of bounds (array length {})",
292 idx,
293 arr.len()
294 ))
295 })?;
296
297 sql_parts.push(format!("${}", placeholder_num));
298 placeholder_num += 1;
299 bindings.push(value.clone());
300 }
301 ParamSlot::Named(name) => {
302 let value = resolve_named_param(name, exchange)?;
303 sql_parts.push(format!("${}", placeholder_num));
304 placeholder_num += 1;
305 bindings.push(value);
306 }
307 ParamSlot::InClause(name) => {
308 let value = resolve_named_param(name, exchange)?;
309
310 let arr = value.as_array().ok_or_else(|| {
311 CamelError::ProcessorError(format!(
312 "IN clause parameter '{}' must be an array, got type {}",
313 name,
314 match &value {
315 serde_json::Value::Null => "null",
316 serde_json::Value::Bool(_) => "bool",
317 serde_json::Value::Number(_) => "number",
318 serde_json::Value::String(_) => "string",
319 serde_json::Value::Array(_) => "array",
320 serde_json::Value::Object(_) => "object",
321 }
322 ))
323 })?;
324
325 if arr.is_empty() {
326 sql_parts.push("NULL".to_string());
329 } else {
330 let placeholders: Vec<String> = arr
331 .iter()
332 .map(|_| {
333 let p = format!("${}", placeholder_num);
334 placeholder_num += 1;
335 p
336 })
337 .collect();
338
339 sql_parts.push(placeholders.join(in_separator));
341 bindings.extend(arr.iter().cloned());
342 }
343 }
344 ParamSlot::Expression(expr) => {
345 let value = resolve_expression_param(expr, exchange)?;
346 sql_parts.push(format!("${}", placeholder_num));
347 placeholder_num += 1;
348 bindings.push(value);
349 }
350 }
351 }
352
353 sql_parts.push(tpl.fragments.last().cloned().unwrap_or_default());
355
356 Ok(PreparedQuery {
357 sql: sql_parts.join(""),
358 bindings,
359 })
360}
361
362fn resolve_named_param(name: &str, exchange: &Exchange) -> Result<serde_json::Value, CamelError> {
372 let path = ExchangeLookupPath::parse(name).map_err(|e| {
373 CamelError::ProcessorError(format!("Invalid placeholder name {name:?}: {e}"))
374 })?;
375 if let ExchangeLookupPath::Body(segments) = &path
379 && segments.is_empty()
380 {
381 return Err(CamelError::ProcessorError(format!(
382 "SQL placeholder ':#{name}' requires a path (e.g. ':#{name}.field') — bare scope not supported"
383 )));
384 }
385 path.lookup(exchange).ok_or_else(|| {
386 CamelError::ProcessorError(format!(
387 "Named parameter '{name}' not found in body, headers, or properties"
388 ))
389 })
390}
391
392fn resolve_expression_param(
398 expr: &str,
399 exchange: &Exchange,
400) -> Result<serde_json::Value, CamelError> {
401 let path = ExchangeLookupPath::parse(expr)
405 .map_err(|e| CamelError::ProcessorError(format!("Invalid expression {expr:?}: {e}")))?;
406 if let ExchangeLookupPath::Body(segments) = &path
409 && segments.is_empty()
410 {
411 return Err(CamelError::ProcessorError(format!(
412 "SQL expression ':#${{{expr}}}' requires a path (e.g. ':#${{{expr}.field}}') — bare scope not supported"
413 )));
414 }
415 path.lookup(exchange).ok_or_else(|| {
416 CamelError::ProcessorError(format!(
417 "Expression '{expr}' not found in body, headers, or properties"
418 ))
419 })
420}
421
422pub fn is_select_query(sql: &str) -> bool {
424 let trimmed = sql.trim_start().to_uppercase();
425 if trimmed.starts_with("WITH") {
426 return trimmed.contains("SELECT")
427 && !trimmed.contains("INSERT INTO")
428 && !trimmed.contains("UPDATE ")
429 && !trimmed.contains("DELETE FROM");
430 }
431
432 trimmed.starts_with("SELECT")
433 || trimmed.starts_with("VALUES")
434 || trimmed.starts_with("TABLE")
435 || trimmed.starts_with("SHOW")
436 || trimmed.starts_with("EXPLAIN")
437}
438
439const DEFAULT_IN_SEPARATOR: &str = ", ";
440
441pub fn resolve_params_default(
442 tpl: &QueryTemplate,
443 exchange: &Exchange,
444) -> Result<PreparedQuery, CamelError> {
445 resolve_params(tpl, exchange, DEFAULT_IN_SEPARATOR)
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451 use camel_component_api::{Body, Exchange, Message};
452
453 #[test]
454 fn parse_no_params() {
455 let tpl = parse_query_template("select * from users", '#').unwrap();
456 assert_eq!(tpl.fragments.len(), 1);
457 assert!(tpl.params.is_empty());
458 }
459
460 #[test]
461 fn parse_positional_params() {
462 let tpl = parse_query_template("insert into t values (#, #)", '#').unwrap();
463 assert_eq!(tpl.params.len(), 2);
464 assert!(matches!(tpl.params[0], ParamSlot::Positional(0)));
465 assert!(matches!(tpl.params[1], ParamSlot::Positional(1)));
466 }
467
468 #[test]
469 fn parse_named_params() {
470 let tpl =
471 parse_query_template("select * from t where id = :#id and name = :#name", '#').unwrap();
472 assert_eq!(tpl.params.len(), 2);
473 assert!(matches!(&tpl.params[0], ParamSlot::Named(n) if n == "id"));
474 assert!(matches!(&tpl.params[1], ParamSlot::Named(n) if n == "name"));
475 }
476
477 #[test]
478 fn parse_mixed_params() {
479 let tpl =
480 parse_query_template("select * from t where id = :#id and status = #", '#').unwrap();
481 assert_eq!(tpl.params.len(), 2);
482 assert!(matches!(&tpl.params[0], ParamSlot::Named(n) if n == "id"));
483 assert!(matches!(tpl.params[1], ParamSlot::Positional(0)));
484 }
485
486 #[test]
487 fn parse_in_clause() {
488 let tpl = parse_query_template("select * from t where id in (:#in:ids)", '#').unwrap();
489 assert_eq!(tpl.params.len(), 1);
490 assert!(matches!(&tpl.params[0], ParamSlot::InClause(n) if n == "ids"));
491 }
492
493 #[test]
494 fn resolve_named_from_headers() {
495 let tpl = parse_query_template("select * from t where id = :#id", '#').unwrap();
496 let mut msg = Message::default();
497 msg.set_header("id", serde_json::json!(42));
498 let ex = Exchange::new(msg);
499
500 let prepared = resolve_params(&tpl, &ex, ", ").unwrap();
501 assert_eq!(prepared.sql, "select * from t where id = $1");
502 assert_eq!(prepared.bindings.len(), 1);
503 assert_eq!(prepared.bindings[0], serde_json::json!(42));
504 }
505
506 #[test]
507 fn resolve_named_from_body_map() {
508 let tpl = parse_query_template("select * from t where id = :#id", '#').unwrap();
509 let msg = Message::new(Body::Json(serde_json::json!({"id": 99})));
510 let ex = Exchange::new(msg);
511
512 let prepared = resolve_params(&tpl, &ex, ", ").unwrap();
513 assert_eq!(prepared.bindings[0], serde_json::json!(99));
514 }
515
516 #[test]
517 fn resolve_positional_from_body_array() {
518 let tpl = parse_query_template("insert into t values (#, #)", '#').unwrap();
519 let msg = Message::new(Body::Json(serde_json::json!(["foo", 42])));
520 let ex = Exchange::new(msg);
521
522 let prepared = resolve_params(&tpl, &ex, ", ").unwrap();
523 assert_eq!(prepared.sql, "insert into t values ($1, $2)");
524 assert_eq!(prepared.bindings[0], serde_json::json!("foo"));
525 assert_eq!(prepared.bindings[1], serde_json::json!(42));
526 }
527
528 #[test]
529 fn resolve_named_from_properties() {
530 let tpl = parse_query_template("select * from t where id = :#myProp", '#').unwrap();
531 let mut ex = Exchange::new(Message::default());
532 ex.set_property("myProp", serde_json::json!(7));
533
534 let prepared = resolve_params(&tpl, &ex, ", ").unwrap();
535 assert_eq!(prepared.bindings[0], serde_json::json!(7));
536 }
537
538 #[test]
539 fn resolve_named_not_found() {
540 let tpl = parse_query_template("select * from t where id = :#missing", '#').unwrap();
541 let ex = Exchange::new(Message::default());
542
543 let result = resolve_params(&tpl, &ex, ", ");
544 assert!(result.is_err());
545 }
546
547 #[test]
548 fn resolve_in_clause_expansion() {
549 let tpl = parse_query_template("select * from t where id in (:#in:ids)", '#').unwrap();
550 let mut msg = Message::default();
551 msg.set_header("ids", serde_json::json!([1, 2, 3]));
552 let ex = Exchange::new(msg);
553
554 let prepared = resolve_params(&tpl, &ex, ", ").unwrap();
555 assert_eq!(prepared.sql, "select * from t where id in ($1, $2, $3)");
556 assert_eq!(
557 prepared.bindings,
558 vec![
559 serde_json::json!(1),
560 serde_json::json!(2),
561 serde_json::json!(3)
562 ]
563 );
564 }
565
566 #[test]
567 fn build_sql_correct_placeholders() {
568 let tpl = parse_query_template(
569 "select * from t where a = :#x and b = # and c in (:#in:ids)",
570 '#',
571 )
572 .unwrap();
573 let mut msg = Message::new(Body::Json(serde_json::json!(["pos_val"])));
574 msg.set_header("x", serde_json::json!("hello"));
575 msg.set_header("ids", serde_json::json!([10, 20]));
576 let ex = Exchange::new(msg);
577
578 let prepared = resolve_params(&tpl, &ex, ", ").unwrap();
579 assert_eq!(
580 prepared.sql,
581 "select * from t where a = $1 and b = $2 and c in ($3, $4)"
582 );
583 assert_eq!(prepared.bindings.len(), 4);
584 }
585
586 #[test]
587 fn is_select() {
588 assert!(is_select_query("SELECT * FROM t"));
589 assert!(is_select_query(" select * from t"));
590 assert!(is_select_query("WITH cte AS (SELECT 1) SELECT * FROM cte"));
591 assert!(is_select_query(
592 "with results as (select 1) select * from results"
593 ));
594 assert!(!is_select_query(
595 "WITH cte AS (SELECT id FROM t) INSERT INTO other SELECT * FROM cte"
596 ));
597 assert!(is_select_query("TABLE users"));
598 assert!(is_select_query("SHOW TABLES"));
599 assert!(is_select_query("EXPLAIN SELECT * FROM t"));
600 assert!(!is_select_query("INSERT INTO t VALUES (1)"));
601 assert!(!is_select_query("UPDATE t SET x = 1"));
602 assert!(!is_select_query("DELETE FROM t"));
603 }
604
605 #[test]
606 fn parse_trailing_param() {
607 let tpl = parse_query_template("select * from t where id = #", '#').unwrap();
608 assert_eq!(tpl.params.len(), 1);
609 assert_eq!(tpl.fragments.len(), 2);
610 assert_eq!(tpl.fragments[0], "select * from t where id = ");
611 assert_eq!(tpl.fragments[1], "");
612 }
613
614 #[test]
615 fn parse_leading_param() {
616 let tpl = parse_query_template("# = id", '#').unwrap();
617 assert_eq!(tpl.params.len(), 1);
618 assert_eq!(tpl.fragments.len(), 2);
619 assert_eq!(tpl.fragments[0], "");
620 assert_eq!(tpl.fragments[1], " = id");
621 }
622
623 #[test]
624 fn parse_consecutive_params() {
625 let tpl = parse_query_template("# # #", '#').unwrap();
626 assert_eq!(tpl.params.len(), 3);
627 assert_eq!(tpl.fragments.len(), 4);
628 assert_eq!(tpl.fragments[0], "");
629 assert_eq!(tpl.fragments[1], " ");
630 assert_eq!(tpl.fragments[2], " ");
631 assert_eq!(tpl.fragments[3], "");
632 }
633
634 #[test]
635 fn resolution_priority_body_over_headers() {
636 let tpl = parse_query_template("select * from t where id = :#id", '#').unwrap();
638 let mut msg = Message::new(Body::Json(serde_json::json!({"id": 1})));
639 msg.set_header("id", serde_json::json!(2)); let ex = Exchange::new(msg);
641
642 let prepared = resolve_params(&tpl, &ex, ", ").unwrap();
643 assert_eq!(prepared.bindings[0], serde_json::json!(1)); }
645
646 #[test]
647 fn resolution_priority_headers_over_properties() {
648 let tpl = parse_query_template("select * from t where id = :#id", '#').unwrap();
650 let mut msg = Message::default();
651 msg.set_header("id", serde_json::json!(10));
652 let mut ex = Exchange::new(msg);
653 ex.set_property("id", serde_json::json!(20)); let prepared = resolve_params(&tpl, &ex, ", ").unwrap();
656 assert_eq!(prepared.bindings[0], serde_json::json!(10)); }
658
659 #[test]
660 fn custom_placeholder_char() {
661 let tpl = parse_query_template("select * from t where id = :@id", '@').unwrap();
662 assert_eq!(tpl.params.len(), 1);
663 assert!(matches!(&tpl.params[0], ParamSlot::Named(n) if n == "id"));
664 }
665
666 #[test]
667 fn parse_expression_param() {
668 let tpl = parse_query_template("select * from t where id = :#${body.id}", '#').unwrap();
669 assert_eq!(tpl.params.len(), 1);
670 assert!(matches!(&tpl.params[0], ParamSlot::Expression(e) if e == "body.id"));
671 }
672
673 #[test]
674 fn resolve_expression_from_body() {
675 let tpl = parse_query_template("select * from t where id = :#${body.id}", '#').unwrap();
676 let msg = Message::new(Body::Json(serde_json::json!({"id": 42})));
677 let ex = Exchange::new(msg);
678 let prepared = resolve_params(&tpl, &ex, ", ").unwrap();
679 assert_eq!(prepared.sql, "select * from t where id = $1");
680 assert_eq!(prepared.bindings[0], serde_json::json!(42));
681 }
682
683 #[test]
684 fn resolve_expression_from_header() {
685 let tpl =
686 parse_query_template("select * from t where name = :#${header.name}", '#').unwrap();
687 let mut msg = Message::default();
688 msg.set_header("name", serde_json::json!("alice"));
689 let ex = Exchange::new(msg);
690 let prepared = resolve_params(&tpl, &ex, ", ").unwrap();
691 assert_eq!(prepared.bindings[0], serde_json::json!("alice"));
692 }
693
694 #[test]
695 fn resolve_expression_from_property() {
696 let tpl =
697 parse_query_template("select * from t where k = :#${property.myKey}", '#').unwrap();
698 let mut ex = Exchange::new(Message::default());
699 ex.set_property("myKey", serde_json::json!(99));
700 let prepared = resolve_params(&tpl, &ex, ", ").unwrap();
701 assert_eq!(prepared.bindings[0], serde_json::json!(99));
702 }
703
704 #[test]
705 fn parse_hash_in_string_literal() {
706 let tpl =
708 parse_query_template("select * from t where x = '#literal' and id = #", '#').unwrap();
709 assert_eq!(tpl.params.len(), 1);
710 assert!(matches!(tpl.params[0], ParamSlot::Positional(0)));
711 }
712
713 #[test]
714 fn parse_escaped_quote_in_literal() {
715 let tpl =
717 parse_query_template("select * from t where x = 'it''s' and id = #", '#').unwrap();
718 assert_eq!(tpl.params.len(), 1);
719 assert!(matches!(tpl.params[0], ParamSlot::Positional(0)));
720 }
721
722 #[test]
723 fn empty_in_clause_produces_null() {
724 let tpl = parse_query_template("select * from t where id in (:#in:ids)", '#').unwrap();
725 let mut msg = Message::default();
726 msg.set_header("ids", serde_json::json!([]));
727 let ex = Exchange::new(msg);
728 let prepared = resolve_params(&tpl, &ex, ", ").unwrap();
729 assert_eq!(prepared.sql, "select * from t where id in (NULL)");
730 assert!(prepared.bindings.is_empty());
731 }
732
733 #[test]
734 fn in_clause_custom_separator() {
735 let tpl = parse_query_template("select * from t where id in (:#in:ids)", '#').unwrap();
736 let mut msg = Message::default();
737 msg.set_header("ids", serde_json::json!([1, 2, 3]));
738 let ex = Exchange::new(msg);
739
740 let prepared = resolve_params(&tpl, &ex, ";").unwrap();
741 assert_eq!(prepared.sql, "select * from t where id in ($1;$2;$3)");
742 }
743
744 #[test]
745 fn parse_kebab_case_named_param() {
746 let tpl = parse_query_template("select * from t where id = :#my-param", '#').unwrap();
750 assert_eq!(tpl.params.len(), 1);
751 assert!(
752 matches!(&tpl.params[0], ParamSlot::Named(n) if n == "my-param"),
753 "got {:?}",
754 tpl.params[0]
755 );
756 assert_eq!(tpl.fragments[0], "select * from t where id = ");
757 assert_eq!(tpl.fragments[1], ""); }
759
760 #[test]
761 fn parse_dotted_named_param() {
762 let tpl = parse_query_template("select * from t where x = :#body.user.name", '#').unwrap();
764 assert!(
765 matches!(&tpl.params[0], ParamSlot::Named(n) if n == "body.user.name"),
766 "got {:?}",
767 tpl.params[0]
768 );
769 assert_eq!(tpl.fragments[1], "");
770 }
771
772 #[test]
773 fn parse_named_param_terminated_by_paren() {
774 let tpl = parse_query_template("fn(:#id)", '#').unwrap();
776 assert!(
777 matches!(&tpl.params[0], ParamSlot::Named(n) if n == "id"),
778 "got {:?}",
779 tpl.params[0]
780 );
781 assert_eq!(tpl.fragments[1], ")");
782 }
783
784 #[test]
785 fn parse_named_param_terminated_by_comma() {
786 let tpl = parse_query_template("fn(:#a, :#b)", '#').unwrap();
787 assert!(matches!(&tpl.params[0], ParamSlot::Named(n) if n == "a"));
788 assert!(matches!(&tpl.params[1], ParamSlot::Named(n) if n == "b"));
789 }
790
791 #[test]
792 fn parse_named_param_terminated_by_equals() {
793 let tpl = parse_query_template("select :#x=1", '#').unwrap();
794 assert!(matches!(&tpl.params[0], ParamSlot::Named(n) if n == "x"));
795 assert_eq!(tpl.fragments[1], "=1");
796 }
797
798 #[test]
799 fn parse_named_param_terminated_by_semicolon() {
800 let tpl = parse_query_template("select :#x;", '#').unwrap();
801 assert!(matches!(&tpl.params[0], ParamSlot::Named(n) if n == "x"));
802 assert_eq!(tpl.fragments[1], ";");
803 }
804
805 #[test]
806 fn parse_in_clause_kebab_case_name() {
807 let tpl = parse_query_template("select * from t where id in (:#in:my-ids)", '#').unwrap();
809 assert!(
810 matches!(&tpl.params[0], ParamSlot::InClause(n) if n == "my-ids"),
811 "got {:?}",
812 tpl.params[0]
813 );
814 }
815
816 #[test]
817 fn resolve_named_dotted_body_path_walks_json() {
818 let tpl =
822 parse_query_template("select * from users where name = :#body.user.name", '#').unwrap();
823 assert!(
824 matches!(&tpl.params[0], ParamSlot::Named(n) if n == "body.user.name"),
825 "got {:?}",
826 tpl.params[0]
827 );
828
829 let msg = Message::new(Body::Json(serde_json::json!({
830 "user": { "name": "alice" }
831 })));
832 let ex = Exchange::new(msg);
833
834 let prepared = resolve_params(&tpl, &ex, ", ").unwrap();
835 assert_eq!(prepared.sql, "select * from users where name = $1");
836 assert_eq!(prepared.bindings[0], serde_json::json!("alice"));
837 }
838
839 #[test]
840 fn resolve_named_header_scope_flat_dotted_key() {
841 let tpl =
843 parse_query_template("select * from t where x = :#header.correlation.id", '#').unwrap();
844 let mut msg = Message::default();
845 msg.set_header("correlation.id", serde_json::json!("abc-123"));
846 let ex = Exchange::new(msg);
847
848 let prepared = resolve_params(&tpl, &ex, ", ").unwrap();
849 assert_eq!(prepared.bindings[0], serde_json::json!("abc-123"));
850 }
851
852 #[test]
853 fn resolve_named_property_scope_alias() {
854 let tpl =
856 parse_query_template("select * from t where x = :#exchangeProperty.tenantId", '#')
857 .unwrap();
858 let mut ex = Exchange::new(Message::default());
859 ex.set_property("tenantId", serde_json::json!("acme"));
860
861 let prepared = resolve_params(&tpl, &ex, ", ").unwrap();
862 assert_eq!(prepared.bindings[0], serde_json::json!("acme"));
863 }
864
865 #[test]
866 fn resolve_named_unscoped_kebab_case_fallback_to_property() {
867 let tpl = parse_query_template("select * from t where x = :#my-param", '#').unwrap();
869 let mut ex = Exchange::new(Message::default());
870 ex.set_property("my-param", serde_json::json!(7));
871
872 let prepared = resolve_params(&tpl, &ex, ", ").unwrap();
873 assert_eq!(prepared.bindings[0], serde_json::json!(7));
874 }
875
876 #[test]
877 fn resolve_expression_nested_body_walk() {
878 let tpl = parse_query_template("select * from users where name = :#${body.user.name}", '#')
880 .unwrap();
881 let msg = Message::new(Body::Json(serde_json::json!({
882 "user": { "name": "bob" }
883 })));
884 let ex = Exchange::new(msg);
885
886 let prepared = resolve_params(&tpl, &ex, ", ").unwrap();
887 assert_eq!(prepared.bindings[0], serde_json::json!("bob"));
888 }
889
890 #[test]
891 fn resolve_named_missing_returns_clear_error() {
892 let tpl = parse_query_template("select * from t where x = :#nope", '#').unwrap();
893 let ex = Exchange::new(Message::default());
894
895 let err = resolve_params(&tpl, &ex, ", ").unwrap_err();
896 let msg = format!("{err}");
897 assert!(
898 msg.contains("nope"),
899 "error should mention the missing name: {msg}"
900 );
901 }
902
903 #[test]
904 fn resolve_named_bare_body_rejected() {
905 let tpl = parse_query_template("select * from t where x = :#body", '#').unwrap();
910 let ex = Exchange::new(Message::new(Body::Json(serde_json::json!({"a": 1}))));
911
912 let err = resolve_params(&tpl, &ex, ", ").unwrap_err();
913 let msg = format!("{err}");
914 assert!(
915 msg.contains("body") && (msg.contains("path") || msg.contains("requires")),
916 "bare ':#body' must be rejected with a clear message mentioning path requirement: {msg}"
917 );
918 }
919
920 #[test]
921 fn resolve_named_bare_header_property_exchange_property_rejected() {
922 for bare in ["header", "property", "exchangeProperty"] {
926 let sql = format!("select * from t where x = :#{bare}");
927 let tpl = parse_query_template(&sql, '#').unwrap();
928 let ex = Exchange::new(Message::default());
929 let err = resolve_params(&tpl, &ex, ", ").unwrap_err();
930 let msg = format!("{err}");
931 assert!(
932 msg.contains(bare),
933 "bare ':#{bare}' must error with a message mentioning the scope name: {msg}"
934 );
935 }
936 }
937
938 #[test]
939 fn resolve_expression_bare_body_rejected() {
940 let tpl = parse_query_template("select * from t where x = :#${body}", '#').unwrap();
943 let ex = Exchange::new(Message::new(Body::Json(serde_json::json!({"a": 1}))));
944
945 let err = resolve_params(&tpl, &ex, ", ").unwrap_err();
946 let msg = format!("{err}");
947 assert!(
948 msg.contains("body") && (msg.contains("path") || msg.contains("requires")),
949 "bare ':#${{body}}' must be rejected: {msg}"
950 );
951 }
952
953 #[test]
954 fn parse_postgres_cast_after_placeholder_leaves_double_colon_as_sql() {
955 let tpl = parse_query_template("select :#id::text as id_text from t", '#').unwrap();
959 assert!(
960 matches!(&tpl.params[0], ParamSlot::Named(n) if n == "id"),
961 "got {:?}",
962 tpl.params[0]
963 );
964 assert_eq!(tpl.fragments[0], "select ");
965 assert_eq!(tpl.fragments[1], "::text as id_text from t");
966 }
967
968 #[test]
969 fn resolve_postgres_cast_binds_id_value() {
970 let tpl = parse_query_template("select :#id::text as id_text from t", '#').unwrap();
971 let mut msg = Message::default();
972 msg.set_header("id", serde_json::json!(42));
973 let ex = Exchange::new(msg);
974
975 let prepared = resolve_params(&tpl, &ex, ", ").unwrap();
976 assert_eq!(prepared.sql, "select $1::text as id_text from t");
977 assert_eq!(prepared.bindings[0], serde_json::json!(42));
978 }
979
980 #[test]
981 fn parse_single_colon_terminates_name() {
982 let tpl = parse_query_template("select :#id:x from t", '#').unwrap();
985 assert!(
986 matches!(&tpl.params[0], ParamSlot::Named(n) if n == "id"),
987 "got {:?}",
988 tpl.params[0]
989 );
990 assert_eq!(tpl.fragments[1], ":x from t");
991 }
992
993 #[test]
994 fn resolve_in_clause_kebab_case_name_from_property() {
995 let tpl = parse_query_template("select * from t where id in (:#in:my-ids)", '#').unwrap();
998 let mut ex = Exchange::new(Message::default());
999 ex.set_property("my-ids", serde_json::json!([1, 2, 3]));
1000
1001 let prepared = resolve_params(&tpl, &ex, ", ").unwrap();
1002 assert_eq!(prepared.sql, "select * from t where id in ($1, $2, $3)");
1003 assert_eq!(
1004 prepared.bindings,
1005 vec![
1006 serde_json::json!(1),
1007 serde_json::json!(2),
1008 serde_json::json!(3)
1009 ]
1010 );
1011 }
1012}