use crate::search::{converters::IndexValue, extractor::ExtractedValue};
pub struct SqliteSearchIndexWriter;
impl SqliteSearchIndexWriter {
pub fn new() -> Self {
Self
}
pub fn insert_sql() -> &'static str {
r#"
INSERT INTO search_index (
tenant_id, resource_type, resource_id, param_name, param_url,
value_string, value_token_system, value_token_code, value_token_display,
value_date, value_date_precision,
value_number, value_quantity_value, value_quantity_unit, value_quantity_system,
value_reference, value_uri, composite_group,
value_identifier_type_system, value_identifier_type_code
) VALUES (
?1, ?2, ?3, ?4, ?5,
?6, ?7, ?8, ?9,
?10, ?11,
?12, ?13, ?14, ?15,
?16, ?17, ?18,
?19, ?20
)
"#
}
pub fn delete_sql() -> &'static str {
"DELETE FROM search_index WHERE tenant_id = ?1 AND resource_type = ?2 AND resource_id = ?3"
}
pub fn delete_param_sql() -> &'static str {
"DELETE FROM search_index WHERE tenant_id = ?1 AND resource_type = ?2 AND resource_id = ?3 AND param_name = ?4"
}
pub fn to_sql_params(
tenant_id: &str,
resource_type: &str,
resource_id: &str,
extracted: &ExtractedValue,
) -> Vec<SqlValue> {
let mut params = vec![
SqlValue::String(tenant_id.to_string()),
SqlValue::String(resource_type.to_string()),
SqlValue::String(resource_id.to_string()),
SqlValue::String(extracted.param_name.clone()),
SqlValue::String(extracted.param_url.clone()),
];
match &extracted.value {
IndexValue::String(s) => {
params.push(SqlValue::OptString(Some(s.clone()))); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); }
IndexValue::Token {
system,
code,
display,
identifier_type_system,
identifier_type_code,
} => {
params.push(SqlValue::Null); params.push(SqlValue::OptString(system.clone())); params.push(SqlValue::String(code.clone())); params.push(SqlValue::OptString(display.clone())); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::OptInt(
extracted.composite_group.map(|g| g as i64),
)); params.push(SqlValue::OptString(identifier_type_system.clone())); params.push(SqlValue::OptString(identifier_type_code.clone())); return params;
}
IndexValue::Date { value, precision } => {
params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::String(value.clone())); params.push(SqlValue::String(precision.to_string())); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); }
IndexValue::Number(n) => {
params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Float(*n)); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); }
IndexValue::Quantity {
value,
unit,
system,
code: _,
} => {
params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Float(*value)); params.push(SqlValue::OptString(unit.clone())); params.push(SqlValue::OptString(system.clone())); params.push(SqlValue::Null); params.push(SqlValue::Null); }
IndexValue::Reference {
reference,
resource_type: _,
resource_id: _,
} => {
params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::String(reference.clone())); params.push(SqlValue::Null); }
IndexValue::Uri(uri) => {
params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::Null); params.push(SqlValue::String(uri.clone())); }
}
params.push(SqlValue::OptInt(
extracted.composite_group.map(|g| g as i64),
)); params.push(SqlValue::Null); params.push(SqlValue::Null);
params
}
}
impl Default for SqliteSearchIndexWriter {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub enum SqlValue {
String(String),
OptString(Option<String>),
Int(i64),
OptInt(Option<i64>),
Float(f64),
Null,
}
impl SqlValue {
pub fn is_null(&self) -> bool {
matches!(
self,
SqlValue::Null | SqlValue::OptString(None) | SqlValue::OptInt(None)
)
}
pub fn as_sql_string(&self) -> Option<String> {
match self {
SqlValue::String(s) => Some(s.clone()),
SqlValue::OptString(Some(s)) => Some(s.clone()),
SqlValue::Int(i) => Some(i.to_string()),
SqlValue::OptInt(Some(i)) => Some(i.to_string()),
SqlValue::Float(f) => Some(f.to_string()),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{DatePrecision, SearchParamType};
#[test]
fn test_string_value_params() {
let extracted = ExtractedValue {
param_name: "name".to_string(),
param_url: "http://hl7.org/fhir/SearchParameter/Patient-name".to_string(),
param_type: SearchParamType::String,
value: IndexValue::String("Smith".to_string()),
composite_group: None,
};
let params =
SqliteSearchIndexWriter::to_sql_params("tenant1", "Patient", "123", &extracted);
assert_eq!(params.len(), 20); assert!(matches!(¶ms[0], SqlValue::String(s) if s == "tenant1"));
assert!(matches!(¶ms[5], SqlValue::OptString(Some(s)) if s == "Smith"));
}
#[test]
fn test_token_value_params() {
let extracted = ExtractedValue {
param_name: "identifier".to_string(),
param_url: "http://hl7.org/fhir/SearchParameter/Patient-identifier".to_string(),
param_type: SearchParamType::Token,
value: IndexValue::Token {
system: Some("http://example.org".to_string()),
code: "12345".to_string(),
display: None,
identifier_type_system: None,
identifier_type_code: None,
},
composite_group: None,
};
let params =
SqliteSearchIndexWriter::to_sql_params("tenant1", "Patient", "123", &extracted);
assert_eq!(params.len(), 20); assert!(matches!(¶ms[6], SqlValue::OptString(Some(s)) if s == "http://example.org"));
assert!(matches!(¶ms[7], SqlValue::String(s) if s == "12345"));
}
#[test]
fn test_token_with_display_params() {
let extracted = ExtractedValue {
param_name: "code".to_string(),
param_url: "http://hl7.org/fhir/SearchParameter/clinical-code".to_string(),
param_type: SearchParamType::Token,
value: IndexValue::Token {
system: Some("http://loinc.org".to_string()),
code: "12345-6".to_string(),
display: Some("Test Display".to_string()),
identifier_type_system: None,
identifier_type_code: None,
},
composite_group: None,
};
let params =
SqliteSearchIndexWriter::to_sql_params("tenant1", "Observation", "123", &extracted);
assert_eq!(params.len(), 20);
assert!(matches!(¶ms[8], SqlValue::OptString(Some(s)) if s == "Test Display")); }
#[test]
fn test_identifier_with_type_params() {
let extracted = ExtractedValue {
param_name: "identifier".to_string(),
param_url: "http://hl7.org/fhir/SearchParameter/Patient-identifier".to_string(),
param_type: SearchParamType::Token,
value: IndexValue::Token {
system: Some("http://hospital.org/mrn".to_string()),
code: "MRN12345".to_string(),
display: None,
identifier_type_system: Some(
"http://terminology.hl7.org/CodeSystem/v2-0203".to_string(),
),
identifier_type_code: Some("MR".to_string()),
},
composite_group: None,
};
let params =
SqliteSearchIndexWriter::to_sql_params("tenant1", "Patient", "123", &extracted);
assert_eq!(params.len(), 20);
assert!(
matches!(¶ms[18], SqlValue::OptString(Some(s)) if s == "http://terminology.hl7.org/CodeSystem/v2-0203")
);
assert!(matches!(¶ms[19], SqlValue::OptString(Some(s)) if s == "MR"));
}
#[test]
fn test_date_value_params() {
let extracted = ExtractedValue {
param_name: "birthdate".to_string(),
param_url: "http://hl7.org/fhir/SearchParameter/Patient-birthdate".to_string(),
param_type: SearchParamType::Date,
value: IndexValue::Date {
value: "2024-01-15".to_string(),
precision: DatePrecision::Day,
},
composite_group: None,
};
let params =
SqliteSearchIndexWriter::to_sql_params("tenant1", "Patient", "123", &extracted);
assert!(matches!(¶ms[9], SqlValue::String(s) if s == "2024-01-15")); }
#[test]
fn test_quantity_value_params() {
let extracted = ExtractedValue {
param_name: "value-quantity".to_string(),
param_url: "http://hl7.org/fhir/SearchParameter/Observation-value-quantity".to_string(),
param_type: SearchParamType::Quantity,
value: IndexValue::Quantity {
value: 5.4,
unit: Some("mg".to_string()),
system: Some("http://unitsofmeasure.org".to_string()),
code: Some("mg".to_string()),
},
composite_group: None,
};
let params =
SqliteSearchIndexWriter::to_sql_params("tenant1", "Observation", "456", &extracted);
assert!(matches!(¶ms[12], SqlValue::Float(f) if (*f - 5.4).abs() < 0.001)); assert!(matches!(¶ms[13], SqlValue::OptString(Some(s)) if s == "mg")); }
}