Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e546de5
feat: add ASOF join physical operator
Xuanwo Jul 23, 2026
bd58e1d
feat: add ASOF join logical semantics
Xuanwo Jul 23, 2026
fe3b182
feat: support ASOF JOIN SQL
Xuanwo Jul 23, 2026
68ac8ec
Merge remote-tracking branch 'origin/main' into xuanwo/asof-physical
Xuanwo Jul 23, 2026
c8c4813
Merge branch 'xuanwo/asof-physical' into xuanwo/asof-logical
Xuanwo Jul 23, 2026
67d65dd
Merge branch 'xuanwo/asof-logical' into xuanwo/asof-sql
Xuanwo Jul 23, 2026
73fd599
Merge remote-tracking branch 'origin/main' into xuanwo/asof-physical
Xuanwo Jul 27, 2026
43110a3
feat: broadcast ASOF right input
Xuanwo Jul 27, 2026
a15b693
Merge branch 'xuanwo/asof-physical' into xuanwo/asof-logical
Xuanwo Jul 27, 2026
0ddb676
Merge branch 'xuanwo/asof-logical' into xuanwo/asof-sql
Xuanwo Jul 27, 2026
23e6060
test: cover broadcast ASOF SQL execution
Xuanwo Jul 27, 2026
339d785
fix: account shared ASOF build buffers once
Xuanwo Jul 27, 2026
a71c19f
Merge branch 'xuanwo/asof-physical' into xuanwo/asof-logical
Xuanwo Jul 27, 2026
3a4f995
fix: align ASOF logical contracts
Xuanwo Jul 27, 2026
a503c09
Merge branch 'xuanwo/asof-logical' into xuanwo/asof-sql
Xuanwo Jul 27, 2026
fd8fddb
fix: align ASOF USING and broadcast docs
Xuanwo Jul 27, 2026
34552d1
perf: compare ASOF keys without scalar materialization
Xuanwo Jul 28, 2026
d618879
test: cover temporal and multi-partition ASOF joins
Xuanwo Jul 28, 2026
c967002
Merge branch 'xuanwo/asof-physical' into xuanwo/asof-logical
Xuanwo Jul 28, 2026
80f7572
Merge branch 'xuanwo/asof-logical' into xuanwo/asof-sql
Xuanwo Jul 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion datafusion/core/src/physical_planner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ use crate::physical_plan::explain::ExplainExec;
use crate::physical_plan::filter::FilterExecBuilder;
use crate::physical_plan::joins::utils as join_utils;
use crate::physical_plan::joins::{
CrossJoinExec, HashJoinExec, NestedLoopJoinExec, PartitionMode, SortMergeJoinExec,
AsOfJoinExec, AsOfMatchExpr, CrossJoinExec, HashJoinExec, NestedLoopJoinExec,
PartitionMode, SortMergeJoinExec,
};
use crate::physical_plan::limit::{GlobalLimitExec, LocalLimitExec};
use crate::physical_plan::projection::{ProjectionExec, ProjectionExpr};
Expand Down Expand Up @@ -1865,6 +1866,53 @@ impl DefaultPhysicalPlanner {
join
}
}
LogicalPlan::AsOfJoin(join) => {
let [physical_left, physical_right] = children.two()?;
let join_on = join
.on
.iter()
.map(|(left, right)| {
Ok((
create_physical_expr(
left,
join.left.schema(),
execution_props,
planning_ctx,
)?,
create_physical_expr(
right,
join.right.schema(),
execution_props,
planning_ctx,
)?,
))
})
.collect::<Result<join_utils::JoinOn>>()?;
let match_condition = AsOfMatchExpr::new(
create_physical_expr(
&join.match_condition.left,
join.left.schema(),
execution_props,
planning_ctx,
)?,
join.match_condition.op,
create_physical_expr(
&join.match_condition.right,
join.right.schema(),
execution_props,
planning_ctx,
)?,
);
let right_output_indices =
(0..join.right.schema().fields().len()).collect();
Arc::new(AsOfJoinExec::try_new(
physical_left,
physical_right,
join_on,
match_condition,
right_output_indices,
)?)
}
LogicalPlan::RecursiveQuery(RecursiveQuery {
name,
is_distinct,
Expand Down Expand Up @@ -2364,6 +2412,7 @@ fn extract_dml_filters(
| LogicalPlan::Sort(_)
| LogicalPlan::Union(_)
| LogicalPlan::Join(_)
| LogicalPlan::AsOfJoin(_)
| LogicalPlan::Repartition(_)
| LogicalPlan::Aggregate(_)
| LogicalPlan::Window(_)
Expand Down
42 changes: 42 additions & 0 deletions datafusion/core/tests/physical_optimizer/filter_pushdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ use datafusion_physical_plan::{
coalesce_partitions::CoalescePartitionsExec,
collect,
filter::{FilterExec, FilterExecBuilder},
joins::{AsOfJoinExec, AsOfMatchExpr},
projection::ProjectionExec,
repartition::RepartitionExec,
sorts::sort::SortExec,
Expand Down Expand Up @@ -123,6 +124,47 @@ fn test_pushdown_volatile_functions_not_allowed() {
);
}

#[test]
fn test_asof_join_pushes_only_left_filters() {
let left = TestScanBuilder::new(schema()).with_support(true).build();
let right = TestScanBuilder::new(schema()).with_support(true).build();
let join = Arc::new(
AsOfJoinExec::try_new(
left,
right,
vec![(col("a", &schema()).unwrap(), col("a", &schema()).unwrap())],
AsOfMatchExpr::new(
col("b", &schema()).unwrap(),
Operator::GtEq,
col("b", &schema()).unwrap(),
),
vec![0, 1, 2],
)
.unwrap(),
);
let left_filter = Arc::new(BinaryExpr::new(
Arc::new(Column::new("c", 2)),
Operator::Gt,
Arc::new(Literal::new(ScalarValue::Float64(Some(0.0)))),
)) as Arc<dyn PhysicalExpr>;
let right_filter = Arc::new(BinaryExpr::new(
Arc::new(Column::new("c", 5)),
Operator::Gt,
Arc::new(Literal::new(ScalarValue::Float64(Some(0.0)))),
)) as Arc<dyn PhysicalExpr>;
let predicate = Arc::new(BinaryExpr::new(left_filter, Operator::And, right_filter));
let plan = Arc::new(FilterExec::try_new(predicate, join).unwrap());
let mut config = ConfigOptions::default();
config.execution.parquet.pushdown_filters = true;
let optimized = FilterPushdown::new().optimize(plan, &config).unwrap();
let formatted = format_plan_for_test(&optimized);

assert!(formatted.contains("FilterExec: c@5 > 0"), "{formatted}");
assert!(formatted.contains("AsOfJoinExec:"), "{formatted}");
assert!(formatted.contains("predicate=c@2 > 0"), "{formatted}");
assert_eq!(formatted.matches("predicate=").count(), 1, "{formatted}");
}

/// Show that we can use config options to determine how to do pushdown.
#[test]
fn test_pushdown_into_scan_with_config_options() {
Expand Down
Loading
Loading