Skip to content

refactor: Migrate ScalarSubqueryExpr to self-serialization proto pattern#23130

Merged
adriangb merged 4 commits into
apache:mainfrom
mattp5657:add_proto_conversion_scalar_subquery
Jul 12, 2026
Merged

refactor: Migrate ScalarSubqueryExpr to self-serialization proto pattern#23130
adriangb merged 4 commits into
apache:mainfrom
mattp5657:add_proto_conversion_scalar_subquery

Conversation

@mattp5657

@mattp5657 mattp5657 commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

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_proto in impl PhysicalExpr for ScalarSubqueryExpr (not inherent), #[cfg(feature = "proto")]
  • ScalarSubqueryExpr::try_from_proto wired into from_proto.rs
  • central to_proto.rs + from_proto.rs arms deleted in this PR
  • direct hook test incl. bad-input case; test module at end of file
  • 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

@github-actions github-actions Bot added physical-expr Changes to the physical-expr crates proto Related to proto crate labels Jun 23, 2026
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.
@mattp5657 mattp5657 force-pushed the add_proto_conversion_scalar_subquery branch from 690d82c to 4abb2f5 Compare June 23, 2026 18:06
@mattp5657 mattp5657 marked this pull request as ready for review June 23, 2026 18:13
@adriangb adriangb requested a review from Copilot July 9, 2026 13:17

Copilot AI left a comment

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.

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 from to_proto.rs in favor of the expression’s try_to_proto implementation.
  • Updated from_proto.rs to delegate ExprType::ScalarSubquery decoding to ScalarSubqueryExpr::try_from_proto, while still sourcing ScalarSubqueryResults from the plan decode context.
  • Added direct unit tests in scalar_subquery.rs to exercise the new hooks and error paths under feature = "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.

Comment on lines 62 to 64
pub fn results(&self) -> &ScalarSubqueryResults {
&self.results
}
Comment on lines +135 to +145
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,
},
)),
}))
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

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
     Cloning apache/main
    Building datafusion-physical-expr v54.0.0 (current)
       Built [  36.391s] (current)
     Parsing datafusion-physical-expr v54.0.0 (current)
      Parsed [   0.046s] (current)
    Building datafusion-physical-expr v54.0.0 (baseline)
       Built [  29.129s] (baseline)
     Parsing datafusion-physical-expr v54.0.0 (baseline)
      Parsed [   0.049s] (baseline)
    Checking datafusion-physical-expr v54.0.0 -> v54.0.0 (no change; assume patch)
     Checked [   0.380s] 223 checks: 222 pass, 1 fail, 0 warn, 30 skip

--- failure type_method_marked_deprecated: type method #[deprecated] added ---

Description:
A type method is now #[deprecated]. Downstream crates will get a compiler warning when using this method.
        ref: https://doc.rust-lang.org/reference/attributes/diagnostics.html#the-deprecated-attribute
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.48.0/src/lints/type_method_marked_deprecated.ron

Failed in:
  method datafusion_physical_expr::scalar_subquery::ScalarSubqueryExpr::data_type in /home/runner/work/datafusion/datafusion/datafusion/physical-expr/src/scalar_subquery.rs:70
  method datafusion_physical_expr::scalar_subquery::ScalarSubqueryExpr::nullable in /home/runner/work/datafusion/datafusion/datafusion/physical-expr/src/scalar_subquery.rs:78
  method datafusion_physical_expr::scalar_subquery::ScalarSubqueryExpr::index in /home/runner/work/datafusion/datafusion/datafusion/physical-expr/src/scalar_subquery.rs:87

     Summary semver requires new minor version: 0 major and 1 minor checks failed
    Finished [  66.925s] datafusion-physical-expr
    Building datafusion-proto v54.0.0 (current)
       Built [  60.271s] (current)
     Parsing datafusion-proto v54.0.0 (current)
      Parsed [   0.018s] (current)
    Building datafusion-proto v54.0.0 (baseline)
       Built [  59.832s] (baseline)
     Parsing datafusion-proto v54.0.0 (baseline)
      Parsed [   0.019s] (baseline)
    Checking datafusion-proto v54.0.0 -> v54.0.0 (no change; assume patch)
     Checked [   0.254s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [ 121.589s] datafusion-proto

@github-actions github-actions Bot added the auto detected api change Auto detected API change label Jul 9, 2026

@adriangb adriangb left a comment

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.

Thanks @mattp5657 !!

Comment on lines -62 to -73
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
}

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.

We should deprecate this according to https://datafusion.apache.org/contributor-guide/api-health.html and open a tracking issue to remove them.

@adriangb adriangb Jul 9, 2026

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.

Suggested change
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,

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.

This is just carried over, but it looks like it could be a bug. Could we address it here?

Suggested change
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()
)
})?,

@adriangb

Copy link
Copy Markdown
Contributor

@mattp5657 if you're able to resolve conflicts and address pr review we can get this merged 😄

@mattp5657

Copy link
Copy Markdown
Contributor Author

Yup of course. Just finishing up some other things on my plate then will jump on this.

Appreciate the review!

@mattp5657

Copy link
Copy Markdown
Contributor Author

Should be able to pick it up tomorrow🙂

@mattp5657

mattp5657 commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

@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?

@adriangb adriangb added this pull request to the merge queue Jul 12, 2026
Merged via the queue into apache:main with commit 360a56d Jul 12, 2026
39 checks passed
saadtajwar pushed a commit to saadtajwar/datafusion that referenced this pull request Jul 12, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto detected api change Auto detected API change 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.

3 participants