helios_persistence/backends/sqlite/search/parameter_handlers/
token.rs1use crate::types::{SearchModifier, SearchValue};
4
5use super::super::query_builder::{SqlFragment, SqlParam};
6
7pub struct TokenHandler;
9
10impl TokenHandler {
11 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
28 if matches!(modifier, Some(SearchModifier::Not)) {
30 let inner = Self::build_sql(value, None, param_offset);
31 return SqlFragment::with_params(format!("NOT ({})", inner.sql), inner.params);
32 }
33
34 if matches!(modifier, Some(SearchModifier::Text)) {
36 return SqlFragment::with_params(
38 format!(
39 "value_token_display COLLATE NOCASE LIKE '%' || ?{} || '%'",
40 param_num
41 ),
42 vec![SqlParam::string(value.value.to_lowercase())],
43 );
44 }
45
46 if matches!(modifier, Some(SearchModifier::TextAdvanced)) {
49 return Self::build_text_advanced_sql(&value.value, param_offset);
50 }
51
52 if matches!(modifier, Some(SearchModifier::CodeOnly)) {
54 return SqlFragment::with_params(
55 format!("value_token_code = ?{}", param_num),
56 vec![SqlParam::string(&value.value)],
57 );
58 }
59
60 if matches!(modifier, Some(SearchModifier::OfType)) {
62 return Self::build_of_type_sql(&value.value, param_offset);
63 }
64
65 let token_value = &value.value;
67
68 if let Some(pipe_pos) = token_value.find('|') {
69 let system = &token_value[..pipe_pos];
70 let code = &token_value[pipe_pos + 1..];
71
72 if system.is_empty() {
73 SqlFragment::with_params(
75 format!(
76 "(value_token_system IS NULL OR value_token_system = '') AND value_token_code = ?{}",
77 param_num
78 ),
79 vec![SqlParam::string(code)],
80 )
81 } else if code.is_empty() {
82 SqlFragment::with_params(
84 format!("value_token_system = ?{}", param_num),
85 vec![SqlParam::string(system)],
86 )
87 } else {
88 SqlFragment::with_params(
90 format!(
91 "value_token_system = ?{} AND value_token_code = ?{}",
92 param_num,
93 param_num + 1
94 ),
95 vec![SqlParam::string(system), SqlParam::string(code)],
96 )
97 }
98 } else {
99 SqlFragment::with_params(
101 format!("value_token_code = ?{}", param_num),
102 vec![SqlParam::string(token_value)],
103 )
104 }
105 }
106
107 fn build_text_advanced_sql(query: &str, param_offset: usize) -> SqlFragment {
120 use super::super::fts::Fts5Search;
121
122 Fts5Search::build_advanced_query(query, param_offset + 1)
125 }
126
127 fn build_of_type_sql(value: &str, param_offset: usize) -> SqlFragment {
140 let mut param_num = param_offset + 1;
141
142 let parts: Vec<&str> = value.splitn(3, '|').collect();
144
145 match parts.len() {
146 3 => {
147 let type_system = parts[0];
148 let type_code = parts[1];
149 let identifier_value = parts[2];
150
151 let mut conditions = Vec::new();
152 let mut params = Vec::new();
153
154 if !identifier_value.is_empty() {
156 conditions.push(format!("value_token_code = ?{}", param_num));
157 params.push(SqlParam::string(identifier_value));
158 param_num += 1;
159 }
160
161 if !type_system.is_empty() {
163 conditions.push(format!("value_identifier_type_system = ?{}", param_num));
164 params.push(SqlParam::string(type_system));
165 param_num += 1;
166 }
167
168 if !type_code.is_empty() {
170 conditions.push(format!("value_identifier_type_code = ?{}", param_num));
171 params.push(SqlParam::string(type_code));
172 }
173
174 if conditions.is_empty() {
175 SqlFragment::new("1 = 0")
177 } else {
178 SqlFragment::with_params(conditions.join(" AND "), params)
179 }
180 }
181 2 => {
182 let type_code = parts[0];
184 let identifier_value = parts[1];
185
186 let mut conditions = Vec::new();
187 let mut params = Vec::new();
188
189 if !identifier_value.is_empty() {
190 conditions.push(format!("value_token_code = ?{}", param_num));
191 params.push(SqlParam::string(identifier_value));
192 param_num += 1;
193 }
194
195 if !type_code.is_empty() {
196 conditions.push(format!("value_identifier_type_code = ?{}", param_num));
197 params.push(SqlParam::string(type_code));
198 }
199
200 if conditions.is_empty() {
201 SqlFragment::new("1 = 0")
202 } else {
203 SqlFragment::with_params(conditions.join(" AND "), params)
204 }
205 }
206 1 => {
207 SqlFragment::with_params(
209 format!("value_token_code = ?{}", param_num),
210 vec![SqlParam::string(value)],
211 )
212 }
213 _ => {
214 SqlFragment::new("1 = 0")
216 }
217 }
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use crate::types::SearchPrefix;
225
226 #[test]
227 fn test_token_code_only() {
228 let value = SearchValue::new(SearchPrefix::Eq, "12345");
229 let frag = TokenHandler::build_sql(&value, None, 0);
230
231 assert!(frag.sql.contains("value_token_code = ?1"));
232 assert_eq!(frag.params.len(), 1);
233 }
234
235 #[test]
236 fn test_token_system_and_code() {
237 let value = SearchValue::new(SearchPrefix::Eq, "http://loinc.org|12345-6");
238 let frag = TokenHandler::build_sql(&value, None, 0);
239
240 assert!(frag.sql.contains("value_token_system = ?1"));
241 assert!(frag.sql.contains("value_token_code = ?2"));
242 assert_eq!(frag.params.len(), 2);
243 }
244
245 #[test]
246 fn test_token_no_system() {
247 let value = SearchValue::new(SearchPrefix::Eq, "|12345");
248 let frag = TokenHandler::build_sql(&value, None, 0);
249
250 assert!(frag.sql.contains("IS NULL OR"));
251 assert!(frag.sql.contains("value_token_code = ?1"));
252 }
253
254 #[test]
255 fn test_token_system_only() {
256 let value = SearchValue::new(SearchPrefix::Eq, "http://loinc.org|");
257 let frag = TokenHandler::build_sql(&value, None, 0);
258
259 assert!(frag.sql.contains("value_token_system = ?1"));
260 assert!(!frag.sql.contains("value_token_code"));
261 }
262
263 #[test]
264 fn test_token_not_modifier() {
265 let value = SearchValue::new(SearchPrefix::Eq, "12345");
266 let frag = TokenHandler::build_sql(&value, Some(&SearchModifier::Not), 0);
267
268 assert!(frag.sql.starts_with("NOT ("));
269 }
270
271 #[test]
272 fn test_of_type_full_format() {
273 let value = SearchValue::new(
275 SearchPrefix::Eq,
276 "http://terminology.hl7.org/CodeSystem/v2-0203|MR|12345",
277 );
278 let frag = TokenHandler::build_sql(&value, Some(&SearchModifier::OfType), 0);
279
280 assert!(frag.sql.contains("value_token_code = ?1"));
282 assert!(frag.sql.contains("value_identifier_type_system = ?2"));
283 assert!(frag.sql.contains("value_identifier_type_code = ?3"));
284 assert_eq!(frag.params.len(), 3);
285 }
286
287 #[test]
288 fn test_of_type_no_system() {
289 let value = SearchValue::new(SearchPrefix::Eq, "|MR|12345");
291 let frag = TokenHandler::build_sql(&value, Some(&SearchModifier::OfType), 0);
292
293 assert!(frag.sql.contains("value_token_code = ?1"));
295 assert!(frag.sql.contains("value_identifier_type_code = ?2"));
296 assert_eq!(frag.params.len(), 2);
297 }
298
299 #[test]
300 fn test_of_type_value_only() {
301 let value = SearchValue::new(SearchPrefix::Eq, "MR|12345");
303 let frag = TokenHandler::build_sql(&value, Some(&SearchModifier::OfType), 0);
304
305 assert!(frag.sql.contains("value_token_code = ?1"));
307 assert!(frag.sql.contains("value_identifier_type_code = ?2"));
308 assert_eq!(frag.params.len(), 2);
309 if let SqlParam::String(s) = &frag.params[0] {
311 assert_eq!(s, "12345");
312 } else {
313 panic!("Expected string parameter for identifier value");
314 }
315 if let SqlParam::String(s) = &frag.params[1] {
316 assert_eq!(s, "MR");
317 } else {
318 panic!("Expected string parameter for type code");
319 }
320 }
321
322 #[test]
327 fn test_text_advanced_simple() {
328 let value = SearchValue::new(SearchPrefix::Eq, "headache");
329 let frag = TokenHandler::build_sql(&value, Some(&SearchModifier::TextAdvanced), 0);
330
331 assert!(frag.sql.contains("search_index_fts"));
333 assert!(frag.sql.contains("MATCH"));
334 assert_eq!(frag.params.len(), 1);
335 }
336
337 #[test]
338 fn test_text_advanced_boolean_or() {
339 let value = SearchValue::new(SearchPrefix::Eq, "headache OR migraine");
340 let frag = TokenHandler::build_sql(&value, Some(&SearchModifier::TextAdvanced), 0);
341
342 assert!(frag.sql.contains("MATCH"));
343 if let SqlParam::String(s) = &frag.params[0] {
345 assert!(s.contains("OR"), "Query should contain OR: {}", s);
346 }
347 }
348
349 #[test]
350 fn test_text_advanced_phrase() {
351 let value = SearchValue::new(SearchPrefix::Eq, "\"heart failure\"");
352 let frag = TokenHandler::build_sql(&value, Some(&SearchModifier::TextAdvanced), 0);
353
354 assert!(frag.sql.contains("MATCH"));
355 if let SqlParam::String(s) = &frag.params[0] {
357 assert!(
358 s.contains("\"heart failure\""),
359 "Query should contain phrase: {}",
360 s
361 );
362 }
363 }
364
365 #[test]
366 fn test_text_advanced_prefix() {
367 let value = SearchValue::new(SearchPrefix::Eq, "cardio*");
368 let frag = TokenHandler::build_sql(&value, Some(&SearchModifier::TextAdvanced), 0);
369
370 assert!(frag.sql.contains("MATCH"));
371 if let SqlParam::String(s) = &frag.params[0] {
373 assert!(s.contains("cardio*"), "Query should contain prefix: {}", s);
374 }
375 }
376
377 #[test]
378 fn test_text_advanced_not() {
379 let value = SearchValue::new(SearchPrefix::Eq, "-surgery");
380 let frag = TokenHandler::build_sql(&value, Some(&SearchModifier::TextAdvanced), 0);
381
382 assert!(frag.sql.contains("MATCH"));
383 if let SqlParam::String(s) = &frag.params[0] {
385 assert!(s.contains("NOT"), "Query should contain NOT: {}", s);
386 }
387 }
388}