You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by er...@apache.org on 2013/01/27 00:03:32 UTC

svn commit: r1438986 - in /lucene/dev/branches/branch_4x: ./ dev-tools/ lucene/ lucene/analysis/ lucene/analysis/icu/src/java/org/apache/lucene/collation/ lucene/backwards/ lucene/benchmark/ lucene/codecs/ lucene/core/ lucene/core/src/test/org/apache/l...

Author: erick
Date: Sat Jan 26 23:03:31 2013
New Revision: 1438986

URL: http://svn.apache.org/viewvc?rev=1438986&view=rev
Log:
Fix for SOLR-3926, thanks Eirik

Modified:
    lucene/dev/branches/branch_4x/   (props changed)
    lucene/dev/branches/branch_4x/dev-tools/   (props changed)
    lucene/dev/branches/branch_4x/lucene/   (props changed)
    lucene/dev/branches/branch_4x/lucene/BUILD.txt   (props changed)
    lucene/dev/branches/branch_4x/lucene/CHANGES.txt   (props changed)
    lucene/dev/branches/branch_4x/lucene/JRE_VERSION_MIGRATION.txt   (props changed)
    lucene/dev/branches/branch_4x/lucene/LICENSE.txt   (props changed)
    lucene/dev/branches/branch_4x/lucene/MIGRATE.txt   (props changed)
    lucene/dev/branches/branch_4x/lucene/NOTICE.txt   (props changed)
    lucene/dev/branches/branch_4x/lucene/README.txt   (props changed)
    lucene/dev/branches/branch_4x/lucene/SYSTEM_REQUIREMENTS.txt   (props changed)
    lucene/dev/branches/branch_4x/lucene/analysis/   (props changed)
    lucene/dev/branches/branch_4x/lucene/analysis/icu/src/java/org/apache/lucene/collation/ICUCollationKeyFilterFactory.java   (props changed)
    lucene/dev/branches/branch_4x/lucene/backwards/   (props changed)
    lucene/dev/branches/branch_4x/lucene/benchmark/   (props changed)
    lucene/dev/branches/branch_4x/lucene/build.xml   (props changed)
    lucene/dev/branches/branch_4x/lucene/codecs/   (props changed)
    lucene/dev/branches/branch_4x/lucene/common-build.xml   (props changed)
    lucene/dev/branches/branch_4x/lucene/core/   (props changed)
    lucene/dev/branches/branch_4x/lucene/core/src/test/org/apache/lucene/index/TestBackwardsCompatibility.java   (props changed)
    lucene/dev/branches/branch_4x/lucene/core/src/test/org/apache/lucene/index/index.40.cfs.zip   (props changed)
    lucene/dev/branches/branch_4x/lucene/core/src/test/org/apache/lucene/index/index.40.nocfs.zip   (props changed)
    lucene/dev/branches/branch_4x/lucene/core/src/test/org/apache/lucene/index/index.40.optimized.cfs.zip   (props changed)
    lucene/dev/branches/branch_4x/lucene/core/src/test/org/apache/lucene/index/index.40.optimized.nocfs.zip   (props changed)
    lucene/dev/branches/branch_4x/lucene/demo/   (props changed)
    lucene/dev/branches/branch_4x/lucene/facet/   (props changed)
    lucene/dev/branches/branch_4x/lucene/grouping/   (props changed)
    lucene/dev/branches/branch_4x/lucene/highlighter/   (props changed)
    lucene/dev/branches/branch_4x/lucene/ivy-settings.xml   (props changed)
    lucene/dev/branches/branch_4x/lucene/join/   (props changed)
    lucene/dev/branches/branch_4x/lucene/licenses/   (props changed)
    lucene/dev/branches/branch_4x/lucene/memory/   (props changed)
    lucene/dev/branches/branch_4x/lucene/misc/   (props changed)
    lucene/dev/branches/branch_4x/lucene/module-build.xml   (props changed)
    lucene/dev/branches/branch_4x/lucene/queries/   (props changed)
    lucene/dev/branches/branch_4x/lucene/queryparser/   (props changed)
    lucene/dev/branches/branch_4x/lucene/sandbox/   (props changed)
    lucene/dev/branches/branch_4x/lucene/site/   (props changed)
    lucene/dev/branches/branch_4x/lucene/spatial/   (props changed)
    lucene/dev/branches/branch_4x/lucene/suggest/   (props changed)
    lucene/dev/branches/branch_4x/lucene/test-framework/   (props changed)
    lucene/dev/branches/branch_4x/lucene/tools/   (props changed)
    lucene/dev/branches/branch_4x/solr/   (props changed)
    lucene/dev/branches/branch_4x/solr/CHANGES.txt   (contents, props changed)
    lucene/dev/branches/branch_4x/solr/LICENSE.txt   (props changed)
    lucene/dev/branches/branch_4x/solr/NOTICE.txt   (props changed)
    lucene/dev/branches/branch_4x/solr/README.txt   (props changed)
    lucene/dev/branches/branch_4x/solr/SYSTEM_REQUIREMENTS.txt   (props changed)
    lucene/dev/branches/branch_4x/solr/build.xml   (props changed)
    lucene/dev/branches/branch_4x/solr/cloud-dev/   (props changed)
    lucene/dev/branches/branch_4x/solr/common-build.xml   (props changed)
    lucene/dev/branches/branch_4x/solr/contrib/   (props changed)
    lucene/dev/branches/branch_4x/solr/core/   (props changed)
    lucene/dev/branches/branch_4x/solr/example/   (props changed)
    lucene/dev/branches/branch_4x/solr/licenses/   (props changed)
    lucene/dev/branches/branch_4x/solr/licenses/httpclient-LICENSE-ASL.txt   (props changed)
    lucene/dev/branches/branch_4x/solr/licenses/httpclient-NOTICE.txt   (props changed)
    lucene/dev/branches/branch_4x/solr/licenses/httpcore-LICENSE-ASL.txt   (props changed)
    lucene/dev/branches/branch_4x/solr/licenses/httpcore-NOTICE.txt   (props changed)
    lucene/dev/branches/branch_4x/solr/licenses/httpmime-LICENSE-ASL.txt   (props changed)
    lucene/dev/branches/branch_4x/solr/licenses/httpmime-NOTICE.txt   (props changed)
    lucene/dev/branches/branch_4x/solr/scripts/   (props changed)
    lucene/dev/branches/branch_4x/solr/site/   (props changed)
    lucene/dev/branches/branch_4x/solr/solrj/   (props changed)
    lucene/dev/branches/branch_4x/solr/solrj/src/java/org/apache/solr/client/solrj/SolrQuery.java
    lucene/dev/branches/branch_4x/solr/solrj/src/test/org/apache/solr/client/solrj/SolrQueryTest.java
    lucene/dev/branches/branch_4x/solr/test-framework/   (props changed)
    lucene/dev/branches/branch_4x/solr/testlogging.properties   (props changed)
    lucene/dev/branches/branch_4x/solr/webapp/   (props changed)

