You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Split proto serialization to encapsulate private state (#21835) (#21929)
## Which issue does this PR close?
- Closes#21835.
## Rationale for this change
`datafusion-proto` serializes every built-in `PhysicalExpr` through a
single
~300-line `downcast_ref` chain, with a mirror `match` on the decode
side. That
chain lives outside the crate where each expression is defined, so every
field
an expression wants to round-trip has to be made `pub`. #21807 is the
cautionary tale: it had to add five `pub` "proto-only, not stable" items
to
`DynamicFilterPhysicalExpr` just to serialize an `RwLock`-wrapped inner.
This PR adds the infrastructure so a `PhysicalExpr` can serialize itself
and
keep its state private.
## What changes are included in this PR?
A `PhysicalExpr` can now opt into serializing itself, in both
directions:
```rust
fn try_to_proto(&self, ctx: &PhysicalExprEncodeCtx) -> Result<Option<PhysicalExprNode>>
fn try_from_proto(node: &PhysicalExprNode, ctx: &PhysicalExprDecodeCtx) -> Result<Arc<dyn PhysicalExpr>>
```
`try_to_proto` returning `Ok(None)` (the default) means "fall through to
the
old downcast chain", so the change is purely additive — nothing is
forced to
migrate.
`Column` and `BinaryExpr` are migrated as working demos; everything
else stays on the old path and migrates later, one expression at a time,
with
no wire-format change.
Five stacked commits, each builds green on its own and is independently
reviewable (or splittable into its own PR):
1. **Extract `datafusion-proto-models` crate** — move the `.proto` file
and
prost-generated types into a lightweight crate (mirrors the existing
`datafusion-proto-common` split).
2. **Add the `try_to_proto` hook** — feature-gated, off by default.
3. **Migrate `Column` encode.**
4. **Add the decode side and migrate `Column` decode.**
5. **Migrate `BinaryExpr`** (both directions).
## A few design decisions worth flagging
- **`FromProto` / `TryFromProto` traits instead of plain `From` /
`TryFrom`.**
Once the prost types move into their own crate they are *foreign* to
`datafusion-proto`, and the orphan rule forbids `impl From<&protobuf::X>
for Y`
when both `X` and `Y` are foreign. So those conversions become
`FromProto` /
`TryFromProto` traits in `datafusion_proto::convert`, and callers go
from
`(&x).into()` to `Y::from_proto(&x)`. This is a known workaround, not
the end
state — see Future work.
- **The ctx is a concrete struct, not `&dyn`.** `PhysicalExprEncodeCtx`
/
`PhysicalExprDecodeCtx` wrap a sealed dispatch trait. Keeping them
concrete
keeps `&dyn` out of every expression's signature and gives a stable
place to
add helpers (UDF encoding, registry hooks) later without churning a
public
trait.
- **`try_from_proto` takes the whole `PhysicalExprNode`**, not the
pre-unwrapped variant payload, so every expression's decoder has the
same
signature and can still see outer-node fields like `expr_id`.
## Are these changes tested?
No new behavior, so no new tests. `Column` and `BinaryExpr` produce and
consume
the same wire format as before; the existing `roundtrip_physical_plan` /
`roundtrip_physical_expr` tests already cover both directions and now
exercise
the new path.
## Are there any user-facing changes?
Small API breaks in `datafusion-proto`:
- `try_from_physical_plan_with_converter` /
`try_into_physical_plan_with_converter`
move to a `PhysicalPlanNodeExt` trait — callers add
`use datafusion_proto::physical_plan::PhysicalPlanNodeExt;`.
- Foreign-foreign `From` / `TryFrom` conversions become `FromProto` /
`TryFromProto` (see Design decisions above).
- `datafusion_proto::generated::*` is deprecated in favor of
`datafusion_proto::protobuf`; it still works.
The new `proto` feature on `datafusion-physical-expr(-common)` is off by
default, so crates that don't serialize plans pay nothing.
## Future work
- Migrate the remaining built-in expressions — including
`DynamicFilterPhysicalExpr`, the original motivation — one per follow-up
PR.
- Apply the same pattern to `ExecutionPlan` serialization.
- Drop the `FromProto` / `TryFromProto` workaround: collapse
`datafusion-proto-common` into `datafusion-proto-models` and push the
conversion impls down to the target-type crates so callers use plain
`From` /
`TryFrom` again. Full dep-graph analysis and a step-by-step plan are in
[#21835
(comment)](#21835 (comment)).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0 commit comments