TableRef.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.
// This file is copied from
// https://github.com/apache/impala/blob/branch-2.9.0/fe/src/main/java/org/apache/impala/TableRef.java
// and modified by Doris

package org.apache.doris.analysis;

import org.apache.doris.catalog.Env;
import org.apache.doris.catalog.TableIf;
import org.apache.doris.catalog.Type;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.ErrorCode;
import org.apache.doris.common.ErrorReport;
import org.apache.doris.common.FeMetaVersion;
import org.apache.doris.common.Pair;
import org.apache.doris.common.UserException;
import org.apache.doris.common.io.Text;
import org.apache.doris.common.io.Writable;
import org.apache.doris.common.util.TimeUtils;
import org.apache.doris.datasource.hive.HMSExternalTable;
import org.apache.doris.datasource.hudi.HudiUtils;
import org.apache.doris.persist.gson.GsonUtils;
import org.apache.doris.rewrite.ExprRewriter;
import org.apache.doris.rewrite.ExprRewriter.ClauseType;

import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.gson.annotations.SerializedName;
import org.apache.commons.collections.CollectionUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.StringJoiner;
import java.util.regex.Matcher;

/**
 * Superclass of all table references, including references to views, base tables
 * (Hdfs, HBase or DataSource tables), and nested collections. Contains the join
 * specification. An instance of a TableRef (and not a subclass thereof) represents
 * an unresolved table reference that must be resolved during analysis. All resolved
 * table references are subclasses of TableRef.
 *
 * The analysis of table refs follows a two-step process:
 *
 * 1. Resolution: A table ref's path is resolved and then the generic TableRef is
 * replaced by a concrete table ref (a BaseTableRef, CollectionTableRef or ViewRef)
 * in the originating stmt and that is given the resolved path. This step is driven by
 * Analyzer.resolveTableRef().
 *
 * 2. Analysis/registration: After resolution, the concrete table ref is analyzed
 * to register a tuple descriptor for its resolved path and register other table-ref
 * specific state with the analyzer (e.g., whether it is outer/semi joined, etc.).
 *
 * Therefore, subclasses of TableRef should never call the analyze() of its superclass.
 *
 * TODO for 2.3: The current TableRef class hierarchy and the related two-phase analysis
 * feels convoluted and is hard to follow. We should reorganize the TableRef class
 * structure for clarity of analysis and avoid a table ref 'switching genders' in between
 * resolution and registration.
 *
 * TODO for 2.3: Rename this class to CollectionRef and re-consider the naming and
 * structure of all subclasses.
 */
public class TableRef implements ParseNode, Writable {
    private static final Logger LOG = LogManager.getLogger(TableRef.class);
    @SerializedName("n")
    protected TableName name;
    // Legal aliases of this table ref. Contains the explicit alias as its sole element if
    // there is one. Otherwise, contains the two implicit aliases. Implicit aliases are set
    // in the c'tor of the corresponding resolved table ref (subclasses of TableRef) during
    // analysis. By convention, for table refs with multiple implicit aliases, aliases_[0]
    // contains the fully-qualified implicit alias to ensure that aliases_[0] always
    // uniquely identifies this table ref regardless of whether it has an explicit alias.
    @SerializedName("a")
    protected String[] aliases;
    protected List<Long> sampleTabletIds;
    // Indicates whether this table ref is given an explicit alias,
    protected boolean hasExplicitAlias;
    protected JoinOperator joinOp;
    protected boolean isInBitmap;
    // for mark join
    protected boolean isMark;
    // we must record mark tuple name for re-analyze
    protected String markTupleName;

    protected List<String> usingColNames;
    protected ArrayList<LateralViewRef> lateralViewRefs;
    protected Expr onClause;
    // the ref to the left of us, if we're part of a JOIN clause
    protected TableRef leftTblRef;
    protected TableSample tableSample;

