Skip to content

Reapply "Add ExecutionPlan::apply_expressions() (apache#20337)" (apache#22437) - #24018

Open
jayshrivastava wants to merge 7 commits into
apache:mainfrom
jayshrivastava:js/dynamic-filters-method
Open

Reapply "Add ExecutionPlan::apply_expressions() (apache#20337)" (apache#22437)#24018
jayshrivastava wants to merge 7 commits into
apache:mainfrom
jayshrivastava:js/dynamic-filters-method

Conversation

@jayshrivastava

@jayshrivastava jayshrivastava commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

This change does not close the above issues because it does not implement a way to tell if a node is a producer dynamic filters.

Rationale for this change

See #23814 and datafusion-contrib/datafusion-distributed#553.

To send dynamic filter updates across the network, there needs to be a way to get access to PhysicalExpr from ExecutionPlan. As discussed in #23814, the cleanest way to do this is to add ExecutionPlan::apply_expressions, which mirrors a similar method for logical plan nodes.

What changes are included in this PR?

There's 3 commits in this PR:

Firstly, commit 1 re-applies the changes in #20337 (reverted in #22437).

Some of the reasons for why the original PR was reverted include
(a) apply_expressions is too complicated to implement and there's no concrete need to justify this complexity
(b) there was no usage of apply_expressions inside this repo

To address (a)

  • justification for adding this method is provided in make dynamic filters in ExecutionPlan nodes discoverable #23814
  • commit 2 in this PR adds helper methods apply_expression_roots and apply_no_expressions which abstract away the TreeNodeRecursion complexity from implementors. Now, apply_expressions very trivial to implement ex.
fn apply_expressions(
    &self,
    f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
) -> Result<TreeNodeRecursion> {
    apply_expression_roots([&self.predicate_1], f)
    apply_expression_roots([&self.predicate_2], f)
    apply_expression_roots([&self.other_expression], f)
}
  • the method traverses over &Arc<dyn PhysicalExpr> rather than &dyn PhysicalExpr to reduce complexity around lifetimes

To address (b):

  • commit 3 adds a usage of apply_expressions in physical-plan/src/aggregates/mod.rs. Previously, there was a hack that checked if a filter was pushed down using Arc::strong_count(dyn_filter) > 1. Now it uses apply_expressions
  • similarly, commit 4 removes is_used from dynamic filters which used to check Arc references counts to see if a filter was pushed down. Now, the hash join uses apply_expressions to find pushed down filters.

Are these changes tested?

Yes.

Are there any user-facing changes?

There's a new mandatory method ExecutionPlan::apply_expressions(). See the upgrading guide and documentation for details.

@github-actions github-actions Bot added documentation Improvements or additions to documentation optimizer Optimizer rules core Core DataFusion crate catalog Related to the catalog crate proto Related to proto crate datasource Changes to the datasource crate ffi Changes to the ffi crate physical-plan Changes to the physical-plan crate labels Jul 30, 2026
@jayshrivastava jayshrivastava changed the title Js/dynamic filters method Reapply "Add ExecutionPlan::apply_expressions() (apache#20337)" (apache#22437) Jul 30, 2026
@github-actions

github-actions Bot commented Jul 30, 2026

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 v54.1.0 (current)
       Built [  87.268s] (current)
     Parsing datafusion v54.1.0 (current)
      Parsed [   0.030s] (current)
    Building datafusion v54.1.0 (baseline)
       Built [  87.908s] (baseline)
     Parsing datafusion v54.1.0 (baseline)
      Parsed [   0.030s] (baseline)
    Checking datafusion v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.777s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [ 177.942s] datafusion
    Building datafusion-catalog v54.1.0 (current)
       Built [  33.166s] (current)
     Parsing datafusion-catalog v54.1.0 (current)
      Parsed [   0.021s] (current)
    Building datafusion-catalog v54.1.0 (baseline)
       Built [  33.363s] (baseline)
     Parsing datafusion-catalog v54.1.0 (baseline)
      Parsed [   0.020s] (baseline)
    Checking datafusion-catalog v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.130s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [  67.945s] datafusion-catalog
    Building datafusion-datasource v54.1.0 (current)
       Built [  35.284s] (current)
     Parsing datafusion-datasource v54.1.0 (current)
      Parsed [   0.031s] (current)
    Building datafusion-datasource v54.1.0 (baseline)
       Built [  35.115s] (baseline)
     Parsing datafusion-datasource v54.1.0 (baseline)
      Parsed [   0.029s] (baseline)
    Checking datafusion-datasource v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.308s] 223 checks: 222 pass, 1 fail, 0 warn, 30 skip

--- failure trait_method_added: pub trait method added ---

Description:
A non-sealed public trait added a new method without a default implementation, which breaks downstream implementations of the trait
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#trait-new-item-no-default
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.49.0/src/lints/trait_method_added.ron

Failed in:
  trait method datafusion_datasource::source::DataSource::apply_expressions in file /home/runner/work/datafusion/datafusion/datafusion/datasource/src/source.rs:240
  trait method datafusion_datasource::file::FileSource::apply_expressions in file /home/runner/work/datafusion/datafusion/datafusion/datasource/src/file.rs:368

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [  72.137s] datafusion-datasource
    Building datafusion-datasource-arrow v54.1.0 (current)
       Built [  33.489s] (current)
     Parsing datafusion-datasource-arrow v54.1.0 (current)
      Parsed [   0.010s] (current)
    Building datafusion-datasource-arrow v54.1.0 (baseline)
       Built [  33.207s] (baseline)
     Parsing datafusion-datasource-arrow v54.1.0 (baseline)
      Parsed [   0.014s] (baseline)
    Checking datafusion-datasource-arrow v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.082s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [  67.929s] datafusion-datasource-arrow
    Building datafusion-datasource-avro v54.1.0 (current)
       Built [  34.691s] (current)
     Parsing datafusion-datasource-avro v54.1.0 (current)
      Parsed [   0.011s] (current)
    Building datafusion-datasource-avro v54.1.0 (baseline)
       Built [  34.571s] (baseline)
     Parsing datafusion-datasource-avro v54.1.0 (baseline)
      Parsed [   0.009s] (baseline)
    Checking datafusion-datasource-avro v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.082s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [  70.608s] datafusion-datasource-avro
    Building datafusion-datasource-csv v54.1.0 (current)
       Built [  37.043s] (current)
     Parsing datafusion-datasource-csv v54.1.0 (current)
      Parsed [   0.011s] (current)
    Building datafusion-datasource-csv v54.1.0 (baseline)
       Built [  36.261s] (baseline)
     Parsing datafusion-datasource-csv v54.1.0 (baseline)
      Parsed [   0.011s] (baseline)
    Checking datafusion-datasource-csv v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.126s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [  74.787s] datafusion-datasource-csv
    Building datafusion-datasource-json v54.1.0 (current)
       Built [  37.264s] (current)
     Parsing datafusion-datasource-json v54.1.0 (current)
      Parsed [   0.012s] (current)
    Building datafusion-datasource-json v54.1.0 (baseline)
       Built [  36.364s] (baseline)
     Parsing datafusion-datasource-json v54.1.0 (baseline)
      Parsed [   0.012s] (baseline)
    Checking datafusion-datasource-json v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.100s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [  74.950s] datafusion-datasource-json
    Building datafusion-datasource-parquet v54.1.0 (current)
       Built [  41.847s] (current)
     Parsing datafusion-datasource-parquet v54.1.0 (current)
      Parsed [   0.180s] (current)
    Building datafusion-datasource-parquet v54.1.0 (baseline)
       Built [  40.648s] (baseline)
     Parsing datafusion-datasource-parquet v54.1.0 (baseline)
      Parsed [   0.031s] (baseline)
    Checking datafusion-datasource-parquet v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.197s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [  84.978s] datafusion-datasource-parquet
    Building datafusion-ffi v54.1.0 (current)
       Built [  49.751s] (current)
     Parsing datafusion-ffi v54.1.0 (current)
      Parsed [   0.052s] (current)
    Building datafusion-ffi v54.1.0 (baseline)
       Built [  50.007s] (baseline)
     Parsing datafusion-ffi v54.1.0 (baseline)
      Parsed [   0.052s] (baseline)
    Checking datafusion-ffi v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.297s] 223 checks: 221 pass, 1 fail, 1 warn, 30 skip

--- failure constructible_struct_adds_field: externally-constructible struct adds field ---

Description:
A pub struct constructible with a struct literal has a new pub field. Existing struct literals must be updated to include the new field.
        ref: https://doc.rust-lang.org/reference/expressions/struct-expr.html
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.49.0/src/lints/constructible_struct_adds_field.ron

Failed in:
  field ForeignLibraryModule.create_exec_with_expressions in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/tests/mod.rs:116
  field FFI_ExecutionPlan.apply_expressions in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/execution_plan.rs:56
  field FFI_ExecutionPlan.version in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/execution_plan.rs:105

--- warning repr_c_plain_struct_fields_reordered: struct fields reordered in repr(C) struct ---

Description:
A public repr(C) struct had its fields reordered. This can change the struct's memory layout, possibly breaking FFI use cases that depend on field position and order.
        ref: https://doc.rust-lang.org/reference/type-layout.html#reprc-structs
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.49.0/src/lints/repr_c_plain_struct_fields_reordered.ron

Failed in:
  FFI_ExecutionPlan.dynamic_expressions_produced moved from position 3 to 4, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/execution_plan.rs:60
  FFI_ExecutionPlan.with_new_children moved from position 4 to 5, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/execution_plan.rs:63
  FFI_ExecutionPlan.name moved from position 5 to 6, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/execution_plan.rs:67
  FFI_ExecutionPlan.execute moved from position 6 to 7, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/execution_plan.rs:71
  FFI_ExecutionPlan.repartitioned moved from position 7 to 8, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/execution_plan.rs:77
  FFI_ExecutionPlan.metrics moved from position 8 to 9, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/execution_plan.rs:86
  FFI_ExecutionPlan.partition_statistics moved from position 9 to 10, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/execution_plan.rs:92
  FFI_ExecutionPlan.clone moved from position 10 to 11, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/execution_plan.rs:99
  FFI_ExecutionPlan.release moved from position 11 to 12, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/execution_plan.rs:102
  FFI_ExecutionPlan.private_data moved from position 12 to 14, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/execution_plan.rs:109
  FFI_ExecutionPlan.library_marker_id moved from position 13 to 15, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/execution_plan.rs:114
  ForeignLibraryModule.create_exec_with_dynamic_expressions moved from position 15 to 16, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/tests/mod.rs:118
  ForeignLibraryModule.create_exec_with_statistics moved from position 16 to 17, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/tests/mod.rs:120
  ForeignLibraryModule.create_table_with_statistics moved from position 17 to 18, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/tests/mod.rs:122
  ForeignLibraryModule.create_physical_optimizer_rule moved from position 18 to 19, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/tests/mod.rs:125
  ForeignLibraryModule.create_context_aware_optimizer_rule moved from position 19 to 20, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/tests/mod.rs:127
  ForeignLibraryModule.create_query_planner moved from position 20 to 21, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/tests/mod.rs:131
  ForeignLibraryModule.version moved from position 21 to 22, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/tests/mod.rs:137
  ForeignLibraryModule.create_first_value_udaf moved from position 22 to 23, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/tests/mod.rs:140

     Summary semver requires new major version: 1 major and 0 minor checks failed
     Warning produced 1 major and 0 minor level warnings
    Finished [ 102.017s] datafusion-ffi
    Building datafusion-physical-expr v54.1.0 (current)
       Built [  23.718s] (current)
     Parsing datafusion-physical-expr v54.1.0 (current)
      Parsed [   0.042s] (current)
    Building datafusion-physical-expr v54.1.0 (baseline)
       Built [  23.867s] (baseline)
     Parsing datafusion-physical-expr v54.1.0 (baseline)
      Parsed [   0.041s] (baseline)
    Checking datafusion-physical-expr v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.439s] 223 checks: 222 pass, 1 fail, 0 warn, 30 skip

