Skip to content

Commit 0e39f1b

Browse files
haochunchangclaude
andcommitted
fix: render aggregate labels without changing EXPLAIN
Aggregate labels came from `UdafHumanDisplayBuilder`. Rendering its FILTER and ORDER BY clauses in SQL syntax for labels also changed physical EXPLAIN text with the option off and hid literal types such as `Float32(1)`. It also broke `reverse_expr`: it still looks for `ORDER BY [`, so a reversed `first_value` showed as `last_value` with the old sort direction. Restore the builder and render aggregate labels in the labeling step, as window labels already are. Ordered-set aggregates now get labels such as `percentile_cont(0.5) WITHIN GROUP (ORDER BY c)`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 5ca9493 commit 0e39f1b

7 files changed

Lines changed: 111 additions & 23 deletions

File tree

‎datafusion/core/src/physical_planner.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4909,7 +4909,7 @@ mod tests {
49094909
vec![expr],
49104910
)
49114911
.await?,
4912-
"AggregateExec: mode=Single, gby=[], aggr=[sum(?table?.column1) FILTER (WHERE ?table?.column2 <= 0) as agg]"
4912+
"AggregateExec: mode=Single, gby=[], aggr=[sum(?table?.column1) FILTER (WHERE ?table?.column2 <= Int64(0)) as agg]"
49134913
);
49144914

49154915
Ok(())
@@ -4933,7 +4933,7 @@ mod tests {
49334933
vec![expr],
49344934
)
49354935
.await?,
4936-
"AggregateExec: mode=Single, gby=[], aggr=[first_value(?table?.column1) RESPECT NULLS ORDER BY ?table?.column2 NULLS FIRST as agg]"
4936+
"AggregateExec: mode=Single, gby=[], aggr=[first_value(?table?.column1) RESPECT NULLS ORDER BY [?table?.column2 ASC NULLS FIRST] as agg]"
49374937
);
49384938

49394939
Ok(())

‎datafusion/core/tests/sql/column_labels.rs‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,25 @@ async fn column_labels_aggregate_functions() -> Result<()> {
155155
Ok(())
156156
}
157157