    // true if this TableRef has been analyzed; implementing subclass should set it to true
    // at the end of analyze() call.
    protected boolean isAnalyzed;
    // Lists of table ref ids and materialized tuple ids of the full sequence of table
    // refs up to and including this one. These ids are cached during analysis because
    // we may alter the chain of table refs during plan generation, but we still rely
    // on the original list of ids for correct predicate assignment.
    // Populated in analyzeJoin().
    protected List<TupleId> allTableRefIds = Lists.newArrayList();
    protected List<TupleId> allMaterializedTupleIds = Lists.newArrayList();
    // ///////////////////////////////////////
    // BEGIN: Members that need to be reset()
    // All physical tuple ids that this table ref is correlated with:
    // Tuple ids of root descriptors from outer query blocks that this table ref
    // (if a CollectionTableRef) or contained CollectionTableRefs (if an InlineViewRef)
    // are rooted at. Populated during analysis.
    protected List<TupleId> correlatedTupleIds = Lists.newArrayList();
    // analysis output
    protected TupleDescriptor desc;
    @SerializedName("p")
    private PartitionNames partitionNames = null;
    private ArrayList<String> joinHints;
    private ArrayList<String> sortHints;
    private ArrayList<String> commonHints; //The Hints is set by user
    private boolean isForcePreAggOpened;
    // set after analyzeJoinHints(); true if explicitly set via hints
    private boolean isBroadcastJoin;
    private boolean isPartitionJoin;
    private String sortColumn = null;

    private TableSnapshot tableSnapshot;

    private TableScanParams scanParams;

    // END: Members that need to be reset()
    // ///////////////////////////////////////

    public TableRef() {
        // for persist
    }

    public TableRef(TableName name, String alias) {
        this(name, alias, null);
    }

    public TableRef(TableName name, String alias, PartitionNames partitionNames) {
        this(name, alias, partitionNames, null);
    }

    public TableRef(TableName name, String alias, PartitionNames partitionNames, ArrayList<String> commonHints) {
        this(name, alias, partitionNames, null, null, commonHints);
    }

    /**
     * This method construct TableRef.
     */
    public TableRef(TableName name, String alias, PartitionNames partitionNames, ArrayList<Long> sampleTabletIds,
                    TableSample tableSample, ArrayList<String> commonHints) {
        this(name, alias, partitionNames, sampleTabletIds, tableSample, commonHints, null);
    }

    public TableRef(TableName name, String alias, PartitionNames partitionNames, ArrayList<Long> sampleTabletIds,
                    TableSample tableSample, ArrayList<String> commonHints, TableSnapshot tableSnapshot) {
        this(name, alias, partitionNames, sampleTabletIds, tableSample, commonHints, tableSnapshot, null);
    }

    public TableRef(TableName name, String alias, PartitionNames partitionNames,
                    ArrayList<Long> sampleTabletIds, TableSample tableSample, ArrayList<String> commonHints,
                    TableSnapshot tableSnapshot, TableScanParams scanParams) {
        this.name = name;
        if (alias != null) {
            if (Env.isStoredTableNamesLowerCase()) {
                alias = alias.toLowerCase();
            }
            aliases = new String[]{alias};
            hasExplicitAlias = true;
        } else {
            hasExplicitAlias = false;
        }
        this.partitionNames = partitionNames;
        this.sampleTabletIds = sampleTabletIds;
        this.tableSample = tableSample;
        this.commonHints = commonHints;
        this.tableSnapshot = tableSnapshot;
        this.scanParams = scanParams;
        isAnalyzed = false;
    }

    // Only used to clone
    // this will reset all the 'analyzed' stuff
    protected TableRef(TableRef other) {
        name = other.name;
        aliases = other.aliases;
        hasExplicitAlias = other.hasExplicitAlias;
        joinOp = other.joinOp;
        isMark = other.isMark;
        markTupleName = other.markTupleName;
        // NOTE: joinHints and sortHints maybe changed after clone. so we new one List.
        joinHints =
                (other.joinHints != null) ? Lists.newArrayList(other.joinHints) : null;
        sortHints =
                (other.sortHints != null) ? Lists.newArrayList(other.sortHints) : null;
        onClause = (other.onClause != null) ? other.onClause.clone().reset() : null;
        partitionNames = (other.partitionNames != null) ? new PartitionNames(other.partitionNames) : null;
        tableSnapshot = (other.tableSnapshot != null) ? new TableSnapshot(other.tableSnapshot) : null;
        scanParams = other.scanParams;
        tableSample = (other.tableSample != null) ? new TableSample(other.tableSample) : null;
        commonHints = other.commonHints;

        usingColNames =
                (other.usingColNames != null) ? Lists.newArrayList(other.usingColNames) : null;
        // The table ref links are created at the statement level, so cloning a set of linked
        // table refs is the responsibility of the statement.
        leftTblRef = null;
        isAnalyzed = other.isAnalyzed;
        allTableRefIds = Lists.newArrayList(other.allTableRefIds);
        allMaterializedTupleIds = Lists.newArrayList(other.allMaterializedTupleIds);
        correlatedTupleIds = Lists.newArrayList(other.correlatedTupleIds);
        desc = other.desc;
        lateralViewRefs = null;
        if (other.lateralViewRefs != null) {
            lateralViewRefs = Lists.newArrayList();
            for (LateralViewRef viewRef : other.lateralViewRefs) {
                lateralViewRefs.add((LateralViewRef) viewRef.clone());
            }
        }
        this.sampleTabletIds = other.sampleTabletIds;
    }

    public PartitionNames getPartitionNames() {
        return partitionNames;
    }

    @Override
    public void analyze(Analyzer analyzer) throws AnalysisException, UserException {
        ErrorReport.reportAnalysisException(ErrorCode.ERR_UNRESOLVED_TABLE_REF, tableRefToSql());
    }

    @Override
    public String toSql() {
        if (joinOp == null) {
            // prepend "," if we're part of a sequence of table refs w/o an
            // explicit JOIN clause
            return (leftTblRef != null ? ", " : "") + tableRefToSql();
        }

        StringBuilder output = new StringBuilder(" " + joinOpToSql() + " ");
        if (joinHints != null && !joinHints.isEmpty()) {
            output.append("[").append(Joiner.on(", ").join(joinHints)).append("] ");
        }
        output.append(tableRefToSql()).append(" ");
        if (usingColNames != null) {
            output.append("USING (").append(Joiner.on(", ").join(usingColNames)).append(")");
        } else if (onClause != null) {
            output.append("ON ").append(onClause.toSql());
        }
        return output.toString();
    }

    /**
     * Creates and returns a empty TupleDescriptor registered with the analyzer. The
     * returned tuple descriptor must have its source table set via descTbl.setTable()).
     * This method is called from the analyzer when registering this table reference.
     */
    public TupleDescriptor createTupleDescriptor(Analyzer analyzer) throws AnalysisException {
        ErrorReport.reportAnalysisException(ErrorCode.ERR_UNRESOLVED_TABLE_REF, tableRefToSql());
        return null;
    }

    public JoinOperator getJoinOp() {
        // if it's not explicitly set, we're doing an inner join
        return (joinOp == null ? JoinOperator.INNER_JOIN : joinOp);
    }

    public void setJoinOp(JoinOperator op) {
        this.joinOp = op;
    }

    public boolean isInBitmap() {
        return isInBitmap;
    }

    public void setInBitmap(boolean inBitmap) {
        isInBitmap = inBitmap;
    }

    public boolean isMark() {
        return isMark;
    }

    public String getMarkTupleName() {
        return markTupleName;
    }

    public void setMark(TupleDescriptor markTuple) {
        this.isMark = markTuple != null;
        if (isMark) {
            this.markTupleName = markTuple.getAlias();
        } else {
            this.markTupleName = null;
        }
    }

    public Expr getOnClause() {
        return onClause;
    }

    public void setOnClause(Expr e) {
        this.onClause = e;
    }

    public TableName getName() {
        return name;
    }

    public List<Long> getSampleTabletIds() {
        return sampleTabletIds;
    }

    public ArrayList<String> getCommonHints() {
        return commonHints;
    }

    public TableSample getTableSample() {
        return tableSample;
    }

    public TableSnapshot getTableSnapshot() {
        return tableSnapshot;
    }

    public Boolean haveDesc() {
        return desc != null;
    }

    public TableScanParams getScanParams() {
        return scanParams;
    }

    /**
     * This method should only be called after the TableRef has been analyzed.
     */
    public TupleDescriptor getDesc() {
        Preconditions.checkState(isAnalyzed);
        // after analyze(), desc should be set.
        Preconditions.checkState(desc != null);
        return desc;
    }

    /**
     * This method should only be called after the TableRef has been analyzed.
     */
    public TupleId getId() {
        Preconditions.checkState(isAnalyzed);
        // after analyze(), desc should be set.
        Preconditions.checkState(desc != null);
        return desc.getId();
    }

    /**
     * Return the list of materialized tuple ids from the TableRef.
     * This method should only be called after the TableRef has been analyzed.
     */
    public List<TupleId> getMaterializedTupleIds() {
        // This function should only be called after analyze().
        Preconditions.checkState(isAnalyzed);
        Preconditions.checkNotNull(desc);
        return desc.getId().asList();
    }

