Skip to content

fix: preserve EmptyExec and PlaceholderRowExec partition count across proto round-trip#23643

Merged
adriangb merged 3 commits into
apache:mainfrom
andygrove:fix-empty-exec-partitions-proto
Jul 16, 2026
Merged

fix: preserve EmptyExec and PlaceholderRowExec partition count across proto round-trip#23643
adriangb merged 3 commits into
apache:mainfrom
andygrove:fix-empty-exec-partitions-proto

Conversation

@andygrove

@andygrove andygrove commented Jul 16, 2026

Copy link
Copy Markdown
Member

Which issue does this PR close?

Rationale for this change

EmptyExecNode and PlaceholderRowExecNode encoded only a schema, so the partition count set by EmptyExec::with_partitions(n) was silently lost across a physical-plan round-trip: a plan reporting n partitions before serialization reported 1 after.

This is silent data loss rather than an error — the decoded plan is well-formed but describes a different plan than the one encoded. It bites any setup that plans in one process and executes in another. In Ballista, an optimizer rule collapses provably-empty sub-plans into an EmptyExec inheriting the replaced node's partition count; the scheduler sizes the stage's task count from that count, ships the plan to an executor, and every task above partition 0 fails with:

Internal("Assertion failed: partition < self.partitions: EmptyExec invalid partition 1 (expected less than 1)")

What changes are included in this PR?

  • Add a uint32 partitions field to EmptyExecNode and PlaceholderRowExecNode in datafusion.proto, and regenerate the prost/pbjson code via regen.sh.
  • Write partitions on encode and apply it via with_partitions(...) on decode.

Both EmptyExec and PlaceholderRowExec mirror their private partitions field into PlanProperties as UnknownPartitioning(n), so the encoder reads the count via properties().output_partitioning().partition_count() on the existing ExecutionPlan trait. No new public API is added to datafusion-physical-plan.

The wire format stays compatible in both directions. Plans encoded before this field existed carry no value for it, which prost surfaces as 0; decode maps that to the previous default of 1. Plans encoded after this change add a field that older readers skip.

Note for #23501, which migrates these nodes to the try_to_proto / try_from_proto pattern: that issue specifies "schema only" and a byte-for-byte identical wire format, which would reintroduce this bug. The partitions field should be folded into that rewrite.

Are these changes tested?

Yes, three new tests in roundtrip_physical_plan.rs: partition-count round-trips for EmptyExec and PlaceholderRowExec, plus one decoding partitions: 0 nodes directly to pin the backward-compatibility mapping. The round-trip tests were confirmed to fail against the unfixed decoder with exactly the reported symptom (4 partitions decoding to 1).

Are there any user-facing changes?

No API changes to datafusion-physical-plan. Encoded plans gain a new protobuf field, which is backward and forward compatible as described above. The generated EmptyExecNode and PlaceholderRowExecNode structs gain a partitions field, which breaks downstream code constructing them with an exhaustive struct literal; this is documented in the 55.0.0 upgrade guide.

EmptyExecNode and PlaceholderRowExecNode encoded only a schema, so the
count set by with_partitions was silently dropped on a physical plan
round-trip and the decoded plan reported a single partition.

Add a partitions field to both messages, write it on encode, and apply it
on decode. Plans encoded before the field existed carry no value, which
decodes as 0 and maps back to the previous default of 1.
@github-actions github-actions Bot added proto Related to proto crate physical-plan Changes to the physical-plan crate labels Jul 16, 2026
@github-actions

