Skip to content

refactor(proto): put Partitioning / sort-expression serde on the types - #24003

Open
adriangb wants to merge 3 commits into
apache:mainfrom
pydantic:prep/partitioning-proto-on-types
Open

refactor(proto): put Partitioning / sort-expression serde on the types#24003
adriangb wants to merge 3 commits into
apache:mainfrom
pydantic:prep/partitioning-proto-on-types

Conversation

@adriangb

@adriangb adriangb commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

Partitioning's protobuf conversion currently exists in three places:

  • inline in RepartitionExec::try_to_proto,
  • inline in RepartitionExec::try_from_proto,
  • in datafusion-proto's serialize_partitioning / parse_protobuf_partitioning.

Every plan or data source migrated to the hooks that has an output
partitioning has so far copied it again — #23683 is about to add a fourth
copy for FileScanConfig.

The flat PhysicalSortExprNode encoding is the same story, one level down and
more widespread: AggregateExec's ordering requirement, SymmetricHashJoinExec's
left and right sort expressions, the window expressions, and range partitioning
each hand-roll the same map / collect over
PhysicalSortExprNode { expr, asc, nulls_first }, on both the encode and the
decode side. #23683 (output_ordering) and #23752 / #23781 (the sinks' required
ordering) are about to add two more.

There is no reason for this logic to live in the callers: it converts a
Partitioning (and the PhysicalSortExprs and ScalarValues inside it), and
needs nothing from the plan level beyond the ability to encode a child
expression.

What changes are included in this PR?

Put the single copy next to the types that own it, taking the expression-level
context (datafusion-physical-expr and -common already carry the proto
feature):

  • PhysicalSortExpr::try_to_proto / try_from_proto (physical-expr-common)
  • sort_exprs_try_to_proto / sort_exprs_try_from_proto (physical-expr-common),
    the sequence form every caller actually needs
  • Partitioning::try_to_proto / try_from_proto (physical-expr)

So plan hooks can reach them, ExecutionPlanEncodeCtx / ExecutionPlanDecodeCtx
now back the expression-level contexts (PhysicalExprEncode /
PhysicalExprDecode) and hand one out via expr_ctx(). That bridge is useful
beyond partitioning: from here on any plan hook can pass its ctx straight to an
expression-level conversion, which is the shape the rest of the migration wants.

The sequence encoder is generic over Borrow<PhysicalSortExpr>, so one function
serves a LexOrdering, a &[PhysicalSortExpr], and a LexRequirement mapped
through PhysicalSortExpr::from. The decoder returns the expressions rather
than a LexOrdering, because callers disagree on what an empty list means: "no
ordering declared" for a scan, an error for an operator that requires one.

Routed through the new methods:

  • RepartitionExec and datafusion-proto's central serializer, which also
    retires serialize_range_partitioning, serialize_range_split_point,
    parse_protobuf_range_partitioning and parse_protobuf_range_split_point,
  • AggregateExec's ordering requirement, SymmetricHashJoinExec's left/right
    sort expressions, and the window expressions — encode and decode each.

Net: −380 / +458 lines with the new tests included; production code shrinks. The
next operator that needs partitioning or ordering serde writes one line instead
of sixty.

Are these changes tested?

