package edu.caltech.nanodb.queryast;


import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

import edu.caltech.nanodb.commands.InsertCommand;
import edu.caltech.nanodb.commands.SelectCommand;
import edu.caltech.nanodb.expressions.BooleanOperator;
import edu.caltech.nanodb.expressions.ColumnValue;
import edu.caltech.nanodb.expressions.CompareOperator;
import edu.caltech.nanodb.expressions.Expression;
import edu.caltech.nanodb.expressions.FunctionCall;
import edu.caltech.nanodb.functions.TableFunction;
import edu.caltech.nanodb.relations.ColumnInfo;
import edu.caltech.nanodb.relations.JoinType;
import edu.caltech.nanodb.relations.Schema;
import edu.caltech.nanodb.relations.SchemaNameException;
import edu.caltech.nanodb.relations.TableInfo;
import edu.caltech.nanodb.storage.TableManager;


/**
 * This class represents a hierarchy of one or more base and derived relations
 * that produce the rows considered by <tt>SELECT</tt> clauses.
 * <p>
 * The "join condition type" specified in the {@link #condType} field
 * indicates the way that the join condition was specified in the original SQL
 * expression.  This value may be {@code null} if the join specified no
 * condition, which would occur with these SQL expressions:
 * <ul>
 *   <li>SELECT * FROM t1, t2 WHERE t1.a = t2.a;</li>
 *   <li>SELECT * FROM t1 JOIN t2 WHERE t1.a = t2.a;</li>
 * </ul>
 * <p>
 * If <tt>condType</tt> is set to <tt>JoinConditionType.JOIN_ON_EXPR</tt> then
 * the join condition was specified in an <tt>ON</tt> clause, such as:
 * <ul>
 *   <li>SELECT * FROM t1 JOIN t2 ON t1.a = t2.a;</li>
 * </ul>
 * The <tt>ON</tt> predicate is available via the {@link #getOnExpression}
 * method.
 * <p>
 * Finally, if the <tt>condType</tt> is either
 * <tt>JoinConditionType.JOIN_USING</tt> or
 * <tt>JoinConditionType.NATURAL_JOIN</tt> then the join condition was
 * implicitly specified via a SQL <tt>USING</tt> clause, or via a natural join,
 * such as:
 * <ul>
 *   <li>SELECT * FROM t1 JOIN t2 USING (a);</li>
 *   <li>SELECT * FROM t1 NATURAL JOIN t2;</li>
 * </ul>
 * In these cases the join predicate must be generated from examining the
 * schemas of the participating tables.  The generated predicate is only
 * available <em>after</em> the {@link #computeSchema} method has been called (which
 * is indirectly invoked by the {@link SelectCommand} and {@link InsertCommand}
 * classes before planning and executing a query), and is available via the
 * {@link #getComputedJoinExpr()} method.
 *
 * @see SelectClause
 */
public class FromClause {

    /**
     * This enumeration specifies the overall type of the <tt>FROM</tt>
     * clause.  Since different kinds of FROM clauses have different
     * characteristics, we drive some of the logic in this class with this
     * enumeration.
     */
    public enum ClauseType {
        /** This clause is a simple table (a "base relation"). */
        BASE_TABLE,

        /**
         * This clause is a derived relation specified as a named
         * <tt>SELECT</tt> subquery.
         */
        SELECT_SUBQUERY,

        /**
         * This clause is a derived relation specified as a join of two
         * relations.
         */
        JOIN_EXPR,

        /** This clause is a table-returning function. */
        TABLE_FUNCTION
    }


    /**
     * For FROM clauses that contain join expressions, this enumeration
     * specifies the kind of join-condition for each join expression.
     */
    public enum JoinConditionType {
        /**
         * Perform a natural join, which implicitly specifies that values in all
         * shared columns must be equal.
         */
        NATURAL_JOIN,

        /**
         * The join clause specifies a USING clause, which explicitly lists the
         * shared columns whose values must be equal.
         */
        JOIN_USING,

        /**
         * The join clause specifies an ON clause with an expression that must
         * evaluate to true.
         */
        JOIN_ON_EXPR
    }


    /** A logging object for reporting anything interesting that happens. */
    private static Logger logger = LogManager.getLogger(FromClause.class);


    /**
     * The kind of FROM clause this object represents, either a base table, a
     * derived table (a nested SELECT), or a join expression.
     */
    private ClauseType clauseType;


