/*
 * 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.phoenix.compile;

import static org.apache.phoenix.query.QueryConstants.BASE_TABLE_BASE_COLUMN_COUNT;
import static org.apache.phoenix.schema.PTable.ImmutableStorageScheme.ONE_CELL_PER_COLUMN;
import static org.apache.phoenix.schema.PTable.QualifierEncodingScheme.NON_ENCODED_QUALIFIERS;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

import com.google.common.collect.ImmutableList;

import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.util.Pair;
import org.apache.phoenix.exception.SQLExceptionCode;
import org.apache.phoenix.exception.SQLExceptionInfo;
import org.apache.phoenix.expression.AndExpression;
import org.apache.phoenix.expression.CoerceExpression;
import org.apache.phoenix.expression.Expression;
import org.apache.phoenix.expression.LiteralExpression;
import org.apache.phoenix.expression.function.MinAggregateFunction;
import org.apache.phoenix.jdbc.PhoenixConnection;
import org.apache.phoenix.jdbc.PhoenixStatement;
import org.apache.phoenix.parse.AliasedNode;
import org.apache.phoenix.parse.AndBooleanParseNodeVisitor;
import org.apache.phoenix.parse.AndParseNode;
import org.apache.phoenix.parse.AndRewriterBooleanParseNodeVisitor;
import org.apache.phoenix.parse.BindTableNode;
import org.apache.phoenix.parse.ColumnDef;
import org.apache.phoenix.parse.ColumnParseNode;
import org.apache.phoenix.parse.ComparisonParseNode;
import org.apache.phoenix.parse.ConcreteTableNode;
import org.apache.phoenix.parse.DerivedTableNode;
import org.apache.phoenix.parse.EqualParseNode;
import org.apache.phoenix.parse.HintNode.Hint;
import org.apache.phoenix.parse.JoinTableNode;
import org.apache.phoenix.parse.JoinTableNode.JoinType;
import org.apache.phoenix.parse.NamedTableNode;
import org.apache.phoenix.parse.OrderByNode;
import org.apache.phoenix.parse.ParseNode;
import org.apache.phoenix.parse.ParseNodeFactory;
import org.apache.phoenix.parse.SelectStatement;
import org.apache.phoenix.parse.StatelessTraverseAllParseNodeVisitor;
import org.apache.phoenix.parse.TableName;
import org.apache.phoenix.parse.TableNode;
import org.apache.phoenix.parse.TableNodeVisitor;
import org.apache.phoenix.parse.TableWildcardParseNode;
import org.apache.phoenix.schema.ColumnNotFoundException;
import org.apache.phoenix.schema.ColumnRef;
import org.apache.phoenix.schema.LocalIndexDataColumnRef;
import org.apache.phoenix.schema.MetaDataEntityNotFoundException;
import org.apache.phoenix.schema.PColumn;
import org.apache.phoenix.schema.PNameFactory;
import org.apache.phoenix.schema.PTable;
import org.apache.phoenix.schema.PTable.IndexType;
import org.apache.phoenix.schema.PTableImpl;
import org.apache.phoenix.schema.PTableType;
import org.apache.phoenix.schema.ProjectedColumn;
import org.apache.phoenix.schema.SortOrder;
import org.apache.phoenix.schema.TableRef;
import org.apache.phoenix.schema.types.PBoolean;
import org.apache.phoenix.schema.types.PDataType;
import org.apache.phoenix.schema.types.PDate;
import org.apache.phoenix.schema.types.PDecimal;
import org.apache.phoenix.schema.types.PDouble;
import org.apache.phoenix.schema.types.PInteger;
import org.apache.phoenix.schema.types.PLong;
import org.apache.phoenix.schema.types.PSmallint;
import org.apache.phoenix.schema.types.PTimestamp;
import org.apache.phoenix.schema.types.PTinyint;
import org.apache.phoenix.schema.types.PVarbinary;
import org.apache.phoenix.schema.types.PVarchar;
import org.apache.phoenix.util.EncodedColumnsUtil;
import org.apache.phoenix.util.IndexUtil;
import org.apache.phoenix.util.ParseNodeUtil;
import org.apache.phoenix.util.SchemaUtil;

import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;


public class JoinCompiler {

    public enum Strategy {
        HASH_BUILD_LEFT,
        HASH_BUILD_RIGHT,
        SORT_MERGE,
    }

    public enum ColumnRefType {
        JOINLOCAL,
        GENERAL,
    }

    private final PhoenixStatement statement;
    private final SelectStatement select;
    private final ColumnResolver origResolver;
    private final boolean useStarJoin;
    private final Map<ColumnRef, ColumnRefType> columnRefs;
    private final Map<ColumnRef, ColumnParseNode> columnNodes;
    private final boolean useSortMergeJoin;

    private JoinCompiler(PhoenixStatement statement, SelectStatement select, ColumnResolver resolver) {
        this.statement = statement;
        this.select = select;
        this.origResolver = resolver;
        this.useStarJoin = !select.getHint().hasHint(Hint.NO_STAR_JOIN);
        this.columnRefs = new HashMap<ColumnRef, ColumnRefType>();
        this.columnNodes = new HashMap<ColumnRef, ColumnParseNode>();
        this.useSortMergeJoin = select.getHint().hasHint(Hint.USE_SORT_MERGE_JOIN);
    }

    /**
     * After this method is called, the inner state of the parameter resolver may be changed by
     * {@link FromCompiler#refreshDerivedTableNode} because of some sql optimization,
     * see also {@link Table#pruneSubselectAliasedNodes()}.
     * @param statement
     * @param select
     * @param resolver
     * @return
     * @throws SQLException
     */
    public static JoinTable compile(PhoenixStatement statement, SelectStatement select, ColumnResolver resolver) throws SQLException {
        JoinCompiler compiler = new JoinCompiler(statement, select, resolver);
        JoinTableConstructor constructor = compiler.new JoinTableConstructor();
        Pair<Table, List<JoinSpec>> res = select.getFrom().accept(constructor);
        JoinTable joinTable = res.getSecond() == null ? compiler.new JoinTable(res.getFirst()) : compiler.new JoinTable(res.getFirst(), res.getSecond());
        if (select.getWhere() != null) {
            joinTable.pushDownFilter(select.getWhere());
        }

        ColumnRefParseNodeVisitor generalRefVisitor = new ColumnRefParseNodeVisitor(resolver, statement.getConnection());
        ColumnRefParseNodeVisitor joinLocalRefVisitor = new ColumnRefParseNodeVisitor(resolver, statement.getConnection());

        joinTable.pushDownColumnRefVisitors(generalRefVisitor, joinLocalRefVisitor);

        ParseNodeUtil.applyParseNodeVisitor(select, generalRefVisitor, false);

        compiler.columnNodes.putAll(joinLocalRefVisitor.getColumnRefMap());
        compiler.columnNodes.putAll(generalRefVisitor.getColumnRefMap());

        for (ColumnRef ref : generalRefVisitor.getColumnRefMap().keySet()) {
            compiler.columnRefs.put(ref, ColumnRefType.GENERAL);
        }
        for (ColumnRef ref : joinLocalRefVisitor.getColumnRefMap().keySet()) {
            if (!compiler.columnRefs.containsKey(ref))
                compiler.columnRefs.put(ref, ColumnRefType.JOINLOCAL);
        }

        /**
         * After {@link ColumnRefParseNodeVisitor} is pushed down,
         * pruning columns for each {@link JoinCompiler.Table} if
         * {@link @link JoinCompiler.Table#isSubselect()}.
         */
        joinTable.pruneSubselectAliasedNodes();
        return joinTable;
    }

    private class JoinTableConstructor implements TableNodeVisitor<Pair<Table, List<JoinSpec>>> {

        private TableRef resolveTable(String alias, TableName name) throws SQLException {
            if (alias != null)
                return origResolver.resolveTable(null, alias);

            return origResolver.resolveTable(name.getSchemaName(), name.getTableName());
        }

        @Override
        public Pair<Table, List<JoinSpec>> visit(BindTableNode boundTableNode) throws SQLException {
            TableRef tableRef = resolveTable(boundTableNode.getAlias(), boundTableNode.getName());
            boolean isWildCard = isWildCardSelectForTable(select.getSelect(), tableRef, origResolver);
            Table table = new Table(boundTableNode, isWildCard, Collections.<ColumnDef>emptyList(), boundTableNode.getTableSamplingRate(), tableRef);
            return new Pair<Table, List<JoinSpec>>(table, null);
        }

        @Override
        public Pair<Table, List<JoinSpec>> visit(JoinTableNode joinNode) throws SQLException {
            Pair<Table, List<JoinSpec>> lhs = joinNode.getLHS().accept(this);
            Pair<Table, List<JoinSpec>> rhs = joinNode.getRHS().accept(this);
            JoinTable joinTable = rhs.getSecond() == null ? new JoinTable(rhs.getFirst()) : new JoinTable(rhs.getFirst(), rhs.getSecond());
            List<JoinSpec> joinSpecs = lhs.getSecond();
            if (joinSpecs == null) {
                joinSpecs = new ArrayList<JoinSpec>();
            }
            joinSpecs.add(new JoinSpec(joinNode.getType(), joinNode.getOnNode(), joinTable, joinNode.isSingleValueOnly(), origResolver));

            return new Pair<Table, List<JoinSpec>>(lhs.getFirst(), joinSpecs);
        }

        @Override
        public Pair<Table, List<JoinSpec>> visit(NamedTableNode namedTableNode)
                throws SQLException {
            TableRef tableRef = resolveTable(namedTableNode.getAlias(), namedTableNode.getName());
            boolean isWildCard = isWildCardSelectForTable(select.getSelect(), tableRef, origResolver);
            Table table = new Table(namedTableNode, isWildCard, namedTableNode.getDynamicColumns(), namedTableNode.getTableSamplingRate(), tableRef);
            return new Pair<Table, List<JoinSpec>>(table, null);
        }

        @Override
        public Pair<Table, List<JoinSpec>> visit(DerivedTableNode subselectNode)
                throws SQLException {
            TableRef tableRef = resolveTable(subselectNode.getAlias(), null);
            boolean isWildCard = isWildCardSelectForTable(select.getSelect(), tableRef, origResolver);
            Table table = new Table(subselectNode, isWildCard, tableRef);
            return new Pair<Table, List<JoinSpec>>(table, null);
        }
    }

    public class JoinTable {
        private final Table leftTable;
        private final List<JoinSpec> joinSpecs;
        private List<ParseNode> postFilters;
        private final List<Table> allTables;
        private final List<TableRef> allTableRefs;
        private final boolean allLeftJoin;
        private final boolean isPrefilterAccepted;
        private final List<JoinSpec> prefilterAcceptedTables;

        private JoinTable(Table table) {
            this.leftTable = table;
            this.joinSpecs = Collections.<JoinSpec>emptyList();
            this.postFilters = Collections.EMPTY_LIST;
            this.allTables = Collections.<Table>singletonList(table);
            this.allTableRefs = Collections.<TableRef>singletonList(table.getTableRef());
            this.allLeftJoin = false;
            this.isPrefilterAccepted = true;
            this.prefilterAcceptedTables = Collections.<JoinSpec>emptyList();
        }

        private JoinTable(Table table, List<JoinSpec> joinSpecs) {
            this.leftTable = table;
            this.joinSpecs = joinSpecs;
            this.postFilters = new ArrayList<ParseNode>();
            this.allTables = new ArrayList<Table>();
            this.allTableRefs = new ArrayList<TableRef>();
            this.allTables.add(table);
            boolean allLeftJoin = true;
            int lastRightJoinIndex = -1;
            boolean hasFullJoin = false;
            for (int i = 0; i < joinSpecs.size(); i++) {
                JoinSpec joinSpec = joinSpecs.get(i);
                this.allTables.addAll(joinSpec.getRhsJoinTable().getAllTables());
                allLeftJoin = allLeftJoin && joinSpec.getType() == JoinType.Left;
                hasFullJoin = hasFullJoin || joinSpec.getType() == JoinType.Full;
                if (joinSpec.getType() == JoinType.Right) {
                    lastRightJoinIndex = i;
                }
            }
            for (Table t : this.allTables) {
                this.allTableRefs.add(t.getTableRef());
            }
            this.allLeftJoin = allLeftJoin;
            this.isPrefilterAccepted = !hasFullJoin && lastRightJoinIndex == -1;
            this.prefilterAcceptedTables = new ArrayList<JoinSpec>();
            for (int i = lastRightJoinIndex == -1 ? 0 : lastRightJoinIndex; i < joinSpecs.size(); i++) {
                JoinSpec joinSpec = joinSpecs.get(i);
                if (joinSpec.getType() != JoinType.Left && joinSpec.getType() != JoinType.Anti && joinSpec.getType() != JoinType.Full) {
                    prefilterAcceptedTables.add(joinSpec);
                }
            }
        }

        public Table getLeftTable() {
            return leftTable;
        }

        public List<JoinSpec> getJoinSpecs() {
            return joinSpecs;
        }

        public List<Table> getAllTables() {
            return allTables;
        }

        public List<TableRef> getAllTableRefs() {
            return allTableRefs;
        }

        public List<TableRef> getLeftTableRef() {
            return Collections.<TableRef>singletonList(leftTable.getTableRef());
        }

        public boolean isAllLeftJoin() {
            return allLeftJoin;
        }

        public SelectStatement getStatement() {
            return select;
        }

        public ColumnResolver getOriginalResolver() {
            return origResolver;
        }

        public Map<ColumnRef, ColumnRefType> getColumnRefs() {
            return columnRefs;
        }

        public ParseNode getPostFiltersCombined() {
            return combine(postFilters);
        }

        public void addPostJoinFilter(ParseNode parseNode) {
            if(this.postFilters == Collections.EMPTY_LIST) {
                this.postFilters = new ArrayList<ParseNode>();
            }
            this.postFilters.add(parseNode);
        }

        public void addLeftTableFilter(ParseNode parseNode) throws SQLException {
            if (isPrefilterAccepted) {
                leftTable.addFilter(parseNode);
            } else {
                addPostJoinFilter(parseNode);
            }
        }

        public List<JoinSpec> getPrefilterAcceptedJoinSpecs() {
            return this.prefilterAcceptedTables;
        }

        /**
         * try to decompose filter and push down to single table.
         * @param filter
         * @throws SQLException
         */
        public void pushDownFilter(ParseNode filter) throws SQLException {
            if (joinSpecs.isEmpty()) {
                leftTable.addFilter(filter);
                return;
            }

            WhereNodeVisitor visitor = new WhereNodeVisitor(
                    origResolver,
                    this,
                    statement.getConnection());
            filter.accept(visitor);
        }

        public void pushDownColumnRefVisitors(
                ColumnRefParseNodeVisitor generalRefVisitor,
                ColumnRefParseNodeVisitor joinLocalRefVisitor) throws SQLException {
            for (ParseNode node : leftTable.getPostFilters()) {
                node.accept(generalRefVisitor);
            }
            for (ParseNode node : postFilters) {
                node.accept(generalRefVisitor);
            }
            for (JoinSpec joinSpec : joinSpecs) {
                JoinTable joinTable = joinSpec.getRhsJoinTable();
                boolean hasSubJoin = !joinTable.getJoinSpecs().isEmpty();
                for (EqualParseNode node : joinSpec.getOnConditions()) {
                    node.getLHS().accept(generalRefVisitor);
                    if (hasSubJoin) {
                        node.getRHS().accept(generalRefVisitor);
                    } else {
                        node.getRHS().accept(joinLocalRefVisitor);
                    }
                }
                joinTable.pushDownColumnRefVisitors(generalRefVisitor, joinLocalRefVisitor);
            }
        }

        /**
         * Pruning columns for each {@link JoinCompiler.Table} if
         * {@link @link JoinCompiler.Table#isSubselect()}.
         * @throws SQLException
         */
        public void pruneSubselectAliasedNodes() throws SQLException {
            this.leftTable.pruneSubselectAliasedNodes();
            for (JoinSpec joinSpec : joinSpecs) {
                JoinTable rhsJoinTablesContext = joinSpec.getRhsJoinTable();;
                rhsJoinTablesContext.pruneSubselectAliasedNodes();
            }
        }

        public Expression compilePostFilterExpression(StatementContext context) throws SQLException {
            List<ParseNode> filtersCombined = Lists.<ParseNode> newArrayList(postFilters);
            return JoinCompiler.compilePostFilterExpression(context, filtersCombined);
        }

        /**
         * Return a list of all applicable join strategies. The order of the strategies in the
         * returned list is based on the static rule below. However, the caller can decide on
         * an optimal join strategy by evaluating and comparing the costs.
         * 1. If hint USE_SORT_MERGE_JOIN is specified,
         *    return a singleton list containing only SORT_MERGE.
         * 2. If 1) matches pattern "A LEFT/INNER/SEMI/ANTI JOIN B"; or
         *       2) matches pattern "A LEFT/INNER/SEMI/ANTI JOIN B (LEFT/INNER/SEMI/ANTI JOIN C)+"
         *          and hint NO_STAR_JOIN is not specified,
         *    add BUILD_RIGHT to the returned list.
         * 3. If matches pattern "A RIGHT/INNER JOIN B", where B is either a named table reference
         *    or a flat sub-query,
         *    add BUILD_LEFT to the returned list.
         * 4. add SORT_MERGE to the returned list.
         */
        public List<Strategy> getApplicableJoinStrategies() {
            List<Strategy> strategies = Lists.newArrayList();
            if (useSortMergeJoin) {
                strategies.add(Strategy.SORT_MERGE);
            } else {
                if (getStarJoinVector() != null) {
                    strategies.add(Strategy.HASH_BUILD_RIGHT);
                }
                JoinSpec lastJoinSpec = joinSpecs.get(joinSpecs.size() - 1);
                JoinType type = lastJoinSpec.getType();
                if ((type == JoinType.Right || type == JoinType.Inner)
                        && lastJoinSpec.getRhsJoinTable().getJoinSpecs().isEmpty()
                        && lastJoinSpec.getRhsJoinTable().getLeftTable().isFlat()) {
                    strategies.add(Strategy.HASH_BUILD_LEFT);
                }
                strategies.add(Strategy.SORT_MERGE);
            }

            return strategies;
        }

        /**
         * Returns a boolean vector indicating whether the evaluation of join expressions
         * can be evaluated at an early stage if the input JoinSpec can be taken as a
         * star join. Otherwise returns null.
         * @return a boolean vector for a star join; or null for non star join.
         */
        public boolean[] getStarJoinVector() {
            int count = joinSpecs.size();
            if (!leftTable.isFlat() ||
                    (!useStarJoin
                            && count > 1
                            && joinSpecs.get(count - 1).getType() != JoinType.Left
                            && joinSpecs.get(count - 1).getType() != JoinType.Semi
                            && joinSpecs.get(count - 1).getType() != JoinType.Anti
                            && !joinSpecs.get(count - 1).isSingleValueOnly()))
                return null;

            boolean[] vector = new boolean[count];
            for (int i = 0; i < count; i++) {
                JoinSpec joinSpec = joinSpecs.get(i);
                if (joinSpec.getType() != JoinType.Left
                        && joinSpec.getType() != JoinType.Inner
                        && joinSpec.getType() != JoinType.Semi
                        && joinSpec.getType() != JoinType.Anti)
                    return null;
                vector[i] = true;
                Iterator<TableRef> iter = joinSpec.getDependentTableRefs().iterator();
                while (vector[i] == true && iter.hasNext()) {
                    TableRef tableRef = iter.next();
                    if (!tableRef.equals(leftTable.getTableRef())) {
                        vector[i] = false;
                    }
                }
            }

            return vector;
        }

        /**
         * create a new {@link JoinTable} exclude the last {@link JoinSpec},
         * and try to push {@link #postFilters} to the new {@link JoinTable}.
         * @param phoenixConnection
         * @return
         * @throws SQLException
         */
        public JoinTable createSubJoinTable(
                PhoenixConnection phoenixConnection) throws SQLException {
            assert joinSpecs.size() > 0;
            JoinTable newJoinTablesContext = joinSpecs.size() > 1 ?
                    new JoinTable(leftTable, joinSpecs.subList(0, joinSpecs.size() - 1)) :
                    new JoinTable(leftTable);
            JoinType rightmostJoinType = joinSpecs.get(joinSpecs.size() - 1).getType();
            if(rightmostJoinType == JoinType.Right || rightmostJoinType == JoinType.Full) {
                return newJoinTablesContext;
            }

            if(this.postFilters.isEmpty()) {
                return newJoinTablesContext;
            }

            PushDownPostFilterParseNodeVisitor pushDownPostFilterNodeVistor =
                    new PushDownPostFilterParseNodeVisitor(
                            JoinCompiler.this.origResolver,
                            newJoinTablesContext,
                            phoenixConnection);
            int index = 0;
            List<ParseNode> newPostFilterParseNodes = null;
            for(ParseNode postFilterParseNode : this.postFilters) {
                ParseNode newPostFilterParseNode =
                        postFilterParseNode.accept(pushDownPostFilterNodeVistor);
                if(newPostFilterParseNode != postFilterParseNode &&
                   newPostFilterParseNodes == null) {
                    newPostFilterParseNodes =
                            new ArrayList<ParseNode>(this.postFilters.subList(0, index));
                }
                if(newPostFilterParseNodes != null && newPostFilterParseNode != null) {
                    newPostFilterParseNodes.add(newPostFilterParseNode);
                }
                index++;
            }
            if(newPostFilterParseNodes != null) {
                this.postFilters = newPostFilterParseNodes;
            }
            return newJoinTablesContext;
        }

        public SelectStatement getAsSingleSubquery(SelectStatement query, boolean asSubquery) throws SQLException {
            assert (isFlat(query));

            if (asSubquery)
                return query;

            return NODE_FACTORY.select(select, query.getFrom(), query.getWhere());
        }

        public boolean hasPostReference() {
            for (Table table : allTables) {
                if (table.isWildCardSelect()) {
                    return true;
                }
            }

            for (Map.Entry<ColumnRef, ColumnRefType> e : columnRefs.entrySet()) {
                if (e.getValue() == ColumnRefType.GENERAL &&
                    allTableRefs.contains(e.getKey().getTableRef())) {
                    return true;
                }
            }

            return false;
        }

        public boolean hasFilters() {
           if (!postFilters.isEmpty())
               return true;

           if (isPrefilterAccepted && leftTable.hasFilters())
               return true;

           for (JoinSpec joinSpec : prefilterAcceptedTables) {
               if (joinSpec.getRhsJoinTable().hasFilters())
                   return true;
           }

           return false;
        }
    }

    public class JoinSpec {
        private final JoinType type;
        private final List<EqualParseNode> onConditions;
        private final JoinTable rhsJoinTable;
        private final boolean singleValueOnly;
        private Set<TableRef> dependentTableRefs;
        private OnNodeVisitor onNodeVisitor;

        private JoinSpec(JoinType type, ParseNode onNode, JoinTable joinTable,
                boolean singleValueOnly, ColumnResolver resolver) throws SQLException {
            this.type = type;
            this.onConditions = new ArrayList<EqualParseNode>();
            this.rhsJoinTable = joinTable;
            this.singleValueOnly = singleValueOnly;
            this.dependentTableRefs = new HashSet<TableRef>();
            this.onNodeVisitor = new OnNodeVisitor(resolver, this, statement.getConnection());
            if (onNode != null) {
                this.pushDownOnCondition(onNode);
            }
        }

        /**
         * <pre>
         * 1.in {@link JoinSpec} ctor,try to push the filter in join on clause to where clause,
         *   eg. for "a join b on a.id = b.id and b.code = 1 where a.name is not null", try to
         *   push "b.code =1" in join on clause to where clause.
         * 2.in{@link WhereNodeVisitor#visitLeave(ComparisonParseNode, List)}, for inner join,
         *   try to push the join on condition in where clause to join on clause´╝î
         *   eg. for "a join b on a.id = b.id where a.name = b.name", try to push "a.name=b.name"
         *   in where clause to join on clause.
         * </pre>
         * @param node
         * @throws SQLException
         */
        public void pushDownOnCondition(ParseNode node) throws SQLException {
            node.accept(onNodeVisitor);
        }

        public JoinType getType() {
            return type;
        }

        public List<EqualParseNode> getOnConditions() {
            return onConditions;
        }

        public JoinTable getRhsJoinTable() {
            return rhsJoinTable;
        }

        public List<TableRef>  getRhsJoinTableRefs() {
            return this.rhsJoinTable.getAllTableRefs();
        }

        public void pushDownFilterToRhsJoinTable(ParseNode parseNode) throws SQLException {
             this.rhsJoinTable.pushDownFilter(parseNode);
        }

        public void addOnCondition(EqualParseNode equalParseNode) {
            this.onConditions.add(equalParseNode);
        }

        public void addDependentTableRefs(Collection<TableRef> tableRefs) {
            this.dependentTableRefs.addAll(tableRefs);
        }

        public boolean isSingleValueOnly() {
            return singleValueOnly;
        }

        public Set<TableRef> getDependentTableRefs() {
            return dependentTableRefs;
        }

        public Pair<List<Expression>, List<Expression>> compileJoinConditions(StatementContext lhsCtx, StatementContext rhsCtx, Strategy strategy) throws SQLException {
            if (onConditions.isEmpty()) {
                return new Pair<List<Expression>, List<Expression>>(
                        Collections.<Expression> singletonList(LiteralExpression.newConstant(1)),
                        Collections.<Expression> singletonList(LiteralExpression.newConstant(1)));
            }

            List<Pair<Expression, Expression>> compiled = Lists.<Pair<Expression, Expression>> newArrayListWithExpectedSize(onConditions.size());
            ExpressionCompiler lhsCompiler = new ExpressionCompiler(lhsCtx);
            ExpressionCompiler rhsCompiler = new ExpressionCompiler(rhsCtx);
            for (EqualParseNode condition : onConditions) {
                lhsCompiler.reset();
                Expression left = condition.getLHS().accept(lhsCompiler);
                rhsCompiler.reset();
                Expression right = condition.getRHS().accept(rhsCompiler);
                PDataType toType = getCommonType(left.getDataType(), right.getDataType());
                SortOrder toSortOrder = strategy == Strategy.SORT_MERGE ? SortOrder.ASC : (strategy == Strategy.HASH_BUILD_LEFT ? right.getSortOrder() : left.getSortOrder());
                if (left.getDataType() != toType || left.getSortOrder() != toSortOrder) {
                    left = CoerceExpression.create(left, toType, toSortOrder, left.getMaxLength());
                }
                if (right.getDataType() != toType || right.getSortOrder() != toSortOrder) {
                    right = CoerceExpression.create(right, toType, toSortOrder, right.getMaxLength());
                }
                compiled.add(new Pair<Expression, Expression>(left, right));
            }
            // TODO PHOENIX-4618:
            // For Stategy.SORT_MERGE, we probably need to re-order the join keys based on the
            // specific ordering required by the join's parent, or re-order the following way
            // to align with group-by expressions' re-ordering.
            if (strategy != Strategy.SORT_MERGE) {
                Collections.sort(compiled, new Comparator<Pair<Expression, Expression>>() {
                    @Override
                    public int compare(Pair<Expression, Expression> o1, Pair<Expression, Expression> o2) {
                        Expression e1 = o1.getFirst();
                        Expression e2 = o2.getFirst();
                        boolean isFixed1 = e1.getDataType().isFixedWidth();
                        boolean isFixed2 = e2.getDataType().isFixedWidth();
                        boolean isFixedNullable1 = e1.isNullable() &&isFixed1;
                        boolean isFixedNullable2 = e2.isNullable() && isFixed2;
                        if (isFixedNullable1 == isFixedNullable2) {
                            if (isFixed1 == isFixed2) {
                                return 0;
                            } else if (isFixed1) {
                                return -1;
                            } else {
                                return 1;
                            }
                        } else if (isFixedNullable1) {
                            return 1;
                        } else {
                            return -1;
                        }
                    }
                });
            }
            List<Expression> lConditions = Lists.<Expression> newArrayListWithExpectedSize(compiled.size());
            List<Expression> rConditions = Lists.<Expression> newArrayListWithExpectedSize(compiled.size());
            for (Pair<Expression, Expression> pair : compiled) {
                lConditions.add(pair.getFirst());
                rConditions.add(pair.getSecond());
            }

            return new Pair<List<Expression>, List<Expression>>(lConditions, rConditions);
        }

        private PDataType getCommonType(PDataType lType, PDataType rType) throws SQLException {
            if (lType == rType)
                return lType;

            if (!lType.isComparableTo(rType))
                throw new SQLExceptionInfo.Builder(SQLExceptionCode.TYPE_MISMATCH)
                    .setMessage("On-clause LHS expression and RHS expression must be comparable. LHS type: " + lType + ", RHS type: " + rType)
                    .build().buildException();

            if (lType.isCoercibleTo(PTinyint.INSTANCE)
                && (rType == null || rType.isCoercibleTo(PTinyint.INSTANCE))) {
              return lType; // to preserve UNSIGNED type
            }
            if (lType.isCoercibleTo(PSmallint.INSTANCE)
                && (rType == null || rType.isCoercibleTo(PSmallint.INSTANCE))) {
              return lType; // to preserve UNSIGNED type
            }
            if (lType.isCoercibleTo(PInteger.INSTANCE)
                && (rType == null || rType.isCoercibleTo(PInteger.INSTANCE))) {
              return lType; // to preserve UNSIGNED type
            }
            if (lType.isCoercibleTo(PLong.INSTANCE)
                && (rType == null || rType.isCoercibleTo(PLong.INSTANCE))) {
              return lType; // to preserve UNSIGNED type
            }
            if (lType.isCoercibleTo(PDouble.INSTANCE)
                && (rType == null || rType.isCoercibleTo(PDouble.INSTANCE))) {
              return lType; // to preserve UNSIGNED type
            }
            if (lType.isCoercibleTo(PDecimal.INSTANCE)
                && (rType == null || rType.isCoercibleTo(PDecimal.INSTANCE))) {
              return PDecimal.INSTANCE;
            }
            if (lType.isCoercibleTo(PDate.INSTANCE)
                && (rType == null || rType.isCoercibleTo(PDate.INSTANCE))) {
              return lType;
            }
            if (lType.isCoercibleTo(PTimestamp.INSTANCE)
                && (rType == null || rType.isCoercibleTo(PTimestamp.INSTANCE))) {
              return lType;
            }
            if (lType.isCoercibleTo(PVarchar.INSTANCE)
                && (rType == null || rType.isCoercibleTo(PVarchar.INSTANCE))) {
              return PVarchar.INSTANCE;
            }
            if (lType.isCoercibleTo(PBoolean.INSTANCE)
                && (rType == null || rType.isCoercibleTo(PBoolean.INSTANCE))) {
              return PBoolean.INSTANCE;
            }
            return PVarbinary.INSTANCE;
        }
    }

    public class Table {
        private TableNode tableNode;
        private final boolean isWildcard;
        private final List<ColumnDef> dynamicColumns;
        private final Double tableSamplingRate;
        private SelectStatement subselect;
        private TableRef tableRef;
        private final List<ParseNode> preFilters;
        private final List<ParseNode> postFilters;
        private final boolean filterCanPushDownToSubselect;

        private Table(TableNode tableNode, boolean isWildcard, List<ColumnDef> dynamicColumns,
                      Double tableSamplingRate, TableRef tableRef) {
            this.tableNode = tableNode;
            this.isWildcard = isWildcard;
            this.dynamicColumns = dynamicColumns;
            this.tableSamplingRate=tableSamplingRate;
            this.subselect = null;
            this.tableRef = tableRef;
            this.preFilters = new ArrayList<ParseNode>();
            this.postFilters = Collections.<ParseNode>emptyList();
            this.filterCanPushDownToSubselect = false;
        }

        private Table(DerivedTableNode tableNode, boolean isWildcard, TableRef tableRef) throws SQLException {
            this.tableNode = tableNode;
            this.isWildcard = isWildcard;
            this.dynamicColumns = Collections.<ColumnDef>emptyList();
            this.tableSamplingRate=ConcreteTableNode.DEFAULT_TABLE_SAMPLING_RATE;
            this.subselect = SubselectRewriter.flatten(tableNode.getSelect(), statement.getConnection());
            this.tableRef = tableRef;
            this.preFilters = new ArrayList<ParseNode>();
            this.postFilters = new ArrayList<ParseNode>();
            this.filterCanPushDownToSubselect = SubselectRewriter.isFilterCanPushDownToSelect(subselect);
        }

        public TableNode getTableNode() {
            return tableNode;
        }

        public List<ColumnDef> getDynamicColumns() {
            return dynamicColumns;
        }
        
        public Double getTableSamplingRate() {
            return tableSamplingRate;
        }

        public boolean isSubselect() {
            return subselect != null;
        }

        public SelectStatement getSubselect() {
            return this.subselect;
        }

        /**
         * Pruning columns if {@link #isSubselect()}.
         * Note: If some columns are pruned, the {@link JoinCompiler#origResolver} should be refreshed.
         * @throws SQLException
         */
        public void pruneSubselectAliasedNodes() throws SQLException {
            if(!this.isSubselect()) {
                return;
            }
            Set<String> referencedColumnNames = this.getReferencedColumnNames();
            SelectStatement newSubselectStatement =
                    SubselectRewriter.pruneSelectAliasedNodes(
                            this.subselect,
                            referencedColumnNames,
                            statement.getConnection());
            if(!newSubselectStatement.getSelect().equals(this.subselect.getSelect())) {
                /**
                 * The columns are pruned, so {@link ColumnResolver} should be refreshed.
                 */
                DerivedTableNode newDerivedTableNode =
                        NODE_FACTORY.derivedTable(this.tableNode.getAlias(), newSubselectStatement);
                TableRef newTableRef =
                        FromCompiler.refreshDerivedTableNode(origResolver, newDerivedTableNode);
                assert newTableRef != null;
                this.subselect = newSubselectStatement;
                this.tableRef = newTableRef;
                this.tableNode = newDerivedTableNode;
            }
        }

        /**
         * Collect the referenced columns of this {@link Table}
         * according to {@link JoinCompiler#columnNodes}.
         * @return
         * @throws SQLException
         */
        private Set<String> getReferencedColumnNames() throws SQLException {
            assert(this.isSubselect());
            if (isWildCardSelect()) {
                return null;
            }
            Set<String> referencedColumnNames = new HashSet<String>();
            for (Map.Entry<ColumnRef, ColumnParseNode> entry : columnNodes.entrySet()) {
                if (tableRef.equals(entry.getKey().getTableRef())) {
                    ColumnParseNode columnParseNode = entry.getValue();
                    String normalizedColumnName = SchemaUtil.getNormalizedColumnName(columnParseNode);
                    referencedColumnNames.add(normalizedColumnName);
                }
            }
            return referencedColumnNames;
        }

        /**
         * Returns all the basic select nodes, no aggregation.
         */
        public List<AliasedNode> getSelectNodes() {
            if (isWildCardSelect()) {
                return Collections.singletonList(NODE_FACTORY.aliasedNode(null, NODE_FACTORY.wildcard()));
            }

            List<AliasedNode> ret = new ArrayList<AliasedNode>();
            for (Map.Entry<ColumnRef, ColumnParseNode> entry : columnNodes.entrySet()) {
                if (tableRef.equals(entry.getKey().getTableRef())) {
                    ret.add(NODE_FACTORY.aliasedNode(null, entry.getValue()));
                }
            }
            if (ret.isEmpty()) {
                ret.add(NODE_FACTORY.aliasedNode(null, NODE_FACTORY.literal(1)));
            }
            return ret;
        }

        public List<ParseNode> getPreFilters() {
            return preFilters;
        }

        public List<ParseNode> getPostFilters() {
            return postFilters;
        }

        public TableRef getTableRef() {
            return tableRef;
        }

        public void addFilter(ParseNode filter) throws SQLException {
            if (!isSubselect() || filterCanPushDownToSubselect) {
                this.addPreFilter(filter);
            } else {
                postFilters.add(filter);
            }
        }

        /**
         * If {@link #isSubselect()}, preFilterParseNode is at first rewritten by
         * {@link SubselectRewriter#rewritePreFilterForSubselect}
         * @param preFilterParseNode
         * @throws SQLException
         */
        private void addPreFilter(ParseNode preFilterParseNode) throws SQLException {
            if(this.isSubselect()) {
                preFilterParseNode =
                        SubselectRewriter.rewritePreFilterForSubselect(
                                preFilterParseNode,
                                this.subselect,
                                tableNode.getAlias());
            }
            preFilters.add(preFilterParseNode);
        }

        public ParseNode getPreFiltersCombined() {
            return combine(preFilters);
        }

        public SelectStatement getAsSubquery(List<OrderByNode> orderBy) throws SQLException {
            if (isSubselect()) {
                return SubselectRewriter.applyOrderByAndPostFilters(
                        SubselectRewriter.applyPreFiltersForSubselect(subselect, preFilters, tableNode.getAlias()),
                        orderBy,
                        tableNode.getAlias(),
                        postFilters);
            }
            //for flat table, postFilters is empty , because it can safely pushed down as preFilters.
            assert postFilters == null || postFilters.isEmpty();
            return NODE_FACTORY.select(tableNode, select.getHint(), false, getSelectNodes(), getPreFiltersCombined(), null,
                    null, orderBy, null, null, 0, false, select.hasSequence(),
                    Collections.<SelectStatement> emptyList(), select.getUdfParseNodes());
        }

        public SelectStatement getAsSubqueryForOptimization(boolean applyGroupByOrOrderBy) throws SQLException {
            assert (!isSubselect());

            SelectStatement query = getAsSubquery(null);
            if (!applyGroupByOrOrderBy)
                return query;

            boolean addGroupBy = false;
            boolean addOrderBy = false;
            if (select.getGroupBy() != null && !select.getGroupBy().isEmpty()) {
                ColumnRefParseNodeVisitor groupByVisitor = new ColumnRefParseNodeVisitor(origResolver, statement.getConnection());
                for (ParseNode node : select.getGroupBy()) {
                    node.accept(groupByVisitor);
                }
                Set<TableRef> set = groupByVisitor.getTableRefSet();
                if (set.size() == 1 && tableRef.equals(set.iterator().next())) {
                    addGroupBy = true;
                }
            } else if (select.getOrderBy() != null && !select.getOrderBy().isEmpty()) {
                ColumnRefParseNodeVisitor orderByVisitor = new ColumnRefParseNodeVisitor(origResolver, statement.getConnection());
                for (OrderByNode node : select.getOrderBy()) {
                    node.getNode().accept(orderByVisitor);
                }
                Set<TableRef> set = orderByVisitor.getTableRefSet();
                if (set.size() == 1 && tableRef.equals(set.iterator().next())) {
                    addOrderBy = true;
                }
            }

            if (!addGroupBy && !addOrderBy)
                return query;

            List<AliasedNode> selectList = query.getSelect();
            if (addGroupBy) {
                assert (!isWildCardSelect());
                selectList = new ArrayList<AliasedNode>(query.getSelect().size());
                for (AliasedNode aliasedNode : query.getSelect()) {
                    ParseNode node = NODE_FACTORY.function(
                            MinAggregateFunction.NAME, Collections.singletonList(aliasedNode.getNode()));
                    selectList.add(NODE_FACTORY.aliasedNode(null, node));
                }
            }

            return NODE_FACTORY.select(query.getFrom(), query.getHint(), query.isDistinct(), selectList,
                    query.getWhere(), addGroupBy ? select.getGroupBy() : query.getGroupBy(),
                    addGroupBy ? null : query.getHaving(), addOrderBy ? select.getOrderBy() : query.getOrderBy(),
                    query.getLimit(), query.getOffset(), query.getBindCount(), addGroupBy, query.hasSequence(),
                    query.getSelects(), query.getUdfParseNodes());
        }

        public boolean hasFilters() {
            return isSubselect() ? (!postFilters.isEmpty() || subselect.getWhere() != null || subselect.getHaving() != null) : !preFilters.isEmpty();
        }

        public boolean isFlat() {
            return subselect == null || JoinCompiler.isFlat(subselect);
        }

        protected boolean isWildCardSelect() {
            return isWildcard;
        }

        public void projectColumns(Scan scan) {
            assert(!isSubselect());
            if (isWildCardSelect()) {
                scan.getFamilyMap().clear();
                return;
            }
            for (ColumnRef columnRef : columnRefs.keySet()) {
                if (columnRef.getTableRef().equals(tableRef)
                        && !SchemaUtil.isPKColumn(columnRef.getColumn())
                        && !(columnRef instanceof LocalIndexColumnRef)) {
                	EncodedColumnsUtil.setColumns(columnRef.getColumn(), tableRef.getTable(), scan);
                }
            }
        }

        public PTable createProjectedTable(boolean retainPKColumns, StatementContext context) throws SQLException {
            assert(!isSubselect());
            List<ColumnRef> sourceColumns = new ArrayList<ColumnRef>();
            PTable table = tableRef.getTable();
            if (retainPKColumns) {
                for (PColumn column : table.getPKColumns()) {
                    sourceColumns.add(new ColumnRef(tableRef, column.getPosition()));
                }
            }
            if (isWildCardSelect()) {
                for (PColumn column : table.getColumns()) {
                    if (!retainPKColumns || !SchemaUtil.isPKColumn(column)) {
                        sourceColumns.add(new ColumnRef(tableRef, column.getPosition()));
                    }
                }
            } else {
                for (Map.Entry<ColumnRef, ColumnRefType> e : columnRefs.entrySet()) {
                    ColumnRef columnRef = e.getKey();
                    if (columnRef.getTableRef().equals(tableRef)
                            && (!retainPKColumns || !SchemaUtil.isPKColumn(columnRef.getColumn()))) {
                        if (columnRef instanceof LocalIndexColumnRef) {
                            sourceColumns.add(new LocalIndexDataColumnRef(context, tableRef, IndexUtil.getIndexColumnName(columnRef.getColumn())));
                        } else {
                            sourceColumns.add(columnRef);
                        }
                    }
                }
            }

            return TupleProjectionCompiler.createProjectedTable(tableRef, sourceColumns, retainPKColumns);
        }

        public PTable createProjectedTable(RowProjector rowProjector) throws SQLException {
            assert(isSubselect());
            TableRef tableRef = FromCompiler.getResolverForCompiledDerivedTable(statement.getConnection(), this.tableRef, rowProjector).getTables().get(0);
            List<ColumnRef> sourceColumns = new ArrayList<ColumnRef>();
            PTable table = tableRef.getTable();
            for (PColumn column : table.getColumns()) {
                sourceColumns.add(new ColumnRef(tableRef, column.getPosition()));
            }
            return TupleProjectionCompiler.createProjectedTable(tableRef, sourceColumns, false);
        }
    }

    /**
     * Push down {@link JoinTable#postFilters} of Outermost-JoinTable to
     * {@link JoinTable#postFilters} of Sub-JoinTable
     */
    private static class PushDownPostFilterParseNodeVisitor extends AndRewriterBooleanParseNodeVisitor {
        private ColumnRefParseNodeVisitor columnRefParseNodeVisitor;
        /**
         * Sub-JoinTable to accept pushed down PostFilters.
         */
        private JoinTable joinTable;

        public PushDownPostFilterParseNodeVisitor(
                ColumnResolver resolver,
                JoinTable joinTablesContext,
                PhoenixConnection connection) {
            super(NODE_FACTORY);
            this.joinTable = joinTablesContext;
            this.columnRefParseNodeVisitor = new ColumnRefParseNodeVisitor(resolver, connection);
        }

        @Override
        protected ParseNode leaveBooleanNode(
                ParseNode parentParseNode, List<ParseNode> childParseNodes) throws SQLException {
            columnRefParseNodeVisitor.reset();
            parentParseNode.accept(columnRefParseNodeVisitor);
            ColumnRefParseNodeVisitor.ColumnRefType columnRefType =
                    columnRefParseNodeVisitor.getContentType(
                            this.joinTable.getAllTableRefs());
            if(columnRefType == ColumnRefParseNodeVisitor.ColumnRefType.NONE ||
               columnRefType == ColumnRefParseNodeVisitor.ColumnRefType.SELF_ONLY){
                this.joinTable.postFilters.add(parentParseNode);
                return null;
            }
            return parentParseNode;
        }
    }

    private static class WhereNodeVisitor extends AndBooleanParseNodeVisitor<Void> {
        private ColumnRefParseNodeVisitor columnRefVisitor;
        private JoinTable joinTable;

        public WhereNodeVisitor(
                ColumnResolver resolver,
                JoinTable joinTablesContext,
                PhoenixConnection connection) {
            this.joinTable = joinTablesContext;
            this.columnRefVisitor = new ColumnRefParseNodeVisitor(resolver, connection);
        }

        @Override
        protected Void leaveBooleanNode(ParseNode node,
                List<Void> l) throws SQLException {
            columnRefVisitor.reset();
            node.accept(columnRefVisitor);
            ColumnRefParseNodeVisitor.ColumnRefType type =
                    columnRefVisitor.getContentType(this.joinTable.getLeftTableRef());
            switch (type) {
            case NONE:
            case SELF_ONLY:
                this.joinTable.addLeftTableFilter(node);
                break;
            case FOREIGN_ONLY:
                JoinTable matched = null;
                for (JoinSpec joinSpec : this.joinTable.getPrefilterAcceptedJoinSpecs()) {
                    if (columnRefVisitor.getContentType(
                            joinSpec.getRhsJoinTable().getAllTableRefs()) ==
                        ColumnRefParseNodeVisitor.ColumnRefType.SELF_ONLY) {
                        matched = joinSpec.getRhsJoinTable();
                        break;
                    }
                }
                if (matched != null) {
                    matched.pushDownFilter(node);
                } else {
                    this.joinTable.addPostJoinFilter(node);
                }
                break;
            default:
                this.joinTable.addPostJoinFilter(node);
                break;
            }
            return null;
        }

        @Override
        protected Void leaveNonBooleanNode(ParseNode node, List<Void> l) throws SQLException {
            return null;
        }

        @Override
        public Void visitLeave(AndParseNode node, List<Void> l) throws SQLException {
            return null;
        }

        @Override
        public Void visitLeave(ComparisonParseNode node, List<Void> l)
                throws SQLException {
            if (!(node instanceof EqualParseNode))
                return leaveBooleanNode(node, l);

            List<JoinSpec> prefilterAcceptedJoinSpecs =
                    this.joinTable.getPrefilterAcceptedJoinSpecs();
            ListIterator<JoinSpec> iter =
                    prefilterAcceptedJoinSpecs.listIterator(prefilterAcceptedJoinSpecs.size());
            while (iter.hasPrevious()) {
                JoinSpec joinSpec = iter.previous();
                if (joinSpec.getType() != JoinType.Inner || joinSpec.isSingleValueOnly()) {
                    continue;
                }

                try {
                    joinSpec.pushDownOnCondition(node);
                    return null;
                } catch (SQLException e) {
                }
            }

            return leaveBooleanNode(node, l);
        }
    }

    private static class OnNodeVisitor extends AndBooleanParseNodeVisitor<Void> {
        private final ColumnRefParseNodeVisitor columnRefVisitor;
        private final JoinSpec joinSpec;

        public OnNodeVisitor(
                ColumnResolver resolver, JoinSpec joinSpec, PhoenixConnection connection) {
            this.joinSpec = joinSpec;
            this.columnRefVisitor = new ColumnRefParseNodeVisitor(resolver, connection);
        }

        @Override
        protected Void leaveBooleanNode(ParseNode node,
                List<Void> l) throws SQLException {
            columnRefVisitor.reset();
            node.accept(columnRefVisitor);
            ColumnRefParseNodeVisitor.ColumnRefType type =
                    columnRefVisitor.getContentType(this.joinSpec.getRhsJoinTableRefs());
            if (type == ColumnRefParseNodeVisitor.ColumnRefType.NONE
                    || type == ColumnRefParseNodeVisitor.ColumnRefType.SELF_ONLY) {
                this.joinSpec.pushDownFilterToRhsJoinTable(node);
            } else {
                throwAmbiguousJoinConditionException();
            }
            return null;
        }

        @Override
        protected Void leaveNonBooleanNode(ParseNode node, List<Void> l) throws SQLException {
            return null;
        }

        @Override
        public Void visitLeave(AndParseNode node, List<Void> l) throws SQLException {
            return null;
        }

        @Override
        public Void visitLeave(ComparisonParseNode node, List<Void> l)
                throws SQLException {
            if (!(node instanceof EqualParseNode))
                return leaveBooleanNode(node, l);
            columnRefVisitor.reset();
            node.getLHS().accept(columnRefVisitor);
            ColumnRefParseNodeVisitor.ColumnRefType lhsType =
                    columnRefVisitor.getContentType(this.joinSpec.getRhsJoinTableRefs());
            Set<TableRef> lhsTableRefSet = Sets.newHashSet(columnRefVisitor.getTableRefSet());
            columnRefVisitor.reset();
            node.getRHS().accept(columnRefVisitor);
            ColumnRefParseNodeVisitor.ColumnRefType rhsType =
                    columnRefVisitor.getContentType(this.joinSpec.getRhsJoinTableRefs());
            Set<TableRef> rhsTableRefSet = Sets.newHashSet(columnRefVisitor.getTableRefSet());
            if ((lhsType == ColumnRefParseNodeVisitor.ColumnRefType.SELF_ONLY || lhsType == ColumnRefParseNodeVisitor.ColumnRefType.NONE)
                    && (rhsType == ColumnRefParseNodeVisitor.ColumnRefType.SELF_ONLY || rhsType == ColumnRefParseNodeVisitor.ColumnRefType.NONE)) {
                this.joinSpec.pushDownFilterToRhsJoinTable(node);
            } else if (lhsType == ColumnRefParseNodeVisitor.ColumnRefType.FOREIGN_ONLY
                    && rhsType == ColumnRefParseNodeVisitor.ColumnRefType.SELF_ONLY) {
                this.joinSpec.addOnCondition((EqualParseNode) node);
                this.joinSpec.addDependentTableRefs(lhsTableRefSet);
            } else if (rhsType == ColumnRefParseNodeVisitor.ColumnRefType.FOREIGN_ONLY
                    && lhsType == ColumnRefParseNodeVisitor.ColumnRefType.SELF_ONLY) {
                this.joinSpec.addOnCondition(NODE_FACTORY.equal(node.getRHS(), node.getLHS()));
                this.joinSpec.addDependentTableRefs(rhsTableRefSet);
            } else {
                throwAmbiguousJoinConditionException();
            }
            return null;
        }

        /*
         * Conditions in the ON clause can only be:
         * 1) an equal test between a self table expression and a foreign
         *    table expression.
         * 2) a boolean condition referencing to the self table only.
         * Otherwise, it can be ambiguous.
         */
        public void throwAmbiguousJoinConditionException() throws SQLException {
            throw new SQLExceptionInfo.Builder(SQLExceptionCode.AMBIGUOUS_JOIN_CONDITION).build().buildException();
        }
    }

    private static class LocalIndexColumnRef extends ColumnRef {
        private final TableRef indexTableRef;

        public LocalIndexColumnRef(TableRef tableRef, String familyName,
                String columnName, TableRef indexTableRef) throws MetaDataEntityNotFoundException {
            super(tableRef, familyName, columnName);
            this.indexTableRef = indexTableRef;
        }

        @Override
        public TableRef getTableRef() {
            return indexTableRef;
        }
    }

    private static class ColumnRefParseNodeVisitor extends StatelessTraverseAllParseNodeVisitor {
        public enum ColumnRefType {NONE, SELF_ONLY, FOREIGN_ONLY, COMPLEX};

        private final ColumnResolver resolver;
        private final PhoenixConnection connection;
        private final Set<TableRef> tableRefSet;
        private final Map<ColumnRef, ColumnParseNode> columnRefMap;

        public ColumnRefParseNodeVisitor(ColumnResolver resolver, PhoenixConnection connection) {
            this.resolver = resolver;
            this.tableRefSet = new HashSet<TableRef>();
            this.columnRefMap = new HashMap<ColumnRef, ColumnParseNode>();
            this.connection = connection;
        }

        public void reset() {
            this.tableRefSet.clear();
            this.columnRefMap.clear();
        }

        @Override
        public Void visit(ColumnParseNode node) throws SQLException {
            ColumnRef columnRef = null;
            try {
                columnRef = resolver.resolveColumn(node.getSchemaName(), node.getTableName(), node.getName());
            } catch (ColumnNotFoundException e) {
                // This could be a LocalIndexDataColumnRef. If so, the table name must have
                // been appended by the IndexStatementRewriter, and we can convert it into.
                TableRef tableRef = resolver.resolveTable(node.getSchemaName(), node.getTableName());
                if (tableRef.getTable().getIndexType() == IndexType.LOCAL) {
                    TableRef parentTableRef = FromCompiler.getResolver(
                            NODE_FACTORY.namedTable(null, TableName.create(tableRef.getTable()
                                    .getSchemaName().getString(), tableRef.getTable()
                                    .getParentTableName().getString())), connection).resolveTable(
                            tableRef.getTable().getSchemaName().getString(),
                            tableRef.getTable().getParentTableName().getString());
                    columnRef = new LocalIndexColumnRef(parentTableRef,
                            IndexUtil.getDataColumnFamilyName(node.getName()),
                            IndexUtil.getDataColumnName(node.getName()), tableRef);
                } else {
                    throw e;
                }
            }
            columnRefMap.put(columnRef, node);
            tableRefSet.add(columnRef.getTableRef());
            return null;
        }

        public Set<TableRef> getTableRefSet() {
            return tableRefSet;
        }

        public Map<ColumnRef, ColumnParseNode> getColumnRefMap() {
            return columnRefMap;
        }

        public ColumnRefType getContentType(List<TableRef> selfTableRefs) {
            if (tableRefSet.isEmpty())
                return ColumnRefType.NONE;

            ColumnRefType ret = ColumnRefType.NONE;
            for (TableRef tRef : tableRefSet) {
                boolean isSelf = selfTableRefs.contains(tRef);
                switch (ret) {
                case NONE:
                    ret = isSelf ? ColumnRefType.SELF_ONLY : ColumnRefType.FOREIGN_ONLY;
                    break;
                case SELF_ONLY:
                    ret = isSelf ? ColumnRefType.SELF_ONLY : ColumnRefType.COMPLEX;
                    break;
                case FOREIGN_ONLY:
                    ret = isSelf ? ColumnRefType.COMPLEX : ColumnRefType.FOREIGN_ONLY;
                    break;
                default: // COMPLEX do nothing
                    break;
                }

                if (ret == ColumnRefType.COMPLEX) {
                    break;
                }
            }

            return ret;
        }
    }

    // for creation of new statements
    private static final ParseNodeFactory NODE_FACTORY = new ParseNodeFactory();

    private static boolean isFlat(SelectStatement select) {
        return !select.isJoin()
                && !select.isAggregate()
                && !select.isDistinct()
                && !(select.getFrom() instanceof DerivedTableNode)
                && select.getLimit() == null
                && select.getOffset() == null;
    }

    private static ParseNode combine(List<ParseNode> nodes) {
        if (nodes.isEmpty())
            return null;

        if (nodes.size() == 1)
            return nodes.get(0);

        return NODE_FACTORY.and(nodes);
    }

    private boolean isWildCardSelectForTable(List<AliasedNode> select, TableRef tableRef, ColumnResolver resolver) throws SQLException {
        ColumnRefParseNodeVisitor visitor = new ColumnRefParseNodeVisitor(resolver, statement.getConnection());
        for (AliasedNode aliasedNode : select) {
            ParseNode node = aliasedNode.getNode();
            if (node instanceof TableWildcardParseNode) {
                TableName tableName = ((TableWildcardParseNode) node).getTableName();
                if (tableRef.equals(resolver.resolveTable(tableName.getSchemaName(), tableName.getTableName()))) {
                    return true;
                }

            }
        }
        return false;
    }

    private static Expression compilePostFilterExpression(StatementContext context, List<ParseNode> postFilters) throws SQLException {
        if (postFilters.isEmpty())
            return null;

        ExpressionCompiler expressionCompiler = new ExpressionCompiler(context);
        List<Expression> expressions = new ArrayList<Expression>(postFilters.size());
        for (ParseNode postFilter : postFilters) {
            expressionCompiler.reset();
            Expression expression = postFilter.accept(expressionCompiler);
            expressions.add(expression);
        }

        if (expressions.size() == 1)
            return expressions.get(0);

        return AndExpression.create(expressions);
    }

    public static PTable joinProjectedTables(PTable left, PTable right, JoinType type) throws SQLException {
        Preconditions.checkArgument(left.getType() == PTableType.PROJECTED);
        Preconditions.checkArgument(right.getType() == PTableType.PROJECTED);
        List<PColumn> merged = Lists.<PColumn> newArrayList();
        if (type == JoinType.Full) {
            for (PColumn c : left.getColumns()) {
                merged.add(new ProjectedColumn(c.getName(), c.getFamilyName(),
                        c.getPosition(), true, ((ProjectedColumn) c).getSourceColumnRef(), SchemaUtil.isPKColumn(c) ? null : c.getName().getBytes()));
            }
        } else {
            merged.addAll(left.getColumns());
        }
        int position = merged.size();
        for (PColumn c : right.getColumns()) {
            if (!SchemaUtil.isPKColumn(c)) {
                PColumn column = new ProjectedColumn(c.getName(), c.getFamilyName(), 
                        position++, type == JoinType.Inner ? c.isNullable() : true, 
                        ((ProjectedColumn) c).getSourceColumnRef(), c.getName().getBytes());
                merged.add(column);
            }
        }
        if (left.getBucketNum() != null) {
            merged.remove(0);
        }
        return new PTableImpl.Builder()
                .setType(left.getType())
                .setState(left.getIndexState())
                .setTimeStamp(left.getTimeStamp())
                .setIndexDisableTimestamp(left.getIndexDisableTimestamp())
                .setSequenceNumber(left.getSequenceNumber())
                .setImmutableRows(left.isImmutableRows())
                .setDisableWAL(PTable.DEFAULT_DISABLE_WAL)
                .setMultiTenant(left.isMultiTenant())
                .setStoreNulls(left.getStoreNulls())
                .setViewType(left.getViewType())
                .setViewIndexIdType(left.getviewIndexIdType())
                .setViewIndexId(left.getViewIndexId())
                .setIndexType(left.getIndexType())
                .setTransactionProvider(left.getTransactionProvider())
                .setUpdateCacheFrequency(left.getUpdateCacheFrequency())
                .setNamespaceMapped(left.isNamespaceMapped())
                .setAutoPartitionSeqName(left.getAutoPartitionSeqName())
                .setAppendOnlySchema(left.isAppendOnlySchema())
                .setImmutableStorageScheme(ONE_CELL_PER_COLUMN)
                .setQualifierEncodingScheme(NON_ENCODED_QUALIFIERS)
                .setBaseColumnCount(BASE_TABLE_BASE_COLUMN_COUNT)
                .setEncodedCQCounter(PTable.EncodedCQCounter.NULL_COUNTER)
                .setUseStatsForParallelization(left.useStatsForParallelization())
                .setExcludedColumns(ImmutableList.of())
                .setTenantId(left.getTenantId())
                .setSchemaName(left.getSchemaName())
                .setTableName(PNameFactory.newName(SchemaUtil.getTableName(left.getName().getString(),
                        right.getName().getString())))
                .setPkName(left.getPKName())
                .setRowKeyOrderOptimizable(left.rowKeyOrderOptimizable())
                .setBucketNum(left.getBucketNum())
                .setIndexes(left.getIndexes() == null ? Collections.emptyList() : left.getIndexes())
                .setParentSchemaName(left.getParentSchemaName())
                .setParentTableName(left.getParentTableName())
                .setPhysicalNames(ImmutableList.of())
                .setColumns(merged)
                .build();
    }

}