diff --git a/src/sql/src/plan/query.rs b/src/sql/src/plan/query.rs index c0628051e5238..5449efb47042b 100644 --- a/src/sql/src/plan/query.rs +++ b/src/sql/src/plan/query.rs @@ -3978,10 +3978,22 @@ fn plan_using_constraint( column_name.clone().to_string(), )); - // Should be safe to use either `lhs` or `rhs` here since the column - // is available in both scopes and must have the same type of the new item. - // We (arbitrarily) choose the left name. - map_exprs.push(HirScalarExpr::named_column(lhs, Arc::clone(&lhs_name))); + // The aliased column `alias.col` must take the same value as the + // unqualified join output column `col`. For INNER and LEFT joins + // that's the LHS value, for RIGHT joins it's the RHS value, and + // for FULL OUTER joins it's COALESCE(lhs, rhs). Using `lhs` + // unconditionally produces wrong results for RIGHT/FULL joins on + // rows where the LHS side is NULL. + let alias_expr = match kind { + JoinKind::LeftOuter { .. } | JoinKind::Inner { .. } => { + HirScalarExpr::named_column(lhs, Arc::clone(&lhs_name)) + } + JoinKind::RightOuter => HirScalarExpr::named_column(rhs, Arc::clone(&rhs_name)), + JoinKind::FullOuter => { + HirScalarExpr::call_variadic(Coalesce, vec![expr1.clone(), expr2.clone()]) + } + }; + map_exprs.push(alias_expr); } join_exprs.push(expr1.call_binary(expr2, expr_func::Eq)); diff --git a/test/sqllogictest/joins.slt b/test/sqllogictest/joins.slt index 59c0eb7368c6f..10dd5d1fe5447 100644 --- a/test/sqllogictest/joins.slt +++ b/test/sqllogictest/joins.slt @@ -842,6 +842,19 @@ SELECT ROW(x.*) FROM t1 JOIN t2 USING (f1) AS x; row ("(3)") +query I +SELECT x.f1 FROM t1 FULL JOIN t2 USING (f1) AS x ORDER BY 1 +---- +1 +3 +5 + +query I +SELECT x.f1 FROM t1 RIGHT JOIN t2 USING (f1) AS x ORDER BY 1 +---- +3 +5 + statement ok CREATE VIEW v1 AS SELECT x.* FROM t1 JOIN t2 USING (f1) AS x WHERE x.f1 = 3;