From 1a54fa3dc2e18a01bd96a7899d61586c9ba50cb7 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha Date: Wed, 28 Jan 2026 20:50:21 +1100 Subject: [PATCH 1/4] fix: counts api 1. list type fields 2. text with empty value 3. boolean fields --- src/alerts/alert_structs.rs | 3 + src/alerts/alerts_utils.rs | 182 ++++++++++++++++++++---------------- 2 files changed, 107 insertions(+), 78 deletions(-) diff --git a/src/alerts/alert_structs.rs b/src/alerts/alert_structs.rs index c37f30d53..575f3cc79 100644 --- a/src/alerts/alert_structs.rs +++ b/src/alerts/alert_structs.rs @@ -180,6 +180,9 @@ pub struct ConditionConfig { pub column: String, pub operator: WhereConfigOperator, pub value: Option, + #[serde(rename = "type")] + #[serde(default)] + pub column_type: Option, } #[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] diff --git a/src/alerts/alerts_utils.rs b/src/alerts/alerts_utils.rs index 0d5552f31..efb377d80 100644 --- a/src/alerts/alerts_utils.rs +++ b/src/alerts/alerts_utils.rs @@ -29,7 +29,7 @@ use tracing::trace; use crate::{ alerts::{ AlertTrait, LogicalOperator, WhereConfigOperator, - alert_structs::{AlertQueryResult, Conditions, GroupResult}, + alert_structs::{AlertQueryResult, ConditionConfig, Conditions, GroupResult}, extract_aggregate_aliases, }, handlers::http::{ @@ -364,84 +364,8 @@ 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()); - } - - let value = condition.value.as_ref().unwrap(); - - 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 '\\'") - } - _ => { - 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) - } - }; - exprs.push(format!("\"{}\" {}", condition.column, operator_and_value)) - } else { - exprs.push(format!("\"{}\" {}", condition.column, condition.operator)) - } + exprs.push(condition_to_expr(condition)?); } - Ok(exprs.join(" AND ")) } _ => Err(String::from("Invalid option 'or', only 'and' is supported")), @@ -452,6 +376,108 @@ pub fn get_filter_string(where_clause: &Conditions) -> Result { } } +fn condition_to_expr(condition: &ConditionConfig) -> Result { + // is null / is not null don't take a value + if condition.operator == WhereConfigOperator::IsNull + || condition.operator == WhereConfigOperator::IsNotNull + { + if condition.value.as_ref().is_some_and(|v| !v.is_empty()) { + return Err( + "value must be null when operator is either `is null` or `is not null`".into(), + ); + } + return Ok(format!("\"{}\" {}", condition.column, condition.operator)); + } + + let value = condition.value.as_deref().unwrap_or(""); + + let is_list_type = condition + .column_type + .as_ref() + .is_some_and(|t| t.starts_with("list")); + + if is_list_type { + list_condition_expr(&condition.column, &condition.operator, value) + } else { + scalar_condition_expr( + &condition.column, + &condition.operator, + value, + condition.column_type.as_deref(), + ) + } +} + +fn list_condition_expr( + column: &str, + operator: &WhereConfigOperator, + value: &str, +) -> Result { + match operator { + WhereConfigOperator::Contains => Ok(format!("array_has_all(\"{column}\", ARRAY[{value}])")), + WhereConfigOperator::DoesNotContain => { + Ok(format!("NOT array_has_all(\"{column}\", ARRAY[{value}])")) + } + WhereConfigOperator::Equal => Ok(format!("\"{column}\" = ARRAY[{value}]")), + WhereConfigOperator::NotEqual => Ok(format!("\"{column}\" != ARRAY[{value}]")), + _ => Err(format!( + "Operator '{operator}' is not supported for list type columns" + )), + } +} + +fn scalar_condition_expr( + column: &str, + operator: &WhereConfigOperator, + value: &str, + column_type: Option<&str>, +) -> Result { + let operator_and_value = match operator { + WhereConfigOperator::Contains => { + format!("LIKE '%{}%' ESCAPE '\\'", escape_like(value)) + } + WhereConfigOperator::DoesNotContain => { + format!("NOT LIKE '%{}%' ESCAPE '\\'", escape_like(value)) + } + WhereConfigOperator::ILike => { + format!("ILIKE '%{}%' ESCAPE '\\'", escape_like(value)) + } + WhereConfigOperator::BeginsWith => { + format!("LIKE '{}%' ESCAPE '\\'", escape_like(value)) + } + WhereConfigOperator::DoesNotBeginWith => { + format!("NOT LIKE '{}%' ESCAPE '\\'", escape_like(value)) + } + WhereConfigOperator::EndsWith => { + format!("LIKE '%{}' ESCAPE '\\'", escape_like(value)) + } + WhereConfigOperator::DoesNotEndWith => { + format!("NOT LIKE '%{}' ESCAPE '\\'", escape_like(value)) + } + _ => { + let formatted = match column_type { + Some("bool") | Some("boolean") => value.to_string(), + Some("int") | Some("float") | Some("number") => value.to_string(), + Some(_) => format!("'{}'", value.replace("'", "''")), + None => match ValueType::from_string(value.to_owned()) { + ValueType::Number(val) => format!("{val}"), + ValueType::Boolean(val) => format!("{val}"), + ValueType::String(val) => format!("'{}'", val.replace("'", "''")), + }, + }; + format!("{operator} {formatted}") + } + }; + Ok(format!("\"{column}\" {operator_and_value}")) +} + +fn escape_like(value: &str) -> String { + value + .replace("'", "''") + .replace('%', "\\%") + .replace('_', "\\_") +} + enum ValueType { Number(f64), String(String), From a8759f7dff02b561737bb48a3fdd3989466087e7 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha Date: Wed, 28 Jan 2026 21:33:57 +1100 Subject: [PATCH 2/4] deepsource fix --- src/alerts/alerts_utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/alerts/alerts_utils.rs b/src/alerts/alerts_utils.rs index efb377d80..cef7b22c7 100644 --- a/src/alerts/alerts_utils.rs +++ b/src/alerts/alerts_utils.rs @@ -473,7 +473,7 @@ fn scalar_condition_expr( fn escape_like(value: &str) -> String { value - .replace("'", "''") + .replace('\'', "''") .replace('%', "\\%") .replace('_', "\\_") } From f1a6869ca84130523a231d78b215134f0ebe66db Mon Sep 17 00:00:00 2001 From: Nikhil Sinha Date: Wed, 28 Jan 2026 23:00:44 +1100 Subject: [PATCH 3/4] fix list type --- src/alerts/alerts_utils.rs | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/alerts/alerts_utils.rs b/src/alerts/alerts_utils.rs index cef7b22c7..86324d62b 100644 --- a/src/alerts/alerts_utils.rs +++ b/src/alerts/alerts_utils.rs @@ -413,13 +413,22 @@ fn list_condition_expr( operator: &WhereConfigOperator, value: &str, ) -> Result { + // Strip surrounding brackets if present to avoid nested arrays + let inner_value = value + .trim() + .strip_prefix('[') + .and_then(|s| s.strip_suffix(']')) + .unwrap_or(value); + match operator { - WhereConfigOperator::Contains => Ok(format!("array_has_all(\"{column}\", ARRAY[{value}])")), - WhereConfigOperator::DoesNotContain => { - Ok(format!("NOT array_has_all(\"{column}\", ARRAY[{value}])")) + WhereConfigOperator::Contains => { + Ok(format!("array_has_all(\"{column}\", ARRAY[{inner_value}])")) } - WhereConfigOperator::Equal => Ok(format!("\"{column}\" = ARRAY[{value}]")), - WhereConfigOperator::NotEqual => Ok(format!("\"{column}\" != ARRAY[{value}]")), + WhereConfigOperator::DoesNotContain => Ok(format!( + "NOT array_has_all(\"{column}\", ARRAY[{inner_value}])" + )), + WhereConfigOperator::Equal => Ok(format!("\"{column}\" = ARRAY[{inner_value}]")), + WhereConfigOperator::NotEqual => Ok(format!("\"{column}\" != ARRAY[{inner_value}]")), _ => Err(format!( "Operator '{operator}' is not supported for list type columns" )), @@ -458,11 +467,11 @@ fn scalar_condition_expr( let formatted = match column_type { Some("bool") | Some("boolean") => value.to_string(), Some("int") | Some("float") | Some("number") => value.to_string(), - Some(_) => format!("'{}'", value.replace("'", "''")), + Some(_) => format!("'{}'", value.replace('\'', "''")), None => match ValueType::from_string(value.to_owned()) { ValueType::Number(val) => format!("{val}"), ValueType::Boolean(val) => format!("{val}"), - ValueType::String(val) => format!("'{}'", val.replace("'", "''")), + ValueType::String(val) => format!("'{}'", val.replace('\'', "''")), }, }; format!("{operator} {formatted}") From 5ad6b4f0540c4324e9a394f88abfc597fc877d76 Mon Sep 17 00:00:00 2001 From: Nikhil Sinha Date: Thu, 29 Jan 2026 01:33:08 +1100 Subject: [PATCH 4/4] parse bool/f64, escape --- src/alerts/alerts_utils.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/alerts/alerts_utils.rs b/src/alerts/alerts_utils.rs index 86324d62b..c57e4acfc 100644 --- a/src/alerts/alerts_utils.rs +++ b/src/alerts/alerts_utils.rs @@ -465,13 +465,19 @@ fn scalar_condition_expr( } _ => { let formatted = match column_type { - Some("bool") | Some("boolean") => value.to_string(), - Some("int") | Some("float") | Some("number") => value.to_string(), - Some(_) => format!("'{}'", value.replace('\'', "''")), + Some("bool") | Some("boolean") => value + .parse::() + .map_err(|_| format!("Invalid boolean literal: {value}"))? + .to_string(), + Some("int") | Some("float") | Some("number") => value + .parse::() + .map_err(|_| format!("Invalid numeric literal: {value}"))? + .to_string(), + Some(_) => format!("'{}'", value.replace("'", "''")), None => match ValueType::from_string(value.to_owned()) { ValueType::Number(val) => format!("{val}"), ValueType::Boolean(val) => format!("{val}"), - ValueType::String(val) => format!("'{}'", val.replace('\'', "''")), + ValueType::String(val) => format!("'{}'", val.replace("'", "''")), }, }; format!("{operator} {formatted}") @@ -482,6 +488,7 @@ fn scalar_condition_expr( fn escape_like(value: &str) -> String { value + .replace('\\', "\\\\") .replace('\'', "''") .replace('%', "\\%") .replace('_', "\\_")