Skip to content

Upgrade DataFusion fork to 55.0.0 and preserve fork patches - #73

Merged
xudong963 merged 14 commits into
branch-55from
upgrade/datafusion-55-fork-patches
Aug 25, 2026
Merged

Upgrade DataFusion fork to 55.0.0 and preserve fork patches#73
xudong963 merged 14 commits into
branch-55from
upgrade/datafusion-55-fork-patches

Conversation

@xudong963

@xudong963 xudong963 commented Aug 23, 2026

Copy link
Copy Markdown

Rationale for this change

Upgrade the maintained fork from DataFusion 54 to Apache DataFusion 55.0.0 while preserving all production and maintained-branch patches.

What changes are included in this PR?

  • Accounts for all 31 fork commits (22 since Apache 54.0.0 + 9 fixes-branch-only): 27 Upstreamed, 2 Ported, 2 Reimplemented, 0 Blocked.
  • Restores the missing SMJ ordering and Interleave fallback behavior.
  • Ports projected-equivalence reuse and concrete grouped Top-K storage.
  • Retains reverse Parquet row-group controls.
  • Preserves Parquet schema adaptation and follows DF55's scalar max_in_list_size protobuf encoding.
  • Updates h2 to resolve RUSTSEC-2026-0258.

Baseline: d5552342012888b7d1a3ab88d92e3d292fc0cde0
Head: 0437d4fc10ab72e60d5a041b77f29da08d6f825f

Are these changes tested?

Yes. Formatting, all-target/all-feature Clippy, the pre-review physical-plan/workspace suites, 35 focused Top-K tests, targeted review regressions, all 503 sqllogictest files, and cargo-audit pass. No snapshots changed.

Are there any user-facing changes?

The existing reverse-row-group API remains public. DF54-fork serialized plans must be drained before rollout because their protobuf field 37 conflicts with DF55.

Port the sort-merge join behavior carried by fork commit 0f7361a onto DataFusion 55. Preserve descending sort options already provided by the left input and default only unmatched join keys.
Reimplement the fallback carried by fork commit 0f7361a for DataFusion 55's replace_children API. Compatible children retain InterleaveExec; incompatible partitioning safely degrades to UnionExec.
Port fork commit b8f1618 using the final Apache implementation from PR apache#24445 (f951020), which landed after the 55.0.0 tag.
Carry forward the end-to-end regressions from fork commits 0f7361a, 5df21f6, and 87fa898. DataFusion 55's new reader architecture passes all three without implementation changes.
Port the downstream-facing ParquetSource API from fork commit 0f7361a. DataFusion 55 retained the behavior internally but restricted the setter and getter to crate tests.
Reimplements fork commit 32b0778 for DataFusion 55. Keep max_row_group_bytes on upstream field 37 while using an optional oneof on field 38 so omitted values retain the documented default and explicit zero still disables IN-list pruning.

