Skip to content

[branch-55] fix: ensure deferred-filtered outer joins preserve streamed output order (#24573) - #27358

Merged
alamb merged 1 commit into
apache:branch-55from
Sunninsky:backport_24573_branch-55
Nov 12, 2026
Merged

alamb merged 1 commit into
apache:branch-55from
Sunninsky:backport_24573_branch-55

Conversation

@Sunninsky

Copy link
Copy Markdown

Which issue does this PR close?

Rationale for this change

LEFT, RIGHT and FULL sort-merge joins with a join filter can return rows out of order. These join types advertise that they preserve the order of one input (maintains_input_order is [true, false] for LEFT), so an operator above is allowed to rely on it. Deferred-filtered joins stage their output in a second BatchCoalescer (self.output), but the final flush at end of input bypassed it and emitted its batch directly, so rows still buffered in output from an earlier flush came out after it.

A LEFT JOIN where some keys match large buffered groups and the trailing keys match one row each reproduces it: the streamed keys come back as [5, 6, 0, 1, 2, 3, 4] instead of [0, 1, 2, 3, 4, 5, 6]. Through DataFusion Comet we saw a native aggregate above such a join trust the advertised order, close a group early and return it twice (8 rows where Spark returns 7). This is a wrong result, which makes it eligible for backport.

#24573 fixed it on main (labelled v56.0.0); it is on branch-56 but not on branch-55, so it is in neither 55.1.0 nor 55.2.0. It was requested for 55.3.0 on #26868 (#26868 (comment)).

What changes are included in this PR?

Are these changes tested?

Yes. On this branch (Rust 1.95.0) I ran:

  • cargo test -p datafusion-physical-plan --lib: 1705 passed, including the five tests fix: ensure deferred-filtered outer joins preserve streamed output order #24573 added.
  • With materializing_stream.rs reverted to this branch's version and the tests kept, cargo test -p datafusion-physical-plan --lib sort_merge_join fails the three streamed-order tests (left_join_with_filter_preserves_streamed_order, right_join_with_filter_preserves_streamed_order, left_join_with_partial_filter_preserves_streamed_order).
  • cargo test -p datafusion-sqllogictest --test sqllogictests -- join: all 16 matching files pass.
  • cargo check -p datafusion-physical-plan --benches compiles the changed benchmark.
  • cargo fmt --all -- --check and cargo clippy -p datafusion-physical-plan --all-targets --all-features -- -D warnings pass.

Are there any user-facing changes?

Outer sort-merge joins with a join filter return rows in the order they advertise. No public API change.

🤖 Generated with Claude Code

…der (apache#24573)

`LEFT`/`RIGHT`/`FULL` sort-merge joins with a join filter could return
rows out of
order. These join types advertise that they preserve the ordering of one
input
(`maintains_input_order` is `[true, false]` for `LEFT`), so downstream
operators are
allowed to rely on it.

Deferred-filtered joins stage their output in a second `BatchCoalescer`
(`self.output`)
because the filter correction step emits ragged batch sizes. The final
flush at
end-of-input bypassed that buffer and emitted its batch directly, so any
rows still
buffered in `output` from an earlier flush were emitted *after* it.

A `LEFT JOIN` where some keys match large buffered groups and the
trailing keys match a
single row each reproduces this: the large groups trip the flush gate
and push
sub-threshold batches that stay buffered, while the trailing keys never
trip the gate
and land in the final flush. Streamed keys came back as `[5, 6, 0, 1, 2,
3, 4]` instead
of `[0, 1, 2, 3, 4, 5, 6]`.

Bug fix:

- `on_children_exhausted` now pushes the final filtered batch into
`self.output` instead
of emitting it directly, so all deferred-filtered output leaves through
a single
  buffer and stays in order.

Cleanups in the same file, no behavior change:

- `emit_completed_output` drains every completed batch from
`self.output`; previously
  each flush emitted at most one and left the rest buffered.
- `join_arrays` returns `Result` instead of `unwrap()`-ing. A failing
join-key
expression previously panicked the worker thread. `StreamedBatch::new`
and
  `BufferedBatch::new` became `try_new`.
- `materialize_right_columns` maps buffered batch indices to interleave
sources with a
linear scan instead of a `HashMap` — a key group spans a handful of
batches at most,
  and this ran per matched chunk.
- Extracted `new_output_coalescer`, replacing four copies of the same
`BatchCoalescer::new(..).with_biggest_coalesce_batch_size(..)`
construction.

Yes. Added `left_join_with_filter_preserves_streamed_order`, which
builds the mixed
group-size shape described above and asserts the streamed key column
comes back in
order. It fails on `main` with `[5, 6, 0, 1, 2, 3, 4]`.

Also ran the full `datafusion-physical-plan` test suite and the joins
sqllogictests.

Yes — outer sort-merge joins with a join filter now return rows in the
order the
operator claims to produce them. No API changes.

(cherry picked from commit d1fe988)

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
@github-actions github-actions Bot added the physical-plan Changes to the physical-plan crate label Nov 12, 2026

@alamb alamb 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.

Looks good -- thank you. I verified this is a clean cherry pick

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.50575% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.24%. Comparing base (4f4d73f) to head (b7c2917).

Files with missing lines Patch % Lines
.../src/joins/sort_merge_join/materializing_stream.rs 88.50% 2 Missing and 8 partials ⚠️
@@              Coverage Diff              @@
##           branch-55   #27358      +/-   ##
=============================================
- Coverage      81.24%   81.24%   -0.01%     
=============================================
  Files           1110     1110              
  Lines         389341   389351      +10     
  Branches      389341   389351      +10     
=============================================
+ Hits          316324   316331       +7     
+ Misses         54446    54440       -6     
- Partials       18571    18580       +9     
Files with missing lines Coverage Δ
.../src/joins/sort_merge_join/materializing_stream.rs 89.82% <88.50%> (+0.53%) ⬆️

... and 7 files with indirect coverage changes

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

@alamb
alamb marked this pull request as ready for review November 12, 2026 23:07
@alamb
alamb merged commit e6f7f5d into apache:branch-55 Nov 12, 2026
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

physical-plan Changes to the physical-plan crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants