/* This file is part of VoltDB.
* Copyright (C) 2008-2017 VoltDB Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with VoltDB. If not, see <http://www.gnu.org/licenses/>.
*/
package org.voltdb.plannodes;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import org.voltdb.expressions.AbstractExpression;
import org.voltdb.expressions.TupleValueExpression;
/**
* This class encapsulates the representation and common operations for
* a PlanNode's output schema.
*/
public class NodeSchema {
// Sometimes there are columns with identical names within a given table
// and its schema. We want to be able to differentiate these columns so that
// m_columnsMapHelper can produce the right offset for columns that have the same
// name but are physically different. We use the "differentiator" (an attribute
// of a TVE) to do this.
private static final Comparator<SchemaColumn> BY_NAME =
new Comparator<SchemaColumn>() {
@Override
public int compare(SchemaColumn col1, SchemaColumn col2) {
int nameCompare = col1.compareNames(col2);
if (nameCompare != 0) {
return nameCompare;
}
return col1.getDifferentiator() - col2.getDifferentiator();
}
};
// The list of columns produced by a plan node, in storage order.
private final ArrayList<SchemaColumn> m_columns;
// A helpful map that goes from a schema column to the columns index in the list.
private final TreeMap<SchemaColumn, Integer> m_columnsMapHelper;
public NodeSchema() {
m_columns = new ArrayList<SchemaColumn>();
m_columnsMapHelper = new TreeMap<>(BY_NAME);
}
/**
* Add a column to this schema.
*
* Unless actively modified, updated, or sorted, the column order is
* implicitly the order in which columns are added using this call.
*
* Note that it's possible to add the same column to a schema more than once.
* In this case we replace the old entry for the column in the map (so it will
* stay the same size), the column list will grow by one, and the updated map entry
* will point the the second instance of the column in the list.
*/
public void addColumn(SchemaColumn column) {
int size = m_columns.size();
m_columnsMapHelper.put(column, size);
m_columns.add(column);
}
public void addColumn(String tableName, String tableAlias,
String columnName, String columnAlias,
AbstractExpression expression) {
SchemaColumn scol = new SchemaColumn(
tableName, tableAlias,
columnName, columnAlias,
expression);
addColumn(scol);
}
public void addColumn(String tableName, String tableAlias,
String columnName, String columnAlias,
AbstractExpression expression,
int differentiator) {
SchemaColumn scol = new SchemaColumn(
tableName, tableAlias,
columnName, columnAlias,
expression, differentiator);
addColumn(scol);
}
/**
* @return a list of the columns in this schema. These columns will be
* in the order in which they will appear at the output of this node.
*/
public ArrayList<SchemaColumn> getColumns() { return m_columns; }
public int size() { return m_columns.size(); }
/**
* Retrieve the SchemaColumn that matches the provided arguments.
* @param tableName
* @param tableAlias
* @param columnName
* @param columnAlias
* @return The matching SchemaColumn. Returns null if the column wasn't
* found.
*/
public SchemaColumn find(String tableName, String tableAlias,
String columnName, String columnAlias) {
SchemaColumn col = new SchemaColumn(tableName, tableAlias,
columnName, columnAlias);
int index = findIndexOfColumn(col);
if (index != -1) {
return m_columns.get(index);
}
return null;
}
/**
* A subclass of SchemaColumn that always returns -1 for the differentiator,
* meaning that it will sort lowest among schema columns with the same names.
*/
private static class SchemaColumnFloor extends SchemaColumn {
SchemaColumnFloor(SchemaColumn column) {
super(column.getTableName(), column.getTableAlias(),
column.getColumnName(), column.getColumnAlias(),
column.getExpression());
}
@Override
public int getDifferentiator() { return -1; }
}
private int findIndexOfColumn(SchemaColumn column) {
SchemaColumn floorSchemaColumn = new SchemaColumnFloor(column);
SortedMap<SchemaColumn, Integer> submap =
m_columnsMapHelper.tailMap(floorSchemaColumn);
int index = -1;
// If more than one column in this NodeSchema has the same name of the column
// we're looking for, then we "break the tie" and prefer the one with a matching
// differentiator field.
for (Map.Entry<SchemaColumn, Integer> entry : submap.entrySet()) {
SchemaColumn key = entry.getKey();
if (key.compareNames(column) != 0) {
break;
}
index = entry.getValue();
if (column.getDifferentiator() == key.getDifferentiator()) {
// An exact match
break;
}
}
return index;
}
/** Convenience method for looking up the column offset for a TVE using
* getIndexOf(). This is a common operation because every TVE in every
* AbstractExpression in a plan node needs to have its column_idx updated
* during the column index resolution phase.
*/
public int getIndexOfTve(TupleValueExpression tve) {
SchemaColumn column = new SchemaColumn(
tve.getTableName(), tve.getTableAlias(),
tve.getColumnName(), tve.getColumnAlias(),
tve, tve.getDifferentiator());
return findIndexOfColumn(column);
}
private static final Comparator<SchemaColumn> TVE_IDX_COMPARE =
new Comparator<SchemaColumn>() {
@Override
public int compare(SchemaColumn col1, SchemaColumn col2) {
TupleValueExpression tve1 =
(TupleValueExpression) col1.getExpression();
TupleValueExpression tve2 =
(TupleValueExpression) col2.getExpression();
int colIndex1 = tve1.getColumnIndex();
int colIndex2 = tve2.getColumnIndex();
return (colIndex1 < colIndex2) ? -1 :
(colIndex1 > colIndex2) ? 1 : 0;
}
};
/**
* Sort schema columns by TVE index. All elements
* must be TupleValueExpressions. Modification is made in-place.
*/
void sortByTveIndex() { Collections.sort(m_columns, TVE_IDX_COMPARE); }
/**
* Sort a sub-range of the schema columns by TVE index. All elements
* must be TupleValueExpressions. Modification is made in-place.
* @param fromIndex lower bound of range to be sorted, inclusive
* @param toIndex upper bound of range to be sorted, exclusive
*/
void sortByTveIndex(int fromIndex, int toIndex) {
Collections.sort(m_columns.subList(fromIndex, toIndex), TVE_IDX_COMPARE);
}
@Override
public NodeSchema clone() {
NodeSchema copy = new NodeSchema();
for (SchemaColumn column : m_columns) {
copy.addColumn(column.clone());
}
return copy;
}
public NodeSchema replaceTableClone(String tableAlias) {
NodeSchema copy = new NodeSchema();
for (int colIndex = 0; colIndex < m_columns.size(); ++colIndex) {
SchemaColumn column = m_columns.get(colIndex);
String colAlias = column.getColumnAlias();
int differentiator = column.getDifferentiator();
TupleValueExpression tve = new TupleValueExpression(
tableAlias, tableAlias, colAlias, colAlias,
colIndex, differentiator);
tve.setTypeSizeAndInBytes(column);
copy.addColumn(tableAlias, tableAlias,
colAlias, colAlias,
tve, differentiator);
}
return copy;
}
@Override
public boolean equals (Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (obj instanceof NodeSchema == false) {
return false;
}
NodeSchema schema = (NodeSchema) obj;
if (schema.size() != size()) {
return false;
}
ArrayList<SchemaColumn> columns = schema.getColumns();
for (int colIndex = 0; colIndex < size(); colIndex++ ) {
SchemaColumn col1 = columns.get(colIndex);
if ( ! col1.equals(m_columns.get(colIndex))) {
return false;
}
}
return true;
}
// Similar to the equals method above, but consider SchemaColumn objects as equal if their
// names are the same. Don't worry about the differentiator field.
public boolean equalsOnlyNames(NodeSchema otherSchema) {
if (otherSchema == null) {
return false;
}
if (otherSchema.size() != size()) {
return false;
}
ArrayList<SchemaColumn> columns = otherSchema.getColumns();
for (int colIndex = 0; colIndex < size(); colIndex++ ) {
SchemaColumn col1 = columns.get(colIndex);
SchemaColumn col2 = m_columns.get(colIndex);
if (col1.compareNames(col2) != 0) {
return false;
}
}
return true;
}
@Override
public int hashCode() {
int result = 0;
for (SchemaColumn column : m_columns) {
result += column.hashCode();
}
return result;
}
/**
* Returns a copy of this NodeSchema but with all non-TVE expressions
* replaced with an appropriate TVE. This is used primarily when generating
* a node's output schema based on its childrens' schema; we want to
* carry the columns across but leave any non-TVE expressions behind.
*/
NodeSchema copyAndReplaceWithTVE() {
NodeSchema copy = new NodeSchema();
int colIndex = 0;
for (SchemaColumn column : m_columns) {
copy.addColumn(column.copyAndReplaceWithTVE(colIndex));
++colIndex;
}
return copy;
}
/**
* Append the provided schema to this schema and return the result
* as a new schema. Columns order: [this][provided schema columns].
*/
NodeSchema join(NodeSchema schema) {
NodeSchema copy = this.clone();
for (SchemaColumn column: schema.getColumns()) {
copy.addColumn(column.clone());
}
return copy;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("NodeSchema:\n");
for (int colIndex = 0; colIndex < m_columns.size(); ++colIndex) {
sb.append("Column " + colIndex + ":\n");
sb.append(m_columns.get(colIndex).toString()).append("\n");
}
return sb.toString();
}
public String toExplainPlanString() {
StringBuilder sb = new StringBuilder();
String separator = "schema: {";
for (SchemaColumn column : m_columns) {
String colAsString = column.toString();
sb.append(separator).append(colAsString);
separator = ", ";
}
sb.append("}");
return sb.toString();
}
public void addAllSubexpressionsOfClassFromNodeSchema(
Set<AbstractExpression> exprs,
Class<? extends AbstractExpression> aeClass) {
for (SchemaColumn column : getColumns()) {
AbstractExpression colExpr = column.getExpression();
if (colExpr == null) {
continue;
}
Collection<AbstractExpression> found =
colExpr.findAllSubexpressionsOfClass(aeClass);
exprs.addAll(found);
}
}
}