    /**
     * Return the list of tuple ids materialized by the full sequence of
     * table refs up to this one.
     */
    public List<TupleId> getAllMaterializedTupleIds() {
        if (leftTblRef != null) {
            List<TupleId> result = Lists.newArrayList(leftTblRef.getAllMaterializedTupleIds());
            result.addAll(getMaterializedTupleIds());
            return result;
        } else {
            return getMaterializedTupleIds();
        }
    }

    /**
     * Returns true if this table ref has a resolved path that is rooted at a registered
     * tuple descriptor, false otherwise.
     */
    public boolean isRelative() {
        return false;
    }

    /**
     * Indicates if this TableRef directly or indirectly references another TableRef from
     * an outer query block.
     */
    public boolean isCorrelated() {
        return !correlatedTupleIds.isEmpty();
    }

    public TableIf getTable() {
        return desc.getTable();
    }

    public List<String> getUsingClause() {
        return this.usingColNames;
    }

    public void setUsingClause(List<String> colNames) {
        this.usingColNames = colNames;
    }

    public TableRef getLeftTblRef() {
        return leftTblRef;
    }

    public void setLeftTblRef(TableRef leftTblRef) {
        this.leftTblRef = leftTblRef;
    }

    public ArrayList<String> getJoinHints() {
        return joinHints;
    }

    public void setJoinHints(ArrayList<String> hints) {
        this.joinHints = hints;
    }

    public boolean hasJoinHints() {
        return CollectionUtils.isNotEmpty(joinHints);
    }

    public boolean isBroadcastJoin() {
        return isBroadcastJoin;
    }

    public boolean isPartitionJoin() {
        return isPartitionJoin;
    }

    public boolean isForcePreAggOpened() {
        return isForcePreAggOpened;
    }

    public void setSortHints(ArrayList<String> hints) {
        this.sortHints = hints;
    }

    public String getSortColumn() {
        return sortColumn;
    }

    public ArrayList<LateralViewRef> getLateralViewRefs() {
        return lateralViewRefs;
    }

    public void setLateralViewRefs(ArrayList<LateralViewRef> lateralViewRefs) {
        this.lateralViewRefs = lateralViewRefs;
    }

    protected void analyzeLateralViewRef(Analyzer analyzer) throws UserException {
        if (lateralViewRefs == null) {
            return;
        }
        for (LateralViewRef lateralViewRef : lateralViewRefs) {
            lateralViewRef.setRelatedTable(this);
            lateralViewRef.analyze(analyzer);
        }
    }

    protected void analyzeSortHints() throws AnalysisException {
        if (sortHints == null) {
            return;
        }
        for (String hint : sortHints) {
            sortColumn = hint;
        }
    }

    protected void analyzeSample() throws AnalysisException {
        if ((sampleTabletIds != null || tableSample != null)
                && desc.getTable().getType() != TableIf.TableType.OLAP
                && desc.getTable().getType() != TableIf.TableType.HMS_EXTERNAL_TABLE) {
            throw new AnalysisException("Sample table " + desc.getTable().getName()
                + " type " + desc.getTable().getType() + " is not supported");
        }
    }

    /**
     * Parse hints.
     */
    private void analyzeJoinHints() throws AnalysisException {
        if (joinHints == null) {
            return;
        }
        for (String hint : joinHints) {
            if (hint.toUpperCase().equals("BROADCAST")) {
                if (joinOp == JoinOperator.RIGHT_OUTER_JOIN
                        || joinOp == JoinOperator.FULL_OUTER_JOIN
                        || joinOp == JoinOperator.RIGHT_SEMI_JOIN
                        || joinOp == JoinOperator.RIGHT_ANTI_JOIN) {
                    throw new AnalysisException(
                            joinOp.toString() + " does not support BROADCAST.");
                }
                if (isPartitionJoin) {
                    throw new AnalysisException("Conflicting JOIN hint: " + hint);
                }
                isBroadcastJoin = true;
            } else if (hint.toUpperCase().equals("SHUFFLE")) {
                if (joinOp == JoinOperator.CROSS_JOIN) {
                    throw new AnalysisException("CROSS JOIN does not support SHUFFLE.");
                }
                if (isBroadcastJoin) {
                    throw new AnalysisException("Conflicting JOIN hint: " + hint);
                }
                isPartitionJoin = true;
            } else {
                throw new AnalysisException("JOIN hint not recognized: " + hint);
            }
        }
    }

