Skip to content

Proto: migrate file sink serialization - #23781

Merged
adriangb merged 4 commits into
apache:mainfrom
Phoenix500526:issue/23519
Aug 7, 2026
Merged

Proto: migrate file sink serialization#23781
adriangb merged 4 commits into
apache:mainfrom
Phoenix500526:issue/23519

Conversation

@Phoenix500526

Copy link
Copy Markdown
Contributor

This is a stacked PR based on #23752. Only the commits on top of #23752 are part of this review.

Which issue does this PR close?

Rationale for this change

#23752 adds the DataSink::try_to_proto hook and moves the shared
FileSinkConfig protobuf conversion into datafusion-datasource.

This PR uses that foundation to move CSV, JSON, and Parquet sink
serialization out of the central datafusion-proto downcast chain.
Each concrete sink now owns its format-specific protobuf encoding and
decoding logic.

What changes are included in this PR?

  • Implement DataSink::try_to_proto for CsvSink, JsonSink, and
    ParquetSink.
  • Add inherent try_from_proto methods to reconstruct each sink's
    DataSinkExec.
  • Repoint the physical-plan decode arms to the sink-owned decoders.
  • Add feature-gated protobuf dependencies to the three format crates.
  • Remove the active central DataSinkExec serialization dispatch after
    migrating its final built-in sink.
  • Retain the old serialization helpers as deprecated compatibility
    delegates.
  • Let sinks without a built-in protobuf representation fall through to
    the physical extension codec.
  • Preserve the existing protobuf wire representation.

The migrations are split into one commit per sink.

Are these changes tested?

Yes. Existing sink round-trip tests cover the protobuf representation.

The following checks passed:

  • Focused CSV, JSON, and Parquet sink round-trip tests.
  • All datafusion-proto integration tests.
  • cargo check -p datafusion-proto --no-default-features.
  • cargo fmt --all.
  • cargo clippy --all-targets --all-features -- -D warnings.
  • The required extended workspace test suite, including all 495 SQL
    logic test files.

Are there any user-facing changes?

No functional or wire-format changes are intended.

The old compatibility helpers remain available but are deprecated.

@Phoenix500526
Phoenix500526 marked this pull request as draft July 21, 2026 23:59
@github-actions github-actions Bot added proto Related to proto crate datasource Changes to the datasource crate labels Jul 22, 2026
@github-actions

github-actions Bot commented Jul 22, 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-datasource v54.1.0 (current)
       Built [  49.375s] (current)
     Parsing datafusion-datasource v54.1.0 (current)
      Parsed [   0.035s] (current)
    Building datafusion-datasource v54.1.0 (baseline)
       Built [  42.667s] (baseline)
     Parsing datafusion-datasource v54.1.0 (baseline)
      Parsed [   0.034s] (baseline)
    Checking datafusion-datasource v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.354s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [  93.861s] datafusion-datasource
    Building datafusion-datasource-csv v54.1.0 (current)
       Built [  42.802s] (current)
     Parsing datafusion-datasource-csv v54.1.0 (current)
      Parsed [   0.013s] (current)
    Building datafusion-datasource-csv v54.1.0 (baseline)
       Built [  39.181s] (baseline)
     Parsing datafusion-datasource-csv v54.1.0 (baseline)
      Parsed [   0.013s] (baseline)
    Checking datafusion-datasource-csv v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.134s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [  83.113s] datafusion-datasource-csv
    Building datafusion-datasource-json v54.1.0 (current)
       Built [  42.609s] (current)
     Parsing datafusion-datasource-json v54.1.0 (current)
      Parsed [   0.014s] (current)
    Building datafusion-datasource-json v54.1.0 (baseline)
       Built [  39.795s] (baseline)
     Parsing datafusion-datasource-json v54.1.0 (baseline)
      Parsed [   0.014s] (baseline)
    Checking datafusion-datasource-json v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.120s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [  83.574s] datafusion-datasource-json
    Building datafusion-datasource-parquet v54.1.0 (current)
       Built [  49.483s] (current)
     Parsing datafusion-datasource-parquet v54.1.0 (current)
      Parsed [   0.034s] (current)
    Building datafusion-datasource-parquet v54.1.0 (baseline)
       Built [  44.679s] (baseline)
     Parsing datafusion-datasource-parquet v54.1.0 (baseline)
      Parsed [   0.034s] (baseline)
    Checking datafusion-datasource-parquet v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.221s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [  95.603s] datafusion-datasource-parquet
    Building datafusion-proto v54.1.0 (current)
       Built [  59.666s] (current)
     Parsing datafusion-proto v54.1.0 (current)
      Parsed [   0.020s] (current)
    Building datafusion-proto v54.1.0 (baseline)
       Built [  59.965s] (baseline)
     Parsing datafusion-proto v54.1.0 (baseline)
      Parsed [   0.021s] (baseline)
    Checking datafusion-proto v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.370s] 223 checks: 222 pass, 1 fail, 0 warn, 30 skip

--- failure trait_method_marked_deprecated: trait method #[deprecated] added ---

Description:
A trait 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.49.0/src/lints/trait_method_marked_deprecated.ron

Failed in:
  method try_into_json_sink_physical_plan in trait datafusion_proto::physical_plan::PhysicalPlanNodeExt in /home/runner/work/datafusion/datafusion/datafusion/proto/src/physical_plan/mod.rs:662
  method try_into_csv_sink_physical_plan in trait datafusion_proto::physical_plan::PhysicalPlanNodeExt in /home/runner/work/datafusion/datafusion/datafusion/proto/src/physical_plan/mod.rs:662
  method try_into_parquet_sink_physical_plan in trait datafusion_proto::physical_plan::PhysicalPlanNodeExt in /home/runner/work/datafusion/datafusion/datafusion/proto/src/physical_plan/mod.rs:662
  method try_from_data_sink_exec in trait datafusion_proto::physical_plan::PhysicalPlanNodeExt in /home/runner/work/datafusion/datafusion/datafusion/proto/src/physical_plan/mod.rs:662

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

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

codecov-commenter commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 59.42623% with 99 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.02%. Comparing base (24483db) to head (d2f7924).

