Skip to content

Commit 64af410

Browse files
authored
Add support for USING to SQL unparser (#11636)
* Add support for USING to SQL unparser * cargo fmt * Downgrade USING to ON when necessary when unparsing When the conditions and filters in the LogicalPlan are not in a form compatible with USING, we can instead use ON - so we do.
1 parent d6e016e commit 64af410

File tree

2 files changed

+107
-45
lines changed

2 files changed

+107
-45
lines changed

datafusion/sql/src/unparser/plan.rs

Lines changed: 105 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18-
use datafusion_common::{internal_err, not_impl_err, plan_err, DataFusionError, Result};
18+
use datafusion_common::{
19+
internal_err, not_impl_err, plan_err, Column, DataFusionError, Result,
20+
};
1921
use datafusion_expr::{
2022
expr::Alias, Distinct, Expr, JoinConstraint, JoinType, LogicalPlan, Projection,
2123
};
@@ -368,37 +370,11 @@ impl Unparser<'_> {
368370
self.select_to_sql_recursively(input, query, select, relation)
369371
}
370372
LogicalPlan::Join(join) => {
371-
match join.join_constraint {
372-
JoinConstraint::On => {}
373-
JoinConstraint::Using => {
374-
return not_impl_err!(
375-
"Unsupported join constraint: {:?}",
376-
join.join_constraint
377-
)
378-
}
379-
}
380-
381-
// parse filter if exists
382-
let join_filter = match &join.filter {
383-
Some(filter) => Some(self.expr_to_sql(filter)?),
384-
None => None,
385-
};
386-
387-
// map join.on to `l.a = r.a AND l.b = r.b AND ...`
388-
let eq_op = ast::BinaryOperator::Eq;
389-
let join_on = self.join_conditions_to_sql(&join.on, eq_op)?;
390-
391-
// Merge `join_on` and `join_filter`
392-
let join_expr = match (join_filter, join_on) {
393-
(Some(filter), Some(on)) => Some(self.and_op_to_sql(filter, on)),
394-
(Some(filter), None) => Some(filter),
395-
(None, Some(on)) => Some(on),
396-
(None, None) => None,
397-
};
398-
let join_constraint = match join_expr {
399-
Some(expr) => ast::JoinConstraint::On(expr),
400-
None => ast::JoinConstraint::None,
401-
};
373+
let join_constraint = self.join_constraint_to_sql(
374+
join.join_constraint,
375+
&join.on,
376+
join.filter.as_ref(),
377+
)?;
402378

403379
let mut right_relation = RelationBuilder::default();
404380

@@ -583,24 +559,108 @@ impl Unparser<'_> {
583559
}
584560
}
585561

586-
fn join_conditions_to_sql(
562+
/// Convert the components of a USING clause to the USING AST. Returns
563+
/// 'None' if the conditions are not compatible with a USING expression,
564+
/// e.g. non-column expressions or non-matching names.
565+
fn join_using_to_sql(
587566
&self,
588-
join_conditions: &Vec<(Expr, Expr)>,
589-
eq_op: ast::BinaryOperator,
590-
) -> Result<Option<ast::Expr>> {
591-
// Only support AND conjunction for each binary expression in join conditions
592-
let mut exprs: Vec<ast::Expr> = vec![];
567+
join_conditions: &[(Expr, Expr)],
568+
) -> Option<ast::JoinConstraint> {
569+
let mut idents = Vec::with_capacity(join_conditions.len());
593570
for (left, right) in join_conditions {
594-
// Parse left
571+
match (left, right) {
572+
(
573+
Expr::Column(Column {
574+
relation: _,
575+
name: left_name,
576+
}),
577+
Expr::Column(Column {
578+
relation: _,
579+
name: right_name,
580+
}),
581+
) if left_name == right_name => {
582+
idents.push(self.new_ident_quoted_if_needs(left_name.to_string()));
583+
}
584+
// USING is only valid with matching column names; arbitrary expressions
585+
// are not allowed
586+
_ => return None,
587+
}
588+
}
589+
Some(ast::JoinConstraint::Using(idents))
590+
}
591+
592+
/// Convert a join constraint and associated conditions and filter to a SQL AST node
593+
fn join_constraint_to_sql(
594+
&self,
595+
constraint: JoinConstraint,
596+
conditions: &[(Expr, Expr)],
597+
filter: Option<&Expr>,
598+
) -> Result<ast::JoinConstraint> {
599+
match (constraint, conditions, filter) {
600+
// No constraints
601+
(JoinConstraint::On | JoinConstraint::Using, [], None) => {
602+
Ok(ast::JoinConstraint::None)
603+
}
604+
605+
(JoinConstraint::Using, conditions, None) => {
606+
match self.join_using_to_sql(conditions) {
607+
Some(using) => Ok(using),
608+
// As above, this should not be reachable from parsed SQL,
609+
// but a user could create this; we "downgrade" to ON.
610+
None => self.join_conditions_to_sql_on(conditions, None),
611+
}
612+
}
613+
614+
// Two cases here:
615+
// 1. Straightforward ON case, with possible equi-join conditions
616+
// and additional filters
617+
// 2. USING with additional filters; we "downgrade" to ON, because
618+
// you can't use USING with arbitrary filters. (This should not
619+
// be accessible from parsed SQL, but may have been a
620+
// custom-built JOIN by a user.)
621+
(JoinConstraint::On | JoinConstraint::Using, conditions, filter) => {
622+
self.join_conditions_to_sql_on(conditions, filter)
623+
}
624+
}
625+
}
626+
627+
// Convert a list of equi0join conditions and an optional filter to a SQL ON
628+
// AST node, with the equi-join conditions and the filter merged into a
629+
// single conditional expression
630+
fn join_conditions_to_sql_on(
631+
&self,
632+
join_conditions: &[(Expr, Expr)],
633+
filter: Option<&Expr>,
634+
) -> Result<ast::JoinConstraint> {
635+
let mut condition = None;
636+
// AND the join conditions together to create the overall condition
637+
for (left, right) in join_conditions {
638+
// Parse left and right
595639
let l = self.expr_to_sql(left)?;
596-
// Parse right
597640
let r = self.expr_to_sql(right)?;
598-
// AND with existing expression
599-
exprs.push(self.binary_op_to_sql(l, r, eq_op.clone()));
641+
let e = self.binary_op_to_sql(l, r, ast::BinaryOperator::Eq);
642+
condition = match condition {
643+
Some(expr) => Some(self.and_op_to_sql(expr, e)),
644+
None => Some(e),
645+
};
600646
}
601-
let join_expr: Option<ast::Expr> =
602-
exprs.into_iter().reduce(|r, l| self.and_op_to_sql(r, l));
603-
Ok(join_expr)
647+
648+
// Then AND the non-equijoin filter condition as well
649+
condition = match (condition, filter) {
650+
(Some(expr), Some(filter)) => {
651+
Some(self.and_op_to_sql(expr, self.expr_to_sql(filter)?))
652+
}
653+
(Some(expr), None) => Some(expr),
654+
(None, Some(filter)) => Some(self.expr_to_sql(filter)?),
655+
(None, None) => None,
656+
};
657+
658+
let constraint = match condition {
659+
Some(filter) => ast::JoinConstraint::On(filter),
660+
None => ast::JoinConstraint::None,
661+
};
662+
663+
Ok(constraint)
604664
}
605665

606666
fn and_op_to_sql(&self, lhs: ast::Expr, rhs: ast::Expr) -> ast::Expr {

datafusion/sql/tests/cases/plan_to_sql.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ fn roundtrip_statement() -> Result<()> {
8484
"select 1;",
8585
"select 1 limit 0;",
8686
"select ta.j1_id from j1 ta join (select 1 as j1_id) tb on ta.j1_id = tb.j1_id;",
87+
"select ta.j1_id from j1 ta join (select 1 as j1_id) tb using (j1_id);",
8788
"select ta.j1_id from j1 ta join (select 1 as j1_id) tb on ta.j1_id = tb.j1_id where ta.j1_id > 1;",
8889
"select ta.j1_id from (select 1 as j1_id) ta;",
8990
"select ta.j1_id from j1 ta;",
@@ -142,6 +143,7 @@ fn roundtrip_statement() -> Result<()> {
142143
r#"SELECT id, count(distinct id) over (ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING),
143144
sum(id) OVER (PARTITION BY first_name ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) from person"#,
144145
"SELECT id, sum(id) OVER (PARTITION BY first_name ROWS BETWEEN 5 PRECEDING AND 2 FOLLOWING) from person",
146+
"WITH t1 AS (SELECT j1_id AS id, j1_string name FROM j1), t2 AS (SELECT j2_id AS id, j2_string name FROM j2) SELECT * FROM t1 JOIN t2 USING (id, name)",
145147
];
146148

147149
// For each test sql string, we transform as follows:

0 commit comments

Comments
 (0)