    /**
     * Parse PreAgg hints.
     */
    protected void analyzeHints() throws AnalysisException {
        if (commonHints == null || commonHints.isEmpty()) {
            return;
        }
        // Currently only 'PREAGGOPEN' is supported
        for (String hint : commonHints) {
            if (hint.toUpperCase().equals("PREAGGOPEN")) {
                isForcePreAggOpened = true;
                break;
            }
        }
    }

    protected void analyzeTableSnapshot(Analyzer analyzer) throws AnalysisException {
        if (tableSnapshot == null) {
            return;
        }
        TableIf.TableType tableType = this.getTable().getType();
        if (tableType == TableIf.TableType.HMS_EXTERNAL_TABLE) {
            HMSExternalTable extTable = (HMSExternalTable) this.getTable();
            switch (extTable.getDlaType()) {
                case ICEBERG:
                    if (tableSnapshot.getType() == TableSnapshot.VersionType.TIME) {
                        String asOfTime = tableSnapshot.getTime();
                        Matcher matcher = TimeUtils.DATETIME_FORMAT_REG.matcher(asOfTime);
                        if (!matcher.matches()) {
                            throw new AnalysisException("Invalid datetime string: " + asOfTime);
                        }
                    }
                    break;
                case HUDI:
                    if (tableSnapshot.getType() == TableSnapshot.VersionType.VERSION) {
                        throw new AnalysisException("Hudi table only supports timestamp as snapshot ID");
                    }
                    try {
                        tableSnapshot.setTime(HudiUtils.formatQueryInstant(tableSnapshot.getTime()));
                    } catch (Exception e) {
                        throw new AnalysisException("Failed to parse hudi timestamp: " + e.getMessage(), e);
                    }
                    break;
                default:
                    ErrorReport.reportAnalysisException(ErrorCode.ERR_NONSUPPORT_TIME_TRAVEL_TABLE);
            }
        } else if (tableType == TableIf.TableType.ICEBERG_EXTERNAL_TABLE) {
            if (tableSnapshot.getType() == TableSnapshot.VersionType.TIME) {
                String asOfTime = tableSnapshot.getTime();
                Matcher matcher = TimeUtils.DATETIME_FORMAT_REG.matcher(asOfTime);
                if (!matcher.matches()) {
                    throw new AnalysisException("Invalid datetime string: " + asOfTime);
                }
            }
        } else {
            ErrorReport.reportAnalysisException(ErrorCode.ERR_NONSUPPORT_TIME_TRAVEL_TABLE);
        }
    }

