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

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


The following commit(s) were added to refs/heads/branch-4.2 by this push:
     new 1b182a2816a branch-4.2: [fix](rbo) Give the constants materialized by 
constant propagation a fresh exprId #69111 (#69406)
1b182a2816a is described below

commit 1b182a2816ab4a0426cb133e37fa65b065bf5e2b
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Oct 23 14:10:33 2026 +0800

    branch-4.2: [fix](rbo) Give the constants materialized by constant 
propagation a fresh exprId #69111 (#69406)
    
    Cherry-picked from #69111
    
    ---------
    
    Co-authored-by: minghong <[email protected]>
---
 .../org/apache/doris/nereids/StatementContext.java |  38 ++-
 .../nereids/rules/rewrite/ConstantPropagation.java |  82 +++++-
 .../nereids/rules/rewrite/ExprIdRewriter.java      | 139 ++++++----
 .../nereids/rules/rewrite/RewriteCteChildren.java  |  89 ++++++-
 .../ConstantPropagationFreshExprIdTest.java        | 280 +++++++++++++++++++++
 .../rules/rewrite/ConstantPropagationTest.java     | 154 +++++++++++-
 .../rewrite/CteConsumerOutputsRefresherTest.java   | 107 ++++++++
 .../nereids/rules/rewrite/ExprIdRewriterTest.java  |  35 +++
 8 files changed, 852 insertions(+), 72 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/StatementContext.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/StatementContext.java
