adriangb commented on code in PR #2436:
URL: 
https://github.com/apache/datafusion-sqlparser-rs/pull/2436#discussion_r3810256442


##########
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,

Review Comment:
   Worth a comment here, since this line is the one that changes behaviour 
beyond `IS [NOT] DISTINCT FROM` and its placement is not self-evident:
   
   ````suggestion
               // "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,
   ````



##########
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'")
+    );

Review Comment:
   The `PgOther` change is in the **default** `prec_value`, so it applies to 
every dialect that does not override it, not just MySQL. Testing only through 
`mysql()` under-covers the hunk. `mysql_and_generic()` is the cheap widening 
here, and `->>` is worth adding since `LongArrow` rides the same row:
   
   ````suggestion
           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'")
       );
   ````



##########
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)?;

Review Comment:
   Non-blocking, but a one-line note here would save the next reader from 
re-deriving why this is not `parse_expr()`:
   
   ````suggestion
                           // 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)?;
   ````



##########
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,
+            ..
+        }
+    );

Review Comment:
   These do catch the bug this PR fixes (the unfixed parser roots these at 
`IsDistinctFrom`, not `BinaryOp`). But matching only on the root operator 
leaves the left subtree unpinned, so a future partial regression that produced 
`IsDistinctFrom(a, 1 AND b) OR c` would still be rooted at `Or` and pass. Since 
`AND`-binds-tighter-than-`OR` is the specific property being claimed, it is 
worth asserting the whole tree:
   
   ````suggestion
       // `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")
       );
   ````



##########
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")
+    );

Review Comment:
   Three gaps I would add here. All three pass on this branch as written.
   
   **Prefix `NOT`.** `UnaryNot` (15) is below `Is` (17), so `NOT` takes the 
whole predicate. This is untested, and it is the case where a wrong grouping is 
most dangerous: `NOT (A AND B)` vs `(NOT A) AND B` are silently different 
results rather than a type error.
   
   **Comparison inside the right operand.** `Eq` (20) is above `Is` (17), the 
opposite side of the boundary from `AND`/`OR`. The `b + 1` case covers 
arithmetic; this covers the row that is only three points away and therefore 
the more likely one to get wrong.
   
   **`IS ... DISTINCT FROM` on both sides of `AND`.** The existing cases use `b 
= 2` as the right conjunct. The shape that actually broke in the wild 
(apache/datafusion#23692, multi-column equality-delete resolution in Iceberg) 
has the operator on both sides, which exercises re-entry into the same parse 
path.
   
   ````suggestion
           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 shape reported in apache/datafusion#23692: 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")
       );
   ````



##########
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 { .. }
+    );
+}

Review Comment:
   Since `parse_json_arrow_comparison_precedence` is really testing the default 
precedence table rather than anything MySQL-specific, it may belong in 
`tests/sqlparser_common.rs` run across dialects. Something like this covers all 
11 non-lambda dialects in `all_dialects()` in one assertion:
   
   ```rust
   #[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")
       );
   }
   ```
   
   That second assertion is the one I would most want in the suite: it is the 
only arrow coverage that reaches DuckDB, ClickHouse, Databricks and Snowflake, 
which this hunk *does* affect whenever the left operand is not a bare 
identifier. (Note `SparkSqlDialect` is not in `all_dialects()` at all, so 
nothing here reaches it either way.)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to