    /**
     * Analyze the join clause.
     * The join clause can only be analyzed after the left table has been analyzed
     * and the TupleDescriptor (desc) of this table has been created.
     */
    public void analyzeJoin(Analyzer analyzer) throws AnalysisException {
        Preconditions.checkState(leftTblRef == null || leftTblRef.isAnalyzed);
        Preconditions.checkState(desc != null);
        analyzeJoinHints();

        // Populate the lists of all table ref and materialized tuple ids.
        allTableRefIds.clear();
        allMaterializedTupleIds.clear();
        if (leftTblRef != null) {
            allTableRefIds.addAll(leftTblRef.getAllTableRefIds());
            allMaterializedTupleIds.addAll(leftTblRef.getAllMaterializedTupleIds());
        }
        allTableRefIds.add(getId());
        allMaterializedTupleIds.addAll(getMaterializedTupleIds());

        if (usingColNames != null) {
            // Turn USING clause into equivalent ON clause.
            Preconditions.checkState(onClause == null);
            for (String colName : usingColNames) {
                // check whether colName exists both for our table and the one
                // to the left of us
                if (leftTblRef.getDesc().getTable().getColumn(colName) == null) {
                    throw new AnalysisException("Unknown column " + colName + " for alias " + leftTblRef.getAlias()
                            + " (in" + " \"" + this.toSql() + "\")");
                }
                if (desc.getTable().getColumn(colName) == null) {
                    throw new AnalysisException("Unknown column " + colName + " for alias " + getAlias() + " (in \""
                            + this.toSql() + "\")");
                }

                // create predicate "<left>.colName = <right>.colName"
                BinaryPredicate eqPred = new BinaryPredicate(BinaryPredicate.Operator.EQ,
                        new SlotRef(leftTblRef.getAliasAsName(), colName),
                        new SlotRef(getAliasAsName(), colName));
                if (onClause == null) {
                    onClause = eqPred;
                } else {
                    onClause = new CompoundPredicate(CompoundPredicate.Operator.AND, onClause, eqPred);
                }
            }
        }

        //
        if (leftTblRef != null) {
            for (TupleId tupleId : leftTblRef.getAllTableRefIds()) {
                Pair<TupleId, TupleId> tids = Pair.of(tupleId, getId());
                analyzer.registerAnyTwoTalesJoinOperator(tids, joinOp);
            }
        }

        // at this point, both 'this' and leftTblRef have been analyzed and registered;
        // register the tuple ids of the TableRefs on the nullable side of an outer join
        if (joinOp == JoinOperator.LEFT_OUTER_JOIN
                || joinOp == JoinOperator.FULL_OUTER_JOIN) {
            analyzer.registerOuterJoinedTids(getId().asList(), this);
            analyzer.registerOuterJoinedRightSideTids(getId().asList());
        }
        if (joinOp == JoinOperator.RIGHT_OUTER_JOIN
                || joinOp == JoinOperator.FULL_OUTER_JOIN) {
            analyzer.registerOuterJoinedTids(leftTblRef.getAllTableRefIds(), this);
            analyzer.registerOuterJoinedLeftSideTids(leftTblRef.getAllTableRefIds());
        }
        // register the tuple ids of a full outer join
        if (joinOp == JoinOperator.FULL_OUTER_JOIN) {
            analyzer.registerFullOuterJoinedTids(leftTblRef.getAllTableRefIds(), this);
            analyzer.registerFullOuterJoinedTids(getId().asList(), this);
        }

        // register the tuple id of the rhs of a left semi join
        TupleId semiJoinedTupleId = null;
        if (joinOp == JoinOperator.LEFT_SEMI_JOIN
                || joinOp == JoinOperator.LEFT_ANTI_JOIN
                || joinOp == JoinOperator.NULL_AWARE_LEFT_ANTI_JOIN) {
            analyzer.registerSemiJoinedTid(getId(), this);
            semiJoinedTupleId = getId();
        }
        // register the tuple id of the lhs of a right semi join
        if (joinOp == JoinOperator.RIGHT_SEMI_JOIN
                || joinOp == JoinOperator.RIGHT_ANTI_JOIN) {
            analyzer.registerSemiJoinedTid(leftTblRef.getId(), this);
            semiJoinedTupleId = leftTblRef.getId();
        }

        //  right anti join use shuffle , basecase broadcast join can't support right anti
        if (joinOp == JoinOperator.RIGHT_ANTI_JOIN || joinOp == JoinOperator.RIGHT_SEMI_JOIN) {
            isPartitionJoin = true;
            isBroadcastJoin = false;
        }

        // cross join can't be used with ON clause
        if (onClause != null && joinOp == JoinOperator.CROSS_JOIN) {
            throw new AnalysisException("Cross join can't be used with ON clause");
        }

        if (onClause != null) {
            analyzer.setVisibleSemiJoinedTuple(semiJoinedTupleId);
            onClause.analyze(analyzer);
            analyzer.setVisibleSemiJoinedTuple(null);
            if (!onClause.getType().isBoolean()) {
                onClause = onClause.castTo(Type.BOOLEAN);
            }
            onClause.checkReturnsBool("ON clause", true);
            if (onClause.contains(Expr.isAggregatePredicate())) {
                throw new AnalysisException(
                        "aggregate function not allowed in ON clause: " + toSql());
            }
            if (onClause.contains(AnalyticExpr.class)) {
                throw new AnalysisException(
                        "analytic expression not allowed in ON clause: " + toSql());
            }
            Set<TupleId> onClauseTupleIds = Sets.newHashSet();
            List<Expr> conjuncts = onClause.getConjuncts();
            // Outer join clause conjuncts are registered for this particular table ref
            // (ie, can only be evaluated by the plan node that implements this join).
            // The exception are conjuncts that only pertain to the nullable side
            // of the outer join; those can be evaluated directly when materializing tuples
            // without violating outer join semantics.
            analyzer.registerOnClauseConjuncts(conjuncts, this);
            for (Expr e : conjuncts) {
                List<TupleId> tupleIds = Lists.newArrayList();
                e.getIds(tupleIds, null);
                onClauseTupleIds.addAll(tupleIds);
            }
        } else if (!isRelative() && !isCorrelated()
                && (getJoinOp().isOuterJoin() || getJoinOp().isSemiJoin())) {
            throw new AnalysisException(
                    joinOp.toString() + " requires an ON or USING clause.");
        } else {
            // Indicate that this table ref has an empty ON-clause.
            analyzer.registerOnClauseConjuncts(Collections.<Expr>emptyList(), this);
        }

        if (lateralViewRefs != null) {
            for (LateralViewRef lateralViewRef : lateralViewRefs) {
                allTableRefIds.add(lateralViewRef.getId());
            }
        }
    }

