fix(expr): prune In predicates that straddle metrics bounds - #3144
fix(expr): prune In predicates that straddle metrics bounds#3144M-Tesla wants to merge 3 commits into
Conversation
InclusiveMetricsEvaluator and ManifestEvaluator tested each bound against the full literal set, so IN lists with values both below lower and above upper were not pruned. Narrow the set the way Java, PyIceberg, and StrictMetricsEvaluator::not_in already do.
| // Narrow the set against each bound, matching Java / PyIceberg. | ||
| let mut filtered_literals = literals.clone(); | ||
|
|
||
| if let Some(lower_bound) = self.lower_bound(field_id) { |
There was a problem hiding this comment.
Looks like RowGroupMetricsEvaluator::in also has the same issue?
There was a problem hiding this comment.
Thanks — it did. RowGroupMetricsEvaluator::in now uses the same both-bounds check, with a regression test for float bounds [4.0, 6.0] and IN (2.0, 8.0).
| } | ||
|
|
||
| // Narrow the set against each bound, matching Java / PyIceberg. | ||
| let mut filtered_literals = literals.clone(); |
There was a problem hiding this comment.
I wonder if something like might be more readable. We avoid creating a mutable filtered_literals as well this way
let lower_bound = self.lower_bound(field_id);
let upper_bound = self.upper_bound(field_id);
if lower_bound.is_some_and(Datum::is_nan) || upper_bound.is_some_and(Datum::is_nan) {
return ROWS_MIGHT_MATCH;
}
let any_literal_in_bounds = match (lower_bound, upper_bound) {
(Some(lower), Some(upper)) => {
literals.iter().any(|datum| datum.ge(lower) && datum.le(upper))
}
(Some(lower), None) => literals.iter().any(|datum| datum.ge(lower)),
(None, Some(upper)) => literals.iter().any(|datum| datum.le(upper)),
(None, None) => true,
};
if !any_literal_in_bounds {
return ROWS_CANNOT_MATCH;
}
ROWS_MIGHT_MATCH
There was a problem hiding this comment.
Agreed, that's cleaner. Switched to any() over the original set instead of clone + retain.
| } | ||
|
|
||
| // Narrow the set against each bound, matching InclusiveMetricsEvaluator. | ||
| let mut filtered_literals = literals.clone(); |
There was a problem hiding this comment.
Same applies here https://github.com/apache/iceberg-rust/pull/3144/changes#r3938316479
There was a problem hiding this comment.
Done here as well — ManifestEvaluator::in now uses the same any() form.
Apply review feedback: use any() instead of clone/retain, and prune straddling In predicates in RowGroupMetricsEvaluator too.
|
Thanks @dhruvarya-db — applied your suggestions in b75d924: |
dhruvarya-db
left a comment
There was a problem hiding this comment.
LGTM (I am not a maintainer though)
| // if all values are less than lower bound, rows cannot match. | ||
| return ROWS_CANNOT_MATCH; | ||
| } | ||
| if lower_bound.is_some_and(|d| d.is_nan()) || upper_bound.is_some_and(|d| d.is_nan()) { |
There was a problem hiding this comment.
I wonder if it is worth it to factor out this pattern into a function and reuse it across the three callsites?
There was a problem hiding this comment.
Good call, the three in evaluators share that match. Pulled it into a small crate-private helper and left bound loading / NaN handling at each callsite.
The three In evaluators used the same match. Pull it into a crate-private helper.
Which issue does this PR close?
What changes are included in this PR?
InclusiveMetricsEvaluator::inandManifestEvaluator::intested the lower bound and the upper bound against the full literal set independently. AnInlist whose values sit entirely outside[lower, upper]but straddle it — for example bounds[30, 79]andid IN (5, 104)— was therefore not pruned.Both evaluators now narrow the literal set against each bound in turn, matching Iceberg Java, PyIceberg, and the existing
StrictMetricsEvaluator::not_inimplementation in this crate. Scan results were already correct (the plan was a superset); this only avoids opening files and manifests that cannot contain a match.Are these changes tested?
Unit tests in
inclusive_metrics_evaluatorandmanifest_evaluatorfor the straddling case (id IN (5, 104)against bounds[30, 79]). ExistingIntests in those modules still pass.Locally:
cargo fmt --all -- --check,cargo clippy -p iceberg --all-targets --all-features -- -D warnings, andcargo test -p iceberg --lib expr::visitors.AI Disclosure
Assisted draft of the bound-narrowing change and regression tests. The algorithm matches Iceberg Java, PyIceberg, and
StrictMetricsEvaluator::not_in. Reviewed and verified with the checks above.