changeset 1153:fe3f66cc83e7

Implement prepared statements (Part 2). Reviewed-by: ebaron Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2013-June/007215.html
author Severin Gehwolf <sgehwolf@redhat.com>
date Fri, 28 Jun 2013 16:46:25 +0200
parents 4ba43dcff78e
children ed7fab187035
files storage/core/src/test/java/com/redhat/thermostat/storage/core/QueuedStorageTest.java storage/core/src/test/java/com/redhat/thermostat/storage/internal/dao/AgentInfoDAOTest.java storage/core/src/test/java/com/redhat/thermostat/storage/internal/statement/ParsedStatementTest.java storage/core/src/test/java/com/redhat/thermostat/storage/internal/statement/PreparedStatementImplTest.java storage/core/src/test/java/com/redhat/thermostat/storage/internal/statement/StatementDescriptorParserTest.java storage/core/src/test/java/com/redhat/thermostat/storage/internal/statement/WhereExpressions.java storage/core/src/test/java/com/redhat/thermostat/storage/internal/statement/WhereExpressionsTest.java
diffstat 7 files changed, 1880 insertions(+), 14 deletions(-) [+]
line wrap: on
line diff
--- a/storage/core/src/test/java/com/redhat/thermostat/storage/core/QueuedStorageTest.java	Fri Jun 28 15:27:50 2013 +0200
+++ b/storage/core/src/test/java/com/redhat/thermostat/storage/core/QueuedStorageTest.java	Fri Jun 28 16:46:25 2013 +0200
@@ -562,6 +562,13 @@
                 Thread.currentThread().interrupt();
             }
         }
+
+        @Override
+        public PreparedStatement prepareStatement(StatementDescriptor desc)
+                throws DescriptorParsingException {
+            // not implemented
+            return null;
+        }
         
     }
 }
--- a/storage/core/src/test/java/com/redhat/thermostat/storage/internal/dao/AgentInfoDAOTest.java	Fri Jun 28 15:27:50 2013 +0200
+++ b/storage/core/src/test/java/com/redhat/thermostat/storage/internal/dao/AgentInfoDAOTest.java	Fri Jun 28 16:46:25 2013 +0200
@@ -54,11 +54,15 @@
 
 import com.redhat.thermostat.storage.core.Category;
 import com.redhat.thermostat.storage.core.Cursor;
+import com.redhat.thermostat.storage.core.DescriptorParsingException;
 import com.redhat.thermostat.storage.core.HostRef;
 import com.redhat.thermostat.storage.core.Key;
+import com.redhat.thermostat.storage.core.PreparedStatement;
 import com.redhat.thermostat.storage.core.Query;
 import com.redhat.thermostat.storage.core.Remove;
 import com.redhat.thermostat.storage.core.Replace;
+import com.redhat.thermostat.storage.core.StatementDescriptor;
+import com.redhat.thermostat.storage.core.StatementExecutionException;
 import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.core.Update;
 import com.redhat.thermostat.storage.dao.AgentInfoDAO;
@@ -116,17 +120,18 @@
         assertTrue(keys.contains(AgentInfoDAO.CONFIG_LISTEN_ADDRESS));
     }
 
+    @SuppressWarnings({ "unchecked", "rawtypes" })
     @Test