    public void rewriteExprs(ExprRewriter rewriter, Analyzer analyzer)
            throws AnalysisException {
        Preconditions.checkState(isAnalyzed);
        if (onClause != null) {
            Expr expr = onClause.clone();
            onClause = rewriter.rewrite(onClause, analyzer, ClauseType.fromJoinType(joinOp));
            if (joinOp.isOuterJoin() || joinOp.isSemiAntiJoin()) {
                if (onClause instanceof BoolLiteral && !((BoolLiteral) onClause).getValue()) {
                    onClause = expr;
                }
            }
        }
    }

    private String joinOpToSql() {
        Preconditions.checkState(joinOp != null);
        switch (joinOp) {
            case INNER_JOIN:
                return "INNER JOIN";
            case LEFT_OUTER_JOIN:
                return "LEFT OUTER JOIN";
            case LEFT_SEMI_JOIN:
                return "LEFT SEMI JOIN";
            case LEFT_ANTI_JOIN:
                return "LEFT ANTI JOIN";
            case RIGHT_SEMI_JOIN:
                return "RIGHT SEMI JOIN";
            case RIGHT_ANTI_JOIN:
                return "RIGHT ANTI JOIN";
            case RIGHT_OUTER_JOIN:
                return "RIGHT OUTER JOIN";
            case FULL_OUTER_JOIN:
                return "FULL OUTER JOIN";
            case CROSS_JOIN:
                return "CROSS JOIN";
            case NULL_AWARE_LEFT_ANTI_JOIN:
                return "NULL AWARE LEFT ANTI JOIN";
            default:
                return "bad join op: " + joinOp;
        }
    }

    /**
     * Return the list of table ref ids of the full sequence of table refs up to
     * and including this one.
     */
    public List<TupleId> getAllTableRefIds() {
        Preconditions.checkState(isAnalyzed);
        return allTableRefIds;
    }

    /**
     * Return the table ref presentation to be used in the toSql string
     */
    // tbl1
    // tbl1 alias_tbl1
    // tbl1 alias_tbl1 lateral view explode_split(k1, ",") tmp1 as e1
    // (select xxx from xxx) t1 alias_tbl1 xxx
    public String tableRefToSql() {
        String tblName = tableNameToSql();
        if (lateralViewRefs != null) {
            for (LateralViewRef viewRef : lateralViewRefs) {
                tblName += " " + viewRef.toSql();
            }
        }
        if (partitionNames != null) {
            StringJoiner sj = new StringJoiner(",", "", " ");
            for (String partName : partitionNames.getPartitionNames()) {
                sj.add(partName);
            }
            return tblName + " PARTITION(" + sj.toString() + ")";
        }
        return tblName;
    }

    protected String tableNameToSql() {
        String aliasSql = null;
        String alias = getExplicitAlias();
        if (alias != null) {
            aliasSql = ToSqlUtils.getIdentSql(alias);
        }
        String tblName = name.toSql() + ((aliasSql != null) ? " " + aliasSql : "");
        return tblName;
    }

    public String tableRefToDigest() {
        return tableRefToSql();
    }

    public String toDigest() {
        if (joinOp == null) {
            // prepend "," if we're part of a sequence of table refs w/o an
            // explicit JOIN clause
            return (leftTblRef != null ? ", " : "") + tableRefToDigest();
        }

        StringBuilder output = new StringBuilder(" " + joinOpToSql() + " ");
        if (joinHints != null && !joinHints.isEmpty()) {
            output.append("[").append(Joiner.on(", ").join(joinHints)).append("] ");
        }
        output.append(tableRefToDigest()).append(" ");
        if (usingColNames != null) {
            output.append("USING (").append(Joiner.on(", ").join(usingColNames)).append(")");
        } else if (onClause != null) {
            output.append("ON ").append(onClause.toDigest());
        }
        return output.toString();
    }

