use crate::types::{CompositeSearchComponent, SearchParamType, SearchPrefix, SearchValue};
use super::super::query_builder::SqlFragment;
use super::{DateHandler, NumberHandler, QuantityHandler, StringHandler, TokenHandler};
pub struct CompositeHandler;
#[derive(Debug, Clone)]
pub struct CompositeComponentDef {
pub param_type: SearchParamType,
pub column_prefix: String,
}
impl CompositeHandler {
pub fn build_composite_sql(
value: &SearchValue,
_param_name: &str,
components: &[CompositeSearchComponent],
param_offset: usize,
) -> SqlFragment {
let composite_value = &value.value;
let parts: Vec<&str> = composite_value.split('$').collect();
if parts.len() != components.len() || components.is_empty() {
return SqlFragment::new("1 = 0");
}
let mut component_conditions = Vec::new();
let mut all_params = Vec::new();
let mut current_offset = param_offset;
for (part, component) in parts.iter().zip(components.iter()) {
let component_value = Self::parse_component_value(part);
let fragment = Self::build_component_sql_from_type(
&component_value,
component.param_type,
current_offset,
);
if fragment.sql == "1 = 0" {
return SqlFragment::new("1 = 0");
}
component_conditions.push(fragment.sql);
current_offset += fragment.params.len();
all_params.extend(fragment.params);
}
let conditions_sql = component_conditions.join(" AND ");
SqlFragment::with_params(format!("({})", conditions_sql), all_params)
}
pub fn build_sql(
value: &SearchValue,
components: &[CompositeComponentDef],
param_offset: usize,
) -> SqlFragment {
let composite_value = &value.value;
let parts: Vec<&str> = composite_value.split('$').collect();
if parts.len() != components.len() {
return SqlFragment::new("1 = 0");
}
let mut conditions = Vec::new();
let mut params = Vec::new();
let mut current_offset = param_offset;
for (part, component) in parts.iter().zip(components.iter()) {
let component_value = Self::parse_component_value(part);
let fragment = Self::build_component_sql(&component_value, component, current_offset);
if fragment.sql == "1 = 0" {
return SqlFragment::new("1 = 0");
}
conditions.push(fragment.sql);
current_offset += fragment.params.len();
params.extend(fragment.params);
}
SqlFragment::with_params(format!("({})", conditions.join(" AND ")), params)
}
fn build_component_sql_from_type(
value: &SearchValue,
param_type: SearchParamType,
param_offset: usize,
) -> SqlFragment {
match param_type {
SearchParamType::Token => TokenHandler::build_sql(value, None, param_offset),
SearchParamType::String => StringHandler::build_sql(value, None, param_offset),
SearchParamType::Date => DateHandler::build_sql(value, param_offset),
SearchParamType::Number => NumberHandler::build_sql(value, param_offset),
SearchParamType::Quantity => QuantityHandler::build_sql(value, param_offset),
_ => SqlFragment::new("1 = 0"),
}
}
fn parse_component_value(part: &str) -> SearchValue {
let prefixes = [
("ne", SearchPrefix::Ne),
("gt", SearchPrefix::Gt),
("lt", SearchPrefix::Lt),
("ge", SearchPrefix::Ge),
("le", SearchPrefix::Le),
("sa", SearchPrefix::Sa),
("eb", SearchPrefix::Eb),
("ap", SearchPrefix::Ap),
("eq", SearchPrefix::Eq),
];
for (prefix_str, prefix) in prefixes {
if let Some(stripped) = part.strip_prefix(prefix_str) {
return SearchValue::new(prefix, stripped);
}
}
SearchValue::new(SearchPrefix::Eq, part)
}
fn build_component_sql(
value: &SearchValue,
component: &CompositeComponentDef,
param_offset: usize,
) -> SqlFragment {
match component.param_type {
SearchParamType::Token => {
TokenHandler::build_sql(value, None, param_offset)
}
SearchParamType::String => StringHandler::build_sql(value, None, param_offset),
SearchParamType::Date => DateHandler::build_sql(value, param_offset),
SearchParamType::Number => NumberHandler::build_sql(value, param_offset),
SearchParamType::Quantity => QuantityHandler::build_sql(value, param_offset),
_ => {
SqlFragment::new("1 = 0")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_composite_token_quantity() {
let value = SearchValue::new(SearchPrefix::Eq, "http://loinc.org|8480-6$lt60");
let components = vec![
CompositeComponentDef {
param_type: SearchParamType::Token,
column_prefix: "code".to_string(),
},
CompositeComponentDef {
param_type: SearchParamType::Quantity,
column_prefix: "value".to_string(),
},
];
let frag = CompositeHandler::build_sql(&value, &components, 0);
assert!(frag.sql.contains("value_token_system"));
assert!(frag.sql.contains("value_quantity_value"));
assert!(frag.sql.contains("AND"));
}
#[test]
fn test_composite_mismatched_parts() {
let value = SearchValue::new(SearchPrefix::Eq, "value1");
let components = vec![
CompositeComponentDef {
param_type: SearchParamType::Token,
column_prefix: "code".to_string(),
},
CompositeComponentDef {
param_type: SearchParamType::Quantity,
column_prefix: "value".to_string(),
},
];
let frag = CompositeHandler::build_sql(&value, &components, 0);
assert!(frag.sql.contains("1 = 0"));
}
#[test]
fn test_composite_token_date() {
let value = SearchValue::new(SearchPrefix::Eq, "active$ge2024-01-01");
let components = vec![
CompositeComponentDef {
param_type: SearchParamType::Token,
column_prefix: "status".to_string(),
},
CompositeComponentDef {
param_type: SearchParamType::Date,
column_prefix: "date".to_string(),
},
];
let frag = CompositeHandler::build_sql(&value, &components, 0);
assert!(frag.sql.contains("value_token_code"));
assert!(frag.sql.contains("value_date"));
}
}