adriangb opened a new issue, #24625:
URL: https://github.com/apache/datafusion/issues/24625

   ## Is your feature request related to a problem or challenge?
   
   #23494 moved built-in `ExecutionPlan` serialization onto per-type 
`try_to_proto` / `try_from_proto` hooks. The result is genuinely nicer to 
implement than the central `downcast_ref` chain: encode and decode live next to 
the plan, and private state stays private.
   
   Third-party plans can't fully use it. `ExecutionPlan::try_to_proto` is 
callable by them, but there is no way back — decoding an extension node still 
routes through `PhysicalExtensionCodec::try_decode`.
   
   The blocker is the wire type:
   
   ```rust
   pub struct PhysicalExtensionNode {
       node: Vec<u8>,
       inputs: Vec<PhysicalPlanNode>,
   }
   ```
   
   There is no type discriminator, so **the codec *is* the discriminator**. 
That's why `ComposedPhysicalExtensionCodec` has to try each registered codec in 
sequence and treat a decode error as "not mine". Resolution depends on 
registration order and on error strings, and a name collision between two 
independent crates' codecs is undetectable.
   
   ## Describe the solution you'd like
   
   Add an optional name to the wire type plus a per-type decoder registry:
   
   1. `optional string plan_name = 3;` on `PhysicalExtensionNode` — additive 
and proto3-compatible: old writers omit it, old readers ignore it.
   2. A session-scoped registry mapping that name to a decoder fn.
   3. Decode rule: name present and registered → registry; otherwise → the 
existing codec chain, unchanged.
   
   Per-plan opt-in with a codec fallback, which is the same migration shape 
#23494 already used for encode.
   
   Three things make this cheaper than it sounds:
   
   - **The decoder signature already exists and is uniform.** `try_from_proto` 
is a plain inherent fn, not a trait method, so there's no `Self`-return 
object-safety problem:
   
     ```rust
     pub fn try_from_proto(
         node: &PhysicalPlanNode,
         ctx: &ExecutionPlanDecodeCtx<'_>,
     ) -> Result<Arc<dyn ExecutionPlan>>
     ```
   
     The registry is `HashMap<String, fn(&PhysicalPlanNode, 
&ExecutionPlanDecodeCtx) -> Result<Arc<dyn ExecutionPlan>>>`.
   
   - **`ExecutionPlanDecodeCtx` already exposes `task_ctx()`**, so 
session-dependent extension plans can decode through this path, not just 
self-contained ones.
   
   - **It mirrors a policy already in the codebase.** UDF decode already 
implements "payload → codec; else registry → codec fallback" 
(`datafusion/proto/src/physical_plan/mod.rs`). This is the same policy one 
layer up.
   
   The encode side needs one helper — something like 
`ctx.encode_extension(name, bytes, children)` — otherwise every extension 
author hand-rolls the `Extension` wrapper and some will forget to recurse into 
`inputs`.
   
   Registration would look like `register_execution_plan::<MyExec>()` on the 
session, sitting alongside the existing `FunctionRegistry`.
   
   ## Worked example: datafusion-distributed
   
   
[datafusion-distributed](https://github.com/datafusion-contrib/datafusion-distributed)
 ships six extension plans and serializes every query plan across the network, 
so it exercises this path hard. Concretely, on DataFusion 55:
   
   - **`src/codec/distributed_codec.rs` is 833 lines.** It holds six 
`downcast_ref` arms in `try_encode` and a matching six-arm `match` in 
`try_decode`, for `NetworkShuffleExec`, `NetworkCoalesceExec`, 
`NetworkBroadcastExec`, `BroadcastExec`, `ChildrenIsolatorUnionExec` and 
`SamplerExec`. This is exactly the central dispatch chain #23494 set out to 
remove, just re-created downstream — every extension project rebuilds it.
   
   - **`src/codec/user_codec.rs` is 33 lines that exist purely for 
composition.** Its only job is accumulating a `Vec<Arc<dyn 
PhysicalExtensionCodec>>` into a `ComposedPhysicalExtensionCodec` so the 
library's own codec and the end user's codec can coexist. It exists only 
because codecs don't compose by name. A name-keyed registry deletes the file 
and makes collisions detectable at registration instead of resolving by 
ordering.
   
   - **The public API gets smaller.** 
`DistributedExt::with_distributed_user_codec(MyCodec)` — which users must 
remember to call on *both* the coordinator and every worker — becomes 
`register_execution_plan::<MyExec>()`, with no separate codec type to author at 
all.
   
   - **The docs get shorter.** The project's "distributing custom 
ExecutionPlans" guide spends the first of its three sections teaching users to 
write a `PhysicalExtensionCodec` before they can distribute anything.
   
   Two details from that project worth noting as design validation:
   
   - Its plans are **session-dependent**: `NetworkShuffleExec` reconstructs a 
worker connection pool out of the `TaskContext` at decode time. 
`ExecutionPlanDecodeCtx::task_ctx()` already covers this, which is good 
evidence the registry path is viable for real-world extension plans.
   
   - It implements **no expr or UDF codec methods at all**, so a plan-only 
registry would take it completely off `PhysicalExtensionCodec` — no partial 
migration, no keeping a codec around for the leftovers.
   
   ## Describe alternatives you've considered
   
   - **Keep codecs and migrate only encode to `try_to_proto`.** This is 
possible today, but it splits encode and decode across different files for the 
same plan while keeping the registration burden identical. Strictly worse than 
either endpoint.
   
   - **A global static registry.** Rejected: session-scoped matches the 
`FunctionRegistry` precedent and stays testable and multi-tenant-safe.
   
   ## Additional context
   
   Registered names should probably be namespaced (e.g. 
`datafusion-distributed.NetworkShuffleExec`) so collisions surface at 
registration rather than as a mis-decode.
   
   This does **not** retire `PhysicalExtensionCodec` — extension 
`PhysicalExpr`s and UDF payloads still need it, and the codec fallback stays 
for unmigrated plans regardless. Companion issue for the expression side: 
PLACEHOLDER_EXPR_ISSUE
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to