RewriteCteChildren.java

// 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.CascadesContext;
import org.apache.doris.nereids.annotation.DependsRules;
import org.apache.doris.nereids.jobs.JobContext;
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;
import org.apache.doris.nereids.trees.expressions.SlotReference;
import org.apache.doris.nereids.trees.plans.LimitPhase;
import org.apache.doris.nereids.trees.plans.Plan;
import org.apache.doris.nereids.trees.plans.RelationId;
import org.apache.doris.nereids.trees.plans.logical.LogicalCTEAnchor;
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.LogicalFilter;
import org.apache.doris.nereids.trees.plans.logical.LogicalLimit;
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.visitor.CustomRewriter;
import org.apache.doris.nereids.trees.plans.visitor.DefaultPlanRewriter;
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;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

/**
 * rewrite CteAnchor consumer side and producer side recursively, all CteAnchor must at top of the plan
 */
@DependsRules({PullUpCteAnchor.class, CTEInline.class})
public class RewriteCteChildren extends DefaultPlanRewriter<CascadesContext> implements CustomRewriter {

    private final List<RewriteJob> jobs;
    private final boolean runCboRules;

    public RewriteCteChildren(List<RewriteJob> jobs, boolean runCboRules) {
        this.jobs = jobs;
        this.runCboRules = runCboRules;
    }

    @Override
    public Plan rewriteRoot(Plan plan, JobContext jobContext) {
        return plan.accept(this, jobContext.getCascadesContext());
    }

    @Override
    public Plan visit(Plan plan, CascadesContext context) {
        Rewriter.getCteChildrenRewriter(context, jobs, runCboRules).execute();
        return context.getRewritePlan();
    }

    @Override
    public Plan visitLogicalCTEAnchor(LogicalCTEAnchor<? extends Plan, ? extends Plan> cteAnchor,
            CascadesContext cascadesContext) {
        LogicalPlan outer;
        if (cascadesContext.getStatementContext().getRewrittenCteConsumer().containsKey(cteAnchor.getCteId())) {
            outer = cascadesContext.getStatementContext().getRewrittenCteConsumer().get(cteAnchor.getCteId());
        } else {
            CascadesContext outerCascadesCtx = CascadesContext.newSubtreeContext(
                    Optional.empty(), cascadesContext, cteAnchor.child(1),
                    cascadesContext.getCurrentJobContext().getRequiredProperties());
            AtomicReference<LogicalPlan> outerResult = new AtomicReference<>();
            StatsDerive statsDerive = new StatsDerive(false);
            cteAnchor.child(0).accept(statsDerive, new StatsDerive.DeriveContext());
            outerCascadesCtx.withPlanProcess(cascadesContext.showPlanProcess(), () -> {
                outerResult.set((LogicalPlan) cteAnchor.child(1).accept(this, outerCascadesCtx));
            });
            outer = outerResult.get();
            cascadesContext.addPlanProcesses(outerCascadesCtx.getPlanProcesses());
            cascadesContext.getStatementContext().getRewrittenCteConsumer().put(cteAnchor.getCteId(), outer);
        }
        Set<LogicalCTEConsumer> cteConsumers = Sets.newHashSet();
        outer.foreach(p -> {
            if (p instanceof LogicalCTEConsumer) {
                LogicalCTEConsumer logicalCTEConsumer = (LogicalCTEConsumer) p;
                if (logicalCTEConsumer.getCteId().equals(cteAnchor.getCteId())) {
                    cteConsumers.add(logicalCTEConsumer);
                }
            }
            return false;
        });
        cascadesContext.getCteIdToConsumers().put(cteAnchor.getCteId(), cteConsumers);
        if (cteConsumers.isEmpty()) {
            return outer;
        }
        // Save original producer output before rewrite, to detect ExprId changes
        // caused by rules like EliminateGroupByKey that wrap slots with any_value().
        List<Slot> oldProducerOutput = cteAnchor.child(0).getOutput();
        Plan producer = cteAnchor.child(0).accept(this, cascadesContext);
        // visitLogicalCTEProducer may insert a pruning Project that drops producer outputs
        // not needed by any consumer, changing output arity. Align the old output with the
        // same prune set so that ExprId changes of surviving slots are still propagated.
        Set<Slot> neededProducerOutputs = cascadesContext.getStatementContext()
                .getCteIdToOutputIds().get(cteAnchor.getCteId());
        if (neededProducerOutputs != null && neededProducerOutputs.size() < oldProducerOutput.size()) {
            oldProducerOutput = oldProducerOutput.stream()
                    .filter(neededProducerOutputs::contains)
                    .collect(Collectors.toList());
        }
        outer = syncCteConsumerSlotMaps(oldProducerOutput, producer.getOutput(),
                cteAnchor.getCteId(), outer, cascadesContext);
        return cteAnchor.withChildren(producer, outer);
    }

