Skip to content

feat: add OptionalFilterGate and optional_filter_mode config - #25674

Draft
adriangb wants to merge 2 commits into
apache:mainfrom
pydantic:optional-filter-gate
Draft

adriangb wants to merge 2 commits into
apache:mainfrom
pydantic:optional-filter-gate

Conversation

@adriangb

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

An optional filter (#25673) can be skipped without changing the result. Consumers (the Parquet row filter, FilterExec) need one shared way to decide when to skip it: stop evaluating a filter that removes too few rows, and start again when it may have become useful (for example, when a TopK threshold tightens).

Earlier attempts (#20160, #22236) either decided only when a file opened, or kept state in a shared expression so that one partition's measurements paused the filter in another partition. This PR keeps the decision per stream and has no wall clock, so the tests are deterministic.

What changes are included in this PR?

datafusion_physical_expr::optional_filter_gate:

  • OptionalFilterGate (one per stream, not shared). A state machine with two states:
    • Evaluate: evaluate the filter and count rows in and out. After sample_batches batches, if the pass ratio is above max_pass_ratio, pause.
    • Paused: skip the filter for N batches, then probe again. N doubles after each pause that the probe confirms (up to max_pause_batches). N resets when a probe finds the filter selective.
    • When the filter's generation changes (snapshot_generation, for example a dynamic filter update), the gate goes back to Evaluate and resets N.
    • API: begin_batch(num_rows) -> GateDecision plus record(rows_in, rows_out) for consumers that evaluate the filter themselves (for example an ArrowPredicate), and evaluate(batch) -> Option<BooleanArray> for the simple case.
  • OptionalFilterSiteStats (one per operator and filter, shared by its streams). A new stream starts from the pooled verdict for the current generation. So a new file does not pay for a new sample. A running stream is never changed by another stream.
  • While paused, the gate does not change its counters or backoff (a regression test covers this).

Config (no behavior change yet, no consumer uses the gate in this PR):

  • datafusion.execution.optional_filter_mode: always (default, today's behavior) | adaptive | pruning_only.
  • datafusion.execution.optional_filter_max_pass_ratio: default 0.8.

What is the testing strategy for this PR?

14 deterministic unit tests: pause on a non-selective filter, stay on for a selective one, backoff doubling and reset, generation reset (with a real DynamicFilterPhysicalExpr), a TopK-like tightening pattern, skewed input, pooled seeding, and the "no backoff growth while paused" regression. Config docs and information_schema.slt are updated.

Are there any user-facing changes?

Two new config options, with defaults that keep today's behavior. New public API in datafusion-physical-expr.

🤖 Generated with Claude Code

adriangb and others added 2 commits September 23, 2026 20:05
…orrectness

Add `OptionalFilterPhysicalExpr`, a transparent wrapper that marks a filter
as optional: a consumer can skip it without changing the query result. A
consumer can skip it only when the wrapper is a direct conjunct of the root
AND chain of its predicate. In all other positions the wrapper is
transparent, because `evaluate()` always evaluates the inner expression.
`snapshot()` returns the inner expression, so pruning sees through it.

Also add:
- `split_optional`, `is_optional_filter`, `as_dynamic_filter` and
  `debug_assert_optional_on_root_chain` helpers in `physical_expr::utils`
- `PhysicalOptionalFilterNode` proto message (field 29 in
  `PhysicalExprNode`) with self-encoding `try_to_proto`/`try_from_proto`

No producer uses the wrapper yet, so there is no behavior change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a runtime gate that pauses optional filters (filters that are not
needed for correctness, such as hash join and TopK dynamic filters) when
they remove too few rows. The gate is a per-stream state machine
(Evaluate / Paused with exponential backoff) that restarts evaluation
when the filter generation changes. Gates of one plan site share
lock-free pooled statistics, which seed the state of new gates.

Add the `datafusion.execution.optional_filter_mode` (always | adaptive |
pruning_only, default always) and
`datafusion.execution.optional_filter_max_pass_ratio` (default 0.8)
options. No operator uses the gate yet, so behavior does not change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation physical-expr Changes to the physical-expr crates sqllogictest SQL Logic Tests (.slt) common Related to common crate proto Related to proto crate labels Sep 24, 2026
@github-actions

Copy link
Copy Markdown

Thank you for opening this pull request!

Reviewer note: cargo-semver-checks reported the current version number is not SemVer-compatible with the changes in this pull request (compared against the base branch).

Details
     Cloning apache/main
    Building datafusion-common v55.1.0 (current)
       Built [  33.847s] (current)
     Parsing datafusion-common v55.1.0 (current)
      Parsed [   0.069s] (current)
    Building datafusion-common v55.1.0 (baseline)
       Built [  33.559s] (baseline)
     Parsing datafusion-common v55.1.0 (baseline)
      Parsed [   0.068s] (baseline)
    Checking datafusion-common v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   1.028s] 223 checks: 222 pass, 1 fail, 0 warn, 31 skip

--- failure constructible_struct_adds_field: struct exhaustively constructible through public API adds field ---

Description:
A pub struct that could be exhaustively constructed with a literal using only public API has a new pub field, breaking existing exhaustive literals.
        ref: https://doc.rust-lang.org/reference/expressions/struct-expr.html
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/constructible_struct_adds_field.ron

Failed in:
  field ExecutionOptions.optional_filter_mode in /home/runner/work/datafusion/datafusion/datafusion/common/src/config.rs:949
  field ExecutionOptions.optional_filter_max_pass_ratio in /home/runner/work/datafusion/datafusion/datafusion/common/src/config.rs:949

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [  71.023s] datafusion-common
    Building datafusion-physical-expr v55.1.0 (current)
       Built [  29.171s] (current)
     Parsing datafusion-physical-expr v55.1.0 (current)
      Parsed [   0.054s] (current)
    Building datafusion-physical-expr v55.1.0 (baseline)
       Built [  29.264s] (baseline)
     Parsing datafusion-physical-expr v55.1.0 (baseline)
      Parsed [   0.053s] (baseline)
    Checking datafusion-physical-expr v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.545s] 223 checks: 223 pass, 31 skip
     Summary no semver update required
    Finished [  60.483s] datafusion-physical-expr
    Building datafusion-proto v55.1.0 (current)
       Built [  55.767s] (current)
     Parsing datafusion-proto v55.1.0 (current)
      Parsed [   0.020s] (current)
    Building datafusion-proto v55.1.0 (baseline)
       Built [  55.565s] (baseline)
     Parsing datafusion-proto v55.1.0 (baseline)
      Parsed [   0.021s] (baseline)
    Checking datafusion-proto v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.151s] 223 checks: 223 pass, 31 skip
     Summary no semver update required
    Finished [ 113.517s] datafusion-proto
    Building datafusion-proto-models v55.1.0 (current)
       Built [  25.116s] (current)
     Parsing datafusion-proto-models v55.1.0 (current)
      Parsed [   0.141s] (current)
    Building datafusion-proto-models v55.1.0 (baseline)
       Built [  25.012s] (baseline)
     Parsing datafusion-proto-models v55.1.0 (baseline)
      Parsed [   0.144s] (baseline)
    Checking datafusion-proto-models v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   2.589s] 223 checks: 222 pass, 1 fail, 0 warn, 31 skip

