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

englefly pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 02cb614b1ed [fix](rbo) Give the constants materialized by constant 
propagation a fresh exprId (#69111)
02cb614b1ed is described below

commit 02cb614b1ed5e11f21cd782a8af5b9432820a4e8
Author: minghong <[email protected]>
AuthorDate: Wed Oct 21 19:05:09 2026 +0800

    [fix](rbo) Give the constants materialized by constant propagation a fresh 
exprId (#69111)
    
    ### What problem does this PR solve?
    
    Problem Summary:
    When ConstantPropagation replaces an aggregate output by a constant, it
    materializes the constant in a
    project above the aggregate, and the project item reused the exprId of
    the replaced output:
    
        LogicalProject ( projects=[..., 1 AS `id`#1, ...] )
        +-- LogicalAggregate ( groupByExpr=[id#1], outputExpr=[id#1] )
    
    So one exprId had two definitions, the aggregate output slot and the
    constant, whose nullable and data
    type may differ. Rules which compare exprIds only then take them as the
    same column. For RQG case
    11000258898 EliminateUnnecessaryProject removed such a project after the
    aggregate had been rewritten
    into `Limit 1`, the plan above fell back to the nullable output slot of
    the set operation, and the last
    AdjustNullable pass reported
    
    AdjustNullable convert slot 2004-04-28#55 from not-nullable to nullable
    
    Fix:
    Give the constant a fresh exprId only when it really shadows a column,
    i.e. when the exprId is still
    produced by the plan below; otherwise keep it:
    
    * the constant gets a fresh exprId, and the references above are
    refreshed with ExprIdRewriter, when the
    aggregate still outputs the replaced column (its grouping key is kept
    because all grouping keys are
    constant) or when a project item re-defines a column which its child
    still outputs. That is the shadow
      the old code created, e.g. the RQG case 11000258898 of DORIS-28416.
    * the constant keeps the exprId when the aggregate removed the constant
    grouping key from its outputs:
    the project item is then the only definition of the column, nothing
    below references the exprId, and
    consumers which rely on the identity of the column, e.g. the
    materialized view matching, keep working.
    The plan of such a query is byte identical to the plan before this
    change, including the exprIds
    (verified with the query of `agg_optimize_when_uniform/mv2_0`, for the
    query side and for the MV side
      which is generated with constant propagation disabled).
    
    The sink also has to follow the fresh exprIds, otherwise it would
    reference a slot which its child does
    not output any more; only exprIds are remapped there, the name and the
    data type declared by the sink are
    kept (that is why the rule does not replace the slots of the sink by
    literals).
    
    A CTE is rewritten in separate subtree contexts (producer body and
    consumers) and the consumers, which
    map their slots to the producer outputs by exprId, are rewritten before
    the producer. The renamed exprIds
    are published per CTE (`StatementContext#producerExprIdRemap`), and
    `RewriteCteChildren` refreshes both
    maps of every consumer with the last exprId of the chain
    (`ExprIdRewriter#lastExprId`).
    
    Reproduction: ConstantPropagationFreshExprIdTest runs this rule alone,
    so the project above the aggregate
    is still visible in the plan, and asserts that it does not reuse the
    exprId of the aggregate output and
    that the plan above references the fresh exprIds.
    ConstantPropagationTest used to pin the reused exprIds
    in its constant project expectations; it now asserts fresh exprIds. Both
    fail before this change.
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test: Unit Test
    - ConstantPropagationFreshExprIdTest (new): fails before with "the
    constant project reuses the exprId
          of the aggregate output".
    - ConstantPropagationTest: expectations updated, fails before with "the
    constant project item must not
          reuse the exprId of the replaced output slot".
    - 31 related FE unit test classes (constant propagation, constant cte,
    set operation, column pruning,
    fd, uniform, sql plan suite, runtime filter, ...) rerun: 385 tests, 0
    failures.
    - Behavior changed: Yes. A constant materialized above an aggregate has
    its own exprId and the plan above
    references it, so one exprId no longer describes two different columns.
    - Does this need documentation: No
---
 .../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        | 288 +++++++++++++++++++++
 .../rules/rewrite/ConstantPropagationTest.java     | 154 ++++++++++-
 .../rewrite/CteConsumerOutputsRefresherTest.java   | 107 ++++++++
 .../nereids/rules/rewrite/ExprIdRewriterTest.java  |  69 +++++
 8 files changed, 894 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 9f372233711..cd71830ed64 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
@@ -197,6 +197,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;
 
@@ -958,6 +963,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();
@@ -968,6 +986,7 @@ public class StatementContext implements Closeable {
         cteIdToConsumerGroup.clear();
         rewrittenCteProducer.clear();
         rewrittenCteConsumer.clear();
+        producerExprIdRemap.clear();
     }
 
     /**
@@ -982,7 +1001,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. */
@@ -1010,6 +1030,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) {
@@ -1038,6 +1069,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
@@ -1050,7 +1082,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;
@@ -1059,6 +1092,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 12570f95035..c1531addfa1 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;
         }
@@ -295,7 +344,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()
@@ -303,7 +352,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);
     }
 
     /**
@@ -350,8 +402,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.
@@ -371,12 +422,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 24ed8e1f068..4dcadc7e2f4 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,64 +127,38 @@ 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;
 
         /**
@@ -146,24 +171,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..674723d15a5
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ConstantPropagationFreshExprIdTest.java
@@ -0,0 +1,288 @@
+// 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());
+    }
+
+    @Test
+    void testRqgCase11000258898() {
+        // the RQG case this PR was reported from: on the reported base it 
tripped "AdjustNullable convert slot
+        // 2004-04-28#xx from not-nullable to nullable" during the rewrite, so 
the rewrite and the planning of the
+        // query have to succeed
+        PlanChecker.from(connectContext).analyze(SQL_11000258898).rewrite();
+        PlanChecker.from(connectContext).checkPlannerResult(SQL_11000258898);
+    }
+
+    @Test
+    void testRqgCase11000259585() {
+        // the second case of the same RQG report, same failure mode: the 
rewrite and the planning have to succeed
+        PlanChecker.from(connectContext).analyze(SQL_11000259585).rewrite();
+        PlanChecker.from(connectContext).checkPlannerResult(SQL_11000259585);
+    }
+
+    @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 14b82090d18..eea5de6aa85 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..148d8ecb648 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,23 @@
 
 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.Add;
 import org.apache.doris.nereids.trees.expressions.Alias;
+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.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.commands.merge.MergeUtils;
+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;
@@ -30,6 +41,8 @@ import org.junit.jupiter.api.Test;
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * Test for ExprIdRewriter, especially for visitAlias functionality.
@@ -47,6 +60,62 @@ import java.util.Map;
  */
 public class ExprIdRewriterTest {
 
+    @Test
+    void testGuardedBranchIsRewritten() {
+        // MERGE INTO wraps every assignment in a ShortCircuitIf, and such a 
branch has to be rewritten as well:
+        // a reference which keeps an old exprId would point to a slot which 
the rewritten child does not output
+        ExprId oldExprId = new ExprId(1);
+        ExprId newExprId = new ExprId(2);
+        SlotReference guardedSlot = slot(oldExprId, "k");
+        SlotReference conditionSlot = slot(new ExprId(3), "v");
+        // MergeUtils.selectBranch is what MergeIntoCommand builds every 
assignment with
+        NamedExpression assignment = new Alias(new ExprId(9),
+                MergeUtils.selectBranch(new EqualTo(conditionSlot, new 
IntegerLiteral(1)),
+                        new Add(guardedSlot, new IntegerLiteral(1)), 
guardedSlot), "value");
+
+        LogicalOlapScan scan = new 
LogicalOlapScan(PlanConstructor.getNextRelationId(),
+                PlanConstructor.student, ImmutableList.of(""));
+        LogicalProject<LogicalOlapScan> project
+                = new LogicalProject<>(ImmutableList.of(assignment), scan);
+        Map<ExprId, ExprId> replaceMap = new HashMap<>();
+        replaceMap.put(oldExprId, newExprId);
+        CascadesContext cascadesContext = 
MemoTestUtils.createCascadesContext(scan);
+        Plan rewritten = new ExprIdRewriter(new 
ExprIdRewriter.ReplaceRule(replaceMap, true),
+                new JobContext(cascadesContext, null)).rewriteExpr(project, 
replaceMap);
+
+        Expression rewrittenAssignment = ((Alias) ((LogicalProject<?>) 
rewritten).getProjects().get(0)).child();
+        Set<ExprId> rewrittenSlotExprIds = 
rewrittenAssignment.collect(expression -> expression instanceof SlotReference)
+                .stream().map(expression -> ((SlotReference) 
expression).getExprId()).collect(Collectors.toSet());
+        Assertions.assertFalse(rewrittenSlotExprIds.contains(oldExprId), 
rewrittenAssignment.toString());
+        Assertions.assertTrue(rewrittenSlotExprIds.contains(newExprId), 
rewrittenAssignment.toString());
+    }
+
+    @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)).rewriteExpr(project, 
replaceMap);
+
+        NamedExpression rewrittenItem = ((LogicalProject<?>) 
rewritten).getProjects().get(0);
+        Assertions.assertEquals(newAliasExprId, rewrittenItem.getExprId(), 
rewrittenItem.toString());
+        Assertions.assertEquals("field1", rewrittenItem.getName());
+    }
+
+    private static SlotReference slot(ExprId exprId, String name) {
+        return new SlotReference(exprId, name, IntegerType.INSTANCE, true, 
ImmutableList.of("test", "t"));
+    }
+
     /**
      * Test that visitAlias correctly rewrites Alias ExprId.
      *


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

Reply via email to