-    public void verifyGetAllAgentInformationWithOneAgentInStorage() {
-        @SuppressWarnings("unchecked")
-        Cursor<AgentInformation> agentCursor = mock(Cursor.class);
+    public void verifyGetAllAgentInformationWithOneAgentInStorage()
+            throws DescriptorParsingException, StatementExecutionException {
+        Cursor agentCursor = mock(Cursor.class);
         when(agentCursor.hasNext()).thenReturn(true).thenReturn(false);
         when(agentCursor.next()).thenReturn(agent1).thenReturn(null);
 
         Storage storage = mock(Storage.class);
-        Query query = mock(Query.class);
-        when(query.execute()).thenReturn(agentCursor);
-        when(storage.createQuery(any(Category.class))).thenReturn(query);
+        PreparedStatement stmt = mock(PreparedStatement.class);
+        when(storage.prepareStatement(any(StatementDescriptor.class))).thenReturn(stmt);
+        when(stmt.executeQuery()).thenReturn(agentCursor);
         AgentInfoDAOImpl dao = new AgentInfoDAOImpl(storage);
 
         List<AgentInformation> allAgentInfo = dao.getAllAgentInformation();
@@ -138,26 +143,29 @@
         assertEquals(expected, result);
     }
 
+    @SuppressWarnings({ "unchecked", "rawtypes" })
     @Test
-    public void verifyGetAliveAgent() {
-        @SuppressWarnings("unchecked")
-        Cursor<AgentInformation> agentCursor = mock(Cursor.class);
+    public void verifyGetAliveAgent() throws DescriptorParsingException, StatementExecutionException {
+        Cursor agentCursor = mock(Cursor.class);
         when(agentCursor.hasNext()).thenReturn(true).thenReturn(false);
         when(agentCursor.next()).thenReturn(agent1).thenReturn(null);
 
         Query query = mock(Query.class);
         Storage storage = mock(Storage.class);
+        PreparedStatement stmt = mock(PreparedStatement.class);
+        when(storage.prepareStatement(any(StatementDescriptor.class))).thenReturn(stmt);
+        when(stmt.executeQuery()).thenReturn(agentCursor);
         when(storage.createQuery(any(Category.class))).thenReturn(query);
         when(query.execute()).thenReturn(agentCursor);
 
         AgentInfoDAO dao = new AgentInfoDAOImpl(storage);
         List<AgentInformation> aliveAgents = dao.getAliveAgents();
 
-        verify(storage).createQuery(AgentInfoDAO.CATEGORY);
-        Expression expr = factory.equalTo(AgentInfoDAO.ALIVE_KEY, Boolean.TRUE);
-        verify(query).where(eq(expr));
-        verify(query).execute();
-        verifyNoMoreInteractions(query);
+        verify(storage).prepareStatement(any(StatementDescriptor.class));
+        verify(stmt).executeQuery();
+        verify(stmt).setString(eq(0), eq(AgentInfoDAOImpl.ALIVE_KEY.getName()));
+        verify(stmt).setBoolean(eq(1), eq(true));
+        verifyNoMoreInteractions(stmt);
 
         assertEquals(1, aliveAgents.size());
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/test/java/com/redhat/thermostat/storage/internal/statement/ParsedStatementTest.java	Fri Jun 28 16:46:25 2013 +0200
@@ -0,0 +1,469 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat 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 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat 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 Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.storage.internal.statement;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.redhat.thermostat.common.Pair;
+import com.redhat.thermostat.storage.core.Cursor;
+import com.redhat.thermostat.storage.core.Key;
+import com.redhat.thermostat.storage.core.Query;
+import com.redhat.thermostat.storage.core.Query.SortDirection;
+import com.redhat.thermostat.storage.query.BinaryComparisonExpression;
+import com.redhat.thermostat.storage.query.BinaryComparisonOperator;
+import com.redhat.thermostat.storage.query.BinaryLogicalExpression;
+import com.redhat.thermostat.storage.query.BinaryLogicalOperator;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.LiteralExpression;
+
+public class ParsedStatementTest {
+
+    private Query<?> statement;
+    
+    @Before
+    public void setup() {
+        statement = new TestQuery();
+    }
+    
+    @After
+    public void tearDown() {
+        statement = null;
+    }
+    
+    @SuppressWarnings("rawtypes")
+    @Test
+    public void canPatchWhereAndExpr() throws IllegalPatchException {
+        // create the parsedStatementImpl we are going to use
+        ParsedStatement parsedStmt = new ParsedStatement(statement);
+        SuffixExpression suffixExpn = new SuffixExpression();
+        suffixExpn.setLimitExpn(null);
+        suffixExpn.setSortExpn(null);
+        // WHERE a = ? AND c = ?
+        WhereExpression expn = new WhereExpression();
+        BinaryExpressionNode and = new BinaryExpressionNode(expn.getRoot());
+        expn.getRoot().setValue(and);
+        and.setOperator(BinaryLogicalOperator.AND);
+        BinaryExpressionNode leftEqual = new BinaryExpressionNode(and);
+        BinaryExpressionNode rightEqual = new BinaryExpressionNode(and);
+        leftEqual.setOperator(BinaryComparisonOperator.EQUALS);
+        rightEqual.setOperator(BinaryComparisonOperator.EQUALS);
+        and.setLeftChild(leftEqual);
+        and.setRightChild(rightEqual);
+        TerminalNode a = new TerminalNode(leftEqual);
+        Key aKey = new Key("a", false);
+        a.setValue(aKey);
+        TerminalNode b = new TerminalNode(leftEqual);
+        UnfinishedValueNode patchB = new UnfinishedValueNode();
+        patchB.setParameterIndex(0);
+        patchB.setType(String.class);
+        b.setValue(patchB);
+        leftEqual.setLeftChild(a);
+        leftEqual.setRightChild(b);
+        TerminalNode c = new TerminalNode(rightEqual);
+        c.setValue("c");
+        rightEqual.setLeftChild(c);
+        TerminalNode d = new TerminalNode(rightEqual);
+        UnfinishedValueNode dPatch = new UnfinishedValueNode();
+        dPatch.setParameterIndex(1);
+        dPatch.setType(Integer.class);
+        d.setValue(dPatch);
+        rightEqual.setRightChild(d);
+        suffixExpn.setWhereExpn(expn);
+        parsedStmt.setNumFreeParams(1);
+        parsedStmt.setSuffixExpression(suffixExpn);
+        // next, create the PreparedStatement we are going to use for
+        // patching.
+        PreparedStatementImpl preparedStatement = new PreparedStatementImpl(2);
+        preparedStatement.setString(0, "test1");
+        preparedStatement.setInt(1, 2);
+        // finally test the patching
+        Query<?> query = (Query)parsedStmt.patchQuery(preparedStatement);
+        assertTrue(query instanceof TestQuery);
+        TestQuery q = (TestQuery)query;
+        Expression expectedExpression = q.expr;
+        assertTrue(expectedExpression instanceof BinaryLogicalExpression);
+        BinaryLogicalExpression andFinal = (BinaryLogicalExpression)expectedExpression;
+        assertEquals(BinaryLogicalOperator.AND, andFinal.getOperator());
+        assertTrue(andFinal.getLeftOperand() instanceof BinaryComparisonExpression);
+        assertTrue(andFinal.getRightOperand() instanceof BinaryComparisonExpression);
+        BinaryComparisonExpression left = (BinaryComparisonExpression)andFinal.getLeftOperand();
+        BinaryComparisonExpression right = (BinaryComparisonExpression)andFinal.getRightOperand();
+        assertEquals(BinaryComparisonOperator.EQUALS, left.getOperator());
+        assertEquals(BinaryComparisonOperator.EQUALS, right.getOperator());
+        assertTrue(left.getLeftOperand() instanceof LiteralExpression);
+        assertTrue(left.getRightOperand() instanceof LiteralExpression);
+        LiteralExpression leftLiteral1 = (LiteralExpression)left.getLeftOperand();
+        LiteralExpression rightLiteral1 = (LiteralExpression)left.getRightOperand();
+        assertEquals(aKey, leftLiteral1.getValue());
+        assertEquals("test1", rightLiteral1.getValue());
+        LiteralExpression leftLiteral2 = (LiteralExpression)right.getLeftOperand();
+        LiteralExpression rightLiteral2 = (LiteralExpression)right.getRightOperand();
+        assertEquals("c", leftLiteral2.getValue());
+        // right literal value should have been patched to a "d"
+        assertEquals(2, rightLiteral2.getValue());
+    }
+    
+    @SuppressWarnings("rawtypes")
+    @Test
+    public void canPatchBasicWhereEquals() throws IllegalPatchException {
+        // create the parsedStatementImpl we are going to use
+        ParsedStatement parsedStmt = new ParsedStatement(statement);
+        SuffixExpression suffixExpn = new SuffixExpression();
+        suffixExpn.setLimitExpn(null);
+        suffixExpn.setSortExpn(null);
+        // WHERE a = ?
+        WhereExpression expn = new WhereExpression();
+        BinaryExpressionNode and = new BinaryExpressionNode(expn.getRoot());
+        expn.getRoot().setValue(and);
+        and.setOperator(BinaryComparisonOperator.EQUALS);
+        TerminalNode a = new TerminalNode(and);
+        a.setValue(new Key("a", false));
+        TerminalNode b = new TerminalNode(and);
+        UnfinishedValueNode bPatch = new UnfinishedValueNode();
+        bPatch.setParameterIndex(0);
+        bPatch.setType(boolean.class);
+        b.setValue(bPatch);
+        and.setLeftChild(a);
+        and.setRightChild(b);
+        suffixExpn.setWhereExpn(expn);
+        parsedStmt.setNumFreeParams(1);
+        parsedStmt.setSuffixExpression(suffixExpn);
+        // next, create the PreparedStatement we are going to use for
+        // patching.
+        PreparedStatementImpl preparedStatement = new PreparedStatementImpl(1);
+        preparedStatement.setBoolean(0, true);
+        // finally test the patching
+        Query<?> query = (Query)parsedStmt.patchQuery(preparedStatement);
+        assertTrue(query instanceof TestQuery);
+        TestQuery q = (TestQuery)query;
+        Expression expectedExpression = q.expr;
+        assertTrue(expectedExpression instanceof BinaryComparisonExpression);
+        BinaryComparisonExpression root = (BinaryComparisonExpression)expectedExpression;
+        assertEquals(BinaryComparisonOperator.EQUALS, root.getOperator());
+        assertTrue(root.getLeftOperand() instanceof LiteralExpression);
+        assertTrue(root.getRightOperand() instanceof LiteralExpression);
+        LiteralExpression leftLiteral1 = (LiteralExpression)root.getLeftOperand();
+        LiteralExpression rightLiteral1 = (LiteralExpression)root.getRightOperand();
+        assertEquals(new Key("a", false), leftLiteral1.getValue());
+        // this should have gotten patched to a "b"
+        assertEquals(true, rightLiteral1.getValue());
+        // now do it again with a different value
+        preparedStatement = new PreparedStatementImpl(1);
+        preparedStatement.setBoolean(0, false);
+        query = (Query)parsedStmt.patchQuery(preparedStatement);
+        assertTrue(query instanceof TestQuery);
+        q = (TestQuery)query;
+        expectedExpression = q.expr;
+        assertTrue(expectedExpression instanceof BinaryComparisonExpression);
+        root = (BinaryComparisonExpression)expectedExpression;
+        assertEquals(BinaryComparisonOperator.EQUALS, root.getOperator());
+        assertTrue(root.getLeftOperand() instanceof LiteralExpression);
+        assertTrue(root.getRightOperand() instanceof LiteralExpression);
+        leftLiteral1 = (LiteralExpression)root.getLeftOperand();
+        rightLiteral1 = (LiteralExpression)root.getRightOperand();
+        assertEquals(new Key("a", false), leftLiteral1.getValue());
+        assertEquals(false, rightLiteral1.getValue());
+    }
+    
+    @SuppressWarnings("rawtypes")
+    @Test
+    public void canPatchBasicWhereEqualsLHSKeyAndRHSValue() throws IllegalPatchException {
+        // create the parsedStatementImpl we are going to use
+        ParsedStatement parsedStmt = new ParsedStatement(statement);
+        SuffixExpression suffixExpn = new SuffixExpression();
+        suffixExpn.setLimitExpn(null);
+        suffixExpn.setSortExpn(null);
+        // WHERE ?s = ?b
+        WhereExpression expn = new WhereExpression();
+        BinaryExpressionNode and = new BinaryExpressionNode(expn.getRoot());
+        expn.getRoot().setValue(and);
+        and.setOperator(BinaryComparisonOperator.EQUALS);
+        TerminalNode a = new TerminalNode(and);
+        UnfinishedValueNode aPatch = new UnfinishedValueNode();
+        aPatch.setLHS(true);
+        aPatch.setType(String.class);
+        aPatch.setParameterIndex(0);
+        a.setValue(aPatch);
+        TerminalNode b = new TerminalNode(and);
+        UnfinishedValueNode bPatch = new UnfinishedValueNode();
+        bPatch.setParameterIndex(1);
+        bPatch.setType(boolean.class);
+        b.setValue(bPatch);
+        and.setLeftChild(a);
+        and.setRightChild(b);
+        suffixExpn.setWhereExpn(expn);
+        parsedStmt.setNumFreeParams(1);
+        parsedStmt.setSuffixExpression(suffixExpn);
+        // next, create the PreparedStatement we are going to use for
+        // patching.
+        PreparedStatementImpl preparedStatement = new PreparedStatementImpl(2);
+        preparedStatement.setString(0, "a");
+        preparedStatement.setBoolean(1, true);
+        // finally test the patching
+        Query<?> query = (Query)parsedStmt.patchQuery(preparedStatement);
+        assertTrue(query instanceof TestQuery);
+        TestQuery q = (TestQuery)query;
+        Expression expectedExpression = q.expr;
+        assertTrue(expectedExpression instanceof BinaryComparisonExpression);
+        BinaryComparisonExpression root = (BinaryComparisonExpression)expectedExpression;
+        assertEquals(BinaryComparisonOperator.EQUALS, root.getOperator());
+        assertTrue(root.getLeftOperand() instanceof LiteralExpression);
+        assertTrue(root.getRightOperand() instanceof LiteralExpression);
+        LiteralExpression leftLiteral1 = (LiteralExpression)root.getLeftOperand();
+        LiteralExpression rightLiteral1 = (LiteralExpression)root.getRightOperand();
+        assertEquals(new Key("a", false), leftLiteral1.getValue());
+        // this should have gotten patched to a "b"
+        assertEquals(true, rightLiteral1.getValue());
+        // now do it again with a different value
+        preparedStatement = new PreparedStatementImpl(2);
+        preparedStatement.setString(0, "a");
+        preparedStatement.setBoolean(1, false);
+        query = (Query)parsedStmt.patchQuery(preparedStatement);
+        assertTrue(query instanceof TestQuery);
+        q = (TestQuery)query;
+        expectedExpression = q.expr;
+        assertTrue(expectedExpression instanceof BinaryComparisonExpression);
+        root = (BinaryComparisonExpression)expectedExpression;
+        assertEquals(BinaryComparisonOperator.EQUALS, root.getOperator());
+        assertTrue(root.getLeftOperand() instanceof LiteralExpression);
+        assertTrue(root.getRightOperand() instanceof LiteralExpression);
+        leftLiteral1 = (LiteralExpression)root.getLeftOperand();
+        rightLiteral1 = (LiteralExpression)root.getRightOperand();
+        assertEquals(new Key("a", false), leftLiteral1.getValue());
+        assertEquals(false, rightLiteral1.getValue());
+    }
+    
+    @Test
+    public void canPatchBasicLimit() throws IllegalPatchException {
+        // create the parsedStatementImpl we are going to use
+        ParsedStatement parsedStmt = new ParsedStatement(statement);
+        SuffixExpression suffixExpn = new SuffixExpression();
+        LimitExpression limitExpnToPatch = new LimitExpression();
+        UnfinishedLimitValue unfinished = new UnfinishedLimitValue();
+        unfinished.setParameterIndex(0);
+        limitExpnToPatch.setValue(unfinished);
+        suffixExpn.setLimitExpn(limitExpnToPatch);
+        suffixExpn.setSortExpn(null);
+        suffixExpn.setWhereExpn(null);
+        parsedStmt.setSuffixExpression(suffixExpn);
+        // set the value for the one unfinished param
+        PreparedStatementImpl preparedStatement = new PreparedStatementImpl(1);
+        preparedStatement.setInt(0, 3);
+        @SuppressWarnings("rawtypes")
+        Query query = (Query)parsedStmt.patchQuery(preparedStatement);
+        assertTrue(query instanceof TestQuery);
+        TestQuery q = (TestQuery)query;
+        assertEquals(3, q.limitVal);
+    }
+    
+    @SuppressWarnings("rawtypes")
+    @Test
+    public void canPatchBasicSort() throws IllegalPatchException {
+        // create the parsedStatementImpl we are going to use
+        ParsedStatement parsedStmt = new ParsedStatement(statement);
+        SuffixExpression suffixExpn = new SuffixExpression();
+        // SORT ? ASC, b DSC
+        SortExpression sortExpn = new SortExpression();
+        SortMember member = new SortMember();
+        member.setDirection(SortDirection.ASCENDING);
+        UnfinishedSortKey unfinished = new UnfinishedSortKey();
+        unfinished.setParameterIndex(0);
+        member.setSortKey(unfinished);
+        SortMember member2 = new SortMember();
+        member2.setDirection(SortDirection.DESCENDING);
+        member2.setSortKey("b");
+        sortExpn.addMember(member);
+        sortExpn.addMember(member2);
+        suffixExpn.setLimitExpn(null);
+        suffixExpn.setSortExpn(sortExpn);
+        suffixExpn.setWhereExpn(null);
+        parsedStmt.setSuffixExpression(suffixExpn);
+        // set the value for the one unfinished param
+        PreparedStatementImpl preparedStatement = new PreparedStatementImpl(1);
+        preparedStatement.setString(0, "a");
+        Query query = (Query)parsedStmt.patchQuery(preparedStatement);
+        assertTrue(query instanceof TestQuery);
+        TestQuery q = (TestQuery)query;
+        List<Pair<Key, SortDirection>> actualSorts = q.sorts;
+        assertEquals(2, actualSorts.size());
+        Pair first = actualSorts.get(0);
+        Key firstKeyActual = (Key)first.getFirst();
+        Key expectedFirst = new Key("a", false);
+        assertEquals(expectedFirst, firstKeyActual);
+        assertEquals(SortDirection.ASCENDING, first.getSecond());
+        Pair second = actualSorts.get(1);
+        Key secondKeyActual = (Key)second.getFirst();
+        Key expectedSecond = new Key("b", false);
+        assertEquals(expectedSecond, secondKeyActual);
+        assertEquals(SortDirection.DESCENDING, second.getSecond());
+    }
+    
+    @SuppressWarnings("rawtypes")
+    @Test
+    public void failPatchWithWrongType() throws IllegalPatchException {
+        // create the parsedStatementImpl we are going to use
+        ParsedStatement parsedStmt = new ParsedStatement(statement);
+        SuffixExpression suffixExpn = new SuffixExpression();
+        suffixExpn.setLimitExpn(null);
+        suffixExpn.setSortExpn(null);
+        // WHERE 'a' = ?s AND 'c' = ?i
+        WhereExpression expn = new WhereExpression();
+        BinaryExpressionNode and = new BinaryExpressionNode(expn.getRoot());
+        expn.getRoot().setValue(and);
+        and.setOperator(BinaryLogicalOperator.AND);
+        BinaryExpressionNode leftEqual = new BinaryExpressionNode(and);
+        BinaryExpressionNode rightEqual = new BinaryExpressionNode(and);
+        leftEqual.setOperator(BinaryComparisonOperator.EQUALS);
+        rightEqual.setOperator(BinaryComparisonOperator.EQUALS);
+        and.setLeftChild(leftEqual);
+        and.setRightChild(rightEqual);
+        TerminalNode a = new TerminalNode(leftEqual);
+        Key aKey = new Key("a", false);
+        a.setValue(aKey);
+        TerminalNode b = new TerminalNode(leftEqual);
+        UnfinishedValueNode patchB = new UnfinishedValueNode();
+        patchB.setType(String.class);
+        patchB.setParameterIndex(0);
+        b.setValue(patchB);
+        leftEqual.setLeftChild(a);
+        leftEqual.setRightChild(b);
+        TerminalNode c = new TerminalNode(rightEqual);
+        Key cKey = new Key("c", false);
+        c.setValue(cKey);
+        rightEqual.setLeftChild(c);
+        TerminalNode d = new TerminalNode(rightEqual);
+        UnfinishedValueNode dPatch = new UnfinishedValueNode();
+        dPatch.setParameterIndex(1);
+        dPatch.setType(Integer.class);
+        d.setValue(dPatch);
+        rightEqual.setRightChild(d);
+        suffixExpn.setWhereExpn(expn);
+        parsedStmt.setNumFreeParams(1);
+        parsedStmt.setSuffixExpression(suffixExpn);
+        // next, create the PreparedStatement we are going to use for
+        // patching.
+        PreparedStatementImpl preparedStatement = new PreparedStatementImpl(2);
+        preparedStatement.setString(0, "test1");
+        preparedStatement.setString(1, "foo");
+        // finally test the patching
+        try {
+            parsedStmt.patchQuery(preparedStatement);
+            fail("should have failed to patch param 1 with a string value (expected int)");
+        } catch (IllegalPatchException e) {
+            // pass
+            assertTrue(e.getMessage().contains("invalid type when attempting to patch"));
+        }
+    }
+    
+    @Test
+    public void failPatchBasicEqualsIfIndexOutofBounds() {
+        // create the parsedStatementImpl we are going to use
+        ParsedStatement parsedStmt = new ParsedStatement(statement);
+        SuffixExpression suffixExpn = new SuffixExpression();
+        suffixExpn.setLimitExpn(null);
+        suffixExpn.setSortExpn(null);
+        // WHERE a = ?
+        WhereExpression expn = new WhereExpression();
+        BinaryExpressionNode and = new BinaryExpressionNode(expn.getRoot());
+        expn.getRoot().setValue(and);
+        and.setOperator(BinaryComparisonOperator.EQUALS);
+        TerminalNode a = new TerminalNode(and);
+        a.setValue("a");
+        TerminalNode b = new TerminalNode(and);
+        UnfinishedValueNode bPatch = new UnfinishedValueNode();
+        bPatch.setParameterIndex(1); // out of bounds
+        b.setValue(bPatch);
+        and.setLeftChild(a);
+        and.setRightChild(b);
+        suffixExpn.setWhereExpn(expn);
+        parsedStmt.setNumFreeParams(1);
+        parsedStmt.setSuffixExpression(suffixExpn);
+        PreparedStatementImpl preparedStatement = new PreparedStatementImpl(1);
+        preparedStatement.setString(0, "b");
+        // this should fail
+        try {
+            parsedStmt.patchQuery(preparedStatement);
+        } catch (IllegalPatchException e) {
+            // pass
+            assertTrue(e.getCause() instanceof ArrayIndexOutOfBoundsException);
+        }
+    }
+    
+    @SuppressWarnings("rawtypes")
+    private static class TestQuery implements Query {
+
+        private Expression expr;
+        private List<Pair<Key, SortDirection>> sorts;
+        private int limitVal = -1;
+        
+        private TestQuery() {
+            sorts = new ArrayList<>();
+        }
+        
+        @Override
+        public void where(Expression expr) {
+            this.expr = expr;
+        }
+
+        @Override
+        public void sort(Key key, SortDirection direction) {
+            Pair<Key, SortDirection> sortPair = new Pair<>(key, direction);
+            sorts.add(sortPair);
+        }
+
+        @Override
+        public void limit(int n) {
+            this.limitVal = n;
+        }
+
+        @Override
+        public Cursor execute() {
+            // Not implemented
+            return null;
+        }
+        
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/test/java/com/redhat/thermostat/storage/internal/statement/PreparedStatementImplTest.java	Fri Jun 28 16:46:25 2013 +0200
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat 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 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat 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 Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.storage.internal.statement;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.junit.Test;
+
+import com.redhat.thermostat.storage.core.Category;
+import com.redhat.thermostat.storage.core.Cursor;
+import com.redhat.thermostat.storage.core.Key;
+import com.redhat.thermostat.storage.core.Query;
+import com.redhat.thermostat.storage.core.StatementDescriptor;
+import com.redhat.thermostat.storage.core.StatementExecutionException;
+import com.redhat.thermostat.storage.core.Storage;
+import com.redhat.thermostat.storage.query.BinaryComparisonExpression;
+import com.redhat.thermostat.storage.query.BinaryComparisonOperator;
+import com.redhat.thermostat.storage.query.Expression;
+import com.redhat.thermostat.storage.query.LiteralExpression;
+
+public class PreparedStatementImplTest {
+
+    @Test
+    public void failToSetIndexOutOfBounds() {
+        PreparedStatementImpl preparedStatement = new PreparedStatementImpl(2);
+        preparedStatement.setInt(1, 3);
+        preparedStatement.setString(0, "testing");
+        try {
+            preparedStatement.setLong(3, 1);
+            fail("should have thrown exception");
+        } catch (IllegalArgumentException e) {
+            // pass
+        }
+        try {
+            preparedStatement.setInt(4, 1);
+            fail("should have thrown exception");
+        } catch (IllegalArgumentException e) {
+            // pass
+        }
+        try {
+            preparedStatement.setString(10, "ignored");
+            fail("should have thrown exception");
+        } catch (IllegalArgumentException e) {
+            // pass
+        }
+        try {
+            preparedStatement.setStringList(3, new String[] { "ignored" });
+            fail("should have thrown exception");
+        } catch (IllegalArgumentException e) {
+            // pass
+        }
+    }
+    
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Test
+    public void canDoParsingPatchingAndExecution() throws Exception {
+        String queryString = "QUERY foo WHERE 'a' = ?s";
+        StatementDescriptor desc = mock(StatementDescriptor.class);
+        when(desc.getQueryDescriptor()).thenReturn(queryString);
+        Category mockCategory = mock(Category.class);
+        when(desc.getCategory()).thenReturn(mockCategory);
+        when(mockCategory.getName()).thenReturn("foo");
+        Storage storage = mock(Storage.class);
+        StubQuery stmt = new StubQuery();
+        when(storage.createQuery(any(Category.class))).thenReturn(stmt);
+        PreparedStatementImpl preparedStatement = new PreparedStatementImpl(storage, desc);
+        preparedStatement.setString(0, "foo");
+        preparedStatement.executeQuery();
+        assertTrue(stmt.called);
+        LiteralExpression o1 = new LiteralExpression<>(new Key("a", false));
+        LiteralExpression o2 = new LiteralExpression<>("foo"); 
+        BinaryComparisonExpression<LiteralExpression> binComp = new BinaryComparisonExpression<LiteralExpression>(
+                o1, BinaryComparisonOperator.EQUALS, o2);
+        assertEquals(binComp, stmt.expr);
+    }
+    
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Test
+    public void failExecutionWithWronglyTypedParams() throws Exception {
+        String queryString = "QUERY foo WHERE 'a' = ?b";
+        StatementDescriptor desc = mock(StatementDescriptor.class);
+        when(desc.getQueryDescriptor()).thenReturn(queryString);
+        Category mockCategory = mock(Category.class);
+        when(desc.getCategory()).thenReturn(mockCategory);
+        when(mockCategory.getName()).thenReturn("foo");
+        Storage storage = mock(Storage.class);
+        StubQuery stmt = new StubQuery();
+        when(storage.createQuery(any(Category.class))).thenReturn(stmt);
+        PreparedStatementImpl preparedStatement = new PreparedStatementImpl(storage, desc);
+        preparedStatement.setString(0, "foo");
+        try {
+            preparedStatement.executeQuery();
+            fail("Should have thrown SEE due to type mismatch. boolean vs. string");
+        } catch (StatementExecutionException e) {
+            // pass
+            assertTrue(e.getMessage().contains("invalid type when attempting to patch"));
+        }
+    }
+    
+    @SuppressWarnings("rawtypes")
+    private static class StubQuery implements Query {
+
+        private Expression expr;
+        private boolean called = false;
+        
+        @Override
+        public void where(Expression expr) {
+            this.expr = expr;
+        }
+
+        @Override
+        public void sort(Key key, SortDirection direction) {
+            // nothing
+        }
+
+        @Override
+        public void limit(int n) {
+            // nothing
+        }
+
+        @Override
+        public Cursor execute() {
+            called = true;
+            return null;
+        }
+        
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/test/java/com/redhat/thermostat/storage/internal/statement/StatementDescriptorParserTest.java	Fri Jun 28 16:46:25 2013 +0200
@@ -0,0 +1,802 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat 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 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat 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 Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.storage.internal.statement;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.redhat.thermostat.storage.core.Category;
+import com.redhat.thermostat.storage.core.DescriptorParsingException;
+import com.redhat.thermostat.storage.core.Key;
+import com.redhat.thermostat.storage.core.Query;
+import com.redhat.thermostat.storage.core.StatementDescriptor;
+import com.redhat.thermostat.storage.core.Storage;
+import com.redhat.thermostat.storage.core.Query.SortDirection;
+import com.redhat.thermostat.storage.dao.AgentInfoDAO;
+import com.redhat.thermostat.storage.internal.statement.BinaryExpressionNode;
+import com.redhat.thermostat.storage.internal.statement.LimitExpression;
+import com.redhat.thermostat.storage.internal.statement.NotBooleanExpressionNode;
+import com.redhat.thermostat.storage.internal.statement.ParsedStatement;
+import com.redhat.thermostat.storage.internal.statement.SortExpression;
+import com.redhat.thermostat.storage.internal.statement.SortMember;
+import com.redhat.thermostat.storage.internal.statement.StatementDescriptorParser;
+import com.redhat.thermostat.storage.internal.statement.SuffixExpression;
+import com.redhat.thermostat.storage.internal.statement.TerminalNode;
+import com.redhat.thermostat.storage.internal.statement.UnfinishedLimitValue;
+import com.redhat.thermostat.storage.internal.statement.UnfinishedSortKey;
+import com.redhat.thermostat.storage.internal.statement.UnfinishedValueNode;
+import com.redhat.thermostat.storage.internal.statement.WhereExpression;
+import com.redhat.thermostat.storage.query.BinaryComparisonOperator;
+import com.redhat.thermostat.storage.query.BinaryLogicalOperator;
+
+public class StatementDescriptorParserTest {
+
+    private Storage storage;
+    @SuppressWarnings("rawtypes")
+    private Query mockQuery;
+    private StatementDescriptorParser parser;
+    
+    @SuppressWarnings("unchecked")
+    @Before
+    public void setup() {
+        storage = mock(Storage.class);
+        mockQuery = mock(Query.class);
+        when(storage.createQuery(any(AgentInfoDAO.CATEGORY.getClass()))).thenReturn(mockQuery);
+    }
+    
+    @After
+    public void teardown() {
+        storage = null;
+        mockQuery = null;
+    }
+    
+    @Test
+    public void testParseQuerySimple() throws DescriptorParsingException {
+        String descrString = "QUERY " + AgentInfoDAO.CATEGORY.getName();
+        StatementDescriptor desc = getDescriptor(descrString, AgentInfoDAO.CATEGORY);
+        parser = new StatementDescriptorParser(storage, desc);
+        ParsedStatement statement = (ParsedStatement)parser.parse();
+        assertEquals(0, statement.getNumParams());
+        assertEquals(mockQuery.getClass().getName(), statement.getRawStatement().getClass().getName());
+        SuffixExpression tree = statement.getSuffixExpression();
+        assertNull(tree.getLimitExpn());
+        assertNull(tree.getSortExpn());
+        assertNull(tree.getWhereExpn());
+    }
+    
+    @Test
+    public void testParseQuerySimpleWithLimit() throws DescriptorParsingException {
+        String descrString = "QUERY " + AgentInfoDAO.CATEGORY.getName() + " LIMIT ?i";
+        StatementDescriptor desc = getDescriptor(descrString, AgentInfoDAO.CATEGORY);
+        parser = new StatementDescriptorParser(storage, desc);
+        ParsedStatement statement = (ParsedStatement)parser.parse();
+        assertEquals(1, statement.getNumParams());
+        assertEquals(mockQuery.getClass().getName(), statement.getRawStatement().getClass().getName());
+        SuffixExpression expn = statement.getSuffixExpression();
+        assertNotNull(expn.getLimitExpn());
+        assertNull(expn.getSortExpn());
+        assertNull(expn.getWhereExpn());
+        LimitExpression limitExp = expn.getLimitExpn();
+        assertTrue(limitExp.getValue() instanceof UnfinishedLimitValue);
+        UnfinishedLimitValue value = (UnfinishedLimitValue)limitExp.getValue();
+        assertEquals(0, value.getParameterIndex());
+    }
+    
+    @Test
+    public void testParseSortMultiple() throws DescriptorParsingException {
+        String descrString = "QUERY " + AgentInfoDAO.CATEGORY.getName() + " SORT 'a' ASC , 'b' DSC , 'c' ASC";
+        StatementDescriptor desc = getDescriptor(descrString, AgentInfoDAO.CATEGORY);
+        parser = new StatementDescriptorParser(storage, desc);
+        ParsedStatement statement = (ParsedStatement)parser.parse();
+        assertEquals(0, statement.getNumParams());
+        assertEquals(mockQuery.getClass().getName(), statement.getRawStatement().getClass().getName());
+        SuffixExpression suffixExpn = statement.getSuffixExpression();
+        SortExpression sortExpn = suffixExpn.getSortExpn();
+        assertNotNull(sortExpn);
+        assertNull(suffixExpn.getLimitExpn());
+        assertNull(suffixExpn.getWhereExpn());
+        List<SortMember> list = sortExpn.getMembers();
+        assertNotNull(list);
+        assertEquals(3, list.size());
+        assertEquals(SortDirection.ASCENDING, list.get(0).getDirection());
+        assertEquals(SortDirection.DESCENDING, list.get(1).getDirection());
+        assertEquals(SortDirection.ASCENDING, list.get(2).getDirection());
+        assertEquals("a", list.get(0).getSortKey());
+        assertEquals("b", list.get(1).getSortKey());
+        assertEquals("c", list.get(2).getSortKey());
+    }
+    
+    @SuppressWarnings("rawtypes")
+    @Test
+    public void testParseQueryWhereAndSortMultiple() throws DescriptorParsingException {
+        String descrString = "QUERY " + AgentInfoDAO.CATEGORY.getName() + " WHERE 'a' < 'b' AND 'c' = ?s OR NOT 'x' >= ?i SORT 'a' ASC , 'b' DSC , 'c' ASC";
+        StatementDescriptor desc = getDescriptor(descrString, AgentInfoDAO.CATEGORY);
+        parser = new StatementDescriptorParser(storage, desc);
+        ParsedStatement statement = (ParsedStatement)parser.parse();
+        assertEquals(2, statement.getNumParams());
+        assertEquals(mockQuery.getClass().getName(), statement.getRawStatement().getClass().getName());
+        SuffixExpression suffixExpn = statement.getSuffixExpression();
+        assertNotNull(suffixExpn.getSortExpn());
+        assertNull(suffixExpn.getLimitExpn());
+        assertNotNull(suffixExpn.getWhereExpn());
+        List<SortMember> list = suffixExpn.getSortExpn().getMembers();
+        assertNotNull(list);
+        assertEquals(3, list.size());
+        assertEquals(SortDirection.ASCENDING, list.get(0).getDirection());
+        assertEquals(SortDirection.DESCENDING, list.get(1).getDirection());
+        assertEquals(SortDirection.ASCENDING, list.get(2).getDirection());
+        assertEquals("a", list.get(0).getSortKey());
+        assertEquals("b", list.get(1).getSortKey());
+        assertEquals("c", list.get(2).getSortKey());
+        // build the expected expression tree
+        WhereExpression where = new WhereExpression();
+        BinaryExpressionNode or = new BinaryExpressionNode(where.getRoot());
+        where.getRoot().setValue(or);
+        or.setOperator(BinaryLogicalOperator.OR);
+        BinaryExpressionNode and = new BinaryExpressionNode(or);
+        and.setOperator(BinaryLogicalOperator.AND);
+        or.setLeftChild(and);
+        NotBooleanExpressionNode not = new NotBooleanExpressionNode(or);
+        or.setRightChild(not);
+        BinaryExpressionNode unequality = new BinaryExpressionNode(and);
+        unequality.setOperator(BinaryComparisonOperator.LESS_THAN);
+        TerminalNode a = new TerminalNode(unequality);
+        Key aKey = new Key("a", false);
+        a.setValue(aKey);
+        unequality.setLeftChild(a);
+        TerminalNode b = new TerminalNode(unequality);
+        b.setValue("b");
+        unequality.setRightChild(b);
+        and.setLeftChild(unequality);
+        BinaryExpressionNode equality = new BinaryExpressionNode(and);
+        equality.setOperator(BinaryComparisonOperator.EQUALS);
+        TerminalNode c = new TerminalNode(equality);
+        Key cKey = new Key("c", false);
+        c.setValue(cKey);
+        equality.setLeftChild(c);
+        UnfinishedValueNode patch1 = new UnfinishedValueNode();
+        patch1.setParameterIndex(0);
+        patch1.setLHS(false);
+        patch1.setType(String.class);
+        TerminalNode d = new TerminalNode(equality);
+        d.setValue(patch1);
+        equality.setRightChild(d);
+        and.setRightChild(equality);
+        BinaryExpressionNode greaterEqual = new BinaryExpressionNode(not);
+        not.setValue(greaterEqual);
+        greaterEqual.setOperator(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL_TO);
+        TerminalNode x = new TerminalNode(greaterEqual);
+        Key xKey = new Key("x", false);
+        x.setValue(xKey);
+        greaterEqual.setLeftChild(x);
+        UnfinishedValueNode patch2 = new UnfinishedValueNode();
+        patch2.setParameterIndex(1);
+        patch2.setLHS(false);
+        patch2.setType(Integer.class);
+        TerminalNode y = new TerminalNode(greaterEqual);
+        y.setValue(patch2);
+        greaterEqual.setRightChild(y);
+        // finally assert equality
+        assertTrue( WhereExpressions.equals(where, suffixExpn.getWhereExpn()));
+    }
+    
+    @SuppressWarnings("rawtypes")
+    @Test
+    public void testParseQueryWhereOrSortMultiple() throws DescriptorParsingException {
+        String descrString = "QUERY " + AgentInfoDAO.CATEGORY.getName() + " WHERE 'a' < 'b' OR 'c' = ?s SORT 'a' ASC , ?s DSC , 'c' ASC";
+        StatementDescriptor desc = getDescriptor(descrString, AgentInfoDAO.CATEGORY);
+        parser = new StatementDescriptorParser(storage, desc);
+        ParsedStatement statement = (ParsedStatement)parser.parse();
+        assertEquals(2, statement.getNumParams());
+        assertEquals(mockQuery.getClass().getName(), statement.getRawStatement().getClass().getName());
+        SuffixExpression suffixExpn = statement.getSuffixExpression();
+        assertNotNull(suffixExpn.getSortExpn());
+        assertNull(suffixExpn.getLimitExpn());
+        assertNotNull(suffixExpn.getWhereExpn());
+        SortExpression sortExp = suffixExpn.getSortExpn();
+        List<SortMember> list = sortExp.getMembers();
+        assertNotNull(list);
+        assertEquals(3, list.size());
+        assertEquals(SortDirection.ASCENDING, list.get(0).getDirection());
+        assertEquals(SortDirection.DESCENDING, list.get(1).getDirection());
+        assertEquals(SortDirection.ASCENDING, list.get(2).getDirection());
+        assertEquals("a", list.get(0).getSortKey());
+        UnfinishedSortKey unfinished = new UnfinishedSortKey();
+        unfinished.setParameterIndex(1);
+        assertEquals(unfinished, list.get(1).getSortKey());
+        assertEquals("c", list.get(2).getSortKey());
+        // build the expected expression tree
+        WhereExpression where = new WhereExpression();
+        BinaryExpressionNode or = new BinaryExpressionNode(where.getRoot());
+        where.getRoot().setValue(or);
+        or.setOperator(BinaryLogicalOperator.OR);
+        BinaryExpressionNode unequality = new BinaryExpressionNode(or);
+        unequality.setOperator(BinaryComparisonOperator.LESS_THAN);
+        TerminalNode a = new TerminalNode(unequality);
+        Key aKey = new Key("a", false);
+        a.setValue(aKey);
+        unequality.setLeftChild(a);
+        TerminalNode b = new TerminalNode(unequality);
+        b.setValue("b");
+        unequality.setRightChild(b);
+        or.setLeftChild(unequality);
+        BinaryExpressionNode equality = new BinaryExpressionNode(or);
+        equality.setOperator(BinaryComparisonOperator.EQUALS);
+        TerminalNode c = new TerminalNode(equality);
+        Key cKey = new Key("c", false);
+        c.setValue(cKey);
+        equality.setLeftChild(c);
+        UnfinishedValueNode patch1 = new UnfinishedValueNode();
+        patch1.setParameterIndex(0);
+        patch1.setLHS(false);
+        patch1.setType(String.class);
+        TerminalNode d = new TerminalNode(equality);
+        d.setValue(patch1);
+        equality.setRightChild(d);
+        or.setRightChild(equality);
+        assertTrue(WhereExpressions.equals(where, suffixExpn.getWhereExpn()));
+    }
+    
+    @Test
+    public void testParseQuerySimpleWhereAndSimpleSort() throws DescriptorParsingException {
+        String descrString = "QUERY " + AgentInfoDAO.CATEGORY.getName() + " WHERE 'a' < 'b' SORT 'a' DSC";
+        StatementDescriptor desc = getDescriptor(descrString, AgentInfoDAO.CATEGORY);
+        parser = new StatementDescriptorParser(storage, desc);
+        ParsedStatement statement = (ParsedStatement)parser.parse();
+        assertEquals(0, statement.getNumParams());
+        assertEquals(mockQuery.getClass().getName(), statement.getRawStatement().getClass().getName());
+        SuffixExpression suffixExpn = statement.getSuffixExpression();
+        assertNotNull(suffixExpn.getSortExpn());
+        assertNull(suffixExpn.getLimitExpn());
+        assertNotNull(suffixExpn.getWhereExpn());
+        SortExpression sortExp = suffixExpn.getSortExpn();
+        List<SortMember> list = sortExp.getMembers();
+        assertNotNull(list);
+        assertEquals(1, list.size());
+        assertEquals(SortDirection.DESCENDING, list.get(0).getDirection());
+        assertEquals("a", list.get(0).getSortKey());
+        // build the expected expression tree
+        WhereExpression where = new WhereExpression();
+        BinaryExpressionNode unequality = new BinaryExpressionNode(where.getRoot());
+        where.getRoot().setValue(unequality);
+        unequality.setOperator(BinaryComparisonOperator.LESS_THAN);
+        TerminalNode a = new TerminalNode(unequality);
+        @SuppressWarnings("rawtypes")
+        Key aKey = new Key("a", false);
+        a.setValue(aKey);
+        unequality.setLeftChild(a);
+        TerminalNode b = new TerminalNode(unequality);
+        b.setValue("b");
+        unequality.setRightChild(b);
+        assertTrue(WhereExpressions.equals(where, suffixExpn.getWhereExpn()));
+    }
+    
+    @Test
+    public void testParseQuerySimpleWithOneWhere() {
+        String descString = "QUERY " + AgentInfoDAO.CATEGORY.getName() + " WHERE '" + Key.AGENT_ID.getName() + "' = ?s";
+        StatementDescriptor desc = getDescriptor(descString, AgentInfoDAO.CATEGORY);
+        parser = new StatementDescriptorParser(storage, desc);
+        ParsedStatement statement = null; 
+        try {
+            statement = (ParsedStatement)parser.parse();
+        } catch (DescriptorParsingException e) {
+            fail(e.getMessage());
+        }
+        assertEquals(1, statement.getNumParams());
+        assertEquals(mockQuery.getClass().getName(), statement.getRawStatement().getClass().getName());
+        SuffixExpression suffixExpn = statement.getSuffixExpression();
+        assertNull(suffixExpn.getSortExpn());
+        assertNull(suffixExpn.getLimitExpn());
+        assertNotNull(suffixExpn.getWhereExpn());
+        // build the expected expression tree
+        WhereExpression where = new WhereExpression();
+        BinaryExpressionNode equality = new BinaryExpressionNode(where.getRoot());
+        where.getRoot().setValue(equality);
+        equality.setOperator(BinaryComparisonOperator.EQUALS);
+        TerminalNode a = new TerminalNode(equality);
+        @SuppressWarnings("rawtypes")
+        Key aKey = new Key(Key.AGENT_ID.getName(), false);
+        a.setValue(aKey);
+        equality.setLeftChild(a);
+        TerminalNode b = new TerminalNode(equality);
+        UnfinishedValueNode node = new UnfinishedValueNode();
+        node.setParameterIndex(0);
+        node.setLHS(false);
+        node.setType(String.class);
+        b.setValue(node);
+        equality.setRightChild(b);
+        assertTrue(WhereExpressions.equals(where, suffixExpn.getWhereExpn()));
+    }
+
+    @SuppressWarnings("rawtypes")
+    @Test
+    public void testParseSimpleWithAndOr() {
+        String descString = "QUERY " + AgentInfoDAO.CATEGORY.getName() + " WHERE '" + Key.AGENT_ID.getName() + "' = ?s" +
+                            " AND ?s < ?b OR 'a' = 'b'";
+        StatementDescriptor desc = getDescriptor(descString, AgentInfoDAO.CATEGORY);
+        parser = new StatementDescriptorParser(storage, desc);
+        ParsedStatement statement = null; 
+        try {
+            statement = (ParsedStatement)parser.parse();
+        } catch (DescriptorParsingException e) {
+            e.printStackTrace();
+            fail(e.getMessage());
+        }
+        assertEquals(3, statement.getNumParams());
+        assertEquals(mockQuery.getClass().getName(), statement.getRawStatement().getClass().getName());
+        SuffixExpression suffixExpn = statement.getSuffixExpression();
+        assertNull(suffixExpn.getSortExpn());
+        assertNull(suffixExpn.getLimitExpn());
+        assertNotNull(suffixExpn.getWhereExpn());
+        // build the expected expression tree
+        WhereExpression where = new WhereExpression();
+        BinaryExpressionNode or = new BinaryExpressionNode(where.getRoot());
+        where.getRoot().setValue(or);
+        or.setOperator(BinaryLogicalOperator.OR);
+        BinaryExpressionNode and = new BinaryExpressionNode(or);
+        and.setOperator(BinaryLogicalOperator.AND);
+        or.setLeftChild(and);
+        BinaryExpressionNode equality = new BinaryExpressionNode(and);
+        equality.setOperator(BinaryComparisonOperator.EQUALS);
+        TerminalNode a = new TerminalNode(equality);
+        Key aKey = new Key("a", false);
+        a.setValue(aKey);
+        equality.setLeftChild(a);
+        TerminalNode b = new TerminalNode(equality);
+        b.setValue("b");
+        equality.setRightChild(b);
+        or.setRightChild(equality);
+        BinaryExpressionNode equality2 = new BinaryExpressionNode(and);
+        equality2.setOperator(BinaryComparisonOperator.EQUALS);
+        TerminalNode c = new TerminalNode(equality2);
+        Key cKey = new Key(Key.AGENT_ID.getName(), false);
+        c.setValue(cKey);
+        equality2.setLeftChild(c);
+        UnfinishedValueNode patch1 = new UnfinishedValueNode();
+        patch1.setParameterIndex(0);
+        patch1.setLHS(false);
+        patch1.setType(String.class);
+        TerminalNode d = new TerminalNode(equality2);
+        d.setValue(patch1);
+        equality2.setRightChild(d);
+        and.setLeftChild(equality2);
+        BinaryExpressionNode lessThan = new BinaryExpressionNode(and);
+        lessThan.setOperator(BinaryComparisonOperator.LESS_THAN);
+        UnfinishedValueNode patch = new UnfinishedValueNode();
+        patch.setParameterIndex(1);
+        patch.setType(String.class);
+        patch.setLHS(true);
+        TerminalNode x = new TerminalNode(lessThan);
+        x.setValue(patch);
+        lessThan.setLeftChild(x);
+        UnfinishedValueNode patch2 = new UnfinishedValueNode();
+        patch2.setLHS(false);
+        patch2.setParameterIndex(2);
+        patch2.setType(boolean.class);
+        TerminalNode y = new TerminalNode(lessThan);
+        y.setValue(patch2);
+        lessThan.setRightChild(y);
+        and.setRightChild(lessThan);
+        assertTrue(WhereExpressions.equals(where, suffixExpn.getWhereExpn()));
+    }
+
+    @Test
+    public void testParseSimpleWithAnd() {
+        String descString = "QUERY " + AgentInfoDAO.CATEGORY.getName() + " WHERE 'a' = ?s AND ?s = 'd'";
+        StatementDescriptor desc = getDescriptor(descString, AgentInfoDAO.CATEGORY);
+        parser = new StatementDescriptorParser(storage, desc);
+        ParsedStatement statement = null; 
+        try {
+            statement = (ParsedStatement)parser.parse();
+        } catch (DescriptorParsingException e) {
+            e.printStackTrace();
+            fail(e.getMessage());
+        }
+        assertEquals(2, statement.getNumParams());
+        assertEquals(mockQuery.getClass().getName(), statement.getRawStatement().getClass().getName());
+        SuffixExpression suffixExpn = statement.getSuffixExpression();
+        assertNull(suffixExpn.getSortExpn());
+        assertNull(suffixExpn.getLimitExpn());
+        assertNotNull(suffixExpn.getWhereExpn());
+        // build the expected expression tree
+        WhereExpression where = new WhereExpression();
+        BinaryExpressionNode and = new BinaryExpressionNode(where.getRoot());
+        where.getRoot().setValue(and);
+        and.setOperator(BinaryLogicalOperator.AND);
+        BinaryExpressionNode equality = new BinaryExpressionNode(and);
+        equality.setOperator(BinaryComparisonOperator.EQUALS);
+        TerminalNode a = new TerminalNode(equality);
+        @SuppressWarnings("rawtypes")
+        Key aKey = new Key("a", false);
+        a.setValue(aKey);
+        equality.setLeftChild(a);
+        UnfinishedValueNode unfinished = new UnfinishedValueNode();
+        unfinished.setParameterIndex(0);
+        unfinished.setLHS(false);
+        unfinished.setType(String.class);
+        TerminalNode b = new TerminalNode(equality);
+        b.setValue(unfinished);
+        equality.setRightChild(b);
+        and.setLeftChild(equality);
+        BinaryExpressionNode equality2 = new BinaryExpressionNode(and);
+        equality2.setOperator(BinaryComparisonOperator.EQUALS);
+        TerminalNode c = new TerminalNode(equality2);
+        UnfinishedValueNode patch1 = new UnfinishedValueNode();
+        patch1.setParameterIndex(1);
+        patch1.setType(String.class);
+        patch1.setLHS(true);
+        c.setValue(patch1);
+        equality2.setLeftChild(c);
+        TerminalNode d = new TerminalNode(equality2);
+        d.setValue("d");
+        equality2.setRightChild(d);
+        and.setRightChild(equality2);
+        assertTrue(WhereExpressions.equals(where, suffixExpn.getWhereExpn()));
+    }
+
+    @Test
+    public void testParseSimpleWithNotOR() {
+        String descString = "QUERY " + AgentInfoDAO.CATEGORY.getName() + " WHERE NOT 'a' = ?s OR ?s = 'd'";
+        StatementDescriptor desc = getDescriptor(descString, AgentInfoDAO.CATEGORY);
+        parser = new StatementDescriptorParser(storage, desc);
+        ParsedStatement statement = null; 
+        try {
+            statement = (ParsedStatement)parser.parse();
+        } catch (DescriptorParsingException e) {
+            e.printStackTrace();
+            fail(e.getMessage());
+        }
+        assertEquals(2, statement.getNumParams());
+        assertEquals(mockQuery.getClass().getName(), statement.getRawStatement().getClass().getName());
+        SuffixExpression suffixExpn = statement.getSuffixExpression();
+        assertNull(suffixExpn.getSortExpn());
+        assertNull(suffixExpn.getLimitExpn());
+        assertNotNull(suffixExpn.getWhereExpn());
+        // build the expected parse tree
+        WhereExpression expn = new WhereExpression();
+        BinaryExpressionNode or = new BinaryExpressionNode(expn.getRoot());
+        expn.getRoot().setValue(or);
+        or.setOperator(BinaryLogicalOperator.OR);
+        NotBooleanExpressionNode notNode = new NotBooleanExpressionNode(or);
+        BinaryExpressionNode comparison = new BinaryExpressionNode(notNode);
+        notNode.setValue(comparison);
+        or.setLeftChild(notNode);
+        comparison.setOperator(BinaryComparisonOperator.EQUALS);
+        TerminalNode rightCompTerm = new TerminalNode(comparison);
+        TerminalNode leftCompTerm = new TerminalNode(comparison);
+        @SuppressWarnings("rawtypes")
+        Key aKey = new Key("a", false);
+        leftCompTerm.setValue(aKey);
+        UnfinishedValueNode patch1 = new UnfinishedValueNode();
+        patch1.setParameterIndex(0);
+        patch1.setType(String.class);
+        patch1.setLHS(false);
+        rightCompTerm.setValue(patch1);
+        comparison.setLeftChild(leftCompTerm);
+        comparison.setRightChild(rightCompTerm);
+        BinaryExpressionNode otherComparison = new BinaryExpressionNode(or);
+        otherComparison.setOperator(BinaryComparisonOperator.EQUALS);
+        TerminalNode leftUnfinished = new TerminalNode(otherComparison);
+        UnfinishedValueNode patch2 = new UnfinishedValueNode();
+        patch2.setParameterIndex(1);
+        patch2.setLHS(true);
+        patch2.setType(String.class);
+        leftUnfinished.setValue(patch2);
+        TerminalNode other = new TerminalNode(otherComparison);
+        other.setValue("d");
+        otherComparison.setLeftChild(leftUnfinished);
+        otherComparison.setRightChild(other);
+        or.setRightChild(otherComparison);
+        assertTrue(WhereExpressions.equals(expn, suffixExpn.getWhereExpn()));
+    }
+
+    @Test
+    public void testParseSimpleWithOr() {
+        String descString = "QUERY " + AgentInfoDAO.CATEGORY.getName() + " WHERE 'a' = ?s OR ?s = 'd'";
+        StatementDescriptor desc = getDescriptor(descString, AgentInfoDAO.CATEGORY);
+        parser = new StatementDescriptorParser(storage, desc);
+        ParsedStatement statement = null; 
+        try {
+            statement = (ParsedStatement)parser.parse();
+        } catch (DescriptorParsingException e) {
+            e.printStackTrace();
+            fail(e.getMessage());
+        }
+        assertEquals(2, statement.getNumParams());
+        assertEquals(mockQuery.getClass().getName(), statement.getRawStatement().getClass().getName());
+        SuffixExpression suffixExpn = statement.getSuffixExpression();
+        assertNull(suffixExpn.getSortExpn());
+        assertNull(suffixExpn.getLimitExpn());
+        assertNotNull(suffixExpn.getWhereExpn());
+        // build the expected parse tree
+        WhereExpression where = new WhereExpression();
+        BinaryExpressionNode and = new BinaryExpressionNode(where.getRoot());
+        where.getRoot().setValue(and);
+        and.setOperator(BinaryLogicalOperator.OR);
+        BinaryExpressionNode equality = new BinaryExpressionNode(and);
+        equality.setOperator(BinaryComparisonOperator.EQUALS);
+        TerminalNode a = new TerminalNode(equality);
+        @SuppressWarnings("rawtypes")
+        Key aKey = new Key("a", false);
+        a.setValue(aKey);
+        equality.setLeftChild(a);
+        UnfinishedValueNode unfinished = new UnfinishedValueNode();
+        unfinished.setParameterIndex(0);
+        unfinished.setType(String.class);
+        unfinished.setLHS(false);
+        TerminalNode b = new TerminalNode(equality);
+        b.setValue(unfinished);
+        equality.setRightChild(b);
+        and.setLeftChild(equality);
+        BinaryExpressionNode equality2 = new BinaryExpressionNode(and);
+        equality2.setOperator(BinaryComparisonOperator.EQUALS);
+        TerminalNode c = new TerminalNode(equality2);
+        UnfinishedValueNode patch1 = new UnfinishedValueNode();
+        patch1.setParameterIndex(1);
+        patch1.setType(String.class);
+        patch1.setLHS(true);
+        c.setValue(patch1);
+        equality2.setLeftChild(c);
+        TerminalNode d = new TerminalNode(equality2);
+        d.setValue("d");
+        equality2.setRightChild(d);
+        and.setRightChild(equality2);
+        assertTrue(WhereExpressions.equals(where, suffixExpn.getWhereExpn()));
+    }
+    
+    @Test
+    public void testParseSimpleWithLimit() {
+        String descString = "QUERY " + AgentInfoDAO.CATEGORY.getName() + " LIMIT 1";
+        StatementDescriptor desc = getDescriptor(descString, AgentInfoDAO.CATEGORY);
+        parser = new StatementDescriptorParser(storage, desc);
+        ParsedStatement statement = null; 
+        try {
+            statement = (ParsedStatement)parser.parse();
+        } catch (DescriptorParsingException e) {
+            e.printStackTrace();
+            fail(e.getMessage());
+        }
+        assertEquals(0, statement.getNumParams());
+        assertEquals(mockQuery.getClass().getName(), statement.getRawStatement().getClass().getName());
+        SuffixExpression suffixExpn = statement.getSuffixExpression();
+        assertNull(suffixExpn.getSortExpn());
+        assertNull(suffixExpn.getWhereExpn());
+        assertNotNull(suffixExpn.getLimitExpn());
+        assertEquals(1, suffixExpn.getLimitExpn().getValue());
+    }
+    
+    @Test
+    public void rejectLimitWhichIsNotInt() {
+        String descString = "QUERY " + AgentInfoDAO.CATEGORY.getName() + " LIMIT illegal";
+        StatementDescriptor desc = getDescriptor(descString, AgentInfoDAO.CATEGORY);
+        parser = new StatementDescriptorParser(storage, desc);
+        try {
+            parser.parse();
+        } catch (DescriptorParsingException e) {
+            assertEquals("Invalid limit expression. 'illegal' not an integer", e.getMessage());
+        }
+    }
+
+    @Test
+    public void rejectLHSnotString() throws DescriptorParsingException {
+        String descrString = "QUERY " + AgentInfoDAO.CATEGORY.getName() + " WHERE a < 1";
+        StatementDescriptor desc = getDescriptor(descrString, AgentInfoDAO.CATEGORY);
+        parser = new StatementDescriptorParser(storage, desc);
+        try {
+            parser.parse();
+        } catch (DescriptorParsingException e) {
+            // pass
+            assertTrue(e.getMessage().contains("Expected string value. Got term ->a<-"));
+        }
+    }
+    
+    @Test
+    public void rejectIllegalFreeParamType() throws DescriptorParsingException {
+        // ? should be one of '?i', '?s', '?b', '?s['
+        String descrString = "QUERY " + AgentInfoDAO.CATEGORY.getName() + " WHERE ? < 1";
+        StatementDescriptor desc = getDescriptor(descrString, AgentInfoDAO.CATEGORY);
+        parser = new StatementDescriptorParser(storage, desc);
+        try {
+            parser.parse();
+        } catch (DescriptorParsingException e) {
+            // pass
+            assertEquals("Unknown type of free parameter: '?'", e.getMessage());
+        }
+    }
+    
+    @Test
+    public void rejectParseQueryWhereAndSortMultipleIllegalSortModifier() throws DescriptorParsingException {
+        String descrString = "QUERY " + AgentInfoDAO.CATEGORY.getName() + " WHERE 'somekey' < 2 AND 'c' = ?s OR 'a' >= ?i SORT 'a' ASC , 'b' ILLEGAL , 'c' ASC";
+        StatementDescriptor desc = getDescriptor(descrString, AgentInfoDAO.CATEGORY);
+        parser = new StatementDescriptorParser(storage, desc);
+        try {
+            parser.parse();
+        } catch (DescriptorParsingException e) {
+            // pass
+            assertTrue(e.getMessage().contains("Expected ASC or DSC"));
+        }
+    }
+
+    @Test
+    public void rejectParseQueryWhereBoolTerm() throws DescriptorParsingException {
+        String descrString = "QUERY " + AgentInfoDAO.CATEGORY.getName() + " WHERE true AND false";
+        StatementDescriptor desc = getDescriptor(descrString, AgentInfoDAO.CATEGORY);
+        parser = new StatementDescriptorParser(storage, desc);
+        try {
+            parser.parse();
+        } catch (DescriptorParsingException e) {
+            // pass
+        }
+    }
+
+    @Test
+    public void rejectSimpleQueryWithMissingSpaces() throws DescriptorParsingException {
+        // we require a space before every operator/keyword
+        String descrString = "QUERY " + AgentInfoDAO.CATEGORY + " WHERE " + "'a'='b'";
+        StatementDescriptor desc = getDescriptor(descrString, AgentInfoDAO.CATEGORY);
+        parser = new StatementDescriptorParser(storage, desc);
+        try {
+            parser.parse();
+        } catch (DescriptorParsingException e) {
+            // pass
+        }
+    }
+    
+    @Test
+    public void rejectSimpleQueryWithMissingSpaces2() throws DescriptorParsingException {
+        // we require a space before every operator/keyword
+        String descrString = "QUERY " + AgentInfoDAO.CATEGORY + " WHERE " + "'a' ='b'";
+        StatementDescriptor desc = getDescriptor(descrString, AgentInfoDAO.CATEGORY);
+        parser = new StatementDescriptorParser(storage, desc);
+        try {
+            parser.parse();
+        } catch (DescriptorParsingException e) {
+            // pass
+        }
+    }
+    
+    @Test
+    public void rejectSimpleQueryWithInvalidComparison() throws DescriptorParsingException {
+        // <> is illegal
+        String descrString = "QUERY " + AgentInfoDAO.CATEGORY + " WHERE " + "'a' <> 'b'";
+        StatementDescriptor desc = getDescriptor(descrString, AgentInfoDAO.CATEGORY);
+        parser = new StatementDescriptorParser(storage, desc);
+        try {
+            parser.parse();
+        } catch (DescriptorParsingException e) {
+            // pass
+        }
+    }
+    
+    @Test
+    public void rejectInvalidDescriptorStringBadWhere() throws DescriptorParsingException {
+        String descString = "QUERY " + AgentInfoDAO.CATEGORY.getName() + " where '" + Key.AGENT_ID.getName() + "'= ?s";
+        StatementDescriptor desc = getDescriptor(descString, AgentInfoDAO.CATEGORY);
+        parser = new StatementDescriptorParser(storage, desc);
+        try {
+            parser.parse();
+            fail("lower case where not allowed in descriptor. Should have rejected.");
+        } catch (DescriptorParsingException e) {
+            // pass
+            assertTrue(e.getMessage().contains("Unexpected token: 'where'"));
+        }
+    }
+    
+    @Test
+    public void rejectInvalidDescriptorStringBadStatementType() throws DescriptorParsingException {
+        String descString = "UNKNOWN some-unknown-category WHERE 1 = ?i";
+        StatementDescriptor desc = getDescriptor(descString, AgentInfoDAO.CATEGORY);
+        parser = new StatementDescriptorParser(storage, desc);
+        try {
+            parser.parse();
+            fail("UNKNOWN not a valid statement type");
+        } catch (DescriptorParsingException e) {
+            // pass
+            assertTrue(e.getMessage().contains("Unknown statement type"));
+        }
+    }
+    
+    @Test
+    public void rejectInvalidDescriptorStringCategoryMismatch() throws DescriptorParsingException {
+        String descString = "QUERY some-unknown-category WHERE 1 = ?i";
+        StatementDescriptor desc = getDescriptor(descString, AgentInfoDAO.CATEGORY);
+        parser = new StatementDescriptorParser(storage, desc);
+        try {
+            parser.parse();
+            fail("category names in descriptor and Category object did not match!");
+        } catch (DescriptorParsingException e) {
+            // pass
+            assertTrue(e.getMessage().contains("Category mismatch"));
+        }
+    }
+    
+    @Test
+    public void rejectInvalidDescriptorStringBadSortNoArg() throws DescriptorParsingException {
+        String descString = "QUERY " + AgentInfoDAO.CATEGORY.getName() + " WHERE 'a' = ?i SORT";
+        StatementDescriptor desc = getDescriptor(descString, AgentInfoDAO.CATEGORY);
+        parser = new StatementDescriptorParser(storage, desc);
+        try {
+            parser.parse();
+            fail("category names in descriptor and Category object did not match!");
+        } catch (DescriptorParsingException e) {
+            // pass
+            assertTrue(e.getMessage().contains("Invalid where clause"));
+        }
+    }
+    
+    @Test
+    public void rejectInvalidDescriptorStringBadWhereNoArg() throws DescriptorParsingException {
+        String descString = "QUERY " + AgentInfoDAO.CATEGORY.getName() + " WHERE SORT";
+        StatementDescriptor desc = getDescriptor(descString, AgentInfoDAO.CATEGORY);
+        parser = new StatementDescriptorParser(storage, desc);
+        try {
+            parser.parse();
+            fail("category names in descriptor and Category object did not match!");
+        } catch (DescriptorParsingException e) {
+            // pass
+            assertTrue(e.getMessage().contains("SORT"));
+            assertTrue(e.getMessage().contains("Expected string value"));
+        }
+    }
+    
+    private StatementDescriptor getDescriptor(final String desc, final Category<?> category) {
+        return new StatementDescriptor() {
+            
+            @Override
+            public String getQueryDescriptor() {
+                return desc;
+            }
+            
+            @Override
+            public Category<?> getCategory() {
+                return category;
+            }
+        };
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/test/java/com/redhat/thermostat/storage/internal/statement/WhereExpressions.java	Fri Jun 28 16:46:25 2013 +0200
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat 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 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat 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 Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.storage.internal.statement;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import com.redhat.thermostat.storage.internal.statement.BinaryExpressionNode;
+import com.redhat.thermostat.storage.internal.statement.Node;
+import com.redhat.thermostat.storage.internal.statement.NotBooleanExpressionNode;
+import com.redhat.thermostat.storage.internal.statement.TerminalNode;
+import com.redhat.thermostat.storage.internal.statement.WhereExpression;
+
+/**
+ * Helper class for comparing where expression trees.
+ *
+ * @see StatementDescriptorParserTest
+ */
+public class WhereExpressions {
+
+    /**
+     * Compares two where expression parse trees.
+     * 
+     * @param a
+     * @param b
+     * @return true, if all nodes/values were equal. false otherwise.
+     */
+    public static boolean equals(WhereExpression a, WhereExpression b) {
+        Node aRoot = a.getRoot();
+        Node bRoot = b.getRoot();
+        return recEquals(aRoot, bRoot);
+    }
+    
+    private static boolean recEquals(Node a, Node b) {
+        boolean retval = Objects.equals(a, b);
+        if (!retval) {
+            return false;
+        }
+        Node[] nextNodesA = getNextNodes(a);
+        Node[] nextNodesB = getNextNodes(b);
+        if (nextNodesA != null && nextNodesB != null) {
+            retval = retval && (nextNodesA.length == nextNodesB.length);
+            if (!retval) {
+                return false;
+            }
+            // verify nodes at this level are equal
+            for (int i = 0; i < nextNodesA.length; i++) {
+                retval = retval && Objects.equals(nextNodesA[i], nextNodesB[i]);
+                if (!retval) {
+                    return false;
+                }
+            }
+            // recursively check child nodes
+            for (int i = 0; i < nextNodesA.length; i++) {
+                retval = retval && recEquals(nextNodesA[i], nextNodesB[i]);
+                if (!retval) {
+                    return false;
+                }
+            }
+            return retval;
+        }
+        if (nextNodesA != null || nextNodesB != null) {
+            return false;
+        }
+        // all pass
+        return true;
+    }
+
+    private static Node[] getNextNodes(Node node) {
+        if (node instanceof TerminalNode) {
+            return null;
+        } else if (node instanceof BinaryExpressionNode) {
+            List<Node> nodes = new ArrayList<>(2);
+            BinaryExpressionNode binNode = (BinaryExpressionNode)node;
+            if (binNode.getLeftChild() != null) {
+                nodes.add(binNode.getLeftChild());
+            }
+            if (binNode.getRightChild() != null) {
+                nodes.add(binNode.getRightChild());
+            }
+            if (nodes.size() == 0) {
+                return null;
+            }
+            return nodes.toArray(new Node[0]);
+        } else if (node instanceof NotBooleanExpressionNode || node instanceof Node) {
+            Node next = (Node)node.getValue();
+            if (next == null) {
+                return null;
+            }
+            return new Node[] { next };
+        } else {
+            return null;
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/storage/core/src/test/java/com/redhat/thermostat/storage/internal/statement/WhereExpressionsTest.java	Fri Jun 28 16:46:25 2013 +0200
@@ -0,0 +1,283 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat 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 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat 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 Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.storage.internal.statement;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import com.redhat.thermostat.storage.internal.statement.BinaryExpressionNode;
+import com.redhat.thermostat.storage.internal.statement.NotBooleanExpressionNode;
+import com.redhat.thermostat.storage.internal.statement.TerminalNode;
+import com.redhat.thermostat.storage.internal.statement.WhereExpression;
+
+public class WhereExpressionsTest {
+
+    @Test
+    public void testEqualsSelf() {
+        WhereExpression expn1 = new WhereExpression();
+        assertTrue(WhereExpressions.equals(expn1, expn1));
+    }
+    
+    @Test
+    public void testEqualsEmpty() {
+        WhereExpression expn1 = new WhereExpression();
+        WhereExpression expn2 = new WhereExpression();
+        assertTrue(WhereExpressions.equals(expn1, expn2));
+    }
+    
+    @Test
+    public void testEqualsSimpleTerminal() {
+        WhereExpression expn1 = new WhereExpression();
+        WhereExpression expn2 = new WhereExpression();
+        TerminalNode node = new TerminalNode(expn1.getRoot());
+        node.setValue("testing");
+        TerminalNode node2 = new TerminalNode(expn2.getRoot());
+        node2.setValue("testing");
+        expn1.getRoot().setValue(node);
+        expn2.getRoot().setValue(node2);
+        assertTrue(WhereExpressions.equals(expn1, expn2));
+    }
+    
+    @Test
+    public void testEqualsSimpleTerminalWithDifferentValues() {
+        WhereExpression expn1 = new WhereExpression();
+        WhereExpression expn2 = new WhereExpression();
+        TerminalNode node = new TerminalNode(expn1.getRoot());
+        node.setValue("test");
+        TerminalNode node2 = new TerminalNode(expn2.getRoot());
+        node2.setValue("other");
+        expn1.getRoot().setValue(node);
+        expn2.getRoot().setValue(node2);
+        assertFalse(WhereExpressions.equals(expn1, expn2));
+    }
+    
+    @Test
+    public void testEqualsComplexish() {
+        WhereExpression expn1 = new WhereExpression();
+        BinaryExpressionNode binNode1 = new BinaryExpressionNode(expn1.getRoot());
+        expn1.getRoot().setValue(binNode1);
+        binNode1.setOperator("OR");
+        NotBooleanExpressionNode notNode1 = new NotBooleanExpressionNode(binNode1);
+        TerminalNode termNode1 = new TerminalNode(notNode1);
+        termNode1.setValue("testing");
+        notNode1.setValue(termNode1);
+        binNode1.setLeftChild(notNode1);
+        BinaryExpressionNode and1 = new BinaryExpressionNode(binNode1);
+        binNode1.setRightChild(and1);
+        and1.setOperator("AND");
+        BinaryExpressionNode left1 = new BinaryExpressionNode(and1);
+        left1.setOperator("=");
+        TerminalNode termNode1Equal = new TerminalNode(left1);
+        termNode1Equal.setValue("a");
+        TerminalNode termNode1EqualOther = new TerminalNode(left1);
+        termNode1EqualOther.setValue("b");
+        left1.setLeftChild(termNode1Equal);
+        left1.setRightChild(termNode1EqualOther);
+        and1.setLeftChild(left1);
+        BinaryExpressionNode right1 = new BinaryExpressionNode(and1);
+        right1.setOperator("<=");
+        TerminalNode termNode1LessEqual = new TerminalNode(right1);
+        termNode1LessEqual.setValue("x");
+        TerminalNode termNode1LessEqualOther = new TerminalNode(right1);
+        termNode1LessEqualOther.setValue("y");
+        right1.setLeftChild(termNode1LessEqual);
+        right1.setRightChild(termNode1LessEqualOther);
+        and1.setRightChild(right1);
+        
+        // now build the second and equal where expn
+        WhereExpression expn2 = new WhereExpression();
+        BinaryExpressionNode binNode2 = new BinaryExpressionNode(expn2.getRoot());
+        expn2.getRoot().setValue(binNode2);
+        binNode2.setOperator("OR");
+        NotBooleanExpressionNode notNode2 = new NotBooleanExpressionNode(binNode2);
+        TerminalNode termNode2 = new TerminalNode(notNode2);
+        termNode2.setValue("testing");
+        notNode2.setValue(termNode2);
+        binNode2.setLeftChild(notNode2);
+        BinaryExpressionNode and2 = new BinaryExpressionNode(binNode2);
+        binNode2.setRightChild(and2);
+        and2.setOperator("AND");
+        BinaryExpressionNode left2 = new BinaryExpressionNode(and2);
+        left2.setOperator("=");
+        TerminalNode termNode2Equal = new TerminalNode(left2);
+        termNode2Equal.setValue("a");
+        TerminalNode termNode2EqualOther = new TerminalNode(left2);
+        termNode2EqualOther.setValue("b");
+        left2.setLeftChild(termNode2Equal);
+        left2.setRightChild(termNode2EqualOther);
+        and2.setLeftChild(left2);
+        BinaryExpressionNode right2 = new BinaryExpressionNode(and2);
+        right2.setOperator("<=");
+        TerminalNode termNode2LessEqual = new TerminalNode(right2);
+        termNode2LessEqual.setValue("x");
+        TerminalNode termNode2LessEqualOther = new TerminalNode(right2);
+        termNode2LessEqualOther.setValue("y");
+        right2.setLeftChild(termNode2LessEqual);
+        right2.setRightChild(termNode2LessEqualOther);
+        and2.setRightChild(right2);
+        assertTrue(WhereExpressions.equals(expn1, expn2));
+    }
+    
+    @Test
+    public void testNotEqualsComplexish() {
+        WhereExpression expn1 = new WhereExpression();
+        BinaryExpressionNode binNode1 = new BinaryExpressionNode(expn1.getRoot());
+        expn1.getRoot().setValue(binNode1);
+        binNode1.setOperator("OR");
+        NotBooleanExpressionNode notNode1 = new NotBooleanExpressionNode(binNode1);
+        TerminalNode termNode1 = new TerminalNode(notNode1);
+        termNode1.setValue("testing");
+        notNode1.setValue(termNode1);
+        binNode1.setLeftChild(notNode1);
+        BinaryExpressionNode and1 = new BinaryExpressionNode(binNode1);
+        binNode1.setRightChild(and1);
+        and1.setOperator("AND");
+        BinaryExpressionNode left1 = new BinaryExpressionNode(and1);
+        left1.setOperator("=");
+        TerminalNode termNode1Equal = new TerminalNode(left1);
+        termNode1Equal.setValue("d"); // should be "a" to make it equal
+        TerminalNode termNode1EqualOther = new TerminalNode(left1);
+        termNode1EqualOther.setValue("b");
+        left1.setLeftChild(termNode1Equal);
+        left1.setRightChild(termNode1EqualOther);
+        and1.setLeftChild(left1);
+        BinaryExpressionNode right1 = new BinaryExpressionNode(and1);
+        right1.setOperator("<=");
+        TerminalNode termNode1LessEqual = new TerminalNode(right1);
+        termNode1LessEqual.setValue("x");
+        TerminalNode termNode1LessEqualOther = new TerminalNode(right1);
+        termNode1LessEqualOther.setValue("y");
+        right1.setLeftChild(termNode1LessEqual);
+        right1.setRightChild(termNode1LessEqualOther);
+        and1.setRightChild(right1);
+        
+        // now build the second and equal where expn
+        WhereExpression expn2 = new WhereExpression();
+        BinaryExpressionNode binNode2 = new BinaryExpressionNode(expn2.getRoot());
+        expn2.getRoot().setValue(binNode2);
+        binNode2.setOperator("OR");
+        NotBooleanExpressionNode notNode2 = new NotBooleanExpressionNode(binNode2);
+        TerminalNode termNode2 = new TerminalNode(notNode2);
+        termNode2.setValue("testing");
+        notNode2.setValue(termNode2);
+        binNode2.setLeftChild(notNode2);
+        BinaryExpressionNode and2 = new BinaryExpressionNode(binNode2);
+        binNode2.setRightChild(and2);
+        and2.setOperator("AND");
+        BinaryExpressionNode left2 = new BinaryExpressionNode(and2);
+        left2.setOperator("=");
+        TerminalNode termNode2Equal = new TerminalNode(left2);
+        termNode2Equal.setValue("a");
+        TerminalNode termNode2EqualOther = new TerminalNode(left2);
+        termNode2EqualOther.setValue("b");
+        left2.setLeftChild(termNode2Equal);
+        left2.setRightChild(termNode2EqualOther);
+        and2.setLeftChild(left2);
+        BinaryExpressionNode right2 = new BinaryExpressionNode(and2);
+        right2.setOperator("<=");
+        TerminalNode termNode2LessEqual = new TerminalNode(right2);
+        termNode2LessEqual.setValue("x");
+        TerminalNode termNode2LessEqualOther = new TerminalNode(right2);
+        termNode2LessEqualOther.setValue("y");
+        right2.setLeftChild(termNode2LessEqual);
+        right2.setRightChild(termNode2LessEqualOther);
+        and2.setRightChild(right2);
+        assertFalse(WhereExpressions.equals(expn1, expn2));
+    }
+    
+    @Test
+    public void testNotEqualsNotSameTreeAtAll() {
+        WhereExpression expn1 = new WhereExpression();
+        BinaryExpressionNode binNode1 = new BinaryExpressionNode(expn1.getRoot());
+        expn1.getRoot().setValue(binNode1);
+        binNode1.setOperator("OR");
+        NotBooleanExpressionNode notNode1 = new NotBooleanExpressionNode(binNode1);
+        TerminalNode termNode1 = new TerminalNode(notNode1);
+        termNode1.setValue("testing");
+        notNode1.setValue(termNode1);
+        binNode1.setLeftChild(notNode1);
+        BinaryExpressionNode and1 = new BinaryExpressionNode(binNode1);
+        binNode1.setRightChild(and1);
+        and1.setOperator("AND");
+        BinaryExpressionNode left1 = new BinaryExpressionNode(and1);
+        left1.setOperator("=");
+        TerminalNode termNode1Equal = new TerminalNode(left1);
+        termNode1Equal.setValue("d"); // should be "a" to make it equal
+        TerminalNode termNode1EqualOther = new TerminalNode(left1);
+        termNode1EqualOther.setValue("b");
+        left1.setLeftChild(termNode1Equal);
+        left1.setRightChild(termNode1EqualOther);
+        and1.setLeftChild(left1);
+        BinaryExpressionNode right1 = new BinaryExpressionNode(and1);
+        right1.setOperator("<=");
+        TerminalNode termNode1LessEqual = new TerminalNode(right1);
+        termNode1LessEqual.setValue("x");
+        TerminalNode termNode1LessEqualOther = new TerminalNode(right1);
+        termNode1LessEqualOther.setValue("y");
+        right1.setLeftChild(termNode1LessEqual);
+        right1.setRightChild(termNode1LessEqualOther);
+        and1.setRightChild(right1);
+        
+        // now build the second fairly different tree
+        WhereExpression expn2 = new WhereExpression();
+        NotBooleanExpressionNode notNode2 = new NotBooleanExpressionNode(expn2.getRoot());
+        BinaryExpressionNode and2 = new BinaryExpressionNode(notNode2);
+        notNode2.setValue(and2);
+        and2.setOperator("AND");
+        BinaryExpressionNode left2 = new BinaryExpressionNode(and2);
+        left2.setOperator("=");
+        TerminalNode termNode2Equal = new TerminalNode(left2);
+        termNode2Equal.setValue("a");
+        TerminalNode termNode2EqualOther = new TerminalNode(left2);
+        termNode2EqualOther.setValue("b");
+        left2.setLeftChild(termNode2Equal);
+        left2.setRightChild(termNode2EqualOther);
+        and2.setLeftChild(left2);
+        BinaryExpressionNode right2 = new BinaryExpressionNode(and2);
+        right2.setOperator("<=");
+        TerminalNode termNode2LessEqual = new TerminalNode(right2);
+        termNode2LessEqual.setValue("x");
+        TerminalNode termNode2LessEqualOther = new TerminalNode(right2);
+        termNode2LessEqualOther.setValue("y");
+        right2.setLeftChild(termNode2LessEqual);
+        right2.setRightChild(termNode2LessEqualOther);
+        and2.setRightChild(right2);
+        assertFalse(WhereExpressions.equals(expn1, expn2));
+    }
+}