This is an automated email from the ASF dual-hosted git repository.

yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 6b715ca6a8b branch-4.1 [fix](nereids)EliminateGroupByKeyByUniform 
should replace exprId for alias #60020 (#61478)
6b715ca6a8b is described below

commit 6b715ca6a8b499db9c553413ff9de0c6cc2d46b3
Author: minghong <[email protected]>
AuthorDate: Thu Mar 19 10:48:02 2026 +0800

    branch-4.1 [fix](nereids)EliminateGroupByKeyByUniform should replace exprId 
for alias #60020 (#61478)
    
    ### What problem does this PR solve?
    pick 60020
    Issue Number: close #xxx
    
    Related PR: #xxx
    
    Problem Summary:
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [ ] Regression test
        - [ ] Unit Test
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason <!-- Add your reason?  -->
    
    - Behavior changed:
        - [ ] No.
        - [ ] Yes. <!-- Explain the behavior change -->
    
    - Does this need documentation?
        - [ ] No.
    - [ ] Yes. <!-- Add document PR link here. eg:
    https://github.com/apache/doris-website/pull/1214 -->
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label <!-- Add branch pick label that this PR
    should merge into -->
---
 .../rewrite/EliminateGroupByKeyByUniform.java      |   2 +-
 .../nereids/rules/rewrite/ExprIdRewriter.java      |  58 ++++-
 .../nereids/rules/rewrite/ExprIdRewriterTest.java  | 278 +++++++++++++++++++++
 3 files changed, 335 insertions(+), 3 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/EliminateGroupByKeyByUniform.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/EliminateGroupByKeyByUniform.java
index 9928b2d8276..73d79d6b075 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/EliminateGroupByKeyByUniform.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/EliminateGroupByKeyByUniform.java
@@ -64,7 +64,7 @@ public class EliminateGroupByKeyByUniform extends 
DefaultPlanRewriter<Map<ExprId
             return plan;
         }
         Map<ExprId, ExprId> replaceMap = new HashMap<>();
-        ExprIdRewriter.ReplaceRule replaceRule = new 
ExprIdRewriter.ReplaceRule(replaceMap);
+        ExprIdRewriter.ReplaceRule replaceRule = new 
ExprIdRewriter.ReplaceRule(replaceMap, true);
         exprIdReplacer = new ExprIdRewriter(replaceRule, jobContext);
         return plan.accept(this, replaceMap);
     }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ExprIdRewriter.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ExprIdRewriter.java
index 093abaadd13..ddeb1d7d6eb 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ExprIdRewriter.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ExprIdRewriter.java
@@ -25,6 +25,7 @@ import 
org.apache.doris.nereids.rules.expression.ExpressionPatternRuleFactory;
 import org.apache.doris.nereids.rules.expression.ExpressionRewrite;
 import org.apache.doris.nereids.rules.expression.ExpressionRuleExecutor;
 import org.apache.doris.nereids.rules.expression.ExpressionRuleType;
+import org.apache.doris.nereids.trees.expressions.Alias;
 import org.apache.doris.nereids.trees.expressions.ExprId;
 import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.Slot;
@@ -95,10 +96,59 @@ public class ExprIdRewriter extends ExpressionRewrite {
                         }
                     }
                 };
+        private static final DefaultExpressionRewriter<Map<ExprId, ExprId>> 
SLOT_ALIAS_REPLACER =
+                new DefaultExpressionRewriter<Map<ExprId, ExprId>>() {
+                    @Override
+                    public Expression visitSlotReference(SlotReference slot, 
Map<ExprId, ExprId> replaceMap) {
+                        ExprId newId = replaceMap.get(slot.getExprId());
+                        if (newId == null) {
+                            return slot;
+                        }
+                        ExprId lastId = newId;
+                        while (true) {
+                            newId = replaceMap.get(lastId);
+                            if (newId == null) {
+                                return slot.withExprId(lastId);
+                            } else {
+                                lastId = newId;
+                            }
+                        }
+                    }
+
+                    @Override
+                    public Expression visitAlias(Alias alias, Map<ExprId, 
ExprId> replaceMap) {
+                        ExprId newId = replaceMap.get(alias.getExprId());
+                        if (newId == null) {
+                            return alias;
+                        }
+                        ExprId lastId = newId;
+                        while (true) {
+                            newId = replaceMap.get(lastId);
+                            if (newId == null) {
+                                return alias.withExprId(lastId);
+                            } else {
+                                lastId = newId;
+                            }
+                        }
+                    }
+                };
         private final Map<ExprId, ExprId> replaceMap;
