adriangb opened a new pull request, #24479:
URL: https://github.com/apache/datafusion/pull/24479

   ## Which issue does this PR close?
   
   - Closes #23692.
   
   ## Rationale for this change
   
   Combining two or more `IS NOT DISTINCT FROM` conditions with `AND` fails to 
plan:
   
   ```sql
   SELECT l.* FROM l LEFT ANTI JOIN r
     ON l.a IS NOT DISTINCT FROM r.a AND l.b IS NOT DISTINCT FROM r.b
   ```
   
   ```
   Error during planning: Cannot infer common argument type for logical boolean 
operation Int64 AND Boolean
   ```
   
   A single condition works, and adding parentheses around each condition 
works, so the failure looks like a type coercion problem. It isn't. `sqlparser` 
parses the right operand of `IS [NOT] DISTINCT FROM` with `parse_expr()` — that 
is, at the lowest possible precedence — instead of stopping at the first 
operator that binds less tightly than `IS`. Everything after the operator is 
swallowed into its right operand:
   
   ```
   l.a IS NOT DISTINCT FROM r.a AND l.b IS NOT DISTINCT FROM r.b
   
     => IsNotDistinctFrom(l.a, BinaryOp { r.a AND IsNotDistinctFrom(l.b, r.b) })
   ```
   
   The `Int64 AND Boolean` in the error is that inner `r.a AND <bool>`. 
PostgreSQL binds `AND` *less* tightly than `IS`, so the expected parse is `(l.a 
IS NOT DISTINCT FROM r.a) AND (l.b IS NOT DISTINCT FROM r.b)`.
   
   This affects every column type and every join type, and it also affects `IS 
DISTINCT FROM` and plain `WHERE` clauses. It blocks multi-column equality 
delete resolution in Apache Iceberg.
   
   The same greedy `parse_expr()` call is still present on 
`datafusion-sqlparser-rs` `main` (`src/parser/mod.rs:4074`), so the fix is 
applied on the DataFusion side.
   
   ## What changes are included in this PR?
   
   In `datafusion/sql/src/expr/mod.rs`, before planning an expression the 
planner now restores the expected associativity:
   
   1. `has_greedy_distinct_from` cheaply detects whether the mis-parse is 
present. When it isn't — the overwhelmingly common case — nothing else runs.
   2. `flatten_and_or` flattens the `AND` / `OR` spine into its first operand 
plus the remaining `(operator, operand)` pairs, re-attaching each `IS [NOT] 
DISTINCT FROM` to only the *first* operand of its right hand side.
   3. `rebuild_and_or` rebuilds the expression with `AND` binding more tightly 
than `OR`, both left associative.
   
   Prefix `NOT` is handled the same way, since it also binds more tightly than 
`AND` / `OR`. Handling `OR` and `NOT` is required for correctness rather than 
completeness: a purely local rotation gets `a IS NOT DISTINCT FROM 1 AND b IS 
NOT DISTINCT FROM 2 OR c IS NOT DISTINCT FROM 3` wrong, producing `A AND (B OR 
C)` and turning a planning error into a silently wrong result.
   
   Operands are not descended into, so a parenthesised sub-expression keeps its 
explicit grouping and is handled when the planner recurses into it. The 
rewrite's output is a fixed point — it never leaves an `IS [NOT] DISTINCT FROM` 
whose right operand is an `AND` / `OR` — so re-entry cannot loop. Both 
recursive helpers carry the crate's usual `recursive_protection` attribute.
   
   ### Known limitation left in place
   
   The postfix `IS` family at the same precedence level (`a IS NOT DISTINCT 
FROM b IS NULL`) is still associated as `sqlparser` produces it. That behaviour 
is unchanged by this PR and belongs with the broader precedence discussion in 
#22461.
   
   ## Are these changes tested?
   
   Yes.
   
   New planner tests in `datafusion/sql/tests/sql_integration.rs` covering join 
`ON` clauses, `WHERE` clauses, projections, `IS DISTINCT FROM`, `AND` / `OR` 
chains, and parenthesised forms as a control.
   
   New end-to-end cases in 
`datafusion/sqllogictest/test_files/join_is_not_distinct_from.slt`, including 
the `LEFT ANTI JOIN` from the issue, three-condition chains, and the `AND` / 
`OR` and `NOT` precedence cases. The precedence cases are chosen so that 
incorrect grouping returns a different set of rows, not just a different plan — 
the plan display alone does not distinguish `NOT (A AND B)` from `(NOT A) AND 
B`.
   
   Verification run:
   
   - `cargo test -p datafusion-sql` — 565 + 87 + 12 doctests pass
   - full sqllogictest suite — 501/501 files pass
   - `cargo test -p datafusion-optimizer -p datafusion-expr` — passes
   - `cargo clippy -p datafusion-sql -p datafusion-optimizer --all-targets -- 
-D warnings` — clean
   - `cargo fmt --all` applied
   
   ## Are there any user-facing changes?
   
   Yes, and they are the point of the PR: `IS [NOT] DISTINCT FROM` combined 
with `AND` / `OR` / `NOT` without parentheses now parses the way PostgreSQL 
parses it, so queries that previously failed to plan now succeed.
   
   Queries that were already parenthesised are unaffected. No public API 
changes.
   
   ---
   _Generated by [Claude 
Code](https://claude.ai/code/session_01FWoZ9timwHS7Az8nq3E7HS)_


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