Files with missing lines Patch % Lines
datafusion/proto/src/physical_plan/mod.rs 9.09% 30 Missing ⚠️
datafusion/datasource-csv/src/file_format.rs 68.33% 8 Missing and 11 partials ⚠️
datafusion/datasource-json/src/file_format.rs 68.33% 8 Missing and 11 partials ⚠️
datafusion/datasource-parquet/src/sink.rs 68.33% 8 Missing and 11 partials ⚠️
datafusion/datasource/src/sink.rs 76.00% 3 Missing and 3 partials ⚠️
datafusion/proto/src/physical_plan/from_proto.rs 0.00% 3 Missing ⚠️
datafusion/proto/src/physical_plan/to_proto.rs 0.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #23781      +/-   ##
==========================================
- Coverage   81.05%   81.02%   -0.04%     
==========================================
  Files        1106     1106              
  Lines      380556   380652      +96     
  Branches   380556   380652      +96     
==========================================
- Hits       308477   308425      -52     
- Misses      53861    54002     +141     
- Partials    18218    18225       +7     

☔ 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 added a commit to pydantic/datafusion that referenced this pull request Jul 30, 2026
…on-datasource (apache#24006)

## Which issue does this PR close?

- Part of apache#23494. Precursor for apache#23497 / apache#23683 (`DataSource` /
`FileSource` proto hooks) and for apache#23752.

## Rationale for this change

The protobuf conversions for the file-scan leaf types —
`PartitionedFile`,
`FileGroup`, `FileRange` — live in `datafusion-proto` as `TryFromProto`
impls,
because that is historically the only crate that can name both sides
(the
DataFusion type and the prost message are both foreign to it, hence the
`TryFromProto` workaround trait in the first place).

That placement means any *other* crate that needs those conversions has
to
reimplement them. apache#23683 hits exactly this: a `FileSource` serializing
its own
scan config needs to encode file groups, so the first cut of that PR
grew a
private second copy of the `PartitionedFile` wire logic inside
`datafusion-datasource`, which can then drift from the central
serializer.
The same will be true of every source migrated under apache#23516apache#23518.

Nothing about these conversions needs `datafusion-proto`: they are plain
data,
with `ScalarValue` / `Statistics` / `Schema` going through
`datafusion-proto-common`. They belong next to the types.

## What changes are included in this PR?

- New `datafusion_datasource::proto` module, behind a new `proto`
feature on
`datafusion-datasource` (off by default; `datafusion-proto` enables it):
  - `FileRange::try_to_proto` / `try_from_proto`
  - `PartitionedFile::try_to_proto` / `try_from_proto`
  - `FileGroup` <-> `protobuf::FileGroup`
- `datafusion-proto`'s `TryFromProto` impls for those types become
one-line
shims delegating to the new impls, so every existing caller keeps
working
  and the two sides cannot disagree.

### Why these are `TryFrom` and not `try_to_proto` hooks

`TryFromProto` exists because `datafusion-proto` owns neither side of
the
conversions it hosts: with both the DataFusion type and the prost
message
foreign to it, `impl TryFrom<protobuf::X> for X` is rejected by the
orphan
rule, so a local trait was the only way to say the same thing.

Moving a conversion into the crate that owns the DataFusion type removes
that
constraint, and `&T` is `#[fundamental]`, so both directions are
expressible
with the standard trait (checked, not assumed):

```rust
impl TryFrom<&protobuf::PartitionedFile> for PartitionedFile   // ok
impl TryFrom<&PartitionedFile> for protobuf::PartitionedFile   // ok
impl TryFrom<&[PartitionedFile]> for protobuf::FileGroup       // E0117
```

The last one is why `protobuf::FileGroup`'s *slice* conversion stays a
`TryFromProto` shim: `&[PartitionedFile]` is not a type this crate owns,
while
`&FileGroup` is. Callers inside DataFusion go through `FileGroup`.

So the rule this PR sets for the rest of apache#23494: **plain data uses
`TryFrom`;
anything needing an encode/decode context keeps the `try_to_proto(ctx)`
/
`try_from_proto(node, ctx)` hooks**, because the standard trait cannot
carry
that second argument. Usefully, none of the ~40
`TryFromProto`/`FromProto`
impls needs a context, and nothing that needs one was ever a
`TryFromProto`
impl — the two categories are already disjoint, so the shape now tells a
reader whether a conversion recurses.

### Why now

`FromProto` / `TryFromProto` were added in apache#21929, *after* the 54.0.0
release,
and 54.1.0 was cut before any of this landed — so they have never
shipped in a
release. Replacing them with the standard traits, and eventually
deleting them,
is a no-op for semver **today** and a major breaking change the moment
55.0.0
goes out. The same applies to the six inherent `try_to_proto` /
`try_from_proto`
methods this PR would otherwise have added: they are new, unreleased
API, so
choosing their final shape costs nothing right now.

