1717
1818use crate :: planner:: { ContextProvider , PlannerContext , SqlToRel } ;
1919use datafusion_common:: {
20- DataFusionError , Diagnostic , Result , Span , not_impl_err, plan_err,
20+ DFSchemaRef , DataFusionError , Diagnostic , Result , Span , not_impl_err, plan_err,
2121} ;
2222use datafusion_expr:: { LogicalPlan , LogicalPlanBuilder } ;
23- use sqlparser:: ast:: { SetExpr , SetOperator , SetQuantifier , Spanned } ;
23+ use sqlparser:: ast:: {
24+ Expr as SQLExpr , Ident , SelectItem , SetExpr , SetOperator , SetQuantifier , Spanned ,
25+ } ;
2426
2527impl < S : ContextProvider > SqlToRel < ' _ , S > {
2628 #[ cfg_attr( feature = "recursive_protection" , recursive:: recursive) ]
@@ -42,7 +44,27 @@ impl<S: ContextProvider> SqlToRel<'_, S> {
4244 let left_span = Span :: try_from_sqlparser_span ( left. span ( ) ) ;
4345 let right_span = Span :: try_from_sqlparser_span ( right. span ( ) ) ;
4446 let left_plan = self . set_expr_to_plan ( * left, planner_context) ;
45- let right_plan = self . set_expr_to_plan ( * right, planner_context) ;
47+
48+ // For non-*ByName operations, add missing aliases to right side using left schema's
49+ // column names. This allows queries like
50+ // `SELECT 1 c1, 0 c2, 0 c3 UNION ALL SELECT 2, 0, 0`
51+ // where the right side has duplicate literal values.
52+ // We only do this if the left side succeeded.
53+ let right = if let Ok ( plan) = & left_plan
54+ && matches ! (
55+ set_quantifier,
56+ SetQuantifier :: All
57+ | SetQuantifier :: Distinct
58+ | SetQuantifier :: None
59+ ) {
60+ alias_set_expr ( * right, plan. schema ( ) )
61+ } else {
62+ * right
63+ } ;
64+
65+ let right_plan = self . set_expr_to_plan ( right, planner_context) ;
66+
67+ // Handle errors from both sides, collecting them if both failed
4668 let ( left_plan, right_plan) = match ( left_plan, right_plan) {
4769 ( Ok ( left_plan) , Ok ( right_plan) ) => ( left_plan, right_plan) ,
4870 ( Err ( left_err) , Err ( right_err) ) => {
@@ -160,3 +182,75 @@ impl<S: ContextProvider> SqlToRel<'_, S> {
160182 }
161183 }
162184}
185+
186+ // Adds aliases to SELECT items in a SetExpr using the provided schema.
187+ // This ensures that unnamed expressions on the right side of a UNION/INTERSECT/EXCEPT
188+ // get aliased with the column names from the left side, allowing queries like
189+ // `SELECT 1 AS a, 0 AS b, 0 AS c UNION ALL SELECT 2, 0, 0` to work correctly.
190+ fn alias_set_expr ( set_expr : SetExpr , schema : & DFSchemaRef ) -> SetExpr {
191+ match set_expr {
192+ SetExpr :: Select ( mut select) => {
193+ alias_select_items ( & mut select. projection , schema) ;
194+ SetExpr :: Select ( select)
195+ }
196+ SetExpr :: SetOperation {
197+ op,
198+ left,
199+ right,
200+ set_quantifier,
201+ } => {
202+ // For nested set operations, only alias the leftmost branch
203+ // since that's what determines the output column names
204+ SetExpr :: SetOperation {
205+ op,
206+ left : Box :: new ( alias_set_expr ( * left, schema) ) ,
207+ right,
208+ set_quantifier,
209+ }
210+ }
211+ SetExpr :: Query ( mut query) => {
212+ // Handle parenthesized queries like (SELECT ... UNION ALL SELECT ...)
213+ query. body = Box :: new ( alias_set_expr ( * query. body , schema) ) ;
214+ SetExpr :: Query ( query)
215+ }
216+ // For other cases (Values, etc.), return as-is
217+ other => other,
218+ }
219+ }
220+
221+ // Adds aliases to literal value expressions where missing, based on the input schema, as these are
222+ // the ones that can cause duplicate name issues (e.g. `SELECT 0, 0` has two columns named `Int64(0)`).
223+ // Other expressions typically have unique names.
224+ fn alias_select_items ( items : & mut [ SelectItem ] , schema : & DFSchemaRef ) {
225+ let mut col_idx = 0 ;
226+ for item in items. iter_mut ( ) {
227+ match item {
228+ SelectItem :: UnnamedExpr ( expr) if is_literal_value ( expr) => {
229+ if let Some ( field) = schema. fields ( ) . get ( col_idx) {
230+ * item = SelectItem :: ExprWithAlias {
231+ expr : expr. clone ( ) ,
232+ alias : Ident :: new ( field. name ( ) ) ,
233+ } ;
234+ }
235+ col_idx += 1 ;
236+ }
237+ SelectItem :: UnnamedExpr ( _) | SelectItem :: ExprWithAlias { .. } => {
238+ col_idx += 1 ;
239+ }
240+ SelectItem :: Wildcard ( _) | SelectItem :: QualifiedWildcard ( _, _) => {
241+ // Wildcards expand to multiple columns - skip position tracking
242+ }
243+ }
244+ }
245+ }
246+
247+ /// Returns true if the expression is a literal value that could cause duplicate names.
248+ fn is_literal_value ( expr : & SQLExpr ) -> bool {
249+ matches ! (
250+ expr,
251+ SQLExpr :: Value ( _)
252+ | SQLExpr :: UnaryOp { .. }
253+ | SQLExpr :: TypedString { .. }
254+ | SQLExpr :: Interval ( _)
255+ )
256+ }
0 commit comments