You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: fold Limit/Sort into outer SELECT when Projection claims Aggregate through them
When a Projection's `reconstruct_select_statement` reaches through a
Limit or Sort to claim an Aggregate, the Limit/Sort arm would later see
`already_projected` and wrap everything in a spurious derived subquery,
emitting the aggregate twice.
Fix: in the Projection arm, after claiming the Aggregate, detect if the
direct child is a Limit or Sort. If so, fold its clauses (LIMIT/OFFSET
or ORDER BY) into the current query and recurse into the Limit/Sort's
child, skipping the node entirely.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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)"#;
.unwrap_or_else(|e| panic!("Failed to parse sql: {sql}\n{e}"));
2943
-
2944
-
println!("Logical plan:\n{plan}");
2945
-
println!(
2946
-
"\nLogical plan (verbose):\n{}",
2947
-
plan.display_indent_schema()
2925
+
roundtrip_statement_with_dialect_helper!(
2926
+
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)"#,
2927
+
parser_dialect:GenericDialect{},
2928
+
unparser_dialect:UnparserDefaultDialect{},
2929
+
expected: @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)"#,
2948
2930
);
2949
-
2950
-
let unparser = Unparser::new(&UnparserDefaultDialect{});
2951
-
let roundtrip_statement = unparser.plan_to_sql(&plan)?;
2952
-
let actual = &roundtrip_statement.to_string();
2953
-
2954
-
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)"#);
2955
2931
Ok(())
2956
2932
}
2957
2933
2958
-
/// Same as roundtrip_aggregate_over_subquery but with the Projection between
2959
-
/// Limit and Aggregate removed — the aliases are inlined into the Aggregate.
2960
-
///
2961
-
/// Plan shape:
2962
-
/// Projection: __agg_0 AS "max1(j1_id)", __agg_1 AS "max2(j1_id)"
2963
-
/// Limit: fetch=20
2964
-
/// Aggregate: aggr=[[max(bla.j1_rename) AS __agg_0, max(bla.j1_rename) AS __agg_1]]
2965
-
/// SubqueryAlias: bla
2966
-
/// Projection: j1.j1_id AS j1_rename
2967
-
/// TableScan: j1
2934
+
/// Projection → Limit → Aggregate (aliases inlined into Aggregate, no
2935
+
/// intermediate Projection). Verifies the Limit is folded into the outer
2936
+
/// SELECT rather than creating a spurious derived subquery.
let sql = unparser.plan_to_sql(&plan)?.to_string();
3008
-
println!("\nUnparsed SQL:\n{sql}");
3009
-
2964
+
let sql = Unparser::default().plan_to_sql(&plan)?.to_string();
2965
+
insta::assert_snapshot!(sql, @r#"SELECT max(bla.j1_rename) AS "max1(j1_id)", max(bla.j1_rename) AS "max2(j1_id)" FROM (SELECT j1.j1_id AS j1_rename FROM j1) AS bla LIMIT 20"#);
3010
2966
Ok(())
3011
2967
}
3012
2968
3013
-
/// Same as test_unparse_aggregate_over_subquery_no_inner_proj but the outer
3014
-
/// Projection references the aggregate columns WITHOUT renaming them.
3015
-
/// The output column names should still match the Aggregate's aliases.
3016
-
///
3017
-
/// Plan shape:
3018
-
/// Projection: __agg_0, __agg_1
3019
-
/// Aggregate: aggr=[[max(bla.j1_rename) AS __agg_0, max(bla.j1_rename) AS __agg_1]]
3020
-
/// SubqueryAlias: bla
3021
-
/// Projection: j1.j1_id AS j1_rename
3022
-
/// TableScan: j1
2969
+
/// Projection → Aggregate (aliases inlined, no rename in outer Projection).
2970
+
/// Verifies the aggregate aliases are preserved as output column names.
let sql = Unparser::default().plan_to_sql(&plan)?.to_string();
2995
+
insta::assert_snapshot!(sql, @"SELECT max(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");
2996
+
Ok(())
2997
+
}
3054
2998
3055
-
let unparser = Unparser::default();
3056
-
let sql = unparser.plan_to_sql(&plan)?.to_string();
3057
-
println!("\nUnparsed SQL:\n{sql}");
2999
+
/// Projection → Sort → Aggregate (aliases inlined into Aggregate).
3000
+
/// Verifies the Sort is folded into the outer SELECT rather than creating
let sql = Unparser::default().plan_to_sql(&plan)?.to_string();
3024
+
insta::assert_snapshot!(sql, @r#"SELECT max(bla.j1_rename) AS "max1(j1_id)" FROM (SELECT j1.j1_id AS j1_rename FROM j1) AS bla ORDER BY max(bla.j1_rename) ASC NULLS FIRST"#);
0 commit comments