/* This file is part of VoltDB.
* Copyright (C) 2008-2010 VoltDB L.L.C.
*
* VoltDB is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VoltDB 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VoltDB. If not, see <http://www.gnu.org/licenses/>.
*/
package org.voltdb.plannodes;
import java.util.*;
import java.util.Map.Entry;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONString;
import org.json.JSONStringer;
import org.json.JSONException;
import org.voltdb.catalog.Cluster;
import org.voltdb.catalog.Database;
import org.voltdb.compiler.DatabaseEstimates;
import org.voltdb.compiler.ScalarValueHints;
import org.voltdb.planner.PlanAssembler;
import org.voltdb.planner.PlanColumn;
import org.voltdb.planner.PlanStatistics;
import org.voltdb.planner.PlannerContext;
import org.voltdb.planner.StatsField;
import org.voltdb.types.*;
import edu.brown.plannodes.PlanNodeUtil;
import edu.brown.utils.ClassUtil;
public abstract class AbstractPlanNode implements JSONString, Cloneable, Comparable<AbstractPlanNode> {
public enum Members {
ID,
PLAN_NODE_TYPE,
INLINE_NODES,
CHILDREN_IDS,
PARENT_IDS,
OUTPUT_COLUMNS,
IS_INLINE,
}
private int m_id = -1;
protected List<AbstractPlanNode> m_children = new ArrayList<AbstractPlanNode>();
protected List<AbstractPlanNode> m_parents = new ArrayList<AbstractPlanNode>();
protected HashSet<AbstractPlanNode> m_dominators = new HashSet<AbstractPlanNode>();
// PAVLO: We need this figure out how to reconstruct the tree
protected List<Integer> m_childrenIds = new ArrayList<Integer>();
protected List<Integer> m_parentIds = new ArrayList<Integer>();
// TODO: planner accesses this data directly. Should be protected.
protected ArrayList<Integer> m_outputColumns = new ArrayList<Integer>();
protected List<ScalarValueHints> m_outputColumnHints = new ArrayList<ScalarValueHints>();
protected long m_estimatedOutputTupleCount = 0;
/**
* Some PlanNodes can take advantage of inline PlanNodes to perform
* certain additional tasks while performing their main operation, rather than
* having to re-read tuples from intermediate results
*/
protected Map<PlanNodeType, AbstractPlanNode> m_inlineNodes = new HashMap<PlanNodeType, AbstractPlanNode>();
protected boolean m_isInline = false;
protected final PlannerContext m_context;
/**
* Instantiates a new plan node.
*
* @param id the id
*/
protected AbstractPlanNode(PlannerContext context, int id) {
assert(context != null);
assert(id != 0);
m_context = context;
m_id = id;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof AbstractPlanNode) {
AbstractPlanNode other = (AbstractPlanNode)obj;
return (m_id == other.m_id &&
m_isInline == other.m_isInline &&
this.getPlanNodeType() == other.getPlanNodeType() &&
this.getOutputColumnGUIDs().equals(other.getOutputColumnGUIDs()));
}
return (false);
}
protected final int getId() {
return (m_id);
}
@Override
public final Object clone() throws CloneNotSupportedException {
return (this.clone(true, true));
}
public Object clone(boolean clone_children, boolean clone_inline) throws CloneNotSupportedException {
AbstractPlanNode clone = (AbstractPlanNode)super.clone();
clone.overrideId(PlanAssembler.getNextPlanNodeId());
clone.m_children = new ArrayList<AbstractPlanNode>();
clone.m_parents = new ArrayList<AbstractPlanNode>();
clone.m_dominators = new HashSet<AbstractPlanNode>(m_dominators);
clone.m_childrenIds = new ArrayList<Integer>();
clone.m_outputColumns = new ArrayList<Integer>(m_outputColumns);
clone.m_outputColumnHints = new ArrayList<ScalarValueHints>(m_outputColumnHints);
clone.m_inlineNodes = new HashMap<PlanNodeType, AbstractPlanNode>();
// Clone Children
if (clone_children) {
// clone.m_children.clear();
// clone.m_childrenIds.clear();
for (AbstractPlanNode child_node : this.m_children) {
AbstractPlanNode child_clone = (AbstractPlanNode)child_node.clone(clone_inline, clone_children);
child_clone.m_parents.clear();
child_clone.m_parentIds.clear();
child_clone.m_parents.add(clone);
child_clone.m_parentIds.add(clone.m_id);
clone.m_children.add(child_clone);
clone.m_childrenIds.add(child_clone.m_id);
} // FOR
}
// Clone Inlines
if (clone_inline) {
// clone.m_inlineNodes.clear();
for (Entry<PlanNodeType, AbstractPlanNode> e : this.m_inlineNodes.entrySet()) {
AbstractPlanNode inline_clone = (AbstractPlanNode)e.getValue().clone(clone_inline, clone_children);
clone.m_inlineNodes.put(e.getKey(), inline_clone);
} // FOR
}
return (clone);
}
public void overrideId(int newId) {
m_id = newId;
}
/**
* Create a PlanNode that clones the configuration information but
* is not inserted in the plan graph and has a unique plan node id.
*/
protected void produceCopyForTransformation(AbstractPlanNode copy) {
for (Integer colGuid : m_outputColumns) {
copy.m_outputColumns.add(colGuid);
}
copy.m_outputColumnHints.addAll(m_outputColumnHints);
copy.m_estimatedOutputTupleCount = m_estimatedOutputTupleCount;
// clone is not yet implemented for every node.
assert(m_inlineNodes.size() == 0);
assert(m_isInline == false);
// the api requires the copy is not (yet) connected
assert (copy.m_parents.size() == 0);
assert (copy.m_children.size() == 0);
}
public abstract PlanNodeType getPlanNodeType();
public void setOutputColumns(Collection<Integer> col_guids) {
this.m_outputColumns.clear();
this.m_outputColumns.addAll(col_guids);
}
public boolean updateOutputColumns(Database db) {
//System.out.println("updateOutputColumns Node type: " + this.getPlanNodeType() + " # of inline nodes: " + this.getInlinePlanNodes().size());
ArrayList<Integer> childCols = new ArrayList<Integer>();
for (AbstractPlanNode child : m_children) {
boolean result = child.updateOutputColumns(db);
assert(result);
// print child inline columns
// for (Integer out : child.m_outputColumns)
// {
// System.out.println(m_context.get(out).displayName());
// }
childCols.addAll(child.m_outputColumns);
}
ArrayList<Integer> new_output_cols = new ArrayList<Integer>();
new_output_cols = createOutputColumns(db, childCols);
for (AbstractPlanNode child : m_inlineNodes.values()) {
if (child instanceof IndexScanPlanNode)
continue;
new_output_cols = child.createOutputColumns(db, new_output_cols);
}
// Before we wipe out the old column list, free any PlanColumns that
// aren't getting reused
for (Integer col : m_outputColumns)
{
if (!new_output_cols.contains(col))
{
m_context.freeColumn(col);
}
}
m_outputColumns = new_output_cols;
return true;
}
/** By default, a plan node does not alter its input schema */
@SuppressWarnings("unchecked")
protected ArrayList<Integer> createOutputColumns(Database db, ArrayList<Integer> input) {
return (ArrayList<Integer>)input.clone();
}
/**
* Get number of output columns for this node
* @return
*/
public int getOutputColumnGUIDCount() {
return (this.m_outputColumns.size());
}
/**
* Return the PlanColumn GUID at the given offset
* @param idx
* @return
*/
public int getOutputColumnGUID(int idx) {
return (this.m_outputColumns.get(idx));
}
/**
* Get the list of the Output PlanColumn GUIDs
* @return
*/
public List<Integer> getOutputColumnGUIDs() {
return (this.m_outputColumns);
}
public PlanColumn findMatchingOutputColumn(String tableName,
String columnName,
String columnAlias)
{
boolean found = false;
PlanColumn retval = null;
for (Integer colguid : m_outputColumns) {
PlanColumn plancol = m_context.get(colguid);
if ((plancol.originTableName().equals(tableName)) &&
((plancol.originColumnName().equals(columnName)) ||
(plancol.originColumnName().equals(columnAlias))))
{
found = true;
retval = plancol;
break;
}
}
if (!found) {
assert(found) : "Found no candidate output column.";
throw new RuntimeException("Found no candidate output column.");
}
return retval;
}
public void validate() throws Exception {
//
// Make sure our children have us listed as their parents
//
for (AbstractPlanNode child : m_children) {
if (!child.m_parents.contains(this)) {
throw new Exception("ERROR: The child PlanNode '" + child.toString() + "' does not " +
"have its parent PlanNode '" + toString() + "' in its parents list");
}
child.validate();
}
//
// Inline PlanNodes
//
if (!m_inlineNodes.isEmpty()) {
for (AbstractPlanNode node : m_inlineNodes.values()) {
//
// Make sure that we're not attached to some kind of tree somewhere...
//
if (!node.m_children.isEmpty()) {
throw new Exception("ERROR: The inline PlanNode '" + node + "' has children inside of PlanNode '" + this + "'");
} else if (!node.m_parents.isEmpty()) {
throw new Exception("ERROR: The inline PlanNode '" + node + "' has parents inside of PlanNode '" + this + "'");
} else if (!node.isInline()) {
throw new Exception("ERROR: The inline PlanNode '" + node + "' was not marked as inline for PlanNode '" + this + "'");
} else if (!node.getInlinePlanNodes().isEmpty()) {
throw new Exception("ERROR: The inline PlanNode '" + node + "' has its own inline PlanNodes inside of PlanNode '" + this + "'");
}
node.validate();
}
}
}
@Override
public final String toString() {
return String.format("%s[#%02d]", getPlanNodeType().toString(), m_id);
}
public boolean computeEstimatesRecursively(PlanStatistics stats, Cluster cluster, Database db, DatabaseEstimates estimates, ScalarValueHints[] paramHints) {
assert(estimates != null);
m_outputColumnHints.clear();
m_estimatedOutputTupleCount = 0;
// recursively compute and collect stats from children
for (AbstractPlanNode child : m_children) {
boolean result = child.computeEstimatesRecursively(stats, cluster, db, estimates, paramHints);
assert(result);
m_outputColumnHints.addAll(child.m_outputColumnHints);
m_estimatedOutputTupleCount += child.m_estimatedOutputTupleCount;
stats.incrementStatistic(0, StatsField.TUPLES_READ, m_estimatedOutputTupleCount);
}
return true;
}
/**
* Gets the id.
*
* @return the id
*/
public Integer getPlanNodeId() {
return m_id;
}
/**
* Add a plan node as a child of this node and link this node as it's parent.
* @param child The node to add.
*/
public void addAndLinkChild(AbstractPlanNode child) {
m_children.add(child);
child.m_parents.add(this);
}
/** Remove child from this node.
* @param child to remove.
*/
public void unlinkChild(AbstractPlanNode child) {
m_children.remove(child);
child.m_parents.remove(this);
}
/**
* Gets the children.
* @return the children
*/
public int getChildPlanNodeCount() {
return m_children.size();
}
/**
* @param index
* @return The child node of this node at a given index or null if none exists.
*/
public AbstractPlanNode getChild(int index) {
return m_children.get(index);
}
/**
* Gets all of the children of this node
* @return
*/
public List<AbstractPlanNode> getChildren() {
return (Collections.unmodifiableList(m_children));
}
public void clearChildren() {
m_children.clear();
m_childrenIds.clear();
}
public boolean hasChild(AbstractPlanNode receive) {
return m_children.contains(receive);
}
/**
* Gets the number of parents.
* @return the parents
*/
public int getParentPlanNodeCount() {
return m_parents.size();
}
public AbstractPlanNode getParent(int index) {
return m_parents.get(index);
}
/**
* Gets all of the parents of this node
* @return
*/
public List<AbstractPlanNode> getParents() {
return (Collections.unmodifiableList(m_parents));
}
public void clearParents() {
m_parents.clear();
m_parentIds.clear();
}
public void removeFromGraph() {
for (AbstractPlanNode parent : m_parents)
parent.m_children.remove(this);
for (AbstractPlanNode child : m_children)
child.m_parents.remove(this);
m_parents.clear();
m_children.clear();
}
/** Interject the provided node between this node and this node's current children */
public void addIntermediary(AbstractPlanNode node) {
// transfer this node's children to node
Iterator<AbstractPlanNode> it = m_children.iterator();
while (it.hasNext()) {
AbstractPlanNode child = it.next();
it.remove(); // remove this.child from m_children
assert(child.getParentPlanNodeCount() == 1) :
String.format("Expected %s to have only one parent but it has %s", child, child.getParents());
child.clearParents(); // and reset child's parents list
node.addAndLinkChild(child); // set node.child and child.parent
}
// and add node to this node's children
assert(m_children.size() == 0);
addAndLinkChild(node);
}
/**
* @return The map of inlined nodes.
*/
public Map<PlanNodeType, AbstractPlanNode> getInlinePlanNodes() {
return m_inlineNodes;
}
public int getInlinePlanNodeCount() {
return (m_inlineNodes.size());
}
/**
* @param node
*/
public void addInlinePlanNode(AbstractPlanNode node) {
node.m_isInline = true;
m_inlineNodes.put(node.getPlanNodeType(), node);
node.m_children.clear();
node.m_parents.clear();
}
/**
*
* @param type
*/
public void removeInlinePlanNode(PlanNodeType type) {
if (m_inlineNodes.containsKey(type)) {
m_inlineNodes.remove(type);
}
}
/**
*
* @param type
* @return An inlined node of the given type or null if none.
*/
@SuppressWarnings("unchecked")
public <T extends AbstractPlanNode> T getInlinePlanNode(PlanNodeType type) {
return (T)m_inlineNodes.get(type);
}
/**
* Return all of the inline AbstractPlanNodes with the same class
* @param clazz
* @return
*/
@SuppressWarnings("unchecked")
public <T extends AbstractPlanNode> Collection<T> getInlinePlanNodes(Class<T> clazz) {
Set<T> ret = new HashSet<T>();
for (AbstractPlanNode inline : this.m_inlineNodes.values()) {
if (ClassUtil.getSuperClasses(inline.getClass()).contains(clazz)) {
ret.add((T)inline);
}
} // FOR
return (ret);
}
/**
*
* @return Is this node inlined in another node.
*/
public Boolean isInline() {
return m_isInline;
}
/**
* @return the dominator list for a node
*/
public HashSet<AbstractPlanNode> getDominators() {
return m_dominators;
}
/**
* Initialize a hashset for each node containing that node's dominators
* (the set of predecessors that *always* precede this node in a traversal
* of the plan-graph in reverse-execution order (from root to leaves)).
*/
public void calculateDominators() {
HashSet<AbstractPlanNode> visited = new HashSet<AbstractPlanNode>();
calculateDominators_recurse(visited);
}
private void calculateDominators_recurse(HashSet<AbstractPlanNode> visited) {
if (visited.contains(this)) {
assert(false): "do not expect loops in plangraph.";
return;
}
visited.add(this);
m_dominators.clear();
m_dominators.add(this);
// find nodes that are in every parent's dominator set.
HashMap<AbstractPlanNode, Integer> union = new HashMap<AbstractPlanNode, Integer>();
for (AbstractPlanNode n : m_parents) {
for (AbstractPlanNode d : n.getDominators()) {
if (union.containsKey(d))
union.put(d, union.get(d) + 1);
else
union.put(d, 1);
}
}
for (AbstractPlanNode pd : union.keySet() ) {
if (union.get(pd) == m_parents.size())
m_dominators.add(pd);
}
for (AbstractPlanNode n : m_children)
n.calculateDominators_recurse(visited);
}
/**
* @param type plan node type to search for
* @return a list of nodes that are eventual successors of this node of the desired type
*/
public List<AbstractPlanNode> findAllNodesOfType(PlanNodeType type) {
HashSet<AbstractPlanNode> visited = new HashSet<AbstractPlanNode>();
ArrayList<AbstractPlanNode> collected = new ArrayList<AbstractPlanNode>();
findAllNodesOfType_recurse(type, collected, visited);
return collected;
}
public void findAllNodesOfType_recurse(PlanNodeType type,ArrayList<AbstractPlanNode> collected,
HashSet<AbstractPlanNode> visited)
{
if (visited.contains(this)) {
assert(false): "do not expect loops in plangraph.";
return;
}
visited.add(this);
if (getPlanNodeType() == type)
collected.add(this);
for (AbstractPlanNode n : m_children)
n.findAllNodesOfType_recurse(type, collected, visited);
}
public void freeColumns(Set<Integer> skip) {
Collection<Integer> guids = PlanNodeUtil.getAllPlanColumnGuids(this);
guids.removeAll(skip);
for (Integer guid : guids) {
m_context.freeColumn(guid);
} // FOR
}
@Override
public int compareTo(AbstractPlanNode other) {
int diff = 0;
// compare child nodes
HashMap<Integer, AbstractPlanNode> nodesById = new HashMap<Integer, AbstractPlanNode>();
for (AbstractPlanNode node : m_children)
nodesById.put(node.getPlanNodeId(), node);
for (AbstractPlanNode node : other.m_children) {
AbstractPlanNode myNode = nodesById.get(node.getPlanNodeId());
diff = myNode.compareTo(node);
if (diff != 0) return diff;
}
// compare inline nodes
HashMap<Integer, Entry<PlanNodeType, AbstractPlanNode>> inlineNodesById =
new HashMap<Integer, Entry<PlanNodeType, AbstractPlanNode>>();
for (Entry<PlanNodeType, AbstractPlanNode> e : m_inlineNodes.entrySet())
inlineNodesById.put(e.getValue().getPlanNodeId(), e);
for (Entry<PlanNodeType, AbstractPlanNode> e : other.m_inlineNodes.entrySet()) {
Entry<PlanNodeType, AbstractPlanNode> myE = inlineNodesById.get(e.getValue().getPlanNodeId());
if (myE.getKey() != e.getKey()) return -1;
diff = myE.getValue().compareTo(e.getValue());
if (diff != 0) return diff;
}
diff = m_id - other.m_id;
return diff;
}
// produce a file that can imported into graphviz for easier visualization
public String toDOTString() {
StringBuilder sb = new StringBuilder();
// id [label=id: value-type <value-type-attributes>];
// id -> child_id;
// id -> child_id;
sb.append(m_id).append(" [label=\"").append(m_id).append(": ").append(getPlanNodeType()).append("\" ");
sb.append(getValueTypeDotString(this));
sb.append("];\n");
for (AbstractPlanNode node : m_inlineNodes.values()) {
sb.append(m_id).append(" -> ").append(node.getPlanNodeId().intValue()).append(";\n");
sb.append(node.toDOTString());
}
for (AbstractPlanNode node : m_children) {
sb.append(m_id).append(" -> ").append(node.getPlanNodeId().intValue()).append(";\n");
}
return sb.toString();
}
// maybe not worth polluting
private String getValueTypeDotString(AbstractPlanNode pn) {
PlanNodeType pnt = pn.getPlanNodeType();
if (pn.isInline()) {
return "fontcolor=\"white\" style=\"filled\" fillcolor=\"red\"";
}
if (pnt == PlanNodeType.SEND || pnt == PlanNodeType.RECEIVE) {
return "fontcolor=\"white\" style=\"filled\" fillcolor=\"black\"";
}
return "";
}
@Override
public String toJSONString() {
JSONStringer stringer = new JSONStringer();
try
{
stringer.object();
toJSONString(stringer);
stringer.endObject();
}
catch (JSONException e)
{
throw new RuntimeException("Failed to serialize " + this, e);
// System.exit(-1);
}
return stringer.toString();
}
public void toJSONString(JSONStringer stringer) throws JSONException {
stringer.key(Members.ID.name()).value(m_id);
stringer.key(Members.PLAN_NODE_TYPE.name()).value(getPlanNodeType().toString());
stringer.key(Members.IS_INLINE.name()).value(m_isInline);
stringer.key(Members.INLINE_NODES.name()).array();
PlanNodeType types[] = new PlanNodeType[m_inlineNodes.size()];
int i = 0;
for (PlanNodeType type : m_inlineNodes.keySet()) {
types[i++] = type;
}
Arrays.sort(types);
for (PlanNodeType type : types) {
AbstractPlanNode node = m_inlineNodes.get(type);
assert(node != null);
assert(node instanceof JSONString);
stringer.value(node);
}
/*for (Map.Entry<PlanNodeType, AbstractPlanNode> entry : m_inlineNodes.entrySet()) {
assert (entry.getValue() instanceof JSONString);
stringer.value(entry.getValue());
}*/
stringer.endArray();
stringer.key(Members.CHILDREN_IDS.name()).array();
for (AbstractPlanNode node : m_children) {
stringer.value(node.getPlanNodeId().intValue());
}
stringer.endArray().key(Members.PARENT_IDS.name()).array();
for (AbstractPlanNode node : m_parents) {
stringer.value(node.getPlanNodeId().intValue());
}
stringer.endArray(); //end inlineNodes
stringer.key(Members.OUTPUT_COLUMNS.name());
stringer.array();
for (int col = 0; col < m_outputColumns.size(); col++) {
PlanColumn column = m_context.get(m_outputColumns.get(col));
column.toJSONString(stringer);
}
stringer.endArray();
}
abstract protected void loadFromJSONObject(JSONObject obj, Database db) throws JSONException;
public static AbstractPlanNode fromJSONObject(JSONObject obj, Database db) throws JSONException {
PlanNodeType pnt = PlanNodeType.valueOf(obj.getString(Members.PLAN_NODE_TYPE.name()));
AbstractPlanNode node = null;
try {
node = (AbstractPlanNode)ClassUtil.newInstance(pnt.getPlanNodeClass(),
new Object[]{ PlannerContext.singleton(), 1 },
new Class[]{ PlannerContext.class, Integer.class });
} catch (Exception e) {
e.printStackTrace();
return null;
}
node.m_id = obj.getInt(Members.ID.name());
node.m_isInline = obj.getBoolean(Members.IS_INLINE.name());
JSONArray inlineNodes = obj.getJSONArray(Members.INLINE_NODES.name());
for (int ii = 0; ii < inlineNodes.length(); ii++) {
JSONObject inobj = inlineNodes.getJSONObject(ii);
AbstractPlanNode inlineNode = AbstractPlanNode.fromJSONObject(inobj, db);
node.m_inlineNodes.put(inlineNode.getPlanNodeType(), inlineNode);
}
JSONArray childrenIds = obj.getJSONArray(Members.CHILDREN_IDS.name());
for (int ii = 0; ii < childrenIds.length(); ii++) {
node.m_childrenIds.add(childrenIds.getInt(ii));
}
JSONArray parentIds = obj.getJSONArray(Members.PARENT_IDS.name());
for (int ii = 0; ii < parentIds.length(); ii++) {
node.m_parentIds.add(parentIds.getInt(ii));
}
JSONArray outputColumns = obj.getJSONArray(Members.OUTPUT_COLUMNS.name());
for (int ii = 0; ii < outputColumns.length(); ii++) {
JSONObject jsonObject = outputColumns.getJSONObject(ii);
PlanColumn column = PlanColumn.fromJSONObject(jsonObject, db);
assert(column != null);
node.m_outputColumns.add(column.guid());
// System.err.println(String.format("[%02d] %s => %s", ii, node, column));
}
node.loadFromJSONObject(obj, db);
return node;
}
}