Skip to content
Closed
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
7 changes: 6 additions & 1 deletion src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 5 additions & 2 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
196 changes: 196 additions & 0 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions tests/sqlparser_mysql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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'")
);
}
Loading