    /**
     * If the <tt>FROM</tt> clause specifies a base table then this field
     * holds the table or function's name.  This field will only be set to a
     * non-{@code null} value if {@link #clauseType} is set to
     * {@link ClauseType#BASE_TABLE}.
     */
    private String tableName;


    /**
     * If the <tt>FROM</tt> clause is a table-returning function then this
     * field holds the name of the table-function and its arguments.  This
     * field will only be set to a non-{@code null} value if
     * {@link #clauseType} is set to {@link ClauseType#TABLE_FUNCTION}.
     */
    private FunctionCall tableFunction = null;


    /**
     * If the <tt>FROM</tt> clause specifies a nested <tt>SELECT</tt> command
     * with an alias, this field holds the nested query.  This field will
     * only be set to a non-<code>null</code> value if {@link #clauseType}
     * is set to {@link ClauseType#SELECT_SUBQUERY}.
     */
    private SelectClause derivedTable = null;


    /**
     * If this <tt>FROM</tt> clause is a base table or a derived table, then
     * this field stores the alias for the table.  The alias is optional for
     * base tables, but it is required for derived tables.
     */
    private String aliasName = null;


    /**
     * If this <tt>FROM</tt> clause is a join expression then this references
     * the left child of the join.  This field will only be set to a
     * non-<code>null</code> value if {@link #clauseType}
     * is set to {@link ClauseType#JOIN_EXPR}.
     */
    private FromClause leftChild;


    /**
     * If this <tt>FROM</tt> clause is a join expression then this references
     * the right child of the join.  This field will only be set to a
     * non-<code>null</code> value if {@link #clauseType}
     * is set to {@link ClauseType#JOIN_EXPR}.
     */
    private FromClause rightChild;


    /**
     * If this object represents a join expression, this field specifies
     * the type of join to use, whether it is an inner join, some kind of
     * outer join, or a cross join.
     */
    private JoinType joinType;


    /**
     * If this object represents a join expression, this field specifies the
     * type of condition used in the join; either a natural join, a join with
     * an <tt>ON</tt> expression, or a join with a <tt>USING</tt> clause.
     */
    private JoinConditionType condType;


    private List<String> joinUsingNames = new ArrayList<>();


    /** The expression used for joining tables in this <tt>FROM</tt> clause. */
    private Expression joinOnExpr = null;


    /**
     * This value is filled in with the schema of the data that this
     * <tt>FROM</tt> clause produces, when the {@link #computeSchema} method
     * is called.  This is done when a SQL statement is being prepared for
     * execution, and is typically kicked off by the
     * {@link SelectClause#computeSchema} method, which in turn prepares the
     * from-clause objects.
     */
    private Schema computedSchema = null;


    /**
     * For <tt>FROM</tt> clauses that either directly or indirectly specify a
     * join condition, this value is filled in with the join condition to apply,
     * when the {@link #computeSchema} method is called.  This is done when a SQL
     * statement is being prepared for execution, and is typically kicked off by
     * the {@link SelectClause#computeSchema} method, which in turn prepares the
     * from-clause objects.
     */
    private Expression computedJoinExpr = null;


    /**
     * In cases of <tt>NATURAL</tt> joins or joins with a <tt>USING</tt> clause,
     * this value is filled in with {@link SelectValue} objects that can be used
     * to perform a suitable project to eliminate duplicate columns.  If no
     * duplicate columns are to be eliminated, this value will be <tt>null</tt>.
     * This value is populated when the {@link #computeSchema} method is called, and
     * is typically kicked off by the {@link SelectClause#computeSchema} method,
     * which in turn prepares the from-clause objects.
     */
    private ArrayList<SelectValue> computedSelectValues = null;


    /**
     * Construct a new <tt>FROM</tt> clause of type {@link ClauseType#BASE_TABLE}.
     * The alias name may be <tt>null</tt> in the case of base-tables.
     *
     * @param tableName the name of the table being selected from
     * @param aliasName an optional alias used to refer to the table in the rest
     *        of the query
     */
    public FromClause(String tableName, String aliasName) {
        if (tableName == null)
            throw new IllegalArgumentException("tableName cannot be null");

        clauseType = ClauseType.BASE_TABLE;

        this.tableName = tableName;
        this.aliasName = aliasName;
    }


    public FromClause(FunctionCall tableFunction, String aliasName) {
        if (tableFunction == null)
            throw new IllegalArgumentException("functionCall cannot be null");

        if (!(tableFunction.getFunction() instanceof TableFunction)) {
            throw new IllegalArgumentException(
                "Function must be a subclass of TableFunction");
        }

        clauseType = ClauseType.TABLE_FUNCTION;

        this.tableFunction = tableFunction;
        this.aliasName = aliasName;
    }


    /**
     * Construct a new <tt>FROM</tt> clause of type
     * {@link FromClause.ClauseType#SELECT_SUBQUERY}.  The alias name must be
     * specified for derived tables.
     *
     * @param derivedTable the subquery that is referenced by this from-clause
     * @param aliasName the relation-name given for this subquery
     *
     * @throws NullPointerException if either <tt>derivedTable</tt> or
     *         <tt>aliasName</tt> is <tt>null</tt>
     */
    public FromClause(SelectClause derivedTable, String aliasName) {
        if (derivedTable == null)
            throw new NullPointerException("derivedTable cannot be null");

        if (aliasName == null) {
            throw new NullPointerException(
                "aliasName cannot be null for a derived table");
        }

        clauseType = ClauseType.SELECT_SUBQUERY;

        this.derivedTable = derivedTable;
        this.aliasName = aliasName;
    }


    /**
     * Construct a new <tt>FROM</tt> clause of type
     * {@link FromClause.ClauseType#JOIN_EXPR}.  The two sub-relations being
     * joined are specified as <tt>FromClause</tt> instances.
     *
     * @param left the left relation being joined
     * @param right the right relation being joined
     * @param type the kind of join being performed
     *
     * @throws NullPointerException if either sub-relation is <tt>null</tt>, or
     *         if the join-type is <tt>null</tt>.
     */
    public FromClause(FromClause left, FromClause right, JoinType type) {
        if (left == null)
            throw new NullPointerException("left cannot be null");

        if (right == null)
            throw new NullPointerException("right cannot be null");

        if (type == null)
            throw new NullPointerException("type cannot be null");

        clauseType = ClauseType.JOIN_EXPR;

        leftChild = left;
        rightChild = right;
        joinType = type;
    }



    /**
     * Returns the kind of <tt>FROM</tt> clause this object represents.
     *
     * @return the clause-type.  This value will never be <tt>null</tt>.
     */
    public ClauseType getClauseType() {
        return clauseType;
    }


    /**
     * Returns true if this from clause specifies a base table, as opposed
     * to a derived table or a join expression.
     *
     * @return <tt>true</tt> if this clause specifies a base table.
     */
    public boolean isBaseTable() {
        return (clauseType == ClauseType.BASE_TABLE);
    }


    /**
     * Returns true if this from clause specifies a derived table, as
     * opposed to a base table or a join expression.
     *
     * @return {@code true} if this clause is a derived table specified as a
     *         <tt>SELECT</tt> statement.
     */
    public boolean isDerivedTable() {
        return (clauseType == ClauseType.SELECT_SUBQUERY);
    }


    /**
     * Returns true if this from clause specifies a join expression, as opposed
     * to a base table or a join expression.
     *
     * @return {@code true} if this clause is a join expression.
     */
    public boolean isJoinExpr() {
        return (clauseType == ClauseType.JOIN_EXPR);
    }


    /**
     * Returns the table name. This value will be null if this is a derived table
     * or a join expression.
     *
     * @return the name of the table.
     *
     * @throws IllegalStateException if the from-clause's type is not a
     *         {@link ClauseType#BASE_TABLE}.
     */
    public String getTableName() {
        if (clauseType != ClauseType.BASE_TABLE) {
            throw new IllegalStateException("From-clause is a " + clauseType +
                " clause, not a BASE_TABLE.");
        }

        return tableName;
    }


    /**
     * Returns the select clause for a derived table. This value will be
     * null if this is not derived.
     *
     * @return the derived select clause
     *
     * @throws IllegalStateException if the from-clause's type is not a
     *         {@link ClauseType#SELECT_SUBQUERY}.
     */
    public SelectClause getSelectClause() {
        if (clauseType != ClauseType.SELECT_SUBQUERY) {
            throw new IllegalStateException("From-clause is a " + clauseType +
                " clause, not a SELECT_SUBQUERY.");
        }

        return derivedTable;
    }


    /**
     * If the from-clause is a join expression, this method returns the left
     * child-clause.
     *
     * @return the left child-clause of this join clause
     *
     * @throws IllegalStateException if this from-clause is not a join
     *         expression.
     */
    public FromClause getLeftChild() {
        if (clauseType != ClauseType.JOIN_EXPR) {
            throw new IllegalStateException("From-clause is a " + clauseType +
                " clause, not a JOIN_EXPR.");
        }

        return leftChild;
    }


    /**
     * If the from-clause is a join expression, this method returns the right
     * child-clause.
     *
     * @return the right child-clause of this join clause
     *
     * @throws IllegalStateException if this from-clause is not a join
     *         expression.
     */
    public FromClause getRightChild() {
        if (clauseType != ClauseType.JOIN_EXPR) {
            throw new IllegalStateException("From-clause is a " + clauseType +
                " clause, not a JOIN_EXPR.");
        }

        return rightChild;
    }


    /**
     * Returns the name of the relation that the results should go into. In the
     * case where an alias exists, this returns the alias. If an alias does not
     * exist, this must return the table name for the base table.
     *
     * @return the result relation name
     *
     * @throws IllegalStateException if the clause-type is a join expression,
     *         since that kind of from-clause doesn't have a result name.
     */
    public String getResultName() {
        // Join expressions aren't renamed; only tables and SELECT subqueries
        // can be given a name (in fact, SELECT subqueries require one).
        if (clauseType == ClauseType.JOIN_EXPR) {
            throw new IllegalStateException(clauseType +
                " clauses don't have a result name");
        }

        if (aliasName != null)
            return aliasName;

        return tableName;
    }


    public boolean isRenamed() {
        // Join expressions aren't renamed; only tables and SELECT subqueries
        // can be given a name (in fact, SELECT subqueries require one).
        if (clauseType == ClauseType.JOIN_EXPR)
            return false;

        return (aliasName != null);
    }


    /**
     * Returns the join type of a join clause.
     *
     * @return the join type
     *
     * @throws IllegalStateException if the clause-type is not a join clause.
     *         Only join clauses have a join type.
     */
    public JoinType getJoinType() {
        if (clauseType != ClauseType.JOIN_EXPR) {
            throw new IllegalStateException(clauseType +
                " clauses don't have a join type");
        }

        return joinType;
    }


    /**
     * Returns true if this join clause is an outer join, false otherwise.
     *
     * @return true if this join clause is an outer join, false otherwise.
     *
     * @throws IllegalStateException if the clause-type is not a join clause.
     *         Only join clauses have a join type.
     */
    public boolean isOuterJoin() {
        if (clauseType != ClauseType.JOIN_EXPR) {
            throw new IllegalStateException(
                "This from-clause is not a join expression.");
        }

        return (joinType == JoinType.LEFT_OUTER ||
                joinType == JoinType.RIGHT_OUTER ||
                joinType == JoinType.FULL_OUTER);
    }


    public boolean hasOuterJoinOnLeft() {
        if (clauseType != ClauseType.JOIN_EXPR) {
            throw new IllegalStateException(
                "This from-clause is not a join expression.");
        }

        return joinType == JoinType.LEFT_OUTER ||
               joinType == JoinType.FULL_OUTER;
    }


    public boolean hasOuterJoinOnRight() {
        if (clauseType != ClauseType.JOIN_EXPR) {
            throw new IllegalStateException(
                "This from-clause is not a join expression.");
        }

        return joinType == JoinType.RIGHT_OUTER ||
               joinType == JoinType.FULL_OUTER;
    }



    /**
     * Returns the join condition type.  This value will be null if there is
     * only 1 table in the outer array of from clauses.
     *
     * @return the join condition type
     *
     * @throws IllegalStateException if the clause-type is not a join clause.
     *         Only join clauses have a join-condition type.
     */
    public JoinConditionType getConditionType() {
        if (clauseType != ClauseType.JOIN_EXPR) {
            throw new IllegalStateException(clauseType +
                " clauses don't have a join-condition type");
        }

        return condType;
    }


    /**
     * Sets the join condition type.
     *
     * @param type the join condition type
     *
     * @throws IllegalStateException if the clause-type is not a join clause.
     *         Only join clauses have a join-condition type.
     */
    public void setConditionType(JoinConditionType type) {
        if (clauseType != ClauseType.JOIN_EXPR) {
            throw new IllegalStateException(clauseType +
                " clauses don't have a join-condition type");
        }

        condType = type;
    }


