AbstractPlan.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.trees.plans;

import org.apache.doris.nereids.analyzer.Unbound;
import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.memo.GroupExpression;
import org.apache.doris.nereids.properties.LogicalProperties;
import org.apache.doris.nereids.properties.UnboundLogicalProperties;
import org.apache.doris.nereids.stats.HboUtils;
import org.apache.doris.nereids.trees.AbstractTreeNode;
import org.apache.doris.nereids.trees.expressions.ExprId;
import org.apache.doris.nereids.trees.expressions.Slot;
import org.apache.doris.nereids.trees.expressions.StatementScopeIdGenerator;
import org.apache.doris.nereids.trees.plans.TreeStringPlan.TreeStringNode;
import org.apache.doris.nereids.trees.plans.logical.AbstractLogicalPlan;
import org.apache.doris.nereids.trees.plans.physical.AbstractPhysicalPlan;
import org.apache.doris.nereids.trees.plans.physical.PhysicalHashAggregate;
import org.apache.doris.nereids.util.MutableState;
import org.apache.doris.nereids.util.TreeStringUtils;
import org.apache.doris.nereids.util.Utils;
import org.apache.doris.statistics.Statistics;

import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import org.json.JSONArray;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;

/**
 * Abstract class for all concrete plan node.
 */
public abstract class AbstractPlan extends AbstractTreeNode<Plan> implements Plan {
    public static final String FRAGMENT_ID = "fragment";
    private static final ObjectId zeroId = new ObjectId(0);
    protected final ObjectId id;

    protected final Statistics statistics;
    protected final PlanType type;
    protected final Optional<GroupExpression> groupExpression;
    protected final Supplier<LogicalProperties> logicalPropertiesSupplier;

    /**
     * all parameter constructor.
     */
    protected AbstractPlan(PlanType type, Optional<GroupExpression> groupExpression,
            Optional<LogicalProperties> optLogicalProperties, @Nullable Statistics statistics,
            List<Plan> children) {
        super(children);
        this.type = Objects.requireNonNull(type, "type can not be null");
        this.groupExpression = Objects.requireNonNull(groupExpression, "groupExpression can not be null");
        Objects.requireNonNull(optLogicalProperties, "logicalProperties can not be null");
        this.logicalPropertiesSupplier = Suppliers.memoize(() ->
                optLogicalProperties.orElseGet(this::computeLogicalProperties));
        this.statistics = statistics;
        this.id = StatementScopeIdGenerator.newObjectId();
    }

    protected AbstractPlan(PlanType type, Optional<GroupExpression> groupExpression,
            Supplier<LogicalProperties> logicalPropertiesSupplier, @Nullable Statistics statistics,
            List<Plan> children, boolean useZeroId) {
        super(children);
        this.type = Objects.requireNonNull(type, "type can not be null");
        this.groupExpression = Objects.requireNonNull(groupExpression, "groupExpression can not be null");
        this.logicalPropertiesSupplier = logicalPropertiesSupplier;
        this.statistics = statistics;
        Preconditions.checkArgument(useZeroId);
        this.id = zeroId;
    }

    @Override
    public PlanType getType() {
        return type;
    }

    public Optional<GroupExpression> getGroupExpression() {
        return groupExpression;
    }

    public Statistics getStats() {
        return statistics;
    }

    @Override
    public boolean canBind() {
        if (bound()) {
            return false;
        }
        for (Plan child : children()) {
            if (!child.bound()) {
                return false;
            }
        }
        return true;
    }

    public String getFingerprint() {
        return "";
    }

    /**
     * Get fingerprint of plan.
     */
    public String getPlanTreeFingerprint() {
        StringBuilder builder = new StringBuilder();
        builder.append(this.getFingerprint());
        if (this.children().isEmpty()) {
            return builder.toString();
        } else {
            List<Plan> mutableChildren = new ArrayList<>(children);
            // the following sorting by scans depended on will increase the plan matching possibility
            // during hbo plan matching stage, e.g, the final physical plan is encoded as 'a join b',
            // but the group plan is 'b join a' which can be matched also.
            // NOTE: it will increase the risk brought from rf, as above, the physical plan 'a join b'
            // is with rf's potential influence which will not exactly the same as 'b join a',
            // but when we ignore the join sides as above and want to increase the hbo plan stats.'s adaptability,
            // it may bring the unsuitable matching and increase the dependence for the rf-safe checking.
            Collections.sort(mutableChildren, new Comparator<Plan>() {
                @Override
                public int compare(Plan plan1, Plan plan2) {
                    List<String> scanQualifierList1 = new ArrayList<>();
                    List<String> scanQualifierList2 = new ArrayList<>();
                    HboUtils.collectScanQualifierList((AbstractPlan) plan1, scanQualifierList1);
                    HboUtils.collectScanQualifierList((AbstractPlan) plan2, scanQualifierList2);
                    Collections.sort(scanQualifierList1);
                    Collections.sort(scanQualifierList2);
                    String qualifiedName1 = Utils.qualifiedName(scanQualifierList1, "");
                    String qualifiedName2 = Utils.qualifiedName(scanQualifierList2, "");
                    return qualifiedName1.compareTo(qualifiedName2);
                }
            });
            for (Plan plan : mutableChildren) {
                if (plan instanceof GroupPlan) {
                    builder.append(((GroupPlan) plan).getFingerprint());
                } else if (plan instanceof AbstractLogicalPlan) {
                    builder.append(((AbstractPlan) plan).getPlanTreeFingerprint());
                } else if (plan instanceof AbstractPhysicalPlan) {
                    if (!isLocalAggPhysicalNode((AbstractPhysicalPlan) plan)) {
                        builder.append(((AbstractPlan) plan).getPlanTreeFingerprint());
                    } else {
                        builder.append(((AbstractPlan) plan.child(0)).getPlanTreeFingerprint());
                    }
                } else {
                    throw new AnalysisException("illegal plan type getPlanTreeFingerprint");
                }
            }
            return builder.toString();
        }
    }