The other reason to settle it here rather than in a follow-up: this is
the PR
that establishes the pattern for the data-source family (apache#23516-apache#23519
and
apache#23752 / apache#23781 are all queued behind it). Whichever shape merges first
is the
one they will copy.

Retiring the remaining ~34 impls is still its own follow-up. Two notes
for
whoever picks it up: the sink and format-option conversions can move
next to
their types the same way, but the ones for `datafusion-common`-owned
types
(`JoinType`, `NullEquality`, `TableReference`, `UnnestOptions`, ...)
cannot —
`datafusion-common` cannot depend on `datafusion-proto-models` (it is
underneath it via `datafusion-proto-common`). Their legal home is
`proto-models` itself, implementing on the local proto type, which is
already
how `proto-common` hosts the `ScalarValue` / `Statistics` conversions.

## Are these changes tested?

Yes.

- New unit tests in `datafusion_datasource::proto` covering the
`PartitionedFile` round trip (path, size, mtime, partition values,
range,
arrow schema, statistics), the `FileGroup` round trip, and the
invalid-path
  error.
- The existing `datafusion-proto` tests now exercise the delegating
shims, so
  they also pin the shims themselves.
- `datafusion-proto`, all features: 227 passed / 0 failed.
- `datafusion-datasource` with `proto`: 180 passed / 0 failed.
- Full workspace run: 10347 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`,
a crate this PR does not touch and which sits below every crate it does;
they
  fail the same way on the base commit on macOS.
- `cargo fmt` and `ci/scripts/rust_clippy.sh` clean.

## Are there any user-facing changes?

The protobuf wire format is unchanged, and no existing API changes
shape.

Additive:

- New `proto` feature on `datafusion-datasource` (off by default).
- New `TryFrom` impls in both directions between `FileRange`,
  `PartitionedFile`, `FileGroup` and their protobuf messages, under that
feature. No new names are added to the crate's API surface: the trait is
  `core::convert::TryFrom`.

Note for reviewers: while writing the round-trip test I found that
`PartitionedFile` statistics do not round-trip cleanly on `main` — filed
as
apache#23998. This PR preserves that behavior exactly rather than changing
decode
semantics in a refactor; the test documents it.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@adriangb

Copy link
Copy Markdown
Contributor

Thanks @Phoenix500526 — reviewing the commits on top of #23752 as you asked. The migration itself is faithful: each sink's encode matches the arm it replaces field-for-field (including sink_schema and decoding sort_order against the input schema), the central DataSinkExec downcast block is removed, the decode arms are repointed, and the old helpers are kept as deprecated delegates. That is the shape #23494 asks for.

Four things before this comes out of draft.

Rebase. Main has moved: #24006 merged and this now conflicts in datafusion/datasource/Cargo.toml, datafusion/proto/src/physical_plan/mod.rs, from_proto.rs and roundtrip_physical_plan.rs. Most of the resolution is deletion — datafusion-datasource already carries a proto feature, and PartitionedFile / FileGroup <-> proto already live in datafusion_datasource::proto.

Use expect_plan_variant! instead of hand-rolling the variant match. Each try_from_proto currently opens with

let sink_node = match &node.physical_plan_type {
    Some(protobuf::physical_plan_node::PhysicalPlanType::CsvSink(sink)) => sink.as_ref(),
    _ => return datafusion_common::internal_err!("PhysicalPlanNode is not a CsvSink"),
};

which is what the macro exists for. It is #[macro_export]ed from datafusion-physical-plan, so the format crates can use it as datafusion_physical_plan::expect_plan_variant!, and every plan migrated under this epic on main already does. It also produces the same error string, so nothing changes behaviorally.

The TryFromProto impls for the sink messages are now duplicated logic. impl TryFromProto<&CsvSink> for protobuf::CsvSink (and the Json/Parquet siblings, both directions) still carry their original bodies in datafusion-proto, while this PR adds a second copy of the same field mapping inside CsvSink::try_to_proto. That is the drift risk the epic is trying to remove, and it is the one thing here that is not yet single-sourced.

Since these types live in the format crates, the conversions can move next to them as standard TryFrom impls — see #24006 for the pattern and #24019 for why. Concretely, give each sink an inherent conversion for its message:

impl TryFrom<&CsvSink> for protobuf::CsvSink { ... }
impl TryFrom<&protobuf::CsvSink> for CsvSink { ... }

then have CsvSink::try_to_proto / try_from_proto use them for the sink field, and reduce datafusion-proto's TryFromProto impls to delegates. As a bonus this restores the TryFrom<&CsvSink> / TryFrom<&protobuf::CsvSink> impls that shipped in 54.1.0 and were dropped on main (#24019), so it is a regression fix rather than new API.

Consider merging this into #23752. The two are one logical change — #23752 adds a hook with no built-in implementor, and until this PR lands every built-in sink encodes its input subtree twice (the hook encodes it, the sink returns Ok(None), and the central chain encodes it again). Merged together they are also easier to review as a single before/after, and it removes the need for parse_sink_sort_order to be public API in #23752 with no caller. Entirely your call — the stack is reviewable as-is if you would rather keep it split.

One small thing: the non-parquet arm changed its panic message from "Trying to use ParquetSink without parquet feature enabled" to "Unable to process a Parquet PhysicalPlan when parquet feature is not enabled". Harmless, but if it was not deliberate it is worth keeping the original so the string stays greppable.

pull Bot pushed a commit to Stars1233/datafusion that referenced this pull request Jul 31, 2026
apache#24003)

## Which issue does this PR close?

- Part of apache#23494 (the `try_to_proto` / `try_from_proto` migration).
Precursor for apache#23497 / apache#23683 / apache#23498 and for the remaining per-plan
migrations that need to encode an output partitioning or an ordering.

## 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 — apache#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. apache#23683 (`output_ordering`) and apache#23752 / apache#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 `PhysicalSortExpr`s and `ScalarValue`s 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.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@Phoenix500526

Copy link
Copy Markdown
Contributor Author

Thanks @Phoenix500526 — reviewing the commits on top of #23752 as you asked. The migration itself is faithful: each sink's encode matches the arm it replaces field-for-field (including sink_schema and decoding sort_order against the input schema), the central DataSinkExec downcast block is removed, the decode arms are repointed, and the old helpers are kept as deprecated delegates. That is the shape #23494 asks for.

Four things before this comes out of draft.

Rebase. Main has moved: #24006 merged and this now conflicts in datafusion/datasource/Cargo.toml, datafusion/proto/src/physical_plan/mod.rs, from_proto.rs and roundtrip_physical_plan.rs. Most of the resolution is deletion — datafusion-datasource already carries a proto feature, and PartitionedFile / FileGroup <-> proto already live in datafusion_datasource::proto.

Use expect_plan_variant! instead of hand-rolling the variant match. Each try_from_proto currently opens with

let sink_node = match &node.physical_plan_type {
    Some(protobuf::physical_plan_node::PhysicalPlanType::CsvSink(sink)) => sink.as_ref(),
    _ => return datafusion_common::internal_err!("PhysicalPlanNode is not a CsvSink"),
};

which is what the macro exists for. It is #[macro_export]ed from datafusion-physical-plan, so the format crates can use it as datafusion_physical_plan::expect_plan_variant!, and every plan migrated under this epic on main already does. It also produces the same error string, so nothing changes behaviorally.

The TryFromProto impls for the sink messages are now duplicated logic. impl TryFromProto<&CsvSink> for protobuf::CsvSink (and the Json/Parquet siblings, both directions) still carry their original bodies in datafusion-proto, while this PR adds a second copy of the same field mapping inside CsvSink::try_to_proto. That is the drift risk the epic is trying to remove, and it is the one thing here that is not yet single-sourced.

Since these types live in the format crates, the conversions can move next to them as standard TryFrom impls — see #24006 for the pattern and #24019 for why. Concretely, give each sink an inherent conversion for its message:

impl TryFrom<&CsvSink> for protobuf::CsvSink { ... }
impl TryFrom<&protobuf::CsvSink> for CsvSink { ... }

then have CsvSink::try_to_proto / try_from_proto use them for the sink field, and reduce datafusion-proto's TryFromProto impls to delegates. As a bonus this restores the TryFrom<&CsvSink> / TryFrom<&protobuf::CsvSink> impls that shipped in 54.1.0 and were dropped on main (#24019), so it is a regression fix rather than new API.

Consider merging this into #23752. The two are one logical change — #23752 adds a hook with no built-in implementor, and until this PR lands every built-in sink encodes its input subtree twice (the hook encodes it, the sink returns Ok(None), and the central chain encodes it again). Merged together they are also easier to review as a single before/after, and it removes the need for parse_sink_sort_order to be public API in #23752 with no caller. Entirely your call — the stack is reviewable as-is if you would rather keep it split.

One small thing: the non-parquet arm changed its panic message from "Trying to use ParquetSink without parquet feature enabled" to "Unable to process a Parquet PhysicalPlan when parquet feature is not enabled". Harmless, but if it was not deliberate it is worth keeping the original so the string stays greppable.

Done

CsvSink serialization still depended on central downcasts in
datafusion-proto. Moving encode and decode ownership into the CSV sink
exercises the DataSink hook and keeps format-specific wire logic with
the concrete type.

Retain the old decode helper as a deprecated compatibility shim.

Refs apache#23519

Signed-off-by: Jiawei Zhao <Phoenix500526@163.com>
JsonSink serialization still depended on central downcasts in
datafusion-proto. Moving encode and decode ownership into the JSON sink
exercises the DataSink hook and keeps format-specific wire logic with
the concrete type.

Retain the old decode helper as a deprecated compatibility shim.

Refs apache#23519

Signed-off-by: Jiawei Zhao <Phoenix500526@163.com>
ParquetSink serialization still depended on the final concrete sink
downcast in datafusion-proto. Moving encode and decode ownership into
the Parquet sink completes the DataSink migration and keeps
format-specific wire logic with the concrete type.

Remove the now-unused central dispatch path while retaining its public
helpers as deprecated compatibility shims.

Refs apache#23519

Signed-off-by: Jiawei Zhao <Phoenix500526@163.com>
@Phoenix500526
Phoenix500526 marked this pull request as ready for review August 6, 2026 15:32
@adriangb
adriangb requested a lite review from Copilot August 6, 2026 15:34

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

This PR continues the proto-migration work from #23752 by moving CSV/JSON/Parquet file sink protobuf serialization/deserialization out of datafusion-proto and into the concrete sink crates via the DataSink::try_to_proto hook, while keeping wire compatibility and leaving deprecated compatibility delegates in place.

Changes:

  • Implement DataSink::try_to_proto + sink-owned try_from_proto reconstruction for CsvSink, JsonSink, and ParquetSink.
  • Repoint datafusion-proto physical plan decoding to call the sink-owned decoders and remove the central DataSinkExec downcast-based serialization dispatch.
  • Add feature-gated proto dependencies/features to the format crates to host their protobuf logic.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
datafusion/proto/src/physical_plan/to_proto.rs Switch legacy TryFromProto sink conversions to delegate to sink-owned TryFrom impls.
datafusion/proto/src/physical_plan/mod.rs Route decode arms to sink-owned try_from_proto; remove central DataSinkExec serialization special-case; keep deprecated compatibility helpers.
datafusion/proto/src/physical_plan/from_proto.rs Switch legacy TryFromProto sink conversions to delegate to sink-owned TryFrom impls.
datafusion/proto/Cargo.toml Enable proto feature on csv/json/parquet datasource crates to access sink-owned protobuf implementations.
datafusion/datasource/src/sink.rs Add DataSinkExec::decode_sort_order helper for sink-owned protobuf decoding.
datafusion/datasource-parquet/src/sink.rs Add Parquet sink-owned protobuf encode/decode (try_to_proto, TryFrom, try_from_proto).
datafusion/datasource-parquet/Cargo.toml Add proto feature + optional datafusion-proto-models dependency wiring.
datafusion/datasource-json/src/file_format.rs Add JSON sink-owned protobuf encode/decode (try_to_proto, TryFrom, try_from_proto).
datafusion/datasource-json/Cargo.toml Add proto feature + optional datafusion-proto-models dependency wiring.
datafusion/datasource-csv/src/file_format.rs Add CSV sink-owned protobuf encode/decode (try_to_proto, TryFrom, try_from_proto).
datafusion/datasource-csv/Cargo.toml Add proto feature + optional datafusion-proto-models dependency wiring.
Cargo.lock Record new dependency edges from enabling proto-models in the sink crates.

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

Comment thread datafusion/datasource/src/sink.rs Outdated
use arrow::array::{ArrayRef, RecordBatch, UInt64Array};
use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
use datafusion_common::{Result, assert_eq_or_internal_err};
use datafusion_common::{Result, assert_eq_or_internal_err, internal_datafusion_err};

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.

Done

Comment on lines +795 to +796
#[cfg(not(feature = "parquet"))]
panic!("Trying to use ParquetSink without `parquet` feature enabled")

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.

Done

Moving protobuf models out of datafusion-proto made its standard
TryFrom implementations violate Rust's orphan rules. Replacing them
with TryFromProto silently removed APIs shipped in 54.1.0.

Define standard conversions in each sink's owning crate and keep the
legacy traits as delegates so one mapping serves both call paths.

Refs apache#23519
Refs apache#24019

Signed-off-by: Jiawei Zhao <Phoenix500526@163.com>
@adriangb
adriangb added this pull request to the merge queue Aug 7, 2026
Merged via the queue into apache:main with commit 491de04 Aug 7, 2026
38 checks passed
@Phoenix500526
Phoenix500526 deleted the issue/23519 branch August 7, 2026 03:03
adriangb added a commit to pydantic/datafusion that referenced this pull request Aug 9, 2026
The real `TryFrom` impls for `PartitionedFile`, `FileRange`, `FileGroup`
(apache#24006) and for `JsonSink` / `CsvSink` / `ParquetSink` / `FileSinkConfig`
(apache#23781) now live next to the types, so the `TryFromProto` copies in
`datafusion-proto` were pure delegation.

`TryFrom<&[PartitionedFile]> for protobuf::FileGroup` has no `TryFrom`
equivalent — a slice is not a local type in any crate that could host the
impl — so it becomes `datafusion_datasource::proto::partitioned_files_to_proto`,
re-exported from `datafusion_proto::physical_plan::to_proto`.

The `PartitionedFile` tests move to `datafusion-datasource` alongside the
logic they cover; the two that duplicated existing coverage there are dropped.

Part of apache#24019.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
adriangb added a commit to pydantic/datafusion that referenced this pull request Aug 9, 2026
The real `TryFrom` impls for `PartitionedFile`, `FileRange`, `FileGroup`
(apache#24006) and for `JsonSink` / `CsvSink` / `ParquetSink` / `FileSinkConfig`
(apache#23781) now live next to the types, so the `TryFromProto` copies in
`datafusion-proto` were pure delegation.

`TryFrom<&[PartitionedFile]> for protobuf::FileGroup` goes away here and comes
back in the `datafusion-proto-models` commit, which is the one crate that can
express it.

The `PartitionedFile` tests move to `datafusion-datasource` alongside the logic
they cover; the two that duplicated existing coverage there are dropped.

Part of apache#24019.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
adriangb added a commit to pydantic/datafusion that referenced this pull request Aug 11, 2026
The real `TryFrom` impls for `PartitionedFile`, `FileRange`, `FileGroup`
(apache#24006) and for `JsonSink` / `CsvSink` / `ParquetSink` / `FileSinkConfig`
(apache#23781) now live next to the types, so the `TryFromProto` copies in
`datafusion-proto` were pure delegation.

`TryFrom<&[PartitionedFile]> for protobuf::FileGroup` goes away here and comes
back in the `datafusion-proto-models` commit, which is the one crate that can
express it.

The `PartitionedFile` tests move to `datafusion-datasource` alongside the logic
they cover; the two that duplicated existing coverage there are dropped.

Part of apache#24019.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alamb added a commit to AlvaroBalbin/datafusion that referenced this pull request Aug 11, 2026
…ache#24205)

## Which issue does this PR close?

- Closes apache#24019.
- closes apache#23494

## Rationale for this change

`datafusion-proto` 54.1.0 publishes 39 `From` / `TryFrom` impls
converting between DataFusion types and their protobuf messages. On
`main` all of them were replaced by the crate-local `FromProto` /
`TryFromProto` traits introduced in apache#21929, so code written against the
released version stops compiling:

```rust
let proto = protobuf::PartitionedFile::try_from(&file)?;   // no longer resolves on main
let frame = WindowFrame::try_from(proto_frame)?;           // no longer resolves on main
```

That was collateral damage from the orphan-rule workaround, which was
needed during the migration but can now be unwound to result in no
breaking change across releases.

## What changes are included in this PR?

Each conversion moves to a crate that owns one side of it, and goes back
to being a plain `From` / `TryFrom` — the shape 54.1.0 published. Error
types are unchanged (`FromProtoError` decoding, `ToProtoError` encoding,
`DataFusionError` for the datasource types).

| Types | New home |
|---|---|
| `PartitionedFile`, `FileRange`, `FileGroup`, `JsonSink`, `CsvSink`,
`ParquetSink`, `FileSinkConfig` | already moved by apache#24006 / apache#23781 —
this PR just deletes the `TryFromProto` shims that delegated to them |
| `WindowFrame`, `WindowFrameBound`, `WindowFrameUnits`,
`MergeIntoClauseKind`, `NullTreatment` | `datafusion-expr`, behind a new
`proto` feature (optional `datafusion-proto-common` /
`datafusion-proto-models` deps, mirroring `datafusion-datasource`) |
| `UnnestOptions`, `TableReference`, `StringifiedPlan`, `JoinType`,
`JoinConstraint`, `NullEquality`, `CsvOptions`, `JsonOptions`, and the
parquet options types | `datafusion-proto-models`, on the local proto
type — their DataFusion side sits *below* that crate in the graph, the
same arrangement `datafusion-proto-common` already uses for
`ScalarValue` / `Statistics` |
| `CsvFormatFactory`, `JsonFormatFactory`, `ParquetFormatFactory` |
`datafusion-datasource-{csv,json,parquet}`, behind each crate's existing
`proto` feature |
| `Column` <-> `protobuf::PhysicalColumn` | `datafusion-physical-expr`;
`Column::try_to_proto` / `try_from_proto` now go through it instead of
building the message inline |

`TryFrom<&[PartitionedFile]> for protobuf::FileGroup` needed one extra
step. `datafusion-datasource` cannot host it — `&T` is `#[fundamental]`
but `[T]` is not, so `&[PartitionedFile]` counts as foreign there
(`error[E0117]: slices are always foreign`). But in
`datafusion-proto-models` the *self* type is local, which is all the
orphan rule needs, and staying generic over the element avoids naming
`PartitionedFile`, which sits above that crate in the graph:

```rust
impl<T> TryFrom<&[T]> for protobuf::FileGroup
where
    for<'a> &'a T: TryInto<protobuf::PartitionedFile, Error = DataFusionError>,
{ ... }
```

The bound is satisfied by `TryFrom<&PartitionedFile> for
protobuf::PartitionedFile` in `datafusion-datasource`, so
`protobuf::FileGroup::try_from(&files[..])` resolves for callers exactly
as it did in 54.1.0.

Two items beyond the issue's checklist, both needed to reach zero
implementors:

- the parquet options conversions (`ParquetOptions`,
`TableParquetOptions`, `ParquetColumnOptions`, `ParquetCdcOptions`) —
the issue's table undercounts `file_formats.rs` because they live in a
private module, but trait impls are global, so they were public API too.
They return as `TryFrom`; `main` had already made them fallible, so an
exact restore of 54.1.0's infallible `From` isn't available.
- `From<&protobuf::PhysicalColumn> for Column`, which the issue's
evidence table counts but no work item names.

Not restored, and worth calling out: `From<protobuf::dml_node::Type> for
WriteOp` and its reverse. `main` replaced them with `parse_write_op` /
`serialize_write_op` because `MergeInto` carries a payload a `From` impl
cannot express. That is a separate, deliberate change.

Finally, `convert.rs` and `convert_required_proto!` are deleted. apache#21929
introduced `FromProto` / `TryFromProto` so the `datafusion-proto-models`
extraction could land without relocating ~39 conversions at the same
time, and flagged them there as "a known workaround, not the end state",
with dropping them listed under Future work. With every conversion moved
they have no implementors and no callers. Neither trait has ever shipped
in a release, so they are removed outright rather than deprecated —
there is nothing for downstream users to migrate off, and doing it now
keeps the workaround out of the released API entirely.

## Are these changes tested?

Yes.

- New `datafusion/proto/tests/cases/public_conversions.rs` coerces all
45 proto conversions in the touched crates to `fn` pointers (the 39 from
54.1.0 plus the ones added on `main`). This is the regression guard the
issue asks for: it fails to compile when an impl is removed, and stays
quiet when one merely moves between crates, which is exactly the case
`cargo-semver-checks` cannot see.
- New round-trip tests next to the moved impls in `datafusion-expr` and
`datafusion-proto-models` (window frames, table references, join enums,
unnest options, stringified plans).
- The `PartitionedFile` tests move from `datafusion-proto` to
`datafusion-datasource`, alongside the logic they cover; two that
duplicated existing coverage there are dropped.
- Existing round-trip suites (`roundtrip_logical_plan`,
`roundtrip_physical_plan`) pass unchanged, which is the real wire-format
check.
- Every moved impl body was diffed against `main`: 22 are byte-identical
modulo the trait rename, and the other 9 differ only by `Self::`
shorthand, error-type aliasing, and rustfmt reflow. No serialization
logic changed.
- `./dev/rust_lint.sh`, `cargo machete`, and the extended test suite all
pass at HEAD. Also checked: `datafusion-proto` without `parquet`,
`datafusion-expr` with `proto` off and `--no-default-features`, the
format crates without `proto`, and `json` on both proto crates.

## Are there any user-facing changes?

Yes, and they restore rather than break the released API.

- The 39 conversions removed since 54.1.0 compile again. Trait impls are
global, so `X::try_from(&proto)` / `proto.try_into()` resolve regardless
of which crate now hosts the impl — no import changes needed, and no
upgrade-guide entry for the moves.
- One genuine delta remains: the parquet options conversions are
`TryFrom` rather than 54.1.0's infallible `From`. That predates this PR
— `main` had already made them fallible — but it is a real 54.1.0 ->
55.0.0 break and was undocumented, so it is now in the 55.0.0 upgrade
guide with a migration snippet.
- `FromProto` / `TryFromProto` and `convert_required_proto!` are gone.
Not a breaking change: they exist only on `main` and appear nowhere in
54.0.0 or 54.1.0.
- `datafusion-expr` gains an off-by-default `proto` feature. Additive.
- `datafusion-proto-models` gains a direct `datafusion-common`
dependency (already present transitively) and two new public modules.

Keeping the `api change` label for the parquet options fallibility.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Andrew Lamb <andrew@nerdnetworks.org>
kosiew pushed a commit to kosiew/datafusion that referenced this pull request Aug 12, 2026
…on-datasource (apache#24006)

## Which issue does this PR close?

- Part of apache#23494. Precursor for apache#23497 / apache#23683 (`DataSource` /
`FileSource` proto hooks) and for apache#23752.

## Rationale for this change

The protobuf conversions for the file-scan leaf types —
`PartitionedFile`,
`FileGroup`, `FileRange` — live in `datafusion-proto` as `TryFromProto`
impls,
because that is historically the only crate that can name both sides
(the
DataFusion type and the prost message are both foreign to it, hence the
`TryFromProto` workaround trait in the first place).

That placement means any *other* crate that needs those conversions has
to
reimplement them. apache#23683 hits exactly this: a `FileSource` serializing
its own
scan config needs to encode file groups, so the first cut of that PR
grew a
private second copy of the `PartitionedFile` wire logic inside
`datafusion-datasource`, which can then drift from the central
serializer.
The same will be true of every source migrated under apache#23516apache#23518.

Nothing about these conversions needs `datafusion-proto`: they are plain
data,
with `ScalarValue` / `Statistics` / `Schema` going through
`datafusion-proto-common`. They belong next to the types.

## What changes are included in this PR?

- New `datafusion_datasource::proto` module, behind a new `proto`
feature on
`datafusion-datasource` (off by default; `datafusion-proto` enables it):
  - `FileRange::try_to_proto` / `try_from_proto`
  - `PartitionedFile::try_to_proto` / `try_from_proto`
  - `FileGroup` <-> `protobuf::FileGroup`
- `datafusion-proto`'s `TryFromProto` impls for those types become
one-line
shims delegating to the new impls, so every existing caller keeps
working
  and the two sides cannot disagree.

### Why these are `TryFrom` and not `try_to_proto` hooks

`TryFromProto` exists because `datafusion-proto` owns neither side of
the
conversions it hosts: with both the DataFusion type and the prost
message
foreign to it, `impl TryFrom<protobuf::X> for X` is rejected by the
orphan
rule, so a local trait was the only way to say the same thing.

Moving a conversion into the crate that owns the DataFusion type removes
that
constraint, and `&T` is `#[fundamental]`, so both directions are
expressible
with the standard trait (checked, not assumed):

```rust
impl TryFrom<&protobuf::PartitionedFile> for PartitionedFile   // ok
impl TryFrom<&PartitionedFile> for protobuf::PartitionedFile   // ok
impl TryFrom<&[PartitionedFile]> for protobuf::FileGroup       // E0117
```

The last one is why `protobuf::FileGroup`'s *slice* conversion stays a
`TryFromProto` shim: `&[PartitionedFile]` is not a type this crate owns,
while
`&FileGroup` is. Callers inside DataFusion go through `FileGroup`.

So the rule this PR sets for the rest of apache#23494: **plain data uses
`TryFrom`;
anything needing an encode/decode context keeps the `try_to_proto(ctx)`
/
`try_from_proto(node, ctx)` hooks**, because the standard trait cannot
carry
that second argument. Usefully, none of the ~40
`TryFromProto`/`FromProto`
impls needs a context, and nothing that needs one was ever a
`TryFromProto`
impl — the two categories are already disjoint, so the shape now tells a
reader whether a conversion recurses.

### Why now

`FromProto` / `TryFromProto` were added in apache#21929, *after* the 54.0.0
release,
and 54.1.0 was cut before any of this landed — so they have never
shipped in a
release. Replacing them with the standard traits, and eventually
deleting them,
is a no-op for semver **today** and a major breaking change the moment
55.0.0
goes out. The same applies to the six inherent `try_to_proto` /
`try_from_proto`
methods this PR would otherwise have added: they are new, unreleased
API, so
choosing their final shape costs nothing right now.

The other reason to settle it here rather than in a follow-up: this is
the PR
that establishes the pattern for the data-source family (apache#23516-apache#23519
and
apache#23752 / apache#23781 are all queued behind it). Whichever shape merges first
is the
one they will copy.

Retiring the remaining ~34 impls is still its own follow-up. Two notes
for
whoever picks it up: the sink and format-option conversions can move
next to
their types the same way, but the ones for `datafusion-common`-owned
types
(`JoinType`, `NullEquality`, `TableReference`, `UnnestOptions`, ...)
cannot —
`datafusion-common` cannot depend on `datafusion-proto-models` (it is
underneath it via `datafusion-proto-common`). Their legal home is
`proto-models` itself, implementing on the local proto type, which is
already
how `proto-common` hosts the `ScalarValue` / `Statistics` conversions.

## Are these changes tested?

Yes.

- New unit tests in `datafusion_datasource::proto` covering the
`PartitionedFile` round trip (path, size, mtime, partition values,
range,
arrow schema, statistics), the `FileGroup` round trip, and the
invalid-path
  error.
- The existing `datafusion-proto` tests now exercise the delegating
shims, so
  they also pin the shims themselves.
- `datafusion-proto`, all features: 227 passed / 0 failed.
- `datafusion-datasource` with `proto`: 180 passed / 0 failed.
- Full workspace run: 10347 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`,
a crate this PR does not touch and which sits below every crate it does;
they
  fail the same way on the base commit on macOS.
- `cargo fmt` and `ci/scripts/rust_clippy.sh` clean.

## Are there any user-facing changes?

The protobuf wire format is unchanged, and no existing API changes
shape.

Additive:

- New `proto` feature on `datafusion-datasource` (off by default).
- New `TryFrom` impls in both directions between `FileRange`,
  `PartitionedFile`, `FileGroup` and their protobuf messages, under that
feature. No new names are added to the crate's API surface: the trait is
  `core::convert::TryFrom`.

Note for reviewers: while writing the round-trip test I found that
`PartitionedFile` statistics do not round-trip cleanly on `main` — filed
as
apache#23998. This PR preserves that behavior exactly rather than changing
decode
semantics in a refactor; the test documents it.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
kosiew pushed a commit to kosiew/datafusion that referenced this pull request Aug 12, 2026
apache#24003)

## Which issue does this PR close?

- Part of apache#23494 (the `try_to_proto` / `try_from_proto` migration).
Precursor for apache#23497 / apache#23683 / apache#23498 and for the remaining per-plan
migrations that need to encode an output partitioning or an ordering.

## 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 — apache#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. apache#23683 (`output_ordering`) and apache#23752 / apache#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 `PhysicalSortExpr`s and `ScalarValue`s 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.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
kosiew pushed a commit to kosiew/datafusion that referenced this pull request Aug 12, 2026
This is a stacked PR based on apache#23752. Only the commits on top of apache#23752
are part of this review.

## Which issue does this PR close?

<!--
We generally require a GitHub issue to be filed for all bug fixes and
enhancements and this helps us generate change logs for our releases.
You can link an issue to this PR using the GitHub syntax. For example
`Closes apache#123` indicates that this PR will close issue apache#123.
-->

- Closes apache#23519.

## Rationale for this change

<!--
Why are you proposing this change? If this is already explained clearly
in the issue then this section is not needed.
Explaining clearly why changes are proposed helps reviewers understand
your changes and offer better suggestions for fixes.
-->

apache#23752 adds the `DataSink::try_to_proto` hook and moves the shared
`FileSinkConfig` protobuf conversion into `datafusion-datasource`.

This PR uses that foundation to move CSV, JSON, and Parquet sink
serialization out of the central `datafusion-proto` downcast chain.
Each concrete sink now owns its format-specific protobuf encoding and
decoding logic.

## What changes are included in this PR?

<!--
There is no need to duplicate the description in the issue here but it
is sometimes worth providing a summary of the individual changes in this
PR.
-->

- Implement `DataSink::try_to_proto` for `CsvSink`, `JsonSink`, and
  `ParquetSink`.
- Add inherent `try_from_proto` methods to reconstruct each sink's
  `DataSinkExec`.
- Repoint the physical-plan decode arms to the sink-owned decoders.
- Add feature-gated protobuf dependencies to the three format crates.
- Remove the active central `DataSinkExec` serialization dispatch after
  migrating its final built-in sink.
- Retain the old serialization helpers as deprecated compatibility
  delegates.
- Let sinks without a built-in protobuf representation fall through to
  the physical extension codec.
- Preserve the existing protobuf wire representation.

The migrations are split into one commit per sink.


## Are these changes tested?

<!--
We typically require tests for all PRs in order to:
1. Prevent the code from being accidentally broken by subsequent changes
2. Serve as another way to document the expected behavior of the code

If tests are not included in your PR, please explain why (for example,
are they covered by existing tests)?
-->

Yes. Existing sink round-trip tests cover the protobuf representation.

The following checks passed:

- Focused CSV, JSON, and Parquet sink round-trip tests.
- All `datafusion-proto` integration tests.
- `cargo check -p datafusion-proto --no-default-features`.
- `cargo fmt --all`.
- `cargo clippy --all-targets --all-features -- -D warnings`.
- The required extended workspace test suite, including all 495 SQL
  logic test files.

## Are there any user-facing changes?

<!--
If there are user-facing changes then we may require documentation to be
updated before approving the PR.
-->

<!--
If there are any breaking changes to public APIs, please add the `api
change` label.
-->
No functional or wire-format changes are intended.

The old compatibility helpers remain available but are deprecated.

---------

Signed-off-by: Jiawei Zhao <Phoenix500526@163.com>
kosiew pushed a commit to kosiew/datafusion that referenced this pull request Aug 12, 2026
…ache#24205)

## Which issue does this PR close?

- Closes apache#24019.
- closes apache#23494

## Rationale for this change

`datafusion-proto` 54.1.0 publishes 39 `From` / `TryFrom` impls
converting between DataFusion types and their protobuf messages. On
`main` all of them were replaced by the crate-local `FromProto` /
`TryFromProto` traits introduced in apache#21929, so code written against the
released version stops compiling:

```rust
let proto = protobuf::PartitionedFile::try_from(&file)?;   // no longer resolves on main
let frame = WindowFrame::try_from(proto_frame)?;           // no longer resolves on main
```

That was collateral damage from the orphan-rule workaround, which was
needed during the migration but can now be unwound to result in no
breaking change across releases.

## What changes are included in this PR?

Each conversion moves to a crate that owns one side of it, and goes back
to being a plain `From` / `TryFrom` — the shape 54.1.0 published. Error
types are unchanged (`FromProtoError` decoding, `ToProtoError` encoding,
`DataFusionError` for the datasource types).

| Types | New home |
|---|---|
| `PartitionedFile`, `FileRange`, `FileGroup`, `JsonSink`, `CsvSink`,
`ParquetSink`, `FileSinkConfig` | already moved by apache#24006 / apache#23781 —
this PR just deletes the `TryFromProto` shims that delegated to them |
| `WindowFrame`, `WindowFrameBound`, `WindowFrameUnits`,
`MergeIntoClauseKind`, `NullTreatment` | `datafusion-expr`, behind a new
`proto` feature (optional `datafusion-proto-common` /
`datafusion-proto-models` deps, mirroring `datafusion-datasource`) |
| `UnnestOptions`, `TableReference`, `StringifiedPlan`, `JoinType`,
`JoinConstraint`, `NullEquality`, `CsvOptions`, `JsonOptions`, and the
parquet options types | `datafusion-proto-models`, on the local proto
type — their DataFusion side sits *below* that crate in the graph, the
same arrangement `datafusion-proto-common` already uses for
`ScalarValue` / `Statistics` |
| `CsvFormatFactory`, `JsonFormatFactory`, `ParquetFormatFactory` |
`datafusion-datasource-{csv,json,parquet}`, behind each crate's existing
`proto` feature |
| `Column` <-> `protobuf::PhysicalColumn` | `datafusion-physical-expr`;
`Column::try_to_proto` / `try_from_proto` now go through it instead of
building the message inline |

`TryFrom<&[PartitionedFile]> for protobuf::FileGroup` needed one extra
step. `datafusion-datasource` cannot host it — `&T` is `#[fundamental]`
but `[T]` is not, so `&[PartitionedFile]` counts as foreign there
(`error[E0117]: slices are always foreign`). But in
`datafusion-proto-models` the *self* type is local, which is all the
orphan rule needs, and staying generic over the element avoids naming
`PartitionedFile`, which sits above that crate in the graph:

```rust
impl<T> TryFrom<&[T]> for protobuf::FileGroup
where
    for<'a> &'a T: TryInto<protobuf::PartitionedFile, Error = DataFusionError>,
{ ... }
```

The bound is satisfied by `TryFrom<&PartitionedFile> for
protobuf::PartitionedFile` in `datafusion-datasource`, so
`protobuf::FileGroup::try_from(&files[..])` resolves for callers exactly
as it did in 54.1.0.

Two items beyond the issue's checklist, both needed to reach zero
implementors:

- the parquet options conversions (`ParquetOptions`,
`TableParquetOptions`, `ParquetColumnOptions`, `ParquetCdcOptions`) —
the issue's table undercounts `file_formats.rs` because they live in a
private module, but trait impls are global, so they were public API too.
They return as `TryFrom`; `main` had already made them fallible, so an
exact restore of 54.1.0's infallible `From` isn't available.
- `From<&protobuf::PhysicalColumn> for Column`, which the issue's
evidence table counts but no work item names.

Not restored, and worth calling out: `From<protobuf::dml_node::Type> for
WriteOp` and its reverse. `main` replaced them with `parse_write_op` /
`serialize_write_op` because `MergeInto` carries a payload a `From` impl
cannot express. That is a separate, deliberate change.

Finally, `convert.rs` and `convert_required_proto!` are deleted. apache#21929
introduced `FromProto` / `TryFromProto` so the `datafusion-proto-models`
extraction could land without relocating ~39 conversions at the same
time, and flagged them there as "a known workaround, not the end state",
with dropping them listed under Future work. With every conversion moved
they have no implementors and no callers. Neither trait has ever shipped
in a release, so they are removed outright rather than deprecated —
there is nothing for downstream users to migrate off, and doing it now
keeps the workaround out of the released API entirely.

## Are these changes tested?

Yes.

- New `datafusion/proto/tests/cases/public_conversions.rs` coerces all
45 proto conversions in the touched crates to `fn` pointers (the 39 from
54.1.0 plus the ones added on `main`). This is the regression guard the
issue asks for: it fails to compile when an impl is removed, and stays
quiet when one merely moves between crates, which is exactly the case
`cargo-semver-checks` cannot see.
- New round-trip tests next to the moved impls in `datafusion-expr` and
`datafusion-proto-models` (window frames, table references, join enums,
unnest options, stringified plans).
- The `PartitionedFile` tests move from `datafusion-proto` to
`datafusion-datasource`, alongside the logic they cover; two that
duplicated existing coverage there are dropped.
- Existing round-trip suites (`roundtrip_logical_plan`,
`roundtrip_physical_plan`) pass unchanged, which is the real wire-format
check.
- Every moved impl body was diffed against `main`: 22 are byte-identical
modulo the trait rename, and the other 9 differ only by `Self::`
shorthand, error-type aliasing, and rustfmt reflow. No serialization
logic changed.
- `./dev/rust_lint.sh`, `cargo machete`, and the extended test suite all
pass at HEAD. Also checked: `datafusion-proto` without `parquet`,
`datafusion-expr` with `proto` off and `--no-default-features`, the
format crates without `proto`, and `json` on both proto crates.

## Are there any user-facing changes?

Yes, and they restore rather than break the released API.

- The 39 conversions removed since 54.1.0 compile again. Trait impls are
global, so `X::try_from(&proto)` / `proto.try_into()` resolve regardless
of which crate now hosts the impl — no import changes needed, and no
upgrade-guide entry for the moves.
- One genuine delta remains: the parquet options conversions are
`TryFrom` rather than 54.1.0's infallible `From`. That predates this PR
— `main` had already made them fallible — but it is a real 54.1.0 ->
55.0.0 break and was undocumented, so it is now in the 55.0.0 upgrade
guide with a migration snippet.
- `FromProto` / `TryFromProto` and `convert_required_proto!` are gone.
Not a breaking change: they exist only on `main` and appear nowhere in
54.0.0 or 54.1.0.
- `datafusion-expr` gains an off-by-default `proto` feature. Additive.
- `datafusion-proto-models` gains a direct `datafusion-common`
dependency (already present transitively) and two new public modules.

Keeping the `api change` label for the parquet options fallibility.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Andrew Lamb <andrew@nerdnetworks.org>
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 datasource Changes to the datasource crate proto Related to proto crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proto: migrate the file sinks (CsvSink, JsonSink, ParquetSink)

4 participants