    /**
     * For join conditions of the form <tt>USING (col1, col2, ...)</tt>, this
     * method returns the list of column names that were specified.  If this
     * join expression does not use a <tt>USING</tt> expression then this method
     * returns <tt>null</tt>.
     *
     * @return the list of column names specified in the <tt>USING</tt> clause
     *
     * @throws IllegalStateException if the clause-type is not a join clause.
     *         Only join clauses have a join condition.
     */
    public List<String> getUsingNames() {
        if (clauseType != ClauseType.JOIN_EXPR) {
            throw new IllegalStateException(clauseType +
                " clauses don't have a join condition");
        }

        return joinUsingNames;
    }


    /**
     * For join conditions of the form <tt>USING (col1, col2, ...)</tt>, this
     * method allows the column names to be added to this from clause.  This
     * method is used by the parser to construct the from clause as it is being
     * parsed.
     *
     * @param name the name of the column to use in the join operation
     *
     * @throws IllegalStateException if the clause-type is not a join clause.
     *         Only join clauses have a join condition.
     */
    public void addUsingName(String name) {
        if (clauseType != ClauseType.JOIN_EXPR) {
            throw new IllegalStateException(clauseType +
                " clauses don't have a join condition");
        }

        if (name == null)
            throw new NullPointerException("name cannot be null");

        joinUsingNames.add(name);
    }


    /**
     * Returns the join predicate if this from clause specifies an <tt>ON</tt>
     * clause.  Returns <tt>null</tt> if there is no <tt>ON</tt> clause.
     *
     * @return the expression specified in the <tt>ON</tt> clause
     *
     * @throws IllegalStateException if the clause-type is not a join clause.
     *         Only join clauses have a join condition.
     */
    public Expression getOnExpression() {
        if (clauseType != ClauseType.JOIN_EXPR) {
            throw new IllegalStateException(clauseType +
                " clauses don't have a join condition");
        }

        return joinOnExpr;
    }


    /**
     * Sets the join predicate if this from clause specifies an <tt>ON</tt>
     * clause.  Returns <tt>null</tt> if there is no <tt>ON</tt> clause.
     *
     * @param expr the expression specified in the <tt>ON</tt> clause
     *
     * @throws IllegalStateException if the clause-type is not a join clause.
     *         Only join clauses have a join condition.
     */
    public void setOnExpression(Expression expr) {
        if (clauseType != ClauseType.JOIN_EXPR) {
            throw new IllegalStateException(clauseType +
                " clauses don't have a join condition");
        }

        joinOnExpr = expr;
        joinOnExpr = joinOnExpr.simplify();
    }


