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

   ## Which issue does this PR close?
   
   - Closes #19264.
   
   ## Rationale for this change
   
   `physical_expr::analyze` is a public API. As reported in #19264, an external
   user calling `analyze()` to infer bounds for pushdown into another library
   receives `None` (infeasible) for the column interval when the expression is
   `NOT (a = 0.0)` and the input domain of `a` contains `0.0` but is not equal
   to it (e.g. `a ∈ [-1, 1]`).
   
   The `ExprBoundaries::interval` doc states that `None` means "evaluating the
   given column results in an empty set" — i.e. the predicate is always false
   over the input interval. But `NOT (a = 0.0)` is true for every `a ≠ 0`, so
   over `[-1, 1]` the predicate is satisfiable and the correct result is the
   full input domain `[-1, 1]`. The current `None` is a contract violation.
   
   This revives the fix from #20138, which was closed as stale after review. It
   preserves the reviewed identical-singleton infeasibility case (identified by
   @berkaysynnada and @pepijnve during #20138 review) and adds direct unit
   coverage that isolates the ordinary identical-singleton case. The `[0,0]`
   `analyze()` test cannot reach that arm because `update_ranges` 
short-circuits;
   the nested signed-zero `analyze()` test does reach it through the 
pre-existing
   interval/runtime equality divergence (see "Are these changes tested?" below).
   
   Scope: the `FilterExec` statistics path does not currently reach `analyze()`
   for a predicate containing a `NotExpr`. `check_support` 
(`intervals/utils.rs`)
   accepts only `BinaryExpr`, `Column`, `Literal`, `CastExpr` and 
`NegativeExpr`,
   so such a predicate falls through to `false` and 
`FilterExec::statistics_helper`
   takes its default-selectivity branch instead. This PR is about the public
   `analyze()` contract, which #19264 reports against directly.
   
   ## What changes are included in this PR?
   
   - `datafusion/physical-expr/src/intervals/cp_solver.rs`:
     - `propagate_comparison` for `Operator::Eq` with `parent == 
Interval::FALSE`:
       the arm previously returned `Ok(None)` for all cases, which the caller
       (`ExprIntervalGraph::propagate_constraints`) interprets as infeasible.
       The correct semantics is that `a = b` being certainly false means `a != 
b`,
       which excludes at most a single point from each operand's interval.
       A single interval cannot represent that excluded point, so returning the
       children unchanged is a safe over-approximation — except when equality is
       provably true for the two singleton operands under the comparison
       semantics, in which case `a = b` is certainly true and `NOT(a = b)` is
       genuinely infeasible.
     - Added `singleton_values_equal` helper: compares two singleton
       `ScalarValue`s using SQL comparison semantics. `ScalarValue::PartialEq`
       is bit-wise for floats (`to_bits`), under which `-0.0 != +0.0`; but
       SQL/IEEE-754 comparison treats them as equal. The helper uses
       `normalize_float_zero_scalar` (the same normalization DataFusion applies
       in `physical-expr-common/src/datum.rs` for runtime comparison) before
       structural equality, so the guard correctly identifies `[-0.0,-0.0]` and
       `[+0.0,+0.0]` as certainly-equal singletons.
     - Added 4 direct unit tests that call `propagate_comparison` directly,
       forcing the `Eq + FALSE` branch:
       - `test_propagate_eq_false_identical_singletons`: `[0,0]` vs `[0,0]` →
         `None` (nearest-invalid boundary).
       - `test_propagate_eq_false_signed_zero_singletons`: `[-0.0,-0.0]` vs
         `[+0.0,+0.0]` → `None` (Float32 and Float64). This case is reachable
         in nested boolean contexts (see E2E test below).
       - `test_propagate_eq_false_distinct_singletons`: `[0,0]` vs `[1,1]` →
         `Some` (nearest-valid boundary).
       - `test_propagate_eq_false_overlapping_intervals`: `[-1,1]` vs `[0,0]` →
         `Some` (the #19264 case), `[-1,1]` vs `[0,2]` → `Some`.
   - `datafusion/physical-expr/src/analysis.rs`:
     - Added 3 E2E regression tests via `analyze()`:
       `test_analyze_not_eq_around_zero` (input `[-1, 1]`, expects `Some([-1, 
1])`),
       `test_analyze_not_eq_clamped_at_zero` (input `[0, 0]`, expects `None`),
       and `test_analyze_not_eq_nested_signed_zero_infeasible` (nested
       `NOT(a = +0.0) AND b` with `a ∈ [-0.0,-0.0]`, `b ∈ [false,true]` →
       expects infeasible). The nested test proves that the case-(1)
       short-circuit in `update_ranges` does not protect the production arm:
       the bottom-up pass evaluates `a = +0.0` over `[-0.0,-0.0]` as `FALSE`
       (because `Interval::equal` uses structural equality), so `NOT` becomes
       `TRUE`, the root becomes `TRUE_OR_FALSE`, and top-down propagation
       forces `a = +0.0` to `FALSE`, reaching
       `propagate_comparison(Eq, FALSE, [-0.0,-0.0], [+0.0,+0.0])`.
   
   The guarded production logic:
   
   ```rust
   Operator::Eq => {
       if !left_child.is_unbounded()
           && !right_child.is_unbounded()
           && left_child.lower() == left_child.upper()
           && right_child.lower() == right_child.upper()
           && singleton_values_equal(left_child.lower(), right_child.lower())
       {
           Ok(None)
       } else {
           Ok(Some((left_child.clone(), right_child.clone())))
       }
   }
   ```
   
   ## Are these changes tested?
   
   Yes, at two levels.
   
   `analysis.rs` covers the user-visible `analyze()` behavior. The `[-1, 1]` 
case
   from the issue does reach the fixed branch: its bounds evaluate to
   `TRUE_OR_FALSE`, so `ExprIntervalGraph::update_ranges` runs propagation and
   `NotExpr` hands `Eq` a `FALSE` parent. Its `[0, 0]` counterpart does not —
   those bounds evaluate to `FALSE`, so `update_ranges` short-circuits to
   `Infeasible` before `propagate_constraints` is called.
   
   `cp_solver.rs` therefore adds unit tests calling `propagate_comparison`
   directly. These isolate the ordinary identical-singleton guard that the
   `[0,0]` `analyze()` test cannot reach, while the nested signed-zero E2E test
   separately proves that the production arm is reachable from `analyze()`.
   
   Negative control, run on the pinned code baseline (`6eaca8bfe`) with the
   production arm temporarily reverted to `Ok(None)`. The two modified files'
   pre-patch production blobs are identical to the final parent (`19f2e85fa`);
   intervening upstream changes do not touch them. Each test is split so that 
its
   baseline verdict is unambiguous — 3 of the 7 fail:
   
   | test | on baseline | role |
   | --- | --- | --- |
   | `test_analyze_not_eq_around_zero` | **FAILS** — returns `None`, expected 
`[-1, 1]` | regression evidence (#19264) |
   | `test_analyze_not_eq_nested_signed_zero_infeasible` | passes 
(accidentally) | nested reachability + signed-zero guard |
   | `test_propagate_eq_false_overlapping_intervals` | **FAILS** — `[-1,1]` vs 
`[0,0]` returns `None`, expected `Some` | regression evidence |
   | `test_propagate_eq_false_distinct_singletons` | **FAILS** — `[0,0]` vs 
`[1,1]` returns `None`, expected `Some` | regression evidence |
   | `test_propagate_eq_false_signed_zero_singletons` | passes (accidentally) | 
signed-zero guard |
   | `test_propagate_eq_false_identical_singletons` | passes | boundary guard |
   | `test_analyze_not_eq_clamped_at_zero` | passes | boundary guard |
   
   All 7 pass on this branch.
   
   `test_analyze_not_eq_around_zero` failing on baseline is the direct evidence
   that this PR fixes the reported behavior.
   
   The four tests that pass on baseline are boundary guards, not evidence: the
   baseline returns `None` for every `Eq + FALSE` input, so it agrees with them
   by accident rather than by identifying the singleton-equal case. They are
   included to pin the cases that must stay infeasible. The mutation control
   below is what gives the two signed-zero guards their teeth.
   
   `test_analyze_not_eq_nested_signed_zero_infeasible` deserves special note:
   it passes on baseline (which reports `None` for all `Eq + FALSE`), but a
   guard using structural `ScalarValue::PartialEq` instead of
   `singleton_values_equal` would make it **fail** — the nested `AND` defeats
   the case-(1) short-circuit in `update_ranges`, so propagation reaches the
   `Eq + FALSE` arm with `[-0.0,-0.0]` vs `[+0.0,+0.0]`, and structural equality
   treats them as distinct, producing a false feasible result. This was verified
   by temporarily replacing `singleton_values_equal` with `==` and confirming
   both this test and `test_propagate_eq_false_signed_zero_singletons` fail.
   
   Note on signed zero: `Interval::equal`/`Interval::intersect` use structural
   `ScalarValue::PartialEq`, which is bit-wise for floats (`to_bits`), so
   `[-0.0,-0.0]` and `[+0.0,+0.0]` are distinct at the interval-algebra level
   even though SQL comparison treats them as equal. This divergence is
   pre-existing and not fixed by this PR. The guard added here uses
   `singleton_values_equal` (which normalizes signed zero via
   `normalize_float_zero_scalar`, the same function used in
   `physical-expr-common/src/datum.rs` for runtime comparison) so that the
   `Eq + FALSE` infeasibility check is consistent with the `Eq` operator's
   actual comparison semantics. The standalone `NOT(a = 0.0)` over
   `a ∈ [-0.0,-0.0]` case remains unchanged before and after this PR (the
   bottom-up pass short-circuits before reaching this arm); only the nested
   case, where top-down propagation forces `Eq` to `FALSE`, is affected.
   
   Full local verification:
   
   - `cargo fmt --all -- --check`: clean
   - `cargo clippy --all-targets --all-features -- -D warnings`: clean
   - `./dev/rust_lint.sh`: exit 0
   - extended workspace test suite required by `AGENTS.md` (with `ulimit -n 
8192`): pass
   - `cargo test -p datafusion-physical-expr --lib`: 1602 passed, 0 failed, 2 
ignored
   - `cargo test -p datafusion-physical-optimizer --lib`: 33 passed, 0 failed
   - `cargo test -p datafusion-physical-plan --lib pruning`: 120 passed, 0 
failed
   - `cargo test -p datafusion-sqllogictest --test sqllogictests -- 
simplify_predicates`: pass
   
   All listed lint and test gates, including the extended workspace suite, were
   rerun on SHA `03e6d0ad0`. A subsequent upstream rebase added only the dfbench
   statistics command and changed neither modified production blob; fmt and the
   full physical-expr suite were rerun on final SHA `04acb8d40`.
   
   ## Are there any user-facing changes?
   
   No public API changes. Behavior fix: `analyze()` no longer reports `None`
   (infeasible) for a satisfiable `NOT(a = b)` predicate over an interval that
   contains `b` but is not equal to it. The only case that remains infeasible
   under `Eq + FALSE` is when both children are singletons equal under the
   comparison semantics (including `-0.0` and `+0.0`).
   


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