+        private DefaultExpressionRewriter<Map<ExprId, ExprId>> replacer;
 
-        public ReplaceRule(Map<ExprId, ExprId> replaceMap) {
+        /**
+         * constructor
+         */
+        public ReplaceRule(Map<ExprId, ExprId> replaceMap, boolean 
rewriteAlias) {
             this.replaceMap = replaceMap;
+            if (rewriteAlias) {
+                replacer = SLOT_ALIAS_REPLACER;
+            } else {
+                replacer = SLOT_REPLACER;
+            }
+        }
+
+        public ReplaceRule(Map<ExprId, ExprId> replaceMap) {
+            this(replaceMap, false);
         }
 
         @Override
@@ -106,7 +156,11 @@ public class ExprIdRewriter extends ExpressionRewrite {
             return ImmutableList.of(
                     matchesType(SlotReference.class).thenApply(ctx -> {
                         Slot slot = ctx.expr;
-                        return slot.accept(SLOT_REPLACER, replaceMap);
+                        return slot.accept(replacer, replaceMap);
+                    }).toRule(ExpressionRuleType.EXPR_ID_REWRITE_REPLACE),
+                    matchesType(Alias.class).thenApply(ctx -> {
+                        Alias alias = ctx.expr;
+                        return alias.accept(replacer, replaceMap);
                     }).toRule(ExpressionRuleType.EXPR_ID_REWRITE_REPLACE)
             );
         }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ExprIdRewriterTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ExprIdRewriterTest.java
new file mode 100644
index 00000000000..1acef3d6e74
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ExprIdRewriterTest.java
@@ -0,0 +1,278 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.rules.rewrite;
+
+import org.apache.doris.nereids.trees.expressions.Alias;
+import org.apache.doris.nereids.trees.expressions.ExprId;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
+import org.apache.doris.nereids.types.IntegerType;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Test for ExprIdRewriter, especially for visitAlias functionality.
+ *
+ * Background: In EliminateGroupByKeyByUniform rule, when a group by key is 
uniform,
+ * it will be replaced with any_value() and get a new ExprId. For example:
+ * - Original: field1#8 in aggregate output
+ * - After: any_value(field1#8) AS `field1`#10
+ * This creates a mapping {8 -> 10} in replaceMap.
+ *
+ * The problem: If upper LogicalProject has an Alias like `200 AS field1#8`, 
and
+ * LogicalResultSink expects field1#10, the Alias's ExprId must be rewritten 
from #8 to #10.
+ * Without visitAlias handling, the Alias ExprId won't be replaced, causing the
+ * LogicalResultSink to fail finding the expected input field.
+ */
+public class ExprIdRewriterTest {
+
+    /**
+     * Test that visitAlias correctly rewrites Alias ExprId.
+     *
+     * Scenario from EliminateGroupByKeyByUniform:
+     * - Input plan has: LogicalProject[projects=[200 AS `field1`#8, field2#9]]
+     * - After EliminateGroupByKeyByUniform, replaceMap contains {8 -> 10}
+     * - Expected: Alias ExprId should be rewritten from #8 to #10
+     *
+     * Without visitAlias: Alias remains `200 AS field1#8`, but upper plan 
expects #10
+     * With visitAlias: Alias becomes `200 AS field1#10`, matching upper plan 
expectation
+     */
+    @Test
+    void testVisitAliasRewritesExprId() {
+        // Create an Alias with ExprId #8: 200 AS `field1`#8
+        ExprId originalExprId = new ExprId(8);
+        ExprId targetExprId = new ExprId(10);
+        IntegerLiteral literal = new IntegerLiteral(200);
+        Alias originalAlias = new Alias(originalExprId, literal, "field1");
+
+        // Create replaceMap: {8 -> 10}
+        Map<ExprId, ExprId> replaceMap = new HashMap<>();
+        replaceMap.put(originalExprId, targetExprId);
+
+        // Create ReplaceRule and apply it to the Alias
+        Expression result = originalAlias.accept(
+                new 
org.apache.doris.nereids.trees.expressions.visitor.DefaultExpressionRewriter<Map<ExprId,
 ExprId>>() {
+                    @Override
+                    public Expression visitAlias(Alias alias, Map<ExprId, 
ExprId> context) {
+                        ExprId newId = context.get(alias.getExprId());
+                        if (newId == null) {
+                            return alias;
+                        }
+                        ExprId lastId = newId;
+                        while (true) {
+                            newId = context.get(lastId);
+                            if (newId == null) {
+                                return alias.withExprId(lastId);
+                            } else {
+                                lastId = newId;
+                            }
+                        }
+                    }
+                }, replaceMap);
+
+        // Verify: the result should be an Alias with ExprId #10
+        Assertions.assertTrue(result instanceof Alias, "Result should be an 
Alias");
+        Alias rewrittenAlias = (Alias) result;
+        Assertions.assertEquals(targetExprId, rewrittenAlias.getExprId(),
+                "Alias ExprId should be rewritten from #8 to #10");
+        Assertions.assertEquals("field1", rewrittenAlias.getName(),
+                "Alias name should remain unchanged");
+        Assertions.assertEquals(literal, rewrittenAlias.child(),
+                "Alias child expression should remain unchanged");
+    }
+
+    /**
+     * Test chained ExprId replacement for Alias.
+     * e.g., replaceMap: {0 -> 3, 1 -> 6, 6 -> 7}
+     * Alias with ExprId #1 should be rewritten to #7 (1 -> 6 -> 7)
+     */
+    @Test
+    void testVisitAliasChainedReplacement() {
+        // Create an Alias with ExprId #1
+        ExprId exprId1 = new ExprId(1);
+        ExprId exprId6 = new ExprId(6);
+        ExprId exprId7 = new ExprId(7);
+        IntegerLiteral literal = new IntegerLiteral(100);
+        Alias originalAlias = new Alias(exprId1, literal, "col");
+
+        // Create replaceMap: {0 -> 3, 1 -> 6, 6 -> 7}
+        Map<ExprId, ExprId> replaceMap = new HashMap<>();
+        replaceMap.put(new ExprId(0), new ExprId(3));
+        replaceMap.put(exprId1, exprId6);
+        replaceMap.put(exprId6, exprId7);
+
+        // Apply replacement using the same logic as ReplaceRule.visitAlias
+        Expression result = originalAlias.accept(
+                new 
org.apache.doris.nereids.trees.expressions.visitor.DefaultExpressionRewriter<Map<ExprId,
 ExprId>>() {
+                    @Override
+                    public Expression visitAlias(Alias alias, Map<ExprId, 
ExprId> context) {
+                        ExprId newId = context.get(alias.getExprId());
+                        if (newId == null) {
+                            return alias;
+                        }
+                        ExprId lastId = newId;
+                        while (true) {
+                            newId = context.get(lastId);
+                            if (newId == null) {
+                                return alias.withExprId(lastId);
+                            } else {
+                                lastId = newId;
+                            }
+                        }
+                    }
+                }, replaceMap);
+
+        // Verify: ExprId should follow the chain 1 -> 6 -> 7
+        Assertions.assertTrue(result instanceof Alias, "Result should be an 
Alias");
+        Alias rewrittenAlias = (Alias) result;
+        Assertions.assertEquals(exprId7, rewrittenAlias.getExprId(),
+                "Alias ExprId should follow chain: 1 -> 6 -> 7, final ExprId 
should be #7");
+    }
+
+    /**
+     * Test that Alias without mapping in replaceMap remains unchanged.
+     */
+    @Test
+    void testVisitAliasNoMapping() {
+        // Create an Alias with ExprId #5
+        ExprId exprId5 = new ExprId(5);
+        IntegerLiteral literal = new IntegerLiteral(300);
+        Alias originalAlias = new Alias(exprId5, literal, "unmapped");
+
+        // Create replaceMap that doesn't contain #5
+        Map<ExprId, ExprId> replaceMap = new HashMap<>();
+        replaceMap.put(new ExprId(1), new ExprId(2));
+        replaceMap.put(new ExprId(3), new ExprId(4));
+
+        // Apply replacement
+        Expression result = originalAlias.accept(
+                new 
org.apache.doris.nereids.trees.expressions.visitor.DefaultExpressionRewriter<Map<ExprId,
 ExprId>>() {
+                    @Override
+                    public Expression visitAlias(Alias alias, Map<ExprId, 
ExprId> context) {
+                        ExprId newId = context.get(alias.getExprId());
+                        if (newId == null) {
+                            return alias;
+                        }
+                        ExprId lastId = newId;
+                        while (true) {
+                            newId = context.get(lastId);
+                            if (newId == null) {
+                                return alias.withExprId(lastId);
+                            } else {
+                                lastId = newId;
+                            }
+                        }
+                    }
+                }, replaceMap);
+
+        // Verify: Alias should remain unchanged
+        Assertions.assertSame(originalAlias, result,
+                "Alias without mapping should remain unchanged (same object 
reference)");
+        Assertions.assertEquals(exprId5, ((Alias) result).getExprId(),
+                "Alias ExprId should remain #5");
+    }
+
+    /**
+     * Test SlotReference ExprId rewriting for comparison.
+     * This demonstrates that both SlotReference and Alias need proper ExprId 
rewriting.
+     */
+    @Test
+    void testVisitSlotReferenceRewritesExprId() {
+        // Create a SlotReference with ExprId #8
+        ExprId originalExprId = new ExprId(8);
+        ExprId targetExprId = new ExprId(10);
+        SlotReference originalSlot = new SlotReference(
+                originalExprId, "field1", IntegerType.INSTANCE, true, 
ImmutableList.of());
+
+        // Create replaceMap: {8 -> 10}
+        Map<ExprId, ExprId> replaceMap = new HashMap<>();
+        replaceMap.put(originalExprId, targetExprId);
+
+        // Apply replacement using the same logic as 
ReplaceRule.visitSlotReference
+        Expression result = originalSlot.accept(
+                new 
org.apache.doris.nereids.trees.expressions.visitor.DefaultExpressionRewriter<Map<ExprId,
 ExprId>>() {
+                    @Override
+                    public Expression visitSlotReference(SlotReference slot, 
Map<ExprId, ExprId> context) {
+                        ExprId newId = context.get(slot.getExprId());
+                        if (newId == null) {
+                            return slot;
+                        }
+                        ExprId lastId = newId;
+                        while (true) {
+                            newId = context.get(lastId);
+                            if (newId == null) {
+                                return slot.withExprId(lastId);
+                            } else {
+                                lastId = newId;
+                            }
+                        }
+                    }
+                }, replaceMap);
+
+        // Verify: SlotReference ExprId should be rewritten
+        Assertions.assertTrue(result instanceof SlotReference, "Result should 
be a SlotReference");
+        SlotReference rewrittenSlot = (SlotReference) result;
+        Assertions.assertEquals(targetExprId, rewrittenSlot.getExprId(),
+                "SlotReference ExprId should be rewritten from #8 to #10");
+        Assertions.assertEquals("field1", rewrittenSlot.getName(),
+                "SlotReference name should remain unchanged");
+    }
+
+    /**
+     * Integration test: Verify that without visitAlias, Alias ExprId won't be 
rewritten,
+     * demonstrating the necessity of visitAlias in ExprIdRewriter.ReplaceRule.
+     *
+     * This test shows what happens when visitAlias is NOT implemented:
+     * - The Alias ExprId remains unchanged
+     * - This causes issues in EliminateGroupByKeyByUniform where upper plan 
expects new ExprId
+     */
+    @Test
+    void testWithoutVisitAliasExprIdNotRewritten() {
+        // Create an Alias with ExprId #8
+        ExprId originalExprId = new ExprId(8);
+        ExprId targetExprId = new ExprId(10);
+        IntegerLiteral literal = new IntegerLiteral(200);
+        Alias originalAlias = new Alias(originalExprId, literal, "field1");
+
+        // Create replaceMap: {8 -> 10}
+        Map<ExprId, ExprId> replaceMap = new HashMap<>();
+        replaceMap.put(originalExprId, targetExprId);
+
+        // Apply replacement WITHOUT visitAlias override (default behavior)
+        Expression result = originalAlias.accept(
+                new 
org.apache.doris.nereids.trees.expressions.visitor.DefaultExpressionRewriter<Map<ExprId,
 ExprId>>() {
+                    // Note: No visitAlias override - uses default behavior
+                }, replaceMap);
+
+        // Without visitAlias: Alias ExprId remains #8 (unchanged)
+        Assertions.assertTrue(result instanceof Alias, "Result should be an 
Alias");
+        Alias unchangedAlias = (Alias) result;
+        Assertions.assertEquals(originalExprId, unchangedAlias.getExprId(),
+                "Without visitAlias, Alias ExprId should remain #8 (not 
rewritten)");
+
+        // This demonstrates the bug: upper plan expects #10 but gets #8
+        Assertions.assertNotEquals(targetExprId, unchangedAlias.getExprId(),
+                "This shows the problem: ExprId #10 is expected but #8 is 
returned");
+    }
+}


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

Reply via email to