From 809d37636b1877ff2b2fdb58a798865570e4776b Mon Sep 17 00:00:00 2001 From: Alex Qyoun-ae <4062971+MazterQyou@users.noreply.github.com> Date: Wed, 22 Jul 2026 21:52:27 +0400 Subject: [PATCH] fix: Support `GROUP BY ()` and tuple sublists Signed-off-by: Alex Qyoun-ae <4062971+MazterQyou@users.noreply.github.com> --- datafusion/core/src/sql/planner.rs | 104 +++++++++++++++++++++-------- 1 file changed, 75 insertions(+), 29 deletions(-) diff --git a/datafusion/core/src/sql/planner.rs b/datafusion/core/src/sql/planner.rs index 6208913761852..8b8dd1311489d 100644 --- a/datafusion/core/src/sql/planner.rs +++ b/datafusion/core/src/sql/planner.rs @@ -1141,6 +1141,22 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { )) } }; + // PostgreSQL treats a parenthesized list in GROUP BY as a sublist of grouping + // columns: `GROUP BY (a, b)` is equivalent to `GROUP BY a, b`, and `GROUP BY ()` + // is an empty grouping set producing a single grand-total group. + let mut group_by_has_empty_grouping_set = false; + let group_by_sql_exprs = group_by_sql_exprs + .into_iter() + .flat_map(|e| match e { + SQLExpr::Tuple(exprs) => { + if exprs.is_empty() { + group_by_has_empty_grouping_set = true; + } + exprs + } + e => vec![e], + }) + .collect::>(); let group_by_exprs = group_by_sql_exprs .into_iter() .map(|e| { @@ -1164,37 +1180,40 @@ impl<'a, S: ContextProvider> SqlToRel<'a, S> { .collect::>>()?; // process group by, aggregation or having - let (plan, select_exprs_post_aggr, having_expr_post_aggr_opt) = - if !group_by_exprs.is_empty() || !aggr_exprs.is_empty() { - self.aggregate( - plan, - &select_exprs, - &having_expr_opt, - group_by_exprs, - aggr_exprs, - )? - } else { - if let Some(having_expr) = &having_expr_opt { - let available_columns = select_exprs - .iter() - .map(|expr| expr_as_column_expr(expr, &plan)) - .collect::>>()?; - - // Ensure the HAVING expression is using only columns - // provided by the SELECT. - if !can_columns_satisfy_exprs( - &available_columns, - slice::from_ref(having_expr), - )? { - return Err(DataFusionError::Plan( - "Having references column(s) not provided by the select" - .to_owned(), - )); - } + let (plan, select_exprs_post_aggr, having_expr_post_aggr_opt) = if !group_by_exprs + .is_empty() + || !aggr_exprs.is_empty() + || group_by_has_empty_grouping_set + { + self.aggregate( + plan, + &select_exprs, + &having_expr_opt, + group_by_exprs, + aggr_exprs, + )? + } else { + if let Some(having_expr) = &having_expr_opt { + let available_columns = select_exprs + .iter() + .map(|expr| expr_as_column_expr(expr, &plan)) + .collect::>>()?; + + // Ensure the HAVING expression is using only columns + // provided by the SELECT. + if !can_columns_satisfy_exprs( + &available_columns, + slice::from_ref(having_expr), + )? { + return Err(DataFusionError::Plan( + "Having references column(s) not provided by the select" + .to_owned(), + )); } + } - (plan, select_exprs, having_expr_opt) - }; + (plan, select_exprs, having_expr_opt) + }; let plan = if let Some(having_expr_post_aggr) = having_expr_post_aggr_opt { LogicalPlanBuilder::from(plan) @@ -4094,6 +4113,33 @@ mod tests { quick_test(sql, expected); } + #[test] + fn select_aggregate_with_empty_group_by_grouping_set() { + let sql = "SELECT MAX(age) FROM person GROUP BY ()"; + let expected = "Projection: #MAX(person.age)\ + \n Aggregate: groupBy=[[]], aggr=[[MAX(#person.age)]]\ + \n TableScan: person projection=None"; + quick_test(sql, expected); + } + + #[test] + fn select_no_aggregate_with_empty_group_by_grouping_set() { + let sql = "SELECT 1 FROM person GROUP BY ()"; + let expected = "Projection: Int64(1)\ + \n Aggregate: groupBy=[[]], aggr=[[]]\ + \n TableScan: person projection=None"; + quick_test(sql, expected); + } + + #[test] + fn select_aggregate_with_group_by_tuple() { + let sql = "SELECT state, age, MAX(salary) FROM person GROUP BY (state, age)"; + let expected = "Projection: #person.state, #person.age, #MAX(person.salary)\ + \n Aggregate: groupBy=[[#person.state, #person.age]], aggr=[[MAX(#person.salary)]]\ + \n TableScan: person projection=None"; + quick_test(sql, expected); + } + #[test] fn select_aggregate_with_group_by_having_and_order_by_aggregate() { let sql = "SELECT first_name, MAX(age)