Skip to content

feat: add ASOF join physical operator - #23828

Open
Xuanwo wants to merge 15 commits into
apache:mainfrom
Xuanwo:xuanwo/asof-physical
Open

feat: add ASOF join physical operator#23828
Xuanwo wants to merge 15 commits into
apache:mainfrom
Xuanwo:xuanwo/asof-physical

Conversation

@Xuanwo

@Xuanwo Xuanwo commented Jul 23, 2026

Copy link
Copy Markdown
Member

Which issue does this PR close?

Rationale for this change

This is the first layer of the ASOF JOIN stack. It establishes a
broadcast-based physical execution contract independently so later
floating-point equality, logical-plan, SQL, DataFrame, and serialization changes
can be reviewed as smaller follow-up PRs.

The initial implementation deliberately favors the simpler broadcast design:
the right input must fit in memory and each left partition scans the shared
right-side batches. A repartitioned implementation can be evaluated separately
without changing the ASOF semantics introduced here.

Floating-point equality keys are rejected in this base layer because Arrow's
required sort order distinguishes -0.0 from +0.0 while join equality does
not. #24375 adds the required ordering normalization as an independently
reviewable layer.

What changes are included in this PR?

  • Add AsOfJoinExec for left-preserving, Snowflake-style ASOF semantics.
  • Coalesce and collect the ordered right input once, then share it across all
    left partitions.
  • Keep the left input partitioned so each partition can scan independently and
    preserve the left-side output partitioning.
  • Preserve merge state across input and output batch boundaries.
  • Reserve each retained Arrow buffer exactly once, including when right-side
    batches are zero-copy slices, and expose build, match, and output metrics.
  • Define output properties and statistics for the broadcast execution model.
  • Reject floating-point equality keys until feat: support floating-point ASOF equality keys #24375 supplies a sort/equality
    contract that handles signed zero correctly.
  • Add physical operator tests covering match directions, equality groups,
    batch boundaries, unmatched rows, invalid contracts, shared-buffer memory
    accounting, multi-partition broadcast execution, and float-key rejection.

Are these changes tested?

Yes:

  • cargo fmt --all
  • cargo clippy --all-targets --all-features -- -D warnings
  • cargo test -p datafusion-physical-plan joins::asof_join --all-features
  • Extended workspace tests from the contributor guide
  • FFI integration tests

Are there any user-facing changes?

This adds a new physical operator API. The base operator deliberately rejects
floating-point equality keys; #24375 adds full Float16, Float32, and Float64
support. SQL and DataFrame APIs are left to later dependent PRs.

@github-actions github-actions Bot added the physical-plan Changes to the physical-plan crate label Jul 23, 2026
@codecov-commenter

codecov-commenter commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.10818% with 253 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.22%. Comparing base (ec110ce) to head (5201c34).
⚠️ Report is 23 commits behind head on main.

Files with missing lines Patch % Lines
datafusion/physical-plan/src/joins/asof_join.rs 79.10% 197 Missing and 56 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #23828      +/-   ##
==========================================
+ Coverage   81.19%   81.22%   +0.03%     
==========================================
  Files        1110     1112       +2     
  Lines      388618   391419    +2801     
  Branches   388618   391419    +2801     
==========================================
+ Hits       315531   317937    +2406     
- Misses      54507    54794     +287     
- Partials    18580    18688     +108     

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

@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! This is a really good start. I have done a quick first pass and left some suggestions.

Comment thread datafusion/physical-plan/src/joins/asof_join.rs
Comment thread datafusion/physical-plan/src/joins/asof_join.rs
Comment thread datafusion/physical-plan/src/joins/asof_join.rs Outdated
Comment thread datafusion/physical-plan/src/joins/asof_join.rs Outdated
Comment thread datafusion/physical-plan/src/joins/asof_join.rs
vec![ChildStats::At(partition), ChildStats::Skip]
}