    /**
     * If the producer rewrite changed output ExprIds (e.g. any_value wrapping in
     * EliminateGroupByKey), update CTEConsumer slot maps in the consumer tree to match.
     *
     * @return the consumer tree, updated if any producer ExprIds changed
     */
    private LogicalPlan syncCteConsumerSlotMaps(List<Slot> oldProducerOutput, List<Slot> newProducerOutput,
            CTEId cteId, LogicalPlan outer, CascadesContext cascadesContext) {
        if (oldProducerOutput.size() != newProducerOutput.size()) {
            return outer;
        }
        Map<ExprId, ExprId> exprIdReplaceMap = new HashMap<>();
        for (int i = 0; i < oldProducerOutput.size(); i++) {
            ExprId oldId = oldProducerOutput.get(i).getExprId();
            ExprId newId = newProducerOutput.get(i).getExprId();
            if (!oldId.equals(newId)) {
                exprIdReplaceMap.put(oldId, newId);
            }
        }
        if (exprIdReplaceMap.isEmpty()) {
            return outer;
        }
        // Collect old→new CTEConsumer mappings by walking the consumer tree.
        Map<LogicalCTEConsumer, LogicalCTEConsumer> oldToNew = new LinkedHashMap<>();
        outer.foreach(p -> {
            if (p instanceof LogicalCTEConsumer) {
                LogicalCTEConsumer consumer = (LogicalCTEConsumer) p;
                if (consumer.getCteId().equals(cteId)) {
                    oldToNew.put(consumer, updateCteConsumerSlotMaps(consumer, exprIdReplaceMap));
                }
            }
            return false;
        });
        if (oldToNew.isEmpty()) {
            return outer;
        }
        outer = (LogicalPlan) outer.rewriteUp(p -> {
            Plan replacement = oldToNew.get(p);
            return replacement != null ? replacement : p;
        });
        // Re-collect updated consumers so cteIdToConsumers stays in sync.
        Set<LogicalCTEConsumer> updatedConsumers = Sets.newHashSet();
        outer.foreach(p -> {
            if (p instanceof LogicalCTEConsumer) {
                LogicalCTEConsumer c = (LogicalCTEConsumer) p;
                if (c.getCteId().equals(cteId)) {
                    updatedConsumers.add(c);
                }
            }
            return false;
        });
        cascadesContext.getCteIdToConsumers().put(cteId, updatedConsumers);
        cascadesContext.getStatementContext().getRewrittenCteConsumer().put(cteId, outer);
        return outer;
    }

    /**
     * Rebuild CTEConsumer slot maps so that producer-side slots reference the new ExprIds
     * produced by aggregate rewriting (e.g. any_value wrapping in EliminateGroupByKey).
     */
    private LogicalCTEConsumer updateCteConsumerSlotMaps(
            LogicalCTEConsumer cteConsumer, Map<ExprId, ExprId> exprIdReplaceMap) {
        Map<Slot, Slot> newConsumerToProducer = new LinkedHashMap<>();
        Multimap<Slot, Slot> newProducerToConsumer = LinkedHashMultimap.create();
        for (Slot producerSlot : cteConsumer.getConsumerToProducerOutputMap().values()) {
            ExprId newExprId = exprIdReplaceMap.get(producerSlot.getExprId());
            Slot effectiveProducerSlot = newExprId != null
                    ? (Slot) producerSlot.withExprId(newExprId)
                    : producerSlot;
            for (Slot consumerSlot : cteConsumer.getProducerToConsumerOutputMap().get(producerSlot)) {
                newProducerToConsumer.put(effectiveProducerSlot, consumerSlot);
                newConsumerToProducer.put(consumerSlot, effectiveProducerSlot);
            }
        }
        return (LogicalCTEConsumer) cteConsumer.withTwoMaps(newConsumerToProducer, newProducerToConsumer);
    }

    @Override
    public Plan visitLogicalCTEProducer(LogicalCTEProducer<? extends Plan> cteProducer,
            CascadesContext cascadesContext) {
        LogicalPlan child;
        if (cascadesContext.getStatementContext().getRewrittenCteProducer().containsKey(cteProducer.getCteId())) {
            child = cascadesContext.getStatementContext().getRewrittenCteProducer().get(cteProducer.getCteId());
        } else {
            child = (LogicalPlan) cteProducer.child();
            child = tryToConstructFilter(cascadesContext, cteProducer.getCteId(), child);
            child = tryToConstructLimit(cascadesContext, cteProducer.getCteId(), child);
            Set<Slot> producerOutputs = cascadesContext.getStatementContext()
                    .getCteIdToOutputIds().get(cteProducer.getCteId());
            if (producerOutputs != null && producerOutputs.size() < child.getOutput().size()) {
                ImmutableList.Builder<NamedExpression> projectsBuilder
                        = ImmutableList.builderWithExpectedSize(producerOutputs.size());
                for (Slot slot : child.getOutput()) {
                    if (producerOutputs.contains(slot)) {
                        projectsBuilder.add(slot);
                    }
                }
                child = new LogicalProject<>(projectsBuilder.build(), child);
                child = pushPlanUnderAnchor(child);
            }
            CascadesContext rewrittenCtx = CascadesContext.newSubtreeContext(
                    Optional.of(cteProducer.getCteId()), cascadesContext, child, PhysicalProperties.ANY);
            AtomicReference<LogicalPlan> result = new AtomicReference<>(child);
            rewrittenCtx.withPlanProcess(cascadesContext.showPlanProcess(), () -> {
                result.set((LogicalPlan) result.get().accept(this, rewrittenCtx));
            });
            child = result.get();
            cascadesContext.addPlanProcesses(rewrittenCtx.getPlanProcesses());
            cascadesContext.getStatementContext().getRewrittenCteProducer().put(cteProducer.getCteId(), child);
        }
        LogicalCTEProducer<? extends Plan> rewrittenProducer = (LogicalCTEProducer<? extends Plan>) cteProducer
                .withChildren(child);
        cascadesContext.getStatementContext().setCteProducer(rewrittenProducer.getCteId(), rewrittenProducer);
        return rewrittenProducer;
    }