    /**
     * This function prepares the from-clause for use by the planner to
     * generate an actual execution plan from the clause.  This includes
     * several important tasks:
     * <ul>
     *   <li>The schema of the from-clause's output is determined.</li>
     *   <li>If the from-clause is a join, and the join specifies
     *       <tt>NATURAL</tt>, <tt>USING (col, ...)</tt>, or an <tt>ON</tt>
     *       clause, the join condition is determined</li>
     * </ul>
     * <p>
     * Since a from-clause can be a SQL subquery or a join expression, it
     * should be evident that the method will recursively prepare its children
     * as well.
     * </p>
     * <p>
     * <b>This method really shouldn't be called directly.</b>  Instead, the
     * enclosing {@link SelectClause} will calls this method when the clause's
     * {@link SelectClause#computeSchema} method is invoked.
     * </p>
     *
     * @return the schema of the from-clause's output
     */
    public Schema computeSchema(TableManager tableManager) {

        Schema result = null;

        switch (clauseType) {

        case BASE_TABLE:
            logger.debug("Preparing BASE_TABLE from-clause.");

            TableInfo tableInfo = tableManager.openTable(tableName);
            if (tableInfo == null) {
                throw new SchemaNameException(String.format(
                    "Table \"%s\" doesn't exist.", tableName));
            }

            result = tableInfo.getSchema();

            if (aliasName != null) {
                // Make a copy of the result schema and change the table names.
                result = new Schema(result);
                result.setTableName(aliasName);
            }

            break;

        case SELECT_SUBQUERY:
            logger.debug("Preparing SELECT_SUBQUERY from-clause.");

            // Don't pass in a parent SELECT, since we won't allow correlated
            // subqueries in the FROM clause.  (Donnie's still not entirely
            // sure this is possible in standard SQL.)
            result = derivedTable.computeSchema(tableManager, null);

            assert aliasName != null;

            // Make a copy of the result schema and change the table names.
            result = new Schema(result);
            result.setTableName(aliasName);

            break;

        case JOIN_EXPR:
            logger.debug("Preparing JOIN_EXPR from-clause.  Condition type = " +
                condType);

            result = null;

            Schema leftSchema = leftChild.computeSchema(tableManager);
            Schema rightSchema = rightChild.computeSchema(tableManager);

            // Depending on the join type, we might eliminate duplicate column
            // names.
            if (condType == JoinConditionType.NATURAL_JOIN) {
                // Make sure that each side of the join doesn't have any
                // duplicate column names.  (A schema can have columns with the
                // same name, but from different table names.  This is not
                // allowed for natural joins.)
                if (leftSchema.hasMultipleColumnsWithSameName()) {
                    throw new SchemaNameException("Natural join error:  " +
                        "left child table has multiple columns with same " +
                        "column name");
                }
                if (rightSchema.hasMultipleColumnsWithSameName()) {
                    throw new SchemaNameException("Natural join error:  " +
                        "right child table has multiple columns with same " +
                        "column name");
                }

                Set<String> commonCols = leftSchema.getCommonColumnNames(rightSchema);
                if (commonCols.isEmpty()) {
                    // TODO:  According to the SQL99 standard, this shouldn't
                    //        generate an error.
                    throw new SchemaNameException("Natural join error:  " +
                        "child tables share no common column names!");
                }

                result = buildJoinSchema("Natural join",
                    leftSchema, rightSchema, commonCols);
            }
            else if (condType == JoinConditionType.JOIN_USING) {
                LinkedHashSet<String> commonCols = new LinkedHashSet<>();
                for (String name : joinUsingNames) {
                    if (!commonCols.add(name)) {
                        throw new SchemaNameException("Column name " + name +
                            " was specified multiple times in USING clause");
                    }
                }

                result = buildJoinSchema("Join USING",
                    leftSchema, rightSchema, commonCols);
            }
            else {
                // This join condition-type doesn't alter the result schema.
                // Just lump together the result schemas.
                result = new Schema(leftSchema, rightSchema);
                computedJoinExpr = joinOnExpr;
            }
        }

        // Don't need to do any schema validation in this function, since all the
        // validation takes place in the SELECT clause schema-computation code.

        computedSchema = result;
        return result;
    }


