Skip to content

proto: serialize and dedupe dynamic filters#20416

Open
jayshrivastava wants to merge 3 commits intoapache:mainfrom
jayshrivastava:js/dedupe-dynamic-filter-inner-state
Open

proto: serialize and dedupe dynamic filters#20416
jayshrivastava wants to merge 3 commits intoapache:mainfrom
jayshrivastava:js/dedupe-dynamic-filter-inner-state

Conversation

@jayshrivastava
Copy link
Copy Markdown
Contributor

@jayshrivastava jayshrivastava commented Feb 17, 2026

Which issue does this PR close?

Informs: datafusion-contrib/datafusion-distributed#180
Informs: #21207
Closes: #20418

Rationale for this change

Consider you have a plan with a HashJoinExec and DataSourceExec

HashJoinExec(dynamic_filter_1 on a@0)
  (...left side of join)
  ProjectionExec(a := Column("a", source_index))
    DataSourceExec
      ParquetSource(predicate = dynamic_filter_2)

You serialize the plan, deserialize it, and execute it. What should happen is that the dynamic filter should "work", meaning:

  1. When you deserialize the plan, both the HashJoinExec and DataSourceExec should have pointers to the same DynamicFilterPhysicalExpr
  2. The DynamicFilterPhysicalExpr should be updated during execution by the HashJoinExec and the DataSourceExec should filter out rows

This does not happen today for a few reasons, a couple of which this PR aims to address

  1. DynamicFilterPhysicalExpr is not survive round-tripping. The internal exprs get inlined (ex. it may be serialized as Literal) due to the PhysicalExpr::snapshot() API
  2. Even if DynamicFilterPhysicalExpr survives round-tripping, the one pushed down to the DataSourceExec often has different children. In this case, you have two DynamicFilterPhysicalExpr which
    do not survive deduping, causing referential integrity to be lost.

This PR aims to fix those problems by:

  1. Removing the snapshot() call from the serialization process
  2. Adding protos for DynamicFilterPhysicalExpr so it can be serialized and deserialized
  3. Adding a new concept, a PhysicalExprId, which has two identifiers,
    a "shallow" identifier to indicate two equal expressions which may
    have different children, and an "exact" identifier to indicate two
    exprs that are exactly the same.
  4. Updating the deduping deserializer and protos to now be aware of the
    new "shallow" id, deduping exprs which are the same but have
    different children accordingly.

This change adds tests which roundtrip dynamic filters and assert that
referential integrity is maintained.

Future work:

  1. Serialize dynamic filters in HashJoinExec and other ExecutionPlans which produce dynamic filters
  2. Add tests which actually execute plans after deserialization and assert that dynamic filtering is functional
  3. Add proto converters to the PhysicalExtensionCodec trait so implementors can utilize deduping logic

Are these changes tested?

Yes. This change adds tests which roundtrip dynamic filters and assert that referential integrity is maintained.

Are there any user-facing changes?

  • The default codec does not call snapshot() on PhysicalExpr during serialization anymore. This means that DynamicFilterPhysicalExpr are now serialized and deserialized without snapshotting

@github-actions github-actions Bot added physical-expr Changes to the physical-expr crates proto Related to proto crate labels Feb 17, 2026
@jayshrivastava jayshrivastava changed the title wip proto: serialize dynamic filters Feb 18, 2026
@jayshrivastava jayshrivastava changed the title proto: serialize dynamic filters proto: serialize and dedupe dynamic filters Feb 18, 2026
@jayshrivastava jayshrivastava force-pushed the js/dedupe-dynamic-filter-inner-state branch from 158f2cf to e0c6be3 Compare February 18, 2026 00:18
Copy link
Copy Markdown
Contributor

@NGA-TRAN NGA-TRAN left a comment

Choose a reason for hiding this comment

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

Glad you tracked down the root cause. Great explanation of the two issue.
The changes align well with your description, and the tests look solid. I’ll let the folks who know the Dynamic Filtering details comment further on the specifics.


/// An atomic snapshot of a [`DynamicFilterPhysicalExpr`] used to reconstruct the expression during
/// serialization / deserialization.
pub struct DynamicFilterSnapshot {
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.

Added this type because I thought it looked cleaner / simpler than having getters on the DynamicFilterPhysicalExpr itself.

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.

Maybe we should just add the getters and setters for the internal fields...

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 seems like we end up exposing all of the internals anyway: if there was a way to return a dyn Serializable it'd be one thing, but the proto machinery still needs to know all of the fields to serialize.

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 also think the name snapshot makes it sounds like this is what would be returned from PhysicalExpr::snapshot, but that is not the case.

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.

Could be... you kind of have already this, just to an intermediate structure instead

Copy link
Copy Markdown
Contributor

@gene-bordegaray gene-bordegaray left a comment

Choose a reason for hiding this comment

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

overall looks good, had one idea as to making the api more familiar to those new with it. Let me know what you think

Comment thread datafusion/physical-expr/src/expressions/dynamic_filters.rs Outdated
Comment thread datafusion/physical-expr/src/expressions/dynamic_filters.rs Outdated
Comment thread datafusion/physical-expr/src/expressions/dynamic_filters.rs Outdated
@jayshrivastava jayshrivastava force-pushed the js/dedupe-dynamic-filter-inner-state branch from 5d99a1f to 68e3f72 Compare February 19, 2026 21:01
Comment thread datafusion/physical-expr/src/expressions/dynamic_filters.rs Outdated
Comment thread datafusion/physical-expr/src/expressions/dynamic_filters.rs Outdated
Comment thread datafusion/physical-expr/src/expressions/dynamic_filters.rs
Comment thread datafusion/physical-expr/src/expressions/dynamic_filters.rs
Comment thread datafusion/physical-expr/src/expressions/dynamic_filters.rs Outdated
@jayshrivastava jayshrivastava marked this pull request as ready for review February 23, 2026 15:03
Comment thread datafusion/proto/src/physical_plan/mod.rs
Copy link
Copy Markdown
Contributor

@gabotechs gabotechs left a comment

Choose a reason for hiding this comment

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

Overall, I think we should find alternative ways of doing this that do not imply special-casing the dynamic filter serialization process.

I shared some ideas on how to do this, but they probably need to be fleshed out a bit more.

The things that I think this PR should be achieving is:

