Revert "Remove support for @Query ORDER BY ASC / DESC"

This reverts commit cf066607cacb6f1e182812e422b9115735a46e1d.

Change-Id: I24668bc0580625529a5326cbcde219db675f7b30
diff --git a/src/main/antlr/com/google/gwtorm/schema/Query.g b/src/main/antlr/com/google/gwtorm/schema/Query.g
index f3c5e8f..105d351 100644
--- a/src/main/antlr/com/google/gwtorm/schema/Query.g
+++ b/src/main/antlr/com/google/gwtorm/schema/Query.g
@@ -31,6 +31,8 @@
   ID;
   PLACEHOLDER;
   COMMA;
+  ASC;
+  DESC;
   LIMIT;
   CONSTANT_INTEGER;
   CONSTANT_STRING;
@@ -147,7 +149,17 @@
   ;
 
 orderBy
-  : ORDER^ BY! field (COMMA! field)*
+  : ORDER^ BY! fieldSort (COMMA! fieldSort)*
+  ;
+
+fieldSort
+  : field sortDirection^
+  | field -> ^(ASC field)
+  ;
+
+sortDirection
+  : ASC
+  | DESC
   ;
 
 limit
@@ -200,6 +212,8 @@
 ORDER: 'ORDER' ;
 BY:    'BY'    ;
 AND:   'AND'   ;
+ASC:   'ASC'   ;
+DESC:  'DESC'  ;
 LIMIT: 'LIMIT' ;
 TRUE:  'true'  ;
 FALSE: 'false' ;
diff --git a/src/main/java/com/google/gwtorm/client/Query.java b/src/main/java/com/google/gwtorm/client/Query.java
index f2d99bc..4a0ca9e 100644
--- a/src/main/java/com/google/gwtorm/client/Query.java
+++ b/src/main/java/com/google/gwtorm/client/Query.java
@@ -35,7 +35,7 @@
  *
  * <pre>
  * [WHERE &lt;condition&gt; [AND &lt;condition&gt; ...]]
- * [ORDER BY &lt;property&gt; [, &lt;property&gt; ...]]
+ * [ORDER BY &lt;property&gt; [ASC | DESC] [, &lt;property&gt; [ASC | DESC] ...]]
  * [LIMIT { &lt;count&gt; | ? }]
  *
  * &lt;condition&gt; := &lt;property&gt; { &lt; | &lt;= | &gt; | &gt;= | = } &lt;value&gt;
diff --git a/src/main/java/com/google/gwtorm/nosql/AccessGen.java b/src/main/java/com/google/gwtorm/nosql/AccessGen.java
index 5344e1e..2049c39 100644
--- a/src/main/java/com/google/gwtorm/nosql/AccessGen.java
+++ b/src/main/java/com/google/gwtorm/nosql/AccessGen.java
@@ -306,7 +306,7 @@
     final QueryCGS cgs =
         new QueryCGS(mv, new Type[] {argType}, pCols, new int[] {2}, 1);
     for (ColumnModel f : pCols) {
-      IndexFunctionGen.encodeField(f, mv, cgs);
+      IndexFunctionGen.encodeField(new QueryModel.OrderBy(f, false), mv, cgs);
     }
 
     mv.visitInsn(RETURN);
