From 82fd9e2daf959f421bd20cfdf3aad874afe7bba6 Mon Sep 17 00:00:00 2001 From: Gabor Gevay Date: Mon, 18 May 2026 15:53:35 +0200 Subject: [PATCH] sql: fix wrong results for JOIN ... USING (col) AS t with RIGHT/FULL joins In plan_using_constraint, when an AS alias is present on a USING clause, the qualified reference t.col was unconditionally bound to the LHS column's value. For RIGHT and FULL OUTER joins, rows with no LHS match have a NULL LHS, so t.col returned NULL instead of the joined value. Choose the source of the aliased column based on JoinKind, matching the choice already made for the unaliased join output column a few lines above: - INNER / LEFT -> lhs - RIGHT -> rhs - FULL OUTER -> COALESCE(lhs, rhs) Fixes a regression introduced in #18793. Co-authored-by: Junie --- src/sql/src/plan/query.rs | 20 ++++++++++++++++---- test/sqllogictest/joins.slt | 13 +++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) 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;