/*
 * 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.asterix.optimizer.rules.cbo;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;

import org.apache.asterix.common.annotations.IndexedNLJoinExpressionAnnotation;
import org.apache.asterix.common.annotations.SkipSecondaryIndexSearchExpressionAnnotation;
import org.apache.asterix.metadata.entities.Index;
import org.apache.asterix.om.functions.BuiltinFunctions;
import org.apache.asterix.optimizer.cost.Cost;
import org.apache.asterix.optimizer.cost.ICost;
import org.apache.asterix.optimizer.rules.am.AccessMethodAnalysisContext;
import org.apache.asterix.optimizer.rules.am.IAccessMethod;
import org.apache.asterix.optimizer.rules.am.IOptimizableFuncExpr;
import org.apache.asterix.optimizer.rules.am.IntroduceJoinAccessMethodRule;
import org.apache.asterix.optimizer.rules.am.IntroduceSelectAccessMethodRule;
import org.apache.commons.lang3.mutable.MutableObject;
import org.apache.hyracks.algebricks.common.exceptions.AlgebricksException;
import org.apache.hyracks.algebricks.common.utils.Pair;
import org.apache.hyracks.algebricks.core.algebra.base.ILogicalExpression;
import org.apache.hyracks.algebricks.core.algebra.base.ILogicalOperator;
import org.apache.hyracks.algebricks.core.algebra.base.LogicalExpressionTag;
import org.apache.hyracks.algebricks.core.algebra.base.LogicalOperatorTag;
import org.apache.hyracks.algebricks.core.algebra.base.LogicalVariable;
import org.apache.hyracks.algebricks.core.algebra.expressions.AbstractFunctionCallExpression;
import org.apache.hyracks.algebricks.core.algebra.expressions.BroadcastExpressionAnnotation;
import org.apache.hyracks.algebricks.core.algebra.expressions.ConstantExpression;
import org.apache.hyracks.algebricks.core.algebra.expressions.HashJoinExpressionAnnotation;
import org.apache.hyracks.algebricks.core.algebra.expressions.PredicateCardinalityAnnotation;
import org.apache.hyracks.algebricks.core.algebra.expressions.ScalarFunctionCallExpression;
import org.apache.hyracks.algebricks.core.algebra.functions.AlgebricksBuiltinFunctions;
import org.apache.hyracks.algebricks.core.algebra.operators.logical.AbstractBinaryJoinOperator;
import org.apache.hyracks.algebricks.core.algebra.operators.logical.EmptyTupleSourceOperator;
import org.apache.hyracks.algebricks.core.algebra.operators.logical.SelectOperator;
import org.apache.hyracks.algebricks.core.config.AlgebricksConfig;
import org.apache.hyracks.api.exceptions.ErrorCode;
import org.apache.hyracks.api.exceptions.IWarningCollector;
import org.apache.hyracks.api.exceptions.Warning;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class JoinNode {
    private static final Logger LOGGER = LogManager.getLogger();

    protected JoinEnum joinEnum;
    protected int jnArrayIndex;
    protected int datasetBits; // this is bitmap of all the keyspaceBits present in this joinNode
    protected List<Integer> datasetIndexes;
    protected List<String> datasetNames;
    protected List<String> aliases;
    protected int cheapestPlanIndex;
    protected ICost cheapestPlanCost;
    protected double origCardinality; // without any selections
    protected double cardinality;
    protected double size;
    protected List<Integer> planIndexesArray; // indexes into the PlanNode array in enumerateJoins
    protected int jnIndex, level, highestDatasetId;
    protected JoinNode rightJn, leftJn;
    protected List<Integer> applicableJoinConditions;
    protected EmptyTupleSourceOperator correspondingEmptyTupleSourceOp; // There is a 1-1 relationship between the LVs and the dataSourceScanOps and the leafInputs.
    protected List<Pair<IAccessMethod, Index>> chosenIndexes;
    protected Map<IAccessMethod, AccessMethodAnalysisContext> analyzedAMs;
    protected Index.SampleIndexDetails idxDetails;
    protected static int NO_JN = -1;
    protected static int NO_CARDS = -2;

    public JoinNode(int i) {
        this.jnArrayIndex = i;
        planIndexesArray = new ArrayList<>();
        cheapestPlanIndex = PlanNode.NO_PLAN;
        size = 1; // for now, will be the size of the doc for this joinNode
    }

    public JoinNode(int i, JoinEnum joinE) {
        this(i);
        joinEnum = joinE;
        cheapestPlanCost = joinEnum.getCostHandle().maxCost();
    }

    public boolean IsBaseLevelJoinNode() {
        return this.jnArrayIndex <= joinEnum.numberOfTerms;
    }

    public boolean IsHigherLevelJoinNode() {
        return !IsBaseLevelJoinNode();
    }

    public double computeJoinCardinality() {
        JoinNode[] jnArray = joinEnum.getJnArray();
        List<JoinCondition> joinConditions = joinEnum.getJoinConditions();

        this.applicableJoinConditions = new ArrayList<>();
        findApplicableJoinConditions();

        if (LOGGER.isTraceEnabled() && this.applicableJoinConditions.size() == 0) {
            LOGGER.trace("applicable Join Conditions size is 0 in join Node " + this.jnArrayIndex);
        }

        // Wonder if this computation will result in an overflow exception. Better to multiply them with selectivities also.
        double productJoinCardinality = 1.0;
        for (int idx : this.datasetIndexes) {
            productJoinCardinality *= jnArray[idx].cardinality;
        }

        double productJoinSels = 1.0;
        for (int idx : this.applicableJoinConditions) {
            if (!joinConditions.get(idx).partOfComposite) {
                productJoinSels *= joinConditions.get(idx).selectivity;
            }
        }
        return productJoinCardinality * productJoinSels;
    }

    public double getCardinality() {
        return cardinality;
    }

    public void setCardinality(double card) {
        cardinality = card;
    }

    public double getOrigCardinality() {
        return origCardinality;
    }

    public void setOrigCardinality(double card) {
        origCardinality = card;
    }

    public void setAvgDocSize(double avgDocSize) {
        size = avgDocSize;
    }

    public double getInputSize() {
        return size;
    }

    public double getOutputSize() {
        return size; // need to change this to account for projections
    }

    public JoinNode getLeftJn() {
        return leftJn;
    }

    public JoinNode getRightJn() {
        return rightJn;
    }

    public List<String> getAliases() {
        return aliases;
    }

    public List<String> getDatasetNames() {
        return datasetNames;
    }

    public Index.SampleIndexDetails getIdxDetails() {
        return idxDetails;
    }

    protected boolean nestedLoopsApplicable(ILogicalExpression joinExpr) throws AlgebricksException {

        List<LogicalVariable> usedVarList = new ArrayList<>();
        joinExpr.getUsedVariables(usedVarList);
        if (usedVarList.size() != 2) {
            return false;
        }

        if (joinExpr.getExpressionTag() != LogicalExpressionTag.FUNCTION_CALL) {
            return false;
        }

        LogicalVariable var0 = usedVarList.get(0);
        LogicalVariable var1 = usedVarList.get(1);

        // Find which joinLeafInput these vars belong to.
        // go thru the leaf inputs and see where these variables came from
        ILogicalOperator joinLeafInput0 = joinEnum.findLeafInput(Collections.singletonList(var0));
        if (joinLeafInput0 == null) {
            return false; // this should not happen unless an assignment is between two joins.
        }

        ILogicalOperator joinLeafInput1 = joinEnum.findLeafInput(Collections.singletonList(var1));
        if (joinLeafInput1 == null) {
            return false;
        }

        // We need to find out which one of these is the inner joinLeafInput. So for that get the joinLeafInput of this join node.
        ILogicalOperator innerLeafInput = joinEnum.joinLeafInputsHashMap.get(this.correspondingEmptyTupleSourceOp);

        // This must equal one of the two joinLeafInputsHashMap found above. check for sanity!!
        if (innerLeafInput != joinLeafInput1 && innerLeafInput != joinLeafInput0) {
            return false; // This should not happen. So debug to find out why this happened.
        }

        if (innerLeafInput == joinLeafInput0) {
            joinEnum.localJoinOp.getInputs().get(0).setValue(joinLeafInput1);
        } else {
            joinEnum.localJoinOp.getInputs().get(0).setValue(joinLeafInput0);
        }

        joinEnum.localJoinOp.getInputs().get(1).setValue(innerLeafInput);

        // We will always use the first join Op to provide the joinOp input for invoking rewritePre
        AbstractBinaryJoinOperator joinOp = (AbstractBinaryJoinOperator) joinEnum.localJoinOp;
        joinOp.getCondition().setValue(joinExpr);

        // Now call the rewritePre code
        IntroduceJoinAccessMethodRule tmp = new IntroduceJoinAccessMethodRule();
        boolean retVal = tmp.checkApplicable(new MutableObject<>(joinEnum.localJoinOp), joinEnum.optCtx);

        return retVal;
    }

    /** one is a subset of two */
    private boolean subset(int one, int two) {
        return (one & two) == one;
    }

    protected void findApplicableJoinConditions() {
        List<JoinCondition> joinConditions = joinEnum.getJoinConditions();

        int i = 0;
        for (JoinCondition jc : joinConditions) {
            if (subset(jc.datasetBits, this.datasetBits)) {
                this.applicableJoinConditions.add(i);
            }
            i++;
        }
    }

    protected List<Integer> getNewJoinConditionsOnly() {
        List<Integer> newJoinConditions = new ArrayList<>();
        JoinNode leftJn = this.leftJn;
        JoinNode rightJn = this.rightJn;
        // find the new table being added. This assume only zig zag trees for now.
        int newTableBits = 0;
        if (leftJn.jnArrayIndex <= joinEnum.numberOfTerms) {
            newTableBits = leftJn.datasetBits;
        } else if (rightJn.jnArrayIndex <= joinEnum.numberOfTerms) {
            newTableBits = rightJn.datasetBits;
        }

        if (LOGGER.isTraceEnabled() && newTableBits == 0) {
            LOGGER.trace("newTable Bits == 0");
        }

        // All the new join predicates will have these bits turned on
        for (int idx : this.applicableJoinConditions) {
            if ((joinEnum.joinConditions.get(idx).datasetBits & newTableBits) > 0) {
                newJoinConditions.add(idx);
            }
        }

        return newJoinConditions; // this can be of size 0 because this may be a cartesian join
    }

    public int addSingleDatasetPlans() {
        List<PlanNode> allPlans = joinEnum.allPlans;
        ICost opCost, totalCost;

        opCost = joinEnum.getCostMethodsHandle().costFullScan(this);
        totalCost = opCost;
        if (this.cheapestPlanIndex == PlanNode.NO_PLAN || opCost.costLT(this.cheapestPlanCost)) {
            // for now just add one plan
            PlanNode pn = new PlanNode(allPlans.size(), joinEnum);
            pn.jn = this;
            pn.datasetName = this.datasetNames.get(0);
            pn.correspondingEmptyTupleSourceOp = this.correspondingEmptyTupleSourceOp;
            pn.jnIndexes[0] = this.jnArrayIndex;
            pn.jnIndexes[1] = JoinNode.NO_JN;
            pn.planIndexes[0] = PlanNode.NO_PLAN; // There ane no plans below this plan.
            pn.planIndexes[1] = PlanNode.NO_PLAN; // There ane no plans below this plan.
            pn.opCost = opCost;
            pn.scanOp = PlanNode.ScanMethod.TABLE_SCAN;
            pn.totalCost = totalCost;

            allPlans.add(pn);
            this.planIndexesArray.add(allPlans.size() - 1);
            this.cheapestPlanCost = totalCost;
            this.cheapestPlanIndex = allPlans.size() - 1;
            return this.cheapestPlanIndex;
        }
        return PlanNode.NO_PLAN;
    }

    protected void buildIndexPlan(boolean forceIndexPlan) {
        List<PlanNode> allPlans = joinEnum.allPlans;
        ICost opCost, totalCost;

        opCost = joinEnum.getCostMethodsHandle().costIndexScan(this);
        totalCost = opCost;
        if (this.cheapestPlanIndex == PlanNode.NO_PLAN || opCost.costLT(this.cheapestPlanCost) || forceIndexPlan) {
            // for now just add one plan
            PlanNode pn = new PlanNode(allPlans.size(), joinEnum);
            pn.jn = this;
            pn.datasetName = this.datasetNames.get(0);
            pn.correspondingEmptyTupleSourceOp = this.correspondingEmptyTupleSourceOp;
            pn.jnIndexes[0] = this.jnArrayIndex;
            pn.jnIndexes[1] = JoinNode.NO_JN;
            pn.planIndexes[0] = PlanNode.NO_PLAN; // There ane no plans below this plan.
            pn.planIndexes[1] = PlanNode.NO_PLAN; // There ane no plans below this plan.
            pn.opCost = opCost;
            pn.scanOp = PlanNode.ScanMethod.INDEX_SCAN;
            pn.totalCost = totalCost;

            allPlans.add(pn);
            this.planIndexesArray.add(allPlans.size() - 1);
            this.cheapestPlanCost = totalCost;
            this.cheapestPlanIndex = allPlans.size() - 1;
        }
    }

    protected void costAndChooseIndexPlans(ILogicalOperator leafInput,
            Map<IAccessMethod, AccessMethodAnalysisContext> analyzedAMs) throws AlgebricksException {
        // Skip indexes with selectivity greater than 0.1, add the SKIP_SECONDARY_INDEX annotation to its expression.
        double sel;
        int exprIndex;
        for (Map.Entry<IAccessMethod, AccessMethodAnalysisContext> amEntry : analyzedAMs.entrySet()) {
            AccessMethodAnalysisContext analysisCtx = amEntry.getValue();
            Iterator<Map.Entry<Index, List<Pair<Integer, Integer>>>> indexIt =
                    analysisCtx.getIteratorForIndexExprsAndVars();
            List<IOptimizableFuncExpr> exprs = analysisCtx.getMatchedFuncExprs();
            while (indexIt.hasNext()) {
                Map.Entry<Index, List<Pair<Integer, Integer>>> indexEntry = indexIt.next();
                Index chosenIndex = indexEntry.getKey();
                exprIndex = indexEntry.getValue().get(0).getFirst();
                IOptimizableFuncExpr expr = exprs.get(exprIndex);
                AbstractFunctionCallExpression afce = expr.getFuncExpr();
                PredicateCardinalityAnnotation selectivityAnnotation =
                        afce.getAnnotation(PredicateCardinalityAnnotation.class);
                if (joinEnum.findUseIndexHint(afce)) {
                    buildIndexPlan(true);
                } else if (selectivityAnnotation != null) {
                    sel = selectivityAnnotation.getSelectivity();
                    if (sel >= joinEnum.stats.SELECTIVITY_FOR_SECONDARY_INDEX_SELECTION) {
                        afce.putAnnotation(SkipSecondaryIndexSearchExpressionAnnotation
                                .newInstance(Collections.singleton(chosenIndex.getIndexName())));
                    } else {
                        buildIndexPlan(false);
                    }
                }
            }
        }
    }

    private SelectOperator copySelExprsAndSetTrue(List<ILogicalExpression> selExprs, List<SelectOperator> selOpers,
            ILogicalOperator leafInput) {
        ILogicalOperator op = leafInput;
        SelectOperator firstSelOp = null;
        boolean firstSel = true;
        while (op != null && op.getOperatorTag() != LogicalOperatorTag.EMPTYTUPLESOURCE) {
            if (op.getOperatorTag() == LogicalOperatorTag.SELECT) {
                SelectOperator selOp = (SelectOperator) op;
                if (firstSel) {
                    firstSelOp = selOp;
                    firstSel = false;
                }
                selOpers.add(selOp);
                selExprs.add(selOp.getCondition().getValue());
                selOp.getCondition().setValue(ConstantExpression.TRUE); // we will switch these back later
            }
            op = op.getInputs().get(0).getValue();
        }
        return firstSelOp;
    }

    private void restoreSelExprs(List<ILogicalExpression> selExprs, List<SelectOperator> selOpers) {
        for (int i = 0; i < selExprs.size(); i++) {
            selOpers.get(i).getCondition().setValue(selExprs.get(i));
        }
    }

    private ILogicalExpression andAlltheExprs(List<ILogicalExpression> selExprs) {
        if (selExprs.size() == 1) {
            return selExprs.get(0);
        }

        ScalarFunctionCallExpression andExpr = new ScalarFunctionCallExpression(
                BuiltinFunctions.getBuiltinFunctionInfo(AlgebricksBuiltinFunctions.AND));

        for (ILogicalExpression se : selExprs) {
            andExpr.getArguments().add(new MutableObject<>(se));
        }
        return andExpr;
    }

    // Look for the pattern select, select, subplan and collapse to select, subplan
    // This code does not belong in the CBO!!
    private boolean combineDoubleSelectsBeforeSubPlans(ILogicalOperator op) {
        boolean changes = false;
        while (op != null && op.getOperatorTag() != LogicalOperatorTag.EMPTYTUPLESOURCE) {
            if (op.getOperatorTag() == LogicalOperatorTag.SELECT) {
                SelectOperator selOp1 = (SelectOperator) op;
                if (selOp1.getInputs().get(0).getValue().getOperatorTag().equals(LogicalOperatorTag.SELECT)) {
                    SelectOperator selOp2 = (SelectOperator) (op.getInputs().get(0).getValue());
                    ILogicalOperator op2 = selOp2.getInputs().get(0).getValue();
                    if (op2.getOperatorTag() == LogicalOperatorTag.SUBPLAN) { // found the pattern we are looking for
                        selOp1.getInputs().get(0).setValue(op2);
                        ILogicalExpression exp1 = selOp1.getCondition().getValue();
                        ILogicalExpression exp2 = selOp2.getCondition().getValue();
                        ScalarFunctionCallExpression andExpr = new ScalarFunctionCallExpression(
                                BuiltinFunctions.getBuiltinFunctionInfo(AlgebricksBuiltinFunctions.AND));
                        andExpr.getArguments().add(new MutableObject<>(exp1));
                        andExpr.getArguments().add(new MutableObject<>(exp2));
                        selOp1.getCondition().setValue(andExpr);
                        op = op2.getInputs().get(0).getValue();
                        changes = true;
                    }
                }
            }
            op = op.getInputs().get(0).getValue();
        }
        return changes;
    }

    public void addIndexAccessPlans(ILogicalOperator leafInput) throws AlgebricksException {
        IntroduceSelectAccessMethodRule tmp = new IntroduceSelectAccessMethodRule();
        List<Pair<IAccessMethod, Index>> chosenIndexes = new ArrayList<>();
        Map<IAccessMethod, AccessMethodAnalysisContext> analyzedAMs = new TreeMap<>();

        while (combineDoubleSelectsBeforeSubPlans(leafInput));
        List<ILogicalExpression> selExprs = new ArrayList<>();
        List<SelectOperator> selOpers = new ArrayList<>();
        SelectOperator firstSelop = copySelExprsAndSetTrue(selExprs, selOpers, leafInput);
        if (firstSelop != null) { // if there are no selects, then there is no question of index selections either.
            firstSelop.getCondition().setValue(andAlltheExprs(selExprs));
            boolean index_access_possible =
                    tmp.checkApplicable(new MutableObject<>(leafInput), joinEnum.optCtx, chosenIndexes, analyzedAMs);
            this.chosenIndexes = chosenIndexes;
            this.analyzedAMs = analyzedAMs;
            restoreSelExprs(selExprs, selOpers);
            if (index_access_possible) {
                costAndChooseIndexPlans(leafInput, analyzedAMs);
            }
        } else {
            restoreSelExprs(selExprs, selOpers);
        }
    }

    protected int buildHashJoinPlan(JoinNode leftJn, JoinNode rightJn, ILogicalExpression hashJoinExpr,
            HashJoinExpressionAnnotation hintHashJoin) {
        List<PlanNode> allPlans = joinEnum.allPlans;
        PlanNode pn;
        ICost hjCost, leftExchangeCost, rightExchangeCost, childCosts, totalCost;
        this.leftJn = leftJn;
        this.rightJn = rightJn;
        int leftPlan = leftJn.cheapestPlanIndex;
        int rightPlan = rightJn.cheapestPlanIndex;

        if (hashJoinExpr == null || hashJoinExpr == ConstantExpression.TRUE) {
            return PlanNode.NO_PLAN;
        }

        if (joinEnum.queryPlanShape.equals(AlgebricksConfig.QUERY_PLAN_SHAPE_LEFTDEEP)
                && !leftJn.IsBaseLevelJoinNode()) {
            return PlanNode.NO_PLAN;
        }

        if (joinEnum.queryPlanShape.equals(AlgebricksConfig.QUERY_PLAN_SHAPE_RIGHTDEEP)
                && !rightJn.IsBaseLevelJoinNode()) {
            return PlanNode.NO_PLAN;
        }

        if (rightJn.cardinality * rightJn.size <= leftJn.cardinality * leftJn.size || hintHashJoin != null
                || joinEnum.forceJoinOrderMode
                || !joinEnum.queryPlanShape.equals(AlgebricksConfig.QUERY_PLAN_SHAPE_ZIGZAG)) {
            // We want to build with the smaller side.
            hjCost = joinEnum.getCostMethodsHandle().costHashJoin(this);
            leftExchangeCost = joinEnum.getCostMethodsHandle().computeHJProbeExchangeCost(this);
            rightExchangeCost = joinEnum.getCostMethodsHandle().computeHJBuildExchangeCost(this);
            childCosts = allPlans.get(leftPlan).totalCost.costAdd(allPlans.get(rightPlan).totalCost);
            totalCost = hjCost.costAdd(leftExchangeCost).costAdd(rightExchangeCost).costAdd(childCosts);
            if (this.cheapestPlanIndex == PlanNode.NO_PLAN || totalCost.costLT(this.cheapestPlanCost)) {
                pn = new PlanNode(allPlans.size(), joinEnum);
                pn.jn = this;
                pn.jnIndexes[0] = leftJn.jnArrayIndex;
                pn.jnIndexes[1] = rightJn.jnArrayIndex;
                pn.planIndexes[0] = leftPlan;
                pn.planIndexes[1] = rightPlan;
                pn.joinOp = PlanNode.JoinMethod.HYBRID_HASH_JOIN; // need to check that all the conditions have equality predicates ONLY.
                if (hintHashJoin != null) {
                    hintHashJoin.setBuildSide(HashJoinExpressionAnnotation.BuildSide.RIGHT);
                }
                pn.side = HashJoinExpressionAnnotation.BuildSide.RIGHT;
                pn.joinExpr = hashJoinExpr;
                pn.opCost = hjCost;
                pn.totalCost = totalCost;
                pn.leftExchangeCost = leftExchangeCost;
                pn.rightExchangeCost = rightExchangeCost;
                allPlans.add(pn);
                this.planIndexesArray.add(allPlans.size() - 1);
                this.cheapestPlanCost = totalCost;
                this.cheapestPlanIndex = allPlans.size() - 1;
                return this.cheapestPlanIndex;
            }
        }

        return PlanNode.NO_PLAN;
    }

    protected int buildBroadcastHashJoinPlan(JoinNode leftJn, JoinNode rightJn, ILogicalExpression hashJoinExpr,
            BroadcastExpressionAnnotation hintBroadcastHashJoin) {
        List<PlanNode> allPlans = joinEnum.allPlans;
        PlanNode pn;
        ICost bcastHjCost, leftExchangeCost, rightExchangeCost, childCosts, totalCost;

        this.leftJn = leftJn;
        this.rightJn = rightJn;
        int leftPlan = leftJn.cheapestPlanIndex;
        int rightPlan = rightJn.cheapestPlanIndex;

        if (hashJoinExpr == null || hashJoinExpr == ConstantExpression.TRUE) {
            return PlanNode.NO_PLAN;
        }

        if (joinEnum.queryPlanShape.equals(AlgebricksConfig.QUERY_PLAN_SHAPE_LEFTDEEP)
                && !leftJn.IsBaseLevelJoinNode()) {
            return PlanNode.NO_PLAN;
        }

        if (joinEnum.queryPlanShape.equals(AlgebricksConfig.QUERY_PLAN_SHAPE_RIGHTDEEP)
                && !rightJn.IsBaseLevelJoinNode()) {
            return PlanNode.NO_PLAN;
        }

        if (rightJn.cardinality * rightJn.size <= leftJn.cardinality * leftJn.size || hintBroadcastHashJoin != null
                || joinEnum.forceJoinOrderMode
                || !joinEnum.queryPlanShape.equals(AlgebricksConfig.QUERY_PLAN_SHAPE_ZIGZAG)) {
            // We want to broadcast and build with the smaller side.
            bcastHjCost = joinEnum.getCostMethodsHandle().costBroadcastHashJoin(this);
            leftExchangeCost = joinEnum.getCostHandle().zeroCost();
            rightExchangeCost = joinEnum.getCostMethodsHandle().computeBHJBuildExchangeCost(this);
            childCosts = allPlans.get(leftPlan).totalCost.costAdd(allPlans.get(rightPlan).totalCost);
            totalCost = bcastHjCost.costAdd(rightExchangeCost).costAdd(childCosts);
            if (this.cheapestPlanIndex == PlanNode.NO_PLAN || totalCost.costLT(this.cheapestPlanCost)) {
                pn = new PlanNode(allPlans.size(), joinEnum);
                pn.jn = this;
                pn.jnIndexes[0] = leftJn.jnArrayIndex;
                pn.jnIndexes[1] = rightJn.jnArrayIndex;
                pn.planIndexes[0] = leftPlan;
                pn.planIndexes[1] = rightPlan;
                pn.joinOp = PlanNode.JoinMethod.BROADCAST_HASH_JOIN; // need to check that all the conditions have equality predicates ONLY.
                if (hintBroadcastHashJoin != null) {
                    hintBroadcastHashJoin.setBroadcastSide(BroadcastExpressionAnnotation.BroadcastSide.RIGHT);
                }
                pn.side = HashJoinExpressionAnnotation.BuildSide.RIGHT;
                pn.joinExpr = hashJoinExpr;
                pn.opCost = bcastHjCost;
                pn.totalCost = totalCost;
                pn.leftExchangeCost = leftExchangeCost;
                pn.rightExchangeCost = rightExchangeCost;

                allPlans.add(pn);
                this.planIndexesArray.add(allPlans.size() - 1);
                this.cheapestPlanCost = totalCost;
                this.cheapestPlanIndex = allPlans.size() - 1;
                return this.cheapestPlanIndex;
            }
        }

        return PlanNode.NO_PLAN;
    }

    protected int buildNLJoinPlan(JoinNode leftJn, JoinNode rightJn, ILogicalExpression nestedLoopJoinExpr)
            throws AlgebricksException {
        // Build a nested loops plan, first check if it is possible
        // left right order must be preserved and right side should be a single data set
        List<PlanNode> allPlans = joinEnum.allPlans;
        int numberOfTerms = joinEnum.numberOfTerms;
        PlanNode pn;
        ICost nljCost, leftExchangeCost, rightExchangeCost, childCosts, totalCost;

        this.leftJn = leftJn;
        this.rightJn = rightJn;
        int leftPlan = leftJn.cheapestPlanIndex;
        int rightPlan = rightJn.cheapestPlanIndex;
        if (rightJn.jnArrayIndex > numberOfTerms) {
            // right side consists of more than one table
            return PlanNode.NO_PLAN; // nested loop plan not possible.
        }

        if (nestedLoopJoinExpr == null || !rightJn.nestedLoopsApplicable(nestedLoopJoinExpr)) {
            return PlanNode.NO_PLAN;
        }

        nljCost = joinEnum.getCostMethodsHandle().costIndexNLJoin(this);
        leftExchangeCost = joinEnum.getCostMethodsHandle().computeNLJOuterExchangeCost(this);
        rightExchangeCost = joinEnum.getCostHandle().zeroCost();
        childCosts = allPlans.get(leftPlan).totalCost;
        totalCost = nljCost.costAdd(leftExchangeCost).costAdd(childCosts);
        if (this.cheapestPlanIndex == PlanNode.NO_PLAN || totalCost.costLT(this.cheapestPlanCost)) {
            pn = new PlanNode(allPlans.size(), joinEnum);
            pn.jn = this;
            pn.jnIndexes[0] = leftJn.jnArrayIndex;
            pn.jnIndexes[1] = rightJn.jnArrayIndex;
            pn.planIndexes[0] = leftPlan;
            pn.planIndexes[1] = rightPlan;
            pn.joinOp = PlanNode.JoinMethod.INDEX_NESTED_LOOP_JOIN;
            pn.joinExpr = nestedLoopJoinExpr; // save it so can be used to add the NESTED annotation in getNewTree.
            pn.opCost = nljCost;
            pn.totalCost = totalCost;
            pn.leftExchangeCost = leftExchangeCost;
            pn.rightExchangeCost = rightExchangeCost;

            allPlans.add(pn);
            this.planIndexesArray.add(allPlans.size() - 1);
            this.cheapestPlanCost = totalCost;
            this.cheapestPlanIndex = allPlans.size() - 1;
            return allPlans.size() - 1;
        }
        return PlanNode.NO_PLAN;
    }

    protected int buildCPJoinPlan(JoinNode leftJn, JoinNode rightJn, ILogicalExpression hashJoinExpr,
            ILogicalExpression nestedLoopJoinExpr) {
        // Now build a cartesian product nested loops plan
        List<PlanNode> allPlans = joinEnum.allPlans;
        PlanNode pn;
        ICost cpCost, leftExchangeCost, rightExchangeCost, childCosts, totalCost;

        this.leftJn = leftJn;
        this.rightJn = rightJn;
        int leftPlan = leftJn.cheapestPlanIndex;
        int rightPlan = rightJn.cheapestPlanIndex;

        ILogicalExpression cpJoinExpr = null;
        List<Integer> newJoinConditions = this.getNewJoinConditionsOnly();
        if (hashJoinExpr == null && nestedLoopJoinExpr == null) {
            cpJoinExpr = joinEnum.combineAllConditions(newJoinConditions);
        } else if (hashJoinExpr != null && nestedLoopJoinExpr == null) {
            cpJoinExpr = hashJoinExpr;
        } else if (hashJoinExpr == null) {
            cpJoinExpr = nestedLoopJoinExpr;
        } else if (Objects.equals(hashJoinExpr, nestedLoopJoinExpr)) {
            cpJoinExpr = hashJoinExpr;
        } else {
            ScalarFunctionCallExpression andExpr = new ScalarFunctionCallExpression(
                    BuiltinFunctions.getBuiltinFunctionInfo(AlgebricksBuiltinFunctions.AND));
            andExpr.getArguments().add(new MutableObject<>(hashJoinExpr));
            andExpr.getArguments().add(new MutableObject<>(nestedLoopJoinExpr));
            cpJoinExpr = andExpr;
        }

        cpCost = joinEnum.getCostMethodsHandle().costCartesianProductJoin(this);
        leftExchangeCost = joinEnum.getCostHandle().zeroCost();
        rightExchangeCost = joinEnum.getCostMethodsHandle().computeCPRightExchangeCost(this);
        childCosts = allPlans.get(leftPlan).totalCost.costAdd(allPlans.get(rightPlan).totalCost);
        totalCost = cpCost.costAdd(rightExchangeCost).costAdd(childCosts);
        if (this.cheapestPlanIndex == PlanNode.NO_PLAN || totalCost.costLT(this.cheapestPlanCost)) {
            pn = new PlanNode(allPlans.size(), joinEnum);
            pn.jn = this;
            pn.jnIndexes[0] = leftJn.jnArrayIndex;
            pn.jnIndexes[1] = rightJn.jnArrayIndex;
            pn.planIndexes[0] = leftPlan;
            pn.planIndexes[1] = rightPlan;
            pn.joinOp = PlanNode.JoinMethod.CARTESIAN_PRODUCT_JOIN;
            pn.joinExpr = Objects.requireNonNullElse(cpJoinExpr, ConstantExpression.TRUE);
            pn.opCost = cpCost;
            pn.totalCost = totalCost;
            pn.leftExchangeCost = leftExchangeCost;
            pn.rightExchangeCost = rightExchangeCost;

            allPlans.add(pn);
            this.planIndexesArray.add(allPlans.size() - 1);
            this.cheapestPlanCost = totalCost;
            this.cheapestPlanIndex = allPlans.size() - 1;
            return allPlans.size() - 1;
        }
        return PlanNode.NO_PLAN;
    }

    protected Pair<Integer, ICost> addMultiDatasetPlans(JoinNode leftJn, JoinNode rightJn) throws AlgebricksException {
        this.leftJn = leftJn;
        this.rightJn = rightJn;
        ICost noJoinCost = joinEnum.getCostHandle().maxCost();

        if (leftJn.planIndexesArray.size() == 0 || rightJn.planIndexesArray.size() == 0) {
            return new Pair<>(PlanNode.NO_PLAN, noJoinCost);
        }

        if (this.cardinality >= Cost.MAX_CARD) {
            return new Pair<>(PlanNode.NO_PLAN, noJoinCost); // no card hint available, so do not add this plan
        }

        List<Integer> newJoinConditions = this.getNewJoinConditionsOnly(); // these will be a subset of applicable join conditions.
        ILogicalExpression hashJoinExpr = joinEnum.getHashJoinExpr(newJoinConditions);
        ILogicalExpression nestedLoopJoinExpr = joinEnum.getNestedLoopJoinExpr(newJoinConditions);

        if ((newJoinConditions.size() == 0) && joinEnum.connectedJoinGraph) {
            // at least one plan must be there at each level as the graph is fully connected.
            if (leftJn.cardinality * rightJn.cardinality > 10000.0) {
                return new Pair<>(PlanNode.NO_PLAN, noJoinCost);
            }
        }

        double current_card = this.cardinality;
        if (current_card >= Cost.MAX_CARD) {
            return new Pair<>(PlanNode.NO_PLAN, noJoinCost); // no card hint available, so do not add this plan
        }

        int hjPlan, commutativeHjPlan, bcastHjPlan, commutativeBcastHjPlan, nljPlan, commutativeNljPlan, cpPlan,
                commutativeCpPlan;
        hjPlan = commutativeHjPlan = bcastHjPlan =
                commutativeBcastHjPlan = nljPlan = commutativeNljPlan = cpPlan = commutativeCpPlan = PlanNode.NO_PLAN;

        HashJoinExpressionAnnotation hintHashJoin = joinEnum.findHashJoinHint(newJoinConditions);
        BroadcastExpressionAnnotation hintBroadcastHashJoin = joinEnum.findBroadcastHashJoinHint(newJoinConditions);
        IndexedNLJoinExpressionAnnotation hintNLJoin = joinEnum.findNLJoinHint(newJoinConditions);

        if (leftJn.cheapestPlanIndex == PlanNode.NO_PLAN || rightJn.cheapestPlanIndex == PlanNode.NO_PLAN) {
            return new Pair<>(PlanNode.NO_PLAN, noJoinCost);
        }

        if (hintHashJoin != null) {
            boolean build = (hintHashJoin.getBuildOrProbe() == HashJoinExpressionAnnotation.BuildOrProbe.BUILD);
            boolean probe = (hintHashJoin.getBuildOrProbe() == HashJoinExpressionAnnotation.BuildOrProbe.PROBE);
            boolean validBuildOrProbeObject = false;
            String buildOrProbeObject = hintHashJoin.getName();
            if (buildOrProbeObject != null && (rightJn.datasetNames.contains(buildOrProbeObject)
                    || rightJn.aliases.contains(buildOrProbeObject) || leftJn.datasetNames.contains(buildOrProbeObject)
                    || leftJn.aliases.contains(buildOrProbeObject))) {
                validBuildOrProbeObject = true;
            }
            if (validBuildOrProbeObject) {
                if ((build && (rightJn.datasetNames.contains(buildOrProbeObject)
                        || rightJn.aliases.contains(buildOrProbeObject)))
                        || (probe && (leftJn.datasetNames.contains(buildOrProbeObject)
                                || leftJn.aliases.contains(buildOrProbeObject)))) {
                    hjPlan = buildHashJoinPlan(leftJn, rightJn, hashJoinExpr, hintHashJoin);
                } else if ((build && (leftJn.datasetNames.contains(buildOrProbeObject)
                        || leftJn.aliases.contains(buildOrProbeObject)))
                        || (probe && (rightJn.datasetNames.contains(buildOrProbeObject)
                                || rightJn.aliases.contains(buildOrProbeObject)))) {
                    commutativeHjPlan = buildHashJoinPlan(rightJn, leftJn, hashJoinExpr, hintHashJoin);
                }
            } else {
                // Hints are attached to predicates, so newJoinConditions should not be empty, but adding the check to be safe.
                if (!joinEnum.getJoinConditions().isEmpty() && !newJoinConditions.isEmpty()) {
                    IWarningCollector warningCollector = joinEnum.optCtx.getWarningCollector();
                    if (warningCollector.shouldWarn()) {
                        warningCollector.warn(Warning.of(
                                joinEnum.getJoinConditions().get(newJoinConditions.get(0)).joinCondition
                                        .getSourceLocation(),
                                ErrorCode.INAPPLICABLE_HINT, "hash join",
                                (build ? "build " : "probe ") + "with " + buildOrProbeObject));
                    }
                }
                hjPlan = buildHashJoinPlan(leftJn, rightJn, hashJoinExpr, null);
                if (!joinEnum.forceJoinOrderMode) {
                    commutativeHjPlan = buildHashJoinPlan(rightJn, leftJn, hashJoinExpr, null);
                }
                bcastHjPlan = buildBroadcastHashJoinPlan(leftJn, rightJn, hashJoinExpr, null);
                if (!joinEnum.forceJoinOrderMode) {
                    commutativeBcastHjPlan = buildBroadcastHashJoinPlan(rightJn, leftJn, hashJoinExpr, null);
                }
                nljPlan = buildNLJoinPlan(leftJn, rightJn, nestedLoopJoinExpr);
                if (!joinEnum.forceJoinOrderMode) {
                    commutativeNljPlan = buildNLJoinPlan(rightJn, leftJn, nestedLoopJoinExpr);
                }
                cpPlan = buildCPJoinPlan(leftJn, rightJn, hashJoinExpr, nestedLoopJoinExpr);
                if (!joinEnum.forceJoinOrderMode) {
                    commutativeCpPlan = buildCPJoinPlan(rightJn, leftJn, hashJoinExpr, nestedLoopJoinExpr);
                }
            }
        } else if (hintBroadcastHashJoin != null) {
            boolean validBroadcastObject = false;
            String broadcastObject = hintBroadcastHashJoin.getName();
            if (broadcastObject != null && (rightJn.datasetNames.contains(broadcastObject)
                    || rightJn.aliases.contains(broadcastObject) || leftJn.datasetNames.contains(broadcastObject)
                    || leftJn.aliases.contains(broadcastObject))) {
                validBroadcastObject = true;
            }
            if (validBroadcastObject) {
                if (rightJn.datasetNames.contains(broadcastObject) || rightJn.aliases.contains(broadcastObject)) {
                    bcastHjPlan = buildBroadcastHashJoinPlan(leftJn, rightJn, hashJoinExpr, hintBroadcastHashJoin);
                } else if (leftJn.datasetNames.contains(broadcastObject) || leftJn.aliases.contains(broadcastObject)) {
                    commutativeBcastHjPlan =
                            buildBroadcastHashJoinPlan(rightJn, leftJn, hashJoinExpr, hintBroadcastHashJoin);
                }
            } else if (broadcastObject == null) {
                bcastHjPlan = buildBroadcastHashJoinPlan(leftJn, rightJn, hashJoinExpr, hintBroadcastHashJoin);
                if (!joinEnum.forceJoinOrderMode) {
                    commutativeBcastHjPlan =
                            buildBroadcastHashJoinPlan(rightJn, leftJn, hashJoinExpr, hintBroadcastHashJoin);
                }
            } else {
                // Hints are attached to predicates, so newJoinConditions should not be empty, but adding the check to be safe.
                if (!joinEnum.getJoinConditions().isEmpty() && !newJoinConditions.isEmpty()) {
                    IWarningCollector warningCollector = joinEnum.optCtx.getWarningCollector();
                    if (warningCollector.shouldWarn()) {
                        warningCollector.warn(Warning.of(
                                joinEnum.getJoinConditions().get(newJoinConditions.get(0)).joinCondition
                                        .getSourceLocation(),
                                ErrorCode.INAPPLICABLE_HINT, "broadcast hash join", "broadcast " + broadcastObject));
                    }
                }

                hjPlan = buildHashJoinPlan(leftJn, rightJn, hashJoinExpr, null);
                if (!joinEnum.forceJoinOrderMode) {
                    commutativeHjPlan = buildHashJoinPlan(rightJn, leftJn, hashJoinExpr, null);
                }
                bcastHjPlan = buildBroadcastHashJoinPlan(leftJn, rightJn, hashJoinExpr, null);
                if (!joinEnum.forceJoinOrderMode) {
                    commutativeBcastHjPlan = buildBroadcastHashJoinPlan(rightJn, leftJn, hashJoinExpr, null);
                }
                nljPlan = buildNLJoinPlan(leftJn, rightJn, nestedLoopJoinExpr);
                if (!joinEnum.forceJoinOrderMode) {
                    commutativeNljPlan = buildNLJoinPlan(rightJn, leftJn, nestedLoopJoinExpr);
                }
                cpPlan = buildCPJoinPlan(leftJn, rightJn, hashJoinExpr, nestedLoopJoinExpr);
                if (!joinEnum.forceJoinOrderMode) {
                    commutativeCpPlan = buildCPJoinPlan(rightJn, leftJn, hashJoinExpr, nestedLoopJoinExpr);
                }
            }
        } else if (hintNLJoin != null) {
            nljPlan = buildNLJoinPlan(leftJn, rightJn, nestedLoopJoinExpr);
            if (!joinEnum.forceJoinOrderMode) {
                commutativeNljPlan = buildNLJoinPlan(rightJn, leftJn, nestedLoopJoinExpr);
            }
            if (nljPlan == PlanNode.NO_PLAN && commutativeNljPlan == PlanNode.NO_PLAN) {
                // Hints are attached to predicates, so newJoinConditions should not be empty, but adding the check to be safe.
                if (!joinEnum.getJoinConditions().isEmpty() && !newJoinConditions.isEmpty()) {
                    IWarningCollector warningCollector = joinEnum.optCtx.getWarningCollector();
                    if (warningCollector.shouldWarn()) {
                        warningCollector.warn(Warning.of(
                                joinEnum.getJoinConditions().get(newJoinConditions.get(0)).joinCondition
                                        .getSourceLocation(),
                                ErrorCode.INAPPLICABLE_HINT, "index nested loop join", "ignored"));
                    }
                }
                hjPlan = buildHashJoinPlan(leftJn, rightJn, hashJoinExpr, null);
                if (!joinEnum.forceJoinOrderMode) {
                    commutativeHjPlan = buildHashJoinPlan(rightJn, leftJn, hashJoinExpr, null);
                }
                bcastHjPlan = buildBroadcastHashJoinPlan(leftJn, rightJn, hashJoinExpr, null);
                if (!joinEnum.forceJoinOrderMode) {
                    commutativeBcastHjPlan = buildBroadcastHashJoinPlan(rightJn, leftJn, hashJoinExpr, null);
                }
                cpPlan = buildCPJoinPlan(leftJn, rightJn, hashJoinExpr, nestedLoopJoinExpr);
                if (!joinEnum.forceJoinOrderMode) {
                    commutativeCpPlan = buildCPJoinPlan(rightJn, leftJn, hashJoinExpr, nestedLoopJoinExpr);
                }
            }
        } else {
            hjPlan = buildHashJoinPlan(leftJn, rightJn, hashJoinExpr, null);
            if (!joinEnum.forceJoinOrderMode) {
                commutativeHjPlan = buildHashJoinPlan(rightJn, leftJn, hashJoinExpr, null);
            }
            bcastHjPlan = buildBroadcastHashJoinPlan(leftJn, rightJn, hashJoinExpr, null);
            if (!joinEnum.forceJoinOrderMode) {
                commutativeBcastHjPlan = buildBroadcastHashJoinPlan(rightJn, leftJn, hashJoinExpr, null);
            }
            nljPlan = buildNLJoinPlan(leftJn, rightJn, nestedLoopJoinExpr);
            if (!joinEnum.forceJoinOrderMode) {
                commutativeNljPlan = buildNLJoinPlan(rightJn, leftJn, nestedLoopJoinExpr);
            }
            cpPlan = buildCPJoinPlan(leftJn, rightJn, hashJoinExpr, nestedLoopJoinExpr);
            if (!joinEnum.forceJoinOrderMode) {
                commutativeCpPlan = buildCPJoinPlan(rightJn, leftJn, hashJoinExpr, nestedLoopJoinExpr);
            }
        }

        if (hjPlan == PlanNode.NO_PLAN && commutativeHjPlan == PlanNode.NO_PLAN && bcastHjPlan == PlanNode.NO_PLAN
                && commutativeBcastHjPlan == PlanNode.NO_PLAN && nljPlan == PlanNode.NO_PLAN
                && commutativeNljPlan == PlanNode.NO_PLAN && cpPlan == PlanNode.NO_PLAN
                && commutativeCpPlan == PlanNode.NO_PLAN) {
            return new Pair<>(PlanNode.NO_PLAN, noJoinCost);
        }

        //Reset as these might have changed when we tried the commutative joins.
        this.leftJn = leftJn;
        this.rightJn = rightJn;

        return new Pair<>(this.cheapestPlanIndex, this.cheapestPlanCost);
    }

    @Override
    public String toString() {
        if (planIndexesArray.isEmpty()) {
            return "";
        }
        List<PlanNode> allPlans = joinEnum.allPlans;
        StringBuilder sb = new StringBuilder(128);
        // This will avoid printing JoinNodes that have no plans
        sb.append("Printing Join Node ").append(jnArrayIndex).append('\n');
        sb.append("datasetNames ").append('\n');
        for (String datasetName : datasetNames) {
            // Need to not print newline
            sb.append(datasetName).append(' ');
        }
        sb.append("datasetIndex ").append('\n');
        for (int j = 0; j < datasetIndexes.size(); j++) {
            sb.append(j).append(datasetIndexes.get(j)).append('\n');
        }
        sb.append("datasetBits is ").append(datasetBits).append('\n');
        if (IsBaseLevelJoinNode()) {
            sb.append("orig cardinality is ").append((double) Math.round(origCardinality * 100) / 100).append('\n');
        }
        sb.append("cardinality is ").append((double) Math.round(cardinality * 100) / 100).append('\n');
        if (planIndexesArray.size() == 0) {
            sb.append("No plans considered for this join node").append('\n');
        }
        for (int j = 0; j < planIndexesArray.size(); j++) {
            int k = planIndexesArray.get(j);
            PlanNode pn = allPlans.get(k);
            sb.append("planIndexesArray  [").append(j).append("] is ").append(k).append('\n');
            sb.append("Printing PlanNode ").append(k).append('\n');
            if (IsBaseLevelJoinNode()) {
                sb.append("DATA_SOURCE_SCAN").append('\n');
            } else {
                sb.append("\n");
                sb.append(pn.joinMethod().getFirst()).append('\n');
                sb.append("Printing Join expr ").append('\n');
                if (pn.joinExpr != null) {
                    sb.append(pn.joinExpr).append('\n');
                } else {
                    sb.append("null").append('\n');
                }
            }
            sb.append("card ").append((double) Math.round(cardinality * 100) / 100).append('\n');
            sb.append("------------------").append('\n');
            sb.append("operator cost ").append(pn.opCost.computeTotalCost()).append('\n');
            sb.append("total cost ").append(pn.totalCost.computeTotalCost()).append('\n');
            sb.append("jnIndexes ").append(pn.jnIndexes[0]).append(" ").append(pn.jnIndexes[1]).append('\n');
            if (IsHigherLevelJoinNode()) {
                PlanNode leftPlan = pn.getLeftPlanNode();
                PlanNode rightPlan = pn.getRightPlanNode();
                int l = leftPlan.allPlansIndex;
                int r = rightPlan.allPlansIndex;
                sb.append("planIndexes ").append(l).append(" ").append(r).append('\n');
                sb.append("(lcost = ").append(leftPlan.totalCost.computeTotalCost()).append(") (rcost = ")
                        .append(rightPlan.totalCost.computeTotalCost()).append(")").append('\n');
            }
            sb.append("\n");
        }
        sb.append("jnIndex ").append(jnIndex).append('\n');
        sb.append("datasetBits ").append(datasetBits).append('\n');
        sb.append("cardinality ").append((double) Math.round(cardinality * 100) / 100).append('\n');
        sb.append("size ").append((double) Math.round(size * 100) / 100).append('\n');
        sb.append("level ").append(level).append('\n');
        sb.append("highestDatasetId ").append(highestDatasetId).append('\n');
        sb.append("--------------------------------------").append('\n');
        return sb.toString();
    }

    public void printCostOfAllPlans(StringBuilder sb) {
        List<PlanNode> allPlans = joinEnum.allPlans;
        ICost minCost = joinEnum.getCostHandle().maxCost();
        for (int planIndex : planIndexesArray) {
            ICost planCost = allPlans.get(planIndex).totalCost;
            sb.append("plan ").append(planIndex).append(" cost is ").append(planCost.computeTotalCost()).append('\n');
            if (planCost.costLT(minCost)) {
                minCost = planCost;
            }
        }
        sb.append("LOWEST COST ").append(minCost.computeTotalCost()).append('\n');
    }
}
