1use std::sync::Arc;
11
12use parking_lot::RwLock;
13
14use crate::error::{BackendError, StorageResult};
15use crate::search::SearchParameterRegistry;
16use crate::types::{ChainConfig, ReverseChainedParameter, SearchParamType, SearchValue};
17
18use super::query_builder::{SqlFragment, SqlParam};
19
20#[derive(Debug, Clone)]
22pub struct ChainLink {
23 pub reference_param: String,
25 pub target_type: String,
27}
28
29#[derive(Debug, Clone)]
31pub struct ParsedChain {
32 pub links: Vec<ChainLink>,
34 pub terminal_param: String,
36 pub terminal_type: SearchParamType,
38}
39
40#[derive(Debug, Clone)]
42pub enum ChainError {
43 MaxDepthExceeded {
45 depth: usize,
47 max: usize,
49 },
50 UnknownReferenceParam {
52 resource_type: String,
54 param: String,
56 },
57 AmbiguousTargetType {
59 resource_type: String,
61 param: String,
63 },
64 UnknownTerminalParam {
66 resource_type: String,
68 param: String,
70 },
71 EmptyChain,
73 InvalidSyntax {
75 message: String,
77 },
78}
79
80impl std::fmt::Display for ChainError {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 match self {
83 ChainError::MaxDepthExceeded { depth, max } => {
84 write!(
85 f,
86 "Chain depth {} exceeds maximum allowed depth {}",
87 depth, max
88 )
89 }
90 ChainError::UnknownReferenceParam {
91 resource_type,
92 param,
93 } => {
94 write!(
95 f,
96 "Unknown reference parameter '{}' for resource type '{}'",
97 param, resource_type
98 )
99 }
100 ChainError::AmbiguousTargetType {
101 resource_type,
102 param,
103 } => {
104 write!(
105 f,
106 "Ambiguous target type for parameter '{}' on '{}'. Use type modifier.",
107 param, resource_type
108 )
109 }
110 ChainError::UnknownTerminalParam {
111 resource_type,
112 param,
113 } => {
114 write!(
115 f,
116 "Unknown terminal parameter '{}' for resource type '{}'",
117 param, resource_type
118 )
119 }
120 ChainError::EmptyChain => write!(f, "Empty chain"),
121 ChainError::InvalidSyntax { message } => write!(f, "Invalid chain syntax: {}", message),
122 }
123 }
124}
125
126impl From<ChainError> for BackendError {
127 fn from(e: ChainError) -> Self {
128 BackendError::Internal {
129 backend_name: "sqlite".to_string(),
130 message: e.to_string(),
131 source: None,
132 }
133 }
134}
135
136pub struct ChainQueryBuilder {
141 tenant_id: String,
143 base_type: String,
145 registry: Arc<RwLock<SearchParameterRegistry>>,
147 config: ChainConfig,
149 param_offset: usize,
151}
152
153impl ChainQueryBuilder {
154 pub fn new(
156 tenant_id: impl Into<String>,
157 base_type: impl Into<String>,
158 registry: Arc<RwLock<SearchParameterRegistry>>,
159 ) -> Self {
160 Self {
161 tenant_id: tenant_id.into(),
162 base_type: base_type.into(),
163 registry,
164 config: ChainConfig::default(),
165 param_offset: 2, }
167 }
168
169 pub fn with_config(mut self, config: ChainConfig) -> Self {
171 self.config = config;
172 self
173 }
174
175 pub fn with_param_offset(mut self, offset: usize) -> Self {
177 self.param_offset = offset;
178 self
179 }
180
181 pub fn parse_chain(&self, chain_str: &str) -> Result<ParsedChain, ChainError> {
191 if chain_str.is_empty() {
192 return Err(ChainError::EmptyChain);
193 }
194
195 let parts: Vec<&str> = chain_str.split('.').collect();
196 if parts.len() < 2 {
197 return Err(ChainError::InvalidSyntax {
198 message: "Chain must have at least two parts (reference.param)".to_string(),
199 });
200 }
201
202 let chain_depth = parts.len() - 1; if !self.config.validate_forward_depth(chain_depth) {
205 return Err(ChainError::MaxDepthExceeded {
206 depth: chain_depth,
207 max: self.config.max_forward_depth,
208 });
209 }
210
211 let mut links = Vec::new();
212 let mut current_type = self.base_type.clone();
213
214 for part in parts.iter().take(parts.len() - 1) {
216 let (ref_param, explicit_type) = self.parse_chain_part(part);
217
218 let target_type = self.resolve_target_type(¤t_type, &ref_param, explicit_type)?;
220
221 links.push(ChainLink {
222 reference_param: ref_param,
223 target_type: target_type.clone(),
224 });
225
226 current_type = target_type;
227 }
228
229 let terminal_param = parts[parts.len() - 1].to_string();
231 let terminal_type = self.resolve_terminal_type(¤t_type, &terminal_param)?;
232
233 Ok(ParsedChain {
234 links,
235 terminal_param,
236 terminal_type,
237 })
238 }
239
240 fn parse_chain_part(&self, part: &str) -> (String, Option<String>) {
244 if let Some((param, type_mod)) = part.split_once(':') {
245 (param.to_string(), Some(type_mod.to_string()))
246 } else {
247 (part.to_string(), None)
248 }
249 }
250
251 fn resolve_target_type(
257 &self,
258 resource_type: &str,
259 ref_param: &str,
260 explicit_type: Option<String>,
261 ) -> Result<String, ChainError> {
262 if let Some(t) = explicit_type {
264 return Ok(t);
265 }
266
267 let registry = self.registry.read();
269 if let Some(param_def) = registry.get_param(resource_type, ref_param) {
270 if param_def.param_type != SearchParamType::Reference {
272 return Err(ChainError::UnknownReferenceParam {
273 resource_type: resource_type.to_string(),
274 param: ref_param.to_string(),
275 });
276 }
277
278 if let Some(ref targets) = param_def.target {
280 if targets.len() == 1 {
281 return Ok(targets[0].clone());
282 } else if targets.is_empty() {
283 return Ok(crate::search::chain_resolver::infer_target_type(ref_param));
285 } else {
286 return Ok(crate::search::chain_resolver::infer_target_type(ref_param));
290 }
291 }
292 }
293
294 Ok(crate::search::chain_resolver::infer_target_type(ref_param))
296 }
297
298 fn resolve_terminal_type(
300 &self,
301 resource_type: &str,
302 param_name: &str,
303 ) -> Result<SearchParamType, ChainError> {
304 let registry = self.registry.read();
305 if let Some(param_def) = registry.get_param(resource_type, param_name) {
306 Ok(param_def.param_type)
307 } else {
308 match param_name {
310 "_id" | "id" => Ok(SearchParamType::Token),
311 "name" | "family" | "given" | "text" | "display" => Ok(SearchParamType::String),
312 "identifier" | "code" | "status" | "type" | "category" => {
313 Ok(SearchParamType::Token)
314 }
315 _ => Err(ChainError::UnknownTerminalParam {
316 resource_type: resource_type.to_string(),
317 param: param_name.to_string(),
318 }),
319 }
320 }
321 }
322
323 pub fn build_forward_chain_sql(
350 &self,
351 chain: &ParsedChain,
352 value: &SearchValue,
353 ) -> StorageResult<SqlFragment> {
354 if chain.links.is_empty() {
355 return Err(BackendError::Internal {
356 backend_name: "sqlite".to_string(),
357 message: "Empty chain".to_string(),
358 source: None,
359 }
360 .into());
361 }
362
363 let param_num = self.param_offset + 1;
365
366 let (terminal_sql, terminal_param) =
368 self.build_terminal_condition(chain, value, param_num)?;
369
370 let terminal_type = &chain.links[chain.links.len() - 1].target_type;
372
373 let mut current_sql = format!(
375 "SELECT '{}/{}' || si{}.resource_id FROM search_index si{} \
376 WHERE si{}.tenant_id = ?1 AND si{}.resource_type = '{}' \
377 AND si{}.param_name = '{}' AND {}",
378 terminal_type,
379 "", chain.links.len(),
381 chain.links.len(),
382 chain.links.len(),
383 chain.links.len(),
384 terminal_type,
385 chain.links.len(),
386 chain.terminal_param,
387 terminal_sql
388 );
389
390 for (i, link) in chain.links.iter().enumerate().rev() {
392 let link_num = i + 1;
393 let current_type = if i == 0 {
395 &self.base_type
396 } else {
397 &chain.links[i - 1].target_type
398 };
399
400 if i == 0 {
401 current_sql = format!(
403 "SELECT si{link_num}.resource_id FROM search_index si{link_num} \
404 WHERE si{link_num}.tenant_id = ?1 AND si{link_num}.resource_type = '{current_type}' \
405 AND si{link_num}.param_name = '{ref_param}' \
406 AND si{link_num}.value_reference IN ({inner})",
407 link_num = link_num,
408 current_type = current_type,
409 ref_param = link.reference_param,
410 inner = current_sql
411 );
412 } else {
413 current_sql = format!(
415 "SELECT '{current_type}/' || si{link_num}.resource_id FROM search_index si{link_num} \
416 WHERE si{link_num}.tenant_id = ?1 AND si{link_num}.resource_type = '{current_type}' \
417 AND si{link_num}.param_name = '{ref_param}' \
418 AND si{link_num}.value_reference IN ({inner})",
419 current_type = current_type,
420 link_num = link_num,
421 ref_param = link.reference_param,
422 inner = current_sql
423 );
424 }
425 }
426
427 let final_sql = format!("r.id IN ({})", current_sql);
429
430 Ok(SqlFragment::with_params(final_sql, vec![terminal_param]))
431 }
432
433 fn build_terminal_condition(
435 &self,
436 chain: &ParsedChain,
437 value: &SearchValue,
438 param_num: usize,
439 ) -> StorageResult<(String, SqlParam)> {
440 let alias_num = chain.links.len();
441 let alias = format!("si{}", alias_num);
442
443 let (condition, param) = match chain.terminal_type {
444 SearchParamType::String => {
445 let escaped = value.value.replace('%', "\\%").replace('_', "\\_");
446 (
447 format!("{}.value_string LIKE ?{} ESCAPE '\\'", alias, param_num),
448 SqlParam::String(format!("%{}%", escaped)),
449 )
450 }
451 SearchParamType::Token => {
452 if let Some((system, code)) = value.value.split_once('|') {
454 if system.is_empty() {
455 (
456 format!(
457 "({}.value_token_system IS NULL OR {}.value_token_system = '') \
458 AND {}.value_token_code = ?{}",
459 alias, alias, alias, param_num
460 ),
461 SqlParam::String(code.to_string()),
462 )
463 } else {
464 (
465 format!(
466 "{}.value_token_system = '{}' AND {}.value_token_code = ?{}",
467 alias,
468 system.replace('\'', "''"),
469 alias,
470 param_num
471 ),
472 SqlParam::String(code.to_string()),
473 )
474 }
475 } else {
476 (
477 format!("{}.value_token_code = ?{}", alias, param_num),
478 SqlParam::String(value.value.clone()),
479 )
480 }
481 }
482 SearchParamType::Reference => (
483 format!("{}.value_reference LIKE ?{}", alias, param_num),
484 SqlParam::String(format!("%{}%", value.value)),
485 ),
486 SearchParamType::Date => {
487 let date_col = format!("{}.value_date", alias);
489 build_date_condition(&date_col, value, param_num)
490 }
491 SearchParamType::Number => {
492 let num_col = format!("{}.value_number", alias);
493 build_number_condition(&num_col, value, param_num)
494 }
495 SearchParamType::Quantity => {
496 let qty_col = format!("{}.value_quantity_value", alias);
498 build_number_condition(&qty_col, value, param_num)
499 }
500 SearchParamType::Uri => (
501 format!("{}.value_uri = ?{}", alias, param_num),
502 SqlParam::String(value.value.clone()),
503 ),
504 _ => (
505 format!("{}.value_string LIKE ?{}", alias, param_num),
506 SqlParam::String(format!("%{}%", value.value)),
507 ),
508 };
509
510 Ok((condition, param))
511 }
512
513 pub fn build_reverse_chain_sql(
537 &self,
538 reverse_chain: &ReverseChainedParameter,
539 ) -> StorageResult<SqlFragment> {
540 let depth = reverse_chain.depth();
542 if !self.config.validate_reverse_depth(depth) {
543 return Err(BackendError::Internal {
544 backend_name: "sqlite".to_string(),
545 message: format!(
546 "Reverse chain depth {} exceeds maximum {}",
547 depth, self.config.max_reverse_depth
548 ),
549 source: None,
550 }
551 .into());
552 }
553
554 let param_num = self.param_offset + 1;
555 let (sql, params) = self.build_reverse_chain_recursive(reverse_chain, 1, param_num)?;
556
557 Ok(SqlFragment::with_params(
558 format!("r.id IN ({})", sql),
559 params,
560 ))
561 }
562
563 fn build_reverse_chain_recursive(
565 &self,
566 rc: &ReverseChainedParameter,
567 depth: usize,
568 param_num: usize,
569 ) -> StorageResult<(String, Vec<SqlParam>)> {
570 let alias = format!("si{}", depth);
571
572 if rc.is_terminal() {
573 let value = rc.value.as_ref().ok_or_else(|| BackendError::Internal {
575 backend_name: "sqlite".to_string(),
576 message: "Terminal reverse chain must have a value".to_string(),
577 source: None,
578 })?;
579
580 let (search_condition, search_param) = self.build_reverse_terminal_condition(
582 &rc.source_type,
583 &rc.search_param,
584 value,
585 depth + 1,
586 param_num,
587 )?;
588
589 let depth2 = depth + 1;
591 let sql = format!(
592 "SELECT SUBSTR({alias}.value_reference, INSTR({alias}.value_reference, '/') + 1) \
593 FROM search_index {alias} \
594 WHERE {alias}.tenant_id = ?1 AND {alias}.resource_type = '{src_type}' \
595 AND {alias}.param_name = '{ref_param}' \
596 AND {alias}.value_reference LIKE '{base_type}/%' \
597 AND {alias}.resource_id IN (\
598 SELECT si{depth2}.resource_id FROM search_index si{depth2} \
599 WHERE si{depth2}.tenant_id = ?1 AND si{depth2}.resource_type = '{src_type}' \
600 AND si{depth2}.param_name = '{search_param_name}' AND {search_condition}\
601 )",
602 alias = alias,
603 src_type = rc.source_type,
604 ref_param = rc.reference_param,
605 base_type = self.base_type,
606 depth2 = depth2,
607 search_param_name = rc.search_param,
608 search_condition = search_condition,
609 );
610
611 Ok((sql, vec![search_param]))
612 } else {
613 let inner = rc.nested.as_ref().ok_or_else(|| BackendError::Internal {
615 backend_name: "sqlite".to_string(),
616 message: "Non-terminal reverse chain must have nested chain".to_string(),
617 source: None,
618 })?;
619
620 let inner_builder = ChainQueryBuilder::new(
622 &self.tenant_id,
623 &rc.source_type,
624 Arc::clone(&self.registry),
625 )
626 .with_config(self.config.clone())
627 .with_param_offset(param_num - 1);
628
629 let (inner_sql, inner_params) =
630 inner_builder.build_reverse_chain_recursive(inner, depth + 1, param_num)?;
631
632 let sql = format!(
634 "SELECT SUBSTR({alias}.value_reference, INSTR({alias}.value_reference, '/') + 1) \
635 FROM search_index {alias} \
636 WHERE {alias}.tenant_id = ?1 AND {alias}.resource_type = '{}' \
637 AND {alias}.param_name = '{}' \
638 AND {alias}.value_reference LIKE '{}/%' \
639 AND {alias}.resource_id IN ({inner_sql})",
640 rc.source_type,
641 rc.reference_param,
642 self.base_type,
643 alias = alias,
644 );
645
646 Ok((sql, inner_params))
647 }
648 }
649
650 fn build_reverse_terminal_condition(
652 &self,
653 resource_type: &str,
654 param_name: &str,
655 value: &SearchValue,
656 depth: usize,
657 param_num: usize,
658 ) -> StorageResult<(String, SqlParam)> {
659 let param_type = {
662 let registry = self.registry.read();
663 crate::search::resolve_param_type(
664 ®istry,
665 resource_type,
666 param_name,
667 std::slice::from_ref(value),
668 )
669 };
670
671 let alias = format!("si{}", depth);
672
673 let (condition, param) = match param_type {
674 SearchParamType::String => {
675 let escaped = value.value.replace('%', "\\%").replace('_', "\\_");
676 (
677 format!("{}.value_string LIKE ?{} ESCAPE '\\'", alias, param_num),
678 SqlParam::String(format!("%{}%", escaped)),
679 )
680 }
681 SearchParamType::Token => {
682 if let Some((system, code)) = value.value.split_once('|') {
683 if system.is_empty() {
684 (
685 format!(
686 "({}.value_token_system IS NULL OR {}.value_token_system = '') \
687 AND {}.value_token_code = ?{}",
688 alias, alias, alias, param_num
689 ),
690 SqlParam::String(code.to_string()),
691 )
692 } else {
693 (
694 format!(
695 "{}.value_token_system = '{}' AND {}.value_token_code = ?{}",
696 alias,
697 system.replace('\'', "''"),
698 alias,
699 param_num
700 ),
701 SqlParam::String(code.to_string()),
702 )
703 }
704 } else {
705 (
706 format!("{}.value_token_code = ?{}", alias, param_num),
707 SqlParam::String(value.value.clone()),
708 )
709 }
710 }
711 SearchParamType::Reference => (
712 format!("{}.value_reference LIKE ?{}", alias, param_num),
713 SqlParam::String(format!("%{}%", value.value)),
714 ),
715 SearchParamType::Date => {
716 let date_col = format!("{}.value_date", alias);
717 build_date_condition(&date_col, value, param_num)
718 }
719 SearchParamType::Number => {
720 let num_col = format!("{}.value_number", alias);
721 build_number_condition(&num_col, value, param_num)
722 }
723 SearchParamType::Quantity => {
724 let qty_col = format!("{}.value_quantity_value", alias);
725 build_number_condition(&qty_col, value, param_num)
726 }
727 SearchParamType::Uri => (
728 format!("{}.value_uri = ?{}", alias, param_num),
729 SqlParam::String(value.value.clone()),
730 ),
731 _ => (
732 format!("{}.value_string LIKE ?{}", alias, param_num),
733 SqlParam::String(format!("%{}%", value.value)),
734 ),
735 };
736
737 Ok((condition, param))
738 }
739}
740
741fn build_date_condition(column: &str, value: &SearchValue, param_num: usize) -> (String, SqlParam) {
743 use crate::types::SearchPrefix;
744
745 let (op, val) = match value.prefix {
746 SearchPrefix::Eq => ("=", &value.value),
747 SearchPrefix::Ne => ("!=", &value.value),
748 SearchPrefix::Gt => (">", &value.value),
749 SearchPrefix::Lt => ("<", &value.value),
750 SearchPrefix::Ge => (">=", &value.value),
751 SearchPrefix::Le => ("<=", &value.value),
752 SearchPrefix::Sa => (">", &value.value),
753 SearchPrefix::Eb => ("<", &value.value),
754 SearchPrefix::Ap => {
755 return (
757 format!("DATE({}) = DATE(?{})", column, param_num),
758 SqlParam::String(value.value.clone()),
759 );
760 }
761 };
762
763 (
764 format!("{} {} ?{}", column, op, param_num),
765 SqlParam::String(val.clone()),
766 )
767}
768
769fn build_number_condition(
771 column: &str,
772 value: &SearchValue,
773 param_num: usize,
774) -> (String, SqlParam) {
775 use crate::types::SearchPrefix;
776
777 let num_value = value.value.parse::<f64>().unwrap_or(0.0);
779
780 let (op, val) = match value.prefix {
781 SearchPrefix::Eq => ("=", num_value),
782 SearchPrefix::Ne => ("!=", num_value),
783 SearchPrefix::Gt => (">", num_value),
784 SearchPrefix::Lt => ("<", num_value),
785 SearchPrefix::Ge => (">=", num_value),
786 SearchPrefix::Le => ("<=", num_value),
787 SearchPrefix::Sa => (">", num_value),
788 SearchPrefix::Eb => ("<", num_value),
789 SearchPrefix::Ap => {
790 let lower = num_value * 0.9;
792 let upper = num_value * 1.1;
793 return (
794 format!("{} BETWEEN {} AND {}", column, lower, upper),
795 SqlParam::Float(num_value),
796 );
797 }
798 };
799
800 (
801 format!("{} {} ?{}", column, op, param_num),
802 SqlParam::Float(val),
803 )
804}
805
806#[cfg(test)]
807mod tests {
808 use super::*;
809 use crate::search::SearchParameterDefinition;
810
811 fn create_test_registry() -> Arc<RwLock<SearchParameterRegistry>> {
812 let mut registry = SearchParameterRegistry::new();
813
814 let patient_subject = SearchParameterDefinition::new(
816 "http://hl7.org/fhir/SearchParameter/Observation-subject",
817 "subject",
818 SearchParamType::Reference,
819 "Observation.subject",
820 )
821 .with_base(vec!["Observation"])
822 .with_targets(vec!["Patient"]);
823
824 let patient_org = SearchParameterDefinition::new(
825 "http://hl7.org/fhir/SearchParameter/Patient-organization",
826 "organization",
827 SearchParamType::Reference,
828 "Patient.managingOrganization",
829 )
830 .with_base(vec!["Patient"])
831 .with_targets(vec!["Organization"]);
832
833 let org_name = SearchParameterDefinition::new(
834 "http://hl7.org/fhir/SearchParameter/Organization-name",
835 "name",
836 SearchParamType::String,
837 "Organization.name",
838 )
839 .with_base(vec!["Organization"]);
840
841 let patient_name = SearchParameterDefinition::new(
842 "http://hl7.org/fhir/SearchParameter/Patient-name",
843 "name",
844 SearchParamType::String,
845 "Patient.name",
846 )
847 .with_base(vec!["Patient"]);
848
849 let obs_code = SearchParameterDefinition::new(
850 "http://hl7.org/fhir/SearchParameter/Observation-code",
851 "code",
852 SearchParamType::Token,
853 "Observation.code",
854 )
855 .with_base(vec!["Observation"]);
856
857 registry.register(patient_subject).unwrap();
858 registry.register(patient_org).unwrap();
859 registry.register(org_name).unwrap();
860 registry.register(patient_name).unwrap();
861 registry.register(obs_code).unwrap();
862
863 Arc::new(RwLock::new(registry))
864 }
865
866 #[test]
867 fn test_parse_simple_chain() {
868 let registry = create_test_registry();
869 let builder = ChainQueryBuilder::new("tenant1", "Observation", registry);
870
871 let result = builder.parse_chain("subject.name");
872 assert!(result.is_ok());
873
874 let chain = result.unwrap();
875 assert_eq!(chain.links.len(), 1);
876 assert_eq!(chain.links[0].reference_param, "subject");
877 assert_eq!(chain.links[0].target_type, "Patient");
878 assert_eq!(chain.terminal_param, "name");
879 assert_eq!(chain.terminal_type, SearchParamType::String);
880 }
881
882 #[test]
883 fn test_parse_multi_level_chain() {
884 let registry = create_test_registry();
885 let builder = ChainQueryBuilder::new("tenant1", "Observation", registry);
886
887 let result = builder.parse_chain("subject.organization.name");
888 assert!(result.is_ok());
889
890 let chain = result.unwrap();
891 assert_eq!(chain.links.len(), 2);
892 assert_eq!(chain.links[0].reference_param, "subject");
893 assert_eq!(chain.links[0].target_type, "Patient");
894 assert_eq!(chain.links[1].reference_param, "organization");
895 assert_eq!(chain.links[1].target_type, "Organization");
896 assert_eq!(chain.terminal_param, "name");
897 }
898
899 #[test]
900 fn test_parse_chain_with_type_modifier() {
901 let registry = create_test_registry();
902 let builder = ChainQueryBuilder::new("tenant1", "Observation", registry);
903
904 let result = builder.parse_chain("subject:Patient.name");
905 assert!(result.is_ok());
906
907 let chain = result.unwrap();
908 assert_eq!(chain.links[0].target_type, "Patient");
909 }
910
911 #[test]
912 fn test_max_depth_exceeded() {
913 let registry = create_test_registry();
914 let builder = ChainQueryBuilder::new("tenant1", "Observation", registry)
915 .with_config(ChainConfig::new(2, 2));
916
917 let result = builder.parse_chain("a.b.c.d"); assert!(matches!(
919 result,
920 Err(ChainError::MaxDepthExceeded { depth: 3, max: 2 })
921 ));
922 }
923
924 #[test]
925 fn test_build_forward_chain_sql() {
926 let registry = create_test_registry();
927 let builder = ChainQueryBuilder::new("tenant1", "Observation", registry);
928
929 let chain = builder.parse_chain("subject.name").unwrap();
930 let value = SearchValue::eq("Smith");
931
932 let result = builder.build_forward_chain_sql(&chain, &value);
933 assert!(result.is_ok());
934
935 let fragment = result.unwrap();
936 assert!(fragment.sql.contains("r.id IN"));
937 assert!(fragment.sql.contains("search_index"));
938 assert!(fragment.sql.contains("subject"));
939 assert!(fragment.sql.contains("name"));
940 }
941
942 #[test]
943 fn test_build_reverse_chain_sql() {
944 let registry = create_test_registry();
945 let builder = ChainQueryBuilder::new("tenant1", "Patient", registry);
946
947 let rc = ReverseChainedParameter::terminal(
948 "Observation",
949 "subject",
950 "code",
951 SearchValue::eq("1234-5"),
952 );
953
954 let result = builder.build_reverse_chain_sql(&rc);
955 assert!(result.is_ok());
956
957 let fragment = result.unwrap();
958 assert!(fragment.sql.contains("r.id IN"));
959 assert!(fragment.sql.contains("Observation"));
960 assert!(fragment.sql.contains("subject"));
961 assert!(fragment.sql.contains("code"));
962 assert!(fragment.sql.contains("Patient/%"));
963 }
964
965 #[test]
966 fn test_reverse_chain_depth() {
967 let inner = ReverseChainedParameter::terminal(
968 "Provenance",
969 "target",
970 "agent",
971 SearchValue::eq("Practitioner/123"),
972 );
973 let outer = ReverseChainedParameter::nested("Observation", "subject", inner);
974
975 assert_eq!(outer.depth(), 2);
976 assert!(!outer.is_terminal());
977 }
978}