--- failure enum_variant_added: enum variant added on exhaustive enum ---

Description:
A publicly-visible enum without #[non_exhaustive] has a new variant.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#enum-variant-new
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/enum_variant_added.ron

Failed in:
  variant ExprType:OptionalFilter in /home/runner/work/datafusion/datafusion/datafusion/proto-models/src/generated/prost.rs:1686
  variant ExprType:OptionalFilter in /home/runner/work/datafusion/datafusion/datafusion/proto-models/src/generated/prost.rs:1686

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [  54.590s] datafusion-proto-models
    Building datafusion-pruning v55.1.0 (current)
       Built [  40.535s] (current)
     Parsing datafusion-pruning v55.1.0 (current)
      Parsed [   0.017s] (current)
    Building datafusion-pruning v55.1.0 (baseline)
       Built [  40.589s] (baseline)
     Parsing datafusion-pruning v55.1.0 (baseline)
      Parsed [   0.017s] (baseline)
    Checking datafusion-pruning v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.111s] 223 checks: 223 pass, 31 skip
     Summary no semver update required
    Finished [  82.838s] datafusion-pruning
    Building datafusion-sqllogictest v55.1.0 (current)
       Built [ 100.991s] (current)
     Parsing datafusion-sqllogictest v55.1.0 (current)
      Parsed [   0.023s] (current)
    Building datafusion-sqllogictest v55.1.0 (baseline)
       Built [ 101.043s] (baseline)
     Parsing datafusion-sqllogictest v55.1.0 (baseline)
      Parsed [   0.026s] (baseline)
    Checking datafusion-sqllogictest v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.125s] 223 checks: 223 pass, 31 skip
     Summary no semver update required
    Finished [ 205.305s] datafusion-sqllogictest

@github-actions github-actions Bot added the auto detected api change Auto detected API change label Sep 24, 2026
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.87430% with 118 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.49%. Comparing base (95bb0a0) to head (34eb799).

Files with missing lines Patch % Lines
datafusion/proto-models/src/generated/pbjson.rs 0.00% 57 Missing ⚠️
...n/physical-expr/src/expressions/optional_filter.rs 79.53% 17 Missing and 18 partials ⚠️
...tafusion/physical-expr/src/optional_filter_gate.rs 97.63% 9 Missing and 2 partials ⚠️
datafusion/common/src/config.rs 65.21% 7 Missing and 1 partial ⚠️
datafusion/physical-expr/src/utils/mod.rs 96.94% 1 Missing and 3 partials ⚠️
datafusion/physical-expr/src/simplifier/mod.rs 88.88% 0 Missing and 2 partials ⚠️
datafusion/proto/src/physical_plan/from_proto.rs 0.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main   #25674    +/-   ##
========================================
  Coverage   82.48%   82.49%            
========================================
  Files        1140     1142     +2     
  Lines      437614   438513   +899     
  Branches   437614   438513   +899     
========================================
+ Hits       360986   361766   +780     
- Misses      54834    54925    +91     
- Partials    21794    21822    +28     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto detected api change Auto detected API change common Related to common crate documentation Improvements or additions to documentation physical-expr Changes to the physical-expr crates proto Related to proto crate sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants