Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Comment thread
zvonimir-dd marked this conversation as resolved.
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)
Expand Down
92 changes: 92 additions & 0 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions tests/sqlparser_mysql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 { .. }
);
}