/* 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.planner;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.InputMismatchException;
import java.util.Map.Entry;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.voltdb.VoltType;
import org.voltdb.catalog.Column;
import org.voltdb.catalog.Database;
import org.voltdb.catalog.Table;
import org.voltdb.expressions.AbstractExpression;
import org.voltdb.expressions.AggregateExpression;
import org.voltdb.expressions.ConstantValueExpression;
import org.voltdb.expressions.ExpressionUtil;
import org.voltdb.expressions.ParameterValueExpression;
import org.voltdb.expressions.TupleValueExpression;
import org.voltdb.types.ExpressionType;
import org.voltdb.utils.StringInputStream;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
/**
*
*
*/
public abstract class AbstractParsedStmt {
/**
*
*
*/
static class HSQLXMLErrorHandler implements ErrorHandler {
public void error(SAXParseException exception) throws SAXException {
throw exception;
}
public void fatalError(SAXParseException exception) throws SAXException {
throw exception;
}
public void warning(SAXParseException exception) throws SAXException {
throw exception;
}
}
/**
*
*
*/
public static class TablePair {
public Table t1;
public Table t2;
@Override
public boolean equals(Object obj) {
if ((obj instanceof TablePair) == false)
return false;
TablePair tp = (TablePair)obj;
return (((t1 == tp.t1) && (t2 == tp.t2)) ||
((t1 == tp.t2) && (t2 == tp.t1)));
}
@Override
public int hashCode() {
assert((t1.hashCode() ^ t2.hashCode()) == (t2.hashCode() ^ t1.hashCode()));
return t1.hashCode() ^ t2.hashCode();
}
}
/** The */
public String sql;
/** The */
public ArrayList<ParameterInfo> paramList = new ArrayList<ParameterInfo>();
/** The */
public HashMap<Long, ParameterInfo> paramsById = new HashMap<Long, ParameterInfo>();
/** The */
public ArrayList<Table> tableList = new ArrayList<Table>();
/** The */
public AbstractExpression where = null;
/** The */
public ArrayList<AbstractExpression> whereSelectionList = new ArrayList<AbstractExpression>();
/** The */
public ArrayList<AbstractExpression> noTableSelectionList = new ArrayList<AbstractExpression>();
/** The */
public ArrayList<AbstractExpression> multiTableSelectionList = new ArrayList<AbstractExpression>();
/** The */
public HashMap<Table, ArrayList<AbstractExpression>> tableFilterList = new HashMap<Table, ArrayList<AbstractExpression>>();
/** The */
public HashMap<TablePair, ArrayList<AbstractExpression>> joinSelectionList = new HashMap<TablePair, ArrayList<AbstractExpression>>();
/**
*
* @param sql
* @param xmlSQL
* @param db
*/
public static AbstractParsedStmt parse(String sql, String xmlSQL, Database db) {
final String INSERT_NODE_NAME = "insert";
final String UPDATE_NODE_NAME = "update";
final String DELETE_NODE_NAME = "delete";
final String SELECT_NODE_NAME = "select";
AbstractParsedStmt retval = null;
StringInputStream input = new StringInputStream(xmlSQL);
HSQLXMLErrorHandler errHandler = new HSQLXMLErrorHandler();
Document doc = null;
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
try {
DocumentBuilder builder = factory.newDocumentBuilder();
builder.setErrorHandler(errHandler);
doc = builder.parse(input);
} catch (SAXParseException sxe) {
System.err.println(sxe.getMessage() + ": " + String.valueOf(sxe.getLineNumber()));
throw new InputMismatchException("XML Parsing failure during planning");
} catch (SAXException sxe) {
System.err.println(sxe.getMessage());
throw new InputMismatchException("XML Parsing failure during planning");
} catch (ParserConfigurationException e) {
System.err.println(e.getMessage());
throw new InputMismatchException("XML Parsing failure during planning");
} catch (IOException e) {
System.err.println(e.getMessage());
throw new InputMismatchException("XML Parsing failure during planning");
}
if (doc == null) {
System.err.println("Unexpected error parsing hsql parsed stmt xml");
System.exit(-1);
}
Node docElement = doc.getDocumentElement();
assert(docElement.getNodeName().equalsIgnoreCase("statement"));
Node stmtTypeElement = docElement.getFirstChild();
while (stmtTypeElement.getNodeType() != Node.ELEMENT_NODE)
stmtTypeElement = stmtTypeElement.getNextSibling();
// create non-abstract instances
if (stmtTypeElement.getNodeName().equalsIgnoreCase(INSERT_NODE_NAME)) {
retval = new ParsedInsertStmt();
}
else if (stmtTypeElement.getNodeName().equalsIgnoreCase(UPDATE_NODE_NAME)) {
retval = new ParsedUpdateStmt();
}
else if (stmtTypeElement.getNodeName().equalsIgnoreCase(DELETE_NODE_NAME)) {
retval = new ParsedDeleteStmt();
}
else if (stmtTypeElement.getNodeName().equalsIgnoreCase(SELECT_NODE_NAME)) {
retval = new ParsedSelectStmt();
}
else {
throw new RuntimeException("Unexpected Element: " + stmtTypeElement.getNodeName());
}
// parse tables and parameters
NodeList children = stmtTypeElement.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (node.getNodeName().equalsIgnoreCase("parameters")) {
retval.parseParameters(node, db);
}
if (node.getNodeName().equalsIgnoreCase("tablescans")) {
retval.parseTables(node, db);
}
}
// parse specifics
retval.parse(stmtTypeElement, db);
// split up the where expression into categories
retval.analyzeWhereExpression(db);
// these just shouldn't happen right?
assert(retval.multiTableSelectionList.size() == 0);
assert(retval.noTableSelectionList.size() == 0);
retval.sql = sql;
return retval;
}
/**
*
* @param stmtElement
* @param db
*/
abstract void parse(Node stmtElement, Database db);
/**
* Convert a HSQL VoltXML expression to an AbstractExpression tree.
* @param root
* @param db
* @return configured AbstractExpression
*/
AbstractExpression parseExpressionTree(Node root, Database db) {
String elementName = root.getNodeName().toLowerCase();
NamedNodeMap attrs = root.getAttributes();
AbstractExpression retval = null;
if (elementName.equals("value")) {
retval = parseValueExpression(root, attrs);
}
if (elementName.equals("columnref")) {
retval = parseColumnRefExpression(root, attrs, db);
}
if (elementName.equals("bool")) {
retval = parseBooleanExpresion(root, attrs);
}
if (elementName.equals("operation")) {
retval = parseOperationExpression(root, attrs, db);
}
if (elementName.equals("asterisk")) {
return null;
}
return retval;
}
/**
*
* @param exprNode
* @param attrs
* @return
*/
AbstractExpression parseValueExpression(Node exprNode, NamedNodeMap attrs) {
String type = attrs.getNamedItem("type").getNodeValue();
Node isParam = attrs.getNamedItem("isparam");
VoltType vt = VoltType.typeFromString(type);
int size = VoltType.MAX_VALUE_LENGTH;
assert(vt != VoltType.VOLTTABLE);
if (vt != VoltType.STRING) {
size = vt.getLengthInBytesForFixedTypes();
}
if ((isParam != null) && (isParam.getNodeValue().equalsIgnoreCase("true"))) {
ParameterValueExpression expr = new ParameterValueExpression();
long id = Long.parseLong(attrs.getNamedItem("id").getNodeValue());
ParameterInfo param = paramsById.get(id);
expr.setValueType(vt);
expr.setValueSize(size);
expr.setParameterId(param.index);
return expr;
}
else {
ConstantValueExpression expr = new ConstantValueExpression();
expr.setValueType(vt);
expr.setValueSize(size);
expr.setValue(attrs.getNamedItem("value").getNodeValue());
return expr;
}
}
/**
*
* @param exprNode
* @param attrs
* @param db
* @return
*/
AbstractExpression parseColumnRefExpression(Node exprNode, NamedNodeMap attrs, Database db) {
TupleValueExpression expr = new TupleValueExpression();
String alias = attrs.getNamedItem("alias").getNodeValue();
String tableName = attrs.getNamedItem("table").getNodeValue();
String columnName = attrs.getNamedItem("column").getNodeValue();
Table table = db.getTables().getIgnoreCase(tableName);
assert(table != null);
Column column = table.getColumns().getIgnoreCase(columnName);
assert(column != null);
expr.setColumnAlias(alias);
expr.setColumnName(columnName);
expr.setColumnIndex(column.getIndex());
expr.setTableName(tableName);
expr.setValueType(VoltType.get((byte)column.getType()));
expr.setValueSize(column.getSize());
return expr;
}
/**
*
* @param exprNode
* @param attrs
* @return
*/
AbstractExpression parseBooleanExpresion(Node exprNode, NamedNodeMap attrs) {
ConstantValueExpression expr = new ConstantValueExpression();
expr.setValueType(VoltType.BIGINT);
expr.setValueSize(VoltType.BIGINT.getLengthInBytesForFixedTypes());
if (attrs.getNamedItem("attrs").getNodeValue().equalsIgnoreCase("true"))
expr.setValue("1");
else
expr.setValue("0");
return expr;
}
/**
*
* @param exprNode
* @param attrs
* @param db
* @return
*/
AbstractExpression parseOperationExpression(Node exprNode, NamedNodeMap attrs, Database db) {
String type = attrs.getNamedItem("type").getNodeValue();
ExpressionType exprType = ExpressionType.get(type);
AbstractExpression expr = null;
if (exprType == ExpressionType.INVALID) {
throw new PlanningErrorException("Unsupported operation type '" + type + "'");
}
try {
expr = exprType.getExpressionClass().newInstance();
} catch (Exception e) {
e.printStackTrace();
System.exit(-1);
}
expr.setExpressionType(exprType);
// Allow expressions to read expression-specific data from exprNode.
// Looks like the design fully abstracts other volt classes from
// the XML serialization? Putting this here instead of in derived
// Expression implementations.
if (expr instanceof AggregateExpression) {
Node node;
if ((node = attrs.getNamedItem("distinct")) != null) {
AggregateExpression ae = (AggregateExpression)expr;
ae.m_distinct = Boolean.parseBoolean(node.getNodeValue());
}
}
// setup for children access
NodeList children = exprNode.getChildNodes();
int i = 0;
// get the first (left) node that is an element
Node leftExprNode = children.item(i++);
while ((leftExprNode != null) && (leftExprNode.getNodeType() != Node.ELEMENT_NODE))
leftExprNode = children.item(i++);
assert(leftExprNode != null);
// get the second (right) node that is an element (might be null)
Node rightExprNode = children.item(i++);
while ((rightExprNode != null) && (rightExprNode.getNodeType() != Node.ELEMENT_NODE))
rightExprNode = children.item(i++);
// recursively parse the left subtree (could be another operator or
// a constant/tuple/param value operand).
AbstractExpression leftExpr = parseExpressionTree(leftExprNode, db);
assert((leftExpr != null) || (exprType == ExpressionType.AGGREGATE_COUNT));
expr.setLeft(leftExpr);
if (ExpressionUtil.needsRightExpression(expr)) {
assert(rightExprNode != null);
// recursively parse the right subtree
AbstractExpression rightExpr = parseExpressionTree(rightExprNode, db);
assert(rightExpr != null);
expr.setRight(rightExpr);
}
return expr;
}
/**
*
* @param tablesNode
* @param db
*/
private void parseTables(Node tablesNode, Database db) {
NodeList children = tablesNode.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (node.getNodeName().equalsIgnoreCase("tablescan")) {
String tableName = node.getAttributes().getNamedItem("table").getNodeValue();
Table table = db.getTables().getIgnoreCase(tableName);
assert(table != null);
tableList.add(table);
}
}
}
private void parseParameters(Node paramsNode, Database db) {
NodeList children = paramsNode.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (node.getNodeName().equalsIgnoreCase("parameter")) {
ParameterInfo param = new ParameterInfo();
NamedNodeMap attrs = node.getAttributes();
long id = Long.parseLong(attrs.getNamedItem("id").getNodeValue());
param.index = Integer.parseInt(attrs.getNamedItem("index").getNodeValue());
String typeName = attrs.getNamedItem("type").getNodeValue();
param.type = VoltType.typeFromString(typeName);
paramsById.put(id, param);
paramList.add(param);
}
}
}
/**
*
* @param db
*/
void analyzeWhereExpression(Database db) {
// nothing to do if there's no where expression
if (where == null) return;
// this first chunk of code breaks the code into a list of expression that
// all have to be true for the where clause to be true
ArrayDeque<AbstractExpression> in = new ArrayDeque<AbstractExpression>();
ArrayDeque<AbstractExpression> out = new ArrayDeque<AbstractExpression>();
in.add(where);
int loopOps;
do {
loopOps = 0;
AbstractExpression inExpr = null;
while ((inExpr = in.poll()) != null) {
if (inExpr.getExpressionType() == ExpressionType.CONJUNCTION_AND) {
out.add(inExpr.getLeft());
out.add(inExpr.getRight());
loopOps++;
}
else {
out.add(inExpr);
}
}
// swap the input/output
ArrayDeque<AbstractExpression> temp = in;
in = out;
out = temp;
// continue until a loop occurs that finds no ands
} while (loopOps > 0);
// the where selection list contains all the clauses
whereSelectionList.addAll(in);
// This next bit of code identifies which tables get classified how
HashSet<Table> tableSet = new HashSet<Table>();
for (AbstractExpression expr : whereSelectionList) {
tableSet.clear();
getTablesForExpression(db, expr, tableSet);
if (tableSet.size() == 0) {
noTableSelectionList.add(expr);
}
else if (tableSet.size() == 1) {
Table table = (Table) tableSet.toArray()[0];
ArrayList<AbstractExpression> exprs;
if (tableFilterList.containsKey(table)) {
exprs = tableFilterList.get(table);
}
else {
exprs = new ArrayList<AbstractExpression>();
tableFilterList.put(table, exprs);
}
exprs.add(expr);
}
else if (tableSet.size() == 2) {
TablePair pair = new TablePair();
pair.t1 = (Table) tableSet.toArray()[0];
pair.t2 = (Table) tableSet.toArray()[1];
ArrayList<AbstractExpression> exprs;
if (joinSelectionList.containsKey(pair)) {
exprs = joinSelectionList.get(pair);
}
else {
exprs = new ArrayList<AbstractExpression>();
joinSelectionList.put(pair, exprs);
}
exprs.add(expr);
}
else if (tableSet.size() > 2) {
multiTableSelectionList.add(expr);
}
}
}
/**
*
* @param db
* @param expr
* @param tables
*/
void getTablesForExpression(Database db, AbstractExpression expr, HashSet<Table> tables) {
if (expr.getLeft() != null)
getTablesForExpression(db, expr.getLeft(), tables);
if (expr.getRight() != null)
getTablesForExpression(db, expr.getRight(), tables);
if (expr.getExpressionType() == ExpressionType.VALUE_TUPLE) {
TupleValueExpression tupleExpr = (TupleValueExpression)expr;
String tableName = tupleExpr.getTableName();
Table table = db.getTables().getIgnoreCase(tableName);
tables.add(table);
}
}
@Override
public String toString() {
String retval = "SQL:\n\t" + sql + "\n";
retval += "PARAMETERS:\n\t";
for (ParameterInfo param : paramList) {
retval += param.toString() + " ";
}
retval += "\nTABLE SOURCES:\n\t";
for (Table table : tableList) {
retval += table.getTypeName() + " ";
}
if (where != null) {
retval += "\nWHERE:\n";
retval += "\t" + where.toString() + "\n";
retval += "WHERE SELECTION LIST:\n";
int i = 0;
for (AbstractExpression expr : whereSelectionList)
retval += "\t(" + String.valueOf(i++) + ") " + expr.toString() + "\n";
retval += "NO TABLE SELECTION LIST:\n";
i = 0;
for (AbstractExpression expr : noTableSelectionList)
retval += "\t(" + String.valueOf(i++) + ") " + expr.toString() + "\n";
retval += "TABLE FILTER LIST:\n";
for (Entry<Table, ArrayList<AbstractExpression>> pair : tableFilterList.entrySet()) {
i = 0;
retval += "\tTABLE: " + pair.getKey().getTypeName() + "\n";
for (AbstractExpression expr : pair.getValue())
retval += "\t\t(" + String.valueOf(i++) + ") " + expr.toString() + "\n";
}
retval += "JOIN CLAUSE LIST:\n";
for (Entry<TablePair, ArrayList<AbstractExpression>> pair : joinSelectionList.entrySet()) {
i = 0;
retval += "\tTABLES: " + pair.getKey().t1.getTypeName() + " and " + pair.getKey().t2.getTypeName() + "\n";
for (AbstractExpression expr : pair.getValue())
retval += "\t\t(" + String.valueOf(i++) + ") " + expr.toString() + "\n";
}
}
return retval;
}
}