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);
   }
 }
  /**
   * 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>The schema of the result follows the SQL standard:
   *
   * <ul>
   *   <li>Common columns first, in the order specified, if any.
   *   <li>Columns that appear only in the left table, if any.
   *   <li>Columns that appear only in the right table, if any.
   * </ul>
   *
   * <p>Of course, any of these sets of columns could be empty.
   *
   * @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
   * @param result this output-parameter is updated to hold the schema of the result, as produced by
   *     the join operation.
   */
  private void buildJoinSchema(
      String context,
      Schema leftSchema,
      Schema rightSchema,
      Set<String> commonCols,
      Schema result) {

    preparedJoinExpr = null;
    preparedSelectValues = null;
    if (!commonCols.isEmpty()) {
      preparedSelectValues = new ArrayList<SelectValue>();

      // 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);

        result.addColumnInfo(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);
            preparedSelectValues.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);
            preparedSelectValues.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",
                    new ColumnValue(lhsColInfo.getColumnName()),
                    new ColumnValue(rhsColInfo.getColumnName()));
            selVal = new SelectValue(coalesce, name);
            preparedSelectValues.add(selVal);
        }
      }

      preparedJoinExpr = andOp;
    }

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

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

        SelectValue selVal = new SelectValue(new ColumnValue(colInfo.getColumnName()), null);
        preparedSelectValues.add(selVal);
      }
    }
  }
  /**
   * 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>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
   * </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><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.
   *
   * @return the schema of the from-clause's output
   * @throws IOException if a table's schema cannot be loaded, or some other IO issue occurs.
   */
  public Schema prepare() throws IOException {

    Schema result = null;

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

        TableFileInfo tblFileInfo = StorageManager.getInstance().openTable(tableName);
        result = tblFileInfo.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.");

        result = derivedTable.computeSchema();

        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 = new Schema();

        Schema leftSchema = leftChild.prepare();
        Schema rightSchema = rightChild.prepare();

        // 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!");
          }

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

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

          preparedJoinExpr = 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.

    preparedSchema = result;
    return result;
  }