The old fork's field-37 encoding collides with DataFusion 55's max_row_group_bytes and cannot be decoded unambiguously; the upgrade compatibility guide documents the required rolling-upgrade boundary.
Adds focused regression coverage for the expressionless extension-node fast path carried by fork commit 0f7361a. Mapping expressions must keep the original extension Arc and avoid with_exprs_and_inputs reconstruction.
Ports Apache DataFusion commit f6ad781 (PR apache#24467) onto the 55.0.0 fork baseline. h2 0.4.16 fixes RUSTSEC-2026-0258 and restores the maintained fork's security-audit requirement without adding an ignore.
Port fork commit 2465b2c (X-3345) to DataFusion 55 while preserving the new all-NULL group bookkeeping. Store concrete Arrow arrays in primitive heaps and hash tables, borrow string keys for comparisons, and swap heap slots in place.
Document the protobuf field-37 collision and missing dynamic-filter expr_id incompatibility, with an explicit stop, drain, coordinated-upgrade, and resume checklist.
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Aug 24, 2026
@github-actions github-actions Bot removed the documentation Improvements or additions to documentation label Aug 24, 2026
@xudong963
xudong963 marked this pull request as ready for review August 24, 2026 07:06

@zhuqi-lucas zhuqi-lucas left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Went through this with two questions in mind: are the branch-54 fork patches and their tests all accounted for, and is the new code in this PR correct.

On preservation, it holds up. I checked each fork feature by content rather than by commit, and all of them are present: max_in_list_size, concrete Top-K storage, UDTFs in information_schema.routines, the delegated proto downcasts, the X-2935 dynamic-filter dedup, the EliminateCrossJoin fast path, maintains_input_order in EnforceDistribution (now under ensure_requirements/), reverse Parquet row-group controls, sorting_columns, and the projected-equivalence reuse.

Test coverage holds up too, and several things that look missing by name are not. The Parquet schema-adaptation tests were renamed (test_schema_adaptation_* to schema_adaptation_*) and that file went from 29 tests to 40. The dynamic-filter tests moved into proto/tests/cases/plans/dynamic_filters.rs and grew from 6 to 10, with the BinaryExpr-nesting and with_new_children dedup semantics both still covered. All four SMJ sort-option cases survive under new names and now use insta snapshots. .slt went from 474 files to 504. The one fork test with no successor, test_with_new_children_keeps_properties_when_the_child_is_unchanged, covered a mechanism that DF 55 deprecates (with_new_children_and_same_properties, retired alongside with_new_children in favour of replace_children_if_necessary), so dropping it is right and the three survivors correctly follow the rename to test_replace_children_*. Nothing to do there.

CI is green across all 45 checks including the sqlite suite, Postgres runner and Substrait round-trip.

The one I would like to resolve before merge is the max_in_list_size oneof, commented inline. Short version: branch-55 already has that field from upstream at the same tag with the same default, so rather than preserving a fork patch this introduces a new fork-only divergence in the wire schema. The other two proto comments are consequences of it.

Two process questions:

The description says 31 fork commits across the four categories, but upstream/branch-54..origin/branch-54 is 18 non-merge commits. I assume the difference is that #56 and #64 bundled earlier patches, which is fine, but since the classification table is the main evidence here it would help to say what the 31 is counted against.

This upgrade folds EnforceDistribution and EnforceSorting into ensure_requirements/, which is the precondition for X-2812 on the Atlas side. Atlas runs multiple EnforceSort/Dist passes of its own, so bumping the rev is not going to be the whole job. Worth flagging to whoever picks up the Atlas-side bump.

uint64 max_in_list_size = 38;
// Presence distinguishes an omitted value (default 20) from an explicit
// zero, which disables IN-list pruning.
oneof max_in_list_size_opt {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is the one I would most like to revisit before merge.

branch-55 already carries this field from upstream, identically: config.rs:1294 has pub max_in_list_size: usize, default = 20, and this same proto file has uint64 max_in_list_size = 38; at line 620. Upstream main has both at the same tag with the same default. So the X-3375 backport has been subsumed upstream, and there is no fork patch left to preserve here.

What this hunk does is take upstream's plain field and turn it into a fork-only oneof. That is a new divergence rather than a preserved one, and it lands in the proto schema, where the cost is highest: our nodes now interpret an absent field differently from any upstream-based DataFusion, and every future upgrade has to re-apply the patch.

The stated motivation, that plans predating the field should not read as 0 and silently disable IN-list pruning, is a real concern. But it is equally a concern upstream, and if it is worth fixing it seems worth fixing there so everyone benefits and we stop carrying it.

The practical exposure also looks small: plans are serialized per RemoteExec request rather than persisted, branch-54 has carried the field since X-3375 so every running node writes a value, and reaching the absent case needs a node older than that backport in the rolling window.

Could this hunk go back to upstream's version? Dropping it also removes the two issues I flagged below, since both come from this change.

Also a wording point on the PR description: this is listed under "Preserves ... max_in_list_size protobuf semantics", but upstream's semantics are the plain field. This changes them.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed. 7c67bad drops the provisional oneof; the schema, converters, and generated code now follow the branch-55 scalar field 38 representation. I also updated the PR accounting so X-3375 is Upstreamed rather than Reimplemented.

// which disables IN-list pruning.
max_in_list_size: value.max_in_list_size_opt.map(|opt| match opt {
protobuf::parquet_options::MaxInListSizeOpt::MaxInListSize(v) => v as usize,
}).unwrap_or(20),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Two things here, both of which go away if the oneof above is reverted to upstream's plain field.

First, 20 is duplicated from config.rs:1294 (default = 20). If that default ever moves, this path silently keeps the old value, and the new test_parquet_options_max_in_list_size_absent_uses_default asserts the literal 20 too, so it would not catch the drift. ParquetOptions::default().max_in_list_size would keep the two tied together.

Second, the presence change is not symmetric across versions. In proto3 a plain scalar has implicit presence, so a writer on the old schema with max_in_list_size = 0 emits nothing for tag 38, which is byte-identical to never having set it. A reader on this branch maps that absence to 20. So during a rolling deploy an old node that deliberately set 0 to disable IN-list pruning has that decision read back as 20, which enables it.

Only that one direction is affected: new writers always emit the tag, so old readers are fine, and any non-zero value round-trips either way. It is not a correctness issue, only which row groups get read. It is also genuinely unrecoverable information rather than something this PR lost, since the two cases were already indistinguishable on the wire. Worth a line in the description about the upgrade window if the oneof stays.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Removed with 7c67bad. The duplicated default and asymmetric adapter disappear because both conversion paths now use the upstream scalar field directly.

DataType::Utf8View => Arc::new(StringViewArray::from(ids)),
_ => unreachable!(),
match &self.owned {
StringArrayType::Utf8(_) => Arc::new(StringArray::from(ids)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

take_all used to pick the output array type from self.data_type, the type the table was constructed with. It now picks it from self.owned, which set_batch overwrites with whatever array arrives.

Those agree as long as every batch carries exactly the declared string variant, which should hold. What changes is the failure mode if it ever does not: previously extract_string_value would have hit as_string::<i32>() on the wrong variant and panicked loudly, whereas now the table quietly adapts and take_all emits an array whose type may not match the plan's output schema.

Same pattern for the primitive table at line 312, where self.kt.clone() became self.owned.data_type().clone(). That one worries me slightly more because the parameterised types carry metadata: Decimal128(p, s) and Timestamp(unit, tz) would follow the last batch rather than the declared key type.

I could not construct a case where a batch actually disagrees with the declared type, so this may be entirely theoretical. But since the fields that pinned the intent (data_type, kt) were removed as part of the change, is it worth keeping one of them and asserting the batch matches in set_batch, so a mismatch stays loud?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 00951ea. Both hash-table implementations retain the declared DataType, set_batch asserts an exact type match, and take_all emits the declared type. New regressions cover Utf8 versus LargeUtf8 and Timestamp timezone metadata; all 35 focused Top-K tests pass.

use super::*;
use crate::UserDefinedLogicalNodeCore;

static REBUILD_COUNT: AtomicUsize = AtomicUsize::new(0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Small consistency point. This is a process-global counter, while the probe added in equivalence/properties/mod.rs deliberately uses a thread_local with the comment that "the test binary runs tests in parallel, and a shared count would let one test observe another's hits".

Only map_expressions_does_not_rebuild_expressionless_extension touches REBUILD_COUNT today and it stores 0 first, so nothing races right now. But the second test that uses it would race, and the same PR argues against exactly this shape a few files over. Worth matching the thread_local approach here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 0437d4f by removing the shared counter entirely. Arc::ptr_eq already proves that the extension node was not rebuilt, since rebuilding would allocate a different Arc, so the test no longer needs shared probe state.

Drop the fork-only presence oneof and restore DataFusion 55's scalar field 38 representation. This avoids carrying a divergent wire schema for ephemeral physical plans.
Keep the constructor's Arrow type as the output source of truth and reject batches whose full type metadata differs. This prevents string variants, decimal metadata, or timestamp timezones from being silently adopted from the latest batch.
Use pointer identity alone to prove the expressionless extension was not rebuilt, removing the redundant process-global counter and its potential for cross-test interference.
@xudong963

Copy link
Copy Markdown
Author

Addressed the four inline threads. The description now defines the 31-patch inventory as 22 commits since Apache 54.0.0 plus 9 fixes-branch-only commits. The Atlas EnsureRequirements migration remains downstream work tracked by X-2812.

@xudong963
xudong963 merged commit 9bf6d74 into branch-55 Aug 25, 2026
71 checks passed
@xudong963
xudong963 deleted the upgrade/datafusion-55-fork-patches branch August 25, 2026 01:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants