You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@phoenix.apache.org by ma...@apache.org on 2014/04/01 02:34:48 UTC

[1/3] PHOENIX-71 Support sub-joins

Repository: incubator-phoenix
Updated Branches:
  refs/heads/master 0849b259d -> 12bd7aedd


http://git-wip-us.apache.org/repos/asf/incubator-phoenix/blob/12bd7aed/phoenix-core/src/main/java/org/apache/phoenix/compile/QueryCompiler.java
----------------------------------------------------------------------
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/compile/QueryCompiler.java b/phoenix-core/src/main/java/org/apache/phoenix/compile/QueryCompiler.java
index a354ed1..729459d 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/compile/QueryCompiler.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/compile/QueryCompiler.java
@@ -30,6 +30,7 @@ import org.apache.phoenix.compile.JoinCompiler.JoinTable;
 import org.apache.phoenix.compile.JoinCompiler.JoinedTableColumnResolver;
 import org.apache.phoenix.compile.JoinCompiler.PTableWrapper;
 import org.apache.phoenix.compile.JoinCompiler.ProjectedPTableWrapper;
+import org.apache.phoenix.compile.JoinCompiler.Table;
 import org.apache.phoenix.compile.OrderByCompiler.OrderBy;
 import org.apache.phoenix.execute.AggregatePlan;
 import org.apache.phoenix.execute.BasicQueryPlan;
@@ -116,37 +117,39 @@ public class QueryCompiler {
         SelectStatement select = this.select;
         List<Object> binds = statement.getParameters();
         StatementContext context = new StatementContext(statement, resolver, scan);
-        if (select.getFrom().size() > 1) {
+        if (select.isJoin()) {
             select = JoinCompiler.optimize(context, select, statement);
             if (this.select != select) {
                 ColumnResolver resolver = FromCompiler.getResolverForQuery(select, statement.getConnection());
                 context = new StatementContext(statement, resolver, scan);
             }
-            JoinSpec join = JoinCompiler.getJoinSpec(context, select);
-            return compileJoinQuery(context, select, binds, join, false);
+            JoinTable joinTable = JoinCompiler.compile(select, context.getResolver());
+            return compileJoinQuery(context, binds, joinTable, false);
         } else {
             return compileSingleQuery(context, select, binds, parallelIteratorFactory);
         }
     }
     
     @SuppressWarnings("unchecked")
-    protected QueryPlan compileJoinQuery(StatementContext context, SelectStatement select, List<Object> binds, JoinSpec join, boolean asSubquery) throws SQLException {
+    protected QueryPlan compileJoinQuery(StatementContext context, List<Object> binds, JoinTable joinTable, boolean asSubquery) throws SQLException {
         byte[] emptyByteArray = new byte[0];
-        List<JoinTable> joinTables = join.getJoinTables();
-        if (joinTables.isEmpty()) {
-            ProjectedPTableWrapper projectedTable = join.createProjectedTable(join.getMainTable(), !asSubquery);
+        List<JoinSpec> joinSpecs = joinTable.getJoinSpecs();
+        if (joinSpecs.isEmpty()) {
+            Table table = joinTable.getTable();
+            ProjectedPTableWrapper projectedTable = table.createProjectedTable(!asSubquery);
             ScanProjector.serializeProjectorIntoScan(context.getScan(), JoinCompiler.getScanProjector(projectedTable));
-            context.setCurrentTable(join.getMainTable());
-            context.setResolver(join.getColumnResolver(projectedTable));
-            join.projectColumns(context.getScan(), join.getMainTable());
-            return compileSingleQuery(context, select, binds, null);
+            context.setCurrentTable(table.getTableRef());
+            context.setResolver(projectedTable.createColumnResolver());
+            table.projectColumns(context.getScan());
+            return compileSingleQuery(context, table.getAsSubquery(), binds, null);
         }
         
-        boolean[] starJoinVector = join.getStarJoinVector();
+        boolean[] starJoinVector = joinTable.getStarJoinVector();
         if (starJoinVector != null) {
-            ProjectedPTableWrapper initialProjectedTable = join.createProjectedTable(join.getMainTable(), !asSubquery);
+            Table table = joinTable.getTable();
+            ProjectedPTableWrapper initialProjectedTable = table.createProjectedTable(!asSubquery);
             PTableWrapper projectedTable = initialProjectedTable;
-            int count = joinTables.size();
+            int count = joinSpecs.size();
             ImmutableBytesPtr[] joinIds = new ImmutableBytesPtr[count];
             List<Expression>[] joinExpressions = new List[count];
             List<Expression>[] hashExpressions = new List[count];
@@ -157,22 +160,16 @@ public class QueryCompiler {
             fieldPositions[0] = projectedTable.getTable().getColumns().size() - projectedTable.getTable().getPKColumns().size();
             boolean needsProject = asSubquery;
             for (int i = 0; i < count; i++) {
-                JoinTable joinTable = joinTables.get(i);
-                SelectStatement subStatement = joinTable.getAsSubquery();
-                if (subStatement.getFrom().size() > 1)
-                    throw new SQLFeatureNotSupportedException("Sub queries not supported.");
-                ProjectedPTableWrapper subProjTable = join.createProjectedTable(joinTable.getTable(), false);
-                ColumnResolver resolver = join.getColumnResolver(subProjTable);
+                JoinSpec joinSpec = joinSpecs.get(i);
                 Scan subScan = ScanUtil.newScan(originalScan);
-                ScanProjector.serializeProjectorIntoScan(subScan, JoinCompiler.getScanProjector(subProjTable));
-                StatementContext subContext = new StatementContext(statement, resolver, subScan);
-                subContext.setCurrentTable(joinTable.getTable());
-                join.projectColumns(subScan, joinTable.getTable());
-                joinPlans[i] = compileSingleQuery(subContext, subStatement, binds, null);
-                boolean hasPostReference = join.hasPostReference(joinTable.getTable());
+                StatementContext subContext = new StatementContext(statement, context.getResolver(), subScan);
+                joinPlans[i] = compileJoinQuery(subContext, binds, joinSpec.getJoinTable(), true);
+                ColumnResolver resolver = subContext.getResolver();
+                boolean hasPostReference = joinSpec.getJoinTable().hasPostReference();
                 if (hasPostReference) {
+                    PTableWrapper subProjTable = ((JoinedTableColumnResolver) (resolver)).getPTableWrapper();
                     tables[i] = subProjTable.getTable();
-                    projectedTable = JoinCompiler.mergeProjectedTables(projectedTable, subProjTable, joinTable.getType() == JoinType.Inner);
+                    projectedTable = projectedTable.mergeProjectedTables(subProjTable, joinSpec.getType() == JoinType.Inner);
                     needsProject = true;
                 } else {
                     tables[i] = null;
@@ -180,12 +177,12 @@ public class QueryCompiler {
                 if (!starJoinVector[i]) {
                     needsProject = true;
                 }
-                ColumnResolver leftResolver = starJoinVector[i] ? join.getOriginalResolver() : join.getColumnResolver(projectedTable);
+                ColumnResolver leftResolver = starJoinVector[i] ? joinTable.getOriginalResolver() : projectedTable.createColumnResolver();
                 joinIds[i] = new ImmutableBytesPtr(emptyByteArray); // place-holder
-                Pair<List<Expression>, List<Expression>> joinConditions = joinTable.compileJoinConditions(context, leftResolver, resolver);
+                Pair<List<Expression>, List<Expression>> joinConditions = joinSpec.compileJoinConditions(context, leftResolver, resolver);
                 joinExpressions[i] = joinConditions.getFirst();
                 hashExpressions[i] = joinConditions.getSecond();
-                joinTypes[i] = joinTable.getType();
+                joinTypes[i] = joinSpec.getType();
                 if (i < count - 1) {
                     fieldPositions[i + 1] = fieldPositions[i] + (tables[i] == null ? 0 : (tables[i].getColumns().size() - tables[i].getPKColumns().size()));
                 }
@@ -193,43 +190,47 @@ public class QueryCompiler {
             if (needsProject) {
                 ScanProjector.serializeProjectorIntoScan(context.getScan(), JoinCompiler.getScanProjector(initialProjectedTable));
             }
-            context.setCurrentTable(join.getMainTable());
-            context.setResolver(needsProject ? join.getColumnResolver(projectedTable) : join.getOriginalResolver());
-            join.projectColumns(context.getScan(), join.getMainTable());
-            BasicQueryPlan plan = compileSingleQuery(context, JoinCompiler.getSubqueryWithoutJoin(select, join), binds, parallelIteratorFactory);
-            Expression postJoinFilterExpression = join.compilePostFilterExpression(context);
+            context.setCurrentTable(table.getTableRef());
+            context.setResolver(needsProject ? projectedTable.createColumnResolver() : joinTable.getOriginalResolver());
+            table.projectColumns(context.getScan());
+            BasicQueryPlan plan = compileSingleQuery(context, joinTable.getSubqueryWithoutJoin(asSubquery), binds, parallelIteratorFactory);
+            Expression postJoinFilterExpression = joinTable.compilePostFilterExpression(context);
             HashJoinInfo joinInfo = new HashJoinInfo(projectedTable.getTable(), joinIds, joinExpressions, joinTypes, starJoinVector, tables, fieldPositions, postJoinFilterExpression);
             return new HashJoinPlan(plan, joinInfo, hashExpressions, joinPlans);
         }
         
-        JoinTable lastJoinTable = joinTables.get(joinTables.size() - 1);
-        JoinType type = lastJoinTable.getType();
+        JoinSpec lastJoinSpec = joinSpecs.get(joinSpecs.size() - 1);
+        JoinType type = lastJoinSpec.getType();
         if (type == JoinType.Full)
             throw new SQLFeatureNotSupportedException("Full joins not supported.");
         
         if (type == JoinType.Right || type == JoinType.Inner) {
-            SelectStatement lhs = JoinCompiler.getSubQueryWithoutLastJoin(select, join);
-            SelectStatement rhs = JoinCompiler.getSubqueryForLastJoinTable(select, join);
-            JoinSpec lhsJoin = JoinCompiler.getSubJoinSpecWithoutPostFilters(join);
+            if (!lastJoinSpec.getJoinTable().getJoinSpecs().isEmpty())
+                throw new SQLFeatureNotSupportedException("Right join followed by sub-join is not supported.");
+            
+            JoinTable rhsJoinTable = lastJoinSpec.getJoinTable();
+            Table rhsTable = rhsJoinTable.getTable();
+            SelectStatement rhs = rhsJoinTable.getSubqueryWithoutJoin(asSubquery);
+            JoinTable lhsJoin = joinTable.getSubJoinTableWithoutPostFilters();
             Scan subScan = ScanUtil.newScan(originalScan);
             StatementContext lhsCtx = new StatementContext(statement, context.getResolver(), subScan);
-            QueryPlan lhsPlan = compileJoinQuery(lhsCtx, lhs, binds, lhsJoin, true);
+            QueryPlan lhsPlan = compileJoinQuery(lhsCtx, binds, lhsJoin, true);
             ColumnResolver lhsResolver = lhsCtx.getResolver();
             PTableWrapper lhsProjTable = ((JoinedTableColumnResolver) (lhsResolver)).getPTableWrapper();
-            ProjectedPTableWrapper rhsProjTable = join.createProjectedTable(lastJoinTable.getTable(), !asSubquery);
-            ColumnResolver rhsResolver = join.getOriginalResolver();
+            ProjectedPTableWrapper rhsProjTable = rhsTable.createProjectedTable(!asSubquery);
+            ColumnResolver rhsResolver = joinTable.getOriginalResolver();
             ImmutableBytesPtr[] joinIds = new ImmutableBytesPtr[] {new ImmutableBytesPtr(emptyByteArray)};
-            Pair<List<Expression>, List<Expression>> joinConditions = lastJoinTable.compileJoinConditions(context, lhsResolver, rhsResolver);
+            Pair<List<Expression>, List<Expression>> joinConditions = lastJoinSpec.compileJoinConditions(context, lhsResolver, rhsResolver);
             List<Expression> joinExpressions = joinConditions.getSecond();
             List<Expression> hashExpressions = joinConditions.getFirst();
             int fieldPosition = rhsProjTable.getTable().getColumns().size() - rhsProjTable.getTable().getPKColumns().size();
-            PTableWrapper projectedTable = JoinCompiler.mergeProjectedTables(rhsProjTable, lhsProjTable, type == JoinType.Inner);
+            PTableWrapper projectedTable = rhsProjTable.mergeProjectedTables(lhsProjTable, type == JoinType.Inner);
             ScanProjector.serializeProjectorIntoScan(context.getScan(), JoinCompiler.getScanProjector(rhsProjTable));
-            context.setCurrentTable(lastJoinTable.getTable());
-            context.setResolver(join.getColumnResolver(projectedTable));
-            join.projectColumns(context.getScan(), lastJoinTable.getTable());
+            context.setCurrentTable(rhsTable.getTableRef());
+            context.setResolver(projectedTable.createColumnResolver());
+            rhsTable.projectColumns(context.getScan());
             BasicQueryPlan rhsPlan = compileSingleQuery(context, rhs, binds, parallelIteratorFactory);
-            Expression postJoinFilterExpression = join.compilePostFilterExpression(context);
+            Expression postJoinFilterExpression = joinTable.compilePostFilterExpression(context);
             HashJoinInfo joinInfo = new HashJoinInfo(projectedTable.getTable(), joinIds, new List[] {joinExpressions}, new JoinType[] {type == JoinType.Inner ? type : JoinType.Left}, new boolean[] {true}, new PTable[] {lhsProjTable.getTable()}, new int[] {fieldPosition}, postJoinFilterExpression);
             return new HashJoinPlan(rhsPlan, joinInfo, new List[] {hashExpressions}, new QueryPlan[] {lhsPlan});
         }

http://git-wip-us.apache.org/repos/asf/incubator-phoenix/blob/12bd7aed/phoenix-core/src/main/java/org/apache/phoenix/compile/StatementNormalizer.java
----------------------------------------------------------------------
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/compile/StatementNormalizer.java b/phoenix-core/src/main/java/org/apache/phoenix/compile/StatementNormalizer.java
index 765ebb8..0435301 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/compile/StatementNormalizer.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/compile/StatementNormalizer.java
@@ -19,6 +19,7 @@ package org.apache.phoenix.compile;
 
 import java.sql.SQLException;
 import java.sql.SQLFeatureNotSupportedException;
+import java.util.Collections;
 import java.util.List;
 
 import com.google.common.collect.Lists;
@@ -74,8 +75,7 @@ public class StatementNormalizer extends ParseNodeRewriter {
      * @throws SQLException 
      */
     public static SelectStatement normalize(SelectStatement statement, ColumnResolver resolver) throws SQLException {
-        List<TableNode> from = statement.getFrom();
-        boolean multiTable = from.size() > 1;
+        boolean multiTable = statement.isJoin();
         // Replace WildcardParse with a list of TableWildcardParseNode for multi-table queries
         if (multiTable) {
             List<AliasedNode> selectNodes = statement.getSelect();
@@ -87,11 +87,13 @@ public class StatementNormalizer extends ParseNodeRewriter {
                     if (selectNodes == normSelectNodes) {
                         normSelectNodes = Lists.newArrayList(selectNodes.subList(0, i));
                     }
-                    for (TableNode tNode : from) {
+                    for (TableNode tNode : statement.getFrom()) {
                         TableNameVisitor visitor = new TableNameVisitor();
-                        tNode.accept(visitor);
-                        TableWildcardParseNode node = NODE_FACTORY.tableWildcard(visitor.getTableName());
-                        normSelectNodes.add(NODE_FACTORY.aliasedNode(null, node));
+                        List<TableName> tableNames = tNode.accept(visitor);
+                        for (TableName tableName : tableNames) {
+                            TableWildcardParseNode node = NODE_FACTORY.tableWildcard(tableName);
+                            normSelectNodes.add(NODE_FACTORY.aliasedNode(null, node));
+                        }
                     }
                 } else if (selectNodes != normSelectNodes) {
                     normSelectNodes.add(aliasedNode);
@@ -107,31 +109,33 @@ public class StatementNormalizer extends ParseNodeRewriter {
         return rewrite(statement, new StatementNormalizer(resolver, statement.getSelect().size(), multiTable));
     }
 
-    private static class TableNameVisitor implements TableNodeVisitor {
-        private TableName tableName;
-        
-        public TableName getTableName() {
-            return tableName;
-        }
+    private static class TableNameVisitor implements TableNodeVisitor<List<TableName>> {
 
         @Override
-        public void visit(BindTableNode boundTableNode) throws SQLException {
-            tableName = boundTableNode.getAlias() == null ? boundTableNode.getName() : TableName.create(null, boundTableNode.getAlias());
+        public List<TableName> visit(BindTableNode boundTableNode) throws SQLException {
+            TableName name = boundTableNode.getAlias() == null ? boundTableNode.getName() : TableName.create(null, boundTableNode.getAlias());
+            return Collections.singletonList(name);
         }
 
         @Override
-        public void visit(JoinTableNode joinNode) throws SQLException {
-            joinNode.getTable().accept(this);
+        public List<TableName> visit(JoinTableNode joinNode) throws SQLException {
+            List<TableName> lhs = joinNode.getLHS().accept(this);
+            List<TableName> rhs = joinNode.getRHS().accept(this);
+            List<TableName> ret = Lists.<TableName>newArrayListWithExpectedSize(lhs.size() + rhs.size());
+            ret.addAll(lhs);
+            ret.addAll(rhs);
+            return ret;
         }
 
         @Override
-        public void visit(NamedTableNode namedTableNode)
+        public List<TableName> visit(NamedTableNode namedTableNode)
                 throws SQLException {
-            tableName = namedTableNode.getAlias() == null ? namedTableNode.getName() : TableName.create(null, namedTableNode.getAlias());
+            TableName name = namedTableNode.getAlias() == null ? namedTableNode.getName() : TableName.create(null, namedTableNode.getAlias());
+            return Collections.singletonList(name);
         }
 
         @Override
-        public void visit(DerivedTableNode subselectNode)
+        public List<TableName> visit(DerivedTableNode subselectNode)
                 throws SQLException {
             throw new SQLFeatureNotSupportedException();
         }

http://git-wip-us.apache.org/repos/asf/incubator-phoenix/blob/12bd7aed/phoenix-core/src/main/java/org/apache/phoenix/optimize/QueryOptimizer.java
----------------------------------------------------------------------
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/optimize/QueryOptimizer.java b/phoenix-core/src/main/java/org/apache/phoenix/optimize/QueryOptimizer.java
index 220ef89..b275e3d 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/optimize/QueryOptimizer.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/optimize/QueryOptimizer.java
@@ -80,7 +80,7 @@ public class QueryOptimizer {
         // TODO: the recompile for the index tables could skip the normalize step
         SelectStatement select = (SelectStatement)dataPlan.getStatement();
         // TODO: consider not even compiling index plans if we have a point lookup
-        if (!useIndexes || select.getFrom().size() > 1) {
+        if (!useIndexes || select.isJoin()) {
             return dataPlan;
         }
         PTable dataTable = dataPlan.getTableRef().getTable();

http://git-wip-us.apache.org/repos/asf/incubator-phoenix/blob/12bd7aed/phoenix-core/src/main/java/org/apache/phoenix/parse/BindTableNode.java
----------------------------------------------------------------------
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/parse/BindTableNode.java b/phoenix-core/src/main/java/org/apache/phoenix/parse/BindTableNode.java
index c83b585..52a8948 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/parse/BindTableNode.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/parse/BindTableNode.java
@@ -35,8 +35,8 @@ public class BindTableNode extends ConcreteTableNode {
     }
 
     @Override
-    public void accept(TableNodeVisitor visitor) throws SQLException {
-        visitor.visit(this);
+    public <T> T accept(TableNodeVisitor<T> visitor) throws SQLException {
+        return visitor.visit(this);
     }
 
 }

http://git-wip-us.apache.org/repos/asf/incubator-phoenix/blob/12bd7aed/phoenix-core/src/main/java/org/apache/phoenix/parse/DerivedTableNode.java
----------------------------------------------------------------------
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/parse/DerivedTableNode.java b/phoenix-core/src/main/java/org/apache/phoenix/parse/DerivedTableNode.java
index 0fcde20..b86c76d 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/parse/DerivedTableNode.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/parse/DerivedTableNode.java
@@ -44,8 +44,8 @@ public class DerivedTableNode extends TableNode {
     }
 
     @Override
-    public void accept(TableNodeVisitor visitor) throws SQLException {
-        visitor.visit(this);
+    public <T> T accept(TableNodeVisitor<T> visitor) throws SQLException {
+        return visitor.visit(this);
     }
 
 }

http://git-wip-us.apache.org/repos/asf/incubator-phoenix/blob/12bd7aed/phoenix-core/src/main/java/org/apache/phoenix/parse/JoinPartNode.java
----------------------------------------------------------------------
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/parse/JoinPartNode.java b/phoenix-core/src/main/java/org/apache/phoenix/parse/JoinPartNode.java
new file mode 100644
index 0000000..cdbaaea
--- /dev/null
+++ b/phoenix-core/src/main/java/org/apache/phoenix/parse/JoinPartNode.java
@@ -0,0 +1,53 @@
+/*
+ * 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.parse;
+
+import org.apache.phoenix.parse.JoinTableNode.JoinType;
+
+/**
+ * 
+ * Node representing the partial join clause in the FROM clause of SQL
+ *
+ * 
+ * @since 0.1
+ */
+class JoinPartNode {
+    
+    private final JoinType type;
+    private final ParseNode onNode;
+    private final TableNode table;
+    
+    JoinPartNode(JoinType type, ParseNode onNode, TableNode table) {
+        this.type = type;
+        this.onNode = onNode;
+        this.table = table;
+    }
+
+    JoinType getType() {
+        return type;
+    }
+    
+    ParseNode getOnNode() {
+        return onNode;
+    }
+    
+    TableNode getTable() {
+        return table;
+    }
+}
+

http://git-wip-us.apache.org/repos/asf/incubator-phoenix/blob/12bd7aed/phoenix-core/src/main/java/org/apache/phoenix/parse/JoinTableNode.java
----------------------------------------------------------------------
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/parse/JoinTableNode.java b/phoenix-core/src/main/java/org/apache/phoenix/parse/JoinTableNode.java
index 1e06b5e..cbd6bce 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/parse/JoinTableNode.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/parse/JoinTableNode.java
@@ -32,32 +32,37 @@ public class JoinTableNode extends TableNode {
     public enum JoinType {Inner, Left, Right, Full};
     
     private final JoinType type;
-    private final ParseNode on;
-    private final TableNode table;
+    private final TableNode lhs;
+    private final TableNode rhs;
+    private final ParseNode onNode;
     
-    JoinTableNode(JoinType type, ParseNode on, TableNode table) {
-        super(table.getAlias());
+    JoinTableNode(JoinType type, TableNode lhs, TableNode rhs, ParseNode onNode) {
+        super(null);
         this.type = type;
-        this.on = on;
-        this.table = table;
+        this.lhs = lhs;
+        this.rhs = rhs;
+        this.onNode = onNode;
     }
-
+    
     public JoinType getType() {
         return type;
     }
-
-    public ParseNode getOnNode() {
-        return on;
+    
+    public TableNode getLHS() {
+        return lhs;
     }
     
-    public TableNode getTable() {
-        return table;
+    public TableNode getRHS() {
+        return rhs;
+    }
+    
+    public ParseNode getOnNode() {
+        return onNode;
     }
 
     @Override
-    public void accept(TableNodeVisitor visitor) throws SQLException {
-        visitor.visit(this);
+    public <T> T accept(TableNodeVisitor<T> visitor) throws SQLException {
+        return visitor.visit(this);
     }
-
 }
 

http://git-wip-us.apache.org/repos/asf/incubator-phoenix/blob/12bd7aed/phoenix-core/src/main/java/org/apache/phoenix/parse/NamedTableNode.java
----------------------------------------------------------------------
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/parse/NamedTableNode.java b/phoenix-core/src/main/java/org/apache/phoenix/parse/NamedTableNode.java
index 065c7b2..9379919 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/parse/NamedTableNode.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/parse/NamedTableNode.java
@@ -52,8 +52,8 @@ public class NamedTableNode extends ConcreteTableNode {
     }
 
     @Override
-    public void accept(TableNodeVisitor visitor) throws SQLException {
-        visitor.visit(this);
+    public <T> T accept(TableNodeVisitor<T> visitor) throws SQLException {
+        return visitor.visit(this);
     }
 
     public List<ColumnDef> getDynamicColumns() {

http://git-wip-us.apache.org/repos/asf/incubator-phoenix/blob/12bd7aed/phoenix-core/src/main/java/org/apache/phoenix/parse/ParseNodeFactory.java
----------------------------------------------------------------------
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/parse/ParseNodeFactory.java b/phoenix-core/src/main/java/org/apache/phoenix/parse/ParseNodeFactory.java
index 2523e29..41f4c6c 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/parse/ParseNodeFactory.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/parse/ParseNodeFactory.java
@@ -397,8 +397,20 @@ public class ParseNodeFactory {
         return new IsNullParseNode(child, negate);
     }
 
-    public JoinTableNode join (JoinType type, ParseNode on, TableNode table) {
-        return new JoinTableNode(type, on, table);
+    public TableNode table(TableNode table, List<JoinPartNode> parts) {
+        for (JoinPartNode part : parts) {
+            table = new JoinTableNode(part.getType(), table, part.getTable(), part.getOnNode());
+        }
+        
+        return table;
+    }
+    
+    JoinPartNode joinPart(JoinType type, ParseNode onNode, TableNode table) {
+        return new JoinPartNode(type, onNode, table);
+    }
+
+    public JoinTableNode join(JoinType type, TableNode lhs, TableNode rhs, ParseNode on) {
+        return new JoinTableNode(type, lhs, rhs, on);
     }
 
     public DerivedTableNode derivedTable (String alias, SelectStatement select) {

http://git-wip-us.apache.org/repos/asf/incubator-phoenix/blob/12bd7aed/phoenix-core/src/main/java/org/apache/phoenix/parse/ParseNodeRewriter.java
----------------------------------------------------------------------
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/parse/ParseNodeRewriter.java b/phoenix-core/src/main/java/org/apache/phoenix/parse/ParseNodeRewriter.java
index 6e15c0f..bff5834 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/parse/ParseNodeRewriter.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/parse/ParseNodeRewriter.java
@@ -59,29 +59,21 @@ public class ParseNodeRewriter extends TraverseAllParseNodeVisitor<ParseNode> {
         Map<String,ParseNode> aliasMap = rewriter.getAliasMap();
         List<TableNode> from = statement.getFrom();
         List<TableNode> normFrom = from;
-        if (from.size() > 1) {
-        	for (int i = 1; i < from.size(); i++) {
-        		TableNode tableNode = from.get(i);
-        		if (tableNode instanceof JoinTableNode) {
-        			JoinTableNode joinTableNode = (JoinTableNode) tableNode;
-        			ParseNode onNode = joinTableNode.getOnNode();
-        			rewriter.reset();
-        			ParseNode normOnNode = onNode.accept(rewriter);
-        			if (onNode == normOnNode) {
-        				if (from != normFrom) {
-        					normFrom.add(tableNode);
-        				}
-        				continue;
-        			}
-        			if (from == normFrom) {
-        				normFrom = Lists.newArrayList(from.subList(0, i));
-        			}
-        			TableNode normTableNode = NODE_FACTORY.join(joinTableNode.getType(), normOnNode, joinTableNode.getTable());
-        			normFrom.add(normTableNode);
-        		} else if (from != normFrom) {
-					normFrom.add(tableNode);
-				}
-        	}
+        TableNodeRewriter tableNodeRewriter = new TableNodeRewriter(rewriter);
+        for (int i = 0; i < from.size(); i++) {
+            TableNode tableNode = from.get(i);
+            tableNodeRewriter.reset();
+            TableNode normTableNode = tableNode.accept(tableNodeRewriter);
+            if (normTableNode == tableNode) {
+                if (from != normFrom) {
+                    normFrom.add(tableNode);
+                }
+                continue;
+            }
+            if (from == normFrom) {
+                normFrom = Lists.newArrayList(from.subList(0, i));        		    
+            }
+            normFrom.add(normTableNode);
         }
         ParseNode where = statement.getWhere();
         ParseNode normWhere = where;
@@ -497,4 +489,47 @@ public class ParseNodeRewriter extends TraverseAllParseNodeVisitor<ParseNode> {
             }
         });
 	}
+	
+	private static class TableNodeRewriter implements TableNodeVisitor<TableNode> {
+	    private final ParseNodeRewriter parseNodeRewriter;
+	    
+	    public TableNodeRewriter(ParseNodeRewriter parseNodeRewriter) {
+	        this.parseNodeRewriter = parseNodeRewriter;
+	    }
+	    
+	    public void reset() {
+	    }
+
+        @Override
+        public TableNode visit(BindTableNode boundTableNode) throws SQLException {
+            return boundTableNode;
+        }
+
+        @Override
+        public TableNode visit(JoinTableNode joinNode) throws SQLException {
+            TableNode lhsNode = joinNode.getLHS();
+            TableNode rhsNode = joinNode.getRHS();
+            ParseNode onNode = joinNode.getOnNode();
+            TableNode normLhsNode = lhsNode.accept(this);
+            TableNode normRhsNode = rhsNode.accept(this);
+            parseNodeRewriter.reset();
+            ParseNode normOnNode = onNode.accept(parseNodeRewriter);
+            if (lhsNode == normLhsNode && rhsNode == normRhsNode && onNode == normOnNode)
+                return joinNode;
+            
+            return NODE_FACTORY.join(joinNode.getType(), normLhsNode, normRhsNode, normOnNode);
+        }
+
+        @Override
+        public TableNode visit(NamedTableNode namedTableNode) throws SQLException {
+            return namedTableNode;
+            
+        }
+
+        @Override
+        public TableNode visit(DerivedTableNode subselectNode) throws SQLException {
+            return subselectNode;
+        }
+	    
+	}
 }

http://git-wip-us.apache.org/repos/asf/incubator-phoenix/blob/12bd7aed/phoenix-core/src/main/java/org/apache/phoenix/parse/SelectStatement.java
----------------------------------------------------------------------
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/parse/SelectStatement.java b/phoenix-core/src/main/java/org/apache/phoenix/parse/SelectStatement.java
index c3c0f8d..8caea30 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/parse/SelectStatement.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/parse/SelectStatement.java
@@ -186,4 +186,8 @@ public class SelectStatement implements FilterableStatement {
     public Action getSequenceAction() {
         return Action.RESERVE;
     }
+    
+    public boolean isJoin() {
+        return fromTable.size() > 1 || (fromTable.size() > 0 && fromTable.get(0) instanceof JoinTableNode);
+    }
 }

http://git-wip-us.apache.org/repos/asf/incubator-phoenix/blob/12bd7aed/phoenix-core/src/main/java/org/apache/phoenix/parse/TableNode.java
----------------------------------------------------------------------
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/parse/TableNode.java b/phoenix-core/src/main/java/org/apache/phoenix/parse/TableNode.java
index 66cff35..7ab8d0c 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/parse/TableNode.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/parse/TableNode.java
@@ -39,6 +39,6 @@ public abstract class TableNode {
         return alias;
     }
 
-    public abstract void accept(TableNodeVisitor visitor) throws SQLException;
+    public abstract <T> T accept(TableNodeVisitor<T> visitor) throws SQLException;
 }
 

http://git-wip-us.apache.org/repos/asf/incubator-phoenix/blob/12bd7aed/phoenix-core/src/main/java/org/apache/phoenix/parse/TableNodeVisitor.java
----------------------------------------------------------------------
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/parse/TableNodeVisitor.java b/phoenix-core/src/main/java/org/apache/phoenix/parse/TableNodeVisitor.java
index f275275..8d5e4e7 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/parse/TableNodeVisitor.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/parse/TableNodeVisitor.java
@@ -27,9 +27,9 @@ import java.sql.SQLException;
  * 
  * @since 0.1
  */
-public interface TableNodeVisitor {
-    void visit(BindTableNode boundTableNode) throws SQLException;
-    void visit(JoinTableNode joinNode) throws SQLException;
-    void visit(NamedTableNode namedTableNode) throws SQLException;
-    void visit(DerivedTableNode subselectNode) throws SQLException;
+public interface TableNodeVisitor<E> {
+    E visit(BindTableNode boundTableNode) throws SQLException;
+    E visit(JoinTableNode joinNode) throws SQLException;
+    E visit(NamedTableNode namedTableNode) throws SQLException;
+    E visit(DerivedTableNode subselectNode) throws SQLException;
 }

http://git-wip-us.apache.org/repos/asf/incubator-phoenix/blob/12bd7aed/phoenix-core/src/test/java/org/apache/phoenix/compile/JoinQueryCompilerTest.java
----------------------------------------------------------------------
diff --git a/phoenix-core/src/test/java/org/apache/phoenix/compile/JoinQueryCompilerTest.java b/phoenix-core/src/test/java/org/apache/phoenix/compile/JoinQueryCompilerTest.java
index e6d5f69..b64fe56 100644
--- a/phoenix-core/src/test/java/org/apache/phoenix/compile/JoinQueryCompilerTest.java
+++ b/phoenix-core/src/test/java/org/apache/phoenix/compile/JoinQueryCompilerTest.java
@@ -34,7 +34,7 @@ import java.sql.ResultSet;
 import java.sql.SQLException;
 
 import org.apache.hadoop.hbase.client.Scan;
-import org.apache.phoenix.compile.JoinCompiler.JoinSpec;
+import org.apache.phoenix.compile.JoinCompiler.JoinTable;
 import org.apache.phoenix.jdbc.PhoenixConnection;
 import org.apache.phoenix.jdbc.PhoenixStatement;
 import org.apache.phoenix.parse.SQLParser;
@@ -80,62 +80,62 @@ public class JoinQueryCompilerTest extends BaseConnectionlessQueryTest {
                 + "WHERE t1.\"item_id\" = '0000000001' AND t2.\"item_id\" = '0000000002' AND t3.\"item_id\" = '0000000003'";
 
         String query = String.format(queryTemplate, "INNER", "INNER");
-        JoinSpec joinSpec = getJoinSpec(query, pconn);
-        assertEquals(1, joinSpec.getPreFilters().size());
-        assertEquals(1, joinSpec.getJoinTables().get(0).getPreFilters().size());
-        assertEquals(1, joinSpec.getJoinTables().get(1).getPreFilters().size());
+        JoinTable joinTable = getJoinTable(query, pconn);
+        assertEquals(1, joinTable.getTable().getPreFilters().size());
+        assertEquals(1, joinTable.getJoinSpecs().get(0).getJoinTable().getTable().getPreFilters().size());
+        assertEquals(1, joinTable.getJoinSpecs().get(1).getJoinTable().getTable().getPreFilters().size());
 
         query = String.format(queryTemplate, "INNER", "LEFT");
-        joinSpec = getJoinSpec(query, pconn);
-        assertEquals(1, joinSpec.getPreFilters().size());
-        assertEquals(1, joinSpec.getJoinTables().get(0).getPreFilters().size());
-        assertEquals(0, joinSpec.getJoinTables().get(1).getPreFilters().size());
+        joinTable = getJoinTable(query, pconn);
+        assertEquals(1, joinTable.getTable().getPreFilters().size());
+        assertEquals(1, joinTable.getJoinSpecs().get(0).getJoinTable().getTable().getPreFilters().size());
+        assertEquals(0, joinTable.getJoinSpecs().get(1).getJoinTable().getTable().getPreFilters().size());
 
         query = String.format(queryTemplate, "INNER", "RIGHT");
-        joinSpec = getJoinSpec(query, pconn);
-        assertEquals(0, joinSpec.getPreFilters().size());
-        assertEquals(0, joinSpec.getJoinTables().get(0).getPreFilters().size());
-        assertEquals(1, joinSpec.getJoinTables().get(1).getPreFilters().size());
+        joinTable = getJoinTable(query, pconn);
+        assertEquals(0, joinTable.getTable().getPreFilters().size());
+        assertEquals(0, joinTable.getJoinSpecs().get(0).getJoinTable().getTable().getPreFilters().size());
+        assertEquals(1, joinTable.getJoinSpecs().get(1).getJoinTable().getTable().getPreFilters().size());
 
         query = String.format(queryTemplate, "LEFT", "INNER");
-        joinSpec = getJoinSpec(query, pconn);
-        assertEquals(1, joinSpec.getPreFilters().size());
-        assertEquals(0, joinSpec.getJoinTables().get(0).getPreFilters().size());
-        assertEquals(1, joinSpec.getJoinTables().get(1).getPreFilters().size());
+        joinTable = getJoinTable(query, pconn);
+        assertEquals(1, joinTable.getTable().getPreFilters().size());
+        assertEquals(0, joinTable.getJoinSpecs().get(0).getJoinTable().getTable().getPreFilters().size());
+        assertEquals(1, joinTable.getJoinSpecs().get(1).getJoinTable().getTable().getPreFilters().size());
 
         query = String.format(queryTemplate, "LEFT", "LEFT");
-        joinSpec = getJoinSpec(query, pconn);
-        assertEquals(1, joinSpec.getPreFilters().size());
-        assertEquals(0, joinSpec.getJoinTables().get(0).getPreFilters().size());
-        assertEquals(0, joinSpec.getJoinTables().get(1).getPreFilters().size());
+        joinTable = getJoinTable(query, pconn);
+        assertEquals(1, joinTable.getTable().getPreFilters().size());
+        assertEquals(0, joinTable.getJoinSpecs().get(0).getJoinTable().getTable().getPreFilters().size());
+        assertEquals(0, joinTable.getJoinSpecs().get(1).getJoinTable().getTable().getPreFilters().size());
 
         query = String.format(queryTemplate, "LEFT", "RIGHT");
-        joinSpec = getJoinSpec(query, pconn);
-        assertEquals(0, joinSpec.getPreFilters().size());
-        assertEquals(0, joinSpec.getJoinTables().get(0).getPreFilters().size());
-        assertEquals(1, joinSpec.getJoinTables().get(1).getPreFilters().size());
+        joinTable = getJoinTable(query, pconn);
+        assertEquals(0, joinTable.getTable().getPreFilters().size());
+        assertEquals(0, joinTable.getJoinSpecs().get(0).getJoinTable().getTable().getPreFilters().size());
+        assertEquals(1, joinTable.getJoinSpecs().get(1).getJoinTable().getTable().getPreFilters().size());
 
         query = String.format(queryTemplate, "RIGHT", "INNER");
-        joinSpec = getJoinSpec(query, pconn);
-        assertEquals(0, joinSpec.getPreFilters().size());
-        assertEquals(1, joinSpec.getJoinTables().get(0).getPreFilters().size());
-        assertEquals(1, joinSpec.getJoinTables().get(1).getPreFilters().size());
+        joinTable = getJoinTable(query, pconn);
+        assertEquals(0, joinTable.getTable().getPreFilters().size());
+        assertEquals(1, joinTable.getJoinSpecs().get(0).getJoinTable().getTable().getPreFilters().size());
+        assertEquals(1, joinTable.getJoinSpecs().get(1).getJoinTable().getTable().getPreFilters().size());
 
         query = String.format(queryTemplate, "RIGHT", "RIGHT");
-        joinSpec = getJoinSpec(query, pconn);
-        assertEquals(0, joinSpec.getPreFilters().size());
-        assertEquals(0, joinSpec.getJoinTables().get(0).getPreFilters().size());
-        assertEquals(1, joinSpec.getJoinTables().get(1).getPreFilters().size());
+        joinTable = getJoinTable(query, pconn);
+        assertEquals(0, joinTable.getTable().getPreFilters().size());
+        assertEquals(0, joinTable.getJoinSpecs().get(0).getJoinTable().getTable().getPreFilters().size());
+        assertEquals(1, joinTable.getJoinSpecs().get(1).getJoinTable().getTable().getPreFilters().size());
     }
     
-    private static JoinSpec getJoinSpec(String query, PhoenixConnection connection) throws SQLException {
+    private static JoinTable getJoinTable(String query, PhoenixConnection connection) throws SQLException {
         Scan scan = new Scan();
         SQLParser parser = new SQLParser(query);
         SelectStatement select = parser.parseQuery();
         ColumnResolver resolver = FromCompiler.getResolverForQuery(select, connection);
         select = StatementNormalizer.normalize(select, resolver);
         StatementContext context = new StatementContext(new PhoenixStatement(connection), resolver, scan);
-        return JoinCompiler.getJoinSpec(context, select);        
+        return JoinCompiler.compile(select, context.getResolver());        
     }
 }
 


[3/3] git commit: PHOENIX-71 Support sub-joins

Posted by ma...@apache.org.
PHOENIX-71 Support sub-joins


Project: http://git-wip-us.apache.org/repos/asf/incubator-phoenix/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-phoenix/commit/12bd7aed
Tree: http://git-wip-us.apache.org/repos/asf/incubator-phoenix/tree/12bd7aed
Diff: http://git-wip-us.apache.org/repos/asf/incubator-phoenix/diff/12bd7aed

Branch: refs/heads/master
Commit: 12bd7aeddd4e25a754b76c3f05b00730c4304644
Parents: 0849b25
Author: maryannxue <ma...@apache.org>
Authored: Mon Mar 31 20:33:50 2014 -0400
Committer: maryannxue <ma...@apache.org>
Committed: Mon Mar 31 20:33:50 2014 -0400

----------------------------------------------------------------------
 .../org/apache/phoenix/end2end/HashJoinIT.java  |  258 +++-
 phoenix-core/src/main/antlr3/PhoenixSQL.g       |   23 +-
 .../apache/phoenix/compile/FromCompiler.java    |   17 +-
 .../apache/phoenix/compile/JoinCompiler.java    | 1425 +++++++++---------
 .../apache/phoenix/compile/QueryCompiler.java   |   99 +-
 .../phoenix/compile/StatementNormalizer.java    |   42 +-
 .../apache/phoenix/optimize/QueryOptimizer.java |    2 +-
 .../org/apache/phoenix/parse/BindTableNode.java |    4 +-
 .../apache/phoenix/parse/DerivedTableNode.java  |    4 +-
 .../org/apache/phoenix/parse/JoinPartNode.java  |   53 +
 .../org/apache/phoenix/parse/JoinTableNode.java |   35 +-
 .../apache/phoenix/parse/NamedTableNode.java    |    4 +-
 .../apache/phoenix/parse/ParseNodeFactory.java  |   16 +-
 .../apache/phoenix/parse/ParseNodeRewriter.java |   81 +-
 .../apache/phoenix/parse/SelectStatement.java   |    4 +
 .../org/apache/phoenix/parse/TableNode.java     |    2 +-
 .../apache/phoenix/parse/TableNodeVisitor.java  |   10 +-
 .../phoenix/compile/JoinQueryCompilerTest.java  |   70 +-
 18 files changed, 1192 insertions(+), 957 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-phoenix/blob/12bd7aed/phoenix-core/src/it/java/org/apache/phoenix/end2end/HashJoinIT.java
----------------------------------------------------------------------
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/HashJoinIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/HashJoinIT.java
index 248621b..5ccef2c 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/HashJoinIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/HashJoinIT.java
@@ -226,7 +226,7 @@ public class HashJoinIT extends BaseHBaseManagedTimeIT {
                 "    BUILD HASH TABLE 1\n" +
                 "        CLIENT PARALLEL 1-WAY FULL SCAN OVER " + JOIN_SUPPLIER_TABLE_DISPLAY_NAME,
                 /*
-                 * testSelfJoin
+                 * testSelfJoin()
                  *     SELECT i2.item_id, i1.name FROM joinItemTable i1 
                  *     JOIN joinItemTable i2 ON i1.item_id = i2.item_id 
                  *     ORDER BY i1.item_id
@@ -237,7 +237,7 @@ public class HashJoinIT extends BaseHBaseManagedTimeIT {
                 "        CLIENT PARALLEL 1-WAY FULL SCAN OVER " + JOIN_ITEM_TABLE_DISPLAY_NAME + "\n" +
                 "            SERVER FILTER BY FIRST KEY ONLY",
                 /*
-                 * testSelfJoin
+                 * testSelfJoin()
                  *     SELECT i1.name, i2.name FROM joinItemTable i1 
                  *     JOIN joinItemTable i2 ON i1.item_id = i2.supplier_id 
                  *     ORDER BY i1.name, i2.name
@@ -249,12 +249,12 @@ public class HashJoinIT extends BaseHBaseManagedTimeIT {
                 "    BUILD HASH TABLE 0\n" +
                 "        CLIENT PARALLEL 1-WAY FULL SCAN OVER " + JOIN_ITEM_TABLE_DISPLAY_NAME,
                 /*
-                 * testStarJoin
-                 * SELECT order_id, c.name, i.name iname, quantity, o.date 
-                 * FROM joinOrderTable o 
-                 * JOIN joinCustomerTable c ON o.customer_id = c.customer_id 
-                 * JOIN joinItemTable i ON o.item_id = i.item_id 
-                 * ORDER BY order_id
+                 * testStarJoin()
+                 *     SELECT order_id, c.name, i.name iname, quantity, o.date 
+                 *     FROM joinOrderTable o 
+                 *     JOIN joinCustomerTable c ON o.customer_id = c.customer_id 
+                 *     JOIN joinItemTable i ON o.item_id = i.item_id 
+                 *     ORDER BY order_id
                  */
                 "CLIENT PARALLEL 1-WAY FULL SCAN OVER " + JOIN_ORDER_TABLE_DISPLAY_NAME + "\n" +
                 "    PARALLEL EQUI-JOIN 2 HASH TABLES:\n" +
@@ -263,12 +263,12 @@ public class HashJoinIT extends BaseHBaseManagedTimeIT {
                 "    BUILD HASH TABLE 1\n" +
                 "        CLIENT PARALLEL 1-WAY FULL SCAN OVER " + JOIN_ITEM_TABLE_DISPLAY_NAME,
                 /*
-                 * testStarJoin
-                 * SELECT (*NO_STAR_JOIN*) order_id, c.name, i.name iname, quantity, o.date 
-                 * FROM joinOrderTable o 
-                 * JOIN joinCustomerTable c ON o.customer_id = c.customer_id 
-                 * JOIN joinItemTable i ON o.item_id = i.item_id 
-                 * ORDER BY order_id
+                 * testStarJoin()
+                 *     SELECT (*NO_STAR_JOIN*) order_id, c.name, i.name iname, quantity, o.date 
+                 *     FROM joinOrderTable o 
+                 *     JOIN joinCustomerTable c ON o.customer_id = c.customer_id 
+                 *     JOIN joinItemTable i ON o.item_id = i.item_id 
+                 *     ORDER BY order_id
                  */
                 "CLIENT PARALLEL 1-WAY FULL SCAN OVER " + JOIN_ITEM_TABLE_DISPLAY_NAME + "\n" +
                 "    SERVER SORTED BY [O.order_id]\n" +
@@ -279,6 +279,33 @@ public class HashJoinIT extends BaseHBaseManagedTimeIT {
                 "            PARALLEL EQUI-JOIN 1 HASH TABLES:\n" +
                 "            BUILD HASH TABLE 0\n" +
                 "                CLIENT PARALLEL 1-WAY FULL SCAN OVER " + JOIN_CUSTOMER_TABLE_DISPLAY_NAME,
+                /*
+                 * testSubJoin()
+                 *     SELECT * FROM joinCustomerTable c 
+                 *     INNER JOIN (joinOrderTable o 
+                 *         INNER JOIN (joinSupplierTable s 
+                 *             RIGHT JOIN joinItemTable i ON i.supplier_id = s.supplier_id)
+                 *         ON o.item_id = i.item_id)
+                 *     ON c.customer_id = o.customer_id
+                 *     WHERE c.customer_id <= '0000000005' 
+                 *         AND order_id != '000000000000003' 
+                 *         AND i.name != 'T3' 
+                 *     ORDER BY c.customer_id, i.name
+                 */
+                "CLIENT PARALLEL 1-WAY RANGE SCAN OVER " + JOIN_CUSTOMER_TABLE_DISPLAY_NAME + " [*] - ['0000000005']\n" +
+                "    SERVER SORTED BY [C.customer_id, I.NAME]\n" +
+                "CLIENT MERGE SORT\n" +
+                "    PARALLEL EQUI-JOIN 1 HASH TABLES:\n" +
+                "    BUILD HASH TABLE 0\n" +
+                "        CLIENT PARALLEL 1-WAY FULL SCAN OVER " + JOIN_ORDER_TABLE_DISPLAY_NAME + "\n" +
+                "            SERVER FILTER BY order_id != '000000000000003'\n" +
+                "            PARALLEL EQUI-JOIN 1 HASH TABLES:\n" +
+                "            BUILD HASH TABLE 0\n" +
+                "                CLIENT PARALLEL 1-WAY FULL SCAN OVER " + JOIN_ITEM_TABLE_DISPLAY_NAME + "\n" +
+                "                    SERVER FILTER BY NAME != 'T3'\n" +
+                "                    PARALLEL EQUI-JOIN 1 HASH TABLES:\n" +
+                "                    BUILD HASH TABLE 0\n" +
+                "                        CLIENT PARALLEL 1-WAY FULL SCAN OVER " + JOIN_SUPPLIER_TABLE_DISPLAY_NAME,
                 }});
         testCases.add(new String[][] {
                 {
@@ -404,7 +431,7 @@ public class HashJoinIT extends BaseHBaseManagedTimeIT {
                 "    BUILD HASH TABLE 1\n" +
                 "        CLIENT PARALLEL 1-WAY FULL SCAN OVER " + JOIN_SCHEMA + ".idx_supplier",
                 /*
-                 * testSelfJoin
+                 * testSelfJoin()
                  *     SELECT i2.item_id, i1.name FROM joinItemTable i1 
                  *     JOIN joinItemTable i2 ON i1.item_id = i2.item_id 
                  *     ORDER BY i1.item_id
@@ -415,7 +442,7 @@ public class HashJoinIT extends BaseHBaseManagedTimeIT {
                 "        CLIENT PARALLEL 1-WAY FULL SCAN OVER " + JOIN_SCHEMA + ".idx_item\n" +
                 "            SERVER FILTER BY FIRST KEY ONLY",
                 /*
-                 * testSelfJoin
+                 * testSelfJoin()
                  *     SELECT i1.name, i2.name FROM joinItemTable i1 
                  *     JOIN joinItemTable i2 ON i1.item_id = i2.supplier_id 
                  *     ORDER BY i1.name, i2.name
@@ -428,12 +455,12 @@ public class HashJoinIT extends BaseHBaseManagedTimeIT {
                 "    BUILD HASH TABLE 0\n" +
                 "        CLIENT PARALLEL 1-WAY FULL SCAN OVER " + JOIN_SCHEMA + ".idx_item",
                 /*
-                 * testStarJoin
-                 * SELECT order_id, c.name, i.name iname, quantity, o.date 
-                 * FROM joinOrderTable o 
-                 * JOIN joinCustomerTable c ON o.customer_id = c.customer_id 
-                 * JOIN joinItemTable i ON o.item_id = i.item_id 
-                 * ORDER BY order_id
+                 * testStarJoin()
+                 *     SELECT order_id, c.name, i.name iname, quantity, o.date 
+                 *     FROM joinOrderTable o 
+                 *     JOIN joinCustomerTable c ON o.customer_id = c.customer_id 
+                 *     JOIN joinItemTable i ON o.item_id = i.item_id 
+                 *     ORDER BY order_id
                  */
                 "CLIENT PARALLEL 1-WAY FULL SCAN OVER " + JOIN_ORDER_TABLE_DISPLAY_NAME + "\n" +
                 "    PARALLEL EQUI-JOIN 2 HASH TABLES:\n" +
@@ -443,12 +470,12 @@ public class HashJoinIT extends BaseHBaseManagedTimeIT {
                 "        CLIENT PARALLEL 1-WAY FULL SCAN OVER " + JOIN_SCHEMA + ".idx_item\n" +
                 "            SERVER FILTER BY FIRST KEY ONLY",
                 /*
-                 * testStarJoin
-                 * SELECT (*NO_STAR_JOIN*) order_id, c.name, i.name iname, quantity, o.date 
-                 * FROM joinOrderTable o 
-                 * JOIN joinCustomerTable c ON o.customer_id = c.customer_id 
-                 * JOIN joinItemTable i ON o.item_id = i.item_id 
-                 * ORDER BY order_id
+                 * testStarJoin()
+                 *     SELECT (*NO_STAR_JOIN*) order_id, c.name, i.name iname, quantity, o.date 
+                 *     FROM joinOrderTable o 
+                 *     JOIN joinCustomerTable c ON o.customer_id = c.customer_id 
+                 *     JOIN joinItemTable i ON o.item_id = i.item_id 
+                 *     ORDER BY order_id
                  */
                 "CLIENT PARALLEL 1-WAY FULL SCAN OVER " + JOIN_SCHEMA + ".idx_item\n" +
                 "    SERVER FILTER BY FIRST KEY ONLY\n" +
@@ -460,6 +487,33 @@ public class HashJoinIT extends BaseHBaseManagedTimeIT {
                 "            PARALLEL EQUI-JOIN 1 HASH TABLES:\n" +
                 "            BUILD HASH TABLE 0\n" +
                 "                CLIENT PARALLEL 1-WAY FULL SCAN OVER " + JOIN_SCHEMA + ".idx_customer",
+                /*
+                 * testSubJoin()
+                 *     SELECT * FROM joinCustomerTable c 
+                 *     INNER JOIN (joinOrderTable o 
+                 *         INNER JOIN (joinSupplierTable s 
+                 *             RIGHT JOIN joinItemTable i ON i.supplier_id = s.supplier_id)
+                 *         ON o.item_id = i.item_id)
+                 *     ON c.customer_id = o.customer_id
+                 *     WHERE c.customer_id <= '0000000005' 
+                 *         AND order_id != '000000000000003' 
+                 *         AND i.name != 'T3' 
+                 *     ORDER BY c.customer_id, i.name
+                 */
+                "CLIENT PARALLEL 1-WAY RANGE SCAN OVER " + JOIN_CUSTOMER_TABLE_DISPLAY_NAME + " [*] - ['0000000005']\n" +
+                "    SERVER SORTED BY [C.customer_id, I.0:NAME]\n" +
+                "CLIENT MERGE SORT\n" +
+                "    PARALLEL EQUI-JOIN 1 HASH TABLES:\n" +
+                "    BUILD HASH TABLE 0\n" +
+                "        CLIENT PARALLEL 1-WAY FULL SCAN OVER " + JOIN_ORDER_TABLE_DISPLAY_NAME + "\n" +
+                "            SERVER FILTER BY order_id != '000000000000003'\n" +
+                "            PARALLEL EQUI-JOIN 1 HASH TABLES:\n" +
+                "            BUILD HASH TABLE 0\n" +
+                "                CLIENT PARALLEL 1-WAY FULL SCAN OVER " + JOIN_SCHEMA + ".idx_item\n" +
+                "                    SERVER FILTER BY NAME != 'T3'\n" +
+                "                    PARALLEL EQUI-JOIN 1 HASH TABLES:\n" +
+                "                    BUILD HASH TABLE 0\n" +
+                "                        CLIENT PARALLEL 1-WAY FULL SCAN OVER " + JOIN_SCHEMA + ".idx_supplier",
                 }});
         return testCases;
     }
@@ -1309,13 +1363,16 @@ public class HashJoinIT extends BaseHBaseManagedTimeIT {
     
     @Test
     public void testLeftRightJoin() throws Exception {
-        String query = "SELECT \"order_id\", i.name, s.name, quantity, date FROM " + JOIN_ORDER_TABLE_FULL_NAME + " o LEFT JOIN " 
-            + JOIN_ITEM_TABLE_FULL_NAME + " i ON o.\"item_id\" = i.\"item_id\" RIGHT JOIN "
-            + JOIN_SUPPLIER_TABLE_FULL_NAME + " s ON i.\"supplier_id\" = s.\"supplier_id\" ORDER BY \"order_id\", s.\"supplier_id\" DESC";
+        String query1 = "SELECT \"order_id\", i.name, s.name, quantity, date FROM " + JOIN_ORDER_TABLE_FULL_NAME + " o LEFT JOIN " 
+                + JOIN_ITEM_TABLE_FULL_NAME + " i ON o.\"item_id\" = i.\"item_id\" RIGHT JOIN "
+                + JOIN_SUPPLIER_TABLE_FULL_NAME + " s ON i.\"supplier_id\" = s.\"supplier_id\" ORDER BY \"order_id\", s.\"supplier_id\" DESC";
+        String query2 = "SELECT \"order_id\", i.name, s.name, quantity, date FROM " + JOIN_ORDER_TABLE_FULL_NAME + " o LEFT JOIN " 
+                + "(" + JOIN_ITEM_TABLE_FULL_NAME + " i RIGHT JOIN " + JOIN_SUPPLIER_TABLE_FULL_NAME + " s ON i.\"supplier_id\" = s.\"supplier_id\")" 
+                + " ON o.\"item_id\" = i.\"item_id\" ORDER BY \"order_id\", s.\"supplier_id\" DESC";
         Properties props = new Properties(TEST_PROPERTIES);
         Connection conn = DriverManager.getConnection(getUrl(), props);
         try {
-            PreparedStatement statement = conn.prepareStatement(query);
+            PreparedStatement statement = conn.prepareStatement(query1);
             ResultSet rs = statement.executeQuery();
             assertTrue (rs.next());
             assertNull(rs.getString(1));
@@ -1367,21 +1424,9 @@ public class HashJoinIT extends BaseHBaseManagedTimeIT {
             assertNotNull(rs.getDate(5));
 
             assertFalse(rs.next());
-        } finally {
-            conn.close();
-        }
-    }
-    
-    @Test
-    public void testMultiLeftJoin() throws Exception {
-        String query = "SELECT \"order_id\", i.name, s.name, quantity, date FROM " + JOIN_ORDER_TABLE_FULL_NAME + " o LEFT JOIN " 
-            + JOIN_ITEM_TABLE_FULL_NAME + " i ON o.\"item_id\" = i.\"item_id\" LEFT JOIN "
-            + JOIN_SUPPLIER_TABLE_FULL_NAME + " s ON i.\"supplier_id\" = s.\"supplier_id\"";
-        Properties props = new Properties(TEST_PROPERTIES);
-        Connection conn = DriverManager.getConnection(getUrl(), props);
-        try {
-            PreparedStatement statement = conn.prepareStatement(query);
-            ResultSet rs = statement.executeQuery();
+            
+            statement = conn.prepareStatement(query2);
+            rs = statement.executeQuery();
             assertTrue (rs.next());
             assertEquals(rs.getString(1), "000000000000001");
             assertEquals(rs.getString(2), "T1");
@@ -1420,6 +1465,59 @@ public class HashJoinIT extends BaseHBaseManagedTimeIT {
     }
     
     @Test
+    public void testMultiLeftJoin() throws Exception {
+        String[] queries = {
+                "SELECT \"order_id\", i.name, s.name, quantity, date FROM " + JOIN_ORDER_TABLE_FULL_NAME + " o LEFT JOIN " 
+                        + JOIN_ITEM_TABLE_FULL_NAME + " i ON o.\"item_id\" = i.\"item_id\" LEFT JOIN "
+                        + JOIN_SUPPLIER_TABLE_FULL_NAME + " s ON i.\"supplier_id\" = s.\"supplier_id\"",
+                "SELECT \"order_id\", i.name, s.name, quantity, date FROM " + JOIN_ORDER_TABLE_FULL_NAME + " o LEFT JOIN " 
+                        + "(" + JOIN_ITEM_TABLE_FULL_NAME + " i LEFT JOIN " + JOIN_SUPPLIER_TABLE_FULL_NAME + " s ON i.\"supplier_id\" = s.\"supplier_id\") " 
+                        + "ON o.\"item_id\" = i.\"item_id\""};
+        Properties props = new Properties(TEST_PROPERTIES);
+        Connection conn = DriverManager.getConnection(getUrl(), props);
+        try {
+            for (String query : queries) {
+                PreparedStatement statement = conn.prepareStatement(query);
+                ResultSet rs = statement.executeQuery();
+                assertTrue (rs.next());
+                assertEquals(rs.getString(1), "000000000000001");
+                assertEquals(rs.getString(2), "T1");
+                assertEquals(rs.getString(3), "S1");
+                assertEquals(rs.getInt(4), 1000);
+                assertNotNull(rs.getDate(5));
+                assertTrue (rs.next());
+                assertEquals(rs.getString(1), "000000000000002");
+                assertEquals(rs.getString(2), "T6");
+                assertEquals(rs.getString(3), "S6");
+                assertEquals(rs.getInt(4), 2000);
+                assertNotNull(rs.getDate(5));
+                assertTrue (rs.next());
+                assertEquals(rs.getString(1), "000000000000003");
+                assertEquals(rs.getString(2), "T2");
+                assertEquals(rs.getString(3), "S1");
+                assertEquals(rs.getInt(4), 3000);
+                assertNotNull(rs.getDate(5));
+                assertTrue (rs.next());
+                assertEquals(rs.getString(1), "000000000000004");
+                assertEquals(rs.getString(2), "T6");
+                assertEquals(rs.getString(3), "S6");
+                assertEquals(rs.getInt(4), 4000);
+                assertNotNull(rs.getDate(5));
+                assertTrue (rs.next());
+                assertEquals(rs.getString(1), "000000000000005");
+                assertEquals(rs.getString(2), "T3");
+                assertEquals(rs.getString(3), "S2");
+                assertEquals(rs.getInt(4), 5000);
+                assertNotNull(rs.getDate(5));
+
+                assertFalse(rs.next());
+            }
+        } finally {
+            conn.close();
+        }
+    }
+    
+    @Test
     public void testMultiRightJoin() throws Exception {
         String query = "SELECT \"order_id\", i.name, s.name, quantity, date FROM " + JOIN_ORDER_TABLE_FULL_NAME + " o RIGHT JOIN " 
             + JOIN_ITEM_TABLE_FULL_NAME + " i ON o.\"item_id\" = i.\"item_id\" RIGHT JOIN "
@@ -2306,6 +2404,72 @@ public class HashJoinIT extends BaseHBaseManagedTimeIT {
         }
 
     }
+    
+    @Test
+    public void testSubJoin() throws Exception {
+        String query1 = "SELECT i.name, count(c.name), min(s.name), max(quantity) FROM " + JOIN_ORDER_TABLE_FULL_NAME + " o LEFT JOIN " 
+                + "(" + JOIN_SUPPLIER_TABLE_FULL_NAME + " s RIGHT JOIN " + JOIN_ITEM_TABLE_FULL_NAME + " i ON i.\"supplier_id\" = s.\"supplier_id\")" 
+                + " ON o.\"item_id\" = i.\"item_id\" LEFT JOIN " 
+                + JOIN_CUSTOMER_TABLE_FULL_NAME + " c ON c.\"customer_id\" = o.\"customer_id\" GROUP BY i.name ORDER BY i.name";
+        String query2 = "SELECT c.name, o.\"order_id\", i.name, s.name FROM " + JOIN_CUSTOMER_TABLE_FULL_NAME + " c INNER JOIN " 
+                + "(" + JOIN_ORDER_TABLE_FULL_NAME + " o INNER JOIN " 
+                + "(" + JOIN_SUPPLIER_TABLE_FULL_NAME + " s RIGHT JOIN " + JOIN_ITEM_TABLE_FULL_NAME + " i ON i.\"supplier_id\" = s.\"supplier_id\")" 
+                + " ON o.\"item_id\" = i.\"item_id\") ON c.\"customer_id\" = o.\"customer_id\"" 
+                + " WHERE c.\"customer_id\" <= '0000000005' AND \"order_id\" != '000000000000003' AND i.name != 'T3' ORDER BY c.\"customer_id\", i.name";
+        Properties props = new Properties(TEST_PROPERTIES);
+        Connection conn = DriverManager.getConnection(getUrl(), props);
+        try {
+            PreparedStatement statement = conn.prepareStatement(query1);
+            ResultSet rs = statement.executeQuery();
+            assertTrue (rs.next());
+            assertEquals(rs.getString(1), "T1");
+            assertEquals(rs.getInt(2), 1);
+            assertEquals(rs.getString(3), "S1");
+            assertEquals(rs.getInt(4), 1000);
+            assertTrue (rs.next());
+            assertEquals(rs.getString(1), "T2");
+            assertEquals(rs.getInt(2), 1);
+            assertEquals(rs.getString(3), "S1");
+            assertEquals(rs.getInt(4), 3000);
+            assertTrue (rs.next());
+            assertEquals(rs.getString(1), "T3");
+            assertEquals(rs.getInt(2), 1);
+            assertEquals(rs.getString(3), "S2");
+            assertEquals(rs.getInt(4), 5000);
+            assertTrue (rs.next());
+            assertEquals(rs.getString(1), "T6");
+            assertEquals(rs.getInt(2), 2);
+            assertEquals(rs.getString(3), "S6");
+            assertEquals(rs.getInt(4), 4000);
+
+            assertFalse(rs.next());
+            
+            statement = conn.prepareStatement(query2);
+            rs = statement.executeQuery();
+            assertTrue(rs.next());
+            assertEquals(rs.getString("c.name"), "C3");
+            assertEquals(rs.getString("O.order_id"), "000000000000002");
+            assertEquals(rs.getString("i.name"), "T6");
+            assertEquals(rs.getString("s.name"), "S6");
+            assertTrue(rs.next());
+            assertEquals(rs.getString("c.name"), "C4");
+            assertEquals(rs.getString("O.order_id"), "000000000000001");
+            assertEquals(rs.getString("i.name"), "T1");
+            assertEquals(rs.getString("s.name"), "S1");
+            assertTrue(rs.next());
+            assertEquals(rs.getString("c.name"), "C4");
+            assertEquals(rs.getString("O.order_id"), "000000000000004");
+            assertEquals(rs.getString("i.name"), "T6");
+            assertEquals(rs.getString("s.name"), "S6");
+
+            assertFalse(rs.next());            
+            
+            rs = conn.createStatement().executeQuery("EXPLAIN " + query2);
+            assertEquals(plans[13], QueryUtil.getExplainPlan(rs));
+        } finally {
+            conn.close();
+        }
+    }
 
 }
 

http://git-wip-us.apache.org/repos/asf/incubator-phoenix/blob/12bd7aed/phoenix-core/src/main/antlr3/PhoenixSQL.g
----------------------------------------------------------------------
diff --git a/phoenix-core/src/main/antlr3/PhoenixSQL.g b/phoenix-core/src/main/antlr3/PhoenixSQL.g
index 6ccd03e..4f1d107 100644
--- a/phoenix-core/src/main/antlr3/PhoenixSQL.g
+++ b/phoenix-core/src/main/antlr3/PhoenixSQL.g
@@ -629,23 +629,28 @@ parseOrderByField returns [OrderByNode ret]
 
 parseFrom returns [List<TableNode> ret]
 @init{ret = new ArrayList<TableNode>(4); }
-    :   t=table_ref {$ret.add(t);} (s=sub_table_ref { $ret.add(s); })*
-    ;
-    
-sub_table_ref returns [TableNode ret]
-    :   COMMA t=table_ref { $ret = t; }
-    |   t=join_spec { $ret = t; }
+    :   t=table_ref {$ret.add(t);} (COMMA s=table_ref { $ret.add(s); })*
     ;
 
 table_ref returns [TableNode ret]
+    : t=single_table_ref p=join_parts { $ret = factory.table(t, p); }
+    ;
+
+single_table_ref returns [TableNode ret]
     :   n=bind_name ((AS)? alias=identifier)? { $ret = factory.bindTable(alias, factory.table(null,n)); } // TODO: review
     |   t=from_table_name ((AS)? alias=identifier)? (LPAREN cdefs=dyn_column_defs RPAREN)? { $ret = factory.namedTable(alias,t,cdefs); }
     |   LPAREN SELECT s=hinted_select_node RPAREN ((AS)? alias=identifier)? { $ret = factory.derivedTable(alias, s); }
     ;
 
-join_spec returns [TableNode ret]
-    :   j=join_type JOIN t=table_ref ON e=expression { $ret = factory.join(j, e, t); }
-    ;
+join_parts returns [List<JoinPartNode> ret]
+@init{ret = new ArrayList<JoinPartNode>(4); }
+	:	(p=join_part { $ret.add(p); })*
+	;
+
+join_part returns [JoinPartNode ret]
+	:	j=join_type JOIN r=single_table_ref ON e=expression { $ret = factory.joinPart(j, e, r); }
+	|	j=join_type JOIN LPAREN r=table_ref RPAREN ON e=expression { $ret = factory.joinPart(j, e, r); }
+	;
 
 join_type returns [JoinTableNode.JoinType ret]
     :   INNER?   { $ret = JoinTableNode.JoinType.Inner; }

http://git-wip-us.apache.org/repos/asf/incubator-phoenix/blob/12bd7aed/phoenix-core/src/main/java/org/apache/phoenix/compile/FromCompiler.java
----------------------------------------------------------------------
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/compile/FromCompiler.java b/phoenix-core/src/main/java/org/apache/phoenix/compile/FromCompiler.java
index f43ff4f..500ae5f 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/compile/FromCompiler.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/compile/FromCompiler.java
@@ -146,7 +146,7 @@ public class FromCompiler {
     public static ColumnResolver getResolverForQuery(SelectStatement statement, PhoenixConnection connection)
     		throws SQLException {
     	List<TableNode> fromNodes = statement.getFrom();
-        if (fromNodes.size() == 1)
+        if (!statement.isJoin())
             return new SingleTableColumnResolver(connection, (NamedTableNode)fromNodes.get(0), true);
 
         MultiTableColumnResolver visitor = new MultiTableColumnResolver(connection);
@@ -337,7 +337,7 @@ public class FromCompiler {
     }
     
     // TODO: unused, but should be used for joins - make private once used
-    public static class MultiTableColumnResolver extends BaseColumnResolver implements TableNodeVisitor {
+    public static class MultiTableColumnResolver extends BaseColumnResolver implements TableNodeVisitor<Void> {
         private final ListMultimap<String, TableRef> tableMap;
         private final List<TableRef> tables;
 
@@ -353,17 +353,19 @@ public class FromCompiler {
         }
 
         @Override
-        public void visit(BindTableNode boundTableNode) throws SQLException {
+        public Void visit(BindTableNode boundTableNode) throws SQLException {
             throw new SQLFeatureNotSupportedException();
         }
 
         @Override
-        public void visit(JoinTableNode joinNode) throws SQLException {
-            joinNode.getTable().accept(this);
+        public Void visit(JoinTableNode joinNode) throws SQLException {
+            joinNode.getLHS().accept(this);
+            joinNode.getRHS().accept(this);
+            return null;
         }
 
         @Override
-        public void visit(NamedTableNode tableNode) throws SQLException {
+        public Void visit(NamedTableNode tableNode) throws SQLException {
             String alias = tableNode.getAlias();
             TableRef tableRef = createTableRef(tableNode, true);
             PTable theTable = tableRef.getTable();
@@ -378,10 +380,11 @@ public class FromCompiler {
             	tableMap.put(name, tableRef);
             }
             tables.add(tableRef);
+            return null;
         }
 
         @Override
-        public void visit(DerivedTableNode subselectNode) throws SQLException {
+        public Void visit(DerivedTableNode subselectNode) throws SQLException {
             throw new SQLFeatureNotSupportedException();
         }
 


[2/3] PHOENIX-71 Support sub-joins

Posted by ma...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-phoenix/blob/12bd7aed/phoenix-core/src/main/java/org/apache/phoenix/compile/JoinCompiler.java
----------------------------------------------------------------------
diff --git a/phoenix-core/src/main/java/org/apache/phoenix/compile/JoinCompiler.java b/phoenix-core/src/main/java/org/apache/phoenix/compile/JoinCompiler.java
index fd5174b..6136e4e 100644
--- a/phoenix-core/src/main/java/org/apache/phoenix/compile/JoinCompiler.java
+++ b/phoenix-core/src/main/java/org/apache/phoenix/compile/JoinCompiler.java
@@ -50,7 +50,6 @@ import org.apache.phoenix.parse.CastParseNode;
 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.FunctionParseNode;
@@ -102,179 +101,229 @@ public class JoinCompiler {
         GENERAL,
     }
     
-    public static class JoinSpec {
-        private ColumnResolver origResolver;
-        private TableNode mainTableNode;
-        private List<ColumnDef> dynamicColumns;
-        private TableRef mainTable;
-        private List<AliasedNode> select; // all basic nodes related to mainTable, no aggregation.
-        private List<ParseNode> preFilters;
-        private List<ParseNode> postFilters;
-        private List<JoinTable> joinTables;
-        private boolean useStarJoin;
-        private Map<TableRef, JoinTable> tableRefToJoinTableMap;
-        private Map<ColumnRef, ColumnRefType> columnRefs;
-        
-        private JoinSpec(SelectStatement statement, ColumnResolver resolver) throws SQLException {
-            this.origResolver = resolver;
-            List<AliasedNode> selectList = statement.getSelect();
-            List<TableNode> tableNodes = statement.getFrom();
-            assert (tableNodes.size() > 1);
-            Iterator<TableNode> iter = tableNodes.iterator();
-            Iterator<TableRef> tableRefIter = resolver.getTables().iterator();
-            this.mainTableNode = iter.next();
-            DynamicColumnsVisitor v = new DynamicColumnsVisitor();
-            this.mainTableNode.accept(v);
-            this.dynamicColumns = v.getDynamicColumns();
-            this.mainTable = tableRefIter.next();
-            this.select = extractFromSelect(selectList, mainTable, resolver);
-            this.joinTables = new ArrayList<JoinTable>(tableNodes.size() - 1);
-            this.preFilters = new ArrayList<ParseNode>();
-            this.postFilters = new ArrayList<ParseNode>();
-            this.useStarJoin = !statement.getHint().hasHint(Hint.NO_STAR_JOIN);
-            this.tableRefToJoinTableMap = new HashMap<TableRef, JoinTable>();
-            ColumnParseNodeVisitor generalRefVisitor = new ColumnParseNodeVisitor(resolver);
-            ColumnParseNodeVisitor joinLocalRefVisitor = new ColumnParseNodeVisitor(resolver);
-            ColumnParseNodeVisitor prefilterRefVisitor = new ColumnParseNodeVisitor(resolver);            
-            int lastRightJoinIndex = -1;
-            TableNode tableNode = null;
-            int i = 0;
-            while (iter.hasNext()) {
-                tableNode = iter.next();
-                if (!(tableNode instanceof JoinTableNode))
-                    throw new SQLFeatureNotSupportedException("Implicit joins not supported.");
-                JoinTableNode joinTableNode = (JoinTableNode) tableNode;
-                JoinTable joinTable = new JoinTable(joinTableNode, tableRefIter.next(), statement, resolver);
-                for (ParseNode condition : joinTable.conditions) {
-                    ComparisonParseNode comparisonNode = (ComparisonParseNode) condition;
-                    comparisonNode.getLHS().accept(generalRefVisitor);
-                    comparisonNode.getRHS().accept(joinLocalRefVisitor);
-                }
-                if (joinTable.getType() == JoinType.Right) {
-                	lastRightJoinIndex = i;
-                }
-                joinTables.add(joinTable);
-                tableRefToJoinTableMap.put(joinTable.getTable(), joinTable);
-                i++;
-            }
-            List<TableRef> prefilterAcceptedTables = new ArrayList<TableRef>();
-            for (i = lastRightJoinIndex == -1 ? 0 : lastRightJoinIndex; i < joinTables.size(); i++) {
-                JoinTable joinTable = joinTables.get(i);
-                if (joinTable.getType() != JoinType.Left) {
-                    prefilterAcceptedTables.add(joinTable.getTable());
-                }
-            }
-            if (statement.getWhere() != null) {
-            	if (lastRightJoinIndex > -1 && prefilterAcceptedTables.isEmpty()) {
-            		// conditions can't be pushed down to the scan filter.
-            		postFilters.add(statement.getWhere());
-            	} else {
-            		statement.getWhere().accept(new WhereNodeVisitor(resolver, lastRightJoinIndex > -1, prefilterAcceptedTables));
-            		for (ParseNode prefilter : preFilters) {
-            		    prefilter.accept(prefilterRefVisitor);
-            		}
-            	}
-            	for (ParseNode postfilter : postFilters) {
-            		postfilter.accept(generalRefVisitor);
-            	}
-            }
-            // Delayed to this point, since pre-filters might have been post-fixed by WhereNodeVisitor.
-            for (JoinTable joinTable : joinTables) {
-                for (ParseNode prefilter : joinTable.preFilters) {
-                    prefilter.accept(prefilterRefVisitor);
-                }
+    private final SelectStatement statement;
+    private final ColumnResolver origResolver;
+    private final boolean useStarJoin;
+    private final Map<ColumnRef, ColumnRefType> columnRefs;
+    
+    
+    private JoinCompiler(SelectStatement statement, ColumnResolver resolver) {
+        this.statement = statement;
+        this.origResolver = resolver;
+        this.useStarJoin = !statement.getHint().hasHint(Hint.NO_STAR_JOIN);
+        this.columnRefs = new HashMap<ColumnRef, ColumnRefType>();
+    }
+    
+    public static JoinTable compile(SelectStatement statement, ColumnResolver resolver) throws SQLException {
+        JoinCompiler compiler = new JoinCompiler(statement, resolver);
+        
+        List<TableNode> from = statement.getFrom();
+        if (from.size() > 1) {
+            throw new SQLFeatureNotSupportedException("Cross join not supported.");
+        }
+        
+        JoinTableConstructor constructor = compiler.new JoinTableConstructor();
+        Pair<Table, List<JoinSpec>> res = from.get(0).accept(constructor);
+        JoinTable joinTable = res.getSecond() == null ? compiler.new JoinTable(res.getFirst()) : compiler.new JoinTable(res.getFirst(), res.getSecond());
+        if (statement.getWhere() != null) {
+            joinTable.addFilter(statement.getWhere());
+        }
+        
+        ColumnParseNodeVisitor generalRefVisitor = new ColumnParseNodeVisitor(resolver);
+        ColumnParseNodeVisitor joinLocalRefVisitor = new ColumnParseNodeVisitor(resolver);
+        ColumnParseNodeVisitor prefilterRefVisitor = new ColumnParseNodeVisitor(resolver);
+        
+        joinTable.pushDownColumnRefVisitors(generalRefVisitor, joinLocalRefVisitor, prefilterRefVisitor);
+
+        for (AliasedNode node : statement.getSelect()) {
+            node.getNode().accept(generalRefVisitor);
+        }
+        if (statement.getGroupBy() != null) {
+            for (ParseNode node : statement.getGroupBy()) {
+                node.accept(generalRefVisitor);
             }
-            for (AliasedNode node : selectList) {
+        }
+        if (statement.getHaving() != null) {
+            statement.getHaving().accept(generalRefVisitor);
+        }
+        if (statement.getOrderBy() != null) {
+            for (OrderByNode node : statement.getOrderBy()) {
                 node.getNode().accept(generalRefVisitor);
             }
-            if (statement.getGroupBy() != null) {
-                for (ParseNode node : statement.getGroupBy()) {
-                    node.accept(generalRefVisitor);
+        }
+        
+        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);
+        }
+        for (ColumnRef ref : prefilterRefVisitor.getColumnRefMap().keySet()) {
+            if (!compiler.columnRefs.containsKey(ref))
+                compiler.columnRefs.put(ref, ColumnRefType.PREFILTER);
+        }
+        
+        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());
+            List<AliasedNode> selectNodes = extractFromSelect(statement.getSelect(), tableRef, origResolver);
+            Table table = new Table(boundTableNode, Collections.<ColumnDef>emptyList(), selectNodes, 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, 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());
+            List<AliasedNode> selectNodes = extractFromSelect(statement.getSelect(), tableRef, origResolver);
+            Table table = new Table(namedTableNode, namedTableNode.getDynamicColumns(), selectNodes, tableRef);
+            return new Pair<Table, List<JoinSpec>>(table, null);
+        }
+
+        @Override
+        public Pair<Table, List<JoinSpec>> visit(DerivedTableNode subselectNode)
+                throws SQLException {
+            throw new SQLFeatureNotSupportedException();
+        }
+    }
+    
+    public class JoinTable {
+        private final Table table;
+        private final List<JoinSpec> joinSpecs;
+        private final List<ParseNode> postFilters;
+        private final List<Table> tables;
+        private final List<TableRef> tableRefs;
+        private final boolean hasRightJoin;
+        private final List<JoinTable> prefilterAcceptedTables;
+        
+        private JoinTable(Table table) {
+            this.table = table;
+            this.joinSpecs = Collections.<JoinSpec>emptyList();
+            this.postFilters = Collections.<ParseNode>emptyList();
+            this.tables = Collections.<Table>singletonList(table);
+            this.tableRefs = Collections.<TableRef>singletonList(table.getTableRef());
+            this.hasRightJoin = false;
+            this.prefilterAcceptedTables = Collections.<JoinTable>emptyList();
+        }
+        
+        private JoinTable(Table table, List<JoinSpec> joinSpecs) {
+            this.table = table;
+            this.joinSpecs = joinSpecs;
+            this.postFilters = new ArrayList<ParseNode>();
+            this.tables = new ArrayList<Table>();
+            this.tableRefs = new ArrayList<TableRef>();
+            this.tables.add(table);
+            int lastRightJoinIndex = -1;
+            for (int i = 0; i < joinSpecs.size(); i++) {
+                this.tables.addAll(joinSpecs.get(i).getJoinTable().getTables());
+                if (joinSpecs.get(i).getType() == JoinType.Right) {
+                    lastRightJoinIndex = i;
                 }
             }
-            if (statement.getHaving() != null) {
-                statement.getHaving().accept(generalRefVisitor);
+            for (Table t : this.tables) {
+                this.tableRefs.add(t.getTableRef());
             }
-            if (statement.getOrderBy() != null) {
-                for (OrderByNode node : statement.getOrderBy()) {
-                    node.getNode().accept(generalRefVisitor);
+            this.hasRightJoin = lastRightJoinIndex > -1;
+            this.prefilterAcceptedTables = new ArrayList<JoinTable>();
+            for (int i = lastRightJoinIndex == -1 ? 0 : lastRightJoinIndex; i < joinSpecs.size(); i++) {
+                JoinSpec joinSpec = joinSpecs.get(i);
+                if (joinSpec.getType() != JoinType.Left) {
+                    prefilterAcceptedTables.add(joinSpec.getJoinTable());
                 }
             }
-            this.columnRefs = new HashMap<ColumnRef, ColumnRefType>();
-            for (ColumnRef ref : generalRefVisitor.getColumnRefMap().keySet()) {
-                columnRefs.put(ref, ColumnRefType.GENERAL);
-            }
-            for (ColumnRef ref : joinLocalRefVisitor.getColumnRefMap().keySet()) {
-                if (!columnRefs.containsKey(ref))
-                    columnRefs.put(ref, ColumnRefType.JOINLOCAL);
-            }
-            for (ColumnRef ref : prefilterRefVisitor.getColumnRefMap().keySet()) {
-                if (!columnRefs.containsKey(ref))
-                    columnRefs.put(ref, ColumnRefType.PREFILTER);
-            }
         }
         
-        private JoinSpec(ColumnResolver resolver, TableNode tableNode, List<ColumnDef> dynamicColumns, TableRef table, List<AliasedNode> select, List<ParseNode> preFilters, 
-                List<ParseNode> postFilters, List<JoinTable> joinTables, boolean useStarJoin, Map<TableRef, JoinTable> tableRefToJoinTableMap, Map<ColumnRef, ColumnRefType> columnRefs) {
-            this.origResolver = resolver;
-            this.mainTableNode = tableNode;
-            this.dynamicColumns = dynamicColumns;
-            this.mainTable = table;
-            this.select = select;
-            this.preFilters = preFilters;
-            this.postFilters = postFilters;
-            this.joinTables = joinTables;
-            this.useStarJoin = useStarJoin;
-            this.tableRefToJoinTableMap = tableRefToJoinTableMap;
-            this.columnRefs = columnRefs;
+        public Table getTable() {
+            return table;
         }
         
-        public ColumnResolver getOriginalResolver() {
-            return origResolver;
+        public List<JoinSpec> getJoinSpecs() {
+            return joinSpecs;
         }
         
-        public TableNode getMainTableNode() {
-            return mainTableNode;
+        public List<Table> getTables() {
+            return tables;
         }
         
-        public List<ColumnDef> getDynamicColumns() {
-            return dynamicColumns;
+        public List<TableRef> getTableRefs() {
+            return tableRefs;
         }
         
-        public TableRef getMainTable() {
-            return mainTable;
+        public ColumnResolver getOriginalResolver() {
+            return origResolver;
         }
         
-        public List<AliasedNode> getSelect() {
-            return select;
+        public Map<ColumnRef, ColumnRefType> getColumnRefs() {
+            return columnRefs;
         }
         
-        public List<ParseNode> getPreFilters() {
-            return preFilters;
-        }
-        
-        public List<ParseNode> getPostFilters() {
-            return postFilters;
+        public void addFilter(ParseNode filter) throws SQLException {
+            if (joinSpecs.isEmpty()) {
+                table.getPreFilters().add(filter);
+                return;
+            }
+            
+            WhereNodeVisitor visitor = new WhereNodeVisitor(origResolver, table.getPreFilters(),
+                    postFilters, Collections.<TableRef>singletonList(table.getTableRef()), 
+                    hasRightJoin, prefilterAcceptedTables);
+            filter.accept(visitor);
         }
         
-        public List<JoinTable> getJoinTables() {
-            return joinTables;
+        public void pushDownColumnRefVisitors(ColumnParseNodeVisitor generalRefVisitor, 
+                ColumnParseNodeVisitor joinLocalRefVisitor, 
+                ColumnParseNodeVisitor prefilterRefVisitor) throws SQLException {
+            for (ParseNode node : table.getPreFilters()) {
+                node.accept(prefilterRefVisitor);
+            }
+            for (ParseNode node : postFilters) {
+                node.accept(generalRefVisitor);
+            }
+            for (JoinSpec joinSpec : joinSpecs) {
+                JoinTable joinTable = joinSpec.getJoinTable();
+                boolean hasSubJoin = !joinTable.getJoinSpecs().isEmpty();
+                for (ComparisonParseNode node : joinSpec.getOnConditions()) {
+                    node.getLHS().accept(generalRefVisitor);
+                    if (hasSubJoin) {
+                        node.getRHS().accept(generalRefVisitor);
+                    } else {
+                        node.getRHS().accept(joinLocalRefVisitor);
+                    }
+                }
+                joinTable.pushDownColumnRefVisitors(generalRefVisitor, joinLocalRefVisitor, prefilterRefVisitor);
+            }
         }
         
-        public ParseNode getPreFiltersCombined() {
-            if (preFilters == null || preFilters.isEmpty())
+        public Expression compilePostFilterExpression(StatementContext context) throws SQLException {
+            if (postFilters == null || postFilters.isEmpty())
                 return null;
             
-            if (preFilters.size() == 1)
-                return preFilters.get(0);
-            
-            return NODE_FACTORY.and(preFilters);
-        }
-        
-        public Expression compilePostFilterExpression(StatementContext context) throws SQLException {
-        	if (postFilters == null || postFilters.isEmpty())
-        		return null;
-        	
             ExpressionCompiler expressionCompiler = new ExpressionCompiler(context);
             List<Expression> expressions = new ArrayList<Expression>(postFilters.size());
             for (ParseNode postFilter : postFilters) {
@@ -284,7 +333,7 @@ public class JoinCompiler {
             }
             
             if (expressions.size() == 1)
-            	return expressions.get(0);
+                return expressions.get(0);
             
             return AndExpression.create(expressions);
         }
@@ -297,25 +346,23 @@ public class JoinCompiler {
          * @return a boolean vector for a star join; or null for non star join.
          */
         public boolean[] getStarJoinVector() {
-            assert(!joinTables.isEmpty());
-            
-            int count = joinTables.size();
+            int count = joinSpecs.size();
             if (!useStarJoin 
                     && count > 1 
-                    && joinTables.get(count - 1).getType() != JoinType.Left)
+                    && joinSpecs.get(count - 1).getType() != JoinType.Left)
                 return null;
 
             boolean[] vector = new boolean[count];
             for (int i = 0; i < count; i++) {
-                JoinTable joinTable = joinTables.get(i);
-                if (joinTable.getType() != JoinType.Left 
-                        && joinTable.getType() != JoinType.Inner)
+                JoinSpec joinSpec = joinSpecs.get(i);
+                if (joinSpec.getType() != JoinType.Left 
+                        && joinSpec.getType() != JoinType.Inner)
                     return null;
                 vector[i] = true;
-                Iterator<TableRef> iter = joinTable.getLeftTableRefs().iterator();
+                Iterator<TableRef> iter = joinSpec.getDependencies().iterator();
                 while (vector[i] == true && iter.hasNext()) {
                     TableRef tableRef = iter.next();
-                    if (!tableRef.equals(mainTable)) {
+                    if (!tableRef.equals(table.getTableRef())) {
                         vector[i] = false;
                     }
                 }
@@ -324,328 +371,83 @@ public class JoinCompiler {
             return vector;
         }
         
-        protected boolean isWildCardSelect(TableRef table) {
-            List<AliasedNode> selectList = table.equals(mainTable) ? this.select : tableRefToJoinTableMap.get(table).getSelect();
-            return (selectList.size() == 1 && selectList.get(0).getNode() instanceof TableWildcardParseNode);
-        }
-
-        public void projectColumns(Scan scan, TableRef table) {
-            if (isWildCardSelect(table)) {
-                scan.getFamilyMap().clear();
-                return;
-            }
-            for (ColumnRef columnRef : columnRefs.keySet()) {
-                if (columnRef.getTableRef().equals(table)
-                        && !SchemaUtil.isPKColumn(columnRef.getColumn())) {
-                    scan.addColumn(columnRef.getColumn().getFamilyName().getBytes(), columnRef.getColumn().getName().getBytes());
-                }
-            }
-        }
-        
-        public ProjectedPTableWrapper createProjectedTable(TableRef tableRef, boolean retainPKColumns) throws SQLException {
-        	List<PColumn> projectedColumns = new ArrayList<PColumn>();
-        	List<Expression> sourceExpressions = new ArrayList<Expression>();
-        	ListMultimap<String, String> columnNameMap = ArrayListMultimap.<String, String>create();
-            PTable table = tableRef.getTable();
-            boolean hasSaltingColumn = retainPKColumns && table.getBucketNum() != null;
-            if (retainPKColumns) {
-            	for (PColumn column : table.getPKColumns()) {
-            		addProjectedColumn(projectedColumns, sourceExpressions, columnNameMap,
-            				column, tableRef, column.getFamilyName(), hasSaltingColumn);
-            	}
-            }
-            if (isWildCardSelect(tableRef)) {
-            	for (PColumn column : table.getColumns()) {
-            		if (!retainPKColumns || !SchemaUtil.isPKColumn(column)) {
-            			addProjectedColumn(projectedColumns, sourceExpressions, columnNameMap,
-            					column, tableRef, PNameFactory.newName(ScanProjector.VALUE_COLUMN_FAMILY), hasSaltingColumn);
-            		}
-            	}
-            } else {
-                for (Map.Entry<ColumnRef, ColumnRefType> e : columnRefs.entrySet()) {
-                    ColumnRef columnRef = e.getKey();
-                    if (e.getValue() != ColumnRefType.PREFILTER 
-                            && columnRef.getTableRef().equals(tableRef)
-                            && (!retainPKColumns || !SchemaUtil.isPKColumn(columnRef.getColumn()))) {
-                    	PColumn column = columnRef.getColumn();
-            			addProjectedColumn(projectedColumns, sourceExpressions, columnNameMap,
-            					column, tableRef, PNameFactory.newName(ScanProjector.VALUE_COLUMN_FAMILY), hasSaltingColumn);
-                    }
-                }            	
-            }
-            
-            PTable t = PTableImpl.makePTable(table.getTenantId(), PNameFactory.newName(PROJECTED_TABLE_SCHEMA), table.getName(), PTableType.JOIN,
-                        table.getIndexState(), table.getTimeStamp(), table.getSequenceNumber(), table.getPKName(),
-                        retainPKColumns ? table.getBucketNum() : null, projectedColumns, table.getParentTableName(),
-                        table.getIndexes(), table.isImmutableRows(), Collections.<PName>emptyList(), null, null, table.isWALDisabled(), table.isMultiTenant(), table.getViewType(), table.getViewIndexId());
-            return new ProjectedPTableWrapper(t, columnNameMap, sourceExpressions);
+        public JoinTable getSubJoinTableWithoutPostFilters() {
+            return joinSpecs.size() > 1 ? new JoinTable(table, joinSpecs.subList(0, joinSpecs.size() - 1)) :
+                new JoinTable(table);
         }
         
-        private static void addProjectedColumn(List<PColumn> projectedColumns, List<Expression> sourceExpressions,
-        		ListMultimap<String, String> columnNameMap, PColumn sourceColumn, TableRef sourceTable, PName familyName, boolean hasSaltingColumn) 
-        throws SQLException {
-            if (sourceColumn == SALTING_COLUMN)
-                return;
+        public SelectStatement getSubqueryWithoutJoin(boolean asSubquery) {
+            if (asSubquery)
+                return table.getAsSubquery();
             
-        	int position = projectedColumns.size() + (hasSaltingColumn ? 1 : 0);
-        	PTable table = sourceTable.getTable();
-        	String schemaName = table.getSchemaName().getString();
-        	String tableName = table.getTableName().getString();
-        	String colName = sourceColumn.getName().getString();
-            String fullName = getProjectedColumnName(schemaName, tableName, colName);
-            String aliasedName = sourceTable.getTableAlias() == null ? fullName : getProjectedColumnName(null, sourceTable.getTableAlias(), colName);
-        	
-            columnNameMap.put(colName, aliasedName);
-        	if (!fullName.equals(aliasedName)) {
-        		columnNameMap.put(fullName, aliasedName);
-        	}
-            
-        	PName name = PNameFactory.newName(aliasedName);
-    		PColumnImpl column = new PColumnImpl(name, familyName, sourceColumn.getDataType(), 
-    				sourceColumn.getMaxLength(), sourceColumn.getScale(), sourceColumn.isNullable(), 
-    				position, sourceColumn.getSortOrder(), sourceColumn.getArraySize(), sourceColumn.getViewConstant(), sourceColumn.isViewReferenced());
-        	Expression sourceExpression = new ColumnRef(sourceTable, sourceColumn.getPosition()).newColumnExpression();
-        	projectedColumns.add(column);
-        	sourceExpressions.add(sourceExpression);
-        }
-        
-        public ColumnResolver getColumnResolver(PTableWrapper table) {
-            return new JoinedTableColumnResolver(table, origResolver);
+            return NODE_FACTORY.select(Collections.<TableNode>singletonList(table.getTableNode()), statement.getHint(), statement.isDistinct(), statement.getSelect(), table.getPreFiltersCombined(), statement.getGroupBy(), statement.getHaving(), statement.getOrderBy(), statement.getLimit(), statement.getBindCount(), statement.isAggregate());
         }
         
-        public boolean hasPostReference(TableRef table) {
-            if (isWildCardSelect(table)) 
-                return true;
-            
-            for (Map.Entry<ColumnRef, ColumnRefType> e : columnRefs.entrySet()) {
-                if (e.getValue() == ColumnRefType.GENERAL && e.getKey().getTableRef().equals(table)) {
+        public boolean hasPostReference() {
+            for (Table table : tables) {
+                if (table.isWildCardSelect()) {
                     return true;
                 }
             }
             
-            return false;
-        }
-        
-        private class WhereNodeVisitor  extends TraverseNoParseNodeVisitor<Void> {
-            private ColumnResolver resolver;
-            private boolean hasRightJoin;
-            private List<TableRef> prefilterAcceptedTables;
-            
-            public WhereNodeVisitor(ColumnResolver resolver, boolean hasRightJoin, List<TableRef> prefilterAcceptedTables) {
-                this.resolver = resolver;
-                this.hasRightJoin = hasRightJoin;
-                this.prefilterAcceptedTables = prefilterAcceptedTables;
-            }
-            
-            private Void leaveBooleanNode(ParseNode node,
-                    List<Void> l) throws SQLException {
-                ColumnParseNodeVisitor visitor = new ColumnParseNodeVisitor(resolver);
-                node.accept(visitor);
-                ColumnParseNodeVisitor.ContentType type = visitor.getContentType(mainTable);
-                switch (type) {
-                case NONE:
-                case SELF_ONLY:
-                    if (!hasRightJoin) {
-                        preFilters.add(node);
-                    } else {
-                        postFilters.add(node);
-                    }
-                    break;
-                case FOREIGN_ONLY:
-                    TableRef matched = null;
-                    for (TableRef table : prefilterAcceptedTables) {
-                        if (visitor.getContentType(table) == ColumnParseNodeVisitor.ContentType.SELF_ONLY) {
-                            matched = table;
-                            break;
-                        }
-                    }
-                    if (matched != null) {
-                        tableRefToJoinTableMap.get(matched).preFilters.add(node);
-                    } else {
-                        postFilters.add(node);
-                    }
-                    break;
-                default:
-                    postFilters.add(node);
-                    break;
+            for (Map.Entry<ColumnRef, ColumnRefType> e : columnRefs.entrySet()) {
+                if (e.getValue() == ColumnRefType.GENERAL && tableRefs.contains(e.getKey().getTableRef())) {
+                    return true;
                 }
-                return null;
-            }
-
-            @Override
-            public Void visitLeave(LikeParseNode node,
-                    List<Void> l) throws SQLException {                
-                return leaveBooleanNode(node, l);
-            }
-
-            @Override
-            public boolean visitEnter(AndParseNode node) {
-                return true;
             }
             
-            @Override
-            public Void visitLeave(OrParseNode node, List<Void> l)
-                    throws SQLException {
-                return leaveBooleanNode(node, l);
-            }
-
-            @Override
-            public Void visitLeave(ComparisonParseNode node, List<Void> l) 
-                    throws SQLException {
-                return leaveBooleanNode(node, l);
-            }
-
-            @Override
-            public Void visitLeave(NotParseNode node, List<Void> l)
-                    throws SQLException {
-                return leaveBooleanNode(node, l);
-            }
-
-            @Override
-            public Void visitLeave(InListParseNode node,
-                    List<Void> l) throws SQLException {
-                return leaveBooleanNode(node, l);
-            }
-            
-            @Override
-            public Void visitLeave(IsNullParseNode node, List<Void> l) 
-                    throws SQLException {
-                return leaveBooleanNode(node, l);
-            }
-            
-            @Override
-            public Void visitLeave(FunctionParseNode node, List<Void> l) 
-            		throws SQLException {
-            	return leaveBooleanNode(node, l);
-            }
-            
-            @Override
-            public Void visitLeave(BetweenParseNode node, List<Void> l) 
-            		throws SQLException {
-            	return leaveBooleanNode(node, l);
-            }
-            
-            @Override
-            public Void visitLeave(CaseParseNode node, List<Void> l) 
-            		throws SQLException {
-            	return leaveBooleanNode(node, l);
-            }
-            
-            @Override
-            public Void visitLeave(CastParseNode node, List<Void> l) 
-            		throws SQLException {
-            	return leaveBooleanNode(node, l);
-            }			
+            return false;
         }
     }
     
-    public static JoinSpec getSubJoinSpecWithoutPostFilters(JoinSpec join) {
-        return new JoinSpec(join.origResolver, join.mainTableNode, join.dynamicColumns, join.mainTable, join.select, join.preFilters, new ArrayList<ParseNode>(), 
-                join.joinTables.subList(0, join.joinTables.size() - 1), join.useStarJoin, join.tableRefToJoinTableMap, join.columnRefs);
-    }
-    
-    public static class JoinTable {
-        private JoinType type;
-        private TableNode tableNode; // original table node
-        private List<ColumnDef> dynamicColumns;
-        private TableRef table;
-        private List<AliasedNode> select; // all basic nodes related to this table, no aggregation.
-        private HintNode hint;
-        private List<ParseNode> preFilters;
-        private List<ParseNode> conditions;
-        private SelectStatement subquery;
-        
-        private Set<TableRef> leftTableRefs;
+    public static class JoinSpec {
+        private final JoinType type;
+        private final List<ComparisonParseNode> onConditions;
+        private final JoinTable joinTable;
+        private Set<TableRef> dependencies;
         
-        public JoinTable(JoinTableNode node, TableRef tableRef, SelectStatement statement, ColumnResolver resolver) throws SQLException {
-            if (!(node.getTable() instanceof ConcreteTableNode))
-                throw new SQLFeatureNotSupportedException("Subqueries not supported.");
-            
-            this.type = node.getType();
-            this.tableNode = node.getTable();
-            DynamicColumnsVisitor v = new DynamicColumnsVisitor();
-            this.tableNode.accept(v);
-            this.dynamicColumns = v.getDynamicColumns();
-            this.table = tableRef;
-            this.select = extractFromSelect(statement.getSelect(),tableRef,resolver);
-            this.hint = statement.getHint();
-            this.preFilters = new ArrayList<ParseNode>();
-            this.conditions = new ArrayList<ParseNode>();
-            this.leftTableRefs = new HashSet<TableRef>();
-            node.getOnNode().accept(new OnNodeVisitor(resolver));
+        private JoinSpec(JoinType type, ParseNode onNode, JoinTable joinTable, 
+                ColumnResolver resolver) throws SQLException {
+            this.type = type;
+            this.onConditions = new ArrayList<ComparisonParseNode>();
+            this.joinTable = joinTable;
+            this.dependencies = new HashSet<TableRef>();
+            OnNodeVisitor visitor = new OnNodeVisitor(resolver, onConditions, dependencies, joinTable);
+            onNode.accept(visitor);
         }
         
         public JoinType getType() {
             return type;
         }
         
-        public TableNode getTableNode() {
-            return tableNode;
+        public List<ComparisonParseNode> getOnConditions() {
+            return onConditions;
         }
         
-        public List<ColumnDef> getDynamicColumns() {
-            return dynamicColumns;
+        public JoinTable getJoinTable() {
+            return joinTable;
         }
         
-        public TableRef getTable() {
-            return table;
-        }
-        
-        public List<AliasedNode> getSelect() {
-            return select;
-        }
-        
-        public List<ParseNode> getPreFilters() {
-            return preFilters;
-        }
-        
-        public List<ParseNode> getJoinConditions() {
-            return conditions;
-        }
-        
-        public SelectStatement getSubquery() {
-            return subquery;
-        }
-        
-        public Set<TableRef> getLeftTableRefs() {
-            return leftTableRefs;
-        }
-        
-        public ParseNode getPreFiltersCombined() {
-            if (preFilters == null || preFilters.isEmpty())
-                return null;
-            
-            if (preFilters.size() == 1)
-                return preFilters.get(0);
-            
-            return NODE_FACTORY.and(preFilters);
-        }
-        
-        public SelectStatement getAsSubquery() {
-            if (subquery != null)
-                return subquery;
-            
-            List<TableNode> from = new ArrayList<TableNode>(1);
-            from.add(tableNode);
-            return NODE_FACTORY.select(from, hint, false, select, getPreFiltersCombined(), null, null, null, null, 0, false);
+        public Set<TableRef> getDependencies() {
+            return dependencies;
         }
         
         public Pair<List<Expression>, List<Expression>> compileJoinConditions(StatementContext context, ColumnResolver leftResolver, ColumnResolver rightResolver) throws SQLException {
-        	ColumnResolver resolver = context.getResolver();
-            List<Pair<Expression, Expression>> compiled = new ArrayList<Pair<Expression, Expression>>(conditions.size());
-        	context.setResolver(leftResolver);
+            ColumnResolver resolver = context.getResolver();
+            List<Pair<Expression, Expression>> compiled = new ArrayList<Pair<Expression, Expression>>(onConditions.size());
+            context.setResolver(leftResolver);
             ExpressionCompiler expressionCompiler = new ExpressionCompiler(context);
-            for (ParseNode condition : conditions) {
+            for (ParseNode condition : onConditions) {
                 assert (condition instanceof EqualParseNode);
                 EqualParseNode equalNode = (EqualParseNode) condition;
                 expressionCompiler.reset();
                 Expression left = equalNode.getLHS().accept(expressionCompiler);
                 compiled.add(new Pair<Expression, Expression>(left, null));
             }
-        	context.setResolver(rightResolver);
+            context.setResolver(rightResolver);
             expressionCompiler = new ExpressionCompiler(context);
             Iterator<Pair<Expression, Expression>> iter = compiled.iterator();
-            for (ParseNode condition : conditions) {
+            for (ParseNode condition : onConditions) {
                 Pair<Expression, Expression> p = iter.next();
                 EqualParseNode equalNode = (EqualParseNode) condition;
                 expressionCompiler.reset();
@@ -757,127 +559,337 @@ public class JoinCompiler {
 
             return PDataType.VARBINARY;
         }
+    }
+    
+    public class Table {
+        private final TableNode tableNode;
+        private final List<ColumnDef> dynamicColumns;
+        private final TableRef tableRef;
+        private final List<AliasedNode> selectNodes; // all basic nodes related to this table, no aggregation.
+        private final List<ParseNode> preFilters;
+        
+        private Table(TableNode tableNode, List<ColumnDef> dynamicColumns, 
+                List<AliasedNode> selectNodes, TableRef tableRef) {
+            this.tableNode = tableNode;
+            this.dynamicColumns = dynamicColumns;
+            this.tableRef = tableRef;
+            this.selectNodes = selectNodes;
+            this.preFilters = new ArrayList<ParseNode>();
+        }
+        
+        public TableNode getTableNode() {
+            return tableNode;
+        }
+        
+        public List<ColumnDef> getDynamicColumns() {
+            return dynamicColumns;
+        }
+        
+        public List<AliasedNode> getSelectNodes() {
+            return selectNodes;
+        }
+        
+        public List<ParseNode> getPreFilters() {
+            return preFilters;
+        }
         
-        private class OnNodeVisitor  extends TraverseNoParseNodeVisitor<Void> {
-            private ColumnResolver resolver;
+        public TableRef getTableRef() {
+            return tableRef;
+        }
+        
+        public ParseNode getPreFiltersCombined() {
+            if (preFilters == null || preFilters.isEmpty())
+                return null;
             
-            public OnNodeVisitor(ColumnResolver resolver) {
-                this.resolver = resolver;
-            }
+            if (preFilters.size() == 1)
+                return preFilters.get(0);
             
-            private Void leaveNonEqBooleanNode(ParseNode node,
-                    List<Void> l) throws SQLException {
-                ColumnParseNodeVisitor visitor = new ColumnParseNodeVisitor(resolver);
-                node.accept(visitor);
-                ColumnParseNodeVisitor.ContentType type = visitor.getContentType(table);
-                if (type == ColumnParseNodeVisitor.ContentType.NONE 
-                        || type == ColumnParseNodeVisitor.ContentType.SELF_ONLY) {
-                    preFilters.add(node);
-                } else {
-                    throwUnsupportedJoinConditionException();
-                }
-                return null;
-            }
-
-            @Override
-            public Void visitLeave(LikeParseNode node,
-                    List<Void> l) throws SQLException {                
-                return leaveNonEqBooleanNode(node, l);
-            }
+            return NODE_FACTORY.and(preFilters);
+        }
+        
+        public SelectStatement getAsSubquery() {
+            // TODO handle DerivedTableNode differently?
+            List<TableNode> from = Collections.<TableNode>singletonList(tableNode);
+            return NODE_FACTORY.select(from, statement.getHint(), false, selectNodes, getPreFiltersCombined(), null, null, null, null, 0, false);
+        }
+        
+        protected boolean isWildCardSelect() {
+            return (selectNodes.size() == 1 && selectNodes.get(0).getNode() instanceof TableWildcardParseNode);
+        }
 
-            @Override
-            public boolean visitEnter(AndParseNode node) {
-                return true;
-            }
-            
-            @Override
-            public Void visitLeave(OrParseNode node, List<Void> l)
-                    throws SQLException {
-                return leaveNonEqBooleanNode(node, l);
+        public void projectColumns(Scan scan) {
+            if (isWildCardSelect()) {
+                scan.getFamilyMap().clear();
+                return;
             }
-
-            @Override
-            public Void visitLeave(ComparisonParseNode node, List<Void> l) 
-                    throws SQLException {
-                if (!(node instanceof EqualParseNode))
-                    return leaveNonEqBooleanNode(node, l);
-                ColumnParseNodeVisitor lhsVisitor = new ColumnParseNodeVisitor(resolver);
-                ColumnParseNodeVisitor rhsVisitor = new ColumnParseNodeVisitor(resolver);
-                node.getLHS().accept(lhsVisitor);
-                node.getRHS().accept(rhsVisitor);
-                ColumnParseNodeVisitor.ContentType lhsType = lhsVisitor.getContentType(table);
-                ColumnParseNodeVisitor.ContentType rhsType = rhsVisitor.getContentType(table);
-                if ((lhsType == ColumnParseNodeVisitor.ContentType.SELF_ONLY || lhsType == ColumnParseNodeVisitor.ContentType.NONE)
-                		&& (rhsType == ColumnParseNodeVisitor.ContentType.SELF_ONLY || rhsType == ColumnParseNodeVisitor.ContentType.NONE)) {
-                    preFilters.add(node);
-                } else if (lhsType == ColumnParseNodeVisitor.ContentType.FOREIGN_ONLY 
-                		&& rhsType == ColumnParseNodeVisitor.ContentType.SELF_ONLY) {
-                    conditions.add(node);
-                    leftTableRefs.addAll(lhsVisitor.getTableRefSet());
-                } else if (rhsType == ColumnParseNodeVisitor.ContentType.FOREIGN_ONLY 
-                		&& lhsType == ColumnParseNodeVisitor.ContentType.SELF_ONLY) {
-                    conditions.add(NODE_FACTORY.equal(node.getRHS(), node.getLHS()));
-                    leftTableRefs.addAll(rhsVisitor.getTableRefSet());
-                } else {
-                	throwUnsupportedJoinConditionException();
+            for (ColumnRef columnRef : columnRefs.keySet()) {
+                if (columnRef.getTableRef().equals(tableRef)
+                        && !SchemaUtil.isPKColumn(columnRef.getColumn())) {
+                    scan.addColumn(columnRef.getColumn().getFamilyName().getBytes(), columnRef.getColumn().getName().getBytes());
                 }
-                return null;
             }
-
-            @Override
-            public Void visitLeave(NotParseNode node, List<Void> l)
-                    throws SQLException {
-                return leaveNonEqBooleanNode(node, l);
+        }
+        
+        public ProjectedPTableWrapper createProjectedTable(boolean retainPKColumns) throws SQLException {
+            List<PColumn> projectedColumns = new ArrayList<PColumn>();
+            List<Expression> sourceExpressions = new ArrayList<Expression>();
+            ListMultimap<String, String> columnNameMap = ArrayListMultimap.<String, String>create();
+            PTable table = tableRef.getTable();
+            boolean hasSaltingColumn = retainPKColumns && table.getBucketNum() != null;
+            if (retainPKColumns) {
+                for (PColumn column : table.getPKColumns()) {
+                    addProjectedColumn(projectedColumns, sourceExpressions, columnNameMap,
+                            column, column.getFamilyName(), hasSaltingColumn);
+                }
             }
-
-            @Override
-            public Void visitLeave(InListParseNode node,
-                    List<Void> l) throws SQLException {
-                return leaveNonEqBooleanNode(node, l);
+            if (isWildCardSelect()) {
+                for (PColumn column : table.getColumns()) {
+                    if (!retainPKColumns || !SchemaUtil.isPKColumn(column)) {
+                        addProjectedColumn(projectedColumns, sourceExpressions, columnNameMap,
+                                column, PNameFactory.newName(ScanProjector.VALUE_COLUMN_FAMILY), hasSaltingColumn);
+                    }
+                }
+            } else {
+                for (Map.Entry<ColumnRef, ColumnRefType> e : columnRefs.entrySet()) {
+                    ColumnRef columnRef = e.getKey();
+                    if (e.getValue() != ColumnRefType.PREFILTER 
+                            && columnRef.getTableRef().equals(tableRef)
+                            && (!retainPKColumns || !SchemaUtil.isPKColumn(columnRef.getColumn()))) {
+                        PColumn column = columnRef.getColumn();
+                        addProjectedColumn(projectedColumns, sourceExpressions, columnNameMap,
+                                column, PNameFactory.newName(ScanProjector.VALUE_COLUMN_FAMILY), hasSaltingColumn);
+                    }
+                }               
             }
             
-            @Override
-            public Void visitLeave(IsNullParseNode node, List<Void> l) 
-                    throws SQLException {
-                return leaveNonEqBooleanNode(node, l);
-            }
+            PTable t = PTableImpl.makePTable(table.getTenantId(), PNameFactory.newName(PROJECTED_TABLE_SCHEMA), table.getName(), PTableType.JOIN,
+                        table.getIndexState(), table.getTimeStamp(), table.getSequenceNumber(), table.getPKName(),
+                        retainPKColumns ? table.getBucketNum() : null, projectedColumns, table.getParentTableName(),
+                        table.getIndexes(), table.isImmutableRows(), Collections.<PName>emptyList(), null, null, table.isWALDisabled(), table.isMultiTenant(), table.getViewType(), table.getViewIndexId());
+            return new ProjectedPTableWrapper(t, columnNameMap, sourceExpressions);
+        }
+        
+        private void addProjectedColumn(List<PColumn> projectedColumns, List<Expression> sourceExpressions,
+                ListMultimap<String, String> columnNameMap, PColumn sourceColumn, PName familyName, boolean hasSaltingColumn) 
+        throws SQLException {
+            if (sourceColumn == SALTING_COLUMN)
+                return;
             
-            @Override
-            public Void visitLeave(FunctionParseNode node, List<Void> l) 
-            		throws SQLException {
-            	return leaveNonEqBooleanNode(node, l);
-            }
+            int position = projectedColumns.size() + (hasSaltingColumn ? 1 : 0);
+            PTable table = tableRef.getTable();
+            String schemaName = table.getSchemaName().getString();
+            String tableName = table.getTableName().getString();
+            String colName = sourceColumn.getName().getString();
+            String fullName = getProjectedColumnName(schemaName, tableName, colName);
+            String aliasedName = tableRef.getTableAlias() == null ? fullName : getProjectedColumnName(null, tableRef.getTableAlias(), colName);
             
-            @Override
-            public Void visitLeave(BetweenParseNode node, List<Void> l) 
-            		throws SQLException {
-            	return leaveNonEqBooleanNode(node, l);
+            columnNameMap.put(colName, aliasedName);
+            if (!fullName.equals(aliasedName)) {
+                columnNameMap.put(fullName, aliasedName);
             }
             
-            @Override
-            public Void visitLeave(CaseParseNode node, List<Void> l) 
-            		throws SQLException {
-            	return leaveNonEqBooleanNode(node, l);
+            PName name = PNameFactory.newName(aliasedName);
+            PColumnImpl column = new PColumnImpl(name, familyName, sourceColumn.getDataType(), 
+                    sourceColumn.getMaxLength(), sourceColumn.getScale(), sourceColumn.isNullable(), 
+                    position, sourceColumn.getSortOrder(), sourceColumn.getArraySize(), sourceColumn.getViewConstant(), sourceColumn.isViewReferenced());
+            Expression sourceExpression = new ColumnRef(tableRef, sourceColumn.getPosition()).newColumnExpression();
+            projectedColumns.add(column);
+            sourceExpressions.add(sourceExpression);
+        }
+    }
+    
+    private static abstract class ConditionNodeVisitor extends TraverseNoParseNodeVisitor<Void> {
+        
+        protected abstract Void leaveBooleanNode(ParseNode node, List<Void> l) throws SQLException;
+
+        @Override
+        public Void visitLeave(LikeParseNode node,
+                List<Void> l) throws SQLException {                
+            return leaveBooleanNode(node, l);
+        }
+
+        @Override
+        public boolean visitEnter(AndParseNode node) {
+            return true;
+        }
+        
+        @Override
+        public Void visitLeave(OrParseNode node, List<Void> l)
+                throws SQLException {
+            return leaveBooleanNode(node, l);
+        }
+
+        @Override
+        public Void visitLeave(ComparisonParseNode node, List<Void> l) 
+                throws SQLException {
+            return leaveBooleanNode(node, l);
+        }
+
+        @Override
+        public Void visitLeave(NotParseNode node, List<Void> l)
+                throws SQLException {
+            return leaveBooleanNode(node, l);
+        }
+
+        @Override
+        public Void visitLeave(InListParseNode node,
+                List<Void> l) throws SQLException {
+            return leaveBooleanNode(node, l);
+        }
+        
+        @Override
+        public Void visitLeave(IsNullParseNode node, List<Void> l) 
+                throws SQLException {
+            return leaveBooleanNode(node, l);
+        }
+        
+        @Override
+        public Void visitLeave(FunctionParseNode node, List<Void> l) 
+                throws SQLException {
+            return leaveBooleanNode(node, l);
+        }
+        
+        @Override
+        public Void visitLeave(BetweenParseNode node, List<Void> l) 
+                throws SQLException {
+            return leaveBooleanNode(node, l);
+        }
+        
+        @Override
+        public Void visitLeave(CaseParseNode node, List<Void> l) 
+                throws SQLException {
+            return leaveBooleanNode(node, l);
+        }
+        
+        @Override
+        public Void visitLeave(CastParseNode node, List<Void> l) 
+                throws SQLException {
+            return leaveBooleanNode(node, l);
+        }           
+    }
+    
+    private static class WhereNodeVisitor extends ConditionNodeVisitor {
+        private ColumnResolver resolver;
+        private List<ParseNode> preFilters;
+        private List<ParseNode> postFilters;
+        private List<TableRef> selfTableRefs;
+        private boolean hasRightJoin;
+        private List<JoinTable> prefilterAcceptedTables;
+        
+        public WhereNodeVisitor(ColumnResolver resolver, List<ParseNode> preFilters,
+                List<ParseNode> postFilters, List<TableRef> selfTableRefs, boolean hasRightJoin, 
+                List<JoinTable> prefilterAcceptedTables) {
+            this.resolver = resolver;
+            this.preFilters = preFilters;
+            this.postFilters = postFilters;
+            this.selfTableRefs = selfTableRefs;
+            this.hasRightJoin = hasRightJoin;
+            this.prefilterAcceptedTables = prefilterAcceptedTables;
+        }
+        
+        protected Void leaveBooleanNode(ParseNode node,
+                List<Void> l) throws SQLException {
+            ColumnParseNodeVisitor visitor = new ColumnParseNodeVisitor(resolver);
+            node.accept(visitor);
+            ColumnParseNodeVisitor.ContentType type = visitor.getContentType(selfTableRefs);
+            switch (type) {
+            case NONE:
+            case SELF_ONLY:
+                if (!hasRightJoin) {
+                    preFilters.add(node);
+                } else {
+                    postFilters.add(node);
+                }
+                break;
+            case FOREIGN_ONLY:
+                JoinTable matched = null;
+                for (JoinTable joinTable : prefilterAcceptedTables) {
+                    if (visitor.getContentType(joinTable.getTableRefs()) == ColumnParseNodeVisitor.ContentType.SELF_ONLY) {
+                        matched = joinTable;
+                        break;
+                    }
+                }
+                if (matched != null) {
+                    matched.addFilter(node);
+                } else {
+                    postFilters.add(node);
+                }
+                break;
+            default:
+                postFilters.add(node);
+                break;
             }
-            
-            @Override
-            public Void visitLeave(CastParseNode node, List<Void> l) 
-            		throws SQLException {
-            	return leaveNonEqBooleanNode(node, l);
+            return null;
+        }
+    }
+    
+    private static class OnNodeVisitor extends ConditionNodeVisitor {
+        private ColumnResolver resolver;
+        private List<ComparisonParseNode> onConditions;
+        private Set<TableRef> dependencies;
+        private JoinTable joinTable;
+        
+        public OnNodeVisitor(ColumnResolver resolver, List<ComparisonParseNode> onConditions, 
+                Set<TableRef> dependencies, JoinTable joinTable) {
+            this.resolver = resolver;
+            this.onConditions = onConditions;
+            this.dependencies = dependencies;
+            this.joinTable = joinTable;
+        }
+        
+        protected Void leaveBooleanNode(ParseNode node,
+                List<Void> l) throws SQLException {
+            ColumnParseNodeVisitor visitor = new ColumnParseNodeVisitor(resolver);
+            node.accept(visitor);
+            ColumnParseNodeVisitor.ContentType type = visitor.getContentType(joinTable.getTableRefs());
+            if (type == ColumnParseNodeVisitor.ContentType.NONE 
+                    || type == ColumnParseNodeVisitor.ContentType.SELF_ONLY) {
+                joinTable.addFilter(node);
+            } else {
+                throwUnsupportedJoinConditionException();
             }
+            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.
-             */
-            private void throwUnsupportedJoinConditionException() 
-            		throws SQLFeatureNotSupportedException {
-            	throw new SQLFeatureNotSupportedException("Does not support non-standard or non-equi join conditions.");
-            }			
+        @Override
+        public Void visitLeave(ComparisonParseNode node, List<Void> l) 
+                throws SQLException {
+            if (!(node instanceof EqualParseNode))
+                return leaveBooleanNode(node, l);
+            ColumnParseNodeVisitor lhsVisitor = new ColumnParseNodeVisitor(resolver);
+            ColumnParseNodeVisitor rhsVisitor = new ColumnParseNodeVisitor(resolver);
+            node.getLHS().accept(lhsVisitor);
+            node.getRHS().accept(rhsVisitor);
+            ColumnParseNodeVisitor.ContentType lhsType = lhsVisitor.getContentType(joinTable.getTableRefs());
+            ColumnParseNodeVisitor.ContentType rhsType = rhsVisitor.getContentType(joinTable.getTableRefs());
+            if ((lhsType == ColumnParseNodeVisitor.ContentType.SELF_ONLY || lhsType == ColumnParseNodeVisitor.ContentType.NONE)
+                    && (rhsType == ColumnParseNodeVisitor.ContentType.SELF_ONLY || rhsType == ColumnParseNodeVisitor.ContentType.NONE)) {
+                joinTable.addFilter(node);
+            } else if (lhsType == ColumnParseNodeVisitor.ContentType.FOREIGN_ONLY 
+                    && rhsType == ColumnParseNodeVisitor.ContentType.SELF_ONLY) {
+                onConditions.add(node);
+                dependencies.addAll(lhsVisitor.getTableRefSet());
+            } else if (rhsType == ColumnParseNodeVisitor.ContentType.FOREIGN_ONLY 
+                    && lhsType == ColumnParseNodeVisitor.ContentType.SELF_ONLY) {
+                onConditions.add(NODE_FACTORY.equal(node.getRHS(), node.getLHS()));
+                dependencies.addAll(rhsVisitor.getTableRefSet());
+            } else {
+                throwUnsupportedJoinConditionException();
+            }
+            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.
+         */
+        private void throwUnsupportedJoinConditionException() 
+                throws SQLFeatureNotSupportedException {
+            throw new SQLFeatureNotSupportedException("Does not support non-standard or non-equi join conditions.");
+        }           
     }
     
     private static class ColumnParseNodeVisitor  extends StatelessTraverseAllParseNodeVisitor {
@@ -914,14 +926,33 @@ public class JoinCompiler {
             return columnRefMap;
         }
         
-        public ContentType getContentType(TableRef selfTable) {
+        public ContentType getContentType(List<TableRef> selfTableRefs) {
             if (tableRefSet.isEmpty())
                 return ContentType.NONE;
-            if (tableRefSet.size() > 1)
-                return ContentType.COMPLEX;
-            if (tableRefSet.contains(selfTable))
-                return ContentType.SELF_ONLY;
-            return ContentType.FOREIGN_ONLY;
+            
+            ContentType ret = ContentType.NONE;
+            for (TableRef tRef : tableRefSet) {
+                boolean isSelf = selfTableRefs.contains(tRef);
+                switch (ret) {
+                case NONE:
+                    ret = isSelf ? ContentType.SELF_ONLY : ContentType.FOREIGN_ONLY;
+                    break;
+                case SELF_ONLY:
+                    ret = isSelf ? ContentType.SELF_ONLY : ContentType.COMPLEX;
+                    break;
+                case FOREIGN_ONLY:
+                    ret = isSelf ? ContentType.COMPLEX : ContentType.FOREIGN_ONLY;
+                    break;
+                default: // COMPLEX do nothing
+                    break;    
+                }
+                
+                if (ret == ContentType.COMPLEX) {
+                    break;
+                }
+            }
+            
+            return ret;
         }
     }
     
@@ -929,14 +960,14 @@ public class JoinCompiler {
     // for creation of new statements
     private static ParseNodeFactory NODE_FACTORY = new ParseNodeFactory();
     
-    private static List<AliasedNode> extractFromSelect(List<AliasedNode> select, TableRef table, ColumnResolver resolver) throws SQLException {
+    private static List<AliasedNode> extractFromSelect(List<AliasedNode> select, TableRef tableRef, ColumnResolver resolver) throws SQLException {
         List<AliasedNode> ret = new ArrayList<AliasedNode>();
         ColumnParseNodeVisitor visitor = new ColumnParseNodeVisitor(resolver);
         for (AliasedNode aliasedNode : select) {
             ParseNode node = aliasedNode.getNode();
             if (node instanceof TableWildcardParseNode) {
                 TableName tableName = ((TableWildcardParseNode) node).getTableName();
-                if (table.equals(resolver.resolveTable(tableName.getSchemaName(), tableName.getTableName()))) {
+                if (tableRef.equals(resolver.resolveTable(tableName.getSchemaName(), tableName.getTableName()))) {
                     ret.clear();
                     ret.add(aliasedNode);
                     return ret;
@@ -945,12 +976,12 @@ public class JoinCompiler {
             }
             
             node.accept(visitor);
-            ColumnParseNodeVisitor.ContentType type = visitor.getContentType(table);
+            ColumnParseNodeVisitor.ContentType type = visitor.getContentType(Collections.singletonList(tableRef));
             if (type == ColumnParseNodeVisitor.ContentType.SELF_ONLY) {
                 ret.add(aliasedNode);
             } else if (type == ColumnParseNodeVisitor.ContentType.COMPLEX) {
                 for (Map.Entry<ColumnRef, ColumnParseNode> entry : visitor.getColumnRefMap().entrySet()) {
-                    if (entry.getKey().getTableRef().equals(table)) {
+                    if (entry.getKey().getTableRef().equals(tableRef)) {
                         ret.add(NODE_FACTORY.aliasedNode(null, entry.getValue()));
                     }
                 }
@@ -960,12 +991,8 @@ public class JoinCompiler {
         return ret;
     }
     
-    public static JoinSpec getJoinSpec(StatementContext context, SelectStatement statement) throws SQLException {
-        return new JoinSpec(statement, context.getResolver());
-    }
-    
     public static SelectStatement optimize(StatementContext context, SelectStatement select, PhoenixStatement statement) throws SQLException {
-        ColumnResolver resolver = context.getResolver();
+        final ColumnResolver resolver = context.getResolver();
         TableRef groupByTableRef = null;
         TableRef orderByTableRef = null;
         if (select.getGroupBy() != null && !select.getGroupBy().isEmpty()) {
@@ -987,12 +1014,12 @@ public class JoinCompiler {
                 orderByTableRef = set.iterator().next();
             }
         }
-        JoinSpec join = getJoinSpec(context, select);
+        JoinTable join = compile(select, context.getResolver());
         if (groupByTableRef != null || orderByTableRef != null) {
             QueryCompiler compiler = new QueryCompiler(statement, select, resolver);
             List<Object> binds = statement.getParameters();
             StatementContext ctx = new StatementContext(statement, resolver, new Scan());
-            QueryPlan plan = compiler.compileJoinQuery(ctx, select, binds, join, false);
+            QueryPlan plan = compiler.compileJoinQuery(ctx, binds, join, false);
             TableRef table = plan.getTableRef();
             if (groupByTableRef != null && !groupByTableRef.equals(table)) {
                 groupByTableRef = null;
@@ -1002,94 +1029,81 @@ public class JoinCompiler {
             }            
         }
         
-        Map<TableRef, TableRef> replacement = new HashMap<TableRef, TableRef>();
+        final Map<TableRef, TableRef> replacement = new HashMap<TableRef, TableRef>();
+        
+        for (Table table : join.getTables()) {
+            TableRef tableRef = table.getTableRef();
+            List<ParseNode> groupBy = tableRef.equals(groupByTableRef) ? select.getGroupBy() : null;
+            List<OrderByNode> orderBy = tableRef.equals(orderByTableRef) ? select.getOrderBy() : null;
+            SelectStatement stmt = getSubqueryForOptimizedPlan(select.getHint(), table.getDynamicColumns(), tableRef, join.getColumnRefs(), table.getPreFiltersCombined(), groupBy, orderBy, table.isWildCardSelect());
+            QueryPlan plan = context.getConnection().getQueryServices().getOptimizer().optimize(statement, stmt);
+            if (!plan.getTableRef().equals(tableRef)) {
+                replacement.put(tableRef, plan.getTableRef());            
+            }            
+        }
+        
+        if (replacement.isEmpty()) 
+            return select;
+        
         List<TableNode> from = select.getFrom();
         List<TableNode> newFrom = Lists.newArrayListWithExpectedSize(from.size());
+        for (TableNode node : from) {
+            newFrom.add(node.accept(new TableNodeVisitor<TableNode>() {
+                private TableRef resolveTable(String alias, TableName name) throws SQLException {
+                    if (alias != null)
+                        return resolver.resolveTable(null, alias);
 
-        class TableNodeRewriter implements TableNodeVisitor {
-            private TableRef table;
-            private TableNode replaced;
-            
-            TableNodeRewriter(TableRef table) {
-                this.table = table;
-            }
-            
-            public TableNode getReplacedTableNode() {
-                return replaced;
-            }
+                    return resolver.resolveTable(name.getSchemaName(), name.getTableName());
+                }
 
-            @Override
-            public void visit(BindTableNode boundTableNode) throws SQLException {
-                String alias = boundTableNode.getAlias();
-                replaced = NODE_FACTORY.bindTable(alias == null ? null : '"' + alias + '"', getReplacedTableName());
-            }
+                private TableName getReplacedTableName(TableRef tableRef) {
+                    String schemaName = tableRef.getTable().getSchemaName().getString();
+                    return TableName.create(schemaName.length() == 0 ? null : schemaName, tableRef.getTable().getTableName().getString());
+                }
 
-            @Override
-            public void visit(JoinTableNode joinNode) throws SQLException {
-                joinNode.getTable().accept(this);
-                replaced = NODE_FACTORY.join(joinNode.getType(), joinNode.getOnNode(), replaced);
-            }
+                @Override
+                public TableNode visit(BindTableNode boundTableNode) throws SQLException {
+                    TableRef tableRef = resolveTable(boundTableNode.getAlias(), boundTableNode.getName());
+                    TableRef replaceRef = replacement.get(tableRef);
+                    if (replaceRef == null)
+                        return boundTableNode;
 
-            @Override
-            public void visit(NamedTableNode namedTableNode)
-                    throws SQLException {
-                String alias = namedTableNode.getAlias();
-                replaced = NODE_FACTORY.namedTable(alias == null ? null : '"' + alias + '"', getReplacedTableName(), namedTableNode.getDynamicColumns());
-            }
+                    String alias = boundTableNode.getAlias();
+                    return NODE_FACTORY.bindTable(alias == null ? null : '"' + alias + '"', getReplacedTableName(replaceRef));
+                }
 
-            @Override
-            public void visit(DerivedTableNode subselectNode)
-                    throws SQLException {
-                throw new SQLFeatureNotSupportedException();
-            }
-            
-            private TableName getReplacedTableName() {
-                String schemaName = table.getTable().getSchemaName().getString();
-                return TableName.create(schemaName.length() == 0 ? null : schemaName, table.getTable().getTableName().getString());
-            }
-        };
-        
-        // get optimized plans for join tables
-        for (int i = 1; i < from.size(); i++) {
-            TableNode jNode = from.get(i);
-            assert (jNode instanceof JoinTableNode);
-            TableNode tNode = ((JoinTableNode) jNode).getTable();
-            for (JoinTable jTable : join.getJoinTables()) {
-                if (jTable.getTableNode() != tNode)
-                    continue;
-                TableRef table = jTable.getTable();
-                List<ParseNode> groupBy = table.equals(groupByTableRef) ? select.getGroupBy() : null;
-                List<OrderByNode> orderBy = table.equals(orderByTableRef) ? select.getOrderBy() : null;
-                SelectStatement stmt = getSubqueryForOptimizedPlan(select.getHint(), join.tableRefToJoinTableMap.get(table).getDynamicColumns(), table, join.columnRefs, jTable.getPreFiltersCombined(), groupBy, orderBy, join.isWildCardSelect(table));
-                QueryPlan plan = context.getConnection().getQueryServices().getOptimizer().optimize(statement, stmt);
-                if (!plan.getTableRef().equals(table)) {
-                    TableNodeRewriter rewriter = new TableNodeRewriter(plan.getTableRef());
-                    jNode.accept(rewriter);
-                    newFrom.add(rewriter.getReplacedTableNode());
-                    replacement.put(table, plan.getTableRef());
-                } else {
-                    newFrom.add(jNode);
+                @Override
+                public TableNode visit(JoinTableNode joinNode) throws SQLException {
+                    TableNode lhs = joinNode.getLHS();
+                    TableNode rhs = joinNode.getRHS();
+                    TableNode lhsReplace = lhs.accept(this);
+                    TableNode rhsReplace = rhs.accept(this);
+                    if (lhs == lhsReplace && rhs == rhsReplace)
+                        return joinNode;
+
+                    return NODE_FACTORY.join(joinNode.getType(), lhsReplace, rhsReplace, joinNode.getOnNode());
                 }
-            }
-        }
-        // get optimized plan for main table
-        TableRef table = join.getMainTable();
-        List<ParseNode> groupBy = table.equals(groupByTableRef) ? select.getGroupBy() : null;
-        List<OrderByNode> orderBy = table.equals(orderByTableRef) ? select.getOrderBy() : null;
-        SelectStatement stmt = getSubqueryForOptimizedPlan(select.getHint(), join.dynamicColumns, table, join.columnRefs, join.getPreFiltersCombined(), groupBy, orderBy, join.isWildCardSelect(table));
-        QueryPlan plan = context.getConnection().getQueryServices().getOptimizer().optimize(statement, stmt);
-        if (!plan.getTableRef().equals(table)) {
-            TableNodeRewriter rewriter = new TableNodeRewriter(plan.getTableRef());
-            from.get(0).accept(rewriter);
-            newFrom.add(0, rewriter.getReplacedTableNode());
-            replacement.put(table, plan.getTableRef());            
-        } else {
-            newFrom.add(0, from.get(0));
+
+                @Override
+                public TableNode visit(NamedTableNode namedTableNode)
+                        throws SQLException {
+                    TableRef tableRef = resolveTable(namedTableNode.getAlias(), namedTableNode.getName());
+                    TableRef replaceRef = replacement.get(tableRef);
+                    if (replaceRef == null)
+                        return namedTableNode;
+
+                    String alias = namedTableNode.getAlias();
+                    return NODE_FACTORY.namedTable(alias == null ? null : '"' + alias + '"', getReplacedTableName(replaceRef), namedTableNode.getDynamicColumns());
+                }
+
+                @Override
+                public TableNode visit(DerivedTableNode subselectNode)
+                        throws SQLException {
+                    throw new SQLFeatureNotSupportedException();
+                }
+            }));
         }
         
-        if (replacement.isEmpty()) 
-            return select;
-        
         return IndexStatementRewriter.translate(NODE_FACTORY.select(select, newFrom), resolver, replacement);        
     }
     
@@ -1116,76 +1130,11 @@ public class JoinCompiler {
         return NODE_FACTORY.select(from, hintNode, false, selectList, where, groupBy, null, orderBy, null, 0, false);
     }
     
-    public static SelectStatement getSubqueryWithoutJoin(SelectStatement statement, JoinSpec join) {
-        return NODE_FACTORY.select(statement.getFrom().subList(0, 1), statement.getHint(), statement.isDistinct(), statement.getSelect(), join.getPreFiltersCombined(), statement.getGroupBy(), statement.getHaving(), statement.getOrderBy(), statement.getLimit(), statement.getBindCount(), statement.isAggregate());
-    }
-    
-    // Get the last join table select statement with fixed-up select and where nodes.
-    // Currently does NOT support last join table as a subquery.
-    public static SelectStatement getSubqueryForLastJoinTable(SelectStatement statement, JoinSpec join) throws SQLException {
-        List<JoinTable> joinTables = join.getJoinTables();
-        int count = joinTables.size();
-        assert (count > 0);
-        JoinTable lastJoinTable = joinTables.get(count - 1);
-        if (lastJoinTable.getSubquery() != null) {
-            throw new SQLFeatureNotSupportedException("Subqueries not supported.");
-        }
-        List<TableNode> from = new ArrayList<TableNode>(1);
-        from.add(lastJoinTable.getTableNode());
-        
-        return NODE_FACTORY.select(from, statement.getHint(), statement.isDistinct(), statement.getSelect(), lastJoinTable.getPreFiltersCombined(), statement.getGroupBy(), statement.getHaving(), statement.getOrderBy(), statement.getLimit(), statement.getBindCount(), statement.isAggregate());
-    }
-    
-    // Get subquery with fixed select and where nodes
-    public static SelectStatement getSubQueryWithoutLastJoin(SelectStatement statement, JoinSpec join) {
-        List<TableNode> from = statement.getFrom();
-        assert(from.size() > 1);
-        List<JoinTable> joinTables = join.getJoinTables();
-        int count = joinTables.size();
-        assert (count > 0);
-        List<AliasedNode> select = new ArrayList<AliasedNode>();
-        select.addAll(join.getSelect());
-        for (int i = 0; i < count - 1; i++) {
-            select.addAll(joinTables.get(i).getSelect());
-        }
-        
-        return NODE_FACTORY.select(from.subList(0, from.size() - 1), statement.getHint(), false, select, join.getPreFiltersCombined(), null, null, null, null, statement.getBindCount(), false);
-    }
-    
-    public static PTableWrapper mergeProjectedTables(PTableWrapper lWrapper, PTableWrapper rWrapper, boolean innerJoin) throws SQLException {
-    	PTable left = lWrapper.getTable();
-    	PTable right = rWrapper.getTable();
-    	List<PColumn> merged = new ArrayList<PColumn>();
-    	merged.addAll(left.getColumns());
-    	int position = merged.size();
-    	for (PColumn c : right.getColumns()) {
-    		if (!SchemaUtil.isPKColumn(c)) {
-    			PColumnImpl column = new PColumnImpl(c.getName(), 
-    					PNameFactory.newName(ScanProjector.VALUE_COLUMN_FAMILY), c.getDataType(), 
-    					c.getMaxLength(), c.getScale(), innerJoin ? c.isNullable() : true, position++, 
-    					c.getSortOrder(), c.getArraySize(), c.getViewConstant(), c.isViewReferenced());
-    			merged.add(column);
-    		}
-    	}
-        if (left.getBucketNum() != null) {
-            merged.remove(0);
-        }
-        PTable t = PTableImpl.makePTable(left.getTenantId(), left.getSchemaName(),
-                PNameFactory.newName(SchemaUtil.getTableName(left.getName().getString(), right.getName().getString())), left.getType(), left.getIndexState(), left.getTimeStamp(), left.getSequenceNumber(), left.getPKName(), left.getBucketNum(), merged,
-                left.getParentTableName(), left.getIndexes(), left.isImmutableRows(), Collections.<PName>emptyList(), null, null, PTable.DEFAULT_DISABLE_WAL, left.isMultiTenant(), left.getViewType(), left.getViewIndexId());
-
-        ListMultimap<String, String> mergedMap = ArrayListMultimap.<String, String>create();
-        mergedMap.putAll(lWrapper.getColumnNameMap());
-        mergedMap.putAll(rWrapper.getColumnNameMap());
-        
-        return new PTableWrapper(t, mergedMap);
-    }
-    
     public static ScanProjector getScanProjector(ProjectedPTableWrapper table) {
     	return new ScanProjector(table);
     }
     
-    public static class PTableWrapper {
+    public class PTableWrapper {
     	protected PTable table;
     	protected ListMultimap<String, String> columnNameMap;
     	
@@ -1205,9 +1154,42 @@ public class JoinCompiler {
     	public List<String> getMappedColumnName(String name) {
     		return columnNameMap.get(name);
     	}
+        
+        public ColumnResolver createColumnResolver() {
+            return new JoinedTableColumnResolver(this, origResolver);
+        }
+        
+        public PTableWrapper mergeProjectedTables(PTableWrapper rWrapper, boolean innerJoin) throws SQLException {
+            PTable left = this.getTable();
+            PTable right = rWrapper.getTable();
+            List<PColumn> merged = new ArrayList<PColumn>();
+            merged.addAll(left.getColumns());
+            int position = merged.size();
+            for (PColumn c : right.getColumns()) {
+                if (!SchemaUtil.isPKColumn(c)) {
+                    PColumnImpl column = new PColumnImpl(c.getName(), 
+                            PNameFactory.newName(ScanProjector.VALUE_COLUMN_FAMILY), c.getDataType(), 
+                            c.getMaxLength(), c.getScale(), innerJoin ? c.isNullable() : true, position++, 
+                            c.getSortOrder(), c.getArraySize(), c.getViewConstant(), c.isViewReferenced());
+                    merged.add(column);
+                }
+            }
+            if (left.getBucketNum() != null) {
+                merged.remove(0);
+            }
+            PTable t = PTableImpl.makePTable(left.getTenantId(), left.getSchemaName(),
+                    PNameFactory.newName(SchemaUtil.getTableName(left.getName().getString(), right.getName().getString())), left.getType(), left.getIndexState(), left.getTimeStamp(), left.getSequenceNumber(), left.getPKName(), left.getBucketNum(), merged,
+                    left.getParentTableName(), left.getIndexes(), left.isImmutableRows(), Collections.<PName>emptyList(), null, null, PTable.DEFAULT_DISABLE_WAL, left.isMultiTenant(), left.getViewType(), left.getViewIndexId());
+
+            ListMultimap<String, String> mergedMap = ArrayListMultimap.<String, String>create();
+            mergedMap.putAll(this.getColumnNameMap());
+            mergedMap.putAll(rWrapper.getColumnNameMap());
+            
+            return new PTableWrapper(t, mergedMap);
+        }
     }
     
-    public static class ProjectedPTableWrapper extends PTableWrapper {
+    public class ProjectedPTableWrapper extends PTableWrapper {
     	private List<Expression> sourceExpressions;
     	
     	protected ProjectedPTableWrapper(PTable table, ListMultimap<String, String> columnNameMap, List<Expression> sourceExpressions) {
@@ -1274,39 +1256,6 @@ public class JoinCompiler {
     	return SchemaUtil.getColumnName(SchemaUtil.getTableName(schemaName, tableName), colName);
     }
 
-    private static class DynamicColumnsVisitor implements TableNodeVisitor {
-        private List<ColumnDef> dynamicCols;
-        
-        public DynamicColumnsVisitor() {
-        }
-        
-        public List<ColumnDef> getDynamicColumns() {
-            return dynamicCols == null ? Collections.<ColumnDef> emptyList() : dynamicCols;
-        }
-
-        @Override
-        public void visit(BindTableNode boundTableNode) throws SQLException {
-        }
-
-        @Override
-        public void visit(JoinTableNode joinNode) throws SQLException {
-            // should not expect to see this node.
-            assert(false);
-        }
-
-        @Override
-        public void visit(NamedTableNode namedTableNode)
-                throws SQLException {
-            this.dynamicCols = namedTableNode.getDynamicColumns();
-        }
-
-        @Override
-        public void visit(DerivedTableNode subselectNode)
-                throws SQLException {
-            throw new SQLFeatureNotSupportedException();
-        }
-    };
-
 }