--- failure type_method_marked_deprecated: type method #[deprecated] added ---

Description:
A type method is now #[deprecated]. Downstream crates will get a compiler warning when using this method.
        ref: https://doc.rust-lang.org/reference/attributes/diagnostics.html#the-deprecated-attribute
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.49.0/src/lints/type_method_marked_deprecated.ron

Failed in:
  method datafusion_physical_expr::expressions::DynamicFilterPhysicalExpr::is_used in /home/runner/work/datafusion/datafusion/datafusion/physical-expr/src/expressions/dynamic_filters/mod.rs:441

     Summary semver requires new minor version: 0 major and 1 minor checks failed
    Finished [  49.065s] datafusion-physical-expr
    Building datafusion-physical-optimizer v54.1.0 (current)
       Built [  33.761s] (current)
     Parsing datafusion-physical-optimizer v54.1.0 (current)
      Parsed [   0.019s] (current)
    Building datafusion-physical-optimizer v54.1.0 (baseline)
       Built [  33.873s] (baseline)
     Parsing datafusion-physical-optimizer v54.1.0 (baseline)
      Parsed [   0.019s] (baseline)
    Checking datafusion-physical-optimizer v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.131s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [  69.033s] datafusion-physical-optimizer
    Building datafusion-physical-plan v54.1.0 (current)
       Built [  31.369s] (current)
     Parsing datafusion-physical-plan v54.1.0 (current)
      Parsed [   0.122s] (current)
    Building datafusion-physical-plan v54.1.0 (baseline)
       Built [  30.990s] (baseline)
     Parsing datafusion-physical-plan v54.1.0 (baseline)
      Parsed [   0.128s] (baseline)
    Checking datafusion-physical-plan v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.784s] 223 checks: 222 pass, 1 fail, 0 warn, 30 skip

