-
Notifications
You must be signed in to change notification settings - Fork 2.4k
feat: add ASOF join logical semantics #23829
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,10 +31,10 @@ use crate::expr_rewriter::{ | |
| rewrite_sort_cols_by_aggs, | ||
| }; | ||
| use crate::logical_plan::{ | ||
| Aggregate, Analyze, Distinct, DistinctOn, EmptyRelation, Explain, Filter, Join, | ||
| JoinConstraint, JoinType, Limit, LogicalPlan, Partitioning, PlanType, Prepare, | ||
| Projection, Repartition, Sort, SubqueryAlias, TableScanBuilder, Union, Unnest, | ||
| Values, Window, | ||
| Aggregate, Analyze, AsOfJoin, AsOfMatch, Distinct, DistinctOn, EmptyRelation, | ||
| Explain, Filter, Join, JoinConstraint, JoinType, Limit, LogicalPlan, Partitioning, | ||
| PlanType, Prepare, Projection, Repartition, Sort, SubqueryAlias, TableScanBuilder, | ||
| Union, Unnest, Values, Window, | ||
| }; | ||
| use crate::select_expr::SelectExpr; | ||
| use crate::utils::{ | ||
|
|
@@ -1007,6 +1007,68 @@ impl LogicalPlanBuilder { | |
| ) | ||
| } | ||
|
|
||
| /// Apply a left-preserving ASOF join using equality expressions and one | ||
| /// ordered match condition. | ||
| pub fn asof_join( | ||
| self, | ||
| right: LogicalPlan, | ||
| on: Vec<(Expr, Expr)>, | ||
| match_condition: AsOfMatch, | ||
|
Comment on lines
+1015
to
+1016
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is the user-facing API (see the example code in the Perhaps: on: Expr,
match_condition: Expr,For example, if the SQL input is
But I think we can proceed as is and potentially change it in the SQL integration PR. It would be obvious which design is better once we try to build a plan from SQL. And we're likely to do that before the next release, so API changes are fine.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. Should be fixed in #23830 |
||
| ) -> Result<Self> { | ||
| self.asof_join_with_constraint(right, on, match_condition, JoinConstraint::On) | ||
| } | ||
|
|
||
| /// Apply a left-preserving ASOF join using `USING` equality keys. | ||
| pub fn asof_join_using( | ||
| self, | ||
| right: LogicalPlan, | ||
| using_keys: Vec<Column>, | ||
| match_condition: AsOfMatch, | ||
| ) -> Result<Self> { | ||
| let on = using_keys | ||
| .into_iter() | ||
| .map(|key| { | ||
| let left = Self::normalize(&self.plan, key.clone())?; | ||
| let right = Self::normalize(&right, key)?; | ||
| Ok((Expr::Column(left), Expr::Column(right))) | ||
| }) | ||
| .collect::<Result<_>>()?; | ||
| self.asof_join_with_constraint(right, on, match_condition, JoinConstraint::Using) | ||
| } | ||
|
|
||
| fn asof_join_with_constraint( | ||
| self, | ||
| right: LogicalPlan, | ||
| on: Vec<(Expr, Expr)>, | ||
| match_condition: AsOfMatch, | ||
| join_constraint: JoinConstraint, | ||
| ) -> Result<Self> { | ||
| let normalize = |expr, schema: &DFSchema| { | ||
| normalize_col_with_schemas_and_ambiguity_check(expr, &[&[schema]], &[]) | ||
| }; | ||
| let on = on | ||
| .into_iter() | ||
| .map(|(left, right_expr)| { | ||
| Ok(( | ||
| normalize(left, self.plan.schema())?, | ||
| normalize(right_expr, right.schema())?, | ||
| )) | ||
| }) | ||
| .collect::<Result<_>>()?; | ||
| let match_condition = AsOfMatch { | ||
| left: normalize(match_condition.left, self.plan.schema())?, | ||
| op: match_condition.op, | ||
| right: normalize(match_condition.right, right.schema())?, | ||
| }; | ||
| Ok(Self::new(LogicalPlan::AsOfJoin(AsOfJoin::try_new( | ||
| self.plan, | ||
| Arc::new(right), | ||
| on, | ||
| match_condition, | ||
| join_constraint, | ||
| )?))) | ||
| } | ||
|
|
||
| pub(crate) fn normalize(plan: &LogicalPlan, column: Column) -> Result<Column> { | ||
| if column.relation.is_some() { | ||
| // column is already normalized | ||
|
|
@@ -1776,6 +1838,14 @@ pub fn build_join_schema( | |
| dfschema.with_functional_dependencies(func_dependencies) | ||
| } | ||
|
|
||
| /// Creates the schema for a left-preserving ASOF join. | ||
| /// | ||
| /// Both `ON` and `USING` preserve all qualified input fields. SQL wildcard | ||
| /// expansion handles the unqualified `USING` key as a single column. | ||
| pub fn build_asof_join_schema(left: &DFSchema, right: &DFSchema) -> Result<DFSchema> { | ||
| build_join_schema(left, right, &JoinType::Left) | ||
| } | ||
|
|
||
| /// (Re)qualify the sides of a join if needed, i.e. if the columns from one side would otherwise | ||
| /// conflict with the columns from the other. | ||
| /// This is especially useful for queries that come as Substrait, since Substrait doesn't currently allow specifying | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is something I don't fully understand — it would be great if other reviewers have the knowledge to double-check, or can point me to something that would help me understand it more easily. (Just flagging where I don't have full confidence; I don't think this is a blocker for this PR.)
Specifically, I don't follow the whole lifecycle of projection pushdown.
Let's say the downstream only requires a subset of the columns inside
AsOfJoinExec. I imagine the logical optimizer should try to keep the projection list inside theLogicalPlan, so that we can directly build the physical plan node with the required projection indices.Here, when building the initial physical plan, the projection list is
None, and it seems to depend on a later physical optimizer pass to finish the work.Perhaps there is some room to simplify that process.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. I’ll handle that in a separate follow-up.