use crate::types::{SearchModifier, SearchValue};
use super::super::query_builder::{SqlFragment, SqlParam};
pub struct TokenHandler;
impl TokenHandler {
pub fn build_sql(
value: &SearchValue,
modifier: Option<&SearchModifier>,
param_offset: usize,
) -> SqlFragment {
let param_num = param_offset + 1;
if matches!(modifier, Some(SearchModifier::Not)) {
let inner = Self::build_sql(value, None, param_offset);
return SqlFragment::with_params(format!("NOT ({})", inner.sql), inner.params);
}
if matches!(modifier, Some(SearchModifier::Text)) {
return SqlFragment::with_params(
format!(
"value_token_display COLLATE NOCASE LIKE '%' || ?{} || '%'",
param_num
),
vec![SqlParam::string(value.value.to_lowercase())],
);
}
if matches!(modifier, Some(SearchModifier::TextAdvanced)) {
return Self::build_text_advanced_sql(&value.value, param_offset);
}
if matches!(modifier, Some(SearchModifier::CodeOnly)) {
return SqlFragment::with_params(
format!("value_token_code = ?{}", param_num),
vec![SqlParam::string(&value.value)],
);
}
if matches!(modifier, Some(SearchModifier::OfType)) {
return Self::build_of_type_sql(&value.value, param_offset);
}
let token_value = &value.value;
if let Some(pipe_pos) = token_value.find('|') {
let system = &token_value[..pipe_pos];
let code = &token_value[pipe_pos + 1..];
if system.is_empty() {
SqlFragment::with_params(
format!(
"(value_token_system IS NULL OR value_token_system = '') AND value_token_code = ?{}",
param_num
),
vec![SqlParam::string(code)],
)
} else if code.is_empty() {
SqlFragment::with_params(
format!("value_token_system = ?{}", param_num),
vec![SqlParam::string(system)],
)
} else {
SqlFragment::with_params(
format!(
"value_token_system = ?{} AND value_token_code = ?{}",
param_num,
param_num + 1
),
vec![SqlParam::string(system), SqlParam::string(code)],
)
}
} else {
SqlFragment::with_params(
format!("value_token_code = ?{}", param_num),
vec![SqlParam::string(token_value)],
)
}
}
fn build_text_advanced_sql(query: &str, param_offset: usize) -> SqlFragment {
use super::super::fts::Fts5Search;
Fts5Search::build_advanced_query(query, param_offset + 1)
}
fn build_of_type_sql(value: &str, param_offset: usize) -> SqlFragment {
let mut param_num = param_offset + 1;
let parts: Vec<&str> = value.splitn(3, '|').collect();
match parts.len() {
3 => {
let type_system = parts[0];
let type_code = parts[1];
let identifier_value = parts[2];
let mut conditions = Vec::new();
let mut params = Vec::new();
if !identifier_value.is_empty() {
conditions.push(format!("value_token_code = ?{}", param_num));
params.push(SqlParam::string(identifier_value));
param_num += 1;
}
if !type_system.is_empty() {
conditions.push(format!("value_identifier_type_system = ?{}", param_num));
params.push(SqlParam::string(type_system));
param_num += 1;
}
if !type_code.is_empty() {
conditions.push(format!("value_identifier_type_code = ?{}", param_num));
params.push(SqlParam::string(type_code));
}
if conditions.is_empty() {
SqlFragment::new("1 = 0")
} else {
SqlFragment::with_params(conditions.join(" AND "), params)
}
}
2 => {
let type_code = parts[0];
let identifier_value = parts[1];
let mut conditions = Vec::new();
let mut params = Vec::new();
if !identifier_value.is_empty() {
conditions.push(format!("value_token_code = ?{}", param_num));
params.push(SqlParam::string(identifier_value));
param_num += 1;
}
if !type_code.is_empty() {
conditions.push(format!("value_identifier_type_code = ?{}", param_num));
params.push(SqlParam::string(type_code));
}
if conditions.is_empty() {
SqlFragment::new("1 = 0")
} else {
SqlFragment::with_params(conditions.join(" AND "), params)
}
}
1 => {
SqlFragment::with_params(
format!("value_token_code = ?{}", param_num),
vec![SqlParam::string(value)],
)
}
_ => {
SqlFragment::new("1 = 0")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::SearchPrefix;
#[test]
fn test_token_code_only() {
let value = SearchValue::new(SearchPrefix::Eq, "12345");
let frag = TokenHandler::build_sql(&value, None, 0);
assert!(frag.sql.contains("value_token_code = ?1"));
assert_eq!(frag.params.len(), 1);
}
#[test]
fn test_token_system_and_code() {
let value = SearchValue::new(SearchPrefix::Eq, "http://loinc.org|12345-6");
let frag = TokenHandler::build_sql(&value, None, 0);
assert!(frag.sql.contains("value_token_system = ?1"));
assert!(frag.sql.contains("value_token_code = ?2"));
assert_eq!(frag.params.len(), 2);
}
#[test]
fn test_token_no_system() {
let value = SearchValue::new(SearchPrefix::Eq, "|12345");
let frag = TokenHandler::build_sql(&value, None, 0);
assert!(frag.sql.contains("IS NULL OR"));
assert!(frag.sql.contains("value_token_code = ?1"));
}
#[test]
fn test_token_system_only() {
let value = SearchValue::new(SearchPrefix::Eq, "http://loinc.org|");
let frag = TokenHandler::build_sql(&value, None, 0);
assert!(frag.sql.contains("value_token_system = ?1"));
assert!(!frag.sql.contains("value_token_code"));
}
#[test]
fn test_token_not_modifier() {
let value = SearchValue::new(SearchPrefix::Eq, "12345");
let frag = TokenHandler::build_sql(&value, Some(&SearchModifier::Not), 0);
assert!(frag.sql.starts_with("NOT ("));
}
#[test]
fn test_of_type_full_format() {
let value = SearchValue::new(
SearchPrefix::Eq,
"http://terminology.hl7.org/CodeSystem/v2-0203|MR|12345",
);
let frag = TokenHandler::build_sql(&value, Some(&SearchModifier::OfType), 0);
assert!(frag.sql.contains("value_token_code = ?1"));
assert!(frag.sql.contains("value_identifier_type_system = ?2"));
assert!(frag.sql.contains("value_identifier_type_code = ?3"));
assert_eq!(frag.params.len(), 3);
}
#[test]
fn test_of_type_no_system() {
let value = SearchValue::new(SearchPrefix::Eq, "|MR|12345");
let frag = TokenHandler::build_sql(&value, Some(&SearchModifier::OfType), 0);
assert!(frag.sql.contains("value_token_code = ?1"));
assert!(frag.sql.contains("value_identifier_type_code = ?2"));
assert_eq!(frag.params.len(), 2);
}
#[test]
fn test_of_type_value_only() {
let value = SearchValue::new(SearchPrefix::Eq, "MR|12345");
let frag = TokenHandler::build_sql(&value, Some(&SearchModifier::OfType), 0);
assert!(frag.sql.contains("value_token_code = ?1"));
assert!(frag.sql.contains("value_identifier_type_code = ?2"));
assert_eq!(frag.params.len(), 2);
if let SqlParam::String(s) = &frag.params[0] {
assert_eq!(s, "12345");
} else {
panic!("Expected string parameter for identifier value");
}
if let SqlParam::String(s) = &frag.params[1] {
assert_eq!(s, "MR");
} else {
panic!("Expected string parameter for type code");
}
}
#[test]
fn test_text_advanced_simple() {
let value = SearchValue::new(SearchPrefix::Eq, "headache");
let frag = TokenHandler::build_sql(&value, Some(&SearchModifier::TextAdvanced), 0);
assert!(frag.sql.contains("search_index_fts"));
assert!(frag.sql.contains("MATCH"));
assert_eq!(frag.params.len(), 1);
}
#[test]
fn test_text_advanced_boolean_or() {
let value = SearchValue::new(SearchPrefix::Eq, "headache OR migraine");
let frag = TokenHandler::build_sql(&value, Some(&SearchModifier::TextAdvanced), 0);
assert!(frag.sql.contains("MATCH"));
if let SqlParam::String(s) = &frag.params[0] {
assert!(s.contains("OR"), "Query should contain OR: {}", s);
}
}
#[test]
fn test_text_advanced_phrase() {
let value = SearchValue::new(SearchPrefix::Eq, "\"heart failure\"");
let frag = TokenHandler::build_sql(&value, Some(&SearchModifier::TextAdvanced), 0);
assert!(frag.sql.contains("MATCH"));
if let SqlParam::String(s) = &frag.params[0] {
assert!(
s.contains("\"heart failure\""),
"Query should contain phrase: {}",
s
);
}
}
#[test]
fn test_text_advanced_prefix() {
let value = SearchValue::new(SearchPrefix::Eq, "cardio*");
let frag = TokenHandler::build_sql(&value, Some(&SearchModifier::TextAdvanced), 0);
assert!(frag.sql.contains("MATCH"));
if let SqlParam::String(s) = &frag.params[0] {
assert!(s.contains("cardio*"), "Query should contain prefix: {}", s);
}
}
#[test]
fn test_text_advanced_not() {
let value = SearchValue::new(SearchPrefix::Eq, "-surgery");
let frag = TokenHandler::build_sql(&value, Some(&SearchModifier::TextAdvanced), 0);
assert!(frag.sql.contains("MATCH"));
if let SqlParam::String(s) = &frag.params[0] {
assert!(s.contains("NOT"), "Query should contain NOT: {}", s);
}
}
}