Skip to content

Commit f4c5047

Browse files
committed
fix: hide internal USING merged-key qualifier
Keep wildcard output schemas on their visible input columns while still evaluating RIGHT and FULL USING keys with merged-key semantics. Also unwrap the reserved merged-key alias for GROUP BY planning so invalid ORDER BY keys report the aggregate validation error instead of a schema lookup failure. Signed-off-by: Jiawei Zhao <Phoenix500526@163.com>
1 parent 1709313 commit f4c5047

5 files changed

Lines changed: 95 additions & 14 deletions

File tree

datafusion/expr/src/utils.rs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ use std::cmp::Ordering;
2121
use std::collections::{BTreeSet, HashSet};
2222
use std::sync::Arc;
2323

24+
use crate::conditional_expressions::CaseBuilder;
2425
use crate::expr::{Alias, Sort, WildcardOptions, WindowFunctionParams};
25-
use crate::expr_rewriter::{merged_using_key_or_column, strip_outer_reference};
26+
use crate::expr_rewriter::strip_outer_reference;
27+
use crate::logical_plan::JoinType;
2628
use crate::{
2729
BinaryExpr, Expr, ExprSchemable, Filter, GroupingSet, LogicalPlan, Operator, and,
2830
};
@@ -442,6 +444,38 @@ fn exclude_using_columns(plan: &LogicalPlan) -> Result<HashSet<Column>> {
442444
Ok(excluded)
443445
}
444446

447+
fn merged_using_key_for_wildcard(
448+
col: Column,
449+
merged_keys: &[(Column, Column, JoinType)],
450+
) -> Result<Expr> {
451+
let Some((l, r, join_type)) =
452+
merged_keys.iter().find(|(l, r, _)| l == &col || r == &col)
453+
else {
454+
return Ok(Expr::Column(col));
455+
};
456+
457+
let (expr, visible_col) = match join_type {
458+
JoinType::Right | JoinType::RightSemi | JoinType::RightAnti => {
459+
(Expr::Column(r.clone()), r.clone())
460+
}
461+
JoinType::Full => CaseBuilder::new(
462+
None,
463+
vec![Expr::Column(l.clone()).is_not_null()],
464+
vec![Expr::Column(l.clone())],
465+
Some(Box::new(Expr::Column(r.clone()))),
466+
)
467+
.end()
468+
.map(|expr| (expr, col.clone()))?,
469+
_ => (Expr::Column(l.clone()), l.clone()),
470+
};
471+
472+
if expr == Expr::Column(visible_col.clone()) {
473+
Ok(expr)
474+
} else {
475+
Ok(expr.alias_qualified(visible_col.relation, visible_col.name))
476+
}
477+
}
478+
445479
/// Resolves an `Expr::Wildcard` to a collection of `Expr::Column`'s.
446480
pub fn expand_wildcard(
447481
schema: &DFSchema,
@@ -463,16 +497,16 @@ pub fn expand_wildcard(
463497
columns_to_skip.extend(excluded_columns);
464498
let exprs = get_exprs_except_skipped(schema, &columns_to_skip);
465499

466-
// Resolve the surviving USING / NATURAL key column to the internally named
467-
// merged key expression.
500+
// Resolve the surviving USING / NATURAL key column to the merged key value,
501+
// while preserving the wildcard-visible field qualifier.
468502
let merged_keys = plan.using_key_pairs()?;
469503
if merged_keys.is_empty() {
470504
return Ok(exprs);
471505
}
472506
exprs
473507
.into_iter()
474508
.map(|expr| match expr {
475-
Expr::Column(col) => merged_using_key_or_column(col, true, &merged_keys),
509+
Expr::Column(col) => merged_using_key_for_wildcard(col, &merged_keys),
476510
other => Ok(other),
477511
})
478512
.collect()

datafusion/sql/src/select.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ use datafusion_common::{
4040
use datafusion_common::{RecursionUnnestOption, UnnestOptions};
4141
use datafusion_expr::ExprSchemable;
4242
use datafusion_expr::builder::get_struct_unnested_columns;
43-
use datafusion_expr::expr::{PlannedReplaceSelectItem, Unnest, WildcardOptions};
43+
use datafusion_expr::expr::{Alias, PlannedReplaceSelectItem, Unnest, WildcardOptions};
4444
use datafusion_expr::expr_rewriter::{
4545
merged_using_key_or_column, normalize_col, normalize_sorts,
4646
};
@@ -135,6 +135,40 @@ fn normalize_col_resolving_merged_using_key(
135135
.data()
136136
}
137137

138+
fn unalias_internal_merged_key(expr: Expr, select_exprs: &[Expr]) -> Expr {
139+
match expr {
140+
Expr::Alias(Alias {
141+
expr,
142+
relation: Some(relation),
143+
..
144+
}) if relation.table() == LogicalPlanBuilder::MERGED_KEY_QUALIFIER => *expr,
145+
Expr::Column(column)
146+
if column.relation.as_ref().is_some_and(|relation| {
147+
relation.table() == LogicalPlanBuilder::MERGED_KEY_QUALIFIER
148+
}) =>
149+
{
150+
select_exprs
151+
.iter()
152+
.find_map(|select_expr| match select_expr {
153+
Expr::Alias(Alias {
154+
expr,
155+
relation: Some(relation),
156+
name,
157+
..
158+
}) if relation.table()
159+
== LogicalPlanBuilder::MERGED_KEY_QUALIFIER
160+
&& name == &column.name =>
161+
{
162+
Some(*expr.clone())
163+
}
164+
_ => None,
165+
})
166+
.unwrap_or(Expr::Column(column))
167+
}
168+
other => other,
169+
}
170+
}
171+
138172
impl<S: ContextProvider> SqlToRel<'_, S> {
139173
/// Generate a logic plan from an SQL select
140174
pub(super) fn select_to_plan(
@@ -288,6 +322,8 @@ impl<S: ContextProvider> SqlToRel<'_, S> {
288322
let group_by_expr =
289323
resolve_positions_to_exprs(group_by_expr, &select_exprs)?;
290324
let group_by_expr = normalize_col(group_by_expr, &projected_plan)?;
325+
let group_by_expr =
326+
unalias_internal_merged_key(group_by_expr, &select_exprs);
291327
self.validate_schema_satisfies_exprs(
292328
base_plan.schema(),
293329
std::slice::from_ref(&group_by_expr),

datafusion/sql/tests/sql_integration.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,7 +1074,7 @@ fn join_with_ambiguous_column() {
10741074
assert_snapshot!(
10751075
plan,
10761076
@r"
1077-
Projection: a.id
1077+
Projection: a.id AS id
10781078
Inner Join: Using a.id = b.id
10791079
SubqueryAlias: a
10801080
TableScan: person
@@ -1091,7 +1091,7 @@ fn natural_left_join() {
10911091
assert_snapshot!(
10921092
plan,
10931093
@r"
1094-
Projection: a.l_item_id
1094+
Projection: a.l_item_id AS l_item_id
10951095
Left Join: Using a.l_orderkey = b.l_orderkey, a.l_item_id = b.l_item_id, a.l_description = b.l_description, a.l_extendedprice = b.l_extendedprice, a.price = b.price
10961096
SubqueryAlias: a
10971097
TableScan: lineitem
@@ -1108,7 +1108,7 @@ fn natural_right_join() {
11081108
assert_snapshot!(
11091109
plan,
11101110
@r"
1111-
Projection: b.l_item_id
1111+
Projection: b.l_item_id AS l_item_id
11121112
Right Join: Using a.l_orderkey = b.l_orderkey, a.l_item_id = b.l_item_id, a.l_description = b.l_description, a.l_extendedprice = b.l_extendedprice, a.price = b.price
11131113
SubqueryAlias: a
11141114
TableScan: lineitem
@@ -2551,7 +2551,7 @@ fn join_with_using() {
25512551
assert_snapshot!(
25522552
plan,
25532553
@r"
2554-
Projection: person.first_name, person.id
2554+
Projection: person.first_name, person.id AS id
25552555
Inner Join: Using person.id = person2.id
25562556
TableScan: person
25572557
SubqueryAlias: person2
@@ -3502,6 +3502,17 @@ fn select_groupby_orderby_aggregate_on_non_selected_column_original_issue() {
35023502
);
35033503
}
35043504

3505+
#[test]
3506+
fn natural_join_group_by_order_by_non_grouped_merged_key() {
3507+
let sql = "WITH t1 AS (SELECT 1 AS v1, 2 AS v2) \
3508+
SELECT v1 FROM t1 AS tt1 NATURAL JOIN t1 AS tt2 \
3509+
GROUP BY v1 ORDER BY v2";
3510+
assert_contains!(
3511+
error_message(sql),
3512+
"Column in ORDER BY must be in GROUP BY or an aggregate function"
3513+
);
3514+
}
3515+
35053516
fn logical_plan(sql: &str) -> Result<LogicalPlan> {
35063517
logical_plan_with_options(sql, ParserOptions::default())
35073518
}

datafusion/sqllogictest/test_files/join.slt.part

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1167,7 +1167,7 @@ INNER JOIN t0 ON (t0.v1 + t5.v0) > 0
11671167
WHERE t0.v1 = t1.v0;
11681168
----
11691169
logical_plan
1170-
01)Projection: t1.v0 AS v0, t1.v1 AS v1, t5.v2, t5.v3, t5.v4, t0.v0, t0.v1
1170+
01)Projection: t1.v0, t1.v1, t5.v2, t5.v3, t5.v4, t0.v0, t0.v1
11711171
02)--Inner Join: CAST(t1.v0 AS Float64) = t0.v1 Filter: t0.v1 + CAST(t5.v0 AS Float64) > Float64(0)
11721172
03)----Projection: t1.v0, t1.v1, t5.v0, t5.v2, t5.v3, t5.v4
11731173
04)------Inner Join: t1.v0 = t5.v0, t1.v1 = t5.v1

datafusion/sqllogictest/test_files/joins.slt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4542,7 +4542,7 @@ query TT
45424542
explain SELECT * FROM person a join person b using (id, age);
45434543
----
45444544
logical_plan
4545-
01)Projection: a.id AS id, a.age AS age, a.state, b.state
4545+
01)Projection: a.id, a.age, a.state, b.state
45464546
02)--Inner Join: a.id = b.id, a.age = b.age
45474547
03)----SubqueryAlias: a
45484548
04)------TableScan: person projection=[id, age, state]
@@ -4557,7 +4557,7 @@ query TT
45574557
explain SELECT age FROM (SELECT * FROM person a join person b using (id, age, state));
45584558
----
45594559
logical_plan
4560-
01)Projection: a.age AS age
4560+
01)Projection: a.age
45614561
02)--Inner Join: a.id = b.id, a.age = b.age, a.state = b.state
45624562
03)----SubqueryAlias: a
45634563
04)------TableScan: person projection=[id, age, state]
@@ -4601,7 +4601,7 @@ query TT
46014601
explain SELECT * FROM person a join person b using (id, age, state) join person c using (id, age, state);
46024602
----
46034603
logical_plan
4604-
01)Projection: a.id AS id, a.age AS age, a.state AS state
4604+
01)Projection: a.id, a.age, a.state
46054605
02)--Inner Join: a.id = c.id, a.age = c.age, a.state = c.state
46064606
03)----Projection: a.id, a.age, a.state
46074607
04)------Inner Join: a.id = b.id, a.age = b.age, a.state = b.state
@@ -4636,7 +4636,7 @@ query TT
46364636
explain SELECT * FROM lineitem JOIN lineitem as lineitem2 USING (c1)
46374637
----
46384638
logical_plan
4639-
01)Projection: lineitem.c1 AS c1
4639+
01)Projection: lineitem.c1
46404640
02)--Inner Join: lineitem.c1 = lineitem2.c1
46414641
03)----TableScan: lineitem projection=[c1]
46424642
04)----SubqueryAlias: lineitem2

0 commit comments

Comments
 (0)