--- failure trait_method_added: pub trait method added ---

Description:
A non-sealed public trait added a new method without a default implementation, which breaks downstream implementations of the trait
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#trait-new-item-no-default
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.49.0/src/lints/trait_method_added.ron

Failed in:
  trait method datafusion_physical_plan::execution_plan::ExecutionPlan::apply_expressions in file /home/runner/work/datafusion/datafusion/datafusion/physical-plan/src/execution_plan.rs:323
  trait method datafusion_physical_plan::ExecutionPlan::apply_expressions in file /home/runner/work/datafusion/datafusion/datafusion/physical-plan/src/execution_plan.rs:323

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [  64.653s] datafusion-physical-plan
    Building datafusion-proto v54.1.0 (current)
       Built [  49.754s] (current)
     Parsing datafusion-proto v54.1.0 (current)
      Parsed [   0.016s] (current)
    Building datafusion-proto v54.1.0 (baseline)
       Built [  50.398s] (baseline)
     Parsing datafusion-proto v54.1.0 (baseline)
      Parsed [   0.016s] (baseline)
    Checking datafusion-proto v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.299s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [ 102.515s] datafusion-proto

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

codecov-commenter commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 48.73418% with 486 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.98%. Comparing base (c1cb715) to head (828da33).

Files with missing lines Patch % Lines
datafusion/physical-plan/src/execution_plan.rs 61.85% 66 Missing and 8 partials ⚠️
datafusion/physical-plan/src/test/exec.rs 11.90% 36 Missing and 1 partial ⚠️
datafusion/core/src/physical_planner.rs 0.00% 24 Missing ⚠️
...sion/physical-optimizer/src/output_requirements.rs 0.00% 22 Missing ⚠️
datafusion/datasource/src/file_scan_config/mod.rs 25.00% 18 Missing ⚠️
datafusion/physical-plan/src/aggregates/mod.rs 72.30% 11 Missing and 7 partials ⚠️
datafusion/physical-plan/src/display.rs 0.00% 16 Missing ⚠️
...n/physical-plan/src/sorts/sort_preserving_merge.rs 0.00% 14 Missing ⚠️
...ysical-plan/src/windows/bounded_window_agg_exec.rs 0.00% 14 Missing ⚠️
...usion/physical-plan/src/windows/window_agg_exec.rs 0.00% 14 Missing ⚠️
... and 29 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #24018      +/-   ##
==========================================
- Coverage   81.05%   80.98%   -0.08%     
==========================================
  Files        1106     1106              
  Lines      382210   383149     +939     
  Branches   382210   383149     +939     