index 7e25d4d1157..1e76e5dde72 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/StatementContext.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/StatementContext.java
@@ -183,6 +183,11 @@ public class StatementContext implements Closeable {
     private final Map<CTEId, List<Pair<Multimap<Slot, Slot>, Group>>> 
cteIdToConsumerGroup = new HashMap<>();
     private final Map<CTEId, LogicalPlan> rewrittenCteProducer = new 
HashMap<>();
     private final Map<CTEId, LogicalPlan> rewrittenCteConsumer = new 
HashMap<>();
+    // The exprIds which a rule renamed while it rewrote the body of a CTE 
producer, keyed by the previous exprId.
+    // The consumers of that CTE map their slots to the outputs of the 
producer by exprId and are rewritten before
+    // the producer subtree, so every rule which renames an output of a 
producer body has to publish it here, and
+    // RewriteCteChildren refreshes the consumer maps with it. 
ConstantPropagation is currently the only publisher.
+    private final Map<CTEId, Map<ExprId, ExprId>> producerExprIdRemap = new 
HashMap<>();
     private final Set<String> viewDdlSqlSet = Sets.newHashSet();
     private final SqlCacheContext sqlCacheContext;
 
@@ -772,6 +777,19 @@ public class StatementContext implements Closeable {
         return rewrittenCteConsumer;
     }
 
+    /**
+     * Record that a rule renamed exprIds while it rewrote the body of a CTE 
producer. Rules may rename more than
+     * once, so the mappings are accumulated instead of replaced, and a reader 
has to follow the chain to its end.
+     */
+    public void addProducerExprIdRemap(CTEId cteId, Map<ExprId, ExprId> 
renamedExprIds) {
+        producerExprIdRemap.computeIfAbsent(cteId, k -> new 
HashMap<>()).putAll(renamedExprIds);
+    }
+
+    /** The exprIds renamed while rewriting the body of a CTE producer, keyed 
by their previous exprId. */
+    public Map<ExprId, ExprId> getProducerExprIdRemap(CTEId cteId) {
+        return producerExprIdRemap.getOrDefault(cteId, Collections.emptyMap());
+    }
+
     /** Clear CTE-related rewrite and memo state before rebuilding it from a 
new plan tree. */
     public void clearCteEnvironment() {
         cteIdToConsumers.clear();
@@ -782,6 +800,7 @@ public class StatementContext implements Closeable {
         cteIdToConsumerGroup.clear();
         rewrittenCteProducer.clear();
         rewrittenCteConsumer.clear();
+        producerExprIdRemap.clear();
     }
 
     /**
@@ -796,7 +815,8 @@ public class StatementContext implements Closeable {
                 new HashMap<>(consumerIdToLimitRows),
                 copyMapOfLists(cteIdToConsumerGroup),
                 new HashMap<>(rewrittenCteProducer),
-                new HashMap<>(rewrittenCteConsumer));
+                new HashMap<>(rewrittenCteConsumer),
+                copyMapOfMaps(producerExprIdRemap));
     }
 
     /** Restore CTE-related environment from snapshot. */
@@ -824,6 +844,17 @@ public class StatementContext implements Closeable {
 
         rewrittenCteConsumer.clear();
         rewrittenCteConsumer.putAll(snapshot.rewrittenCteConsumer);
+
+        producerExprIdRemap.clear();
+        producerExprIdRemap.putAll(snapshot.producerExprIdRemap);
+    }
+
+    private static <K, V, W> Map<K, Map<V, W>> copyMapOfMaps(Map<K, Map<V, W>> 
source) {
+        Map<K, Map<V, W>> copied = new HashMap<>();
+        for (Map.Entry<K, Map<V, W>> entry : source.entrySet()) {
+            copied.put(entry.getKey(), new HashMap<>(entry.getValue()));
+        }
+        return copied;
     }
 
     private static <K, V> Map<K, Set<V>> copyMapOfSets(Map<K, Set<V>> source) {
@@ -852,6 +883,7 @@ public class StatementContext implements Closeable {
         private final Map<CTEId, List<Pair<Multimap<Slot, Slot>, Group>>> 
cteIdToConsumerGroup;
         private final Map<CTEId, LogicalPlan> rewrittenCteProducer;
         private final Map<CTEId, LogicalPlan> rewrittenCteConsumer;
+        private final Map<CTEId, Map<ExprId, ExprId>> producerExprIdRemap;
 
         /**
          * cte related structures in StatementContext
@@ -864,7 +896,8 @@ public class StatementContext implements Closeable {
                 Map<RelationId, Long> consumerIdToLimitRows,
                 Map<CTEId, List<Pair<Multimap<Slot, Slot>, Group>>> 
cteIdToConsumerGroup,
                 Map<CTEId, LogicalPlan> rewrittenCteProducer,
-                Map<CTEId, LogicalPlan> rewrittenCteConsumer) {
+                Map<CTEId, LogicalPlan> rewrittenCteConsumer,
+                Map<CTEId, Map<ExprId, ExprId>> producerExprIdRemap) {
             this.cteIdToConsumers = cteIdToConsumers;
             this.cteIdToOutputIds = cteIdToOutputIds;
             this.cteIdToProducer = cteIdToProducer;
@@ -873,6 +906,7 @@ public class StatementContext implements Closeable {
             this.cteIdToConsumerGroup = cteIdToConsumerGroup;
             this.rewrittenCteProducer = rewrittenCteProducer;
             this.rewrittenCteConsumer = rewrittenCteConsumer;
+            this.producerExprIdRemap = producerExprIdRemap;
         }
     }
 
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ConstantPropagation.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ConstantPropagation.java
index 16c2fb5ef24..ae32d9094e3 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ConstantPropagation.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ConstantPropagation.java
@@ -27,14 +27,17 @@ import 
org.apache.doris.nereids.rules.expression.ExpressionRewriteContext;
 import org.apache.doris.nereids.rules.expression.rules.FoldConstantRule;
 import org.apache.doris.nereids.trees.expressions.Alias;
 import org.apache.doris.nereids.trees.expressions.And;
+import org.apache.doris.nereids.trees.expressions.CTEId;
 import org.apache.doris.nereids.trees.expressions.EqualPredicate;
 import org.apache.doris.nereids.trees.expressions.EqualTo;
+import org.apache.doris.nereids.trees.expressions.ExprId;
 import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.Match;
 import org.apache.doris.nereids.trees.expressions.NamedExpression;
 import org.apache.doris.nereids.trees.expressions.Or;
 import org.apache.doris.nereids.trees.expressions.Slot;
 import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.StatementScopeIdGenerator;
 import org.apache.doris.nereids.trees.expressions.literal.BooleanLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.ComparableLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.Literal;
@@ -71,6 +74,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * constant propagation, like: a = 10 and a + b > 30 => a = 10 and 10 + b > 30,
@@ -79,6 +83,17 @@ import java.util.Set;
  * finally use the combine uniforms to replace this plan's expression's slot 
with literals.
  */
 public class ConstantPropagation extends DefaultPlanRewriter<CascadesContext> 
implements CustomRewriter {
+    /**
+     * When this rule replaces a slot with a constant, the new slot describes 
another value than the replaced
+     * slot: the nullable and the data type may differ. Reusing the exprId of 
the replaced slot would make two
+     * different definitions share one exprId, and every rule which only 
compares exprIds (e.g.
+     * EliminateUnnecessaryProject) would treat them as the same column.
+     *
+     * <p>So the constant slot gets a fresh exprId, and this map records 
{@code replaced exprId -> fresh exprId}
+     * to refresh the references in the plan above.
+     */
+    private final Map<ExprId, ExprId> replacedExprIds = Maps.newHashMap();
+    private ExprIdRewriter exprIdRewriter;
 
     @Override
     public Plan rewriteRoot(Plan plan, JobContext jobContext) {
@@ -86,12 +101,33 @@ public class ConstantPropagation extends 
DefaultPlanRewriter<CascadesContext> im
         if (plan.containsType(LogicalApply.class)) {
             return plan;
         }
-        return plan.accept(this, jobContext.getCascadesContext());
+        replacedExprIds.clear();
+        CascadesContext context = jobContext.getCascadesContext();
+        // a present current tree means this plan is the body of a CTE 
producer, see RewriteCteChildren
+        Optional<CTEId> cteId = context.getCurrentTree();
+        exprIdRewriter = new ExprIdRewriter(new 
ExprIdRewriter.ReplaceRule(replacedExprIds, true), jobContext);
+        Plan rewrittenPlan = plan.accept(this, context);
+        if (cteId.isPresent() && !replacedExprIds.isEmpty()) {
+            // The consumers of this CTE map their slots to the outputs of the 
producer by exprId, and they are
+            // rewritten before the producer subtree, so publish the renamed 
exprIds for RewriteCteChildren to
+            // refresh their maps. A rule may rename more than once, so the 
mappings are accumulated.
+            context.getStatementContext().addProducerExprIdRemap(cteId.get(), 
replacedExprIds);
+        }
+        return rewrittenPlan;
+    }
+
+    @Override
+    public Plan visit(Plan plan, CascadesContext context) {
+        plan = visitChildren(this, plan, context);
+        // the children may have replaced some slots by constants with fresh 
exprIds, refresh the references of
+        // this node before rewriting it
+        return exprIdRewriter.rewriteExpr(plan, replacedExprIds);
     }
 
     @Override
     public Plan visitLogicalFilter(LogicalFilter<? extends Plan> filter, 
CascadesContext context) {
         filter = visitChildren(this, filter, context);
+        filter = (LogicalFilter<? extends Plan>) 
exprIdRewriter.rewriteExpr(filter, replacedExprIds);
         Expression oldPredicate = filter.getPredicate();
         Expression newPredicate = replaceConstantsAndRewriteExpr(filter, 
oldPredicate, context);
         if (isExprEqualIgnoreOrder(oldPredicate, newPredicate)) {
@@ -105,6 +141,7 @@ public class ConstantPropagation extends 
DefaultPlanRewriter<CascadesContext> im
     @Override
     public Plan visitLogicalHaving(LogicalHaving<? extends Plan> having, 
CascadesContext context) {
         having = visitChildren(this, having, context);
+        having = (LogicalHaving<? extends Plan>) 
exprIdRewriter.rewriteExpr(having, replacedExprIds);
         Expression oldPredicate = having.getPredicate();
         Expression newPredicate = replaceConstantsAndRewriteExpr(having, 
oldPredicate, context);
         if (isExprEqualIgnoreOrder(oldPredicate, newPredicate)) {
@@ -118,14 +155,17 @@ public class ConstantPropagation extends 
DefaultPlanRewriter<CascadesContext> im
     @Override
     public Plan visitLogicalProject(LogicalProject<? extends Plan> project, 
CascadesContext context) {
         project = visitChildren(this, project, context);
+        project = (LogicalProject<? extends Plan>) 
exprIdRewriter.rewriteExpr(project, replacedExprIds);
         ExpressionRewriteContext rewriteContext = new 
ExpressionRewriteContext(project, context);
         Pair<ImmutableEqualSet<Slot>, Map<Slot, Literal>> childEqualTrait =
                 getChildEqualSetAndConstants(project, rewriteContext);
         ImmutableList.Builder<NamedExpression> newProjectsBuilder
                 = 
ImmutableList.builderWithExpectedSize(project.getProjects().size());
+        Set<ExprId> childOutputExprIds = project.child().getOutput().stream()
+                .map(Slot::getExprId).collect(Collectors.toSet());
         for (NamedExpression expr : project.getProjects()) {
             newProjectsBuilder.add(replaceNameExpressionConstants(
-                    expr, rewriteContext, childEqualTrait.first, 
childEqualTrait.second));
+                    expr, rewriteContext, childEqualTrait.first, 
childEqualTrait.second, childOutputExprIds));
         }
 
         List<NamedExpression> newProjects = newProjectsBuilder.build();
@@ -135,6 +175,7 @@ public class ConstantPropagation extends 
DefaultPlanRewriter<CascadesContext> im
     @Override
     public Plan visitLogicalSort(LogicalSort<? extends Plan> sort, 
CascadesContext context) {
         sort = visitChildren(this, sort, context);
+        sort = (LogicalSort<? extends Plan>) exprIdRewriter.rewriteExpr(sort, 
replacedExprIds);
         ExpressionRewriteContext rewriteContext = new 
ExpressionRewriteContext(sort, context);
         Pair<ImmutableEqualSet<Slot>, Map<Slot, Literal>> childEqualTrait
                 = getChildEqualSetAndConstants(sort, rewriteContext);
@@ -163,6 +204,7 @@ public class ConstantPropagation extends 
DefaultPlanRewriter<CascadesContext> im
     @Override
     public Plan visitLogicalAggregate(LogicalAggregate<? extends Plan> 
aggregate, CascadesContext context) {
         aggregate = visitChildren(this, aggregate, context);
+        aggregate = (LogicalAggregate<? extends Plan>) 
exprIdRewriter.rewriteExpr(aggregate, replacedExprIds);
         ExpressionRewriteContext rewriteContext = new 
ExpressionRewriteContext(aggregate, context);
         Pair<ImmutableEqualSet<Slot>, Map<Slot, Literal>> childEqualTrait =
                 getChildEqualSetAndConstants(aggregate, rewriteContext);
@@ -192,7 +234,6 @@ public class ConstantPropagation extends 
DefaultPlanRewriter<CascadesContext> im
         List<NamedExpression> newOutputExprs = 
Lists.newArrayListWithExpectedSize(oldOutputExprs.size());
         ImmutableList.Builder<NamedExpression> projectBuilder
                 = ImmutableList.builderWithExpectedSize(oldOutputExprs.size());
-
         boolean containsConstantOutput = false;
 
         // after normal agg, group by expressions and output expressions are 
slots,
@@ -208,7 +249,13 @@ public class ConstantPropagation extends 
DefaultPlanRewriter<CascadesContext> im
                 newOutputExprs.add((NamedExpression) newOutputExpr);
             }
 
-            if (replacedExpr.isConstant()) {
+            if (replacedExpr.isConstant() && !replacedExpr.equals(expr)) {
+                // The constant is materialized above the aggregate, but it 
keeps the exprId of the aggregate
+                // output instead of getting a fresh one and refreshing the 
plan above: when the aggregate still
+                // outputs that column, the value is the same constant, and 
when it removed the column, this
+                // project item is the only definition of the exprId. Renaming 
the exprId would break the
+                // consumers which rely on the identity of the column, e.g. 
the materialized view matching, and
+                // would make the plan above be rewritten for nothing.
                 projectBuilder.add(new Alias(expr.getExprId(), replacedExpr, 
expr.getName()));
                 containsConstantOutput = true;
             } else {
@@ -218,6 +265,7 @@ public class ConstantPropagation extends 
DefaultPlanRewriter<CascadesContext> im
         }
 
         if (newGroupByExprs.equals(oldGroupByExprs) && 
newOutputExprs.equals(oldOutputExprs)) {
+            // the aggregate is not changed, the plan above keeps referencing 
the output slots of the aggregate
             return aggregate;
         }
 
@@ -238,6 +286,7 @@ public class ConstantPropagation extends 
DefaultPlanRewriter<CascadesContext> im
         // update: BE not support nested loop mark join, so mark join 
condition need to be an equation,
         // but constant propagation may rewrite an equal to TRUE/FALSE/NULL, 
so we don't rewrite mark join condition.
         join = visitChildren(this, join, context);
+        join = (LogicalJoin<? extends Plan, ? extends Plan>) 
exprIdRewriter.rewriteExpr(join, replacedExprIds);
         if (join.getJoinType().isAsofJoin()) {
             return join;
         }
@@ -293,7 +342,7 @@ public class ConstantPropagation extends 
DefaultPlanRewriter<CascadesContext> im
         // // the select will add a parent plan: result sink. the result sink 
contains a output slot reference, and its
         // // data type is varchar(30),  but if replace the slot reference 
with a varchar literal '1',
         // // then the data type info varchar(30) will lost, because varchar 
literal '1' data type is always varchar(1),
-        // // so t's column will get a error type. so we don't rewrite logical 
sink then.
+        // // so t's column will get a error type. so we don't replace the 
slots of the sink by literals.
         // Pair<ImmutableEqualSet<Slot>, Map<Slot, Literal>> childEqualTrait
         //         = getChildEqualSetAndConstants(sink, context);
         // List<NamedExpression> newOutputExprs = 
sink.getOutputExprs().stream()
@@ -301,7 +350,10 @@ public class ConstantPropagation extends 
DefaultPlanRewriter<CascadesContext> im
         //                 replaceNameExpressionConstants(expr, context, 
childEqualTrait.first, childEqualTrait.second))
         //         .collect(ImmutableList.toImmutableList());
         // return newOutputExprs.equals(sink.getOutputExprs()) ? sink : 
sink.withOutputExprs(newOutputExprs);
-        return sink;
+        // The sink still has to follow the fresh exprIds of the constants 
materialized below it, otherwise it
+        // would reference a slot which its child does not output any more. 
Only the exprId is remapped here, so
+        // the name and the data type declared by the sink are kept.
+        return (LogicalSink<? extends Plan>) exprIdRewriter.rewriteExpr(sink, 
replacedExprIds);
     }
 
     /**
@@ -348,8 +400,7 @@ public class ConstantPropagation extends 
DefaultPlanRewriter<CascadesContext> im
 
     // process NameExpression
     private NamedExpression replaceNameExpressionConstants(NamedExpression 
expr, ExpressionRewriteContext context,
-            ImmutableEqualSet<Slot> equalSet, Map<Slot, Literal> constants) {
-
+            ImmutableEqualSet<Slot> equalSet, Map<Slot, Literal> constants, 
Set<ExprId> childOutputExprIds) {
         // if a project item is a slot reference, and the slot equals to a 
constant value, don't rewrite it.
         // because rule `EliminateUnnecessaryProject ` can eliminate a project 
when the project's output slots equal to
         // its child's output slots.
@@ -369,12 +420,21 @@ public class ConstantPropagation extends 
DefaultPlanRewriter<CascadesContext> im
             return expr;
         }
 
+        // the item is an Alias which is not a plain slot and not a cast of 
one: replacing the constants of an
+        // Alias always yields an Alias again, so only its expression may 
change
         Expression newExpr = replaceConstants(expr, false, context, equalSet, 
constants);
-        if (newExpr instanceof NamedExpression) {
+        if (!(newExpr instanceof Alias) || newExpr.equals(expr)
+                || !childOutputExprIds.contains(expr.getExprId())) {
+            // unchanged, or the child does not produce the column any more, 
then this project item is the only
+            // definition of the exprId and it is kept
             return (NamedExpression) newExpr;
-        } else {
-            return new Alias(expr.getExprId(), newExpr, expr.getName());
         }
+        // the child still produces this column, so the project item would 
shadow it: materialize the constant with
+        // a fresh exprId and refresh the references above, see replacedExprIds
+        Alias replacedAlias = (Alias) newExpr;
+        ExprId freshExprId = StatementScopeIdGenerator.newExprId();
+        replacedExprIds.put(expr.getExprId(), freshExprId);
+        return new Alias(freshExprId, replacedAlias.child(0), 
replacedAlias.getName());
     }
 
     private Expression replaceConstants(Expression expression, boolean 
useInnerInfer, ExpressionRewriteContext context,
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 ddeb1d7d6eb..f1480ae245a 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
@@ -20,18 +20,18 @@ package org.apache.doris.nereids.rules.rewrite;
 import org.apache.doris.nereids.jobs.JobContext;
 import org.apache.doris.nereids.pattern.Pattern;
 import org.apache.doris.nereids.rules.Rule;
-import org.apache.doris.nereids.rules.expression.ExpressionPatternMatcher;
-import org.apache.doris.nereids.rules.expression.ExpressionPatternRuleFactory;
 import org.apache.doris.nereids.rules.expression.ExpressionRewrite;
+import org.apache.doris.nereids.rules.expression.ExpressionRewriteContext;
+import org.apache.doris.nereids.rules.expression.ExpressionRewriteRule;
 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;
+import org.apache.doris.nereids.trees.expressions.NamedExpression;
 import org.apache.doris.nereids.trees.expressions.SlotReference;
 import 
org.apache.doris.nereids.trees.expressions.visitor.DefaultExpressionRewriter;
 import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalSink;
 
 import com.google.common.collect.ImmutableList;
 
@@ -44,11 +44,43 @@ public class ExprIdRewriter extends ExpressionRewrite {
     private final JobContext jobContext;
 
     public ExprIdRewriter(ReplaceRule replaceRule, JobContext jobContext) {
-        super(new 
ExpressionRuleExecutor(ImmutableList.of(bottomUp(replaceRule))));
+        super(new ExpressionRuleExecutor(ImmutableList.of(new 
ReplaceExprIds(replaceRule))));
         rules = buildRules();
         this.jobContext = jobContext;
     }
 
+    /** Replace the exprIds of one expression structurally, see 
ReplaceRule#replaceExprIds. */
+    private static class ReplaceExprIds implements 
ExpressionRewriteRule<ExpressionRewriteContext> {
+        private final ReplaceRule replaceRule;
+
+        ReplaceExprIds(ReplaceRule replaceRule) {
+            this.replaceRule = replaceRule;
+        }
+
+        @Override
+        public Expression rewrite(Expression expression, 
ExpressionRewriteContext context) {
+            return replaceRule.replaceExprIds(expression);
+        }
+    }
+
+    /**
+     * Follow the replacement chain of an exprId to its end: an exprId may be 
renamed more than once, e.g. by
+     * several rules or by several passes of the same rule, and only the last 
exprId exists in the plan then.
+     */
+    public static ExprId lastExprId(Map<ExprId, ExprId> replaceMap, ExprId 
exprId) {
+        ExprId lastExprId = replaceMap.get(exprId);
+        if (lastExprId == null) {
+            return exprId;
+        }
+        while (true) {
+            ExprId nextExprId = replaceMap.get(lastExprId);
+            if (nextExprId == null) {
+                return lastExprId;
+            }
+            lastExprId = nextExprId;
+        }
+    }
+
     /**rewriteExpr*/
     public Plan rewriteExpr(Plan plan, Map<ExprId, ExprId> replaceMap) {
         if (replaceMap.isEmpty()) {
@@ -64,6 +96,25 @@ public class ExprIdRewriter extends ExpressionRewrite {
                 }
             }
         }
+        // ExpressionRewrite has a rule for the common sink types only, e.g. 
there is none for the blackhole sink. A
+        // sink takes its output expressions from its child, so it must follow 
the fresh exprIds of the child as
+        // well, otherwise it would keep expecting a slot which the rewritten 
child does not output any more. So
+        // rewrite the output expressions of any sink type here.
+        if (plan instanceof LogicalSink) {
+            LogicalSink<? extends Plan> sink = (LogicalSink<? extends Plan>) 
plan;
+            ExpressionRewriteContext context = new 
ExpressionRewriteContext(sink, jobContext.getCascadesContext());
+            ImmutableList.Builder<NamedExpression> newOutputExprs
+                    = 
ImmutableList.builderWithExpectedSize(sink.getOutputExprs().size());
+            boolean changed = false;
+            for (NamedExpression outputExpr : sink.getOutputExprs()) {
+                NamedExpression newOutputExpr = (NamedExpression) 
rewriter.rewrite(outputExpr, context);
+                changed |= !newOutputExpr.equals(outputExpr);
+                newOutputExprs.add(newOutputExpr);
+            }
+            if (changed) {
+                return sink.withOutputExprs(newOutputExprs.build());
+            }
+        }
         return plan;
     }
 
@@ -76,63 +127,37 @@ public class ExprIdRewriter extends ExpressionRewrite {
      * e.g. replaceMap:{0:3, 1:6, 6:7}
      * SlotReference:a#0 -> a#3, a#1 -> a#7
      * */
-    public static class ReplaceRule implements ExpressionPatternRuleFactory {
+    public static class ReplaceRule {
         private static final DefaultExpressionRewriter<Map<ExprId, ExprId>> 
SLOT_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) {
+                        if (replaceMap.get(slot.getExprId()) == null) {
                             return slot;
                         }
-                        ExprId lastId = newId;
-                        while (true) {
-                            newId = replaceMap.get(lastId);
-                            if (newId == null) {
-                                return slot.withExprId(lastId);
-                            } else {
-                                lastId = newId;
-                            }
-                        }
+                        return slot.withExprId(lastExprId(replaceMap, 
slot.getExprId()));
                     }
                 };
         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) {
+                        if (replaceMap.get(slot.getExprId()) == null) {
                             return slot;
                         }
-                        ExprId lastId = newId;
-                        while (true) {
-                            newId = replaceMap.get(lastId);
-                            if (newId == null) {
-                                return slot.withExprId(lastId);
-                            } else {
-                                lastId = newId;
-                            }
-                        }
+                        return slot.withExprId(lastExprId(replaceMap, 
slot.getExprId()));
                     }
 
                     @Override
                     public Expression visitAlias(Alias alias, Map<ExprId, 
ExprId> replaceMap) {
-                        ExprId newId = replaceMap.get(alias.getExprId());
-                        if (newId == null) {
+                        if (replaceMap.get(alias.getExprId()) == null) {
                             return alias;
                         }
-                        ExprId lastId = newId;
-                        while (true) {
-                            newId = replaceMap.get(lastId);
-                            if (newId == null) {
-                                return alias.withExprId(lastId);
-                            } else {
-                                lastId = newId;
-                            }
-                        }
+                        return alias.withExprId(lastExprId(replaceMap, 
alias.getExprId()));
                     }
                 };
         private final Map<ExprId, ExprId> replaceMap;
+        private final DefaultExpressionRewriter<Map<ExprId, ExprId>> 
structuralReplacer;
         private DefaultExpressionRewriter<Map<ExprId, ExprId>> replacer;
 
         /**
@@ -145,24 +170,36 @@ public class ExprIdRewriter extends ExpressionRewrite {
             } else {
                 replacer = SLOT_REPLACER;
             }
+            // children first: the id replacement of a node must not stop the 
traversal, e.g. an Alias keeps its own
+            // exprId while a slot of the expression below it is renamed. The 
replacer is applied to the node itself
+            // only, its children are already rewritten here, so the whole 
expression is visited once.
+            this.structuralReplacer = new 
DefaultExpressionRewriter<Map<ExprId, ExprId>>() {
+                @Override
+                public Expression visit(Expression expr, Map<ExprId, ExprId> 
replaceMap) {
+                    Expression withRewrittenChildren = rewriteChildren(this, 
expr, replaceMap);
+                    if (withRewrittenChildren instanceof SlotReference
+                            || (rewriteAlias && withRewrittenChildren 
instanceof Alias)) {
+                        return withRewrittenChildren.accept(replacer, 
replaceMap);
+                    }
+                    return withRewrittenChildren;
+                }
+            };
         }
 
         public ReplaceRule(Map<ExprId, ExprId> replaceMap) {
             this(replaceMap, false);
         }
 
-        @Override
-        public List<ExpressionPatternMatcher<? extends Expression>> 
buildRules() {
-            return ImmutableList.of(
-                    matchesType(SlotReference.class).thenApply(ctx -> {
-                        Slot slot = ctx.expr;
-                        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)
-            );
+        /**
+         * Replace the exprIds of the slots (and of the aliases, when this 
rule was created for them) in one
+         * expression. Unlike the bottom up expression rewriter this enters 
every branch, including the branches
+         * which require short circuit evaluation, e.g. the assignments of 
MERGE INTO: replacing an exprId only
+         * rebuilds the expression and never evaluates it, and a reference 
which keeps an old exprId would point to
+         * a slot which the rewritten child does not output any more.
+         */
+        public Expression replaceExprIds(Expression expression) {
+            return expression.accept(structuralReplacer, replaceMap);
         }
+
     }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/RewriteCteChildren.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/RewriteCteChildren.java
index c957a9e853f..fe68decaa5d 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/RewriteCteChildren.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/RewriteCteChildren.java
@@ -24,6 +24,7 @@ import org.apache.doris.nereids.jobs.executor.Rewriter;
 import org.apache.doris.nereids.jobs.rewrite.RewriteJob;
 import org.apache.doris.nereids.properties.PhysicalProperties;
 import org.apache.doris.nereids.trees.expressions.CTEId;
+import org.apache.doris.nereids.trees.expressions.ExprId;
 import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.NamedExpression;
 import org.apache.doris.nereids.trees.expressions.Slot;
@@ -44,10 +45,15 @@ import org.apache.doris.nereids.util.ExpressionUtils;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Multimap;
 import com.google.common.collect.Sets;
 
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Optional;
 import java.util.Set;
@@ -99,22 +105,93 @@ public class RewriteCteChildren extends 
DefaultPlanRewriter<CascadesContext> imp
             
cascadesContext.addPlanProcesses(outerCascadesCtx.getPlanProcesses());
             
cascadesContext.getStatementContext().getRewrittenCteConsumer().put(cteAnchor.getCteId(),
 outer);
         }
+        Set<LogicalCTEConsumer> cteConsumers = collectCteConsumers(outer, 
cteAnchor.getCteId());
+        cascadesContext.getCteIdToConsumers().put(cteAnchor.getCteId(), 
cteConsumers);
+        if (cteConsumers.isEmpty()) {
+            return outer;
+        }
+        Plan producer = cteAnchor.child(0).accept(this, cascadesContext);
+        // ConstantPropagation is currently the only rule which renames the 
exprIds of a producer output, and it
+        // publishes what it renamed (see 
StatementContext#addProducerExprIdRemap); a rule which renames an output
+        // of a producer body has to do the same, otherwise the consumers, 
which are rewritten before the producer,
+        // keep mapping to an exprId which the producer does not output any 
more. A rule may rename more than once,
+        // so the chain is followed to its end.
+        Map<ExprId, ExprId> producerExprIdRemap = 
cascadesContext.getStatementContext()
+                .getProducerExprIdRemap(cteAnchor.getCteId());
+        if (!producerExprIdRemap.isEmpty()) {
+            Map<ExprId, Slot> producerOutputSlots = new HashMap<>();
+            for (Slot producerOutputSlot : producer.getOutput()) {
+                // a CTE may expose the same slot more than once, e.g. `SELECT 
k, k FROM t`: both positions are the
+                // same producer slot, so the first one is enough for the 
lookup
+                
producerOutputSlots.putIfAbsent(producerOutputSlot.getExprId(), 
producerOutputSlot);
+            }
+            outer = (LogicalPlan) outer.<Plan, Void>accept(new 
CteConsumerOutputsRefresher(cteAnchor.getCteId(),
+                    producerExprIdRemap, producerOutputSlots), null);
+            
cascadesContext.getStatementContext().getRewrittenCteConsumer().put(cteAnchor.getCteId(),
 outer);
+            cascadesContext.getCteIdToConsumers()
+                    .put(cteAnchor.getCteId(), collectCteConsumers(outer, 
cteAnchor.getCteId()));
+        }
+        return cteAnchor.withChildren(producer, outer);
+    }
+
+    private static Set<LogicalCTEConsumer> collectCteConsumers(Plan outer, 
CTEId cteId) {
         Set<LogicalCTEConsumer> cteConsumers = Sets.newHashSet();
         outer.foreach(p -> {
             if (p instanceof LogicalCTEConsumer) {
                 LogicalCTEConsumer logicalCTEConsumer = (LogicalCTEConsumer) p;
-                if 
(logicalCTEConsumer.getCteId().equals(cteAnchor.getCteId())) {
+                if (logicalCTEConsumer.getCteId().equals(cteId)) {
                     cteConsumers.add(logicalCTEConsumer);
                 }
             }
             return false;
         });
-        cascadesContext.getCteIdToConsumers().put(cteAnchor.getCteId(), 
cteConsumers);
-        if (cteConsumers.isEmpty()) {
-            return outer;
+        return cteConsumers;
+    }
+
+    /**
+     * Refresh the producer slots of the two output maps of the consumers of a 
CTE with the exprIds the rules of the
+     * producer body published, see StatementContext#getProducerExprIdRemap.
+     */
+    static class CteConsumerOutputsRefresher extends DefaultPlanRewriter<Void> 
{
+        private final CTEId cteId;
+        private final Map<ExprId, ExprId> producerExprIdRemap;
+        private final Map<ExprId, Slot> producerOutputSlots;
+
+        CteConsumerOutputsRefresher(CTEId cteId, Map<ExprId, ExprId> 
producerExprIdRemap,
+                Map<ExprId, Slot> producerOutputSlots) {
+            this.cteId = cteId;
+            this.producerExprIdRemap = producerExprIdRemap;
+            this.producerOutputSlots = producerOutputSlots;
+        }
+
+        @Override
+        public Plan visitLogicalCTEConsumer(LogicalCTEConsumer cteConsumer, 
Void context) {
+            if (!cteConsumer.getCteId().equals(cteId)) {
+                return cteConsumer;
+            }
+            Map<Slot, Slot> consumerToProducerOutputMap = new 
LinkedHashMap<>();
+            Multimap<Slot, Slot> producerToConsumerOutputMap = 
LinkedHashMultimap.create();
+            for (Map.Entry<Slot, Slot> entry : 
cteConsumer.getConsumerToProducerOutputMap().entrySet()) {
+                Slot newProducerSlot = refreshProducerSlot(entry.getValue());
+                consumerToProducerOutputMap.put(entry.getKey(), 
newProducerSlot);
+                for (Slot consumerSlot : 
cteConsumer.getProducerToConsumerOutputMap().get(entry.getValue())) {
+                    producerToConsumerOutputMap.put(newProducerSlot, 
consumerSlot);
+                }
+            }
+            return cteConsumer.withTwoMaps(consumerToProducerOutputMap, 
producerToConsumerOutputMap);
+        }
+
+        private Slot refreshProducerSlot(Slot producerSlot) {
+            ExprId lastExprId = ExprIdRewriter.lastExprId(producerExprIdRemap, 
producerSlot.getExprId());
+            if (lastExprId.equals(producerSlot.getExprId())) {
+                return producerSlot;
+            }
+            // the column may be pruned by the rewrite of the producer body 
after it was renamed, e.g. by
+            // ColumnPruning when the plan above the consumer stopped reading 
it; such a column has no counterpart
+            // in the producer output, so its entry stays as it is
+            Slot newProducerSlot = producerOutputSlots.get(lastExprId);
+            return newProducerSlot != null ? newProducerSlot : producerSlot;
         }
-        Plan producer = cteAnchor.child(0).accept(this, cascadesContext);
-        return cteAnchor.withChildren(producer, outer);
     }
 
     @Override
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ConstantPropagationFreshExprIdTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ConstantPropagationFreshExprIdTest.java
new file mode 100644
index 00000000000..34985c7255e
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ConstantPropagationFreshExprIdTest.java
@@ -0,0 +1,280 @@
+// 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.NamedExpression;
+import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalAggregate;
+import org.apache.doris.nereids.trees.plans.logical.LogicalCTEConsumer;
+import org.apache.doris.nereids.trees.plans.logical.LogicalCTEProducer;
+import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
+import org.apache.doris.nereids.util.PlanChecker;
+import org.apache.doris.utframe.TestWithFeService;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * The project which ConstantPropagation materializes above an aggregate must 
not re-label a column which the plan
+ * below defines with another value.
+ *
+ * <p>A constant grouping key which the aggregate keeps is passed through as 
the slot of the aggregate, and a
+ * constant grouping key which the aggregate removes keeps the exprId it 
already had, so nothing below defines that
+ * exprId any more. The project item used to reuse the exprId of the removed 
aggregate output while other rules
+ * later brought a subtree which outputs that exprId back below it, and rules 
which compare exprIds only, e.g.
+ * EliminateUnnecessaryProject, then removed the constant project. RQG case 
11000258898 reached such a plan and the
+ * last AdjustNullable pass reported "AdjustNullable convert slot 
2004-04-28#xx from not-nullable to nullable".
+ */
+public class ConstantPropagationFreshExprIdTest extends TestWithFeService {
+    private static final String SQL_11000258898 = "select '2004-04-28', 
'2014-07-07'"
+            + " intersect distinct"
+            + " select col_datetime_3__undef_signed_not_null, 
col_date_undef_signed_not_null"
+            + " from 
table_100_undef_partitions2_keys3_properties4_distributed_by52"
+            + " where col_decimal_12_2__undef_signed = 94.0212"
+            + " union distinct"
+            + " select col_datetime_3__undef_signed, 
col_datetime_3__undef_signed_not_null"
+            + " from 
table_100_undef_partitions2_keys3_properties4_distributed_by54"
+            + " where col_datetime_3__undef_signed = '2011-11-15 13:33:05'"
+            + " intersect"
+            + " select col_date_undef_signed_not_null, 
col_date_undef_signed_not_null"
+            + " from 
table_100_undef_partitions2_keys3_properties4_distributed_by53"
+            + " intersect"
+            + " select col_datetime_3__undef_signed_not_null, 
col_date_undef_signed_not_null"
+            + " from 
table_100_undef_partitions2_keys3_properties4_distributed_by54 ;";
+
+    private static final String SQL_11000259585 = "select * from ((select 
'vZRgkfajsD', 'cKToNmsxok' except (select col_char_25__undef_signed_not_null, 
col_char_40__undef_signed from 
table_100_undef_partitions2_keys3_properties4_distributed_by54 where 
col_date_undef_signed = '2016-09-20' order by 1, 2 desc limit 1, 8) union 
distinct select col_varchar_25__undef_signed, col_char_25__undef_signed from 
table_100_undef_partitions2_keys3_properties4_distributed_by53 where 
col_tinyint_undef_si [...]
+
+    private static final String SQL_MATERIALIZED_CTE = "WITH c AS (SELECT k, 
v, count(*) AS cnt"
+            + " FROM constant_propagation_fresh_expr_id_t WHERE k = 5 AND v = 
7 GROUP BY k, v)"
+            + " SELECT l.k, l.v, l.cnt, r.k, r.v, r.cnt FROM c l CROSS JOIN c 
r";
+
+    private static final String SQL_MATERIALIZED_CTE_CONSTANT_COLUMN
+            = "WITH c AS (SELECT v, k + 0 AS x, count(*) AS cnt"
+            + " FROM constant_propagation_fresh_expr_id_t WHERE k = 5 GROUP BY 
v, k)"
+            + " SELECT l.v, l.x, l.cnt, r.v, r.x, r.cnt FROM c l CROSS JOIN c 
r";
+
+    private static final String SQL_MATERIALIZED_CTE_REPEATED_SLOT
+            = "WITH c AS (SELECT k, v, v, count(*) AS cnt"
+            + " FROM constant_propagation_fresh_expr_id_t WHERE k = 5 AND v = 
7 GROUP BY k, v)"
+            + " SELECT l.*, r.* FROM c l CROSS JOIN c r";
+
+    @Override
+    protected void runBeforeAll() throws Exception {
+        createDatabase("test");
+        createTable("create table test.constant_propagation_fresh_expr_id_t(k 
int null, v int null)"
+                + " distributed by hash(k) properties('replication_num' = 
'1');");
+        createTable("create table 
test.table_100_undef_partitions2_keys3_properties4_distributed_by52 ("
+                + " col_datetime_3__undef_signed_not_null datetime(3) not 
null,"
+                + " col_datetime_3__undef_signed datetime(3) null,"
+                + " col_datetime_undef_signed datetime null,"
+                + " col_datetime_undef_signed_not_null datetime not null,"
+                + " col_date_undef_signed_not_null date not null,"
+                + " col_date_undef_signed date null,"
+                + " col_decimal_12_2__undef_signed decimal(12,2) null,"
+                + " col_decimal_32_6__undef_signed decimal(32,6) null)"
+                + " distributed by hash(col_datetime_3__undef_signed) 
properties('replication_num' = '1');");
+        createTable("create table 
test.table_100_undef_partitions2_keys3_properties4_distributed_by53 ("
+                + " col_date_undef_signed_not_null date not null,"
+                + " col_date_undef_signed date null,"
+                + " col_datetime_3__undef_signed datetime(3) null,"
+                + " col_datetime_3__undef_signed_not_null datetime(3) not 
null,"
+                + " col_datetime_undef_signed datetime null,"
+                + " col_datetime_undef_signed_not_null datetime not null,"
+                + " col_varchar_25__undef_signed varchar(25) null,"
+                + " col_char_25__undef_signed_not_null char(25) not null,"
+                + " col_char_25__undef_signed char(25) null,"
+                + " col_char_40__undef_signed char(40) null,"
+                + " col_varchar_100__undef_signed varchar(100) null,"
+                + " col_tinyint_undef_signed_not_null tinyint not null,"
+                + " col_bigint_undef_signed_not_null bigint not null)"
+                + " distributed by hash(col_date_undef_signed_not_null) 
properties('replication_num' = '1');");
+        createTable("create table 
test.table_100_undef_partitions2_keys3_properties4_distributed_by54 ("
+                + " col_datetime_3__undef_signed datetime(3) null,"
+                + " col_datetime_3__undef_signed_not_null datetime(3) not 
null,"
+                + " col_datetime_undef_signed datetime null,"
+                + " col_datetime_undef_signed_not_null datetime not null,"
+                + " col_date_undef_signed_not_null date not null,"
+                + " col_date_undef_signed date null,"
+                + " col_char_25__undef_signed_not_null char(25) not null,"
+                + " col_char_40__undef_signed char(40) null)"
+                + " distributed by hash(col_datetime_3__undef_signed) 
properties('replication_num' = '1');");
+        createTable("create table 
test.table_100_undef_partitions2_keys3_properties4_distributed_by5 ("
+                + " col_date_undef_signed_not_null date not null,"
+                + " col_datetime_undef_signed datetime null,"
+                + " col_datetime_undef_signed_not_null datetime not null,"
+                + " col_datetime_3__undef_signed datetime(3) null,"
+                + " col_datetime_3__undef_signed_not_null datetime(3) not 
null,"
+                + " col_char_25__undef_signed char(25) null,"
+                + " col_char_25__undef_signed_not_null char(25) not null,"
+                + " col_varchar_100__undef_signed_not_null varchar(100) not 
null,"
+                + " col_varchar_100__undef_signed varchar(100) null)"
+                + " distributed by hash(col_date_undef_signed_not_null) 
properties('replication_num' = '1');");
+        connectContext.setDatabase("test");
+        
connectContext.getSessionVariable().setDisableNereidsRules("PRUNE_EMPTY_PARTITION");
+    }
+
+    @Test
+    void testConstantAggregateProjectKeepsTheExprId() {
+        // run this rule alone, so the project it materializes above the 
aggregate is still part of the plan
+        PlanChecker checker = PlanChecker.from(connectContext)
+                .analyze("select k, v from 
constant_propagation_fresh_expr_id_t where k = 5 and v = 7 group by k, v")
+                .applyCustom(new ConstantPropagation());
+        Plan plan = checker.getPlan();
+        LogicalProject<?> constantProject = findConstantProject(plan);
+        Assertions.assertNotNull(constantProject, plan.treeString());
+        List<NamedExpression> items = constantProject.getProjects();
+        // the kept grouping key is still materialized as a constant above the 
aggregate, with the exprId of the
+        // aggregate output: no fresh exprId and no refresh of the plan above
+        NamedExpression keptKey = items.get(0);
+        Assertions.assertTrue(keptKey instanceof Alias, plan.treeString());
+        Assertions.assertEquals(new IntegerLiteral(5), ((Alias) 
keptKey).child());
+        Assertions.assertTrue(constantProject.child().getOutput().stream()
+                        .anyMatch(slot -> 
slot.getExprId().equals(keptKey.getExprId())),
+                "the kept grouping key keeps the exprId of the aggregate 
output:\n" + plan.treeString());
+        // the removed grouping key is materialized with the exprId it already 
had as well
+        NamedExpression removedKey = items.get(1);
+        Assertions.assertTrue(removedKey instanceof Alias, plan.treeString());
+        Assertions.assertEquals(new IntegerLiteral(7), ((Alias) 
removedKey).child());
+        // the plan above references the outputs of the project, unchanged
+        Set<ExprId> projectOutputIds = constantProject.getOutput().stream()
+                .map(Slot::getExprId).collect(Collectors.toSet());
+        Set<ExprId> rootOutputIds = plan.getOutput().stream()
+                .map(Slot::getExprId).collect(Collectors.toSet());
+        Assertions.assertTrue(rootOutputIds.containsAll(projectOutputIds),
+                "the plan above does not reference the project outputs:\n" + 
plan.treeString());
+    }
+
+    // The two RQG reproductions of the master version (testRqgCase11000258898 
and testRqgCase11000259585) are
+    // not ported: on branch-4.2 both queries already trip "AdjustNullable 
convert slot 2004-04-28#55 from
+    // not-nullable to nullable" (and "cKToNmsxok#45") during the rewrite on 
the pristine branch, i.e. before this
+    // change, and the failure is unchanged after it. ConstantPropagation does 
not modify the plans of these
+    // queries on this branch at all (dumped with the rule applied alone: the 
plan is identical), so the failure
+    // comes from another rule here, while on master the rule does rewrite 
them. The tables those queries need
+    // are still created below, so the SQL constants stay usable if the branch 
is fixed later.
+
+    @Test
+    void testMaterializedCteConsumerMapsStayValid() throws Exception {
+        // the producer body and the consumers are rewritten in different 
subtree contexts and the consumers are
+        // rewritten first, so the fresh exprIds of the producer outputs must 
not cross the CTE boundary
+        Set<ExprId> analyzedProducerOutputIds = cteProducerOutputIds(
+                
PlanChecker.from(connectContext).analyze(SQL_MATERIALIZED_CTE).getPlan());
+        Plan plan = 
PlanChecker.from(connectContext).analyze(SQL_MATERIALIZED_CTE).rewrite().getPlan();
+        assertCteConsumerMapsStayValid(plan);
+        // constant propagation keeps the exprIds of the producer outputs, so 
they are the same as in the analyzed
+        // plan and the consumer maps still resolve; a rule which renames them 
has to refresh the maps, which the
+        // assertCteConsumerMapsStayValid above checks
+        Set<ExprId> rewrittenProducerOutputIds = cteProducerOutputIds(plan);
+        Assertions.assertEquals(analyzedProducerOutputIds.size(), 
rewrittenProducerOutputIds.size(),
+                plan.treeString());
+        Assertions.assertEquals(analyzedProducerOutputIds, 
rewrittenProducerOutputIds, plan.treeString());
+        // the query has to stay plannable
+        
PlanChecker.from(connectContext).checkPlannerResult(SQL_MATERIALIZED_CTE);
+    }
+
+    @Test
+    void testMaterializedCteWithRepeatedOutputSlot() {
+        // a CTE may expose the same slot more than once (LogicalCTEConsumer 
is multimap based), and the lookup of
+        // the producer slots must tolerate that instead of failing on the 
duplicated exprId
+        Set<ExprId> analyzedProducerOutputIds = cteProducerOutputIds(
+                
PlanChecker.from(connectContext).analyze(SQL_MATERIALIZED_CTE_REPEATED_SLOT).getPlan());
+        Plan plan = PlanChecker.from(connectContext)
+                
.analyze(SQL_MATERIALIZED_CTE_REPEATED_SLOT).rewrite().getPlan();
+        assertCteConsumerMapsStayValid(plan);
+        Assertions.assertEquals(analyzedProducerOutputIds.size(), 
cteProducerOutputIds(plan).size(), plan.treeString());
+        
PlanChecker.from(connectContext).checkPlannerResult(SQL_MATERIALIZED_CTE_REPEATED_SLOT);
+    }
+
+    private static Set<ExprId> cteProducerOutputIds(Plan plan) {
+        Set<ExprId> producerOutputExprIds = new java.util.HashSet<>();
+        plan.foreach(node -> {
+            if (node instanceof LogicalCTEProducer) {
+                ((LogicalCTEProducer<?>) node).getOutput().forEach(slot -> 
producerOutputExprIds.add(slot.getExprId()));
+            }
+            return false;
+        });
+        Assertions.assertFalse(producerOutputExprIds.isEmpty(), 
plan.treeString());
+        return producerOutputExprIds;
+    }
+
+    /** Every producer slot a CTE consumer maps to has to be an output slot of 
the producer. */
+    private static void assertCteConsumerMapsStayValid(Plan plan) {
+        Set<ExprId> producerOutputExprIds = new java.util.HashSet<>();
+        Set<LogicalCTEConsumer> consumers = new java.util.HashSet<>();
+        plan.foreach(node -> {
+            if (node instanceof LogicalCTEProducer) {
+                ((LogicalCTEProducer<?>) node).getOutput().forEach(slot -> 
producerOutputExprIds.add(slot.getExprId()));
+            } else if (node instanceof LogicalCTEConsumer) {
+                consumers.add((LogicalCTEConsumer) node);
+            }
+            return false;
+        });
+        Assertions.assertFalse(consumers.isEmpty(), plan.treeString());
+        for (LogicalCTEConsumer consumer : consumers) {
+            for (Slot producerSlot : 
consumer.getProducerToConsumerOutputMap().keySet()) {
+                
Assertions.assertTrue(producerOutputExprIds.contains(producerSlot.getExprId()),
+                        "the consumer map of " + consumer.getRelationId() + " 
references " + producerSlot
+                                + " which the producer does not output: " + 
plan.treeString());
+            }
+            for (Slot producerSlot : 
consumer.getConsumerToProducerOutputMap().values()) {
+                
Assertions.assertTrue(producerOutputExprIds.contains(producerSlot.getExprId()),
+                        "the consumer map of " + consumer.getRelationId() + " 
references " + producerSlot
+                                + " which the producer does not output: " + 
plan.treeString());
+            }
+        }
+    }
+
+    @Test
+    void testMaterializedCteKeepsConstantProducerOutput() {
+        // `k + 0` is uniform (k = 5), so it folds to a constant, and it is an 
output column of the CTE producer:
+        // whether the rule materializes the constant or leaves the column 
alone, the consumers have to keep
+        // mapping to an output which the producer really exposes
+        Set<ExprId> analyzedProducerOutputIds = cteProducerOutputIds(
+                
PlanChecker.from(connectContext).analyze(SQL_MATERIALIZED_CTE_CONSTANT_COLUMN).getPlan());
+        Plan plan = PlanChecker.from(connectContext)
+                
.analyze(SQL_MATERIALIZED_CTE_CONSTANT_COLUMN).rewrite().getPlan();
+        assertCteConsumerMapsStayValid(plan);
+        // the producer keeps its three output columns, whether they are 
rewritten in place or renamed
+        Assertions.assertEquals(analyzedProducerOutputIds.size(), 
cteProducerOutputIds(plan).size(), plan.treeString());
+        
PlanChecker.from(connectContext).checkPlannerResult(SQL_MATERIALIZED_CTE_CONSTANT_COLUMN);
+    }
+
+    private static LogicalProject<?> findConstantProject(Plan plan) {
+        if (plan instanceof LogicalProject
+                && plan.child(0) instanceof LogicalAggregate
+                && ((LogicalProject<?>) 
plan).getProjects().stream().anyMatch(item -> !(item instanceof 
SlotReference))) {
+            return (LogicalProject<?>) plan;
+        }
+        for (Plan child : plan.children()) {
+            LogicalProject<?> found = findConstantProject(child);
+            if (found != null) {
+                return found;
+            }
+        }
+        return null;
+    }
+
+}
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ConstantPropagationTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ConstantPropagationTest.java
index f5acd25df12..3d8ebcb4896 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ConstantPropagationTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ConstantPropagationTest.java
@@ -27,12 +27,14 @@ import org.apache.doris.nereids.trees.expressions.Add;
 import org.apache.doris.nereids.trees.expressions.Alias;
 import org.apache.doris.nereids.trees.expressions.Cast;
 import org.apache.doris.nereids.trees.expressions.EqualTo;
+import org.apache.doris.nereids.trees.expressions.ExprId;
 import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.GreaterThan;
 import org.apache.doris.nereids.trees.expressions.IsNull;
 import org.apache.doris.nereids.trees.expressions.MatchAny;
 import org.apache.doris.nereids.trees.expressions.NamedExpression;
 import org.apache.doris.nereids.trees.expressions.Not;
+import org.apache.doris.nereids.trees.expressions.Slot;
 import org.apache.doris.nereids.trees.expressions.SlotReference;
 import org.apache.doris.nereids.trees.expressions.functions.agg.Sum;
 import org.apache.doris.nereids.trees.expressions.literal.BigIntLiteral;
@@ -43,12 +45,14 @@ import 
org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
 import org.apache.doris.nereids.trees.plans.JoinType;
 import org.apache.doris.nereids.trees.plans.RelationId;
 import org.apache.doris.nereids.trees.plans.logical.LogicalAggregate;
+import org.apache.doris.nereids.trees.plans.logical.LogicalBlackholeSink;
 import org.apache.doris.nereids.trees.plans.logical.LogicalFilter;
 import org.apache.doris.nereids.trees.plans.logical.LogicalHaving;
 import org.apache.doris.nereids.trees.plans.logical.LogicalJoin;
 import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
 import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
+import org.apache.doris.nereids.trees.plans.logical.LogicalSink;
 import org.apache.doris.nereids.trees.plans.logical.LogicalSort;
 import org.apache.doris.nereids.types.BigIntType;
 import org.apache.doris.nereids.types.IntegerType;
@@ -67,6 +71,7 @@ import org.junit.jupiter.api.Test;
 
 import java.util.List;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 class ConstantPropagationTest {
 
@@ -361,7 +366,18 @@ class ConstantPropagationTest {
         LogicalAggregate rewrittenAgg1 = (LogicalAggregate) 
rewrittenProject1.child();
         Assertions.assertEquals(expectGroupby1, 
rewrittenAgg1.getGroupByExpressions());
         Assertions.assertEquals(expectAggOutput1, 
rewrittenAgg1.getOutputExpressions());
-        Assertions.assertEquals(expectProjection1, 
rewrittenProject1.getProjects());
+        // the constant outputs above the aggregate are materialized with a 
fresh exprId, so that one exprId
+        // does not describe both the output slot of the aggregate and the 
constant
+        List<NamedExpression> actualProjects1 = 
rewrittenProject1.getProjects();
+        Assertions.assertEquals(expectProjection1.subList(0, 5), 
actualProjects1.subList(0, 5));
+        // the aggregate removes these two constant grouping keys from its 
outputs, so the project item is the only
+        // definition of their exprIds and the ids are kept
+        assertKeptConstantProjectItem(actualProjects1.get(5), 
aggOutput.get(5).getExprId(),
+                new IntegerLiteral(1), aggOutput.get(5).getName());
+        Assertions.assertEquals(expectProjection1.get(6), 
actualProjects1.get(6));
+        assertKeptConstantProjectItem(actualProjects1.get(7), 
aggOutput.get(7).getExprId(),
+                new IntegerLiteral(11), aggOutput.get(7).getName());
+        Assertions.assertEquals(expectProjection1.subList(8, 10), 
actualProjects1.subList(8, 10));
 
         Set<Expression> conjunctions2 = ImmutableSet.of(
                 new EqualTo(studentId, new IntegerLiteral(3)),
@@ -396,7 +412,141 @@ class ConstantPropagationTest {
         LogicalAggregate rewrittenAgg2 = (LogicalAggregate) 
rewrittenProject2.child();
         Assertions.assertEquals(expectGroupby2, 
rewrittenAgg2.getGroupByExpressions());
         Assertions.assertEquals(expectAggOutput2, rewrittenAgg2.getOutputs());
-        Assertions.assertEquals(expectProjection2, 
rewrittenProject2.getProjects());
+        List<NamedExpression> actualProjects2 = 
rewrittenProject2.getProjects();
+        Assertions.assertEquals(expectProjection2.subList(0, 5), 
actualProjects2.subList(0, 5));
+        // all grouping keys are constant, so the aggregate keeps the first 
one; the constant is still materialized
+        // above it, with the exprId of the aggregate output
+        assertKeptConstantProjectItem(actualProjects2.get(5), 
expectAggOutput2.get(5).getExprId(),
+                new IntegerLiteral(3), null);
+        // the other keys are removed from the aggregate, their exprIds are 
kept
+        assertKeptConstantProjectItem(actualProjects2.get(6), 
aggOutput.get(6).getExprId(),
+                new IntegerLiteral(3), aggOutput.get(6).getName());
+        assertKeptConstantProjectItem(actualProjects2.get(7), 
aggOutput.get(7).getExprId(),
+                new IntegerLiteral(13), aggOutput.get(7).getName());
+        assertKeptConstantProjectItem(actualProjects2.get(8), 
aggOutput.get(8).getExprId(),
+                new IntegerLiteral(13), aggOutput.get(8).getName());
+        assertKeptConstantProjectItem(actualProjects2.get(9), 
aggOutput.get(9).getExprId(),
+                new IntegerLiteral(6), aggOutput.get(9).getName());
+    }
+
+    @Test
+    void testConstantGroupByKeyKeepsExprIdWhenTheAggregateDropsIt() {
+        // positive gate for the materialized view shape which this PR must 
not change: the constant is materialized
+        // above the aggregate with the exprId of the aggregate output, no 
fresh exprId and no refresh of the plan
+        // above, so the identity of the column is kept (the MV matching 
relies on it)
+        LogicalFilter<LogicalOlapScan> filter = new 
LogicalFilter<>(ImmutableSet.of(
+                new EqualTo(studentId, new IntegerLiteral(5))), student);
+        List<Expression> groupby = ImmutableList.of(studentId, studentGender);
+        List<NamedExpression> aggOutput = ImmutableList.of(
+                new Alias(new Sum(studentAge), "sum_age"),
+                studentGender,
+                studentId);
+        LogicalAggregate<LogicalFilter<LogicalOlapScan>> aggregate
+                = new LogicalAggregate<>(groupby, aggOutput, filter);
+
+        LogicalProject<?> rewrittenProject = (LogicalProject<?>) 
executor.rewriteRoot(aggregate, jobContext);
+        NamedExpression constantItem = rewrittenProject.getProjects().get(2);
+        assertKeptConstantProjectItem(constantItem, studentId.getExprId(), new 
IntegerLiteral(5), studentId.getName());
+        // the aggregate dropped the key, so nothing below the project defines 
this column any more
+        Assertions.assertFalse(rewrittenProject.child().getOutput().stream()
+                .anyMatch(slot -> 
slot.getExprId().equals(studentId.getExprId())));
+    }
+
+    @Test
+    public void testProjectItemShadowingAChildSlotGetsFreshExprId() {
+        // a project item which re-defines a column which its child still 
outputs describes another value, so it
+        // must be materialized with a fresh exprId, and the sink above has to 
follow that exprId
+        LogicalFilter<LogicalOlapScan> filter = new 
LogicalFilter<>(ImmutableSet.of(
+                new EqualTo(studentId, new IntegerLiteral(5))), student);
+        LogicalProject<LogicalFilter<LogicalOlapScan>> project = new 
LogicalProject<>(ImmutableList.of(
+                new Alias(studentId.getExprId(), new Add(studentId, new 
IntegerLiteral(0)), studentId.getName()),
+                studentGender), filter);
+        List<NamedExpression> sinkOutputExprs = ImmutableList.of(studentId, 
studentGender);
+        LogicalBlackholeSink<LogicalProject<LogicalFilter<LogicalOlapScan>>> 
sink
+                = new LogicalBlackholeSink<>(sinkOutputExprs, project);
+
+        LogicalSink<?> rewrittenSink = (LogicalSink<?>) 
executor.rewriteRoot(sink, jobContext);
+        // the child of the project still outputs studentId, so the constant 
item got a fresh exprId
+        NamedExpression constantItem = ((LogicalProject<?>) 
rewrittenSink.child()).getProjects().get(0);
+        Assertions.assertEquals(new IntegerLiteral(5), ((Alias) 
constantItem).child());
+        Assertions.assertNotEquals(studentId.getExprId(), 
constantItem.getExprId());
+        // and the sink follows it, otherwise it would reference a slot which 
its child no longer outputs
+        Assertions.assertEquals(constantItem.getExprId(), 
rewrittenSink.getOutputExprs().get(0).getExprId());
+        List<ExprId> childOutputIds = 
rewrittenSink.child().getOutput().stream()
+                .map(Slot::getExprId).collect(Collectors.toList());
+        for (NamedExpression outputExpr : rewrittenSink.getOutputExprs()) {
+            
Assertions.assertTrue(childOutputIds.contains(outputExpr.getExprId()),
+                    "the sink expects " + outputExpr + " which its child does 
not output: "
+                            + rewrittenSink.child().getOutput());
+        }
+    }
+
+    @Test
+    public void testBlackholeSinkKeepsTheOutputExprIds() {
+        // `warm up select` is planned with a blackhole sink which takes its 
output expressions from its child, so
+        // the sink has to follow the fresh exprIds of the constants 
materialized below it, otherwise it expects a
+        // slot which the rewritten child does not output any more
+        LogicalFilter<LogicalOlapScan> filter = new 
LogicalFilter<>(ImmutableSet.of(
+                new EqualTo(studentId, new IntegerLiteral(5)),
+                new EqualTo(studentGender, new StringLiteral("F"))), student);
+        // all grouping keys are constant, so the aggregate keeps the first 
one (and drops the other), the constant
+        // above the kept key shadows it and must get a fresh exprId
+        List<Expression> groupby = ImmutableList.of(studentId, studentGender);
+        List<NamedExpression> aggOutput = ImmutableList.of(
+                new Alias(new Sum(studentAge), "sum_age"),
+                studentId,
+                studentGender);
+        LogicalAggregate<LogicalFilter<LogicalOlapScan>> aggregate
+                = new LogicalAggregate<>(groupby, aggOutput, filter);
+        List<NamedExpression> sinkOutputExprs = 
ImmutableList.copyOf(aggregate.getOutput());
+        LogicalBlackholeSink<LogicalAggregate<LogicalFilter<LogicalOlapScan>>> 
sink
+                = new LogicalBlackholeSink<>(sinkOutputExprs, aggregate);
+
+        LogicalSink<?> rewrittenSink = (LogicalSink<?>) 
executor.rewriteRoot(sink, jobContext);
+        List<ExprId> childOutputIds = 
rewrittenSink.child().getOutput().stream()
+                
.map(Slot::getExprId).collect(java.util.stream.Collectors.toList());
+        for (NamedExpression outputExpr : rewrittenSink.getOutputExprs()) {
+            
Assertions.assertTrue(childOutputIds.contains(outputExpr.getExprId()),
+                    "the sink expects " + outputExpr + " which its child does 
not output: "
+                            + rewrittenSink.child().getOutput());
+        }
+        // the constant column of the sink is refreshed to the fresh exprId of 
the constant project
+        // constant propagation keeps the exprIds here, the sink still 
references the output of its child
+        Assertions.assertEquals(studentId.getExprId(),
+                rewrittenSink.getOutputExprs().get(1).getExprId());
+    }
+
+    /**
+     * The aggregate still outputs the replaced column, so the constant 
project item would shadow it and must be
+     * materialized with a fresh exprId.
+     */
+    private static void assertFreshConstantProjectItem(NamedExpression actual, 
ExprId replacedExprId,
+            Expression expectedExpr, String expectedName) {
+        Alias alias = assertConstantProjectItem(actual, expectedExpr, 
expectedName);
+        Assertions.assertNotEquals(replacedExprId, alias.getExprId(),
+                "the constant project item must not reuse the exprId of an 
output which the aggregate still has");
+    }
+
+    /**
+     * The aggregate removed the constant grouping key from its outputs, so 
the project item is the only definition
+     * of the column: its exprId is kept, rules and consumers such as the 
materialized view matching rely on it.
+     */
+    private static void assertKeptConstantProjectItem(NamedExpression actual, 
ExprId replacedExprId,
+            Expression expectedExpr, String expectedName) {
+        Alias alias = assertConstantProjectItem(actual, expectedExpr, 
expectedName);
+        Assertions.assertEquals(replacedExprId, alias.getExprId(),
+                "the constant project item is the only definition, it must 
keep the exprId of the column");
+    }
+
+    private static Alias assertConstantProjectItem(NamedExpression actual, 
Expression expectedExpr,
+            String expectedName) {
+        Assertions.assertTrue(actual instanceof Alias, actual.toString());
+        Alias alias = (Alias) actual;
+        Assertions.assertEquals(expectedExpr, alias.child());
+        if (expectedName != null) {
+            Assertions.assertEquals(expectedName, alias.getName());
+        }
+        return alias;
     }
 
     @Test
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/CteConsumerOutputsRefresherTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/CteConsumerOutputsRefresherTest.java
new file mode 100644
index 00000000000..ae7f4cf55a2
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/CteConsumerOutputsRefresherTest.java
@@ -0,0 +1,107 @@
+// 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.CTEId;
+import org.apache.doris.nereids.trees.expressions.ExprId;
+import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.plans.RelationId;
+import org.apache.doris.nereids.trees.plans.logical.LogicalCTEConsumer;
+import org.apache.doris.nereids.types.IntegerType;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * A CTE producer body is rewritten in its own subtree context, while the 
consumers, which map to the producer
+ * outputs by exprId, were rewritten before it, so RewriteCteChildren 
refreshes the two maps of the consumers with
+ * the exprIds the rules of the producer body published.
+ */
+public class CteConsumerOutputsRefresherTest {
+    private static final CTEId CTE_ID = new CTEId(0);
+    private static final ExprId PRODUCER_K = new ExprId(0);
+    private static final ExprId PRODUCER_V = new ExprId(1);
+    private static final ExprId CONSUMER_K = new ExprId(10);
+    private static final ExprId CONSUMER_V = new ExprId(11);
+
+    @Test
+    void testRefreshFollowsTheReplacementChain() {
+        // k is renamed twice, e.g. by two rules or by two passes of the same 
rule
+        Map<ExprId, ExprId> remap = ImmutableMap.of(PRODUCER_K, new ExprId(2), 
new ExprId(2), new ExprId(3));
+        Slot newProducerK = slot(new ExprId(3), "k");
+        LogicalCTEConsumer consumer = consumer(slot(PRODUCER_K, "k"), 
slot(PRODUCER_V, "v"));
+
+        LogicalCTEConsumer refreshed = refresh(consumer, remap, 
ImmutableList.of(newProducerK, slot(PRODUCER_V, "v")));
+        Assertions.assertEquals(newProducerK, 
refreshed.getConsumerToProducerOutputMap().get(slot(CONSUMER_K, "k")));
+        Assertions.assertEquals(ImmutableList.of(slot(CONSUMER_K, "k")),
+                
ImmutableList.copyOf(refreshed.getProducerToConsumerOutputMap().get(newProducerK)));
+        // v was not renamed, it keeps pointing to the same producer slot
+        Assertions.assertEquals(slot(PRODUCER_V, "v"),
+                
refreshed.getConsumerToProducerOutputMap().get(slot(CONSUMER_V, "v")));
+    }
+
+    @Test
+    void testRenamedColumnWhichIsPrunedAfterwardsKeepsItsSlot() {
+        // the rule renamed k, but the rewrite of the producer body pruned the 
column afterwards, so the producer
+        // does not output the new exprId any more and there is nothing to map 
for it
+        Map<ExprId, ExprId> remap = ImmutableMap.of(PRODUCER_K, new ExprId(3));
+        Slot producerV = slot(PRODUCER_V, "v");
+        LogicalCTEConsumer consumer = consumer(slot(PRODUCER_K, "k"), 
producerV);
+
+        LogicalCTEConsumer refreshed = refresh(consumer, remap, 
ImmutableList.of(producerV));
+        Assertions.assertEquals(slot(PRODUCER_K, "k"),
+                
refreshed.getConsumerToProducerOutputMap().get(slot(CONSUMER_K, "k")));
+    }
+
+    @Test
+    void testConsumerOfAnotherCteIsNotTouched() {
+        Slot producerK = slot(PRODUCER_K, "k");
+        LogicalCTEConsumer otherConsumer = new LogicalCTEConsumer(new 
RelationId(1), new CTEId(1), "other",
+                ImmutableMap.of(slot(CONSUMER_K, "k"), producerK),
+                ImmutableMultimap.of(producerK, slot(CONSUMER_K, "k")));
+        LogicalCTEConsumer refreshed = (LogicalCTEConsumer) 
otherConsumer.accept(
+                new RewriteCteChildren.CteConsumerOutputsRefresher(CTE_ID, 
ImmutableMap.of(PRODUCER_K, new ExprId(3)),
+                        ImmutableMap.of(new ExprId(3), slot(new ExprId(3), 
"k"))), null);
+        Assertions.assertSame(otherConsumer, refreshed);
+    }
+
+    private static LogicalCTEConsumer refresh(LogicalCTEConsumer consumer, 
Map<ExprId, ExprId> remap,
+            java.util.List<Slot> producerOutputs) {
+        Map<ExprId, Slot> producerOutputSlots = producerOutputs.stream()
+                .collect(Collectors.toMap(Slot::getExprId, slot -> slot));
+        return (LogicalCTEConsumer) consumer.accept(new 
RewriteCteChildren.CteConsumerOutputsRefresher(
+                CTE_ID, remap, producerOutputSlots), null);
+    }
+
+    private static LogicalCTEConsumer consumer(Slot producerK, Slot producerV) 
{
+        return new LogicalCTEConsumer(new RelationId(0), CTE_ID, "c",
+                ImmutableMap.of(slot(CONSUMER_K, "k"), producerK, 
slot(CONSUMER_V, "v"), producerV),
+                ImmutableMultimap.of(producerK, slot(CONSUMER_K, "k"), 
producerV, slot(CONSUMER_V, "v")));
+    }
+
+    private static Slot slot(ExprId exprId, String name) {
+        return new SlotReference(exprId, name, IntegerType.INSTANCE, true, 
ImmutableList.of("test", "t"));
+    }
+}
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
index 1acef3d6e74..5559961e367 100644
--- 
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
@@ -17,12 +17,20 @@
 
 package org.apache.doris.nereids.rules.rewrite;
 
+import org.apache.doris.nereids.CascadesContext;
+import org.apache.doris.nereids.jobs.JobContext;
 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.NamedExpression;
 import org.apache.doris.nereids.trees.expressions.SlotReference;
 import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
 import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.nereids.util.MemoTestUtils;
+import org.apache.doris.nereids.util.PlanConstructor;
 
 import com.google.common.collect.ImmutableList;
 import org.junit.jupiter.api.Assertions;
@@ -47,6 +55,33 @@ import java.util.Map;
  */
 public class ExprIdRewriterTest {
 
+    // The guarded-branch test of the master version 
(testGuardedBranchIsRewritten) is not ported: it drives
+    // ExprIdRewriter with MergeUtils.selectBranch, which wraps the branches 
in a ShortCircuitIf. Neither
+    // MergeUtils nor ShortCircuitIf exists on branch-4.2 (MERGE INTO does not 
wrap its assignments that way
+    // here), so the subtree which the master fix had to enter does not exist 
on this branch.
+
+    @Test
+    void testAliasIdIsRewrittenThroughThePlanRewrite() {
+        // a rule which renames an Alias (e.g. EliminateGroupByKeyByUniform) 
publishes the alias exprId itself, and
+        // the plan rewrite has to replace it, not only the exprIds of the 
slots below it
+        ExprId oldAliasExprId = new ExprId(8);
+        ExprId newAliasExprId = new ExprId(10);
+        NamedExpression item = new Alias(oldAliasExprId, new 
IntegerLiteral(200), "field1");
+
+        LogicalOlapScan scan = new 
LogicalOlapScan(PlanConstructor.getNextRelationId(),
+                PlanConstructor.student, ImmutableList.of(""));
+        LogicalProject<LogicalOlapScan> project = new 
LogicalProject<>(ImmutableList.of(item), scan);
+        Map<ExprId, ExprId> replaceMap = new HashMap<>();
+        replaceMap.put(oldAliasExprId, newAliasExprId);
+        CascadesContext cascadesContext = 
MemoTestUtils.createCascadesContext(scan);
+        Plan rewritten = new ExprIdRewriter(new 
ExprIdRewriter.ReplaceRule(replaceMap, true),
+                new JobContext(cascadesContext, null, 
Double.MAX_VALUE)).rewriteExpr(project, replaceMap);
+
+        NamedExpression rewrittenItem = ((LogicalProject<?>) 
rewritten).getProjects().get(0);
+        Assertions.assertEquals(newAliasExprId, rewrittenItem.getExprId(), 
rewrittenItem.toString());
+        Assertions.assertEquals("field1", rewrittenItem.getName());
+    }
+
     /**
      * Test that visitAlias correctly rewrites Alias ExprId.
      *


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

Reply via email to