diff --git a/datafusion/optimizer/src/decorrelate_predicate_subquery.rs b/datafusion/optimizer/src/decorrelate_predicate_subquery.rs index febbd3a6c84b5..2dfa2701047ad 100644 --- a/datafusion/optimizer/src/decorrelate_predicate_subquery.rs +++ b/datafusion/optimizer/src/decorrelate_predicate_subquery.rs @@ -78,10 +78,21 @@ impl OptimizerRule for DecorrelatePredicateSubquery { } let original_projection = projection.clone(); + // The projection's own schema is the authority for its output field names. + // Earlier rules rewrite expressions with `map_expressions`, which keeps the + // schema but can leave an expression whose display name no longer matches its + // field name (`RewriteSetComparison` turns `x > ALL (…)` into a `CASE` and does + // exactly that). Taking the name from the expression instead would rebuild the + // projection under a different name and fail the "schema is invariant under + // optimization" check. + let field_names = projection + .schema + .fields() + .iter() + .map(|field| field.name().to_string()); let mut cur_input = Arc::unwrap_or_clone(projection.input); let mut rewritten_exprs = Vec::with_capacity(projection.expr.len()); - for expr in projection.expr { - let original_name = expr.schema_name().to_string(); + for (expr, original_name) in projection.expr.into_iter().zip(field_names) { let (new_input, mut rewritten_expr) = rewrite_inner_subqueries(cur_input, expr, config, true, true)?; if has_subquery(&rewritten_expr) { @@ -91,7 +102,10 @@ impl OptimizerRule for DecorrelatePredicateSubquery { } cur_input = new_input; - if rewritten_expr.schema_name().to_string() != original_name { + // `Expr::qualified_name` is the name the rebuilt projection gives this + // expression: the bare column name for a column reference, the alias name + // for an alias, and the display name otherwise. + if rewritten_expr.qualified_name().1 != original_name { rewritten_expr = rewritten_expr.alias(original_name); } rewritten_exprs.push(rewritten_expr); diff --git a/datafusion/sqllogictest/test_files/subquery.slt b/datafusion/sqllogictest/test_files/subquery.slt index d4a42cca0c801..29cfd6c5c6633 100644 --- a/datafusion/sqllogictest/test_files/subquery.slt +++ b/datafusion/sqllogictest/test_files/subquery.slt @@ -4077,6 +4077,29 @@ DROP TABLE nested_agg_outer; statement ok DROP TABLE nested_agg_inner; +# A set comparison in a projection without an alias must not change the projection's +# output field name. `RewriteSetComparison` replaces `x > ALL (…)` with a `CASE` whose +# display name differs from the field name, and rebuilding the projection from the +# expression names failed the optimizer's "schema is invariant" check with an internal +# error instead of returning the answer. +query B +select 2 > all (select v from (values (1), (0)) t(v)); +---- +true + +query BI +select 2 > all (select v from (values (1), (0)) t(v)), 9 as k; +---- +true 9 + +# A control rather than a regression test: an aliased set comparison already keeps its +# name on `main`, so this one does not change with the fix. It pins the alias arm of the +# name comparison against a rule that starts renaming aliased projections. +query B +select 2 > all (select v from (values (1), (0)) t(v)) as aliased; +---- +true + # Regression tests for #25258 - Unaliased set comparison subqueries return wrong results statement ok