==========================================
+ Hits       309812   310286     +474     
- Misses      54091    54529     +438     
- Partials    18307    18334      +27     

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

@jayshrivastava
jayshrivastava force-pushed the js/dynamic-filters-method branch 2 times, most recently from b8794d2 to 5514194 Compare July 30, 2026 23:26
/// `apply_expressions` + `downcast_ref::<DynamicFilterPhysicalExpr>` and
/// counting nodes. Neither API is observable from SQL.
#[tokio::test]
async fn test_discover_dynamic_filters_via_expressions_api() {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This test explicitly covers that dynamic filters are discoverable post-pushdown. This is what we want in datafusion-distributed and I assume other projects want this as well

aggregate.with_dynamic_filter_expr(dynamic_filter)?
} else {
let mut aggregate = aggregate;
aggregate.dynamic_filter = None;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Semi-related. I think this was a bug. We kept the default dynamic filter created by try_new around even there's no pushed down filter. Now, we explicitly remove it. The test I added in datafusion/proto/tests/cases/roundtrip_physical_plan.rs covers this.

Comment thread datafusion-examples/examples/relation_planner/table_sample.rs
@jayshrivastava

Copy link
Copy Markdown
Contributor Author

I see the code coverage is isn't very high. I don't see any good candidates to call apply_expressions generally. Maybe we can implement check_invariants on a few ExecutionPlan implementations which assert apply_expressions returns the right number of expressions.

@jayshrivastava
jayshrivastava marked this pull request as ready for review July 31, 2026 00:19

@gene-bordegaray gene-bordegaray 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.

I had some questions about the exact contract but overall I think this almost there, just some clarification and looking very clean 👍

Comment thread docs/source/library-user-guide/upgrading/54.0.0.md Outdated
Comment thread datafusion/physical-plan/src/async_func.rs
Comment thread datafusion/physical-plan/src/streaming.rs
Comment thread datafusion/physical-plan/src/repartition/mod.rs
Comment thread datafusion-examples/examples/relation_planner/table_sample.rs
let mut found = false;
plan.apply(|node| {
node.apply_expressions(&mut |root| {
root.apply(|expr| {

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.

is there any way to eliminate this triple nesting we do.

From my understanding its:

  1. walking the execution plan tree
  2. Walk the expressions roots in one plan node
  3. Walks inside the one expression

Maybe something like a apply_plan_expressions would make this easier to read?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think this pattern is expected. I followed these docs for apply_expressions on logical plans to implement this for physical plans:

/// // use apply to walk the plan and collect all expressions
/// let mut expressions = HashSet::new();
/// plan.apply(|node| {
/// // collect all expressions in the plan
/// node.apply_expressions(|expr| {
/// expressions.insert(expr.clone());
/// Ok(TreeNodeRecursion::Continue) // control walk of expressions
/// })?;
/// Ok(TreeNodeRecursion::Continue) // control walk of plan nodes
/// }).unwrap();
.

Comment thread datafusion-examples/examples/relation_planner/table_sample.rs
Comment thread datafusion/datasource/src/file.rs Outdated
Comment thread datafusion/physical-plan/src/async_func.rs
&self,
_f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
) -> Result<TreeNodeRecursion> {
Ok(TreeNodeRecursion::Continue)

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.

Since we already have apply_expression_roots to abstract "walk these expressions" case, would it be worth a small sibling helper for the "I have none" case? Right now that's just Ok(TreeNodeRecursion::Continue) repeated at every leaf/no-op node. Wondering if it would make sense to have something like apply_expression_continue() living next to it, just a plain function.

I think something like apply_expression_roots(std::iter::empty(), f) does the same thing, but since we're trying to abstract away TreeNodeRecursion, wondering if it would still make sense to have the dedicated helper

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

apply_expression_roots(std::iter::empty(), f) is nice, but you have to use type annotations in practice:

apply_expression_roots(
      std::iter::empty::<&Arc<dyn PhysicalExpr>>(),
      f,
  )

This is a bit less clean so
I added a helper apply_no_expressions instead.

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.

Jay (or anyone), feel free to disagree. I suggested this because imo it’s clearer to me, but whatever is clearest when upgrading is best.

// So here, we try to use ref count to determine if the dynamic filter
// has actually be pushed down.
// Issue: <https://github.com/apache/datafusion/issues/18856>
let child_accepts_dyn_filter = Arc::strong_count(dyn_filter) > 1;

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.

Even if the rewrite to use apply_expressions looks like it adds a bunch of code, I think this is a nice use case for it, relying on the strong count of the dynamic filter's inner/outer Arc has caused several headaches before. Maybe we could do something similar in HashJoinExec to determine whether the dynamic filter has a consumer? Right now that's done via is_used, which relies on strong count. IIRC it's only used to check for consumers, so maybe we could remove is_used entirely in favor of this approach?

.map(|df| df.filter.is_used())

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a good idea! I'll do it rn

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

@github-actions github-actions Bot added the physical-expr Changes to the physical-expr crates label Aug 6, 2026
@cetra3

cetra3 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

This PR as it stands is in conflict with #23494. I think it may need a pass over it to align with the try_to_proto way of doing things.

Comment thread datafusion/physical-expr/src/expressions/dynamic_filters/mod.rs
@jayshrivastava
jayshrivastava force-pushed the js/dynamic-filters-method branch from 7550339 to db63637 Compare August 7, 2026 13:39
@jayshrivastava

Copy link
Copy Markdown
Contributor Author

@cetra3 Imo, apply_expressions isn't leaking internal state, at least not to the extent prior to #23494. It's just providing iteration over expressions in a plan. It also mirrors the API for logical plan nodes

pub fn apply_expressions<F: FnMut(&Expr) -> Result<TreeNodeRecursion>>(
&self,
mut f: F,
) -> Result<TreeNodeRecursion> {

@gene-bordegaray gene-bordegaray 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.

last thing, other than that I think this is good 👍

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 believe should we apply expressions to this op because of this as well?

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.

it is done at line 315 it seems 🤔

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.

yes but is using apply_no_expressions(f)

I was thinking that since this method can add the ordering to the cached self.cache.eq_properties we would traverse that in the apply_expressions() method here.

Maybe I am still unclear about what needs to be traversed and not @jayshrivastava ?

@alamb alamb 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.

Thank you @jayshrivastava and @gene-bordegaray -- I think this is looking quite nice and ready to go

Since I think we are under some time pressure, I took the liberty of merging up from main and resolving conflicts, and tweaking some documentation

I will also make a proposal PR to reduce the

        datafusion_physical_plan::apply_expression_roots(
            self.required_input_ordering
                .iter()
                .flatten()
                .map(|sort_expr| &sort_expr.expr),
            f,
        )

Boiler plate code, but we can also do that as a follow on

We might also be able to make this pattern easier to read

       datafusion_physical_plan::apply_expression_roots(
            self.projection
                .source
                .iter()
                .map(|proj_expr| &proj_expr.expr),
            f,
        )

By changing apply_expression_roots to take AsRef<Arc<PhysicalExpr>> and then implementing AsRef for ProjectionExpr

I think it could be collapsed into

       datafusion_physical_plan::apply_expression_roots(
            self.projection
                .source
                .iter()
            f,
        )

slice of the last non-empty input batch) and pass it to window expressions
via `WindowEvalContext::with_most_recent_row` instead of copying it into
each partition's state.
Add `apply_expressions` to your implementation. Call `f` on each top-level

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.

This is fine, though it is technically redundant with the (very nice docs) on apply_expressions

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.

Specifically the examples are redundant I think

Comment thread datafusion-examples/examples/relation_planner/table_sample.rs
Comment thread datafusion-examples/examples/relation_planner/table_sample.rs
@alamb

alamb commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Sorry hit submit too early -- I am finishing my review now

@jayshrivastava

Copy link
Copy Markdown
Contributor Author

@alamb Thanks. I'll let you finish the review. Also I can fix up the commits! I figured I'd wait for a review first

@alamb alamb 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.

TLDR looks good to me. I had some small simplification suggestions but I don't think they are required

As I mentioned, I did merge up from main and tweak some comments, but otherwise. think this is ready to go.

Thanks again (and thanks to @LiaCastaneda for starting this work!)

///
/// See [`ExecutionPlan::apply_expressions`] for more details and implementation examples.
///
/// [`ExecutionPlan::apply_expressions`]: datafusion_physical_plan::ExecutionPlan::apply_expressions

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.

This is good to direct readers to ExecutionPlan::apply_expressions and then document that heavily

snapshot: snapshot_fn_wrapper,
snapshot_generation: snapshot_generation_fn_wrapper,
is_volatile_node: is_volatile_node_fn_wrapper,
expression_id: expression_id_fn_wrapper,

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.

this appears to be an unrelated change, but a good one

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.

it is done at line 315 it seems 🤔

@@ -852,6 +911,65 @@ pub trait ExecutionPlan: Any + Debug + DisplayAs + Send + Sync {
}
}

/// Implements [`ExecutionPlan::apply_expressions`] for a node with no expressions.
pub fn apply_no_expressions(

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 would suggest you remove this function (as it is pretty clearly documented what to do when there are no expressions, so it is just one more function that adds complexity but not much help I don't think).

I do realize there is a nice symmetry to have a helper for each case, but in this case I am not sure it is needed

slice of the last non-empty input batch) and pass it to window expressions
via `WindowEvalContext::with_most_recent_row` instead of copying it into
each partition's state.
Add `apply_expressions` to your implementation. Call `f` on each top-level

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.

Specifically the examples are redundant I think

/// should not be called in any child expressions nor in any expressions of child nodes.
///
/// The closure can return [`TreeNodeRecursion::Stop`] to stop iteration, otherwise
/// iteration should continue. ([`TreeNodeRecursion::Jump`] and [`TreeNodeRecursion::Jump`]

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 think this means to say "continue" and "jump" are equivalent. I will push a fix for this

@alamb

alamb commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

@alamb Thanks. I'll let you finish the review. Also I can fix up the commits! I figured I'd wait for a review first

Thanks @jayshrivastava -- I am done, though it appears I (claude) botched the merge. Will fix

partition_count,
generator_count
);
Arc::make_mut(&mut self.cache).partitioning = partitioning;

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.

would this also be in the same boat? we would need to traverse these?

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 catalog Related to the catalog crate core Core DataFusion crate datasource Changes to the datasource crate documentation Improvements or additions to documentation ffi Changes to the ffi crate optimizer Optimizer rules physical-expr Changes to the physical-expr crates physical-plan Changes to the physical-plan crate proto Related to proto crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants