Skip to content

coordinator: display consumer dynamic filters after execution - #623

Open
jayshrivastava wants to merge 13 commits into
mainfrom
js/1-display-dynamic-filters
Open

coordinator: display consumer dynamic filters after execution#623
jayshrivastava wants to merge 13 commits into
mainfrom
js/1-display-dynamic-filters

Conversation

@jayshrivastava

@jayshrivastava jayshrivastava commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Stack

This stack of PRs implements distributed dynamic filtering #528

  1. coordinator: display consumer dynamic filters after execution #623 <- you are here
  2. feat: plan distributed dynamic filters #634
  3. feat: forward remote dynamic filter updates to coordinator #635
  4. coordinator: merge partial dynamic filters  #636
  5. coordinator: forward merged dynamic filters to consumers #637
  6. [do not review] worker: apply merged dynamic filters during execution #639

Closes: #529

Problem

Post df-55 upgrade, dynamic filters should work in the worker-local case. There's no way to observe them working other than looking at metrics.

  ┌───── Stage 2 ── tasks=1
  │ AggregateExec: Final COUNT(*)
  │   [Stage 1] => NetworkCoalesceExec
  └──────────────────────────────────────────────────
    ┌───── Stage 1 ── tasks=2
    │ HashJoinExec: orders.customer_id = selected_customers.customer_id
    │   DistributedLeafExec:
    |     ...
    │   DistributedLeafExec:
    │     t0: DataSourceExec: predicate=DynamicFilter [ empty ]
    │     t1: DataSourceExec: predicate=DynamicFilter [ empty ]
    └────────────────────────────────────────────────

Ideally we want the final filters visible when displaying plans.

Solution

This PR adds a new protocol which is basically identical to the metrics protocol. Even the MetricsStore is now just Store and is generic over TaskMetrics and TaskCompletedDynamicFilters (contains completed dynamic filters for a task).

pub(crate) type MetricsStore = Store<TaskMetrics>;
pub(crate) type CompletedDynamicFilterStore = Store<TaskCompletedDynamicFilters>;

Similar to the metrics protocol, workers now collect completed dynamic filters and send them back to the coordinator.

Coordinator                                               Worker
-----------                                               ------
       Create independent display copies
                    |
                    +-- SetPlan(task 0, filter IDs) -------> Decode plan
                    |                                       |
                    |                                       | execute
                    |                                       |
                    |                                       |
                    |                                       |
                    |                                       |
                    |                                       | task finishes
                    |                                       v
                    |<----- TaskDynamicFilters ----- Serialize completed filters from the consumers
                    |
                    v

Then, at display time, we call apply_reports_to_distributed_leaves which traverses the plan_for_viz and updates the dynamic filters for all the variants:

DistributedLeafExec
  task 0: DynamicFilter [ key@0 >= 1 AND key@0 <= 10 ]
  task 1: DynamicFilter [ empty ]

Notes

Duplicate RPC Messages

We will eventually have more dynamic filter RPCs which manage the worker -> coordinator -> merge -> worker flow mentioned in #553.

In theory, the coordinator will know at merge time what the completed filters are, making the TaskCompletedDynamicFilters and final worker -> coordinator message in this PR irrelevant.

However, I think having these mechanisms be separate is good because a) it helps us validate that the dynamic filter coordinator -> worker flow work using external "oracle", and b) there's no guarantee that the coordinator -> worker propagation happens before the query is done (ex. the DataSourceExec may not block execution waiting for dynamic filters), so it's good to have a separate way to know if the final DataSourceExec applied a filter or not.

AND true and empty filters

DynamicFilter [ sr_returned_date_sk@0 >= 2451545 AND sr_returned_date_sk@0 <= 2451910 AND true ] AND DynamicFilter [ empty ]

In this filter AND true occurs because of apache/datafusion#24277. The first DynamicFilter is active but we lose the HashTableLookupExpr when serializing it to send back to the coordinator.

The 2nd filter is DynamicFilter [ empty ] because this is a dynamic filter produced by a remote producer, which does not get propagated to this node yet. This will be fixed later.

Displaying Dynamic Filters

Protocol is as similar to the metrics protocol as possible. Due to double wrapping (MetricsWrapperExec wraps DistributedLeafExec, it's tricky to do the dynamic filter rewrite after doing the metrics rewrite. So rewrite_distributed_plan_with_dynamic_filters has to be called first.

let plan = rewrite_distributed_plan_with_dynamic_filters(plan).await?;
let plan = rewrite_distributed_plan_with_metrics(plan, DistributedMetricsFormat::Aggregated).await?;
println!("{}", display_plan_ascii(plan.as_ref(), true));

Testing

  • Tests in tests/dynamic_filtering.rs

@jayshrivastava jayshrivastava changed the title display dynamic filters during execution display dynamic filters after execution Aug 11, 2026
@jayshrivastava
jayshrivastava force-pushed the js/1-display-dynamic-filters branch from 2a2bffc to f549dc2 Compare August 13, 2026 13:16
@jayshrivastava
jayshrivastava changed the base branch from js/upgrade-df-55-08-10 to branch-55 August 13, 2026 13:16
@jayshrivastava jayshrivastava changed the title display dynamic filters after execution coordinator: display dynamic filters after execution Aug 13, 2026
@stuhood

stuhood commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Thanks for working on this: this will be very useful!

One quick thought: the dynamic filter will sometimes be much, much larger than what you would actually want to display in an EXPLAIN plan (a large InList or Hash). That suggests that rather than sending the whole filter, what would actually make sense to send back is some sort of human readable summary of the filter?

Also, I have a draft of a related change on our codebase, and it seemed like the easiest mechanism for transferring this kind of information back is via metrics... but the most natural/obvious thing that seemed to be missing in that case was essentially a "string" metric type (we would use it to display a chosen strategy/enum from a scan). Do you think that that might be worth pursuing upstream?

@jayshrivastava

Copy link
Copy Markdown
Collaborator Author

One quick thought: the dynamic filter will sometimes be much, much larger than what you would actually want to display in an EXPLAIN plan (a large InList or Hash). That suggests that rather than sending the whole filter, what would actually make sense to send back is some sort of human readable summary of the filter?

Also, I have a draft of a related change on our codebase, and it seemed like the easiest mechanism for transferring this kind of information back is via metrics... but the most natural/obvious thing that seemed to be missing in that case was essentially a "string" metric type (we would use it to display a chosen strategy/enum from a scan). Do you think that that might be worth pursuing upstream?

Serializing them as a string is reasonable. Rather than a metric, I think we can implement a PhysicalExpr which just wraps a string and inject it into the play for display using DynamicFilterPhysicalExpr::update(string_expr). @gabotechs what do you think?

@gabotechs

Copy link
Copy Markdown
Collaborator

🤔 I'm not sure if I'm understanding the suggestion. Updating a filter with DynamicFilterPhysicalExpr::update is not really related to visualization, it's how you actually update the filter no?

@jayshrivastava
jayshrivastava force-pushed the js/1-display-dynamic-filters branch 2 times, most recently from 4ccec74 to bac65ba Compare August 18, 2026 18:52
Base automatically changed from branch-55 to main August 20, 2026 08:38
gabotechs added a commit that referenced this pull request Aug 20, 2026
## Summary 

Closes
#530

- This PR updates the upstream datafusion SHA to the HEAD of
https://github.com/apache/datafusion/commits/branch-55/ (edit: this
branch is continuously being updated. I will make sure this PR is at the
head before merging)
- Rust upgade to 1.94


## Changes

1. In `src/protobuf/distributed_codec.rs` we now use the
`proto_converter` argument during serde
- We still don't use the `DeduplicatingProtoConverter`, so dynamic
filters don't necessarily work. I think this is outside the scope of
this PR will be addressed in
#623,
which will be rebased after the upgrade.

3. `ExecutionPlan::apply_expressions` is added for every custom
`ExecutionPlan` in this repo
- Wrapper types (`MetricsWrapperExec`, `WorkUnitFileScanConfig`,
`DistributedLeafExec`) delegate to the inner type
- Other plans take`TreeNodeRecursion::Continue` because they have no
expressions (ex. `SamplerExec`)
- Note that `apply_expressions` does not need to yield sort or
partitioning expressions in the plan properties

3. We migrate from `partition_statistics` to `statistics_from_inputs`
for every `ExecutionPlan`.
- `src/distributed_planner/statistics/plan_statistics.rs` can just use
`statistics_from_inputs` directly instead of doing the
`StatisticsWrapper` workaround.

5. Range partitioning is now supported.
- CPU costing now includes range-key comparison cost and has a new unit
test. See src/distributed_planner/
   statistics/complexity_cpu.rs:238.
- I think there's open questions about range partitioning. I've opened
an issue here to make sure it behaves as expected after the upgrade:
#628 (comment)

6. Peak-memory metrics use the existing gauge wire representation.

DataFusion added MetricValue::PeakMemoryUsage. It is serialized as the
existing named-gauge protobuf variant to avoid a wire-format change. See
src/protocol/grpc/
   metrics_proto.rs:124.

The value and name survive, and aggregation is still additive, but
decoding produces a generic Gauge, not PeakMemoryUsage. The practical
difference is mainly display formatting: it
may render as a count rather than human-readable bytes. This is the
clearest remaining compromise/risk in the upgrade.

7. File-scan rebalancing changed its discriminator.

DataFusion removed partitioned_by_file_group;
output_partitioning.is_some() is now the source of truth. See
src/events/defaults/file_scan_config.rs:43. This decides whether files
are
round-robin rebalanced or split through FileGroupPartitioner, so it is
behavior-sensitive even though it is a one-line migration.

8. Two previously ignored correctness tests were enabled.
- See `tests/multi_task_collect_join_repros.rs`
- These were upstream DataFusion correctness fixes, not fixes made
locally in this upgrade.

9. drop(reporter) was made explicit on the sampler’s empty-input path.

The reporter sends its result on Drop; explicitly dropping it both
satisfies the new compiler/lint behavior and guarantees the zero-row EOS
report is sent before returning. See src/
   execution_plans/sampler.rs:259.

10. Plan changes

- `dynamic_rg_pruning=eligible` is now displayed on eligible scans:
1,354 occurrences in TPC-DS, 188 in TPC-H, and 12 in ClickBench
- `DataSourceExec` now displays its output partitioning. See
`tests/join.rs` (eventually, someone should delete this test
#628)
- Project after sort. This looks like some upstream optimizer rule
change ex. `tests/distributed_unions.rs` and
`tests/distributed_aggregation.rs`.
```
-          │   SortExec: expr=[MinTemp@0 ASC NULLS LAST, RainToday@1 ASC NULLS LAST], preserve_partitioning=[true]
-          │     ProjectionExec: expr=[MaxTemp@0 as MinTemp, RainToday@1 as RainToday]
+          │   ProjectionExec: expr=[MaxTemp@0 as MinTemp, RainToday@1 as RainToday]
+          │     SortExec: expr=[MaxTemp@0 ASC NULLS LAST, RainToday@1 ASC NULLS LAST], preserve_partitioning=[true]
```
- LocalLimitExec became more common: TPC-DS went from 0 to 20
occurrences and ClickBench from 1 to 21, reflecting additional local
limit pushdown.
- Subquery/semi-join plans became more distributed:
    - TPC-DS CollectLeft hash joins: 615 → 610
    - TPC-DS partitioned hash joins: 98 → 103
    - TPC-DS left-semi occurrences: 11 → 25
    - TPC-DS network shuffles: 368 → 378
    - TPC-H - just a few
- These are meaningful topology changes: some subqueries now use
partitioned left-semi joins and therefore introduce hash shuffles
instead of collecting/broadcasting one side.
- Scalar rendering improved, especially decimal literals: internal forms
such as Some(0),7,2 now display as CAST(0.00 AS Decimal128(7, 2)).
- Minor changes (Ex. tpcds 21)
- `__common_expr_4` became `__common_expr_3`; that is only an internal
alias renumbering.
- The projection that renamed `d_date` to `__common_expr_2` disappeared.
- `d_date` is retained directly in the join output and referenced
directly by partial/final aggregates.
  - Column positions changed


- File-group allocation changed substantially
- Some explicit RoundRobinBatch repartitions disappeared and scans
gained different numbers of file groups
- Distribute byte ranges across partitions:
apache/datafusion#22439
- Lowers `repartition_file_min_size` from 10 MiB to 1 MiB. The PR
explicitly calls out TPC-DS SF1 dimension tables. Files may be
duplicated across multiple partitions where but each partition reads a
different byte range (this is hidden by <int>....<int>, but we know from
the correctness tests that nothing broke). A lot of tpcds queries now
split across `target_partitions` instead of staying under-partitioned.
In the `tpcds` plan tests, we use `target_partitions=3`.
Example:
```
-                │     t0: DataSourceExec: file_groups={2 groups: [[/testdata/tpcds/plans_sf1_partitions4/date_dim/part-0.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-1.parquet:<int>..<int>, /testdata/tpcds/plans_sf1_partitions4/date_dim/part-2.parquet:<int>..<int>]]}, projection=[d_date_sk, d_week_seq, d_day_name], file_type=parquet, predicate=DynamicFilter [ empty ]
-                │     t1: DataSourceExec: file_groups={2 groups: [[/testdata/tpcds/plans_sf1_partitions4/date_dim/part-0.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-2.parquet:<int>..<int>]]}, projection=[d_date_sk, d_week_seq, d_day_name], file_type=parquet, predicate=DynamicFilter [ empty ]
-                │     t2: DataSourceExec: file_groups={2 groups: [[/testdata/tpcds/plans_sf1_partitions4/date_dim/part-0.parquet:<int>..<int>, /testdata/tpcds/plans_sf1_partitions4/date_dim/part-1.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-2.parquet:<int>..<int>, /testdata/tpcds/plans_sf1_partitions4/date_dim/part-3.parquet:<int>..<int>]]}, projection=[d_date_sk, d_week_seq, d_day_name], file_type=parquet, predicate=DynamicFilter [ empty ]
-                │     t3: DataSourceExec: file_groups={2 groups: [[/testdata/tpcds/plans_sf1_partitions4/date_dim/part-1.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-3.parquet:<int>..<int>]]}, projection=[d_date_sk, d_week_seq, d_day_name], file_type=parquet, predicate=DynamicFilter [ empty ]
+                │     t0: DataSourceExec: file_groups={3 groups: [[/testdata/tpcds/plans_sf1_partitions4/date_dim/part-0.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-1.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-2.parquet:<int>..<int>]]}, projection=[d_date_sk, d_week_seq, d_day_name], file_type=parquet, predicate=DynamicFilter [ empty ]
+                │     t1: DataSourceExec: file_groups={3 groups: [[/testdata/tpcds/plans_sf1_partitions4/date_dim/part-0.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-1.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-2.parquet:<int>..<int>, /testdata/tpcds/plans_sf1_partitions4/date_dim/part-3.parquet:<int>..<int>]]}, projection=[d_date_sk, d_week_seq, d_day_name], file_type=parquet, predicate=DynamicFilter [ empty ]
+                │     t2: DataSourceExec: file_groups={3 groups: [[/testdata/tpcds/plans_sf1_partitions4/date_dim/part-0.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-1.parquet:<int>..<int>, /testdata/tpcds/plans_sf1_partitions4/date_dim/part-2.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-3.parquet:<int>..<int>]]}, projection=[d_date_sk, d_week_seq, d_day_name], file_type=parquet, predicate=DynamicFilter [ empty ]
+                │     t3: DataSourceExec: file_groups={3 groups: [[/testdata/tpcds/plans_sf1_partitions4/date_dim/part-0.parquet:<int>..<int>, /testdata/tpcds/plans_sf1_partitions4/date_dim/part-1.parquet:<int>..<int>, /testdata/tpcds/plans_sf1_partitions4/date_dim/part-1.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-2.parquet:<int>..<int>, /testdata/tpcds/plans_sf1_partitions4/date_dim/part-2.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-3.parquet:<int>..<int>]]}, projection=[d_date_sk, d_week_seq, d_day_name], file_type=parquet, predicate=DynamicFilter [ empty ]
```

---------

Co-authored-by: Gabriel <45515538+gabotechs@users.noreply.github.com>
Co-authored-by: Gabriel <gabriel.musatmestre@datadoghq.com>
@jayshrivastava
jayshrivastava force-pushed the js/1-display-dynamic-filters branch from bac65ba to 75e43d6 Compare August 21, 2026 16:28
@jayshrivastava
jayshrivastava force-pushed the js/1-display-dynamic-filters branch from 75e43d6 to 72a8e6c Compare August 21, 2026 16:48
@jayshrivastava
jayshrivastava marked this pull request as ready for review August 21, 2026 20:29
@jayshrivastava
jayshrivastava force-pushed the js/1-display-dynamic-filters branch from 7142661 to 92fb135 Compare August 21, 2026 20:58
@jayshrivastava

Copy link
Copy Markdown
Collaborator Author

🤔 I'm not sure if I'm understanding the suggestion. Updating a filter with DynamicFilterPhysicalExpr::update is not really related to visualization, it's how you actually update the filter no?

The suggestion is basically to consider returning a string representation from the worker to the coordinator for displaying rather than a serialized PhysicalExpr. We will need to do some hacking to make that work.

I think we can consider this later. The serialization in this PR is simpler and uses the native PhysicalExpr serialization.

alexanderbianchi pushed a commit to alexanderbianchi/datafusion-distributed that referenced this pull request Aug 24, 2026
## Summary 

Closes
datafusion-contrib#530

- This PR updates the upstream datafusion SHA to the HEAD of
https://github.com/apache/datafusion/commits/branch-55/ (edit: this
branch is continuously being updated. I will make sure this PR is at the
head before merging)
- Rust upgade to 1.94


