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