github-actions[bot] commented on code in PR #66570:
URL: https://github.com/apache/doris/pull/66570#discussion_r3773091220


##########
fe/fe-core/src/test/java/org/apache/doris/planner/LocalShuffleNodeCoverageTest.java:
##########
@@ -855,6 +860,263 @@ public void testExchangeNodeBranches() {
         Assertions.assertEquals(LocalExchangeType.NOOP, noopOutput.second);
     }
 
+    @Test
+    public void testAggregationNodeDistinctFinalizeRequiresHash() {
+        // count(distinct k) without group-by: the finalize merge agg emits 
per-instance
+        // scalar values that the parent sums (sum0(multi_distinct_count(...)) 
above), so
+        // the input must be hash-partitioned by the distinct key. Pre-fix 
this agg got
+        // NoRequire and a PASSTHROUGH local exchange below scattered same-key 
rows across
+        // instances → the parent double-counted (result = correct × task 
count).
+        for (String fn : new String[] {"multi_distinct_count", 
"multi_distinct_sum",
+                "multi_distinct_group_concat"}) {
+            AggContext agg = 
buildAggContext(Collections.singletonList(multiDistinctFunction(fn)), /* 
groupByExprs */ true,
+                    /* merge */ true, /* needsFinalize */ true, 
LocalExchangeType.NOOP, KEYED_DISTRIBUTE_EXPRS);
+            Pair<PlanNode, LocalExchangeType> output = 
agg.node.enforceAndDeriveLocalExchange(
+                    agg.ctx, null, LocalExchangeTypeRequire.noRequire());
+            Assertions.assertEquals(LocalExchangeNode.RequireHash.class, 
agg.child.lastRequire.getClass(),
+                    fn + " finalize agg must require hash input");
+            
Assertions.assertEquals(LocalExchangeType.LOCAL_EXECUTION_HASH_SHUFFLE, 
output.second);
+            assertChildLocalExchangeType(agg.node, 0, 
LocalExchangeType.LOCAL_EXECUTION_HASH_SHUFFLE);
+        }
+    }
+
+    @Test
+    public void testAggregationNodeDistinctFinalizeWithParentHashRequirement() 
{
+        // A parent that already requires hash must not change the agg's own 
hash demand.
+        AggContext agg = 
buildAggContext(Collections.singletonList(multiDistinctFunction("multi_distinct_count")),
 /* groupByExprs */ true,
+                /* merge */ true, /* needsFinalize */ true, 
LocalExchangeType.NOOP, KEYED_DISTRIBUTE_EXPRS);
+        Pair<PlanNode, LocalExchangeType> output = 
agg.node.enforceAndDeriveLocalExchange(
+                agg.ctx, null, LocalExchangeTypeRequire.requireHash());
+        Assertions.assertEquals(LocalExchangeNode.RequireHash.class, 
agg.child.lastRequire.getClass());
+        
Assertions.assertEquals(LocalExchangeType.LOCAL_EXECUTION_HASH_SHUFFLE, 
output.second);
+        assertChildLocalExchangeType(agg.node, 0, 
LocalExchangeType.LOCAL_EXECUTION_HASH_SHUFFLE);
+    }
+
+    @Test
+    public void testAggregationNodeDirectMultiDistinctNoKeyStaysNoRequire() {
+        // A directly called scalar multi_distinct_count(col) has 
isDistinct=false and
+        // no child distribute exprs (SplitAggWithoutDistinct builds a LOCAL 
aggregate
+        // without partition exprs). It must NOT be given a HASH requirement — 
a
+        // zero-key HASH exchange would collapse the whole input onto one task 
per BE.
+        AggContext agg = 
buildAggContext(Collections.singletonList(multiDistinctFunction("multi_distinct_count")),
+                /* groupByExprs */ true, /* merge */ true, /* needsFinalize */ 
true,
+                LocalExchangeType.NOOP, null);
+        Pair<PlanNode, LocalExchangeType> output = 
agg.node.enforceAndDeriveLocalExchange(
+                agg.ctx, null, LocalExchangeTypeRequire.noRequire());
+        Assertions.assertEquals(LocalExchangeNode.NoRequire.class, 
agg.child.lastRequire.getClass(),
+                "direct multi_distinct with no effective key must stay 
NoRequire");
+        Assertions.assertEquals(LocalExchangeType.NOOP, output.second);
+        Assertions.assertSame(agg.child, agg.node.getChild(0));
+    }
+
+    @Test
+    public void testAggregationNodeNoPartitionNonFinalizeBaseClassRequire() {
+        // COUNT(*)-style non-finalize (LOCAL) agg: no partition requirement, 
so
+        // the non-finalize arm of the first branch falls back to base class
+        // behavior (NOOP for a non-serial child). The agg exprs are non-empty
+        // (a plain count function) so the AggSink branch is exercised rather
+        // than DistinctStreamingAgg.
+        AggContext agg = buildAggContext(
+                Collections.singletonList(plainAggregateFunction("count")), /* 
groupByExprs */ true,
+                /* merge */ false, /* needsFinalize */ false, 
LocalExchangeType.NOOP, null);
+        Pair<PlanNode, LocalExchangeType> output = 
agg.node.enforceAndDeriveLocalExchange(
+                agg.ctx, null, LocalExchangeTypeRequire.noRequire());
+        Assertions.assertEquals(LocalExchangeNode.NoRequire.class, 
agg.child.lastRequire.getClass());
+        Assertions.assertEquals(LocalExchangeType.NOOP, output.second);
+        Assertions.assertSame(agg.child, agg.node.getChild(0));
+    }
+
+    @Test
+    public void testAggregationNodeNoPartitionFinalizeStaysNoRequire() {

Review Comment:
   [P2] Make these finalize fixtures enter AggSink
   
   Both this COUNT(*) case and 
`testAggregationNodeGroupByFinalizeRequiresHash()` pass an empty 
aggregate-expression list. With the default 
`enableDistinctStreamingAggregation=true` and no `sortByGroupKey`, 
`canUseDistinctStreamingAgg()` is therefore true, so both tests exercise the 
separate DistinctStreamingAgg branch at `AggregationNode.java:291` and never 
reach the changed AggSink/`hasPartitionRequirement()` logic they claim to 
cover. A translated COUNT(*) has a `count` `FunctionCallExpr`. Please pass 
`plainAggregateFunction("count")` in both fixtures (and keep any pure-dedup 
checks separately), so removing the changed AggSink finalize behavior actually 
fails these tests.



##########
fe/fe-core/src/test/java/org/apache/doris/planner/LocalShuffleNodeCoverageTest.java:
##########
@@ -855,6 +860,263 @@ public void testExchangeNodeBranches() {
         Assertions.assertEquals(LocalExchangeType.NOOP, noopOutput.second);
     }
 
+    @Test
+    public void testAggregationNodeDistinctFinalizeRequiresHash() {
+        // count(distinct k) without group-by: the finalize merge agg emits 
per-instance
+        // scalar values that the parent sums (sum0(multi_distinct_count(...)) 
above), so
+        // the input must be hash-partitioned by the distinct key. Pre-fix 
this agg got
+        // NoRequire and a PASSTHROUGH local exchange below scattered same-key 
rows across
+        // instances → the parent double-counted (result = correct × task 
count).
+        for (String fn : new String[] {"multi_distinct_count", 
"multi_distinct_sum",
+                "multi_distinct_group_concat"}) {
+            AggContext agg = 
buildAggContext(Collections.singletonList(multiDistinctFunction(fn)), /* 
groupByExprs */ true,
+                    /* merge */ true, /* needsFinalize */ true, 
LocalExchangeType.NOOP, KEYED_DISTRIBUTE_EXPRS);
+            Pair<PlanNode, LocalExchangeType> output = 
agg.node.enforceAndDeriveLocalExchange(
+                    agg.ctx, null, LocalExchangeTypeRequire.noRequire());
+            Assertions.assertEquals(LocalExchangeNode.RequireHash.class, 
agg.child.lastRequire.getClass(),
+                    fn + " finalize agg must require hash input");
+            
Assertions.assertEquals(LocalExchangeType.LOCAL_EXECUTION_HASH_SHUFFLE, 
output.second);
+            assertChildLocalExchangeType(agg.node, 0, 
LocalExchangeType.LOCAL_EXECUTION_HASH_SHUFFLE);
+        }
+    }
+
+    @Test
+    public void testAggregationNodeDistinctFinalizeWithParentHashRequirement() 
{
+        // A parent that already requires hash must not change the agg's own 
hash demand.
+        AggContext agg = 
buildAggContext(Collections.singletonList(multiDistinctFunction("multi_distinct_count")),
 /* groupByExprs */ true,
+                /* merge */ true, /* needsFinalize */ true, 
LocalExchangeType.NOOP, KEYED_DISTRIBUTE_EXPRS);
+        Pair<PlanNode, LocalExchangeType> output = 
agg.node.enforceAndDeriveLocalExchange(
+                agg.ctx, null, LocalExchangeTypeRequire.requireHash());
+        Assertions.assertEquals(LocalExchangeNode.RequireHash.class, 
agg.child.lastRequire.getClass());
+        
Assertions.assertEquals(LocalExchangeType.LOCAL_EXECUTION_HASH_SHUFFLE, 
output.second);
+        assertChildLocalExchangeType(agg.node, 0, 
LocalExchangeType.LOCAL_EXECUTION_HASH_SHUFFLE);
+    }
+
+    @Test
+    public void testAggregationNodeDirectMultiDistinctNoKeyStaysNoRequire() {
+        // A directly called scalar multi_distinct_count(col) has 
isDistinct=false and
+        // no child distribute exprs (SplitAggWithoutDistinct builds a LOCAL 
aggregate
+        // without partition exprs). It must NOT be given a HASH requirement — 
a
+        // zero-key HASH exchange would collapse the whole input onto one task 
per BE.
+        AggContext agg = 
buildAggContext(Collections.singletonList(multiDistinctFunction("multi_distinct_count")),
+                /* groupByExprs */ true, /* merge */ true, /* needsFinalize */ 
true,
+                LocalExchangeType.NOOP, null);
+        Pair<PlanNode, LocalExchangeType> output = 
agg.node.enforceAndDeriveLocalExchange(
+                agg.ctx, null, LocalExchangeTypeRequire.noRequire());
+        Assertions.assertEquals(LocalExchangeNode.NoRequire.class, 
agg.child.lastRequire.getClass(),
+                "direct multi_distinct with no effective key must stay 
NoRequire");
+        Assertions.assertEquals(LocalExchangeType.NOOP, output.second);
+        Assertions.assertSame(agg.child, agg.node.getChild(0));
+    }
+
+    @Test
+    public void testAggregationNodeNoPartitionNonFinalizeBaseClassRequire() {
+        // COUNT(*)-style non-finalize (LOCAL) agg: no partition requirement, 
so
+        // the non-finalize arm of the first branch falls back to base class
+        // behavior (NOOP for a non-serial child). The agg exprs are non-empty
+        // (a plain count function) so the AggSink branch is exercised rather
+        // than DistinctStreamingAgg.
+        AggContext agg = buildAggContext(
+                Collections.singletonList(plainAggregateFunction("count")), /* 
groupByExprs */ true,
+                /* merge */ false, /* needsFinalize */ false, 
LocalExchangeType.NOOP, null);
+        Pair<PlanNode, LocalExchangeType> output = 
agg.node.enforceAndDeriveLocalExchange(
+                agg.ctx, null, LocalExchangeTypeRequire.noRequire());
+        Assertions.assertEquals(LocalExchangeNode.NoRequire.class, 
agg.child.lastRequire.getClass());
+        Assertions.assertEquals(LocalExchangeType.NOOP, output.second);
+        Assertions.assertSame(agg.child, agg.node.getChild(0));
+    }
+
+    @Test
+    public void testAggregationNodeNoPartitionFinalizeStaysNoRequire() {
+        // COUNT(*)-style agg (no group keys, no DISTINCT aggregates) 
genuinely has no
+        // partition requirement: the input distribution is irrelevant.
+        AggContext agg = buildAggContext(Collections.emptyList(), /* 
groupByExprs */ true,
+                /* merge */ true, /* needsFinalize */ true, 
LocalExchangeType.NOOP, null);
+        Pair<PlanNode, LocalExchangeType> output = 
agg.node.enforceAndDeriveLocalExchange(
+                agg.ctx, null, LocalExchangeTypeRequire.noRequire());
+        Assertions.assertEquals(LocalExchangeNode.NoRequire.class, 
agg.child.lastRequire.getClass());
+        Assertions.assertEquals(LocalExchangeType.NOOP, output.second);
+        Assertions.assertSame(agg.child, agg.node.getChild(0));
+    }
+
+    @Test
+    public void testAggregationNodeDistinctLocalPhaseDefaultLeRequiresHash() {
+        // LOCAL (FIRST/SECOND, non-merge, non-finalize) phase of a distinct 
agg with the
+        // default enable_local_exchange_before_agg=true: BE requires HASH here
+        // (partition_exprs non-empty), so the FE must mirror that.
+        AggContext agg = 
buildAggContext(Collections.singletonList(multiDistinctFunction("multi_distinct_count")),
 /* groupByExprs */ true,
+                /* merge */ false, /* needsFinalize */ false, 
LocalExchangeType.NOOP, KEYED_DISTRIBUTE_EXPRS);
+        Pair<PlanNode, LocalExchangeType> output = 
agg.node.enforceAndDeriveLocalExchange(
+                agg.ctx, null, LocalExchangeTypeRequire.noRequire());
+        Assertions.assertEquals(LocalExchangeNode.RequireHash.class, 
agg.child.lastRequire.getClass(),
+                "LOCAL distinct phase with default LE requires hash");
+        
Assertions.assertEquals(LocalExchangeType.LOCAL_EXECUTION_HASH_SHUFFLE, 
output.second);
+        assertChildLocalExchangeType(agg.node, 0, 
LocalExchangeType.LOCAL_EXECUTION_HASH_SHUFFLE);
+    }
+
+    @Test
+    public void 
testAggregationNodeDistinctLocalPhaseWithLeDisabledStaysNoRequire() {
+        // LOCAL distinct phase + enable_local_exchange_before_agg=false → 
base class
+        // behavior (NOOP for a non-serial child): user explicitly opted out 
of pre-agg LE.
+        AggContext agg = 
buildAggContext(Collections.singletonList(multiDistinctFunction("multi_distinct_count")),
 /* groupByExprs */ true,
+                /* merge */ false, /* needsFinalize */ false, 
LocalExchangeType.NOOP, KEYED_DISTRIBUTE_EXPRS);
+        SessionVariable sessionVariable = new SessionVariable();
+        sessionVariable.enableLocalExchangeBeforeAgg = false;
+        
Mockito.when(agg.connectContext.getSessionVariable()).thenReturn(sessionVariable);
+        Pair<PlanNode, LocalExchangeType> output = 
agg.node.enforceAndDeriveLocalExchange(
+                agg.ctx, null, LocalExchangeTypeRequire.noRequire());
+        Assertions.assertEquals(LocalExchangeNode.NoRequire.class, 
agg.child.lastRequire.getClass(),
+                "LOCAL distinct phase with LE disabled keeps no alignment 
requirement");
+        Assertions.assertEquals(LocalExchangeType.NOOP, output.second);
+        Assertions.assertSame(agg.child, agg.node.getChild(0));
+    }
+
+    @Test
+    public void testAggregationNodeDistinctFirstMergeRequiresHash() {
+        // FIRST_MERGE (correctness-required) keeps the hash demand even when 
the
+        // user opts out of pre-agg local exchanges 
(enable_local_exchange_before_agg
+        // = false): removing the !isMerge() exemption must not weaken it.
+        AggContext agg = 
buildAggContext(Collections.singletonList(multiDistinctFunction("multi_distinct_count")),
 /* groupByExprs */ true,
+                /* merge */ true, /* needsFinalize */ false, 
LocalExchangeType.NOOP, KEYED_DISTRIBUTE_EXPRS);
+        SessionVariable sessionVariable = new SessionVariable();
+        sessionVariable.enableLocalExchangeBeforeAgg = false;
+        
Mockito.when(agg.connectContext.getSessionVariable()).thenReturn(sessionVariable);
+        Pair<PlanNode, LocalExchangeType> output = 
agg.node.enforceAndDeriveLocalExchange(
+                agg.ctx, null, LocalExchangeTypeRequire.noRequire());
+        Assertions.assertEquals(LocalExchangeNode.RequireHash.class, 
agg.child.lastRequire.getClass(),
+                "FIRST_MERGE must keep the hash demand with 
enable_local_exchange_before_agg=false");
+        
Assertions.assertEquals(LocalExchangeType.LOCAL_EXECUTION_HASH_SHUFFLE, 
output.second);
+        assertChildLocalExchangeType(agg.node, 0, 
LocalExchangeType.LOCAL_EXECUTION_HASH_SHUFFLE);
+    }
+
+    @Test
+    public void testAggregationNodeGroupByFinalizeRequiresHash() {
+        // GROUP BY finalize agg requires hash input; when the parent has no 
hash
+        // requirement the semantic partition exprs (group keys) drive the 
decision.
+        AggContext agg = buildAggContext(Collections.emptyList(), /* 
groupByExprs */ false,
+                /* merge */ true, /* needsFinalize */ true, 
LocalExchangeType.NOOP, null);
+        Pair<PlanNode, LocalExchangeType> output = 
agg.node.enforceAndDeriveLocalExchange(
+                agg.ctx, null, LocalExchangeTypeRequire.noRequire());
+        Assertions.assertEquals(LocalExchangeNode.RequireHash.class, 
agg.child.lastRequire.getClass());
+        
Assertions.assertEquals(LocalExchangeType.LOCAL_EXECUTION_HASH_SHUFFLE, 
output.second);
+        assertChildLocalExchangeType(agg.node, 0, 
LocalExchangeType.LOCAL_EXECUTION_HASH_SHUFFLE);
+    }
+
+    @Test
+    public void 
testAggregationNodeGroupByLocalPhaseWithLeDisabledStaysNoRequire() {
+        // GROUP BY local phase + enable_local_exchange_before_agg=false → 
base class
+        // behavior (NOOP for a non-serial child): user explicitly opted out 
of pre-agg LE.
+        // aggExprs is non-empty so the AggSink branch is exercised (an empty 
aggExprs
+        // would route through DistinctStreamingAgg with its own hash logic).
+        AggContext agg = 
buildAggContext(Collections.singletonList(multiDistinctFunction("multi_distinct_count")),
 /* groupByExprs */ false,
+                /* merge */ false, /* needsFinalize */ false, 
LocalExchangeType.NOOP, KEYED_DISTRIBUTE_EXPRS);
+        SessionVariable sessionVariable = new SessionVariable();
+        sessionVariable.enableLocalExchangeBeforeAgg = false;
+        
Mockito.when(agg.connectContext.getSessionVariable()).thenReturn(sessionVariable);
+        Pair<PlanNode, LocalExchangeType> output = 
agg.node.enforceAndDeriveLocalExchange(
+                agg.ctx, null, LocalExchangeTypeRequire.noRequire());
+        Assertions.assertEquals(LocalExchangeNode.NoRequire.class, 
agg.child.lastRequire.getClass());
+        Assertions.assertEquals(LocalExchangeType.NOOP, output.second);
+        Assertions.assertSame(agg.child, agg.node.getChild(0));
+    }
+
+    @Test
+    public void testAggregationNodeRequiresShuffleForCorrectness() {

Review Comment:
   [P2] Exercise the inherited-shuffle aggregate path
   
   This test only calls the node-local `requiresShuffleForCorrectness()`, while 
every aggregate created by `buildAggContext()` uses a Mockito context whose 
`hasShuffleForCorrectnessAncestor()` remains false. The other true-ancestor 
fixtures set the flag directly on Union, and the `agg_phase=1` planner case 
reaches the self-detected `multi_distinct_` path. Consequently, dropping the 
newly added inherited half of `selfOrInheritedShuffled`, or selecting grouping 
expressions instead of the child distribute key in an intermediate aggregate, 
leaves all of these tests green even though the documented `AggGlobal -> 
DISTINCT_LOCAL -> FIRST_MERGE -> FIRST_LOCAL -> Union` chain relies on it. 
Please add an aggregate fixture with inherited shuffle=true and a 
non-multi-distinct aggregate, and assert that the HASH LE uses the child 
distribute expressions; ideally also cover the real multi-stage plan.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to