Skip to content

Support transparent ExecutionPlan downcasts#22559

Open
geoffreyclaude wants to merge 1 commit into
apache:mainfrom
geoffreyclaude:fix/transparent-plan-downcast
Open

Support transparent ExecutionPlan downcasts#22559
geoffreyclaude wants to merge 1 commit into
apache:mainfrom
geoffreyclaude:fix/transparent-plan-downcast

Conversation

@geoffreyclaude
Copy link
Copy Markdown
Contributor

@geoffreyclaude geoffreyclaude commented May 27, 2026

Which issue does this PR close?

Rationale for this change

DataFusion 54 changed ExecutionPlan downcasting to use the Any supertrait directly. That removes ExecutionPlan::as_any, which had also served as a customization point for wrapper nodes: wrappers could identify as themselves internally while exposing the wrapped plan type to normal downcast-based inspection.

This draft PR proposes one possible fix: add an explicit ExecutionPlan::downcast_delegate() hook for wrapper nodes that want their public ExecutionPlan downcast identity to be delegated to another plan.

The proposed behavior intentionally preserves the old as_any override semantics: when a node opts into downcast delegation, intermediate delegating wrappers are invisible to dyn ExecutionPlan::is::<T>() and downcast_ref::<T>(). A different "dual identity" model, where wrappers can downcast both as themselves and as their delegates, may also be useful, but that would be a new behavior rather than a compatibility fix.

This is only a suggested implementation for the linked issue. Other solution proposals, including different API names or a different shape for the hook, are very welcome.

What changes are included in this PR?

  • Adds ExecutionPlan::downcast_delegate() with a default implementation returning None.
  • Updates dyn ExecutionPlan::is::<T>() and downcast_ref::<T>() to delegate to downcast_delegate() when present, otherwise use the current concrete plan type.
  • Documents that downcast_delegate() is only for type introspection and is independent from children() / plan traversal.
  • Adds tests for direct and nested downcast-delegating wrappers, including that intermediate delegating wrappers remain invisible to normal downcast-based inspection.

An alternative API shape would make every plan expose an explicit downcast target. A concrete spelling could make the self-target case explicit:

enum DowncastTarget<'a> {
    SelfTarget,
    Plan(&'a dyn ExecutionPlan),
}

fn downcast_target(&self) -> DowncastTarget<'_> {
    DowncastTarget::SelfTarget
}

pub fn downcast_ref<T: ExecutionPlan>(&self) -> Option<&T> {
    match self.downcast_target() {
        DowncastTarget::Plan(target) => target.downcast_ref::<T>(),
        DowncastTarget::SelfTarget => (self as &dyn Any).downcast_ref(),
    }
}

That frames every plan as having a public downcast target, which is close to the old as_any mental model. A simpler conceptual version would be fn downcast_target(&self) -> &dyn ExecutionPlan with the default target being self, but the real helper still needs an explicit self-target base case to avoid recursing forever. This PR keeps the primary proposal as an explicit Option-based opt-in.

Are these changes tested?

Yes.

  • cargo test -p datafusion-physical-plan execution_plan_downcast
  • cargo test -p datafusion-physical-plan --lib
  • cargo fmt --all -- --check
  • git diff --check

Are there any user-facing changes?

Yes. This adds a new public ExecutionPlan trait method with a default implementation, and it changes ExecutionPlan downcast helpers to honor wrappers that explicitly opt into delegating public downcast identity.

@github-actions github-actions Bot added the physical-plan Changes to the physical-plan crate label May 27, 2026
@geoffreyclaude geoffreyclaude force-pushed the fix/transparent-plan-downcast branch 4 times, most recently from 2128754 to cae7bf1 Compare May 27, 2026 11:46
@timsaucer timsaucer self-requested a review May 27, 2026 11:47
@timsaucer
Copy link
Copy Markdown
Member

I'll review as soon as it's marked ready, but at a high level looks good so far

@geoffreyclaude geoffreyclaude force-pushed the fix/transparent-plan-downcast branch 2 times, most recently from 259871e to 6563792 Compare May 27, 2026 12:18
@geoffreyclaude geoffreyclaude marked this pull request as ready for review May 27, 2026 12:25
@geoffreyclaude geoffreyclaude force-pushed the fix/transparent-plan-downcast branch from 6563792 to fc8f083 Compare May 27, 2026 12:47
@geoffreyclaude
Copy link
Copy Markdown
Contributor Author

I'll review as soon as it's marked ready, but at a high level looks good so far

@timsaucer Should be ready for review now. I just renamed the API to downcast_delegate which seemed clearer to me.

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.

👍 Even if it's not super pretty to have that extra method there, it gets the job done without introducing any breaking API change, and it's landed in a way that only people caring about implementing wrapper types need to care about this, without getting in the way of everyone else, so +1 on my side.

Before moving forward, let's wait for at least @timsaucer to give his opinion here.

Comment thread datafusion/physical-plan/src/execution_plan.rs Outdated
Comment on lines +747 to +750
match self.downcast_delegate() {
Some(delegate) => delegate.is::<T>(),
None => (self as &dyn Any).is::<T>(),
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Won't this prevent you from downcasting to the wrapper type?

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.

Yes, and this is intentional to mimic the old as_any behavior. I've updated the PR description to be clear about this choice.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It seems like the earlier PR would still work for all use cases, right? Because you're always choosing to downcast to a specific type, it seems like it would enable downcasting to either (or many, depending on levels) type.

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.

The previous PR would stop on the first match, rather than go all the way to the leaf child. I agree this is being pedantic, as "real world" cases would probably never have a case where you'd need to match the leaf child but get interrupted by an intermediate node in a "wrapper chain". However, I still find the current implementation clearer and easier to reason about: the leaf node is what is returned.

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.

looks good

I think this is feasible and good as we are gonna get 👍

Comment thread datafusion/physical-plan/src/execution_plan.rs Outdated
@geoffreyclaude geoffreyclaude force-pushed the fix/transparent-plan-downcast branch from fc8f083 to 41a9861 Compare May 27, 2026 13:20
@timsaucer
Copy link
Copy Markdown
Member

Do we need to also open a PR to target branch-54?

@geoffreyclaude
Copy link
Copy Markdown
Contributor Author

geoffreyclaude commented May 27, 2026

Do we need to also open a PR to target branch-54?

@timsaucer I opened #22565 on branch-54, which is a clean cherry-pick of this PR's single commit. I suggest we give both at least 24 hours grace before merging, so others can chime in if needed.

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

Labels

physical-plan Changes to the physical-plan crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ExecutionPlan downcasting no longer supports transparent wrapper nodes

4 participants