## Changes

1. In `src/protobuf/distributed_codec.rs` we now use the
`proto_converter` argument during serde
- We still don't use the `DeduplicatingProtoConverter`, so dynamic
filters don't necessarily work. I think this is outside the scope of
this PR will be addressed in
datafusion-contrib#623,
which will be rebased after the upgrade.

3. `ExecutionPlan::apply_expressions` is added for every custom
`ExecutionPlan` in this repo
- Wrapper types (`MetricsWrapperExec`, `WorkUnitFileScanConfig`,
`DistributedLeafExec`) delegate to the inner type
- Other plans take`TreeNodeRecursion::Continue` because they have no
expressions (ex. `SamplerExec`)
- Note that `apply_expressions` does not need to yield sort or
partitioning expressions in the plan properties

3. We migrate from `partition_statistics` to `statistics_from_inputs`
for every `ExecutionPlan`.
- `src/distributed_planner/statistics/plan_statistics.rs` can just use
`statistics_from_inputs` directly instead of doing the
`StatisticsWrapper` workaround.

5. Range partitioning is now supported.
- CPU costing now includes range-key comparison cost and has a new unit
test. See src/distributed_planner/
   statistics/complexity_cpu.rs:238.
- I think there's open questions about range partitioning. I've opened
an issue here to make sure it behaves as expected after the upgrade:
datafusion-contrib#628 (comment)