    /**
     * <p>
     * This helper function handles a <tt>NATURAL</tt> join or a join with a
     * <tt>USING</tt> clause, determining what columns to use in the join,
     * computing the join predicate to use, and computing the schema of the
     * join result.
     * </p>
     * <p>
     * The schema of the result follows the SQL standard:
     * </p>
     * <ul>
     *     <li>Common columns first, in the order specified, if any.</li>
     *     <li>Columns that appear only in the left table, if any.</li>
     *     <li>Columns that appear only in the right table, if any.</li>
     * </ul>
     * <p>
     * Of course, any of these sets of columns could be empty.
     * </p>
     *
     * @param context a string used in logging messages and exceptions to
     *        indicate the context in which this method was called.
     *
     * @param leftSchema the schema of the table on the left side of the join
     *
     * @param rightSchema the schema of the table on the right side of the join
     *
     * @param commonCols the set of common columns to use
     *
     * @return the schema of the result, as produced by the join operation.
     */
    private Schema buildJoinSchema(String context, Schema leftSchema,
        Schema rightSchema, Set<String> commonCols) {

        computedJoinExpr = null;
        computedSelectValues = null;
        ArrayList<ColumnInfo> resultCols = new ArrayList<>();

        if (!commonCols.isEmpty()) {
            computedSelectValues = new ArrayList<>();

            // We will need to generate a join expression using the common
            // columns.  We will also need a project-spec that will project down
            // to only one copy of the common columns.

            BooleanOperator andOp = new BooleanOperator(BooleanOperator.Type.AND_EXPR);

            // Handle the shared columns.  We need to check that the
            // names aren't ambiguous on one or the other side.
            for (String name: commonCols) {
                checkJoinColumn(context, "left", leftSchema, name);
                checkJoinColumn(context, "right", leftSchema, name);

                // Seems OK, so add it to the result.
                ColumnInfo lhsColInfo = leftSchema.getColumnInfo(name);
                ColumnInfo rhsColInfo = rightSchema.getColumnInfo(name);

                resultCols.add(new ColumnInfo(lhsColInfo.getName(),
                                              lhsColInfo.getType()));

                // Add an equality test between the common columns to the join
                // condition.
                CompareOperator eq = new CompareOperator(CompareOperator.Type.EQUALS,
                    new ColumnValue(lhsColInfo.getColumnName()),
                    new ColumnValue(rhsColInfo.getColumnName()));

                andOp.addTerm(eq);

                // Add a select-value that projects the appropriate source
                // column down to the common column.
                SelectValue selVal;
                switch (joinType) {
                case INNER:
                case LEFT_OUTER:
                    // We can use the left column in the result, as it will
                    // always be non-NULL.
                    selVal = new SelectValue(
                        new ColumnValue(lhsColInfo.getColumnName()), name);
                    computedSelectValues.add(selVal);
                    break;

                case RIGHT_OUTER:
                    // We can use the right column in the result, as it will
                    // always be non-NULL.
                    selVal = new SelectValue(
                        new ColumnValue(rhsColInfo.getColumnName()), name);
                    computedSelectValues.add(selVal);
                    break;

                case FULL_OUTER:
                    // In this case, the LHS column-value could be null, or the
                    // RHS column-value could be null.  Thus, we need to produce
                    // a result of COALESCE(lhs.col, rhs.col) AS col.
                    Expression coalesce = new FunctionCall("COALESCE",
                        /* distinct */ false,
                        new ColumnValue(lhsColInfo.getColumnName()),
                        new ColumnValue(rhsColInfo.getColumnName()));
                    selVal = new SelectValue(coalesce, name);
                    computedSelectValues.add(selVal);
                }
            }

            computedJoinExpr = andOp;
        }

        // Handle the non-shared columns
        for (ColumnInfo colInfo : leftSchema) {
            if (!commonCols.contains(colInfo.getName())) {
                resultCols.add(colInfo);

                SelectValue selVal = new SelectValue(
                    new ColumnValue(colInfo.getColumnName()), null);
                computedSelectValues.add(selVal);
            }
        }
        for (ColumnInfo colInfo : rightSchema) {
            if (!commonCols.contains(colInfo.getName())) {
                resultCols.add(colInfo);

                SelectValue selVal = new SelectValue(
                    new ColumnValue(colInfo.getColumnName()), null);
                computedSelectValues.add(selVal);
            }
        }

        return new Schema(resultCols);
    }


    private void checkJoinColumn(String context, String side, Schema schema,
                                 String colName) {
        int count = schema.numColumnsWithName(colName);
        if (count == 0) {
            throw new SchemaNameException(context + " error:  column name \"" +
                colName + "\" doesn't appear on " + side);
        }
        else if (count > 1) {
            throw new SchemaNameException(context + " error:  column name \"" +
                colName + "\" is ambiguous on " + side);
        }
    }


    public Schema getSchema() {
        return computedSchema;
    }


    public Expression getComputedJoinExpr() {
        return computedJoinExpr;
    }


    public ArrayList<SelectValue> getComputedSelectValues() {
        return computedSelectValues;
    }


    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder();

        buf.append("JoinClause[type=").append(clauseType);
        switch (clauseType) {
        case BASE_TABLE:
            buf.append(", table=").append(tableName);
            break;

        case JOIN_EXPR:
            buf.append(", joinType=").append(joinType);
            buf.append(", condType=").append(condType);

            if (condType != null) {
                switch (condType) {
                case JOIN_ON_EXPR:
                    buf.append(", onExpr=").append(joinOnExpr);
                    break;

                case JOIN_USING:
                    buf.append(", usingNames=").append(joinUsingNames);
                    buf.append(", computedJoinExpr=").append(computedJoinExpr);
                    break;

                case NATURAL_JOIN:
                    buf.append(", computedJoinExpr=").append(computedJoinExpr);
                }
            }

            buf.append(", leftChild=").append(leftChild);
            buf.append(", rightChild=").append(rightChild);

            break;

        case SELECT_SUBQUERY:
            buf.append(", select=").append(derivedTable);
        }

        if (isRenamed())
            buf.append(", alias=").append(aliasName);

        buf.append(']');
        return buf.toString();
    }
}