Skip to content

Commit ebee692

Browse files
zvonimir-ddclaude
andcommitted
Address review feedback on precedence fix
Document why `PgOther` sits above `Is` and why the `IS DISTINCT FROM` right operand is parsed at the caller's precedence. Tighten the loose root-only assertions into full-tree comparisons, and cover `NOT`, `=` as the right operand, and two chained `IS NOT DISTINCT FROM`. Move the `->` comparison-precedence coverage out of the MySQL suite into a cross-dialect test, and add `->>` to the MySQL arrow test. Environment: Datadog workspace Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 4c67348 commit ebee692

4 files changed

Lines changed: 123 additions & 28 deletions

File tree

src/dialect/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -990,6 +990,11 @@ pub trait Dialect: Debug + Any {
990990
Precedence::Caret => 22,
991991
Precedence::Pipe => 21,
992992
Precedence::Colon => 21,
993+
// "any other operator" -- `->`, `@>`, custom operators. PostgreSQL
994+
// places this row above `BETWEEN` / `LIKE` and below `+` / `-`
995+
// (`%left Op OPERATOR RIGHT_ARROW '|'` in gram.y), so it must bind
996+
// more tightly than `IS`, whose right operand would otherwise stop
997+
// short of it.
993998
Precedence::PgOther => 21,
994999
Precedence::Between => 20,
9951000
Precedence::Eq => 20,

src/parser/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4071,6 +4071,9 @@ impl<'a> Parser<'a> {
40714071
} else if self.parse_keywords(&[Keyword::NOT, Keyword::UNKNOWN]) {
40724072
Ok(Expr::IsNotUnknown(Box::new(expr)))
40734073
} else if self.parse_keywords(&[Keyword::DISTINCT, Keyword::FROM]) {
4074+
// The right operand binds no more loosely than `IS`
4075+
// itself, so that e.g. `a IS DISTINCT FROM b AND c`
4076+
// parses as `(a IS DISTINCT FROM b) AND c`.
40744077
let expr2 = self.parse_subexpr(precedence)?;
40754078
Ok(Expr::IsDistinctFrom(Box::new(expr), Box::new(expr2)))
40764079
} else if self.parse_keywords(&[Keyword::NOT, Keyword::DISTINCT, Keyword::FROM])

tests/sqlparser_common.rs

Lines changed: 107 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2023,19 +2023,35 @@ fn parse_is_distinct_from_precedence() {
20232023
);
20242024

20252025
// `AND` binds tighter than `OR` within the surrounding expression.
2026-
assert_matches!(
2027-
verified_expr("a IS DISTINCT FROM 1 AND b OR c"),
2026+
assert_eq!(
20282027
BinaryOp {
2028+
left: Box::new(BinaryOp {
2029+
left: Box::new(IsDistinctFrom(
2030+
Box::new(Identifier(Ident::new("a"))),
2031+
Box::new(Expr::value(number("1"))),
2032+
)),
2033+
op: BinaryOperator::And,
2034+
right: Box::new(Identifier(Ident::new("b"))),
2035+
}),
20292036
op: BinaryOperator::Or,
2030-
..
2031-
}
2037+
right: Box::new(Identifier(Ident::new("c"))),
2038+
},
2039+
verified_expr("a IS DISTINCT FROM 1 AND b OR c")
20322040
);
2033-
assert_matches!(
2034-
verified_expr("a IS DISTINCT FROM 1 OR b AND c"),
2041+
assert_eq!(
20352042
BinaryOp {
2043+
left: Box::new(IsDistinctFrom(
2044+
Box::new(Identifier(Ident::new("a"))),
2045+
Box::new(Expr::value(number("1"))),
2046+
)),
20362047
op: BinaryOperator::Or,
2037-
..
2038-
}
2048+
right: Box::new(BinaryOp {
2049+
left: Box::new(Identifier(Ident::new("b"))),
2050+
op: BinaryOperator::And,
2051+
right: Box::new(Identifier(Ident::new("c"))),
2052+
}),
2053+
},
2054+
verified_expr("a IS DISTINCT FROM 1 OR b AND c")
20392055
);
20402056

20412057
// Explicit parentheses still push the boolean expression into the right operand.
@@ -2074,6 +2090,89 @@ fn parse_is_distinct_from_precedence() {
20742090
),
20752091
verified_expr("a IS DISTINCT FROM b + 1")
20762092
);
2093+
2094+
assert_eq!(
2095+
IsDistinctFrom(
2096+
Box::new(Identifier(Ident::new("a"))),
2097+
Box::new(BinaryOp {
2098+
left: Box::new(Identifier(Ident::new("b"))),
2099+
op: BinaryOperator::Eq,
2100+
right: Box::new(Identifier(Ident::new("c"))),
2101+
}),
2102+
),
2103+
verified_expr("a IS DISTINCT FROM b = c")
2104+
);
2105+
2106+
// `NOT` binds more loosely than `IS`, so it applies to the whole comparison.
2107+
assert_eq!(
2108+
UnaryOp {
2109+
op: UnaryOperator::Not,
2110+
expr: Box::new(IsDistinctFrom(
2111+
Box::new(Identifier(Ident::new("a"))),
2112+
Box::new(Identifier(Ident::new("b"))),
2113+
)),
2114+
},
2115+
verified_expr("NOT a IS DISTINCT FROM b")
2116+
);
2117+
2118+
assert_eq!(
2119+
BinaryOp {
2120+
left: Box::new(IsNotDistinctFrom(
2121+
Box::new(Identifier(Ident::new("a"))),
2122+
Box::new(Identifier(Ident::new("b"))),
2123+
)),
2124+
op: BinaryOperator::And,
2125+
right: Box::new(IsNotDistinctFrom(
2126+
Box::new(Identifier(Ident::new("c"))),
2127+
Box::new(Identifier(Ident::new("d"))),
2128+
)),
2129+
},
2130+
verified_expr("a IS NOT DISTINCT FROM b AND c IS NOT DISTINCT FROM d")
2131+
);
2132+
}
2133+
2134+
#[test]
2135+
fn parse_pg_other_operator_precedence() {
2136+
let arrow_k = |left: Expr| Expr::BinaryOp {
2137+
left: Box::new(left),
2138+
op: BinaryOperator::Arrow,
2139+
right: Box::new(Expr::Value(
2140+
Value::SingleQuotedString("k".into()).with_empty_span(),
2141+
)),
2142+
};
2143+
let t_a = || Expr::CompoundIdentifier(vec![Ident::new("t"), Ident::new("a")]);
2144+
2145+
// `->` binds tighter than comparison operators, so the arrow expression is
2146+
// the left operand rather than the comparison being the arrow's key.
2147+
let expected_eq = |left: Expr| Expr::BinaryOp {
2148+
left: Box::new(arrow_k(left)),
2149+
op: BinaryOperator::Eq,
2150+
right: Box::new(Identifier(Ident::new("b"))),
2151+
};
2152+
2153+
// Dialects with lambda functions read a bare `a ->` as the start of a lambda.
2154+
assert_eq!(
2155+
expected_eq(Identifier(Ident::new("a"))),
2156+
all_dialects_where(|d| !d.supports_lambda_functions()).verified_expr("a -> 'k' = b")
2157+
);
2158+
assert_eq!(
2159+
expected_eq(t_a()),
2160+
all_dialects().verified_expr("t.a -> 'k' = b")
2161+
);
2162+
2163+
// `LIKE` sits below `=` in the precedence table, so cover that boundary too.
2164+
assert_eq!(
2165+
Expr::Like {
2166+
negated: false,
2167+
any: false,
2168+
expr: Box::new(arrow_k(t_a())),
2169+
pattern: Box::new(Expr::Value(
2170+
Value::SingleQuotedString("x".into()).with_empty_span(),
2171+
)),
2172+
escape_char: None,
2173+
},
2174+
all_dialects().verified_expr("t.a -> 'k' LIKE 'x'")
2175+
);
20772176
}
20782177

