Skip to content

fix: filter with LIKE operators #1357

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 66 additions & 21 deletions src/alerts/alerts_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,33 +347,78 @@ pub fn get_filter_string(where_clause: &Conditions) -> Result<String, String> {
LogicalOperator::And => {
let mut exprs = vec![];
for condition in &where_clause.condition_config {
if condition
.value
.as_ref()
.is_some_and(|v| !v.trim().is_empty())
{
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 = NumberOrString::from_string(
condition.value.as_ref().unwrap().to_owned(),
);
match value {
NumberOrString::Number(val) => exprs.push(format!(
"\"{}\" {} {}",
condition.column, condition.operator, val
)),
NumberOrString::String(val) => exprs.push(format!(
"\"{}\" {} '{}'",
condition.column,
condition.operator,
val.replace('\'', "''")
)),
}

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 NumberOrString::from_string(value.to_owned()) {
NumberOrString::Number(val) => format!("{val}"),
NumberOrString::String(val) => {
format!("'{}'", val)
}
};
format!("{} {}", condition.operator, value)
}
};
exprs.push(format!("\"{}\" {}", condition.column, operator_and_value))
} else {
exprs.push(format!("\"{}\" {}", condition.column, condition.operator))
}
Expand All @@ -393,7 +438,7 @@ fn match_alert_operator(expr: &ConditionConfig) -> Expr {
// the form accepts value as a string
// if it can be parsed as a number, then parse it
// else keep it as a string
if expr.value.as_ref().is_some_and(|v| !v.trim().is_empty()) {
if expr.value.as_ref().is_some_and(|v| !v.is_empty()) {
let value = expr.value.as_ref().unwrap();
let value = NumberOrString::from_string(value.clone());

Expand Down
13 changes: 4 additions & 9 deletions src/alerts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ 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.trim().is_empty()) {
let expr1_msg = if expr1.value.as_ref().is_some_and(|v| !v.is_empty()) {
format!(
"{} {} {}",
expr1.column,
Expand All @@ -354,7 +354,7 @@ impl Conditions {
format!("{} {}", expr1.column, expr1.operator)
};

let expr2_msg = if expr2.value.as_ref().is_some_and(|v| !v.trim().is_empty()) {
let expr2_msg = if expr2.value.as_ref().is_some_and(|v| !v.is_empty()) {
format!(
"{} {} {}",
expr2.column,
Expand Down Expand Up @@ -671,18 +671,13 @@ impl AlertConfig {
WhereConfigOperator::IsNull | WhereConfigOperator::IsNotNull
);

if needs_no_value
&& condition
.value
.as_ref()
.is_some_and(|v| !v.trim().is_empty())
{
if needs_no_value && condition.value.as_ref().is_some_and(|v| !v.is_empty()) {
return Err(AlertError::CustomError(
"value must be null when operator is either `is null` or `is not null`"
.into(),
));
}
if !needs_no_value && condition.value.as_ref().is_none_or(|v| v.trim().is_empty()) {
if !needs_no_value && condition.value.as_ref().is_none_or(|v| v.is_empty()) {
return Err(AlertError::CustomError(
"value must not be null when operator is neither `is null` nor `is not null`"
.into(),
Expand Down
2 changes: 1 addition & 1 deletion src/handlers/http/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ pub mod cluster;
pub mod correlation;
pub mod health_check;
pub mod ingest;
pub mod resource_check;
mod kinesis;
pub mod llm;
pub mod logstream;
Expand All @@ -47,6 +46,7 @@ pub mod prism_home;
pub mod prism_logstream;
pub mod query;
pub mod rbac;
pub mod resource_check;
pub mod role;
pub mod users;
pub const MAX_EVENT_PAYLOAD_SIZE: usize = 10485760;
Expand Down
20 changes: 10 additions & 10 deletions src/handlers/http/modal/ingest_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
use std::sync::Arc;
use std::thread;

use actix_web::middleware::from_fn;
use actix_web::web;
use actix_web::Scope;
use actix_web::middleware::from_fn;
use actix_web_prometheus::PrometheusMetrics;
use async_trait::async_trait;
use base64::Engine;
Expand Down Expand Up @@ -68,10 +68,9 @@ impl ParseableServer for IngestServer {
.service(
// Base path "{url}/api/v1"
web::scope(&base_path())
.service(
Server::get_ingest_factory()
.wrap(from_fn(resource_check::check_resource_utilization_middleware))
)
.service(Server::get_ingest_factory().wrap(from_fn(
resource_check::check_resource_utilization_middleware,
)))
.service(Self::logstream_api())
.service(Server::get_about_factory())
.service(Self::analytics_factory())
Expand All @@ -81,10 +80,9 @@ impl ParseableServer for IngestServer {
.service(Server::get_metrics_webscope())
.service(Server::get_readiness_factory()),
)
.service(
Server::get_ingest_otel_factory()
.wrap(from_fn(resource_check::check_resource_utilization_middleware))
);
.service(Server::get_ingest_otel_factory().wrap(from_fn(
resource_check::check_resource_utilization_middleware,
)));
}

async fn load_metadata(&self) -> anyhow::Result<Option<Bytes>> {
Expand Down Expand Up @@ -231,7 +229,9 @@ impl IngestServer {
.to(ingest::post_event)
.authorize_for_stream(Action::Ingest),
)
.wrap(from_fn(resource_check::check_resource_utilization_middleware)),
.wrap(from_fn(
resource_check::check_resource_utilization_middleware,
)),
)
.service(
web::resource("/sync")
Expand Down
14 changes: 6 additions & 8 deletions src/handlers/http/modal/query_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,9 @@ impl ParseableServer for QueryServer {
.service(
web::scope(&base_path())
.service(Server::get_correlation_webscope())
.service(
Server::get_query_factory()
.wrap(from_fn(resource_check::check_resource_utilization_middleware))
)
.service(Server::get_query_factory().wrap(from_fn(
resource_check::check_resource_utilization_middleware,
)))
.service(Server::get_liveness_factory())
.service(Server::get_readiness_factory())
.service(Server::get_about_factory())
Expand All @@ -70,10 +69,9 @@ impl ParseableServer for QueryServer {
.service(Server::get_oauth_webscope(oidc_client))
.service(Self::get_user_role_webscope())
.service(Server::get_roles_webscope())
.service(
Server::get_counts_webscope()
.wrap(from_fn(resource_check::check_resource_utilization_middleware))
)
.service(Server::get_counts_webscope().wrap(from_fn(
resource_check::check_resource_utilization_middleware,
)))
.service(Server::get_metrics_webscope())
.service(Server::get_alerts_webscope())
.service(Self::get_cluster_web_scope()),
Expand Down
40 changes: 19 additions & 21 deletions src/handlers/http/modal/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ use crate::handlers::http::alerts;
use crate::handlers::http::base_path;
use crate::handlers::http::health_check;
use crate::handlers::http::prism_base_path;
use crate::handlers::http::resource_check;
use crate::handlers::http::query;
use crate::handlers::http::resource_check;
use crate::handlers::http::users::dashboards;
use crate::handlers::http::users::filters;
use crate::hottier::HotTierManager;
Expand All @@ -36,9 +36,9 @@ use crate::storage;
use crate::sync;
use crate::sync::sync_start;

use actix_web::middleware::from_fn;
use actix_web::web;
use actix_web::web::resource;
use actix_web::middleware::from_fn;
use actix_web::Resource;
use actix_web::Scope;
use actix_web_prometheus::PrometheusMetrics;
Expand Down Expand Up @@ -73,14 +73,12 @@ impl ParseableServer for Server {
.service(
web::scope(&base_path())
.service(Self::get_correlation_webscope())
.service(
Self::get_query_factory()
.wrap(from_fn(resource_check::check_resource_utilization_middleware))
)
.service(
Self::get_ingest_factory()
.wrap(from_fn(resource_check::check_resource_utilization_middleware))
)
.service(Self::get_query_factory().wrap(from_fn(
resource_check::check_resource_utilization_middleware,
)))
.service(Self::get_ingest_factory().wrap(from_fn(
resource_check::check_resource_utilization_middleware,
)))
.service(Self::get_liveness_factory())
.service(Self::get_readiness_factory())
.service(Self::get_about_factory())
Expand All @@ -93,10 +91,9 @@ impl ParseableServer for Server {
.service(Self::get_oauth_webscope(oidc_client))
.service(Self::get_user_role_webscope())
.service(Self::get_roles_webscope())
.service(
Self::get_counts_webscope()
.wrap(from_fn(resource_check::check_resource_utilization_middleware))
)
.service(Self::get_counts_webscope().wrap(from_fn(
resource_check::check_resource_utilization_middleware,
)))
.service(Self::get_alerts_webscope())
.service(Self::get_metrics_webscope()),
)
Expand All @@ -106,10 +103,9 @@ impl ParseableServer for Server {
.service(Server::get_prism_logstream())
.service(Server::get_prism_datasets()),
)
.service(
Self::get_ingest_otel_factory()
.wrap(from_fn(resource_check::check_resource_utilization_middleware))
)
.service(Self::get_ingest_otel_factory().wrap(from_fn(
resource_check::check_resource_utilization_middleware,
)))
.service(Self::get_generated());
}

Expand Down Expand Up @@ -367,14 +363,16 @@ impl Server {
.route(
web::put()
.to(logstream::put_stream)
.authorize_for_stream(Action::CreateStream)
.authorize_for_stream(Action::CreateStream),
)
// POST "/logstream/{logstream}" ==> Post logs to given log stream
.route(
web::post()
.to(ingest::post_event)
.authorize_for_stream(Action::Ingest)
.wrap(from_fn(resource_check::check_resource_utilization_middleware)),
.wrap(from_fn(
resource_check::check_resource_utilization_middleware,
)),
)
// DELETE "/logstream/{logstream}" ==> Delete log stream
.route(
Expand All @@ -383,7 +381,7 @@ impl Server {
.authorize_for_stream(Action::DeleteStream),
)
.app_data(web::JsonConfig::default().limit(MAX_EVENT_PAYLOAD_SIZE)),
)
)
.service(
// GET "/logstream/{logstream}/info" ==> Get info for given log stream
web::resource("/info").route(
Expand Down
Loading
Loading