diff --git a/src/alerts/alert_structs.rs b/src/alerts/alert_structs.rs index c37f30d53..fbfd363b7 100644 --- a/src/alerts/alert_structs.rs +++ b/src/alerts/alert_structs.rs @@ -179,7 +179,23 @@ pub struct FilterConfig { pub struct ConditionConfig { pub column: String, pub operator: WhereConfigOperator, - pub value: Option, + pub value: Option, + #[serde(rename = "type")] + pub value_type: Option, +} + +impl ConditionConfig { + pub fn value_as_sql_str(&self) -> Option { + let value = self.value.as_ref()?; + + match value { + serde_json::Value::String(s) => Some(s.clone()), + serde_json::Value::Bool(b) => Some(b.to_string()), + serde_json::Value::Number(n) => Some(n.to_string()), + + _ => None, + } + } } #[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] @@ -196,26 +212,22 @@ impl Conditions { LogicalOperator::And | LogicalOperator::Or => { let expr1 = &self.condition_config[0]; let expr2 = &self.condition_config[1]; - let expr1_msg = if expr1.value.as_ref().is_some_and(|v| !v.is_empty()) { - format!( - "{} {} {}", - expr1.column, - expr1.operator, - expr1.value.as_ref().unwrap() - ) + let expr1_msg = if let Some(expr1_val) = expr1.value_as_sql_str() { + format!("{} {} {}", expr1.column, expr1.operator, expr1_val,) } else { - format!("{} {}", expr1.column, expr1.operator) + match expr1.value { + None => format!("{} {}", expr1.column, expr1.operator), + Some(_) => "unsupported JSON value type in filter".into(), + } }; - let expr2_msg = if expr2.value.as_ref().is_some_and(|v| !v.is_empty()) { - format!( - "{} {} {}", - expr2.column, - expr2.operator, - expr2.value.as_ref().unwrap() - ) + let expr2_msg = if let Some(expr2_val) = expr2.value_as_sql_str() { + format!("{} {} {}", expr2.column, expr2.operator, expr2_val) } else { - format!("{} {}", expr2.column, expr2.operator) + match expr2.value { + None => format!("{} {}", expr2.column, expr2.operator), + Some(_) => "unsupported JSON value type in filter".into(), + } }; format!("[{expr1_msg} {op} {expr2_msg}]") diff --git a/src/alerts/alerts_utils.rs b/src/alerts/alerts_utils.rs index 0d5552f31..b0261ddf3 100644 --- a/src/alerts/alerts_utils.rs +++ b/src/alerts/alerts_utils.rs @@ -364,81 +364,131 @@ pub fn get_filter_string(where_clause: &Conditions) -> Result { &LogicalOperator::And => { let mut exprs = vec![]; for condition in &where_clause.condition_config { - if condition.value.as_ref().is_some_and(|v| !v.is_empty()) { - // ad-hoc error check in case value is some and operator is either `is null` or `is not null` - if condition.operator.eq(&WhereConfigOperator::IsNull) - || condition.operator.eq(&WhereConfigOperator::IsNotNull) - { - return Err("value must be null when operator is either `is null` or `is not null`" - .into()); + match condition.value.clone() { + Some(serde_json::Value::String(value)) => { + if !value.is_empty() { + // ad-hoc error check in case value is some and operator is either `is null` or `is not null` + if condition.operator.eq(&WhereConfigOperator::IsNull) + || condition.operator.eq(&WhereConfigOperator::IsNotNull) + { + return Err("value must be null when operator is either `is null` or `is not null`".into()); + } + + let operator_and_value = match condition.operator { + WhereConfigOperator::Contains => { + let escaped_value = value + .replace("'", "\\'") + .replace('%', "\\%") + .replace('_', "\\_"); + format!("LIKE '%{escaped_value}%' ESCAPE '\\'") + } + WhereConfigOperator::DoesNotContain => { + let escaped_value = value + .replace("'", "\\'") + .replace('%', "\\%") + .replace('_', "\\_"); + format!("NOT LIKE '%{escaped_value}%' ESCAPE '\\'") + } + WhereConfigOperator::ILike => { + let escaped_value = value + .replace("'", "\\'") + .replace('%', "\\%") + .replace('_', "\\_"); + format!("ILIKE '%{escaped_value}%' ESCAPE '\\'") + } + WhereConfigOperator::BeginsWith => { + let escaped_value = value + .replace("'", "\\'") + .replace('%', "\\%") + .replace('_', "\\_"); + format!("LIKE '{escaped_value}%' ESCAPE '\\'") + } + WhereConfigOperator::DoesNotBeginWith => { + let escaped_value = value + .replace("'", "\\'") + .replace('%', "\\%") + .replace('_', "\\_"); + format!("NOT LIKE '{escaped_value}%' ESCAPE '\\'") + } + WhereConfigOperator::EndsWith => { + let escaped_value = value + .replace("'", "\\'") + .replace('%', "\\%") + .replace('_', "\\_"); + format!("LIKE '%{escaped_value}' ESCAPE '\\'") + } + WhereConfigOperator::DoesNotEndWith => { + let escaped_value = value + .replace("'", "\\'") + .replace('%', "\\%") + .replace('_', "\\_"); + format!("NOT LIKE '%{escaped_value}' ESCAPE '\\'") + } + _ => format!( + "{} '{}'", + condition.operator, + value.replace("'", "''") + ), + }; + exprs.push(format!( + "\"{}\" {}", + condition.column, operator_and_value + )); + } else { + match condition.operator { + WhereConfigOperator::Equal => { + exprs.push(format!("\"{}\" = ''", condition.column)); + } + WhereConfigOperator::NotEqual => { + exprs.push(format!("\"{}\" != ''", condition.column)); + } + _ => { + tracing::warn!( + "ignoring empty string filter for: {} {}", + condition.column, + condition.operator + ); + continue; + // or return an error instead? + } + } + } } - let value = condition.value.as_ref().unwrap(); + Some(_) => { + let Some(value) = condition.value_as_sql_str() else { + return Err("unsupported JSON value type in filters".into()); + }; + exprs.push(format!( + "\"{}\" {} {}", + condition.column, condition.operator, value + )) + } - let operator_and_value = match condition.operator { - WhereConfigOperator::Contains => { - let escaped_value = value - .replace("'", "\\'") - .replace('%', "\\%") - .replace('_', "\\_"); - format!("LIKE '%{escaped_value}%' ESCAPE '\\'") - } - WhereConfigOperator::DoesNotContain => { - let escaped_value = value - .replace("'", "\\'") - .replace('%', "\\%") - .replace('_', "\\_"); - format!("NOT LIKE '%{escaped_value}%' ESCAPE '\\'") - } - WhereConfigOperator::ILike => { - let escaped_value = value - .replace("'", "\\'") - .replace('%', "\\%") - .replace('_', "\\_"); - format!("ILIKE '%{escaped_value}%' ESCAPE '\\'") - } - WhereConfigOperator::BeginsWith => { - let escaped_value = value - .replace("'", "\\'") - .replace('%', "\\%") - .replace('_', "\\_"); - format!("LIKE '{escaped_value}%' ESCAPE '\\'") - } - WhereConfigOperator::DoesNotBeginWith => { - let escaped_value = value - .replace("'", "\\'") - .replace('%', "\\%") - .replace('_', "\\_"); - format!("NOT LIKE '{escaped_value}%' ESCAPE '\\'") - } - WhereConfigOperator::EndsWith => { - let escaped_value = value - .replace("'", "\\'") - .replace('%', "\\%") - .replace('_', "\\_"); - format!("LIKE '%{escaped_value}' ESCAPE '\\'") - } - WhereConfigOperator::DoesNotEndWith => { - let escaped_value = value - .replace("'", "\\'") - .replace('%', "\\%") - .replace('_', "\\_"); - format!("NOT LIKE '%{escaped_value}' ESCAPE '\\'") + None => match condition.operator { + WhereConfigOperator::IsNull | WhereConfigOperator::IsNotNull => exprs + .push(format!("\"{}\" {}", condition.column, condition.operator)), + + WhereConfigOperator::Equal | WhereConfigOperator::NotEqual => { + if condition.value_type.as_ref().is_some_and(|v| v == "null") { + let operator = match condition.operator { + WhereConfigOperator::Equal => WhereConfigOperator::IsNull, + _ => WhereConfigOperator::IsNotNull, + }; + exprs.push(format!("\"{}\" {}", condition.column, operator)); + } else { + return Err(format!( + "For [value: null], explicitly set [type: \"null\"]" + )); + } } _ => { - let value = match ValueType::from_string(value.to_owned()) { - ValueType::Number(val) => format!("{val}"), - ValueType::Boolean(val) => format!("{val}"), - ValueType::String(val) => { - format!("'{val}'") - } - }; - format!("{} {}", condition.operator, value) + return Err(format!( + "invalid null operation: [{}]", + condition.operator + )); } - }; - exprs.push(format!("\"{}\" {}", condition.column, operator_and_value)) - } else { - exprs.push(format!("\"{}\" {}", condition.column, condition.operator)) + }, } }