20792178
#[test]

tests/sqlparser_mysql.rs

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4962,32 +4962,20 @@ fn parse_is_distinct_from_json_arrow_precedence() {
49624962
)),
49634963
}),
49644964
),
4965-
mysql().verified_expr("a IS DISTINCT FROM b -> 'k'")
4965+
mysql_and_generic().verified_expr("a IS DISTINCT FROM b -> 'k'")
49664966
);
4967-
}
49684967

4969-
#[test]
4970-
fn parse_json_arrow_comparison_precedence() {
4971-
// The same "any other operator" class also binds tighter than the
4972-
// comparison operators and `LIKE`, so the JSON extraction is the left
4973-
// operand rather than swallowing the right-hand side.
49744968
assert_eq!(
4975-
Expr::BinaryOp {
4976-
left: Box::new(Expr::BinaryOp {
4977-
left: Box::new(Expr::Identifier(Ident::new("a"))),
4978-
op: BinaryOperator::Arrow,
4969+
Expr::IsNotDistinctFrom(
4970+
Box::new(Expr::Identifier(Ident::new("a"))),
4971+
Box::new(Expr::BinaryOp {
4972+
left: Box::new(Expr::Identifier(Ident::new("b"))),
4973+
op: BinaryOperator::LongArrow,
49794974
right: Box::new(Expr::Value(
49804975
Value::SingleQuotedString("k".into()).with_empty_span()
49814976
)),
49824977
}),
4983-
op: BinaryOperator::Eq,
4984-
right: Box::new(Expr::value(number("1"))),
4985-
},
4986-
mysql().verified_expr("a -> 'k' = 1")
4987-
);
4988-
4989-
assert_matches!(
4990-
mysql().verified_expr("a -> 'k' LIKE 'x'"),
4991-
Expr::Like { .. }
4978+
),
4979+
mysql_and_generic().verified_expr("a IS NOT DISTINCT FROM b ->> 'k'")
49924980
);
49934981
}

0 commit comments

Comments
 (0)