  • No special casing for dynamic filters in serialization/deserialization code
  • No changes to global protobuf messages just for the sake of dynamic filters, just a new normal entry in the enum PhysicalDynamicFilterNode

Things that IMO we should have, probably in a separate PR:

  • Stop playing with raw pointer addresses in dynamic filters and assign proper unique identifiers, probably using the uuid crate.

Comment thread datafusion/physical-expr/src/expressions/dynamic_filters.rs Outdated
Comment thread datafusion/proto/src/physical_plan/mod.rs Outdated
Comment thread datafusion/proto/src/physical_plan/to_proto.rs Outdated
Comment thread datafusion/physical-expr/src/expressions/dynamic_filters.rs Outdated
Comment thread datafusion/proto/proto/datafusion.proto Outdated
@jayshrivastava jayshrivastava force-pushed the js/dedupe-dynamic-filter-inner-state branch from c5d0e2f to fef4259 Compare February 26, 2026 18:48
jayshrivastava added a commit to jayshrivastava/datafusion that referenced this pull request Feb 26, 2026
Fixups for the cherry-picked commits from PRs apache#19437, apache#20037, apache#20416,
and #2 to work with branch-52's partition-index APIs:

- Update remap_children callers to use instance method signature
- Adapt DynamicFilterUpdate::Global enum for new code paths
- Add missing partitioned_exprs/runtime_partition fields to new constructors
- Remove null_aware field (not on branch-52)
- Replace FilterExecBuilder with FilterExec::try_new
- Remove non-compiling tests that depend on upstream-only APIs
- Fix duplicate imports in roundtrip test file

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
gene-bordegaray pushed a commit to DataDog/datafusion that referenced this pull request Feb 26, 2026
Fixups for the cherry-picked commits from PRs apache#19437, apache#20037, apache#20416,
and jayshrivastava#2 to work with branch-52's partition-index APIs:

- Update remap_children callers to use instance method signature
- Adapt DynamicFilterUpdate::Global enum for new code paths
- Add missing partitioned_exprs/runtime_partition fields to new constructors
- Remove null_aware field (not on branch-52)
- Replace FilterExecBuilder with FilterExec::try_new
- Remove non-compiling tests that depend on upstream-only APIs
- Fix duplicate imports in roundtrip test file

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread datafusion/physical-expr/src/expressions/dynamic_filters.rs Outdated
@jayshrivastava jayshrivastava force-pushed the js/dedupe-dynamic-filter-inner-state branch from cb23b01 to 18b0289 Compare March 19, 2026 15:04
@jayshrivastava jayshrivastava force-pushed the js/dedupe-dynamic-filter-inner-state branch 3 times, most recently from d75e7f8 to e0ec773 Compare April 14, 2026 17:21
@jayshrivastava jayshrivastava force-pushed the js/dedupe-dynamic-filter-inner-state branch from d64c33d to b419d4c Compare April 22, 2026 02:32
Informs: datafusion-contrib/datafusion-distributed#180
Closes: apache#20418

Consider you have a plan with a `HashJoinExec` and `DataSourceExec`
```
HashJoinExec(dynamic_filter_1 on a@0)
  (...left side of join)
  ProjectionExec(a := Column("a", source_index))
    DataSourceExec
      ParquetSource(predicate = dynamic_filter_2)
```

You serialize the plan, deserialize it, and execute it. What should happen is that the dynamic filter should "work", meaning:
1. When you deserialize the plan, both the `HashJoinExec` and `DataSourceExec` should have pointers to the same `DynamicFilterPhysicalExpr`
2. The `DynamicFilterPhysicalExpr` should be updated during execution by the `HashJoinExec`  and the `DataSourceExec` should filter out rows

This does not happen today for a few reasons, a couple of which this PR aims to address
1. `DynamicFilterPhysicalExpr` is not survive round-tripping. The internal exprs get inlined (ex. it may be serialized as `Literal`) due to the `PhysicalExpr::snapshot()` API
2. Even if `DynamicFilterPhysicalExpr` survives round-tripping, the one pushed down to the `DataSourceExec` often has different children. In this case, you have two `DynamicFilterPhysicalExpr` which
do not survive deduping, causing referential integrity to be lost.

This PR aims to fix those problems by:
1. Removing the `snapshot()` call from the serialization process
2. Adding protos for `DynamicFilterPhysicalExpr` so it can be serialized and deserialized
3. Adding a new concept, a `PhysicalExprId`, which has two identifiers,
   a "shallow" identifier to indicate two equal expressions which may
   have different children, and an "exact" identifier to indicate two
   exprs that are exactly the same.
4. Updating the deduping deserializer and protos to now be aware of the
   new "shallow" id, deduping exprs which are the same but have
   different children accordingly.

This change adds tests which roundtrip dynamic filters and assert that
referential integrity is maintained.
@jayshrivastava jayshrivastava force-pushed the js/dedupe-dynamic-filter-inner-state branch from b419d4c to dc683d3 Compare April 22, 2026 03:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

physical-expr Changes to the physical-expr crates proto Related to proto crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Serialize dynamic filters across network boundaries

6 participants