    public static boolean isLocalAggPhysicalNode(AbstractPhysicalPlan plan) {
        if (plan instanceof PhysicalHashAggregate && ((PhysicalHashAggregate<?>) plan).getAggPhase().isLocal()) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Get tree like string describing query plan.
     *
     * @return tree like string describing query plan
     */
    @Override
    public String treeString() {
        return TreeStringUtils.treeString(this,
                plan -> plan.toString(),
                plan -> {
                    if (plan instanceof TreeStringPlan) {
                        Optional<TreeStringNode> treeStringNode = ((TreeStringPlan) plan).parseTreeStringNode();
                        return treeStringNode.isPresent() ? ImmutableList.of(treeStringNode.get()) : ImmutableList.of();
                    }
                    if (plan instanceof TreeStringNode) {
                        return (List) ((TreeStringNode) plan).children;
                    }
                    if (!(plan instanceof Plan)) {
                        return ImmutableList.of();
                    }
                    return (List) ((Plan) plan).children();
                },
                plan -> {
                    if (!(plan instanceof Plan)) {
                        return ImmutableList.of();
                    }
                    return (List) ((Plan) plan).extraPlans();
                },
                plan -> {
                    if (!(plan instanceof Plan)) {
                        return false;
                    }
                    return ((Plan) plan).displayExtraPlanFirst();
                });
    }

    /** top toJson method, can be override by specific operator */
    public JSONObject toJson() {
        JSONObject json = new JSONObject();
        json.put("PlanType", getType().toString());
        if (this.children().isEmpty()) {
            return json;
        }
        JSONArray childrenJson = new JSONArray();
        for (Plan child : children) {
            childrenJson.put(((AbstractPlan) child).toJson());
        }
        json.put("children", childrenJson);
        return json;
    }

    @Override
    public List<Slot> getOutput() {
        return getLogicalProperties().getOutput();
    }

    @Override
    public Set<Slot> getOutputSet() {
        return getLogicalProperties().getOutputSet();
    }

    @Override
    public Set<ExprId> getOutputExprIdSet() {
        return getLogicalProperties().getOutputExprIdSet();
    }

    @Override
    public LogicalProperties getLogicalProperties() {
        // TODO: use bound()?
        if (this instanceof Unbound) {
            return UnboundLogicalProperties.INSTANCE;
        }
        return logicalPropertiesSupplier.get();
    }

    @Override
    public LogicalProperties computeLogicalProperties() {
        boolean hasUnboundChild = false;
        for (Plan child : children) {
            if (!child.bound()) {
                hasUnboundChild = true;
                break;
            }
        }

        if (hasUnboundChild || hasUnboundExpression()) {
            return UnboundLogicalProperties.INSTANCE;
        } else {
            if (this instanceof DiffOutputInAsterisk) {
                return new LogicalProperties(this::computeOutput, this::computeAsteriskOutput, this::computeDataTrait);
            } else {
                return new LogicalProperties(this::computeOutput, this::computeDataTrait);
            }
        }
    }

    public int getId() {
        return id.asInt();
    }

    /**
     * ancestors in the tree
     */
    public List<Plan> getAncestors() {
        List<Plan> ancestors = Lists.newArrayList();
        ancestors.add(this);
        Optional<Object> parent = this.getMutableState(MutableState.KEY_PARENT);
        while (parent.isPresent()) {
            ancestors.add((Plan) parent.get());
            parent = ((Plan) parent.get()).getMutableState(MutableState.KEY_PARENT);
        }
        return ancestors;
    }

    public void updateActualRowCount(long actualRowCount) {
        if (statistics != null) {
            statistics.setActualRowCount(actualRowCount);
        }
    }
}