fn statistics_from_inputs(

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.

Just an idea to make this PR smaller, could we use the default implementation here? We could implement it later in a follow-up PR.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I kept this small override because ASOF has two exact facts the default would discard: the output row count equals the left row count, and unmodified left columns retain their statistics. Right-side column statistics remain unknown. I added a comment to make that scope explicit.

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 see, this makes sense.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks! I also updated it to preserve the same contract through the general projection.

Comment thread datafusion/physical-plan/src/joins/asof_join.rs Outdated
Comment thread datafusion/physical-plan/src/joins/asof_join.rs Outdated
Comment thread datafusion/physical-plan/src/joins/asof_join.rs Outdated
Comment thread datafusion/physical-plan/src/joins/asof_join.rs
@Xuanwo
Xuanwo marked this pull request as ready for review August 3, 2026 04:13
@Xuanwo
Xuanwo requested a review from 2010YOUY01 August 3, 2026 04:14
@2010YOUY01

Copy link
Copy Markdown
Contributor

There is no new commits after the previous review, you may have forgotten to push the local changes 🤔 @Xuanwo

@Xuanwo

Xuanwo commented Aug 7, 2026

Copy link
Copy Markdown
Member Author

There is no new commits after the previous review, you may have forgotten to push the local changes 🤔 @Xuanwo

Oh, sorry, latest commit is a127baa

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

I went over the implementation in detail, and I think it's well organized.

Need to do before merging

Before merging, I think we could add a few basic tests that run the executor and assert the results, just to cover some different cases:

  • No equality condition in on.
  • Different comparison operators in the match condition, such as < and >=.
  • Complex expressions in the on or match conditions, such as MATCH_CONDITION (l_c1 + l_c2) > r_c1 or ON l_c1 = (r_c1 + r_c2). I think these should be supported.

Optional suggestions

The main implementation/design questions I have are:

  1. We need to buffer batch_size output rows before emitting them, now it's implemented through the PendingRows struct, so we need to buffer all source batches to produce the final output. I think this uses more memory and makes the implementation more complex. An alternative is a) keep a in_progress_batch, and emit it after it reaches threshould b) only buffer one (left_batch, right_batch), and materialize its valid indices into the in_progress_batch when the cursor moves across it. This might be fast enough and simpler.
  2. Each loop iteration only advances the right index by one. We can probably explore some fast-forwarding optimization here.

But I suggest we first implement this end to end, including planning, SQL support, more tests, and benchmarks, before exploring these optimizations further. It should be easier to validate the ideas afterward.

For now, I only suggest trying to simplify the existing implementation or adding more documentation to make future iterations easier. I left a few suggestions in the comments.

Comment thread datafusion/physical-plan/src/joins/asof_join.rs Outdated
Comment thread datafusion/physical-plan/src/joins/asof_join.rs Outdated

fn input_distribution_requirements(&self) -> InputDistributionRequirements {
InputDistributionRequirements::new(vec![
Distribution::UnspecifiedDistribution,

@2010YOUY01 2010YOUY01 Aug 10, 2026

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.

Is it the case the optimizer will insert round-robin repartition, if we declare this UnspecifiedDistribution? It should be fine if it's doing so, I was a little bit confused by this name 🤔

vec![ChildStats::At(partition), ChildStats::Skip]
}

fn statistics_from_inputs(

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 see, this makes sense.

Comment thread datafusion/physical-plan/src/joins/asof_join.rs
Comment thread datafusion/physical-plan/src/joins/asof_join.rs Outdated
Comment thread datafusion/physical-plan/src/joins/asof_join.rs
Comment thread datafusion/physical-plan/src/joins/asof_join.rs Outdated
Comment thread datafusion/physical-plan/src/joins/asof_join.rs Outdated
Comment thread datafusion/physical-plan/src/joins/asof_join.rs Outdated
Co-authored-by: Yongting You <2010youy01@gmail.com>
@github-actions github-actions Bot added the auto detected api change Auto detected API change label Aug 11, 2026
@Xuanwo

Xuanwo commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

Thanks for the detailed review — this was very helpful. I added result-based coverage for no equality keys, both >= and <, and complex equality/match expressions. I also aligned projection with the existing join convention, simplified the metrics and tests, and documented the state-machine loop. I left the PendingRows redesign and right-cursor fast-forwarding for a benchmark-backed follow-up after the end-to-end SQL integration, as suggested.

@github-actions github-actions Bot removed the auto detected api change Auto detected API change label Aug 11, 2026
@Xuanwo
Xuanwo requested a review from 2010YOUY01 August 13, 2026 16:21
@2010YOUY01

Copy link
Copy Markdown
Contributor

Great! I got one tricky bug found by AI, otherwise it should be good to go.


Found one blocking correctness issue.


I'm not so sure if there is an easy fix, it it's hard we could first reject floats in the planning, and fix that in a separate PR.

@Xuanwo

Xuanwo commented Aug 14, 2026

Copy link
Copy Markdown
Member Author

I'm not so sure if there is an easy fix, it it's hard we could first reject floats in the planning, and fix that in a separate PR.

Checked a bit, seems to be a mid level fix. Let's fix that in a seperate PR #24375. We can get this in first! 💌

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

I hope we could have ASOFJoinStream that handles the AsOfJoinStreamState and performs the actual join. We could take inspiration from the existing join implementations, such as HashJoinStream.

#[derive(Default)]
struct PendingRows {
/// Distinct source batches referenced by `indices`.
sources: Vec<Arc<RecordBatch>>,

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.

Do we need Vec<Arc<RecordBatch>> and not just Vec<RecordBatch>

@Xuanwo Xuanwo Aug 16, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yep, Arc is intentional here: it keeps per-row clones O(1) and gives PendingRows a stable identity for deduplication. Added a short comment.

Comment thread datafusion/physical-plan/src/joins/asof_join.rs Outdated
Comment thread datafusion/physical-plan/src/joins/asof_join.rs Outdated
@Xuanwo
Xuanwo requested a review from jayzhan211 August 16, 2026 17:09

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

Thanks again!

/// flush pending rows without resetting either cursor or the candidate
/// ```
async fn next_batch(&mut self) -> Result<Option<RecordBatch>> {
fn poll_next_impl(

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 haven't fully figure out what's the best practice to implement the state machines to make them easy to read and extend, probably it is:

Not an issue for now, we could experiment in the future.

@Xuanwo

Xuanwo commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

cc @jayzhan211, @xudong963, and @alamb, would you like to take another look and move forward?

@2010YOUY01

Copy link
Copy Markdown
Contributor

cc @jayzhan211, @xudong963, and @alamb, would you like to take another look and move forward?

datafusion project typically wait for 24 hrs after approval to merge, so others can have a chance to look. This one is a larger PR, probably we could wait for 2 days, unless anyone need more time to review.

https://datafusion.apache.org/contributor-guide/pr_review.html#pr-review-mechanics

@Xuanwo

Xuanwo commented Aug 17, 2026

Copy link
Copy Markdown
Member Author

Got it, thank you @2010YOUY01 for the explanation 🙌

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