158+
#[tokio::test]
159+
async fn column_labels_ordered_set_aggregates() -> Result<()> {
160+
let ctx = labels_ctx().await?;
161+
let queries = [
162+
"SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY c) FROM t",
163+
"SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY c DESC) FROM t",
164+
"SELECT percentile_cont(c, 0.5) FROM t",
165+
];
166+
assert_snapshot!(labels(&ctx, &queries).await?, @r"
167+
SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY c) FROM t
168+
percentile_cont(Float64(0.5)) WITHIN GROUP [t.c ASC NULLS LAST] => percentile_cont(0.5) WITHIN GROUP (ORDER BY c)
169+
SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY c DESC) FROM t
170+
percentile_cont(Float64(0.5)) WITHIN GROUP [t.c DESC NULLS FIRST] => percentile_cont(0.5) WITHIN GROUP (ORDER BY c DESC)
171+
SELECT percentile_cont(c, 0.5) FROM t
172+
percentile_cont(t.c,Float64(0.5)) => percentile_cont(c, 0.5)
173+
");
174+
Ok(())
175+
}
176+
158177
#[tokio::test]
159178
async fn column_labels_window_functions() -> Result<()> {
160179
let ctx = labels_ctx().await?;

‎datafusion/expr/src/udaf.rs‎

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ use datafusion_expr_common::operator::Operator;
3232
use datafusion_physical_expr_common::physical_expr::PhysicalExpr;
3333

3434
use crate::expr::{
35-
AggregateFunction, AggregateFunctionParams, ExprListDisplay, SortListDisplay,
36-
WindowFunctionParams, schema_name_from_exprs,
37-
schema_name_from_exprs_comma_separated_without_space, schema_name_from_sorts,
35+
AggregateFunction, AggregateFunctionParams, ExprListDisplay, WindowFunctionParams,
36+
schema_name_from_exprs, schema_name_from_exprs_comma_separated_without_space,
37+
schema_name_from_sorts,
3838
};
3939
use crate::function::{
4040
AccumulatorArgs, AggregateFunctionSimplification, StateFieldsArgs,
@@ -1131,23 +1131,20 @@ pub fn udaf_default_schema_name<F: AggregateUDFImpl + ?Sized>(
11311131
///
11321132
/// # Example
11331133
/// ```
1134-
/// # use datafusion_expr::{col, lit};
1134+
/// # use datafusion_expr::col;
11351135
/// # use datafusion_expr::expr::AggregateFunctionParams;
11361136
/// # use datafusion_expr::UdafHumanDisplayBuilder;
11371137
/// # let params = AggregateFunctionParams {
11381138
/// # args: vec![col("x")],
11391139
/// # distinct: false,
1140-
/// # filter: Some(Box::new(col("y").gt(lit(5i64)))),
1141-
/// # order_by: vec![col("z").sort(false, true)],
1140+
/// # filter: Some(Box::new(col("y").gt(col("z")))),
1141+
/// # order_by: vec![],
11421142
/// # null_treatment: None,
11431143
/// # };
11441144
/// let human_display = UdafHumanDisplayBuilder::new("my_udaf", &params)
11451145
/// .build()
11461146
/// .unwrap();
1147-
/// assert_eq!(
1148-
/// human_display,
1149-
/// "my_udaf(x) FILTER (WHERE y > 5) ORDER BY z DESC"
1150-
/// );
1147+
/// assert_eq!(human_display, "my_udaf(x) FILTER (WHERE y > z)");
11511148
/// ```
11521149
#[derive(Debug)]
11531150
pub struct UdafHumanDisplayBuilder<'a> {
@@ -1188,13 +1185,14 @@ impl<'a> UdafHumanDisplayBuilder<'a> {
11881185
}
11891186

11901187
if let Some(filter) = filter {
1191-
schema_name
1192-
.write_fmt(format_args!(" FILTER (WHERE {})", filter.human_display()))?;
1188+
schema_name.write_fmt(format_args!(" FILTER (WHERE {filter})"))?;
11931189
}
11941190

11951191
if !order_by.is_empty() {
1196-
schema_name
1197-
.write_fmt(format_args!(" ORDER BY {}", SortListDisplay(order_by)))?;
1192+
schema_name.write_fmt(format_args!(
1193+
" ORDER BY [{}]",
1194+
schema_name_from_sorts(order_by)?
1195+
))?;
11981196
}
11991197

12001198
Ok(schema_name)

‎datafusion/sql/src/column_labels.rs‎

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ use datafusion_common::metadata::{FieldMetadata, column_label_metadata};
3333
use datafusion_common::tree_node::{Transformed, TreeNode};
3434
use datafusion_common::{Column, DFSchema, Dependency, Result, TableReference};
3535
use datafusion_expr::expr::{
36-
Alias, ExprListDisplay, SortListDisplay, WindowFunction, WindowFunctionParams,
36+
AggregateFunction, AggregateFunctionParams, Alias, ExprListDisplay, SortListDisplay,
37+
WindowFunction, WindowFunctionParams,
3738
};
3839
use datafusion_expr::utils::grouping_set_to_exprlist;
3940
use datafusion_expr::{
@@ -189,7 +190,7 @@ fn trace_column(plan: &LogicalPlan, idx: usize) -> Result<Option<Expr>> {
189190
}
190191
idx -= 1;
191192
}
192-
resolve_expr(&aggregate.aggr_expr[idx], &aggregate.input)
193+
resolve_aggregate_expr(&aggregate.aggr_expr[idx], &aggregate.input)
193194
}
194195
LogicalPlan::Window(window) => {
195196
let input_len = window.input.schema().fields().len();
@@ -311,6 +312,76 @@ fn is_count_star_window_alias(column: &Column, alias: &str) -> bool {
311312
&& alias == column.name.replacen(PLANNED, "count(*)", 1)
312313
}
313314

315+
/// Like [`resolve_expr`] for an expression of an `Aggregate` node. An
316+
/// aggregate function is rendered as `name(args)` followed by its
317+
/// `DISTINCT`, null treatment, `FILTER` and `ORDER BY` parts. An ordered-set
318+
/// aggregate is rendered with `WITHIN GROUP`, like in the query.
319+
fn resolve_aggregate_expr(expr: &Expr, input: &LogicalPlan) -> Result<Option<Expr>> {
320+
let Expr::AggregateFunction(aggregate_function) = expr else {
321+
return resolve_expr(expr, input);
322+
};
323+
let AggregateFunction { func, params } = aggregate_function;
324+
let AggregateFunctionParams {
325+
args,
326+
distinct,
327+
filter,
328+
order_by,
329+
null_treatment,
330+
} = params;
331+
332+
let Some(args) = args
333+
.iter()
334+
.map(|e| resolve_expr(e, input))
335+
.collect::<Result<Option<Vec<_>>>>()?
336+
else {
337+
return Ok(None);
338+
};
339+
let mut resolved_order_by = Vec::with_capacity(order_by.len());
340+
for sort in order_by {
341+
let Some(expr) = resolve_expr(&sort.expr, input)? else {
342+
return Ok(None);
343+
};
344+
resolved_order_by.push(sort.with_expr(expr));
345+
}
346+
let order_by = resolved_order_by;
347+
let filter = match filter {
348+
Some(filter) => match resolve_expr(filter, input)? {
349+
Some(filter) => Some(filter),
350+
None => return Ok(None),
351+
},
352+
None => None,
353+
};
354+
355+
let within_group = func.supports_within_group_clause() && !order_by.is_empty();
356+
// The planner puts the ordering expression of a WITHIN GROUP clause first
357+
// in the args, but the query only has the direct args there
358+
let args = if within_group { &args[1..] } else { &args[..] };
359+
let mut label = format!(
360+
"{}({}{})",
361+
func.name(),
362+
if *distinct { "DISTINCT " } else { "" },
363+
ExprListDisplay::comma_separated(args)
364+
);
365+
if within_group {
366+
write!(
367+
label,
368+
" WITHIN GROUP (ORDER BY {})",
369+
SortListDisplay(&order_by)
370+
)?;
371+
}
372+
if let Some(null_treatment) = null_treatment {
373+
write!(label, " {null_treatment}")?;
374+
}
375+
if let Some(filter) = filter {
376+
write!(label, " FILTER (WHERE {})", filter.human_display())?;
377+
}
378+
if !within_group && !order_by.is_empty() {
379+
write!(label, " ORDER BY {}", SortListDisplay(&order_by))?;
380+
}
381+
382+
Ok(Some(Expr::Column(Column::new_unqualified(label))))
383+
}
384+
314385
/// Like [`resolve_expr`] for an expression of a `Window` node. A window
315386
/// function is rendered as `name(args) OVER (...)` without the default parts
316387
/// of its window definition.

‎datafusion/sqllogictest/test_files/agg_func_substitute.slt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,9 @@ logical_plan
7979
03)----TableScan: multiple_ordered_table projection=[a, c]
8080
physical_plan
8181
01)ProjectionExec: expr=[a@0 as a, nth_value(multiple_ordered_table.c,Int64(1) + Int64(100)) ORDER BY [multiple_ordered_table.c ASC NULLS LAST]@1 as result]
82-
02)--AggregateExec: mode=FinalPartitioned, gby=[a@0 as a], aggr=[nth_value(multiple_ordered_table.c, 101) ORDER BY multiple_ordered_table.c as nth_value(multiple_ordered_table.c,Int64(1) + Int64(100)) ORDER BY [multiple_ordered_table.c ASC NULLS LAST]], ordering_mode=Sorted
82+
02)--AggregateExec: mode=FinalPartitioned, gby=[a@0 as a], aggr=[nth_value(multiple_ordered_table.c, 101) ORDER BY [multiple_ordered_table.c ASC NULLS LAST] as nth_value(multiple_ordered_table.c,Int64(1) + Int64(100)) ORDER BY [multiple_ordered_table.c ASC NULLS LAST]], ordering_mode=Sorted
8383
03)----RepartitionExec: partitioning=Hash([a@0], 4), input_partitions=4, preserve_order=true, sort_exprs=a@0 ASC NULLS LAST
84-
04)------AggregateExec: mode=Partial, gby=[a@0 as a], aggr=[nth_value(multiple_ordered_table.c, 101) ORDER BY multiple_ordered_table.c as nth_value(multiple_ordered_table.c,Int64(1) + Int64(100)) ORDER BY [multiple_ordered_table.c ASC NULLS LAST]], ordering_mode=Sorted
84+
04)------AggregateExec: mode=Partial, gby=[a@0 as a], aggr=[nth_value(multiple_ordered_table.c, 101) ORDER BY [multiple_ordered_table.c ASC NULLS LAST] as nth_value(multiple_ordered_table.c,Int64(1) + Int64(100)) ORDER BY [multiple_ordered_table.c ASC NULLS LAST]], ordering_mode=Sorted
8585
05)--------RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1, maintains_sort_order=true
8686
06)----------DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/core/tests/data/window_2.csv]]}, projection=[a, c], output_orderings=[[a@0 ASC NULLS LAST], [c@1 ASC NULLS LAST]], file_type=csv, has_header=true
8787

