Skip to content

Commit 9a31259

Browse files
test: add tests for aggregate over subquery unparsing
Add roundtrip_aggregate_over_subquery and test_unparse_aggregate_over_subquery_no_inner_proj to cover aggregate expressions over subquery aliases. The latter demonstrates a bug where the outer projection resolves aggregate expressions instead of column references when the Projection between Limit and Aggregate is absent. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ae2fdcf commit 9a31259

1 file changed

Lines changed: 85 additions & 0 deletions

File tree

datafusion/sql/tests/cases/plan_to_sql.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2919,6 +2919,91 @@ fn roundtrip_subquery_aggregate_with_column_alias() -> Result<(), DataFusionErro
29192919
Ok(())
29202920
}
29212921

2922+
/// Roundtrip: aggregate over a subquery projection.
2923+
#[test]
2924+
fn roundtrip_aggregate_over_subquery() -> Result<(), DataFusionError> {
2925+
let sql = r#"SELECT __agg_0 AS "min(j1_id)", __agg_1 AS "max(j1_id)" FROM (SELECT min(j1_rename) AS __agg_0, max(j1_rename) AS __agg_1 FROM (SELECT j1_id AS j1_rename FROM j1) AS bla LIMIT 20)"#;
2926+
2927+
let statement = Parser::new(&GenericDialect {})
2928+
.try_with_sql(sql)?
2929+
.parse_statement()?;
2930+
2931+
let state = MockSessionState::default()
2932+
.with_aggregate_function(max_udaf())
2933+
.with_aggregate_function(min_udaf())
2934+
.with_expr_planner(Arc::new(CoreFunctionPlanner::default()))
2935+
.with_expr_planner(Arc::new(NestedFunctionPlanner))
2936+
.with_expr_planner(Arc::new(FieldAccessPlanner));
2937+
2938+
let context = MockContextProvider { state };
2939+
let sql_to_rel = SqlToRel::new(&context);
2940+
let plan = sql_to_rel
2941+
.sql_statement_to_plan(statement)
2942+
.unwrap_or_else(|e| panic!("Failed to parse sql: {sql}\n{e}"));
2943+
2944+
println!("Logical plan:\n{plan}");
2945+
println!("\nLogical plan (verbose):\n{}", plan.display_indent_schema());
2946+
2947+
let unparser = Unparser::new(&UnparserDefaultDialect {});
2948+
let roundtrip_statement = unparser.plan_to_sql(&plan)?;
2949+
let actual = &roundtrip_statement.to_string();
2950+
2951+
insta::assert_snapshot!(actual, @r#"SELECT __agg_0 AS "min(j1_id)", __agg_1 AS "max(j1_id)" FROM (SELECT min(bla.j1_rename) AS __agg_0, max(bla.j1_rename) AS __agg_1 FROM (SELECT j1.j1_id AS j1_rename FROM j1) AS bla LIMIT 20)"#);
2952+
Ok(())
2953+
}
2954+
2955+
/// Same as roundtrip_aggregate_over_subquery but with the Projection between
2956+
/// Limit and Aggregate removed — the aliases are inlined into the Aggregate.
2957+
///
2958+
/// Plan shape:
2959+
/// Projection: __agg_0 AS "max1(j1_id)", __agg_1 AS "max2(j1_id)"
2960+
/// Limit: fetch=20
2961+
/// Aggregate: aggr=[[max(bla.j1_rename) AS __agg_0, max(bla.j1_rename) AS __agg_1]]
2962+
/// SubqueryAlias: bla
2963+
/// Projection: j1.j1_id AS j1_rename
2964+
/// TableScan: j1
2965+
#[test]
2966+
fn test_unparse_aggregate_over_subquery_no_inner_proj() -> Result<()> {
2967+
let context = MockContextProvider {
2968+
state: MockSessionState::default(),
2969+
};
2970+
let j1_schema = context
2971+
.get_table_source(TableReference::bare("j1"))?
2972+
.schema();
2973+
2974+
// (SELECT j1_id AS j1_rename FROM j1) AS bla
2975+
let scan = table_scan(Some("j1"), &j1_schema, None)?.build()?;
2976+
let inner_subquery = LogicalPlanBuilder::from(scan)
2977+
.project(vec![col("j1.j1_id").alias("j1_rename")])?
2978+
.alias("bla")?
2979+
.build()?;
2980+
2981+
// Aggregate with aliases inlined (no separate Projection)
2982+
let plan = LogicalPlanBuilder::from(inner_subquery)
2983+
.aggregate(
2984+
vec![] as Vec<Expr>,
2985+
vec![
2986+
max(col("bla.j1_rename")).alias("__agg_0"),
2987+
max(col("bla.j1_rename")).alias("__agg_1"),
2988+
],
2989+
)?
2990+
.limit(0, Some(20))?
2991+
.project(vec![
2992+
col("__agg_0").alias("max1(j1_id)"),
2993+
col("__agg_1").alias("max2(j1_id)"),
2994+
])?
2995+
.build()?;
2996+
2997+
println!("Logical plan:\n{plan}");
2998+
println!("\nLogical plan (verbose):\n{}", plan.display_indent_schema());
2999+
3000+
let unparser = Unparser::default();
3001+
let sql = unparser.plan_to_sql(&plan)?.to_string();
3002+
println!("\nUnparsed SQL:\n{sql}");
3003+
3004+
Ok(())
3005+
}
3006+
29223007
/// Test that unparsing a manually constructed join with a subquery aggregate
29233008
/// preserves the MAX aggregate function.
29243009
///

0 commit comments

Comments
 (0)