use chrono::{DateTime, Utc};
use crate::types::{
SearchModifier, SearchParamType, SearchParameter, SearchPrefix, SearchQuery, SearchValue,
};
#[derive(Debug, Clone)]
pub struct SqlFragment {
pub sql: String,
pub params: Vec<SqlParam>,
}
#[derive(Debug, Clone)]
pub enum SqlParam {
Text(String),
Float(f64),
Integer(i64),
Bool(bool),
Timestamp(DateTime<Utc>),
Null,
}
impl SqlParam {
pub fn text(s: &str) -> Self {
SqlParam::Text(s.to_string())
}
}
impl SqlFragment {
pub fn new(sql: impl Into<String>) -> Self {
Self {
sql: sql.into(),
params: Vec::new(),
}
}
pub fn with_params(sql: impl Into<String>, params: Vec<SqlParam>) -> Self {
Self {
sql: sql.into(),
params,
}
}
pub fn and(self, other: SqlFragment) -> SqlFragment {
SqlFragment {
sql: format!("({}) AND ({})", self.sql, other.sql),
params: [self.params, other.params].concat(),
}
}
pub fn or(self, other: SqlFragment) -> SqlFragment {
SqlFragment {
sql: format!("({}) OR ({})", self.sql, other.sql),
params: [self.params, other.params].concat(),
}
}
}
pub struct PostgresQueryBuilder;
impl PostgresQueryBuilder {
pub fn build_search_query(query: &SearchQuery, param_offset: usize) -> Option<SqlFragment> {
let mut conditions = Vec::new();
let mut current_offset = param_offset;
for param in &query.parameters {
if let Some(condition) = Self::build_parameter_condition(param, current_offset) {
current_offset += condition.params.len();
conditions.push(condition);
}
}
if conditions.is_empty() {
return None;
}
let mut combined = conditions.remove(0);
for cond in conditions {
combined = combined.and(cond);
}
Some(combined)
}
fn build_parameter_condition(
param: &SearchParameter,
param_offset: usize,
) -> Option<SqlFragment> {
if param.values.is_empty() {
return None;
}
match param.name.as_str() {
"_id" => return Self::build_id_condition(¶m.values, param_offset),
"_lastUpdated" => {
return Self::build_last_updated_condition(¶m.values, param_offset);
}
_ => {}
}
match param.param_type {
SearchParamType::String => Self::build_string_condition(param, param_offset),
SearchParamType::Token => Self::build_token_condition(param, param_offset),
SearchParamType::Date => Self::build_date_condition(param, param_offset),
SearchParamType::Number => Self::build_number_condition(param, param_offset),
SearchParamType::Quantity => Self::build_quantity_condition(param, param_offset),
SearchParamType::Reference => Self::build_reference_condition(param, param_offset),
SearchParamType::Uri => Self::build_uri_condition(param, param_offset),
SearchParamType::Composite => None,
SearchParamType::Special => None,
}
}
fn build_id_condition(values: &[SearchValue], offset: usize) -> Option<SqlFragment> {
let mut conditions = Vec::new();
for (i, value) in values.iter().enumerate() {
let param_num = offset + i + 1;
conditions.push(SqlFragment::with_params(
format!("id = ${}", param_num),
vec![SqlParam::text(&value.value)],
));
}
if conditions.is_empty() {
return None;
}
let mut combined = conditions.remove(0);
for cond in conditions {
combined = combined.or(cond);
}
Some(combined)
}
fn build_last_updated_condition(values: &[SearchValue], offset: usize) -> Option<SqlFragment> {
let mut conditions = Vec::new();
for (i, value) in values.iter().enumerate() {
let param_num = offset + i + 1;
let op = Self::prefix_to_operator(&value.prefix);
conditions.push(SqlFragment::with_params(
format!("last_updated {} ${}", op, param_num),
vec![SqlParam::text(&value.value)],
));
}
if conditions.is_empty() {
return None;
}
let mut combined = conditions.remove(0);
for cond in conditions {
combined = combined.and(cond);
}
Some(combined)
}
fn build_string_condition(param: &SearchParameter, offset: usize) -> Option<SqlFragment> {
let modifier = param.modifier.as_ref();
let mut conditions = Vec::new();
for (i, value) in param.values.iter().enumerate() {
let param_num = offset + i + 1;
let condition = match modifier {
Some(SearchModifier::Exact) => SqlFragment::with_params(
format!(
"id IN (SELECT resource_id FROM search_index WHERE tenant_id = $1 AND resource_type = $2 AND param_name = '{}' AND value_string = ${})",
param.name, param_num
),
vec![SqlParam::text(&value.value)],
),
Some(SearchModifier::Contains) => SqlFragment::with_params(
format!(
"id IN (SELECT resource_id FROM search_index WHERE tenant_id = $1 AND resource_type = $2 AND param_name = '{}' AND value_string ILIKE ${})",
param.name, param_num
),
vec![SqlParam::text(&format!("%{}%", value.value))],
),
_ => {
SqlFragment::with_params(
format!(
"id IN (SELECT resource_id FROM search_index WHERE tenant_id = $1 AND resource_type = $2 AND param_name = '{}' AND value_string ILIKE ${})",
param.name, param_num
),
vec![SqlParam::text(&format!("{}%", value.value))],
)
}
};
conditions.push(condition);
}
if conditions.is_empty() {
return None;
}
let mut combined = conditions.remove(0);
for cond in conditions {
combined = combined.or(cond);
}
Some(combined)
}
fn build_token_condition(param: &SearchParameter, offset: usize) -> Option<SqlFragment> {
let mut conditions = Vec::new();
for (i, value) in param.values.iter().enumerate() {
let base_offset = offset + i * 2;
let condition = if let Some((system, code)) = value.value.split_once('|') {
if system.is_empty() {
SqlFragment::with_params(
format!(
"id IN (SELECT resource_id FROM search_index WHERE tenant_id = $1 AND resource_type = $2 AND param_name = '{}' AND value_token_code = ${})",
param.name,
base_offset + 1
),
vec![SqlParam::text(code)],
)
} else if code.is_empty() {
SqlFragment::with_params(
format!(
"id IN (SELECT resource_id FROM search_index WHERE tenant_id = $1 AND resource_type = $2 AND param_name = '{}' AND value_token_system = ${})",
param.name,
base_offset + 1
),
vec![SqlParam::text(system)],
)
} else {
SqlFragment::with_params(
format!(
"id IN (SELECT resource_id FROM search_index WHERE tenant_id = $1 AND resource_type = $2 AND param_name = '{}' AND value_token_system = ${} AND value_token_code = ${})",
param.name,
base_offset + 1,
base_offset + 2
),
vec![SqlParam::text(system), SqlParam::text(code)],
)
}
} else {
SqlFragment::with_params(
format!(
"id IN (SELECT resource_id FROM search_index WHERE tenant_id = $1 AND resource_type = $2 AND param_name = '{}' AND value_token_code = ${})",
param.name,
base_offset + 1
),
vec![SqlParam::text(&value.value)],
)
};
conditions.push(condition);
}
if conditions.is_empty() {
return None;
}
let mut combined = conditions.remove(0);
for cond in conditions {
combined = combined.or(cond);
}
Some(combined)
}
fn build_date_condition(param: &SearchParameter, offset: usize) -> Option<SqlFragment> {
let mut conditions = Vec::new();
for (i, value) in param.values.iter().enumerate() {
let param_num = offset + i + 1;
let op = Self::prefix_to_operator(&value.prefix);
let timestamp = Self::parse_date_value(&value.value);
conditions.push(SqlFragment::with_params(
format!(
"id IN (SELECT resource_id FROM search_index WHERE tenant_id = $1 AND resource_type = $2 AND param_name = '{}' AND value_date {} ${})",
param.name, op, param_num
),
vec![SqlParam::Timestamp(timestamp)],
));
}
if conditions.is_empty() {
return None;
}
let mut combined = conditions.remove(0);
for cond in conditions {
combined = combined.and(cond);
}
Some(combined)
}
fn build_number_condition(param: &SearchParameter, offset: usize) -> Option<SqlFragment> {
let mut conditions = Vec::new();
for (i, value) in param.values.iter().enumerate() {
let param_num = offset + i + 1;
let op = Self::prefix_to_operator(&value.prefix);
if let Ok(num) = value.value.parse::<f64>() {
conditions.push(SqlFragment::with_params(
format!(
"id IN (SELECT resource_id FROM search_index WHERE tenant_id = $1 AND resource_type = $2 AND param_name = '{}' AND value_number {} ${})",
param.name, op, param_num
),
vec![SqlParam::Float(num)],
));
}
}
if conditions.is_empty() {
return None;
}
let mut combined = conditions.remove(0);
for cond in conditions {
combined = combined.and(cond);
}
Some(combined)
}
fn build_quantity_condition(param: &SearchParameter, offset: usize) -> Option<SqlFragment> {
let mut conditions = Vec::new();
for (i, value) in param.values.iter().enumerate() {
let base_offset = offset + i * 2;
let parts: Vec<&str> = value.value.splitn(3, '|').collect();
if let Some(num_str) = parts.first() {
if let Ok(num) = num_str.parse::<f64>() {
let op = Self::prefix_to_operator(&value.prefix);
if parts.len() >= 3 {
conditions.push(SqlFragment::with_params(
format!(
"id IN (SELECT resource_id FROM search_index WHERE tenant_id = $1 AND resource_type = $2 AND param_name = '{}' AND value_quantity_value {} ${} AND value_quantity_unit = ${})",
param.name, op, base_offset + 1, base_offset + 2
),
vec![SqlParam::Float(num), SqlParam::text(parts[2])],
));
} else {
conditions.push(SqlFragment::with_params(
format!(
"id IN (SELECT resource_id FROM search_index WHERE tenant_id = $1 AND resource_type = $2 AND param_name = '{}' AND value_quantity_value {} ${})",
param.name, op, base_offset + 1
),
vec![SqlParam::Float(num)],
));
}
}
}
}
if conditions.is_empty() {
return None;
}
let mut combined = conditions.remove(0);
for cond in conditions {
combined = combined.and(cond);
}
Some(combined)
}
fn build_reference_condition(param: &SearchParameter, offset: usize) -> Option<SqlFragment> {
let mut conditions = Vec::new();
for (i, value) in param.values.iter().enumerate() {
let param_num = offset + i + 1;
conditions.push(SqlFragment::with_params(
format!(
"id IN (SELECT resource_id FROM search_index WHERE tenant_id = $1 AND resource_type = $2 AND param_name = '{}' AND value_reference = ${})",
param.name, param_num
),
vec![SqlParam::text(&value.value)],
));
}
if conditions.is_empty() {
return None;
}
let mut combined = conditions.remove(0);
for cond in conditions {
combined = combined.or(cond);
}
Some(combined)
}
fn build_uri_condition(param: &SearchParameter, offset: usize) -> Option<SqlFragment> {
let modifier = param.modifier.as_ref();
let mut conditions = Vec::new();
for (i, value) in param.values.iter().enumerate() {
let param_num = offset + i + 1;
let condition = match modifier {
Some(SearchModifier::Below) => SqlFragment::with_params(
format!(
"id IN (SELECT resource_id FROM search_index WHERE tenant_id = $1 AND resource_type = $2 AND param_name = '{}' AND value_uri LIKE ${} || '%')",
param.name, param_num
),
vec![SqlParam::text(&value.value)],
),
Some(SearchModifier::Above) => SqlFragment::with_params(
format!(
"id IN (SELECT resource_id FROM search_index WHERE tenant_id = $1 AND resource_type = $2 AND param_name = '{}' AND ${} LIKE value_uri || '%')",
param.name, param_num
),
vec![SqlParam::text(&value.value)],
),
_ => SqlFragment::with_params(
format!(
"id IN (SELECT resource_id FROM search_index WHERE tenant_id = $1 AND resource_type = $2 AND param_name = '{}' AND value_uri = ${})",
param.name, param_num
),
vec![SqlParam::text(&value.value)],
),
};
conditions.push(condition);
}
if conditions.is_empty() {
return None;
}
let mut combined = conditions.remove(0);
for cond in conditions {
combined = combined.or(cond);
}
Some(combined)
}
fn prefix_to_operator(prefix: &SearchPrefix) -> &'static str {
match prefix {
SearchPrefix::Eq => "=",
SearchPrefix::Ne => "!=",
SearchPrefix::Gt => ">",
SearchPrefix::Lt => "<",
SearchPrefix::Ge => ">=",
SearchPrefix::Le => "<=",
SearchPrefix::Sa => ">", SearchPrefix::Eb => "<", SearchPrefix::Ap => "=", }
}
fn parse_date_value(value: &str) -> DateTime<Utc> {
let normalized = if value.contains('T') {
if value.contains('+') || value.contains('Z') || value.ends_with("-00:00") {
value.to_string()
} else {
format!("{}+00:00", value)
}
} else if value.len() == 10 {
format!("{}T00:00:00+00:00", value)
} else if value.len() == 7 {
format!("{}-01T00:00:00+00:00", value)
} else if value.len() == 4 {
format!("{}-01-01T00:00:00+00:00", value)
} else {
value.to_string()
};
DateTime::parse_from_rfc3339(&normalized)
.map(|dt| dt.with_timezone(&Utc))
.or_else(|_| normalized.parse::<DateTime<Utc>>())
.unwrap_or_else(|_| Utc::now())
}
}