‎datafusion/sqllogictest/test_files/aggregates_simplify.slt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -598,9 +598,9 @@ logical_plan
598598
01)Aggregate: groupBy=[[distinct_simplify_t.g]], aggr=[[min(distinct_simplify_t.v) FILTER (WHERE distinct_simplify_t.v > Int32(3)) AS min(DISTINCT distinct_simplify_t.v) FILTER (WHERE distinct_simplify_t.v > Int64(3))]]
599599
02)--TableScan: distinct_simplify_t projection=[g, v]
600600
physical_plan
601-
01)AggregateExec: mode=FinalPartitioned, gby=[g@0 as g], aggr=[min(distinct_simplify_t.v) FILTER (WHERE distinct_simplify_t.v > 3) as min(DISTINCT distinct_simplify_t.v) FILTER (WHERE distinct_simplify_t.v > Int64(3))]
601+
01)AggregateExec: mode=FinalPartitioned, gby=[g@0 as g], aggr=[min(distinct_simplify_t.v) FILTER (WHERE distinct_simplify_t.v > Int32(3)) as min(DISTINCT distinct_simplify_t.v) FILTER (WHERE distinct_simplify_t.v > Int64(3))]
602602
02)--RepartitionExec: partitioning=Hash([g@0], 4), input_partitions=1
603-
03)----AggregateExec: mode=Partial, gby=[g@0 as g], aggr=[min(distinct_simplify_t.v) FILTER (WHERE distinct_simplify_t.v > 3) as min(DISTINCT distinct_simplify_t.v) FILTER (WHERE distinct_simplify_t.v > Int64(3))]
603+
03)----AggregateExec: mode=Partial, gby=[g@0 as g], aggr=[min(distinct_simplify_t.v) FILTER (WHERE distinct_simplify_t.v > Int32(3)) as min(DISTINCT distinct_simplify_t.v) FILTER (WHERE distinct_simplify_t.v > Int64(3))]
604604
04)------DataSourceExec: partitions=1, partition_sizes=[1]
605605

606606
# min/max(DISTINCT) over floats now sees the raw values. Before this rule, the

‎datafusion/sqllogictest/test_files/expr.slt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2606,7 +2606,7 @@ logical_plan
26062606
03)----TableScan: t projection=[a]
26072607
physical_plan
26082608
01)ProjectionExec: expr=[min(t.a) FILTER (WHERE t.a > Int64(1))@0 as x]
2609-
02)--AggregateExec: mode=Single, gby=[], aggr=[min(t.a) FILTER (WHERE t.a > 1) as min(t.a) FILTER (WHERE t.a > Int64(1))]
2609+
02)--AggregateExec: mode=Single, gby=[], aggr=[min(t.a) FILTER (WHERE t.a > Float32(1)) as min(t.a) FILTER (WHERE t.a > Int64(1))]
26102610
03)----DataSourceExec: partitions=1, partition_sizes=[1]
26112611

26122612

0 commit comments

Comments
 (0)