winding-lines opened a new issue, #3118:
URL: https://github.com/apache/iceberg-rust/issues/3118

   ### Apache Iceberg Rust version
   
   v0.10.1, and `main` at d6d06f3c4a60e5f3a6d3d789a43513a919d9eacb
   
   ### Describe the bug
   
   `InclusiveMetricsEvaluator::in()` and `ManifestEvaluator::in()` test the 
lower bound and the upper bound against the *full* literal set independently, 
instead of narrowing the literal set as Java and PyIceberg do. As a result, an 
`In` predicate whose literals straddle the bounds — none of them actually 
inside `[lower, upper]` — is not pruned.
   
   For a file (or manifest) with bounds `[30, 79]` and `id IN (5, 104)`:
   
   * lower bound check: "is any literal `>= 30`?" — yes, `104` — so keep going;
   * upper bound check: "is any literal `<= 79`?" — yes, `5` — so keep going;
   * result: `ROWS_MIGHT_MATCH`.
   
   Java narrows instead: after the lower-bound filter the set is `{104}`, and 
the upper-bound filter empties it, so it returns `ROWS_CANNOT_MATCH`. 
`InclusiveMetricsEvaluator#in` in `iceberg-core` and 
`_InclusiveMetricsEvaluator.visit_in` in PyIceberg both do this.
   
   Interestingly `StrictMetricsEvaluator::not_in` in this repo already does it 
the right way, with `filtered_literals.retain(...)` — so the two `in` 
implementations are inconsistent with the rest of the crate.
   
   This is a pruning-effectiveness bug, not a correctness bug: the plan is a 
superset, so results are still right, but data files and whole manifests that 
cannot contain a match are opened and read. I hit it comparing scan plans on a 
`bucket[4]`-partitioned table — for `id IN (1, 4, 7)` iceberg-rust planned 5 
data files where PyIceberg planned 3; the two extra files hold `{3}` and `{5}`, 
which land in the same bucket as one of the wanted ids and so survive partition 
filtering, and then survive the metrics filter too.
   
   ### To Reproduce
   
   Two failing unit tests, using the fixtures already in each test module (`id` 
bounds are `[30, 79]`):
   
   ```rust
   // crates/iceberg/src/expr/visitors/inclusive_metrics_evaluator.rs
   #[test]
   fn test_integer_in_straddling_bounds() {
       let result = InclusiveMetricsEvaluator::eval(
           &r#in_int("id", &[INT_MIN_VALUE - 25, INT_MAX_VALUE + 25]),
           &get_test_file_1(),
           true,
       )
       .unwrap();
       assert!(!result, "Should skip: id in (5, 104), bounds are [30, 79]");
   }
   ```
   
   ```rust
   // crates/iceberg/src/expr/visitors/manifest_evaluator.rs
   #[test]
   fn test_in_straddling_bounds() -> Result<()> {
       let case_sensitive = true;
       let schema = create_schema()?;
       let manifest_file = create_manifest_file(create_partitions());
   
       let filter = Predicate::Set(SetExpression::new(
           PredicateOperator::In,
           Reference::new("id"),
           FnvHashSet::from_iter(vec![
               Datum::int(INT_MIN_VALUE - 25),
               Datum::int(INT_MAX_VALUE + 25),
           ]),
       ))
       .bind(schema.clone(), case_sensitive)?;
       assert!(
           !ManifestEvaluator::builder(filter)
               .build()
               .eval(&manifest_file)?,
           "Should not read: id in (5, 104), summary is [30, 79]"
       );
       Ok(())
   }
   ```
   
   Both fail on `main`:
   
   ```
   test 
expr::visitors::inclusive_metrics_evaluator::test::test_integer_in_straddling_bounds
 ... FAILED
   test expr::visitors::manifest_evaluator::test::test_in_straddling_bounds ... 
FAILED
   ```
   
   ### Expected behavior
   
   Both should return `ROWS_CANNOT_MATCH`, matching Java and PyIceberg.
   
   Narrowing the set the way `StrictMetricsEvaluator::not_in` already does is 
enough. In `InclusiveMetricsEvaluator::in`:
   
   ```rust
   let mut filtered_literals = literals.clone();
   
   if let Some(lower_bound) = self.lower_bound(field_id) {
       if lower_bound.is_nan() {
           return ROWS_MIGHT_MATCH;
       }
       filtered_literals.retain(|datum| datum.ge(lower_bound));
       if filtered_literals.is_empty() {
           return ROWS_CANNOT_MATCH;
       }
   }
   
   if let Some(upper_bound) = self.upper_bound(field_id) {
       if upper_bound.is_nan() {
           return ROWS_MIGHT_MATCH;
       }
       filtered_literals.retain(|datum| datum.le(upper_bound));
       if filtered_literals.is_empty() {
           return ROWS_CANNOT_MATCH;
       }
   }
   
   ROWS_MIGHT_MATCH
   ```
   
   and the equivalent in `ManifestEvaluator::in`. With both changed, `cargo 
test -p iceberg --lib` passes: 1614 passed, 0 failed, including the two new 
tests.
   
   ### Willingness to contribute
   
   I can contribute a fix for this bug independently
   


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