    private LogicalPlan pushPlanUnderAnchor(LogicalPlan plan) {
        if (plan.child(0) instanceof LogicalCTEAnchor) {
            LogicalPlan child = (LogicalPlan) plan.withChildren(plan.child(0).child(1));
            return (LogicalPlan) plan.child(0).withChildren(
                    plan.child(0).child(0), pushPlanUnderAnchor(child));
        }
        return plan;
    }

    private LogicalPlan tryToConstructLimit(CascadesContext cascadesContext, CTEId cteId, LogicalPlan child) {
        Set<LogicalCTEConsumer> consumers = cascadesContext.getCteIdToConsumers().get(cteId);
        long limit = 0;
        for (LogicalCTEConsumer consumer : consumers) {
            Long rowsNeeded = cascadesContext.getConsumerIdToLimitRows().get(consumer.getRelationId());
            if (rowsNeeded == null) {
                return child;
            }
            limit = Math.max(limit, rowsNeeded);
        }
        return pushPlanUnderAnchor(new LogicalLimit<>(limit, 0, LimitPhase.ORIGIN, child));
    }

    /*
     * An expression can only be pushed down if it has filter expressions on all consumers that reference the slot.
     * For example, let's assume a producer has two consumers, consumer1 and consumer2:
     *
     * filter(a > 5 and b < 1) -> consumer1
     * filter(a < 8) -> consumer2
     *
     * In this case, the only expression that can be pushed down to the producer is filter(a > 5 or a < 8).
     */
    private LogicalPlan tryToConstructFilter(CascadesContext cascadesContext, CTEId cteId, LogicalPlan child) {
        Set<RelationId> consumerIds = cascadesContext.getCteIdToConsumers().get(cteId).stream()
                .map(LogicalCTEConsumer::getRelationId)
                .collect(Collectors.toSet());
        List<Set<Expression>> filtersAboveEachConsumer = cascadesContext.getConsumerIdToFilters().entrySet().stream()
                .filter(kv -> consumerIds.contains(kv.getKey()))
                .map(Entry::getValue)
                .collect(Collectors.toList());
        Set<Expression> someone = filtersAboveEachConsumer.stream().findFirst().orElse(null);
        if (someone == null) {
            return child;
        }
        int filterSize = cascadesContext.getCteIdToConsumers().get(cteId).size();
        Set<Expression> conjuncts = new HashSet<>();
        for (Expression f : someone) {
            int matchCount = 0;
            Set<SlotReference> slots = f.collect(e -> e instanceof SlotReference);
            Set<Expression> mightBeJoined = new HashSet<>();
            for (Set<Expression> another : filtersAboveEachConsumer) {
                if (another.equals(someone)) {
                    matchCount++;
                    continue;
                }
                Set<Expression> matched = new HashSet<>();
                for (Expression e : another) {
                    Set<SlotReference> otherSlots = e.collect(ae -> ae instanceof SlotReference);
                    if (otherSlots.equals(slots)) {
                        matched.add(e);
                    }
                }
                if (!matched.isEmpty()) {
                    matchCount++;
                }
                mightBeJoined.addAll(matched);
            }
            if (matchCount >= filterSize) {
                mightBeJoined.add(f);
                conjuncts.add(ExpressionUtils.or(mightBeJoined));
            }
        }
        if (!conjuncts.isEmpty()) {
            // distinct conjuncts
            ImmutableSet.Builder<Expression> newConjuncts = ImmutableSet.builderWithExpectedSize(conjuncts.size());
            for (Expression conjunct : conjuncts) {
                newConjuncts.addAll(ExpressionUtils.extractConjunction(conjunct));
            }
            LogicalPlan filter = new LogicalFilter<>(newConjuncts.build(), child);
            return pushPlanUnderAnchor(filter);
        }
        return child;
    }
}