@@ -508,7 +508,7 @@
   private void encodeField(Tree node, MethodVisitor mv, QueryCGS cgs)
       throws OrmException {
     ColumnModel f = ((QueryParser.Column) node.getChild(0)).getField();
-    IndexFunctionGen.encodeField(f, mv, cgs);
+    IndexFunctionGen.encodeField(new QueryModel.OrderBy(f, false), mv, cgs);
   }
 
   private List<Tree> compareOpsOnly(Tree node) throws OrmException {
diff --git a/src/main/java/com/google/gwtorm/nosql/IndexFunctionGen.java b/src/main/java/com/google/gwtorm/nosql/IndexFunctionGen.java
index 15dfae4..ce69b99 100644
--- a/src/main/java/com/google/gwtorm/nosql/IndexFunctionGen.java
+++ b/src/main/java/com/google/gwtorm/nosql/IndexFunctionGen.java
@@ -31,6 +31,7 @@
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
@@ -45,7 +46,7 @@
 
   private final GeneratedClassLoader classLoader;
   private final QueryModel query;
-  private final List<ColumnModel> myFields;
+  private final List<QueryModel.OrderBy> myFields;
   private final Class<T> pojo;
   private final Type pojoType;
 
@@ -59,15 +60,16 @@
     classLoader = loader;
     query = qm;
 
-    myFields = new ArrayList<ColumnModel>();
+    myFields = new ArrayList<QueryModel.OrderBy>();
 
     // Only add each parameter column once, but in the order used.
     // This avoids a range test on the same column from duplicating
     // the data in the index record.
     //
     for (ColumnModel m : leaves(query.getParameters())) {
-      if (!myFields.contains(m)) {
-        myFields.add(m);
+      QueryModel.OrderBy o = new QueryModel.OrderBy(m, false);
+      if (!myFields.contains(o)) {
+        myFields.add(o);
       }
     }
 
@@ -75,11 +77,12 @@
     // add anything else onto the end.
     //
     int p = 0;
-    Iterator<ColumnModel> orderby = leaves(query.getOrderBy()).iterator();
+    Iterator<QueryModel.OrderBy> orderby = orderByLeaves(query.getOrderBy()).iterator();
     while (p < myFields.size() && orderby.hasNext()) {
-      ColumnModel c = orderby.next();
-      if (!myFields.get(p).equals(c)) {
-        myFields.add(c);
+      QueryModel.OrderBy o = orderby.next();
+      ColumnModel c = o.column;
+      if (!myFields.get(p).equals(o)) {
+        myFields.add(o);
         break;
       }
       p++;
@@ -92,7 +95,7 @@
     pojoType = Type.getType(pojo);
   }
 
-  private List<ColumnModel> leaves(List<ColumnModel> in) {
+  private static List<ColumnModel> leaves(List<ColumnModel> in) {
     ArrayList<ColumnModel> r = new ArrayList<ColumnModel>(in.size());
     for (ColumnModel m : in) {
       if (m.isNested()) {
@@ -104,6 +107,20 @@
     return r;
   }
 
+  private static List<QueryModel.OrderBy> orderByLeaves(List<QueryModel.OrderBy> in) {
+    ArrayList<QueryModel.OrderBy> r = new ArrayList<QueryModel.OrderBy>(in.size());
+    for (QueryModel.OrderBy m : in) {
+      if (m.column.isNested()) {
+        for (ColumnModel c : m.column.getAllLeafColumns()) {
+          r.add(new QueryModel.OrderBy(c, m.descending));
+        }
+      } else {
+        r.add(m);
+      }
+    }
+    return r;
+  }
+
   IndexFunction<T> create() throws OrmException {
     init();
     implementConstructor();
@@ -183,7 +200,9 @@
     mv.visitVarInsn(ASTORE, 1);
 
     Set<ColumnModel> checked = new HashSet<ColumnModel>();
-    checkNotNullFields(myFields, checked, mv, cgs);
+    for (QueryModel.OrderBy orderby : myFields) {
+      checkNotNullFields(Collections.singleton(orderby.column), checked, mv, cgs);
+    }
 
     final Tree parseTree = query.getParseTree();
     if (parseTree != null) {
@@ -362,11 +381,11 @@
     mv.visitEnd();
   }
 
-  static void encodeFields(final Collection<ColumnModel> myFields,
+  static void encodeFields(Collection<QueryModel.OrderBy> myFields,
       final MethodVisitor mv, final EncodeCGS cgs) throws OrmException {
-    Iterator<ColumnModel> i = myFields.iterator();
+    Iterator<QueryModel.OrderBy> i = myFields.iterator();
     while (i.hasNext()) {
-      ColumnModel f = i.next();
+      QueryModel.OrderBy f = i.next();
       encodeScalar(f, mv, cgs);
       if (i.hasNext()) {
         cgs.delimiter();
@@ -374,20 +393,22 @@
     }
   }
 
-  static void encodeField(ColumnModel f, final MethodVisitor mv,
+  static void encodeField(QueryModel.OrderBy f, final MethodVisitor mv,
       final EncodeCGS cgs) throws OrmException {
-    if (f.isNested()) {
-      encodeFields(f.getAllLeafColumns(), mv, cgs);
+    if (f.column.isNested()) {
+      encodeFields(orderByLeaves(Collections.singletonList(f)), mv, cgs);
     } else {
       encodeScalar(f, mv, cgs);
     }
   }
 
-  private static void encodeScalar(final ColumnModel f, final MethodVisitor mv,
+  private static void encodeScalar(QueryModel.OrderBy f, final MethodVisitor mv,
       final EncodeCGS cgs) throws OrmException {
-    cgs.setFieldReference(f);
+    String method = f.descending ? "desc" : "add";
+    ColumnModel c = f.column;
+    cgs.setFieldReference(c);
 
-    switch (Type.getType(f.getPrimitiveType()).getSort()) {
+    switch (Type.getType(c.getPrimitiveType()).getSort()) {
       case Type.BOOLEAN:
       case Type.BYTE:
       case Type.SHORT:
@@ -397,7 +418,7 @@
         cgs.pushFieldValue();
         mv.visitInsn(I2L);
         mv.visitMethodInsn(INVOKEVIRTUAL, indexKeyBuilder.getInternalName(),
-            "add", Type.getMethodDescriptor(Type.VOID_TYPE,
+            method, Type.getMethodDescriptor(Type.VOID_TYPE,
                 new Type[] {Type.LONG_TYPE}));
         break;
 
@@ -405,47 +426,47 @@
         cgs.pushBuilder();
         cgs.pushFieldValue();
         mv.visitMethodInsn(INVOKEVIRTUAL, indexKeyBuilder.getInternalName(),
-            "add", Type.getMethodDescriptor(Type.VOID_TYPE,
+            method, Type.getMethodDescriptor(Type.VOID_TYPE,
                 new Type[] {Type.LONG_TYPE}));
         break;
 
       case Type.ARRAY:
       case Type.OBJECT: {
-        if (f.getPrimitiveType() == byte[].class) {
+        if (c.getPrimitiveType() == byte[].class) {
           cgs.pushBuilder();
           cgs.pushFieldValue();
           mv.visitMethodInsn(INVOKEVIRTUAL, indexKeyBuilder.getInternalName(),
-              "add", Type.getMethodDescriptor(Type.VOID_TYPE, new Type[] {Type
+              method, Type.getMethodDescriptor(Type.VOID_TYPE, new Type[] {Type
                   .getType(byte[].class)}));
 
-        } else if (f.getPrimitiveType() == String.class) {
+        } else if (c.getPrimitiveType() == String.class) {
           cgs.pushBuilder();
           cgs.pushFieldValue();
           mv.visitMethodInsn(INVOKEVIRTUAL, indexKeyBuilder.getInternalName(),
-              "add", Type.getMethodDescriptor(Type.VOID_TYPE,
+              method, Type.getMethodDescriptor(Type.VOID_TYPE,
                   new Type[] {string}));
 
-        } else if (f.getPrimitiveType() == java.sql.Timestamp.class
-            || f.getPrimitiveType() == java.util.Date.class
-            || f.getPrimitiveType() == java.sql.Date.class) {
+        } else if (c.getPrimitiveType() == java.sql.Timestamp.class
+            || c.getPrimitiveType() == java.util.Date.class
+            || c.getPrimitiveType() == java.sql.Date.class) {
           cgs.pushBuilder();
           cgs.pushFieldValue();
-          String tsType = Type.getType(f.getPrimitiveType()).getInternalName();
+          String tsType = Type.getType(c.getPrimitiveType()).getInternalName();
           mv.visitMethodInsn(INVOKEVIRTUAL, tsType, "getTime", Type
               .getMethodDescriptor(Type.LONG_TYPE, new Type[] {}));
           mv.visitMethodInsn(INVOKEVIRTUAL, indexKeyBuilder.getInternalName(),
-              "add", Type.getMethodDescriptor(Type.VOID_TYPE,
+              method, Type.getMethodDescriptor(Type.VOID_TYPE,
                   new Type[] {Type.LONG_TYPE}));
         } else {
-          throw new OrmException("Type " + f.getPrimitiveType()
-              + " not supported for field " + f.getPathToFieldName());
+          throw new OrmException("Type " + c.getPrimitiveType()
+              + " not supported for field " + c.getPathToFieldName());
         }
         break;
       }
 
       default:
-        throw new OrmException("Type " + f.getPrimitiveType()
-            + " not supported for field " + f.getPathToFieldName());
+        throw new OrmException("Type " + c.getPrimitiveType()
+            + " not supported for field " + c.getPathToFieldName());
     }
   }
 
diff --git a/src/main/java/com/google/gwtorm/nosql/IndexKeyBuilder.java b/src/main/java/com/google/gwtorm/nosql/IndexKeyBuilder.java
index 8c6d9f4..2bed6ce 100644
--- a/src/main/java/com/google/gwtorm/nosql/IndexKeyBuilder.java
+++ b/src/main/java/com/google/gwtorm/nosql/IndexKeyBuilder.java
@@ -106,6 +106,23 @@
     }
   }
 
+  public void desc(byte[] bin, int pos, int cnt) {
+    while (0 < cnt--) {
+      int b = 0xff - (bin[pos++] & 0xff);
+      if (b == 0x00) {
+        buf.write(0x00);
+        buf.write(0xff);
+
+      } else if (b == 0xff) {
+        buf.write(0xff);
+        buf.write(0x00);
+
+      } else {
+        buf.write(b);
+      }
+    }
+  }
+
   /**
    * Add a raw sequence of bytes.
    * <p>
@@ -118,6 +135,10 @@
     add(bin, 0, bin.length);
   }
 
+  public void desc(byte[] bin) {
+    desc(bin, 0, bin.length);
+  }
+
   /**
    * Encode a string into UTF-8 and append as a sequence of bytes.
    *
@@ -131,6 +152,14 @@
     }
   }
 
+  public void desc(String str) {
+    try {
+      desc(str.getBytes("UTF-8"));
+    } catch (UnsupportedEncodingException e) {
+      throw new RuntimeException("JVM does not support UTF-8", e);
+    }
+  }
+
   /**
    * Add a single character as though it were part of a UTF-8 string.
    *
@@ -149,6 +178,10 @@
     }
   }
 
+  public void desc(char ch) {
+    desc(Character.toString(ch));
+  }
+
   /**
    * Add an integer value as a big-endian variable length integer.
    *
@@ -165,6 +198,10 @@
     buf.write(t, i - 1, t.length - i + 1);
   }
 
+  public void desc(long val) {
+    add(~val);
+  }
+
   /**
    * Add a byte array as-is, without escaping.
    * <p>
diff --git a/src/main/java/com/google/gwtorm/schema/QueryModel.java b/src/main/java/com/google/gwtorm/schema/QueryModel.java
index f557a0f..5cec721 100644
--- a/src/main/java/com/google/gwtorm/schema/QueryModel.java
+++ b/src/main/java/com/google/gwtorm/schema/QueryModel.java
@@ -75,14 +75,41 @@
     return r;
   }
 
-  public List<ColumnModel> getOrderBy() {
-    final ArrayList<ColumnModel> r = new ArrayList<ColumnModel>();
+  public static class OrderBy {
+    public final ColumnModel column;
+    public final boolean descending;
+
+    public OrderBy(ColumnModel column, boolean desc) {
+      this.column = column;
+      this.descending = desc;
+    }
+
+    @Override
+    public int hashCode() {
+      return column.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (other instanceof OrderBy) {
+        OrderBy o = (OrderBy) other;
+        return column.equals(o.column) && descending == o.descending;
+      }
+      return false;
+    }
+  }
+
+  public List<OrderBy> getOrderBy() {
+    ArrayList<OrderBy> r = new ArrayList<OrderBy>();
     if (parsedQuery != null) {
       Tree node = findOrderBy(parsedQuery);
       if (node != null) {
         for (int i = 0; i < node.getChildCount(); i++) {
-          final Tree id = node.getChild(i);
-          r.add(((QueryParser.Column) id).getField());
+          Tree sortOrder = node.getChild(i);
+          Tree id = sortOrder.getChild(0);
+          r.add(new OrderBy(
+              ((QueryParser.Column) id).getField(),
+              sortOrder.getType() == QueryParser.DESC));
         }
       }
     }
@@ -279,7 +306,8 @@
       case QueryParser.ORDER:
         fmt.buf.append(" ORDER BY ");
         for (int i = 0; i < node.getChildCount(); i++) {
-          final Tree id = node.getChild(i);
+          final Tree sortOrder = node.getChild(i);
+          final Tree id = sortOrder.getChild(0);
           if (i > 0) {
             fmt.buf.append(',');
           }
@@ -290,6 +318,9 @@
               fmt.buf.append(fmt.tableAlias);
               fmt.buf.append('.');
               fmt.buf.append(cItr.next().getColumnName());
+              if (sortOrder.getType() == QueryParser.DESC) {
+                fmt.buf.append(" DESC");
+              }
               if (cItr.hasNext()) {
                 fmt.buf.append(',');
               }
@@ -298,6 +329,9 @@
             fmt.buf.append(fmt.tableAlias);
             fmt.buf.append('.');
             fmt.buf.append(col.getColumnName());
+            if (sortOrder.getType() == QueryParser.DESC) {
+              fmt.buf.append(" DESC");
+            }
           }
         }
         break;
diff --git a/src/test/java/com/google/gwtorm/data/PersonAccess.java b/src/test/java/com/google/gwtorm/data/PersonAccess.java
index 594d588..271250a 100644
--- a/src/test/java/com/google/gwtorm/data/PersonAccess.java
+++ b/src/test/java/com/google/gwtorm/data/PersonAccess.java
@@ -30,6 +30,10 @@
   @Query("WHERE age > ? ORDER BY age")
   ResultSet<TestPerson> olderThan(int age) throws OrmException;
 
+  @Query("WHERE age > ? ORDER BY name DESC")
+  ResultSet<TestPerson> olderThanDescByName(int age)
+      throws OrmException;
+
   @Query("WHERE name = 'bob' LIMIT ?")
   ResultSet<TestPerson> firstNBob(int n) throws OrmException;
 
diff --git a/src/test/java/com/google/gwtorm/schema/QueryParserTest.java b/src/test/java/com/google/gwtorm/schema/QueryParserTest.java
index 8eb9936..f9ef3c5 100644
--- a/src/test/java/com/google/gwtorm/schema/QueryParserTest.java
+++ b/src/test/java/com/google/gwtorm/schema/QueryParserTest.java
@@ -131,27 +131,33 @@
     assertEquals(1, t.getChildCount());
 
     final Tree a = t.getChild(0);
-    assertEquals(QueryParser.ID, a.getType());
-    assertTrue(a instanceof QueryParser.Column);
-    assertEquals("a", a.getText());
+    assertEquals(QueryParser.ASC, a.getType());
+    assertEquals(1, a.getChildCount());
+    assertEquals(QueryParser.ID, a.getChild(0).getType());
+    assertTrue(a.getChild(0) instanceof QueryParser.Column);
+    assertEquals("a", a.getChild(0).getText());
   }
 
   public void testOrderByAB() throws QueryParseException {
-    final Tree t = parse("ORDER BY a, b");
+    final Tree t = parse("ORDER BY a DESC, b ASC");
     assertNotNull(t);
     assertEquals(QueryParser.ORDER, t.getType());
     assertEquals(2, t.getChildCount());
     {
       final Tree a = t.getChild(0);
-      assertEquals(QueryParser.ID, a.getType());
-      assertTrue(a instanceof QueryParser.Column);
-      assertEquals("a", a.getText());
+      assertEquals(QueryParser.DESC, a.getType());
+      assertEquals(1, a.getChildCount());
+      assertEquals(QueryParser.ID, a.getChild(0).getType());
+      assertTrue(a.getChild(0) instanceof QueryParser.Column);
+      assertEquals("a", a.getChild(0).getText());
     }
     {
       final Tree b = t.getChild(1);
-      assertEquals(QueryParser.ID, b.getType());
-      assertTrue(b instanceof QueryParser.Column);
-      assertEquals("b", b.getText());
+      assertEquals(QueryParser.ASC, b.getType());
+      assertEquals(1, b.getChildCount());
+      assertEquals(QueryParser.ID, b.getChild(0).getType());
+      assertTrue(b.getChild(0) instanceof QueryParser.Column);
+      assertEquals("b", b.getChild(0).getText());
     }
   }
 
@@ -171,7 +177,10 @@
       assertEquals(QueryParser.ORDER, o.getType());
       assertEquals(1, o.getChildCount());
 
-      final Tree aId = o.getChild(0);
+      final Tree a = o.getChild(0);
+      assertEquals(QueryParser.ASC, a.getType());
+      assertEquals(1, a.getChildCount());
+      final Tree aId = a.getChild(0);
       assertEquals(QueryParser.ID, aId.getType());
       assertTrue(aId instanceof QueryParser.Column);
       assertEquals("a", aId.getText());
diff --git a/src/test/java/com/google/gwtorm/server/PhoneBookDbTestCase.java b/src/test/java/com/google/gwtorm/server/PhoneBookDbTestCase.java
index 55fd759..d87d54d 100644
--- a/src/test/java/com/google/gwtorm/server/PhoneBookDbTestCase.java
+++ b/src/test/java/com/google/gwtorm/server/PhoneBookDbTestCase.java
@@ -293,6 +293,21 @@
     assertEquals(all.get(2).name(), r.get(1).name());
   }
 
+  public void testFetchNotPerson() throws Exception {
+    final PhoneBookDb schema = openAndCreate();
+    final ArrayList<TestPerson> all = new ArrayList<TestPerson>();
+    all.add(new TestPerson(new TestPerson.Key("Bob"), 18));
+    all.add(new TestPerson(new TestPerson.Key("Mary"), 22));
+    all.add(new TestPerson(new TestPerson.Key("Zak"), 33));
+    schema.people().insert(all);
+
+    final List<TestPerson> r =
+        schema.people().olderThanDescByName(18).toList();
+    assertEquals(2, r.size());
+    assertEquals(all.get(2).name(), r.get(0).name());
+    assertEquals(all.get(1).name(), r.get(1).name());
+  }
+
   public void testBooleanType() throws Exception {
     final PhoneBookDb schema = openAndCreate();
     final TestPerson bob = new TestPerson(new TestPerson.Key("Bob"), 18);