Modified: lucene/dev/branches/branch_4x/solr/CHANGES.txt
URL: http://svn.apache.org/viewvc/lucene/dev/branches/branch_4x/solr/CHANGES.txt?rev=1438986&r1=1438985&r2=1438986&view=diff
==============================================================================
--- lucene/dev/branches/branch_4x/solr/CHANGES.txt (original)
+++ lucene/dev/branches/branch_4x/solr/CHANGES.txt Sat Jan 26 23:03:31 2013
@@ -67,6 +67,9 @@ Bug Fixes
 
 * SOLR-4225: Term info page under schema browser shows incorrect count of terms
   (steffkes)
+  
+* SOLR-3926: Solr should support better way of finding active sorts (Eirik Lygre via
+  Erick Erickson)
 
 Optimizations
 ----------------------

Modified: lucene/dev/branches/branch_4x/solr/solrj/src/java/org/apache/solr/client/solrj/SolrQuery.java
URL: http://svn.apache.org/viewvc/lucene/dev/branches/branch_4x/solr/solrj/src/java/org/apache/solr/client/solrj/SolrQuery.java?rev=1438986&r1=1438985&r2=1438986&view=diff
==============================================================================
--- lucene/dev/branches/branch_4x/solr/solrj/src/java/org/apache/solr/client/solrj/SolrQuery.java (original)
+++ lucene/dev/branches/branch_4x/solr/solrj/src/java/org/apache/solr/client/solrj/SolrQuery.java Sat Jan 26 23:03:31 2013
@@ -25,7 +25,10 @@ import org.apache.solr.common.params.Sta
 import org.apache.solr.common.params.TermsParams;
 import org.apache.solr.common.util.DateUtil;
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Date;
+import java.util.List;
 import java.util.Locale;
 import java.util.regex.Pattern;
 
