-
-
Notifications
You must be signed in to change notification settings - Fork 160
/api/v1/counts - handle invalid filter values in request payload #1528
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
base: main
Are you sure you want to change the base?
Changes from all commits
dd180c3
27f672f
32d590b
6e1e2ea
76de1d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -364,81 +364,131 @@ 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.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? | ||
| } | ||
| } | ||
|
Comment on lines
+445
to
+454
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Silently skipping empty string filters may cause unexpected behavior. When an empty string is provided with operators other than Consider returning an error instead to make the invalid filter explicit: 💡 Alternative approach _ => {
- tracing::warn!(
- "ignoring empty string filter for: {} {}",
- condition.column,
- condition.operator
- );
- continue;
- // or return an error instead?
+ return Err(format!(
+ "empty string not supported for operator [{}] on column [{}]",
+ condition.operator, condition.column
+ ));
}🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
|
|
||
| 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 | ||
| )) | ||
| } | ||
|
Comment on lines
+458
to
+466
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find the value_as_sql_str implementation
rg -n "fn value_as_sql_str" -A 30 src/alerts/Repository: parseablehq/parseable Length of output: 2190 🏁 Script executed: #!/bin/bash
# View the full context around lines 458-466 with some surrounding lines
sed -n '440,480p' src/alerts/alerts_utils.rs | cat -nRepository: parseablehq/parseable Length of output: 2725 Add explicit handling for When a request contains 🤖 Prompt for AI Agents |
||
|
|
||
| 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)) | ||
| }, | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Single quote escaping in LIKE patterns uses non-standard SQL syntax.
The
replace("'", "\\'")produces\'for single quote escaping, but standard SQL (and DataFusion) expects''to escape single quotes within string literals. TheESCAPE '\\'clause only affects LIKE wildcard characters (%,_), not string literal quoting.For a value like
test'value, this generatesLIKE '%test\'value%'which breaks SQL parsing.🐛 Proposed fix
WhereConfigOperator::Contains => { let escaped_value = value - .replace("'", "\\'") + .replace("'", "''") .replace('%', "\\%") .replace('_', "\\_"); format!("LIKE '%{escaped_value}%' ESCAPE '\\'") }Apply the same fix to all other LIKE pattern branches (DoesNotContain, ILike, BeginsWith, DoesNotBeginWith, EndsWith, DoesNotEndWith).
📝 Committable suggestion
🤖 Prompt for AI Agents