github-actions Bot commented Jul 16, 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-proto v54.0.0 (current)
       Built [  63.961s] (current)
     Parsing datafusion-proto v54.0.0 (current)
      Parsed [   0.020s] (current)
    Building datafusion-proto v54.0.0 (baseline)
       Built [  58.780s] (baseline)
     Parsing datafusion-proto v54.0.0 (baseline)
      Parsed [   0.021s] (baseline)
    Checking datafusion-proto v54.0.0 -> v54.0.0 (no change; assume patch)
     Checked [   0.373s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [ 124.869s] datafusion-proto
    Building datafusion-proto-models v54.0.0 (current)
       Built [  23.808s] (current)
     Parsing datafusion-proto-models v54.0.0 (current)
      Parsed [   0.137s] (current)
    Building datafusion-proto-models v54.0.0 (baseline)
       Built [  23.639s] (baseline)
     Parsing datafusion-proto-models v54.0.0 (baseline)
      Parsed [   0.139s] (baseline)
    Checking datafusion-proto-models v54.0.0 -> v54.0.0 (no change; assume patch)
     Checked [   2.670s] 223 checks: 222 pass, 1 fail, 0 warn, 30 skip

--- failure constructible_struct_adds_field: externally-constructible struct adds field ---

Description:
A pub struct constructible with a struct literal has a new pub field. Existing struct literals must be updated to include the new field.
        ref: https://doc.rust-lang.org/reference/expressions/struct-expr.html
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.48.0/src/lints/constructible_struct_adds_field.ron

Failed in:
  field EmptyExecNode.partitions in /home/runner/work/datafusion/datafusion/datafusion/proto-models/src/generated/prost.rs:2074
  field EmptyExecNode.partitions in /home/runner/work/datafusion/datafusion/datafusion/proto-models/src/generated/prost.rs:2074
  field PlaceholderRowExecNode.partitions in /home/runner/work/datafusion/datafusion/datafusion/proto-models/src/generated/prost.rs:2083
  field PlaceholderRowExecNode.partitions in /home/runner/work/datafusion/datafusion/datafusion/proto-models/src/generated/prost.rs:2083

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [  51.853s] datafusion-proto-models

@github-actions github-actions Bot added the auto detected api change Auto detected API change label Jul 16, 2026
Adding the partitions field to these generated protobuf structs breaks
downstream code that constructs them with an exhaustive struct literal,
so document the change and migration in the 55.0.0 upgrade guide.
@andygrove andygrove added the api change Changes the API exposed to users of the crate label Jul 16, 2026
@andygrove

Copy link
Copy Markdown
Member Author

The semver check is reporting constructible_struct_adds_field for the new partitions field on EmptyExecNode and PlaceholderRowExecNode:

A pub struct constructible with a struct literal has a new pub field. Existing struct literals must be updated to include the new field.

This is a real, though unavoidable, break: the finding is inherent to adding a field to a prost-generated message, and the field is what fixes the bug. It only affects downstream code that constructs these two structs with an exhaustive struct literal; anyone using ..Default::default() is unaffected, and the protobuf wire format itself stays compatible in both directions.

Marking these #[non_exhaustive] to prevent a recurrence isn't an option, since datafusion-proto constructs them from outside the defining crate and would itself stop compiling.

Per the API health policy, I've addressed it by:

  • adding the api change label, and
  • documenting the change and migration in the 55.0.0 upgrade guide (786aad4).

Happy to reconsider if reviewers would rather see this deferred to a different release.

@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jul 16, 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 for the fix!

Would you be willing to tackle #23501 once we get the base infrastructure across the line? It makes this sort of bug structurally impossible in the future.

Comment thread datafusion/physical-plan/src/empty.rs Outdated
self
}

/// Number of partitions this plan produces

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.

Can we add a note that this method is being made public only for our proto serialization and we don't guarantee any sort of API stability, etc? I know it's improper, we can always decide later to respect public API contracts for this method. But I think it's best to do what we can to err on the side of caution now because we'll probably be removing it again soon.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed these methods. They weren't needed since we already have output_partitioning that the tests can use. Thanks for catching that.

@codecov-commenter

codecov-commenter commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 27.77778% with 26 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@2e78508). Learn more about missing BASE report.

Files with missing lines Patch % Lines
datafusion/proto-models/src/generated/pbjson.rs 0.00% 26 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main   #23643   +/-   ##
=======================================
  Coverage        ?   80.64%           
=======================================
  Files           ?     1086           
  Lines           ?   366132           
  Branches        ?   366132           
=======================================
  Hits            ?   295278           
  Misses          ?    53259           
  Partials        ?    17595           

☔ 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.

@andygrove

Copy link
Copy Markdown
Member Author

Would you be willing to tackle #23501 once we get the base infrastructure across the line? It makes this sort of bug structurally impossible in the future.

Yes, I'd be happy to work on that

…sors

EmptyExec and PlaceholderRowExec mirror their private `partitions` field
into PlanProperties as UnknownPartitioning(n), so the encoder can read the
count through the existing ExecutionPlan trait rather than through newly
added public accessors.

Drop `EmptyExec::partitions()` and `PlaceholderRowExec::partitions()`, which
now have no callers, and read the count via
`properties().output_partitioning().partition_count()` instead. This keeps
the fix free of any new public API in datafusion-physical-plan.

The proto `partitions` field and its semver impact are unaffected.
@github-actions github-actions Bot removed the physical-plan Changes to the physical-plan crate label Jul 16, 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.

Nice fix removing the new pub methods!

@adriangb adriangb enabled auto-merge July 16, 2026 18:40
@adriangb adriangb added this pull request to the merge queue Jul 16, 2026
Merged via the queue into apache:main with commit 0840e5c Jul 16, 2026
39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api change Changes the API exposed to users of the crate auto detected api change Auto detected API change documentation Improvements or additions to documentation proto Related to proto crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

datafusion-proto drops EmptyExec partition count on physical plan round-trip

3 participants