Skip to main content

helios_persistence/backends/sqlite/search/parameter_handlers/
reference.rs

1//! Reference parameter SQL handler.
2
3use crate::types::{SearchModifier, SearchValue};
4
5use super::super::query_builder::{SqlFragment, SqlParam};
6
7/// Handles reference parameter SQL generation.
8pub struct ReferenceHandler;
9
10impl ReferenceHandler {
11    /// Builds SQL for a reference parameter value.
12    ///
13    /// Reference values can be:
14    /// - `id` - local reference (just the id)
15    /// - `Type/id` - relative reference
16    /// - `url` - absolute URL reference
17    ///
18    /// Modifiers:
19    /// - `:Type` - restrict to specific resource type (e.g., subject:Patient)
20    /// - `:identifier` - search by identifier instead of reference
21    pub fn build_sql(
22        value: &SearchValue,
23        modifier: Option<&SearchModifier>,
24        param_offset: usize,
25    ) -> SqlFragment {
26        let param_num = param_offset + 1;
27        let ref_value = &value.value;
28
29        // Handle :identifier modifier
30        if matches!(modifier, Some(SearchModifier::Identifier)) {
31            return Self::build_identifier_condition(ref_value, param_num);
32        }
33
34        // Handle :Type modifier (restrict to specific resource type)
35        if let Some(SearchModifier::Type(type_name)) = modifier {
36            // The reference must be to the specified type
37            let expected_prefix = format!("{}/", type_name);
38
39            if ref_value.contains('/') {
40                // Value already has type - just match it
41                SqlFragment::with_params(
42                    format!("value_reference = ?{}", param_num),
43                    vec![SqlParam::string(ref_value)],
44                )
45            } else {
46                // Value is just an ID - prepend the type
47                SqlFragment::with_params(
48                    format!("value_reference = ?{}", param_num),
49                    vec![SqlParam::string(format!(
50                        "{}{}",
51                        expected_prefix, ref_value
52                    ))],
53                )
54            }
55        } else {
56            // No modifier - match the reference as given
57            Self::build_reference_condition(ref_value, param_num)
58        }
59    }
60
61    /// Builds a condition for a standard reference value.
62    fn build_reference_condition(ref_value: &str, param_num: usize) -> SqlFragment {
63        if ref_value.contains('/') {
64            // Type/id or full URL - exact match
65            SqlFragment::with_params(
66                format!("value_reference = ?{}", param_num),
67                vec![SqlParam::string(ref_value)],
68            )
69        } else {
70            // Just an ID - match any reference ending with this ID
71            // This handles cases where the stored reference might be "Patient/123" but
72            // the search is just "123"
73            SqlFragment::with_params(
74                format!(
75                    "(value_reference = ?{} OR value_reference LIKE '%/' || ?{})",
76                    param_num,
77                    param_num + 1
78                ),
79                vec![SqlParam::string(ref_value), SqlParam::string(ref_value)],
80            )
81        }
82    }
83
84    /// Builds a condition for the :identifier modifier.
85    ///
86    /// This searches for references where the target resource has a matching identifier.
87    /// Requires a join or subquery on the identifier search index.
88    fn build_identifier_condition(identifier_value: &str, param_num: usize) -> SqlFragment {
89        // Parse the identifier value (system|value format)
90        if let Some(pipe_pos) = identifier_value.find('|') {
91            let system = &identifier_value[..pipe_pos];
92            let value = &identifier_value[pipe_pos + 1..];
93
94            if system.is_empty() {
95                // |value - match value with no system
96                SqlFragment::with_params(
97                    format!(
98                        "EXISTS (SELECT 1 FROM search_index si2 WHERE si2.resource_id = SUBSTR(value_reference, INSTR(value_reference, '/') + 1) AND si2.param_name = 'identifier' AND (si2.value_token_system IS NULL OR si2.value_token_system = '') AND si2.value_token_code = ?{})",
99                        param_num
100                    ),
101                    vec![SqlParam::string(value)],
102                )
103            } else if value.is_empty() {
104                // system| - match any value in system
105                SqlFragment::with_params(
106                    format!(
107                        "EXISTS (SELECT 1 FROM search_index si2 WHERE si2.resource_id = SUBSTR(value_reference, INSTR(value_reference, '/') + 1) AND si2.param_name = 'identifier' AND si2.value_token_system = ?{})",
108                        param_num
109                    ),
110                    vec![SqlParam::string(system)],
111                )
112            } else {
113                // system|value - exact match
114                SqlFragment::with_params(
115                    format!(
116                        "EXISTS (SELECT 1 FROM search_index si2 WHERE si2.resource_id = SUBSTR(value_reference, INSTR(value_reference, '/') + 1) AND si2.param_name = 'identifier' AND si2.value_token_system = ?{} AND si2.value_token_code = ?{})",
117                        param_num,
118                        param_num + 1
119                    ),
120                    vec![SqlParam::string(system), SqlParam::string(value)],
121                )
122            }
123        } else {
124            // Just a value - match any system
125            SqlFragment::with_params(
126                format!(
127                    "EXISTS (SELECT 1 FROM search_index si2 WHERE si2.resource_id = SUBSTR(value_reference, INSTR(value_reference, '/') + 1) AND si2.param_name = 'identifier' AND si2.value_token_code = ?{})",
128                    param_num
129                ),
130                vec![SqlParam::string(identifier_value)],
131            )
132        }
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use crate::types::SearchPrefix;
140
141    #[test]
142    fn test_reference_with_type() {
143        let value = SearchValue::new(SearchPrefix::Eq, "Patient/123");
144        let frag = ReferenceHandler::build_sql(&value, None, 0);
145
146        assert!(frag.sql.contains("value_reference = ?1"));
147        assert_eq!(frag.params.len(), 1);
148    }
149
150    #[test]
151    fn test_reference_id_only() {
152        let value = SearchValue::new(SearchPrefix::Eq, "123");
153        let frag = ReferenceHandler::build_sql(&value, None, 0);
154
155        // Should match both exact and with any type prefix
156        assert!(frag.sql.contains("OR"));
157        assert!(frag.sql.contains("LIKE"));
158    }
159
160    #[test]
161    fn test_reference_type_modifier() {
162        let value = SearchValue::new(SearchPrefix::Eq, "123");
163        let frag = ReferenceHandler::build_sql(
164            &value,
165            Some(&SearchModifier::Type("Patient".to_string())),
166            0,
167        );
168
169        assert!(frag.sql.contains("value_reference = ?1"));
170        // The param should be "Patient/123"
171    }
172
173    #[test]
174    fn test_reference_identifier_modifier() {
175        let value = SearchValue::new(SearchPrefix::Eq, "http://example.org|12345");
176        let frag = ReferenceHandler::build_sql(&value, Some(&SearchModifier::Identifier), 0);
177
178        assert!(frag.sql.contains("EXISTS"));
179        assert!(frag.sql.contains("identifier"));
180    }
181}