refactor: Migrate ScalarSubqueryExpr to self-serialization proto pattern#23130
Conversation
Move ScalarSubqueryExpr proto encode/decode into the expression itself via the try_to_proto / try_from_proto hooks, removing the centralized downcast branch in to_proto.rs and the inline construction in from_proto.rs. Because the ScalarSubqueryResults container is runtime-only shared state and not part of the wire format, from_proto.rs still fetches it from the decode context and threads it into try_from_proto. Follows the pattern established in apache#21929.
690d82c to
4abb2f5
Compare
There was a problem hiding this comment.
Pull request overview
Migrates ScalarSubqueryExpr protobuf (de)serialization to the self-serialization hooks (try_to_proto / try_from_proto), removing the centralized encode/decode branches in datafusion-proto and keeping runtime-only ScalarSubqueryResults threaded via the decode context.
Changes:
- Removed
ScalarSubqueryExpr’s bespoke downcast-based encoding fromto_proto.rsin favor of the expression’stry_to_protoimplementation. - Updated
from_proto.rsto delegateExprType::ScalarSubquerydecoding toScalarSubqueryExpr::try_from_proto, while still sourcingScalarSubqueryResultsfrom the plan decode context. - Added direct unit tests in
scalar_subquery.rsto exercise the new hooks and error paths underfeature = "proto".
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| datafusion/proto/src/physical_plan/to_proto.rs | Removes the dedicated ScalarSubquery downcast arm so encoding relies on try_to_proto. |
| datafusion/proto/src/physical_plan/from_proto.rs | Delegates ScalarSubquery decoding to ScalarSubqueryExpr::try_from_proto, threading shared results from decode context. |
| datafusion/physical-expr/src/scalar_subquery.rs | Implements try_to_proto / try_from_proto, removes proto-only accessors, and adds hook-focused tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| pub fn results(&self) -> &ScalarSubqueryResults { | ||
| &self.results | ||
| } |
| use datafusion_proto_models::protobuf; | ||
| Ok(Some(protobuf::PhysicalExprNode { | ||
| expr_id: None, | ||
| expr_type: Some(protobuf::physical_expr_node::ExprType::ScalarSubquery( | ||
| protobuf::PhysicalScalarSubqueryExprNode { | ||
| data_type: Some((&self.data_type).try_into()?), | ||
| nullable: self.nullable, | ||
| index: self.index.as_usize() as u32, | ||
| }, | ||
| )), | ||
| })) |
|
Thank you for opening this pull request! Reviewer note: cargo-semver-checks reported the current version number is not SemVer-compatible with the changes in this pull request (compared against the base branch). Details |
| pub fn data_type(&self) -> &DataType { | ||
| &self.data_type | ||
| } | ||
|
|
||
| pub fn nullable(&self) -> bool { | ||
| self.nullable | ||
| } | ||
|
|
||
| /// Returns the index of this subquery in the shared results container. | ||
| pub fn index(&self) -> SubqueryIndex { | ||
| self.index | ||
| } |
There was a problem hiding this comment.
We should deprecate this according to https://datafusion.apache.org/contributor-guide/api-health.html and open a tracking issue to remove them.
There was a problem hiding this comment.
| pub fn data_type(&self) -> &DataType { | |
| &self.data_type | |
| } | |
| pub fn nullable(&self) -> bool { | |
| self.nullable | |
| } | |
| /// Returns the index of this subquery in the shared results container. | |
| pub fn index(&self) -> SubqueryIndex { | |
| self.index | |
| } | |
| #[deprecated( | |
| since = "55.0.0", | |
| note = "was only used for proto serialization, which no longer needs it; use `return_field` for type/nullability. It will be removed in 61.0.0 or 6 months after 55.0.0 is released, whichever is longer." | |
| )] | |
| pub fn data_type(&self) -> &DataType { | |
| &self.data_type | |
| } | |
| #[deprecated( | |
| since = "55.0.0", | |
| note = "was only used for protger needs it; use `return_field` fortype/nullability. It will be removed in 61.0.0 or 6 months after 55.0.0 is released, whichever is longer." | |
| )] | |
| pub fn nullable(&self) -> bool { | |
| self.nullable | |
| } | |
| /// Returns the index of this subquery in the shared results container. | |
| #[deprecated( | |
| since = "55.0.0", | |
| note = "was only used for protger needs it. It will be removed in61.0.0 or 6 months after 55.0.0 is released, whichever is longer." | |
| )] | |
| pub fn index(&self) -> SubqueryInd | |
| self.index | |
| } |
| protobuf::PhysicalScalarSubqueryExprNode { | ||
| data_type: Some((&self.data_type).try_into()?), | ||
| nullable: self.nullable, | ||
| index: self.index.as_usize() as u32, |
There was a problem hiding this comment.
This is just carried over, but it looks like it could be a bug. Could we address it here?
| index: self.index.as_usize() as u32, | |
| index: u32::try_from(self.index.as_usize()).map_err(|_| { | |
| internal_datafusion_err!( | |
| "scalar subquery index {} does not fit in u32", | |
| self.index.as_usize() | |
| ) | |
| })?, |
|
@mattp5657 if you're able to resolve conflicts and address pr review we can get this merged 😄 |
|
Yup of course. Just finishing up some other things on my plate then will jump on this. Appreciate the review! |
|
Should be able to pick it up tomorrow🙂 |
# Conflicts: # datafusion/proto/src/physical_plan/from_proto.rs # datafusion/proto/src/physical_plan/to_proto.rs
|
@adriangb Should be ready whenever you have a chance, thanks again for all the help on this :). Let me know if any other comments or changes? |
…ern (apache#23130) Move ScalarSubqueryExpr proto encode/decode into the expression itself via the try_to_proto / try_from_proto hooks, removing the centralized downcast branch in to_proto.rs and the inline construction in from_proto.rs. Because the ScalarSubqueryResults container is runtime-only shared state and not part of the wire format, from_proto.rs still fetches it from the decode context and threads it into try_from_proto. Follows the pattern established in apache#21929. ## Which issue does this PR close? - Closes #[22433](apache#22433). ## Rationale for this change datafusion-proto serializes ScalarSubqueryExpr by downcasting in to_proto.rs and rebuilding it inline in from_proto.rs, which forces its internal fields to be exposed as public accessors just for the proto crate. PR #[21929](apache#21929) introduced a self-serialization pattern (try_to_proto / try_from_proto) where each expression owns its own wire format and the central dispatch just delegates. Other expressions like InListExpr and DynamicFilterPhysicalExpr already use it. This change moves ScalarSubqueryExpr to the same pattern, removing its bespoke proto branches and the proto-only public accessors. One wrinkle: ScalarSubqueryExpr's ScalarSubqueryResults is runtime-only shared state, not part of the wire format, so try_from_proto takes it as an extra argument that from_proto.rs threads in from the decode context. ## What changes are included in this PR? - [x] `try_to_proto` in `impl PhysicalExpr for ScalarSubqueryExpr` (not inherent), `#[cfg(feature = "proto")]` - [x] `ScalarSubqueryExpr::try_from_proto` wired into `from_proto.rs` - [x] central `to_proto.rs` + `from_proto.rs` arms deleted in this PR - [x] direct hook test incl. bad-input case; test module at end of file - [x] roundtrip tests pass; fmt + clippy `-D warnings` clean; PR template filled in ## Are these changes tested? - **Unit tests** (`scalar_subquery.rs`, `#[cfg(all(test, feature = "proto"))]` module at end of file) exercise the hooks directly: - `round_trips_through_proto` — encode via `try_to_proto`, decode via `try_from_proto`, asserting `data_type`/`nullable`/`index` and shared-results identity survive. - `rejects_non_scalar_subquery_node` — wrong `ExprType` variant errors cleanly. - `rejects_missing_data_type` — missing required field errors cleanly. - **Round-trip integration tests** (`datafusion-proto`, `--all-features`) cover the full plan path: `roundtrip_scalar_subquery_exec`, `roundtrip_nested_scalar_subquery_exec_scopes_results`, and `roundtrip_scalar_subquery_exec_with_default_converter_executes`. All pass, and `cargo fmt --all` + `cargo clippy --all-features --all-targets -- -D warnings` are clean. ## Are there any user-facing changes? No --------- Co-authored-by: Matthew Patton <matthewpatton@macbookpro.mynetworksettings.com>
Move ScalarSubqueryExpr proto encode/decode into the expression itself via the try_to_proto / try_from_proto hooks, removing the centralized downcast branch in to_proto.rs and the inline construction in from_proto.rs. Because the ScalarSubqueryResults container is runtime-only shared state and not part of the wire format, from_proto.rs still fetches it from the decode context and threads it into try_from_proto.
Follows the pattern established in #21929.
Which issue does this PR close?
Rationale for this change
datafusion-proto serializes ScalarSubqueryExpr by downcasting in to_proto.rs and rebuilding it inline in from_proto.rs, which forces its internal fields to be exposed as public accessors just for the proto crate.
PR #21929 introduced a self-serialization pattern (try_to_proto / try_from_proto) where each expression owns its own wire format and the central dispatch just delegates. Other expressions like InListExpr and DynamicFilterPhysicalExpr already use it. This change moves ScalarSubqueryExpr to the same pattern, removing its bespoke proto branches and the proto-only public accessors.
One wrinkle: ScalarSubqueryExpr's ScalarSubqueryResults is runtime-only shared state, not part of the wire format, so try_from_proto takes it as an extra argument that from_proto.rs threads in from the decode context.
What changes are included in this PR?
try_to_protoinimpl PhysicalExpr for ScalarSubqueryExpr(not inherent),#[cfg(feature = "proto")]ScalarSubqueryExpr::try_from_protowired intofrom_proto.rsto_proto.rs+from_proto.rsarms deleted in this PR-D warningsclean; PR template filled inAre these changes tested?
scalar_subquery.rs,#[cfg(all(test, feature = "proto"))]module at end of file) exercise the hooks directly:round_trips_through_proto— encode viatry_to_proto, decode viatry_from_proto, assertingdata_type/nullable/indexand shared-results identity survive.rejects_non_scalar_subquery_node— wrongExprTypevariant errors cleanly.rejects_missing_data_type— missing required field errors cleanly.datafusion-proto,--all-features) cover the full plan path:roundtrip_scalar_subquery_exec,roundtrip_nested_scalar_subquery_exec_scopes_results, androundtrip_scalar_subquery_exec_with_default_converter_executes.All pass, and
cargo fmt --all+cargo clippy --all-features --all-targets -- -D warningsare clean.Are there any user-facing changes?
No