    public String getAlias() {
        if (!hasExplicitAlias()) {
            return name.toString();
        }
        return getUniqueAlias();
    }

    public TableName getAliasAsName() {
        if (hasExplicitAlias()) {
            return new TableName(null, null, getUniqueAlias());
        }
        return name;
    }

    /**
     * Returns all legal aliases of this table ref.
     */
    public String[] getAliases() {
        return aliases;
    }

    /**
     * Returns the explicit alias or the fully-qualified implicit alias. The returned alias
     * is guaranteed to be unique (i.e., column/field references against the alias cannot
     * be ambiguous).
     */
    public String getUniqueAlias() {
        return aliases[0];
    }

    /**
     * Returns true if this table ref has an explicit alias.
     * Note that getAliases().length() == 1 does not imply an explicit alias because
     * nested collection refs have only a single implicit alias.
     */
    public boolean hasExplicitAlias() {
        return hasExplicitAlias;
    }

    /**
     * Returns the explicit alias if this table ref has one, null otherwise.
     */
    public String getExplicitAlias() {
        if (hasExplicitAlias()) {
            return getUniqueAlias();
        }
        return null;
    }

    public boolean isAnalyzed() {
        return isAnalyzed;
    }

    public boolean isResolved() {
        return !getClass().equals(TableRef.class);
    }

    /**
     * Return the list of tuple ids of the full sequence of table refs up to this one.
     */
    public List<TupleId> getAllTupleIds() {
        Preconditions.checkState(isAnalyzed);
        if (leftTblRef != null) {
            List<TupleId> result = leftTblRef.getAllTupleIds();
            result.add(desc.getId());
            return result;
        } else {
            return Lists.newArrayList(desc.getId());
        }
    }

    /**
     * Set this table's context-dependent join attributes from the given table.
     * Does not clone the attributes.
     */
    protected void setJoinAttrs(TableRef other) {
        this.joinOp = other.joinOp;
        this.isMark = other.isMark;
        this.markTupleName = other.markTupleName;
        this.joinHints = other.joinHints;
        // this.tableHints_ = other.tableHints_;
        this.onClause = other.onClause;
        this.usingColNames = other.usingColNames;
    }

    public void reset() {
        isAnalyzed = false;
        //  resolvedPath_ = null;
        if (usingColNames != null) {
            // The using col names are converted into an on-clause predicate during analysis,
            // so unset the on-clause here.
            onClause = null;
        } else if (onClause != null) {
            onClause.reset();
        }
        leftTblRef = null;
        allTableRefIds.clear();
        allMaterializedTupleIds.clear();
        correlatedTupleIds.clear();
        desc = null;
        if (lateralViewRefs != null) {
            for (LateralViewRef lateralViewRef : lateralViewRefs) {
                lateralViewRef.reset();
            }
        }
    }

    /**
     * Returns a deep clone of this table ref without also cloning the chain of table refs.
     * Sets leftTblRef_ in the returned clone to null.
     */
    @Override
    public TableRef clone() {
        return new TableRef(this);
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(name);
        if (partitionNames != null) {
            sb.append(partitionNames.toSql());
        }
        if (aliases != null && aliases.length > 0) {
            sb.append(" AS ").append(aliases[0]);
        }
        return sb.toString();
    }

    @Override
    public void write(DataOutput out) throws IOException {
        Text.writeString(out, GsonUtils.GSON.toJson(this));
    }

    public static TableRef read(DataInput in) throws IOException {
        if (Env.getCurrentEnvJournalVersion() < FeMetaVersion.VERSION_135) {
            TableRef ref = new TableRef();
            ref.readFields(in);
            return ref;
        } else {
            return GsonUtils.GSON.fromJson(Text.readString(in), TableRef.class);
        }
    }

    private void readFields(DataInput in) throws IOException {
        name = new TableName();
        name.readFields(in);
        if (in.readBoolean()) {
            partitionNames = PartitionNames.read(in);
        }

        if (in.readBoolean()) {
            String alias = Text.readString(in);
            aliases = new String[]{alias};
        }
    }

    public void setPartitionNames(PartitionNames partitionNames) {
        this.partitionNames = partitionNames;
    }

    public void setName(TableName name) {
        this.name = name;
    }
}