diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index f99cbe2ea..43b343cbd 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -990,11 +990,11 @@ pub trait Dialect: Debug + Any { Precedence::Caret => 22, Precedence::Pipe => 21, Precedence::Colon => 21, + Precedence::PgOther => 21, Precedence::Between => 20, Precedence::Eq => 20, Precedence::Like => 19, Precedence::Is => 17, - Precedence::PgOther => 16, Precedence::UnaryNot => 15, Precedence::And => 10, Precedence::Or => 5, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b2b3f42bb..d4b31bbc8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4071,11 +4071,11 @@ impl<'a> Parser<'a> { } else if self.parse_keywords(&[Keyword::NOT, Keyword::UNKNOWN]) { Ok(Expr::IsNotUnknown(Box::new(expr))) } else if self.parse_keywords(&[Keyword::DISTINCT, Keyword::FROM]) { - let expr2 = self.parse_expr()?; + let expr2 = self.parse_subexpr(precedence)?; Ok(Expr::IsDistinctFrom(Box::new(expr), Box::new(expr2))) } else if self.parse_keywords(&[Keyword::NOT, Keyword::DISTINCT, Keyword::FROM]) { - let expr2 = self.parse_expr()?; + let expr2 = self.parse_subexpr(precedence)?; Ok(Expr::IsNotDistinctFrom(Box::new(expr), Box::new(expr2))) } else if self.parse_keyword(Keyword::JSON) { self.parse_is_json_predicate(expr, false) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 0800bc41f..71048b4f9 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1984,6 +1984,98 @@ fn parse_is_not_distinct_from() { ); } +#[test] +fn parse_is_distinct_from_precedence() { + use self::Expr::*; + + // The right operand of `IS [NOT] DISTINCT FROM` binds tighter than `AND`/`OR`, + // so the boolean operator must end up at the root of the tree. + assert_eq!( + BinaryOp { + left: Box::new(IsDistinctFrom( + Box::new(Identifier(Ident::new("a"))), + Box::new(Expr::value(number("1"))), + )), + op: BinaryOperator::And, + right: Box::new(BinaryOp { + left: Box::new(Identifier(Ident::new("b"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::value(number("2"))), + }), + }, + verified_expr("a IS DISTINCT FROM 1 AND b = 2") + ); + + assert_eq!( + BinaryOp { + left: Box::new(IsNotDistinctFrom( + Box::new(Identifier(Ident::new("a"))), + Box::new(Expr::value(number("1"))), + )), + op: BinaryOperator::Or, + right: Box::new(BinaryOp { + left: Box::new(Identifier(Ident::new("b"))), + op: BinaryOperator::Eq, + right: Box::new(Expr::value(number("2"))), + }), + }, + verified_expr("a IS NOT DISTINCT FROM 1 OR b = 2") + ); + + // `AND` binds tighter than `OR` within the surrounding expression. + assert_matches!( + verified_expr("a IS DISTINCT FROM 1 AND b OR c"), + BinaryOp { + op: BinaryOperator::Or, + .. + } + ); + assert_matches!( + verified_expr("a IS DISTINCT FROM 1 OR b AND c"), + BinaryOp { + op: BinaryOperator::Or, + .. + } + ); + + // Explicit parentheses still push the boolean expression into the right operand. + assert_eq!( + IsDistinctFrom( + Box::new(Identifier(Ident::new("a"))), + Box::new(Nested(Box::new(BinaryOp { + left: Box::new(Expr::value(number("1"))), + op: BinaryOperator::And, + right: Box::new(Identifier(Ident::new("b"))), + }))), + ), + verified_expr("a IS DISTINCT FROM (1 AND b)") + ); + + // sqlparser resolves the IS family left-associatively, consistent with how + // `a IS NULL IS NULL` already parses. Deliberately more permissive than + // PostgreSQL, which declares IS as %nonassoc and rejects the chain. + assert_eq!( + IsNull(Box::new(IsDistinctFrom( + Box::new(Identifier(Ident::new("a"))), + Box::new(Identifier(Ident::new("b"))), + ))), + verified_expr("a IS DISTINCT FROM b IS NULL") + ); + + // Operators that bind tighter than `IS` are still part of the right operand. + assert_eq!( + IsDistinctFrom( + Box::new(Identifier(Ident::new("a"))), + Box::new(BinaryOp { + left: Box::new(Identifier(Ident::new("b"))), + op: BinaryOperator::Plus, + right: Box::new(Expr::value(number("1"))), + }), + ), + verified_expr("a IS DISTINCT FROM b + 1") + ); +} + #[test] fn parse_not_precedence() { // NOT has higher precedence than OR/AND, so the following must parse as (NOT true) OR true diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 797a12551..e351a590d 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -4946,3 +4946,48 @@ fn parse_adjacent_string_literal_concatenation() { fn parse_group_by_with_rollup() { mysql().verified_stmt("SELECT * FROM tbl GROUP BY col1, col2 WITH ROLLUP"); } + +#[test] +fn parse_is_distinct_from_json_arrow_precedence() { + // MySQL's `->` binds tighter than `IS [NOT] DISTINCT FROM`, so the JSON + // extraction must stay inside the right operand. + assert_eq!( + Expr::IsDistinctFrom( + Box::new(Expr::Identifier(Ident::new("a"))), + Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("b"))), + op: BinaryOperator::Arrow, + right: Box::new(Expr::Value( + Value::SingleQuotedString("k".into()).with_empty_span() + )), + }), + ), + mysql().verified_expr("a IS DISTINCT FROM b -> 'k'") + ); +} + +#[test] +fn parse_json_arrow_comparison_precedence() { + // The same "any other operator" class also binds tighter than the + // comparison operators and `LIKE`, so the JSON extraction is the left + // operand rather than swallowing the right-hand side. + assert_eq!( + Expr::BinaryOp { + left: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("a"))), + op: BinaryOperator::Arrow, + right: Box::new(Expr::Value( + Value::SingleQuotedString("k".into()).with_empty_span() + )), + }), + op: BinaryOperator::Eq, + right: Box::new(Expr::value(number("1"))), + }, + mysql().verified_expr("a -> 'k' = 1") + ); + + assert_matches!( + mysql().verified_expr("a -> 'k' LIKE 'x'"), + Expr::Like { .. } + ); +}