Skip to content

refactor: CrossJoinStream (simplifying, less state, async generator pattern) - #24291

Open
saadtajwar wants to merge 15 commits into
apache:mainfrom
saadtajwar:saadtajwar/refactor-crossjoinstream
Open

refactor: CrossJoinStream (simplifying, less state, async generator pattern)#24291
saadtajwar wants to merge 15 commits into
apache:mainfrom
saadtajwar:saadtajwar/refactor-crossjoinstream

Conversation

@saadtajwar

@saadtajwar saadtajwar commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

The code for CrossJoinStream had an opportunity to be simplified by removing much of the state related to the polling implementation

What changes are included in this PR?

Removed the polling mechanisms in CrossJoinStream and the state associated with it, and instead simplified to a simple async-generator pattern of fetching the build side -> while there are batches available on the right, fetch them and perform the join

Are these changes tested?

Yes

Are there any user-facing changes?

No, these are internal to the physical execution of the join

@github-actions github-actions Bot added the physical-plan Changes to the physical-plan crate label Aug 12, 2026
@saadtajwar saadtajwar changed the title refactor: CrossJoinStream (simplifying, less state, async) refactor: CrossJoinStream (simplifying, less state, async generator pattern) Aug 12, 2026
…-crossjoinstream

# Conflicts:
#	datafusion/physical-plan/src/joins/cross_join.rs
@codecov-commenter

codecov-commenter commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.45283% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.14%. Comparing base (1164d60) to head (0840347).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
datafusion/physical-plan/src/joins/cross_join.rs 92.45% 1 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #24291      +/-   ##
==========================================
- Coverage   81.14%   81.14%   -0.01%     
==========================================
  Files        1112     1112              
  Lines      386933   386893      -40     
  Branches   386933   386893      -40     
==========================================
- Hits       313978   313935      -43     
- Misses      54462    54465       +3     
  Partials    18493    18493              

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

@saadtajwar

Copy link
Copy Markdown
Contributor Author

@rluvaton & @2010YOUY01 & @buraksenn - ready for review! Thanks in advance for the feedback - this was super fun to work on 😁

@2010YOUY01 2010YOUY01 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.

Thank you! I think for the cross join's complexity, the explicit state management is not necessary, and the generator pattern make it look better.

I have left some suggestions for you to consider.

right: stream,
join_metrics,
left_data: RecordBatch::new_empty(self.left().schema()),
batch_size: enforce_batch_size_in_joins.then_some(batch_size),

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.

should we remove this enforce_batch_size_in_joins?

This should be introduced due to some legacy implementation tries to materialize total_left_buffer_len * right_batch_len at once, now it only materialize one_left_row * right_batch_len in each step, so the underlying restriction is already followed

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.

Ooh I see, yes that makes sense to me!

}

while let Some(right_batch) = self.fetch_probe_batch().await? {
self.process_right_batch(&right_batch, emitter).await?

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.

It seems we can put fetch_probe_batch inside process_right_batch to make it even simpler

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.

Ooh yeah - done!

build_batch(left_index, right_batch, &self.left_data, &self.schema)?;
join_timer.done();

if let Some(batch_size) = self.batch_size {

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.

This slicing step seems not necessary. The result has length the same as right_batch, and the upstream operator should already ensure it's less than batch_size config (this is a global convention for operators)

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.

Removed the batch sizing from the first comment :)

}

#[tokio::test]
async fn test_join_enforce_batch_size_splits_output() -> Result<()> {

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.

Suggested change
async fn test_join_enforce_batch_size_splits_output() -> Result<()> {
// Cross join should follow the global output-size convention: all output batches
// should have fewer than `batch_size` rows, as specified in the config.
async fn test_cross_join_output_length() -> Result<()> {

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.

Removed this test as we removed the batch sizing!

}

#[tokio::test]
async fn elapsed_compute_excludes_probe_input_wait() -> Result<()> {

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.

I found this test and the next one is quite hard to figure out their test goals, are they necessary? If they're auto generated we could remove them, else it would be great to better explain their test goals.

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.

Removed these :)

@saadtajwar
saadtajwar requested a review from 2010YOUY01 August 13, 2026 16:26
@saadtajwar

Copy link
Copy Markdown
Contributor Author

@2010YOUY01 - thank you for the PR review! Pushed changes to address your comments, please let me know your thoughts!

…-crossjoinstream

# Conflicts:
#	datafusion/physical-plan/src/joins/cross_join.rs
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.

3 participants