6. Peak-memory metrics use the existing gauge wire representation.

DataFusion added MetricValue::PeakMemoryUsage. It is serialized as the
existing named-gauge protobuf variant to avoid a wire-format change. See
src/protocol/grpc/
   metrics_proto.rs:124.

The value and name survive, and aggregation is still additive, but
decoding produces a generic Gauge, not PeakMemoryUsage. The practical
difference is mainly display formatting: it
may render as a count rather than human-readable bytes. This is the
clearest remaining compromise/risk in the upgrade.

7. File-scan rebalancing changed its discriminator.

DataFusion removed partitioned_by_file_group;
output_partitioning.is_some() is now the source of truth. See
src/events/defaults/file_scan_config.rs:43. This decides whether files
are
round-robin rebalanced or split through FileGroupPartitioner, so it is
behavior-sensitive even though it is a one-line migration.

8. Two previously ignored correctness tests were enabled.
- See `tests/multi_task_collect_join_repros.rs`
- These were upstream DataFusion correctness fixes, not fixes made
locally in this upgrade.

9. drop(reporter) was made explicit on the sampler’s empty-input path.

The reporter sends its result on Drop; explicitly dropping it both
satisfies the new compiler/lint behavior and guarantees the zero-row EOS
report is sent before returning. See src/
   execution_plans/sampler.rs:259.

10. Plan changes

- `dynamic_rg_pruning=eligible` is now displayed on eligible scans:
1,354 occurrences in TPC-DS, 188 in TPC-H, and 12 in ClickBench
- `DataSourceExec` now displays its output partitioning. See
`tests/join.rs` (eventually, someone should delete this test
datafusion-contrib#628)
- Project after sort. This looks like some upstream optimizer rule
change ex. `tests/distributed_unions.rs` and
`tests/distributed_aggregation.rs`.
```
-          │   SortExec: expr=[MinTemp@0 ASC NULLS LAST, RainToday@1 ASC NULLS LAST], preserve_partitioning=[true]
-          │     ProjectionExec: expr=[MaxTemp@0 as MinTemp, RainToday@1 as RainToday]
+          │   ProjectionExec: expr=[MaxTemp@0 as MinTemp, RainToday@1 as RainToday]
+          │     SortExec: expr=[MaxTemp@0 ASC NULLS LAST, RainToday@1 ASC NULLS LAST], preserve_partitioning=[true]
```
- LocalLimitExec became more common: TPC-DS went from 0 to 20
occurrences and ClickBench from 1 to 21, reflecting additional local
limit pushdown.
- Subquery/semi-join plans became more distributed:
    - TPC-DS CollectLeft hash joins: 615 → 610
    - TPC-DS partitioned hash joins: 98 → 103
    - TPC-DS left-semi occurrences: 11 → 25
    - TPC-DS network shuffles: 368 → 378
    - TPC-H - just a few
- These are meaningful topology changes: some subqueries now use
partitioned left-semi joins and therefore introduce hash shuffles
instead of collecting/broadcasting one side.
- Scalar rendering improved, especially decimal literals: internal forms
such as Some(0),7,2 now display as CAST(0.00 AS Decimal128(7, 2)).
- Minor changes (Ex. tpcds 21)
- `__common_expr_4` became `__common_expr_3`; that is only an internal
alias renumbering.
- The projection that renamed `d_date` to `__common_expr_2` disappeared.
- `d_date` is retained directly in the join output and referenced
directly by partial/final aggregates.
  - Column positions changed


- File-group allocation changed substantially
- Some explicit RoundRobinBatch repartitions disappeared and scans
gained different numbers of file groups
- Distribute byte ranges across partitions:
apache/datafusion#22439
- Lowers `repartition_file_min_size` from 10 MiB to 1 MiB. The PR
explicitly calls out TPC-DS SF1 dimension tables. Files may be
duplicated across multiple partitions where but each partition reads a
different byte range (this is hidden by <int>....<int>, but we know from
the correctness tests that nothing broke). A lot of tpcds queries now
split across `target_partitions` instead of staying under-partitioned.
In the `tpcds` plan tests, we use `target_partitions=3`.
Example:
```
-                │     t0: DataSourceExec: file_groups={2 groups: [[/testdata/tpcds/plans_sf1_partitions4/date_dim/part-0.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-1.parquet:<int>..<int>, /testdata/tpcds/plans_sf1_partitions4/date_dim/part-2.parquet:<int>..<int>]]}, projection=[d_date_sk, d_week_seq, d_day_name], file_type=parquet, predicate=DynamicFilter [ empty ]
-                │     t1: DataSourceExec: file_groups={2 groups: [[/testdata/tpcds/plans_sf1_partitions4/date_dim/part-0.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-2.parquet:<int>..<int>]]}, projection=[d_date_sk, d_week_seq, d_day_name], file_type=parquet, predicate=DynamicFilter [ empty ]
-                │     t2: DataSourceExec: file_groups={2 groups: [[/testdata/tpcds/plans_sf1_partitions4/date_dim/part-0.parquet:<int>..<int>, /testdata/tpcds/plans_sf1_partitions4/date_dim/part-1.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-2.parquet:<int>..<int>, /testdata/tpcds/plans_sf1_partitions4/date_dim/part-3.parquet:<int>..<int>]]}, projection=[d_date_sk, d_week_seq, d_day_name], file_type=parquet, predicate=DynamicFilter [ empty ]
-                │     t3: DataSourceExec: file_groups={2 groups: [[/testdata/tpcds/plans_sf1_partitions4/date_dim/part-1.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-3.parquet:<int>..<int>]]}, projection=[d_date_sk, d_week_seq, d_day_name], file_type=parquet, predicate=DynamicFilter [ empty ]
+                │     t0: DataSourceExec: file_groups={3 groups: [[/testdata/tpcds/plans_sf1_partitions4/date_dim/part-0.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-1.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-2.parquet:<int>..<int>]]}, projection=[d_date_sk, d_week_seq, d_day_name], file_type=parquet, predicate=DynamicFilter [ empty ]
+                │     t1: DataSourceExec: file_groups={3 groups: [[/testdata/tpcds/plans_sf1_partitions4/date_dim/part-0.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-1.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-2.parquet:<int>..<int>, /testdata/tpcds/plans_sf1_partitions4/date_dim/part-3.parquet:<int>..<int>]]}, projection=[d_date_sk, d_week_seq, d_day_name], file_type=parquet, predicate=DynamicFilter [ empty ]
+                │     t2: DataSourceExec: file_groups={3 groups: [[/testdata/tpcds/plans_sf1_partitions4/date_dim/part-0.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-1.parquet:<int>..<int>, /testdata/tpcds/plans_sf1_partitions4/date_dim/part-2.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-3.parquet:<int>..<int>]]}, projection=[d_date_sk, d_week_seq, d_day_name], file_type=parquet, predicate=DynamicFilter [ empty ]
+                │     t3: DataSourceExec: file_groups={3 groups: [[/testdata/tpcds/plans_sf1_partitions4/date_dim/part-0.parquet:<int>..<int>, /testdata/tpcds/plans_sf1_partitions4/date_dim/part-1.parquet:<int>..<int>, /testdata/tpcds/plans_sf1_partitions4/date_dim/part-1.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-2.parquet:<int>..<int>, /testdata/tpcds/plans_sf1_partitions4/date_dim/part-2.parquet:<int>..<int>], [/testdata/tpcds/plans_sf1_partitions4/date_dim/part-3.parquet:<int>..<int>]]}, projection=[d_date_sk, d_week_seq, d_day_name], file_type=parquet, predicate=DynamicFilter [ empty ]
```

---------

Co-authored-by: Gabriel <45515538+gabotechs@users.noreply.github.com>
Co-authored-by: Gabriel <gabriel.musatmestre@datadoghq.com>

@gabotechs gabotechs left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It's looking very good! love the new Store and how that generalizes to dyn filtering.

Also, really good choice having this PR shipped first before the other ones.

Left a first round of comments:

Comment thread docs/source/user-guide/05-metrics.md
use datafusion::execution::TaskContext;
use datafusion::physical_plan::ExecutionPlan;
use datafusion::physical_plan::metrics::{ExecutionPlanMetricsSet, MetricsSet};
use datafusion_proto::protobuf::PhysicalExprNode;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The worker protocol should not be coupled to protobuf, this should depend on either vanilla datafusion types or just raw Vec<u8>.

pub struct TaskDynamicFilter {
pub expression_id: u64,
/// A `DynamicFilterPhysicalExpr` proto containing its final predicate and completion state.
pub expression: PhysicalExprNode,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same as the other datafusion related dependencies in this file, this should be an MaybeEncoded<Arc<dyn PhysicalExpr>> instead, and not be coupled to protobuf specifically.

Implementations of the WorkerChannel trait might not want to build a protobuf message here at all, or they might want to just pass an in-memory Arc<dyn PhysicalExpr>.

I think this change should be trivial.

Comment on lines +176 to 178
dynamic_filters = build_task_completed_dynamic_filters(plan, &task_data.task_ctx)
.unwrap_or_default();
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Aren't we swallowing the error here? if you think that's fine, we can just leave a comment explaining why is it fine.

Comment thread src/worker/impl_coordinator_channel.rs Outdated
Comment on lines +51 to +57
let proto = PhysicalPlanNode::try_from_physical_plan_with_converter(
Arc::clone(variant),
&codec,
&converter,
)?;
proto.try_into_physical_plan_with_converter(task_ctx, &codec, &converter)
})

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why do we need this proto roundtrip here?

let mut prepared_execution = self.prepared_execution()?;
prepared_execution.plan_for_viz = Arc::clone(&plan_for_viz);
Ok(Arc::new(Self {
base_plan: plan_for_viz,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This seems to be wrong. The base plan is supposed to be the base plan that came after physical optimization, not the plan meant for visualization.

Comment thread src/coordinator/distributed.rs Outdated
Comment on lines +41 to +44
pub(crate) metrics_store: Option<Arc<MetricsStore>>,
/// Storage for the completed dynamic filters reported by each worker task.
pub(crate) completed_dynamic_filter_store: Arc<CompletedDynamicFilterStore>,
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

In the same way that metrics collection is optional, it'd be also consistent to make the completed dynamic filters optional. If there's people with a reason for not collecting metrics, for that same reason they might also not want to collect dynamic filters.

Comment on lines +20 to +21
pub async fn rewrite_distributed_plan_with_dynamic_filters(
plan: Arc<dyn ExecutionPlan>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why do we need to rewrite the plan here?

I imagine just mutating the dynamic filters in-place should be enough, we don't need to create a new plan.

Comment on lines +36 to +40
pub(super) fn isolate_distributed_leaf_variants_for_display(
plan: Arc<dyn ExecutionPlan>,
task_ctx: &Arc<TaskContext>,
) -> Result<Arc<dyn ExecutionPlan>> {
let codec = DistributedCodec::new_combined_with_user(task_ctx.session_config());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤔 I don't think I understand why we need this function. If there's a DistributedLeafExec present in the plan, it means that the leaf variants where already isolated

@jayshrivastava jayshrivastava changed the title coordinator: display dynamic filters after execution coordinator: display consumer dynamic filters after execution Aug 25, 2026
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.

[dynamic filtering] 2. collect and display dynamic filters in plans

3 participants