Skip to content

Commit a5da81b

Browse files
committed
fix(unparser): keep a DISTINCT ON's sort expressions when naming its outputs
Rebuilding a node from the expressions a plan reports drops what those expressions do not carry. For `Distinct::On` that is the `ORDER BY`, and `with_new_exprs` asserts it was never asked to carry it — so naming the outputs of a projection under a `DISTINCT ON ... ORDER BY` panicked while unparsing a plan that is otherwise valid. Replace only the input on each node walked through, leaving every other field as it stands, which cannot drop a field the walk does not know about.
1 parent ccef79d commit a5da81b

2 files changed

Lines changed: 77 additions & 18 deletions

File tree

datafusion/sql/src/unparser/utils.rs

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ use datafusion_common::{
2727
tree_node::{Transformed, TransformedResult, TreeNode},
2828
};
2929
use datafusion_expr::{
30-
Aggregate, Expr, LogicalPlan, LogicalPlanBuilder, Projection, SortExpr, Unnest,
31-
Window, expr, utils::grouping_set_to_exprlist,
30+
Aggregate, Distinct, Expr, LogicalPlan, LogicalPlanBuilder, Projection, SortExpr,
31+
Unnest, Window, expr, utils::grouping_set_to_exprlist,
3232
};
3333

3434
use indexmap::IndexSet;
@@ -337,29 +337,60 @@ pub(crate) fn name_derived_scope_outputs(
337337
/// the same way for a different purpose and stops at a narrower set, because a
338338
/// predicate can only reach a projection that stays in its own `SELECT`.
339339
fn name_scope_projection_outputs(plan: &LogicalPlan) -> Result<Option<LogicalPlan>> {
340+
// Each node is rebuilt by replacing its input and leaving every other field as
341+
// it stands. `LogicalPlan::with_new_exprs` is the shorter spelling and the
342+
// wrong one: it reconstructs a node from the expressions a plan reports, and
343+
// that round trip does not carry a `DISTINCT ON`'s sort expressions — it
344+
// panics on a plan that has them.
340345
match plan {
341346
LogicalPlan::Projection(projection) => name_projection_outputs(projection),
342-
// These carry the projection's output names through to the derived table
343-
// unchanged, so the projection below is still the one exposing its columns.
344-
LogicalPlan::Filter(_)
345-
| LogicalPlan::Sort(_)
346-
| LogicalPlan::Limit(_)
347-
| LogicalPlan::Distinct(_)
348-
| LogicalPlan::SubqueryAlias(_) => {
349-
let inputs = plan.inputs();
350-
let [input] = inputs.as_slice() else {
351-
return Ok(None);
352-
};
353-
let Some(named_input) = name_scope_projection_outputs(input)? else {
354-
return Ok(None);
355-
};
356-
plan.with_new_exprs(plan.expressions(), vec![named_input])
357-
.map(Some)
347+
// The nodes below carry the projection's output names out to the derived
348+
// table unchanged, so the projection under them is still the one exposing
349+
// its columns.
350+
LogicalPlan::Filter(filter) => with_named_input(&filter.input, |input| {
351+
let mut filter = filter.clone();
352+
filter.input = input;
353+
LogicalPlan::Filter(filter)
354+
}),
355+
LogicalPlan::Sort(sort) => with_named_input(&sort.input, |input| {
356+
let mut sort = sort.clone();
357+
sort.input = input;
358+
LogicalPlan::Sort(sort)
359+
}),
360+
LogicalPlan::Limit(limit) => with_named_input(&limit.input, |input| {
361+
let mut limit = limit.clone();
362+
limit.input = input;
363+
LogicalPlan::Limit(limit)
364+
}),
365+
LogicalPlan::Distinct(Distinct::All(input)) => {
366+
with_named_input(input, |input| LogicalPlan::Distinct(Distinct::All(input)))
367+
}
368+
LogicalPlan::Distinct(Distinct::On(distinct_on)) => {
369+
with_named_input(&distinct_on.input, |input| {
370+
let mut distinct_on = distinct_on.clone();
371+
distinct_on.input = input;
372+
LogicalPlan::Distinct(Distinct::On(distinct_on))
373+
})
374+
}
375+
LogicalPlan::SubqueryAlias(subquery_alias) => {
376+
with_named_input(&subquery_alias.input, |input| {
377+
let mut subquery_alias = subquery_alias.clone();
378+
subquery_alias.input = input;
379+
LogicalPlan::SubqueryAlias(subquery_alias)
380+
})
358381
}
359382
_ => Ok(None),
360383
}
361384
}
362385

386+
/// Rebuilds a node around a named input, or `None` where the input needs no naming.
387+
fn with_named_input(
388+
input: &Arc<LogicalPlan>,
389+
rebuild: impl FnOnce(Arc<LogicalPlan>) -> LogicalPlan,
390+
) -> Result<Option<LogicalPlan>> {
391+
Ok(name_scope_projection_outputs(input)?.map(|named| rebuild(Arc::new(named))))
392+
}
393+
363394
/// The projection with each unnamed output aliased to the name its schema reports.
364395
fn name_projection_outputs(projection: &Projection) -> Result<Option<LogicalPlan>> {
365396
if !projection.expr.iter().any(output_is_unnamed) {

datafusion/sql/tests/cases/plan_to_sql.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6281,6 +6281,34 @@ fn test_derived_output_under_subquery_alias_is_named() -> Result<()> {
62816281
Ok(())
62826282
}
62836283

6284+
#[test]
6285+
fn test_derived_output_under_distinct_on_with_sort_is_named() -> Result<()> {
6286+
// `DISTINCT ON` carries its own `ORDER BY`, and rebuilding the node from the
6287+
// expressions a plan reports drops it — `LogicalPlan::with_new_exprs` asserts
6288+
// it was not asked to. Replacing only the input keeps every other field, so a
6289+
// plan with sort expressions is named rather than panicked on.
6290+
let schema = Schema::new(vec![
6291+
Field::new("a", DataType::Int32, false),
6292+
Field::new("b", DataType::Int32, false),
6293+
]);
6294+
let plan = table_scan(Some("t"), &schema, Some(vec![0, 1]))?
6295+
.project(vec![col("t.a").add(col("t.b")), col("t.a")])?
6296+
.distinct_on(
6297+
vec![col("t.a")],
6298+
vec![col("t.a + t.b"), col("t.a")],
6299+
Some(vec![col("t.a").sort(true, false)]),
6300+
)?
6301+
.limit(0, Some(5))?
6302+
.project(vec![col("t.a + t.b")])?
6303+
.build()?;
6304+
6305+
assert_snapshot!(
6306+
plan_to_sql(&plan)?,
6307+
@r#"SELECT "t.a + t.b" FROM (SELECT DISTINCT ON (t.a) "t.a + t.b", a FROM (SELECT (t.a + t.b) AS "t.a + t.b", t.a FROM t) ORDER BY a ASC NULLS LAST LIMIT 5)"#
6308+
);
6309+
Ok(())
6310+
}
6311+
62846312
#[test]
62856313
fn test_named_derived_projection_outputs_are_unchanged() -> Result<()> {
62866314
// An alias and a bare column already carry the name the enclosing scope

0 commit comments

Comments
 (0)