Skip to content

Commit dbd017c

Browse files
authored
[query-engine] Add special handling for invalid equality expressions in KQL comparisons (#1400)
## Changes * Add a rule in KQL pest to allow `where [scalar] = [scalar]` so that a more intentional error message can be generated hinting that `==` is the most likely solution
1 parent ecafada commit dbd017c

File tree

3 files changed

+43
-14
lines changed

3 files changed

+43
-14
lines changed

rust/experimental/query_engine/kql-parser/src/kql.pest

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ positive_infinity_token = { "+inf" }
2020
negative_infinity_token = { "-inf" }
2121

2222
// Logical Tokens
23+
invalid_equals_token = @{ "=" ~ !("="|"~") }
2324
equals_token = @{ "==" }
2425
equals_insensitive_token = @{ "=~" }
2526
not_equals_token = @{ "!=" }
@@ -250,7 +251,7 @@ scalar_arithmetic_binary_expression = _{
250251
| (plus_token|minus_token) ~ scalar_unary_expression
251252
}
252253
scalar_logical_binary_expression = _{
253-
(equals_token|equals_insensitive_token|not_equals_token|not_equals_insensitive_token|greater_than_token|greater_than_or_equal_to_token|less_than_token|less_than_or_equal_to_token) ~ scalar_unary_expression
254+
(equals_token|equals_insensitive_token|not_equals_token|not_equals_insensitive_token|greater_than_token|greater_than_or_equal_to_token|less_than_token|less_than_or_equal_to_token|invalid_equals_token) ~ scalar_unary_expression
254255
| matches_regex_token ~ scalar_unary_expression
255256
| (not_contains_cs_token|not_contains_token|not_has_cs_token|not_has_token|contains_cs_token|contains_token|has_cs_token|has_token) ~ scalar_unary_expression
256257
| (not_in_insensitive_token|not_in_token|in_insensitive_token|in_token) ~ scalar_list_expression

rust/experimental/query_engine/kql-parser/src/logical_expressions.rs

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ mod tests {
5353
&[
5454
"1 == 1",
5555
"1 =~ 1",
56+
"1 = 1", // Note: Only valid for pest parsing, should fail when translated to expressions
5657
"(1) != true",
5758
"1 !~ true",
5859
"(1==1) > false",
@@ -78,7 +79,7 @@ mod tests {
7879

7980
#[test]
8081
fn test_parse_comparison_expression() {
81-
let run_test = |input: &str, expected: LogicalExpression| {
82+
let run_test_success = |input: &str, expected: LogicalExpression| {
8283
println!("Testing: {input}");
8384

8485
let state = ParserState::new_with_options(
@@ -107,7 +108,21 @@ mod tests {
107108
assert_eq!(expected, expression);
108109
};
109110

110-
run_test(
111+
let run_test_failure = |input: &str, expected: &str| {
112+
let state = ParserState::new(input);
113+
114+
let mut result = KqlPestParser::parse(Rule::logical_expression, input).unwrap();
115+
116+
let error = parse_logical_expression(result.next().unwrap(), &state).unwrap_err();
117+
118+
if let ParserError::SyntaxError(_, msg) = error {
119+
assert_eq!(expected, msg);
120+
} else {
121+
panic!("Expected SyntaxError");
122+
}
123+
};
124+
125+
run_test_success(
111126
"variable == 'hello world'",
112127
LogicalExpression::EqualTo(EqualToLogicalExpression::new(
113128
QueryLocation::new_fake(),
@@ -123,7 +138,7 @@ mod tests {
123138
)),
124139
);
125140

126-
run_test(
141+
run_test_success(
127142
"variable =~ 'hello world'",
128143
LogicalExpression::EqualTo(EqualToLogicalExpression::new(
129144
QueryLocation::new_fake(),
@@ -139,7 +154,12 @@ mod tests {
139154
)),
140155
);
141156

142-
run_test(
157+
run_test_failure(
158+
"variable = 'hello world'",
159+
"Unexpected assignment operator. Did you mean to use '==' instead?",
160+
);
161+
162+
run_test_success(
143163
"variable !~ 'hello world'",
144164
LogicalExpression::Not(NotLogicalExpression::new(
145165
QueryLocation::new_fake(),
@@ -158,7 +178,7 @@ mod tests {
158178
)),
159179
);
160180

161-
run_test(
181+
run_test_success(
162182
"variable != true",
163183
LogicalExpression::Not(NotLogicalExpression::new(
164184
QueryLocation::new_fake(),
@@ -177,7 +197,7 @@ mod tests {
177197
)),
178198
);
179199

180-
run_test(
200+
run_test_success(
181201
"1 > source_path",
182202
LogicalExpression::GreaterThan(GreaterThanLogicalExpression::new(
183203
QueryLocation::new_fake(),
@@ -196,7 +216,7 @@ mod tests {
196216
)),
197217
);
198218

199-
run_test(
219+
run_test_success(
200220
"(1) >= resource.key",
201221
LogicalExpression::GreaterThanOrEqualTo(GreaterThanOrEqualToLogicalExpression::new(
202222
QueryLocation::new_fake(),
@@ -216,7 +236,7 @@ mod tests {
216236
)),
217237
);
218238

219-
run_test(
239+
run_test_success(
220240
"0 < (variable)",
221241
LogicalExpression::Not(NotLogicalExpression::new(
222242
QueryLocation::new_fake(),
@@ -236,7 +256,7 @@ mod tests {
236256
)),
237257
);
238258

239-
run_test(
259+
run_test_success(
240260
"(0) <= variable",
241261
LogicalExpression::Not(NotLogicalExpression::new(
242262
QueryLocation::new_fake(),
@@ -255,7 +275,7 @@ mod tests {
255275
);
256276

257277
// Note: This whole expression folds to a constant value.
258-
run_test(
278+
run_test_success(
259279
"'hello world' matches regex '^hello world$'",
260280
LogicalExpression::Scalar(ScalarExpression::Static(StaticScalarExpression::Boolean(
261281
BooleanScalarExpression::new(QueryLocation::new_fake(), true),
@@ -264,7 +284,7 @@ mod tests {
264284

265285
// Note: The string regex pattern gets folded into a compiled regex
266286
// expression.
267-
run_test(
287+
run_test_success(
268288
"Name matches regex '^hello world$'",
269289
LogicalExpression::Matches(MatchesLogicalExpression::new(
270290
QueryLocation::new_fake(),
@@ -286,7 +306,7 @@ mod tests {
286306
)),
287307
);
288308

289-
run_test(
309+
run_test_success(
290310
"Name matches regex const_regex",
291311
LogicalExpression::Matches(MatchesLogicalExpression::new(
292312
QueryLocation::new_fake(),

rust/experimental/query_engine/kql-parser/src/scalar_expression.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ static PRATT_PARSER: LazyLock<PrattParser<Rule>> = LazyLock::new(|| {
2929
.op(Op::infix(equals_token, Left)
3030
| Op::infix(equals_insensitive_token, Left)
3131
| Op::infix(not_equals_token, Left)
32-
| Op::infix(not_equals_insensitive_token, Left))
32+
| Op::infix(not_equals_insensitive_token, Left)
33+
| Op::infix(invalid_equals_token, Left))
3334
// <= >= < >
3435
.op(Op::infix(less_than_or_equal_to_token, Left)
3536
| Op::infix(greater_than_or_equal_to_token, Left)
@@ -127,6 +128,13 @@ pub(crate) fn parse_scalar_expression(
127128
.into(),
128129
),
129130

131+
Rule::invalid_equals_token => {
132+
return Err(ParserError::SyntaxError(
133+
location,
134+
"Unexpected assignment operator. Did you mean to use '==' instead?".into(),
135+
));
136+
}
137+
130138
Rule::greater_than_token => ScalarExpression::Logical(
131139
LogicalExpression::GreaterThan(GreaterThanLogicalExpression::new(
132140
location, lhs, rhs,

0 commit comments

Comments
 (0)