Skip to content

Commit f856112

Browse files
fix: walk every input and through every aggregate in columns_read_above_aggregate
The old walk stopped at the first Aggregate found top-down and used any() over the plan's inputs, which short-circuits once one input returns true. So a read of the NULL-filled column above a stacked (non-grouping-set) aggregate, in a later join input, or between nested grouping sets was never collected, wrongly reporting SafeToExtend and producing wrong rows instead of staying correlated. Now a node counts as "below a grouping set" when any grouping-set aggregate is below it specifically (not just any aggregate), every input is visited unconditionally, and the walk continues through every aggregate rather than stopping at the first one. Adds the three repro queries as statement error cases in subquery.slt.
1 parent f02e137 commit f856112

2 files changed

Lines changed: 34 additions & 8 deletions

File tree

‎datafusion/optimizer/src/decorrelate.rs‎

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -918,32 +918,39 @@ fn is_grouping_call(expr: &Expr) -> bool {
918918
matches!(expr, Expr::AggregateFunction(agg) if agg.func.name().eq_ignore_ascii_case("grouping"))
919919
}
920920

921-
/// Columns read by a node strictly above the first `Aggregate` found in
921+
/// Columns read by a node that has a grouping set `Aggregate` below it in
922922
/// `plan`, which is the subquery's original, not yet rewritten, plan.
923923
///
924924
/// [`PullUpCorrelatedExpr::f_up`] runs bottom-up, so by the time it visits an
925925
/// `Aggregate` it cannot yet tell whether a `HAVING` or `Projection` above it
926926
/// reads one of the columns a grouping set pull up would add. This walks the
927-
/// plan top-down instead, before the rewrite starts, and stops at the first
928-
/// `Aggregate` along each branch, collecting the columns every node above it
929-
/// reads in its own expressions. A nested `Subquery` is a different
930-
/// correlation scope and is skipped, the same way
927+
/// plan top-down instead, before the rewrite starts, collecting the columns
928+
/// every node above a grouping set `Aggregate` reads in its own expressions.
929+
/// It visits every input and walks through every `Aggregate` - a node that
930+
/// reads the pulled-up column may sit in a later input, above another
931+
/// (non-grouping-set) `Aggregate`, or between nested grouping sets, not only
932+
/// directly above the first `Aggregate` found. A nested `Subquery` is a
933+
/// different correlation scope and is skipped, the same way
931934
/// [`PullUpCorrelatedExpr::f_down`] skips it.
932935
fn columns_read_above_aggregate(plan: &LogicalPlan) -> BTreeSet<Column> {
933936
fn walk(plan: &LogicalPlan, above: &mut BTreeSet<Column>) -> bool {
934937
if matches!(plan, LogicalPlan::Subquery(_)) {
935938
return false;
936939
}
937-
if matches!(plan, LogicalPlan::Aggregate(_)) {
938-
return true;
940+
let mut found_below = false;
941+
for child in plan.inputs() {
942+
found_below |= walk(child, above);
939943
}
940-
let found_below = plan.inputs().into_iter().any(|child| walk(child, above));
941944
if found_below {
942945
for expr in plan.expressions() {
943946
above.extend(expr.column_refs().into_iter().cloned());
944947
}
945948
}
946949
found_below
950+
|| matches!(plan, LogicalPlan::Aggregate(aggregate) if aggregate
951+
.group_expr
952+
.iter()
953+
.any(|expr| matches!(expr, Expr::GroupingSet(_))))
947954
}
948955

949956
let mut above = BTreeSet::new();

‎datafusion/sqllogictest/test_files/subquery.slt‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2943,6 +2943,25 @@ SELECT gs_outer.k, EXISTS (SELECT 1 FROM gs_inner WHERE gs_inner.k = gs_outer.k
29432943
5 true
29442944
NULL false
29452945

2946+
# A filter above the grouping set `Aggregate`, through another `Aggregate`
2947+
# stacked on top of it, reads the NULL filled column. `walk` must not stop at
2948+
# the first `Aggregate` it finds (the outer `count(*)`) without recursing into
2949+
# its input, or it never sees that read and wrongly decorrelates.
2950+
statement error DataFusion error: This feature is not implemented: Physical plan does not support logical expression ScalarSubquery
2951+
SELECT gs_outer.k, (SELECT count(*) FROM (SELECT gs_inner.k AS kk FROM gs_inner WHERE gs_inner.k = gs_outer.k GROUP BY GROUPING SETS ((gs_inner.k), (gs_inner.j))) t WHERE t.kk IS NULL) FROM gs_outer ORDER BY gs_outer.k;
2952+
2953+
# The same read, but in a later join input rather than directly above the
2954+
# grouping set `Aggregate`. `walk` must visit every input, not stop once one
2955+
# of them turns up a grouping set `Aggregate` below it.
2956+
statement error DataFusion error: This feature is not implemented: Physical plan does not support logical expression Exists
2957+
SELECT gs_outer.k, EXISTS (SELECT 1 FROM (SELECT count(*) AS c FROM gs_inner) a JOIN (SELECT gs_inner.k FROM gs_inner WHERE gs_inner.k = gs_outer.k GROUP BY GROUPING SETS ((gs_inner.k), (gs_inner.j))) b ON a.c > 0 WHERE b.k IS NULL) FROM gs_outer ORDER BY gs_outer.k;
2958+
2959+
# The same read, above a second, nested grouping set `Aggregate` stacked on
2960+
# top of the first. `walk` must keep walking through a grouping set
2961+
# `Aggregate` too, not only a plain one.
2962+
statement error DataFusion error: This feature is not implemented: Physical plan does not support logical expression Exists
2963+
SELECT gs_outer.k, EXISTS (SELECT 1 FROM (SELECT gs_inner.k AS kk, gs_inner.j AS jj FROM gs_inner WHERE gs_inner.k = gs_outer.k GROUP BY GROUPING SETS ((gs_inner.k), (gs_inner.j))) t GROUP BY GROUPING SETS ((t.kk), (t.kk, t.jj)) HAVING t.kk IS NULL) FROM gs_outer ORDER BY gs_outer.k;
2964+
29462965
# The same sets with a HAVING that reads the NULL filled column. For k = 1 the
29472966
# `(j)` set yields the row `(NULL, 10)`, which passes the HAVING, so EXISTS is
29482967
# true; with `(j, k)` that row holds k = 1 and is filtered out. On main this

0 commit comments

Comments
 (0)