helios_persistence/backends/sqlite/search/parameter_handlers/
composite.rs1use crate::types::{CompositeSearchComponent, SearchParamType, SearchPrefix, SearchValue};
4
5use super::super::query_builder::SqlFragment;
6use super::{DateHandler, NumberHandler, QuantityHandler, StringHandler, TokenHandler};
7
8pub struct CompositeHandler;
14
15#[derive(Debug, Clone)]
17pub struct CompositeComponentDef {
18 pub param_type: SearchParamType,
20 pub column_prefix: String,
22}
23
24impl CompositeHandler {
25 pub fn build_composite_sql(
37 value: &SearchValue,
38 _param_name: &str,
39 components: &[CompositeSearchComponent],
40 param_offset: usize,
41 ) -> SqlFragment {
42 let composite_value = &value.value;
43 let parts: Vec<&str> = composite_value.split('$').collect();
44
45 if parts.len() != components.len() || components.is_empty() {
46 return SqlFragment::new("1 = 0");
47 }
48
49 let mut component_conditions = Vec::new();
50 let mut all_params = Vec::new();
51 let mut current_offset = param_offset;
52
53 for (part, component) in parts.iter().zip(components.iter()) {
55 let component_value = Self::parse_component_value(part);
56 let fragment = Self::build_component_sql_from_type(
57 &component_value,
58 component.param_type,
59 current_offset,
60 );
61
62 if fragment.sql == "1 = 0" {
63 return SqlFragment::new("1 = 0");
64 }
65
66 component_conditions.push(fragment.sql);
67 current_offset += fragment.params.len();
68 all_params.extend(fragment.params);
69 }
70
71 let conditions_sql = component_conditions.join(" AND ");
74
75 SqlFragment::with_params(format!("({})", conditions_sql), all_params)
76 }
77
78 pub fn build_sql(
85 value: &SearchValue,
86 components: &[CompositeComponentDef],
87 param_offset: usize,
88 ) -> SqlFragment {
89 let composite_value = &value.value;
90 let parts: Vec<&str> = composite_value.split('$').collect();
91
92 if parts.len() != components.len() {
93 return SqlFragment::new("1 = 0");
95 }
96
97 let mut conditions = Vec::new();
98 let mut params = Vec::new();
99 let mut current_offset = param_offset;
100
101 for (part, component) in parts.iter().zip(components.iter()) {
102 let component_value = Self::parse_component_value(part);
104
105 let fragment = Self::build_component_sql(&component_value, component, current_offset);
107
108 if fragment.sql == "1 = 0" {
109 return SqlFragment::new("1 = 0");
111 }
112
113 conditions.push(fragment.sql);
114 current_offset += fragment.params.len();
115 params.extend(fragment.params);
116 }
117
118 SqlFragment::with_params(format!("({})", conditions.join(" AND ")), params)
121 }
122
123 fn build_component_sql_from_type(
125 value: &SearchValue,
126 param_type: SearchParamType,
127 param_offset: usize,
128 ) -> SqlFragment {
129 match param_type {
130 SearchParamType::Token => TokenHandler::build_sql(value, None, param_offset),
131 SearchParamType::String => StringHandler::build_sql(value, None, param_offset),
132 SearchParamType::Date => DateHandler::build_sql(value, param_offset),
133 SearchParamType::Number => NumberHandler::build_sql(value, param_offset),
134 SearchParamType::Quantity => QuantityHandler::build_sql(value, param_offset),
135 _ => SqlFragment::new("1 = 0"),
136 }
137 }
138
139 fn parse_component_value(part: &str) -> SearchValue {
141 let prefixes = [
143 ("ne", SearchPrefix::Ne),
144 ("gt", SearchPrefix::Gt),
145 ("lt", SearchPrefix::Lt),
146 ("ge", SearchPrefix::Ge),
147 ("le", SearchPrefix::Le),
148 ("sa", SearchPrefix::Sa),
149 ("eb", SearchPrefix::Eb),
150 ("ap", SearchPrefix::Ap),
151 ("eq", SearchPrefix::Eq),
152 ];
153
154 for (prefix_str, prefix) in prefixes {
155 if let Some(stripped) = part.strip_prefix(prefix_str) {
156 return SearchValue::new(prefix, stripped);
157 }
158 }
159
160 SearchValue::new(SearchPrefix::Eq, part)
162 }
163
164 fn build_component_sql(
166 value: &SearchValue,
167 component: &CompositeComponentDef,
168 param_offset: usize,
169 ) -> SqlFragment {
170 match component.param_type {
171 SearchParamType::Token => {
172 TokenHandler::build_sql(value, None, param_offset)
174 }
175 SearchParamType::String => StringHandler::build_sql(value, None, param_offset),
176 SearchParamType::Date => DateHandler::build_sql(value, param_offset),
177 SearchParamType::Number => NumberHandler::build_sql(value, param_offset),
178 SearchParamType::Quantity => QuantityHandler::build_sql(value, param_offset),
179 _ => {
180 SqlFragment::new("1 = 0")
182 }
183 }
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn test_composite_token_quantity() {
193 let value = SearchValue::new(SearchPrefix::Eq, "http://loinc.org|8480-6$lt60");
194
195 let components = vec![
196 CompositeComponentDef {
197 param_type: SearchParamType::Token,
198 column_prefix: "code".to_string(),
199 },
200 CompositeComponentDef {
201 param_type: SearchParamType::Quantity,
202 column_prefix: "value".to_string(),
203 },
204 ];
205
206 let frag = CompositeHandler::build_sql(&value, &components, 0);
207
208 assert!(frag.sql.contains("value_token_system"));
209 assert!(frag.sql.contains("value_quantity_value"));
210 assert!(frag.sql.contains("AND"));
211 }
212
213 #[test]
214 fn test_composite_mismatched_parts() {
215 let value = SearchValue::new(SearchPrefix::Eq, "value1");
216
217 let components = vec![
218 CompositeComponentDef {
219 param_type: SearchParamType::Token,
220 column_prefix: "code".to_string(),
221 },
222 CompositeComponentDef {
223 param_type: SearchParamType::Quantity,
224 column_prefix: "value".to_string(),
225 },
226 ];
227
228 let frag = CompositeHandler::build_sql(&value, &components, 0);
229
230 assert!(frag.sql.contains("1 = 0"));
232 }
233
234 #[test]
235 fn test_composite_token_date() {
236 let value = SearchValue::new(SearchPrefix::Eq, "active$ge2024-01-01");
237
238 let components = vec![
239 CompositeComponentDef {
240 param_type: SearchParamType::Token,
241 column_prefix: "status".to_string(),
242 },
243 CompositeComponentDef {
244 param_type: SearchParamType::Date,
245 column_prefix: "date".to_string(),
246 },
247 ];
248
249 let frag = CompositeHandler::build_sql(&value, &components, 0);
250
251 assert!(frag.sql.contains("value_token_code"));
252 assert!(frag.sql.contains("value_date"));
253 }
254}