diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index f99cbe2ea..ff83a4da6 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -990,11 +990,16 @@ pub trait Dialect: Debug + Any { Precedence::Caret => 22, Precedence::Pipe => 21, Precedence::Colon => 21, + // "any other operator" -- `->`, `@>`, custom operators. PostgreSQL + // places this row above `BETWEEN` / `LIKE` and below `+` / `-` + // (`%left Op OPERATOR RIGHT_ARROW '|'` in gram.y), so it must bind + // more tightly than `IS`, whose right operand would otherwise stop + // short of it. + 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..b7a532130 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4071,11 +4071,14 @@ 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()?; + // The right operand binds no more loosely than `IS` + // itself, so that e.g. `a IS DISTINCT FROM b AND c` + // parses as `(a IS DISTINCT FROM b) AND c`. + 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..8c7041b40 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1984,6 +1984,202 @@ 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_eq!( + BinaryOp { + left: Box::new(BinaryOp { + left: Box::new(IsDistinctFrom( + Box::new(Identifier(Ident::new("a"))), + Box::new(Expr::value(number("1"))), + )), + op: BinaryOperator::And, + right: Box::new(Identifier(Ident::new("b"))), + }), + op: BinaryOperator::Or, + right: Box::new(Identifier(Ident::new("c"))), + }, + verified_expr("a IS DISTINCT FROM 1 AND b OR c") + ); + assert_eq!( + BinaryOp { + left: Box::new(IsDistinctFrom( + 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::And, + right: Box::new(Identifier(Ident::new("c"))), + }), + }, + verified_expr("a IS DISTINCT FROM 1 OR b AND c") + ); + + // 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") + ); + + // Prefix `NOT` binds less tightly than `IS`, so it takes the whole predicate. + assert_eq!( + UnaryOp { + op: UnaryOperator::Not, + expr: Box::new(IsDistinctFrom( + Box::new(Identifier(Ident::new("a"))), + Box::new(Identifier(Ident::new("b"))), + )), + }, + verified_expr("NOT a IS DISTINCT FROM b") + ); + + // Comparison binds tighter than `IS`, so it stays in the right operand. + assert_eq!( + IsDistinctFrom( + Box::new(Identifier(Ident::new("a"))), + Box::new(BinaryOp { + left: Box::new(Identifier(Ident::new("b"))), + op: BinaryOperator::Eq, + right: Box::new(Identifier(Ident::new("c"))), + }), + ), + verified_expr("a IS DISTINCT FROM b = c") + ); + + // The operator on both sides of `AND`, so each conjunct re-enters this + // parse path. + assert_eq!( + BinaryOp { + left: Box::new(IsNotDistinctFrom( + Box::new(Identifier(Ident::new("a"))), + Box::new(Identifier(Ident::new("b"))), + )), + op: BinaryOperator::And, + right: Box::new(IsNotDistinctFrom( + Box::new(Identifier(Ident::new("c"))), + Box::new(Identifier(Ident::new("d"))), + )), + }, + verified_expr("a IS NOT DISTINCT FROM b AND c IS NOT DISTINCT FROM d") + ); +} + +#[test] +fn parse_pg_other_operator_precedence() { + // The "any other operator" row -- `->`, `@>`, custom operators -- binds more + // tightly than comparison, `LIKE`, `BETWEEN` and the `IS` family, matching + // PostgreSQL's `%left Op OPERATOR RIGHT_ARROW` placement. Dialects that + // support lambda functions consume `->` in prefix position instead. + let dialects = all_dialects_where(|d| !d.supports_lambda_functions()); + 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".to_string()))), + }), + op: BinaryOperator::Eq, + right: Box::new(Expr::Identifier(Ident::new("b"))), + }, + dialects.verified_expr("a -> 'k' = b") + ); + + // A lambda is only recognised when `->` directly follows the parameter list, + // so a qualified left operand reaches this precedence in every dialect -- + // including those that support lambdas. + assert_eq!( + Expr::BinaryOp { + left: Box::new(Expr::BinaryOp { + left: Box::new(Expr::CompoundIdentifier(vec![ + Ident::new("t"), + Ident::new("a"), + ])), + op: BinaryOperator::Arrow, + right: Box::new(Expr::value(Value::SingleQuotedString("k".to_string()))), + }), + op: BinaryOperator::Eq, + right: Box::new(Expr::Identifier(Ident::new("b"))), + }, + all_dialects().verified_expr("t.a -> 'k' = b") + ); + + // `LIKE` sits below this row too. + assert_matches!( + dialects.verified_expr("a -> 'k' LIKE 'x'"), + Expr::Like { .. } + ); +} + #[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..84e92836f 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -4946,3 +4946,36 @@ 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_and_generic().verified_expr("a IS DISTINCT FROM b -> 'k'") + ); + + assert_eq!( + Expr::IsNotDistinctFrom( + Box::new(Expr::Identifier(Ident::new("a"))), + Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("b"))), + op: BinaryOperator::LongArrow, + right: Box::new(Expr::Value( + Value::SingleQuotedString("k".into()).with_empty_span() + )), + }), + ), + mysql_and_generic().verified_expr("a IS NOT DISTINCT FROM b ->> 'k'") + ); +}