@@ -44,6 +47,9 @@ public class SolrQuery extends Modifiabl
       return (this == asc) ? desc : asc;
     }
   }
+
+  /** Maintains a map of current sorts */
+  private List<SortClause> sortClauses;
   
   public SolrQuery() {
     super();
@@ -529,38 +535,230 @@ public class SolrQuery extends Modifiabl
     return this.get(HighlightParams.SIMPLE_POST, "");
   }
 
+  /**
+   * Replaces the sort string with a single sort field.
+   * @deprecated Use {@link #setSort(SortClause)} instead, which is part
+   * of an api handling a wider range of sort specifications.
+   */
+  @Deprecated
   public SolrQuery setSortField(String field, ORDER order) {
     this.remove(CommonParams.SORT);
     addValueToParam(CommonParams.SORT, toSortString(field, order));
     return this;
   }
   
+  /**
+   * Adds a sort field to the end of the sort string.
+   * @deprecated Use {@link #addSort(SortClause)} instead, which is part
+   * of an api handling a wider range of sort specifications.
+   */
+  @Deprecated
   public SolrQuery addSortField(String field, ORDER order) {
     return addValueToParam(CommonParams.SORT, toSortString(field, order));
   }
 
+  /**
+   * Removes a sort field to the end of the sort string.
+   * @deprecated Use {@link #removeSort(SortClause)} instead, which is part
+   * of an api handling a wider range of sort specifications.
+   */
+  @Deprecated
   public SolrQuery removeSortField(String field, ORDER order) {
-    String s = this.get(CommonParams.SORT);
-    String removeSort = toSortString(field, order);
-    if (s != null) {
-      String[] sorts = s.split(",");
-      s = join(sorts, ", ", removeSort);
+    String[] sorts = getSortFields();
+    if (sorts != null) {
+      String removeSort = toSortString(field, order);
+      String s = join(sorts, ",", removeSort);
       if (s.length()==0) s=null;
       this.set(CommonParams.SORT, s);
     }
     return this;
   }
   
+  /**
+   * Gets an array of sort specifications.
+   * @deprecated Use {@link #getSorts()} instead, which is part
+   * of an api handling a wider range of sort specifications.
+   */
+  @Deprecated
   public String[] getSortFields() {
     String s = getSortField();
     if (s==null) return null;
-    return s.split(",");
+    return s.trim().split(", *");
   }
 
+  /**
+   * Gets the raw sort field, as it will be sent to Solr.
+   * <p>
+   * The returned sort field will always contain a serialized version
+   * of the sort string built using {@link #setSort(SortClause)},
+   * {@link #addSort(SortClause)}, {@link #addOrUpdateSort(SortClause)},
+   * {@link #removeSort(SortClause)}, {@link #clearSorts()} and 
+   * {@link #setSorts(List)}.
+   */
   public String getSortField() {
     return this.get(CommonParams.SORT);
   }
   
+  /**
+   * Clears current sort information.
+   *
+   * @return the modified SolrQuery object, for easy chaining
+   * @since 4.2
+   */
+  public SolrQuery clearSorts() {
+    sortClauses = null;
+    serializeSorts();
+    return this;
+  }
+
+  /**
+   * Replaces the current sort information.
+   *
+   * @return the modified SolrQuery object, for easy chaining
+   * @since 4.2
+   */
+  public SolrQuery setSorts(List<SortClause> value) {
+    sortClauses = new ArrayList<SortClause>(value);
+    serializeSorts();
+    return this;
+  }
+
+  /**
+   * Gets an a list of current sort clauses.
+   *
+   * @return an immutable list of current sort clauses
+   * @since 4.2
+   */
+  public List<SortClause> getSorts() {
+    if (sortClauses == null) return Collections.emptyList();
+    else return Collections.unmodifiableList(sortClauses);
+  }
+
+  /**
+   * Replaces the current sort information with a single sort clause
+   *
+   * @return the modified SolrQuery object, for easy chaining
+   * @since 4.2
+   */
+  public SolrQuery setSort(String field, ORDER order) {
+    return setSort(new SortClause(field, order));
+  }
+
+  /**
+   * Replaces the current sort information with a single sort clause
+   *
+   * @return the modified SolrQuery object, for easy chaining
+   * @since 4.2
+   */
+  public SolrQuery setSort(SortClause sortClause) {
+    clearSorts();
+    return addSort(sortClause);
+  }
+
+  /**
+   * Adds a single sort clause to the end of the current sort information.
+   *
+   * @return the modified SolrQuery object, for easy chaining
+   * @since 4.2
+   */
+  public SolrQuery addSort(String field, ORDER order) {
+    return addSort(new SortClause(field, order));
+  }
+
+  /**
+   * Adds a single sort clause to the end of the query.
+   *
+   * @return the modified SolrQuery object, for easy chaining
+   * @since 4.2
+   */
+  public SolrQuery addSort(SortClause sortClause) {
+    if (sortClauses == null) sortClauses = new ArrayList<SortClause>();
+    sortClauses.add(sortClause);
+    serializeSorts();
+    return this;
+  }
+
+  /**
+   * Updates or adds a single sort clause to the query.
+   * If the field is already used for sorting, the order
+   * of the existing field is modified; otherwise, it is
+   * added to the end.
+   * <p>
+   * @return the modified SolrQuery object, for easy chaining
+   * @since 4.2
+   */
+  public SolrQuery addOrUpdateSort(String field, ORDER order) {
+    return addOrUpdateSort(new SortClause(field, order));
+  }
+
+  /**
+   * Updates or adds a single sort field specification to the current sort
+   * information. If the sort field already exist in the sort information map,
+   * it's position is unchanged and the sort order is set; if it does not exist,
+   * it is appended at the end with the specified order..
+   *
+   * @return the modified SolrQuery object, for easy chaining
+   * @since 4.2
+   */
+  public SolrQuery addOrUpdateSort(SortClause sortClause) {
+    if (sortClauses != null) {
+      for (int index=0 ; index<sortClauses.size() ; index++) {
+        SortClause existing = sortClauses.get(index);
+        if (existing.getItem().equals(sortClause.getItem())) {
+          sortClauses.set(index, sortClause);
+          serializeSorts();
+          return this;
+        }
+      }
+    }
+    return addSort(sortClause);
+  }
+
+  /**
+   * Removes a single sort field from the current sort information.
+   *
+   * @return the modified SolrQuery object, for easy chaining
+   * @since 4.2
+   */
+  public SolrQuery removeSort(SortClause sortClause) {
+    return removeSort(sortClause.getItem());
+  }
+
+  /**
+   * Removes a single sort field from the current sort information.
+   *
+   * @return the modified SolrQuery object, for easy chaining
+   * @since 4.2
+   */
+  public SolrQuery removeSort(String itemName) {
+    if (sortClauses != null) {
+      for (SortClause existing : sortClauses) {
+        if (existing.getItem().equals(itemName)) {
+          sortClauses.remove(existing);
+          if (sortClauses.isEmpty()) sortClauses = null;
+          serializeSorts();
+          break;
+        }
+      }
+    }
+    return this;
+  }
+
+  private void serializeSorts() {
+    if (sortClauses == null || sortClauses.isEmpty()) {
+      remove(CommonParams.SORT);
+    } else {
+      StringBuilder sb = new StringBuilder();
+      for (SortClause sortClause : sortClauses) {
+        if (sb.length() > 0) sb.append(",");
+        sb.append(sortClause.getItem());
+        sb.append(" ");
+        sb.append(sortClause.getOrder());
+      }
+      set(CommonParams.SORT, sb.toString());
+    }
+  }
+
   public void setGetFieldStatistics( boolean v )
   {
     this.set( StatsParams.STATS, v );
@@ -823,13 +1021,126 @@ public class SolrQuery extends Modifiabl
   private String join(String[] vals, String sep, String removeVal) {
     StringBuilder sb = new StringBuilder();
     for (int i=0; i<vals.length; i++) {
-      if (removeVal==null || !vals[i].equals(removeVal)) {
-        sb.append(vals[i]);
-        if (i<vals.length-1) {
+      if (!vals[i].equals(removeVal)) {
+        if (sb.length() > 0) {
           sb.append(sep);
         }
+        sb.append(vals[i]);
       }
     }
     return sb.toString().trim();
   }
+
+  /**
+   * A single sort clause, encapsulating what to sort and the sort order.
+   * <p>
+   * The item specified can be "anything sortable" by solr; some examples
+   * include a simple field name, the constant string {@code score}, and functions
+   * such as {@code sum(x_f, y_f)}.
+   * <p>
+   * A SortClause can be created through different mechanisms:
+   * <PRE><code>
+   * new SortClause("product", SolrQuery.ORDER.asc);
+   * new SortClause("product", "asc");
+   * SortClause.asc("product");
+   * SortClause.desc("product");
+   * </code></PRE>
+   */
+  public static class SortClause implements java.io.Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private final String item;
+    private final ORDER order;
+
+    /**
+     * Creates a SortClause based on item and order
+     * @param item item to sort on
+     * @param order direction to sort
+     */
+    public SortClause(String item, ORDER order) {
+      this.item = item;
+      this.order = order;
+    }
+
+    /**
+     * Creates a SortClause based on item and order
+     * @param item item to sort on
+     * @param order string value for direction to sort
+     */
+    public SortClause(String item, String order) {
+      this(item, ORDER.valueOf(order));
+    }
+
+    /**
+     * Creates an ascending SortClause for an item
+     * @param item item to sort on
+     */
+    public static SortClause create (String item, ORDER order) {
+      return new SortClause(item, order);
+    }
+
+    /**
+     * Creates a SortClause based on item and order
+     * @param item item to sort on
+     * @param order string value for direction to sort
+     */
+    public static SortClause create(String item, String order) {
+      return new SortClause(item, ORDER.valueOf(order));
+    }
+
+    /**
+     * Creates an ascending SortClause for an item
+     * @param item item to sort on
+     */
+    public static SortClause asc (String item) {
+      return new SortClause(item, ORDER.asc);
+    }
+
+    /**
+     * Creates a decending SortClause for an item
+     * @param item item to sort on
+     */
+    public static SortClause desc (String item) {
+      return new SortClause(item, ORDER.desc);
+    }
+
+    /**
+     * Gets the item to sort, typically a function or a fieldname
+     * @return item to sort
+     */
+    public String getItem() {
+      return item;
+    }
+
+    /**
+     * Gets the order to sort
+     * @return order to sort
+     */
+    public ORDER getOrder() {
+      return order;
+    }
+
+    public boolean equals(Object other){
+      if (this == other) return true;
+      if (!(other instanceof SortClause)) return false;
+      final SortClause that = (SortClause) other;
+      return this.getItem().equals(that.getItem()) && this.getOrder().equals(that.getOrder());
+    }
+
+    public int hashCode(){
+      return this.getItem().hashCode();
+    }
+
+    /**
+     * Gets a human readable description of the sort clause.
+     * <p>
+     * The returned string is not suitable for passing to Solr,
+     * but may be useful in debug output and the like.
+     * @return a description of the current sort clause
+     */
+    public String toString() {
+      return "[" + getClass().getSimpleName() + ": item=" + getItem() + "; order=" + getOrder() + "]";
+    }
+  }
 }

Modified: lucene/dev/branches/branch_4x/solr/solrj/src/test/org/apache/solr/client/solrj/SolrQueryTest.java
URL: http://svn.apache.org/viewvc/lucene/dev/branches/branch_4x/solr/solrj/src/test/org/apache/solr/client/solrj/SolrQueryTest.java?rev=1438986&r1=1438985&r2=1438986&view=diff
==============================================================================
--- lucene/dev/branches/branch_4x/solr/solrj/src/test/org/apache/solr/client/solrj/SolrQueryTest.java (original)
+++ lucene/dev/branches/branch_4x/solr/solrj/src/test/org/apache/solr/client/solrj/SolrQueryTest.java Sat Jan 26 23:03:31 2013
@@ -18,11 +18,17 @@
 package org.apache.solr.client.solrj;
 
 import org.apache.lucene.util.LuceneTestCase;
+import org.apache.solr.client.solrj.SolrQuery.SortClause;
+import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.params.FacetParams;
 
 import junit.framework.Assert;
 import org.apache.solr.common.util.DateUtil;
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.util.Arrays;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.Locale;
@@ -98,6 +104,169 @@ public class SolrQueryTest extends Lucen
     // System.out.println(q);
   }
   
+  /*
+   * Verifies that the old (deprecated) sort methods
+   * allows mix-and-match between the raw field and
+   * the itemized apis.
+   */
+  public void testSortFieldRawStringAndMethods() {
+    SolrQuery q = new SolrQuery("dog");
+    q.set("sort", "price asc,date desc,qty desc");
+    q.removeSortField("date", SolrQuery.ORDER.desc);
+    Assert.assertEquals(2, q.getSortFields().length);
+    q.set("sort", "price asc, date desc, qty desc");
+    q.removeSortField("date", SolrQuery.ORDER.desc);
+    Assert.assertEquals(2, q.getSortFields().length);
+  }
+
+  /*
+   *  Verifies that you can use removeSortField() twice, which
+   *  did not work in 4.0
+   */
+  public void testSortFieldRemoveAfterRemove() {
+    SolrQuery q = new SolrQuery("dog");
+    q.addSortField("price", SolrQuery.ORDER.asc);
+    q.addSortField("date", SolrQuery.ORDER.desc);
+    q.addSortField("qty", SolrQuery.ORDER.desc);
+    q.removeSortField("date", SolrQuery.ORDER.desc);
+    Assert.assertEquals(2, q.getSortFields().length);
+    q.removeSortField("qty", SolrQuery.ORDER.desc);
+    Assert.assertEquals(1, q.getSortFields().length);
+  }
+
+  /*
+   * Verifies that you can remove the last sort field, which
+   * did not work in 4.0
+   */
+  public void testSortFieldRemoveLast() {
+    SolrQuery q = new SolrQuery("dog");
+    q.addSortField("date", SolrQuery.ORDER.desc);
+    q.addSortField("qty", SolrQuery.ORDER.desc);
+    q.removeSortField("qty", SolrQuery.ORDER.desc);
+    Assert.assertEquals("date desc", q.getSortField());
+  }
+
+  /*
+   * Verifies that getSort() returns an immutable map,
+   * for both empty and non-empty situations
+   */
+  public void testGetSortImmutable() {
+    SolrQuery q = new SolrQuery("dog");
+
+    try {
+      q.getSorts().add(new SortClause("price",  SolrQuery.ORDER.asc));
+      fail("The returned (empty) map should be immutable; put() should fail!");
+    } catch (UnsupportedOperationException uoe) {
+      // pass
+    }
+
+    q.addSort("qty", SolrQuery.ORDER.desc);
+    try {
+      q.getSorts().add(new SortClause("price",  SolrQuery.ORDER.asc));
+      fail("The returned (non-empty) map should be immutable; put() should fail!");
+    } catch (UnsupportedOperationException uoe) {
+      // pass
+    }
+
+    // Should work even when setSorts passes an Immutable List
+    q.setSorts(Arrays.asList(new SortClause("price",  SolrQuery.ORDER.asc)));
+    q.addSort(new SortClause("price",  SolrQuery.ORDER.asc));
+  }
+
+  public void testSortClause() {
+    new SolrQuery.SortClause("rating", SolrQuery.ORDER.desc);
+    new SolrQuery.SortClause("rating", SolrQuery.ORDER.valueOf("desc"));
+    new SolrQuery.SortClause("rating", SolrQuery.ORDER.valueOf("desc"));
+    SolrQuery.SortClause.create("rating", SolrQuery.ORDER.desc);
+    SolrQuery.SortClause.create("rating", SolrQuery.ORDER.desc);
+    SolrQuery.SortClause.create("rating", SolrQuery.ORDER.desc);
+
+    SolrQuery.SortClause sc1a = SolrQuery.SortClause.asc("sc1");
+    SolrQuery.SortClause sc1b = SolrQuery.SortClause.asc("sc1");
+    Assert.assertEquals(sc1a, sc1b);
+    Assert.assertEquals(sc1a.hashCode(), sc1b.hashCode());
+
+    SolrQuery.SortClause sc2a = SolrQuery.SortClause.asc("sc2");
+    SolrQuery.SortClause sc2b = SolrQuery.SortClause.desc("sc2");
+    Assert.assertFalse(sc2a.equals(sc2b));
+
+    SolrQuery.SortClause sc3a = SolrQuery.SortClause.asc("sc2");
+    SolrQuery.SortClause sc3b = SolrQuery.SortClause.asc("not sc2");
+    Assert.assertFalse(sc3a.equals(sc3b));
+  }
+
+  /*
+   * Verifies the symbolic sort operations
+   */
+  public void testSort() throws IOException {
+
+    SolrQuery q = new SolrQuery("dog");
+
+    // Simple adds
+    q.addSort("price", SolrQuery.ORDER.asc);
+    q.addSort("date", SolrQuery.ORDER.desc);
+    q.addSort("qty", SolrQuery.ORDER.desc);
+    Assert.assertEquals(3, q.getSorts().size());
+    Assert.assertEquals("price asc,date desc,qty desc", q.get(CommonParams.SORT));
+
+    // Remove one (middle)
+    q.removeSort("date");
+    Assert.assertEquals(2, q.getSorts().size());
+    Assert.assertEquals("price asc,qty desc", q.get(CommonParams.SORT));
+
+    // Remove remaining (last, first)
+    q.removeSort("price");
+    q.removeSort("qty");
+    Assert.assertTrue(q.getSorts().isEmpty());
+    Assert.assertNull(q.get(CommonParams.SORT));
+
+    // Clear sort
+    q.addSort("price", SolrQuery.ORDER.asc);
+    q.clearSorts();
+    Assert.assertTrue(q.getSorts().isEmpty());
+    Assert.assertNull(q.get(CommonParams.SORT));
+
+    // Add vs update
+    q.clearSorts();
+    q.addSort("1", SolrQuery.ORDER.asc);
+    q.addSort("2", SolrQuery.ORDER.asc);
+    q.addSort("3", SolrQuery.ORDER.asc);
+    q.addOrUpdateSort("2", SolrQuery.ORDER.desc);
+    q.addOrUpdateSort("4", SolrQuery.ORDER.desc);
+    Assert.assertEquals("1 asc,2 desc,3 asc,4 desc", q.get(CommonParams.SORT));
+
+    // Using SortClause
+    q.clearSorts();
+    q.addSort(new SortClause("1", SolrQuery.ORDER.asc));
+    q.addSort(new SortClause("2", SolrQuery.ORDER.asc));
+    q.addSort(new SortClause("3", SolrQuery.ORDER.asc));
+    q.addOrUpdateSort(SortClause.desc("2"));
+    q.addOrUpdateSort(SortClause.asc("4"));
+    Assert.assertEquals("1 asc,2 desc,3 asc,4 asc", q.get(CommonParams.SORT));
+    q.setSort(SortClause.asc("A"));
+    q.addSort(SortClause.asc("B"));
+    q.addSort(SortClause.asc("C"));
+    q.addSort(SortClause.asc("D"));
+    Assert.assertEquals("A asc,B asc,C asc,D asc", q.get(CommonParams.SORT));
+
+    // removeSort should ignore the ORDER
+    q.setSort(SortClause.asc("A"));
+    q.addSort(SortClause.asc("B"));
+    q.addSort(SortClause.asc("C"));
+    q.addSort(SortClause.asc("D"));
+    q.removeSort("A");
+    q.removeSort(SortClause.asc("C"));
+    q.removeSort(SortClause.desc("B"));
+    Assert.assertEquals("D asc", q.get(CommonParams.SORT));
+
+    // Verify that a query containing a SortClause is serializable
+    q.clearSorts();
+    q.addSort("1", SolrQuery.ORDER.asc);
+    ObjectOutputStream out = new ObjectOutputStream(new ByteArrayOutputStream());
+    out.writeObject(q);
+    out.close();
+  }
+
   public void testFacetSort() {
     SolrQuery q = new SolrQuery("dog");
     assertEquals("count", q.getFacetSortString());