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; }