Yes.

  • New unit tests for the sequence helpers (option and order fidelity, owned
    LexRequirement input, encode-error propagation, missing inner expression),
    using the existing proto_test_util stubs.
  • The existing round-trip suites cover the rest, and now exercise the shared
    path: all four partitioning variants (datafusion-proto's
    roundtrip_physical_plan tests exercise the central serializer,
    RepartitionExec's own hook tests exercise the plan path), plus aggregate
    ORDER BY, window ORDER BY and symmetric-hash-join sort expressions for the
    ordering helpers. datafusion-proto: 209 passed / 0 failed.
  • Lib suites for the three changed crates: 1613 + 1651 + 80 passed / 0 failed.
  • Full workspace run: 10344 passed
    (cargo test --profile ci --workspace --lib --tests --features avro,json,backtrace,extended_tests,recursive_protection,parquet_encryption).
    The only failures are 8 backtrace-symbolization tests in datafusion-common
    and 4 datafusion-cli tests that hard-code a repo-relative parquet-testing/
    path; both are artifacts of running from a linked worktree on macOS, in crates
    this PR does not touch, and the datafusion-cli four pass once that path
    resolves.
  • cargo fmt and ci/scripts/rust_clippy.sh clean.

Are there any user-facing changes?

The protobuf wire format is unchanged.

Additive API:

  • Partitioning::try_to_proto / try_from_proto, PhysicalSortExpr::try_to_proto /
    try_from_proto, sort_exprs_try_to_proto / sort_exprs_try_from_proto
    (feature proto).
  • ExecutionPlanEncodeCtx::expr_ctx() / ExecutionPlanDecodeCtx::expr_ctx(schema).

Two behavior differences, both in error paths:

  • Out-of-range partition counts now return an error instead of wrapping
    (as usize) or panicking (try_into().unwrap() in
    parse_protobuf_hash_partitioning).
  • A missing sort-expression child now reports which field is missing
    (PhysicalSortExpr is missing required field 'expr') instead of
    Unexpected empty physical expression, and the same message now replaces the
    three bespoke ones in AggregateExec, SymmetricHashJoinExec and the window
    expressions.

Four private helpers in datafusion-proto are removed
(serialize_range_partitioning, serialize_range_split_point,
parse_protobuf_range_partitioning, parse_protobuf_range_split_point); the
public serialize_partitioning / parse_protobuf_partitioning keep their
signatures and behavior.

@github-actions github-actions Bot added physical-expr Changes to the physical-expr crates proto Related to proto crate physical-plan Changes to the physical-plan crate labels Jul 30, 2026
@adriangb adriangb changed the title refactor(proto): put Partitioning / PhysicalSortExpr serde on the types refactor(proto): put Partitioning / sort-expression serde on the types Jul 30, 2026
@adriangb
adriangb requested review from Copilot and kumarUjjawal July 30, 2026 15:37

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

Note

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Refactors protobuf (de)serialization for Partitioning and flat PhysicalSortExprNode orderings to live on/near the owning types, so plan-level hooks and datafusion-proto can reuse shared conversion logic.

Changes:

  • Adds try_to_proto / try_from_proto on Partitioning and PhysicalSortExpr, plus sequence helpers for sort expressions.
  • Updates multiple physical plan nodes and datafusion-proto to route ordering/partitioning serde through the new shared APIs.
  • Introduces expression-level encode/decode context bridges (expr_ctx) on execution plan proto contexts.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
datafusion/proto/src/physical_plan/to_proto.rs Switches partitioning serialization to Partitioning::try_to_proto via an expression-level encode ctx.
datafusion/proto/src/physical_plan/from_proto.rs Switches partitioning parsing to Partitioning::try_from_proto via an expression-level decode ctx.
datafusion/physical-plan/src/windows/proto.rs Replaces hand-rolled window order_by sort-expr encoding/decoding with shared helpers.
datafusion/physical-plan/src/repartition/mod.rs Routes RepartitionExec partitioning serde through Partitioning::{try_to_proto, try_from_proto}.
datafusion/physical-plan/src/proto.rs Adds expr_ctx() adapters and implements PhysicalExprEncode/Decode for plan proto contexts.
datafusion/physical-plan/src/joins/symmetric_hash_join.rs Replaces manual sort-expr serde with shared sort-expr sequence helpers.
datafusion/physical-plan/src/aggregates/mod.rs Replaces manual aggregate ordering requirement serde with shared sort-expr helpers.
datafusion/physical-expr/src/partitioning.rs Adds Partitioning protobuf conversions + tests; adds partition_count helper for safe decode.
datafusion/physical-expr-common/src/sort_expr.rs Adds PhysicalSortExpr protobuf conversions and sequence encode/decode helpers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +540 to +551
let partition_method = match self {
Partitioning::RoundRobinBatch(n) => {
protobuf::partitioning::PartitionMethod::RoundRobin(*n as u64)
}
Partitioning::Hash(exprs, n) => {
protobuf::partitioning::PartitionMethod::Hash(
protobuf::PhysicalHashRepartition {
hash_expr: ctx.encode_children_expressions(exprs)?,
partition_count: *n as u64,
},
)
}

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.

Fixed. Encode now goes through a wire_partition_count helper (u64::try_from, internal error on overflow), mirroring the partition_count narrowing on the decode side, so an out-of-range count is an error in both directions rather than a silent truncation. Applied to all three sites (round-robin, hash, unknown).

Comment on lines +573 to +576
Partitioning::UnknownPartitioning(n) => {
protobuf::partitioning::PartitionMethod::Unknown(*n as u64)
}
};

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.

Same as the sibling comment above — both encode sites now use the checked wire_partition_count helper.

Comment on lines +522 to +528
/// Protobuf conversions for [`Partitioning`].
///
/// Child expressions (hash keys, range orderings) and `ScalarValue` split
/// points are (de)serialized through the expression-level context, so this is
/// the single copy of the partitioning wire format: `RepartitionExec`,
/// `FileScanConfig` and `datafusion-proto`'s central serializer all route
/// through it.

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.

Reworded — FileScanConfig is now described as a next-step consumer (#23683), not a current one: the doc says RepartitionExec and datafusion-proto's central serializer route through this, and the remaining per-plan migrations are meant to do the same rather than grow another copy.

Comment on lines +200 to +206
/// An expression-level encode context backed by this plan context.
///
/// Lets a plan hand `ctx` to expression-level conversions that own their own
/// wire logic — e.g.
/// [`Partitioning::try_to_proto`](datafusion_physical_expr::Partitioning::try_to_proto)
/// and
/// [`PhysicalSortExpr::try_to_proto`](datafusion_physical_expr::PhysicalSortExpr::try_to_proto).

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.

No change needed here: PhysicalSortExpr is re-exported from datafusion_physical_expr (physical-expr/src/lib.rs), so the link resolves. Verified with RUSTDOCFLAGS="-D rustdoc::broken_intra_doc_links" cargo doc --no-deps -p datafusion-physical-plan --features proto — clean.

Comment thread datafusion/physical-plan/src/proto.rs Outdated
Comment on lines +212 to +213
/// Lets [`ExecutionPlanEncodeCtx`] back an [`PhysicalExprEncodeCtx`], so
/// expression-level conversions can be reused from plan hooks.

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.

Fixed — now reads "back a PhysicalExprEncodeCtx".

Comment on lines +326 to +327
/// Lets [`ExecutionPlanDecodeCtx`] back a [`PhysicalExprDecodeCtx`], so
/// expression-level conversions can be reused from plan hooks.

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.

This one already reads "back a PhysicalExprDecodeCtx" — fixed the encode-side twin, which did say "an".

input_schema,
&decoder,
);
partitioning

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.

Could we make it delegate to the new shared decoder, or use checked conversion, so direct callers do not panic on oversized counts on 32-bit targets?

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.

Good catch, done. parse_protobuf_hash_partitioning now delegates to the shared Partitioning::try_from_proto. I also made the symmetric fix on the encode side: Partitioning::try_to_proto now widens through a checked wire_partition_count helper instead of as u6

@codecov-commenter

codecov-commenter commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.96491% with 40 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.85%. Comparing base (1ae6b87) to head (4a85a4d).

Files with missing lines Patch % Lines
datafusion/physical-expr/src/partitioning.rs 81.59% 15 Missing and 15 partials ⚠️
datafusion/physical-plan/src/aggregates/mod.rs 75.00% 2 Missing and 1 partial ⚠️
...ion/physical-plan/src/joins/symmetric_hash_join.rs 82.35% 1 Missing and 2 partials ⚠️
datafusion/physical-plan/src/repartition/mod.rs 80.00% 0 Missing and 2 partials ⚠️
datafusion/physical-plan/src/windows/proto.rs 33.33% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main   #24003   +/-   ##
=======================================
  Coverage   80.85%   80.85%           
=======================================
  Files        1099     1099           
  Lines      374338   374306   -32     
  Branches   374338   374306   -32     
=======================================
- Hits       302671   302649   -22     
  Misses      53621    53621           
+ Partials    18046    18036   -10     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@adriangb
adriangb force-pushed the prep/partitioning-proto-on-types branch from 99ea991 to ee3f054 Compare July 30, 2026 18:40
adriangb and others added 3 commits July 30, 2026 14:00
`Partitioning`'s protobuf conversion existed in three places: inline in
`RepartitionExec::try_to_proto` / `try_from_proto`, and in
`datafusion-proto`'s `serialize_partitioning` /
`parse_protobuf_partitioning`. Each new plan or data source that needs to
encode an output partitioning has so far copied it again.

Put the single copy next to the types that own it, taking the
expression-level context (`datafusion-physical-expr{,-common}` already
carry the `proto` feature):

- `PhysicalSortExpr::try_to_proto` / `try_from_proto` (physical-expr-common)
- `Partitioning::try_to_proto` / `try_from_proto` (physical-expr)

So plan hooks can reach them, `ExecutionPlanEncodeCtx` /
`ExecutionPlanDecodeCtx` now back the expression-level contexts and hand
one out via `expr_ctx()`; any plan hook can pass its ctx to
expression-level conversions from here on. `RepartitionExec` and
`datafusion-proto`'s central serializer both route through the type
methods, which retires `serialize_range_partitioning`,
`serialize_range_split_point`, `parse_protobuf_range_partitioning` and
`parse_protobuf_range_split_point`.

The wire format is unchanged. Behavior differences: out-of-range
partition counts now error instead of wrapping or panicking on an
`unwrap`, and a missing sort-expression child reports which field is
missing rather than "Unexpected empty physical expression".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`PhysicalSortExpr::try_to_proto` / `try_from_proto` convert one sort
expression, but every caller holds a sequence of them, so each one
hand-rolls the same map/collect over the flat `PhysicalSortExprNode`
list: `AggregateExec`'s ordering requirement, `SymmetricHashJoinExec`'s
left/right sort expressions, the window expressions, range partitioning,
and (in flight) `FileScanConfig`'s output orderings and the file sinks'
required ordering.

Add `sort_exprs_try_to_proto` / `sort_exprs_try_from_proto` next to
`PhysicalSortExpr` and route those callers through them. The encode side
takes anything that borrows a `PhysicalSortExpr`, so a `LexOrdering`, a
`&[PhysicalSortExpr]` and a `LexRequirement` mapped through
`PhysicalSortExpr::from` all work. The decode side returns the
expressions rather than a `LexOrdering`, because callers disagree on what
an empty list means: "no ordering declared" for a scan, an error for an
operator that requires one.

Decode now reports the missing inner expression uniformly as
"PhysicalSortExpr is missing required field 'expr'", replacing three
bespoke messages. No wire-format change.
- `parse_protobuf_hash_partitioning` delegates to the shared
  `Partitioning::try_from_proto` instead of keeping its own copy of the
  hash wire format, so its `try_into().unwrap()` on the partition count
  can no longer panic for direct callers on a 32-bit target. Covered by a
  new unit test, since the function has no in-tree callers left.
- Encode validates the partition count with `u64::try_from` rather than
  `as u64`, mirroring the decode side instead of truncating silently.
- Reword the `Partitioning` proto docs: `FileScanConfig` is an intended
  consumer, not a current one.
- Grammar: "a `PhysicalExprEncodeCtx`".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@adriangb
adriangb force-pushed the prep/partitioning-proto-on-types branch from ee3f054 to 4a85a4d Compare July 30, 2026 19:02
@adriangb
adriangb requested a review from kumarUjjawal July 30, 2026 19:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

physical-expr Changes to the physical-expr crates physical-plan Changes to the physical-plan crate proto Related to proto crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants