Skip to content

fix(expr): prune In predicates that straddle metrics bounds - #3144

Open
M-Tesla wants to merge 3 commits into
apache:mainfrom
M-Tesla:fix-3118-in-predicate-straddling-bounds
Open

fix(expr): prune In predicates that straddle metrics bounds#3144
M-Tesla wants to merge 3 commits into
apache:mainfrom
M-Tesla:fix-3118-in-predicate-straddling-bounds

Conversation

@M-Tesla

@M-Tesla M-Tesla commented Sep 4, 2026

Copy link
Copy Markdown

Which issue does this PR close?

What changes are included in this PR?

InclusiveMetricsEvaluator::in and ManifestEvaluator::in tested the lower bound and the upper bound against the full literal set independently. An In list whose values sit entirely outside [lower, upper] but straddle it — for example bounds [30, 79] and id 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_in implementation 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_evaluator and manifest_evaluator for the straddling case (id IN (5, 104) against bounds [30, 79]). Existing In tests in those modules still pass.

Locally: cargo fmt --all -- --check, cargo clippy -p iceberg --all-targets --all-features -- -D warnings, and cargo 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.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like RowGroupMetricsEvaluator::in also has the same issue?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@M-Tesla

M-Tesla commented Sep 4, 2026

Copy link
Copy Markdown
Author

Thanks @dhruvarya-db — applied your suggestions in b75d924: any() instead of clone/retain, and the same straddling-bounds prune in RowGroupMetricsEvaluator.

@dhruvarya-db dhruvarya-db left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it is worth it to factor out this pattern into a function and reuse it across the three callsites?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

In predicate is not pruned when its literals straddle the bounds (InclusiveMetricsEvaluator and ManifestEvaluator)

2 participants