=== modified file 'includes/database/query.inc'
--- includes/database/query.inc	2009-01-19 00:00:08 +0000
+++ includes/database/query.inc	2009-02-19 18:33:53 +0000
@@ -108,6 +108,8 @@
    *
    * @param $tag
    *   The tag to add.
+   * @return
+   *   The called object.
    */
   public function addTag($tag);
 
@@ -154,7 +156,8 @@
    *   follows the same rules as any other PHP identifier.
    * @param $object
    *   The additional data to add to the query. May be any valid PHP variable.
-   *
+   * @return
+   *   The called object.
    */
   public function addMetaData($key, $object);
 
@@ -201,7 +204,12 @@
   abstract protected function execute();
 
   /**
-   * Returns the query as a prepared statement string.
+   * __toString() magic method.
+   *
+   * The toString operation is how we compile a query object to a prepared statement.
+   *
+   * @return
+   *   A prepared statement query string for this object.
    */
   abstract public function __toString();
 }

=== modified file 'includes/database/select.inc'
--- includes/database/select.inc	2009-01-04 20:00:08 +0000
+++ includes/database/select.inc	2009-02-19 18:33:53 +0000
@@ -7,9 +7,618 @@
  */
 
 /**
+ * Interface for extendable query objects.
+ *
+ * "Extenders" follow the "Decorator" OOP design pattern.  That is, they wrap
+ * and "decorate" another object.  In our case, they implement the same interface
+ * as select queries and wrap a select query, to which they delegate almost all
+ * operations.  Subclasses of this class may implement additional methods or
+ * override existing methods as appropriate.  Extenders may also wrap other
+ * extender objects, allowing for arbitrarily complex "enhanced" queries.
+ */
+interface QueryExtendableInterface {
+
+  /**
+   * Enhance this object by wrapping it in an extender object.
+   *
+   * @param $extender_name
+   *   The base name of the extending class.  The base name will be checked
+   *   against the current database connection to allow driver-specific subclasses
+   *   as well, using the same logic as the query objects themselves.  For example,
+   *   PagerDefault_mysql is the MySQL-specific override for PagerDefault.
+   * @return
+   *   The extender object, which now contains a reference to this object.
+   */
+  public function extend($extender_name);
+}
+
+/**
+ * Interface definition for a Select Query object.
+ */
+interface SelectQueryInterface extends QueryConditionInterface, QueryAlterableInterface, QueryExtendableInterface {
+
+  /* Alter accessors to expose the query data to alter hooks. */
+
+  /**
+   * Returns a reference to the fields array for this query.
+   *
+   * Because this method returns by reference, alter hooks may edit the fields
+   * array directly to make their changes. If just adding fields, however, the
+   * use of addField() is preferred.
+   *
+   * Note that this method must be called by reference as well:
+   *
+   * @code
+   * $fields =& $query->getFields();
+   * @endcode
+   *
+   * @return
+   *   A reference to the fields array structure.
+   */
+  public function &getFields();
+
+  /**
+   * Returns a reference to the expressions array for this query.
+   *
+   * Because this method returns by reference, alter hooks may edit the expressions
+   * array directly to make their changes. If just adding expressions, however, the
+   * use of addExpression() is preferred.
+   *
+   * Note that this method must be called by reference as well:
+   *
+   * @code
+   * $fields =& $query->getExpressions();
+   * @endcode
+   *
+   * @return
+   *   A reference to the expression array structure.
+   */
+  public function &getExpressions();
+
+  /**
+   * Returns a reference to the order by array for this query.
+   *
+   * Because this method returns by reference, alter hooks may edit the order-by
+   * array directly to make their changes. If just adding additional ordering
+   * fields, however, the use of orderBy() is preferred.
+   *
+   * Note that this method must be called by reference as well:
+   *
+   * @code
+   * $fields =& $query->getOrderBy();
+   * @endcode
+   *
+   * @return
+   *   A reference to the expression array structure.
+   */
+  public function &getOrderBy();
+
+  /**
+   * Returns a reference to the tables array for this query.
+   *
+   * Because this method returns by reference, alter hooks may edit the tables
+   * array directly to make their changes. If just adding tables, however, the
+   * use of the join() methods is preferred.
+   *
+   * Note that this method must be called by reference as well:
+   *
+   * @code
+   * $fields =& $query->getTables();
+   * @endcode
+   *
+   * @return
+   *   A reference to the tables array structure.
+   */
+  public function &getTables();
+
+  /**
+   * Compiles and returns an associative array of the arguments for this prepared statement.
+   *
+   * @return
+   *   An associative array of all placeholder arguments for this query.
+   */
+  public function getArguments();
+
+  /* Query building operations */
+
+  /**
+   * Sets this query to be DISTINCT.
+   *
+   * @param $distinct
+   *   TRUE to flag this query DISTINCT, FALSE to disable it.
+   * @return
+   *   The called object.
+   */
+  public function distinct($distinct = TRUE);
+
+  /**
+   * Adds a field to the list to be SELECTed.
+   *
+   * @param $table_alias
+   *   The name of the table from which the field comes, as an alias. Generally
+   *   you will want to use the return value of join() here to ensure that it is
+   *   valid.
+   * @param $field
+   *   The name of the field.
+   * @param $alias
+   *   The alias for this field. If not specified, one will be generated
+   *   automatically based on the $table_alias and $field. The alias will be
+   *   checked for uniqueness, so the requested alias may not be the alias
+   *   that is assigned in all cases.
+   * @return
+   *   The unique alias that was assigned for this field.
+   */
+  public function addField($table_alias, $field, $alias = NULL);
+
+  /**
+   * Add multiple fields from the same table to be SELECTed.
+   *
+   * This method does not return the aliases set for the passed fields. In the
+   * majority of cases that is not a problem, as the alias will be the field
+   * name. However, if you do need to know the alias you can call getFields()
+   * and examine the result to determine what alias was created. Alternatively,
+   * simply use addField() for the few fields you care about and this method for
+   * the rest.
+   *
+   * @param $table_alias
+   *   The name of the table from which the field comes, as an alias. Generally
+   *   you will want to use the return value of join() here to ensure that it is
+   *   valid.
+   * @param $fields
+   *   An indexed array of fields present in the specified table that should be
+   *   included in this query. If not specified, $table_alias.* will be generated
+   *   without any aliases.
+   * @return
+   *   The called object.
+   */
+  public function fields($table_alias, array $fields = array());
+
+  /**
+   * Adds an expression to the list of "fields" to be SELECTed.
+   *
+   * An expression can be any arbitrary string that is valid SQL. That includes
+   * various functions, which may in some cases be database-dependent. This
+   * method makes no effort to correct for database-specific functions.
+   *
+   * @param $expression
+   *   The expression string. May contain placeholders.
+   * @param $alias
+   *   The alias for this expression. If not specified, one will be generated
+   *   automatically in the form "expression_#". The alias will be checked for
+   *   uniqueness, so the requested alias may not be the alias that is assigned
+   *   in all cases.
+   * @param $arguments
+   *   Any placeholder arguments needed for this expression.
+   * @return
+   *   The unique alias that was assigned for this expression.
+   */
+  public function addExpression($expression, $alias = NULL, $arguments = array());
+
+  /**
+   * Default Join against another table in the database.
+   *
+   * This method is a convenience method for innerJoin().
+   *
+   * @param $table
+   *   The table against which to join.
+   * @param $alias
+   *   The alias for the table. In most cases this should be the first letter
+   *   of the table, or the first letter of each "word" in the table.
+   * @param $condition
+   *   The condition on which to join this table. If the join requires values,
+   *   this clause should use a named placeholder and the value or values to
+   *   insert should be passed in the 4th parameter. For the first table joined
+   *   on a query, this value is ignored as the first table is taken as the base
+   *   table.
+   * @param $arguments
+   *   An array of arguments to replace into the $condition of this join.
+   * @return
+   *   The unique alias that was assigned for this table.
+   */
+  public function join($table, $alias = NULL, $condition = NULL, $arguments = array());
+
+  /**
+   * Inner Join against another table in the database.
+   *
+   * @param $table
+   *   The table against which to join.
+   * @param $alias
+   *   The alias for the table. In most cases this should be the first letter
+   *   of the table, or the first letter of each "word" in the table.
+   * @param $condition
+   *   The condition on which to join this table. If the join requires values,
+   *   this clause should use a named placeholder and the value or values to
+   *   insert should be passed in the 4th parameter. For the first table joined
+   *   on a query, this value is ignored as the first table is taken as the base
+   *   table.
+   * @param $arguments
+   *   An array of arguments to replace into the $condition of this join.
+   * @return
+   *   The unique alias that was assigned for this table.
+   */
+  public function innerJoin($table, $alias = NULL, $condition = NULL, $arguments = array());
+
+  /**
+   * Left Outer Join against another table in the database.
+   *
+   * @param $table
+   *   The table against which to join.
+   * @param $alias
+   *   The alias for the table. In most cases this should be the first letter
+   *   of the table, or the first letter of each "word" in the table.
+   * @param $condition
+   *   The condition on which to join this table. If the join requires values,
+   *   this clause should use a named placeholder and the value or values to
+   *   insert should be passed in the 4th parameter. For the first table joined
+   *   on a query, this value is ignored as the first table is taken as the base
+   *   table.
+   * @param $arguments
+   *   An array of arguments to replace into the $condition of this join.
+   * @return
+   *   The unique alias that was assigned for this table.
+   */
+  public function leftJoin($table, $alias = NULL, $condition = NULL, $arguments = array());
+
+  /**
+   * Right Outer Join against another table in the database.
+   *
+   * @param $table
+   *   The table against which to join.
+   * @param $alias
+   *   The alias for the table. In most cases this should be the first letter
+   *   of the table, or the first letter of each "word" in the table.
+   * @param $condition
+   *   The condition on which to join this table. If the join requires values,
+   *   this clause should use a named placeholder and the value or values to
+   *   insert should be passed in the 4th parameter. For the first table joined
+   *   on a query, this value is ignored as the first table is taken as the base
+   *   table.
+   * @param $arguments
+   *   An array of arguments to replace into the $condition of this join.
+   * @return
+   *   The unique alias that was assigned for this table.
+   */
+  public function rightJoin($table, $alias = NULL, $condition = NULL, $arguments = array());
+
+  /**
+   * Join against another table in the database.
+   *
+   * This method does the "hard" work of queuing up a table to be joined against.
+   * In some cases, that may include dipping into the Schema API to find the necessary
+   * fields on which to join.
+   *
+   * @param $type
+   *   The type of join. Typically one one of INNER, LEFT OUTER, and RIGHT OUTER.
+   * @param $table
+   *   The table against which to join. May be a string or another SelectQuery
+   *   object. If a query object is passed, it will be used as a subselect.
+   * @param $alias
+   *   The alias for the table. In most cases this should be the first letter
+   *   of the table, or the first letter of each "word" in the table. If omitted,
+   *   one will be dynamically generated.
+   * @param $condition
+   *   The condition on which to join this table. If the join requires values,
+   *   this clause should use a named placeholder and the value or values to
+   *   insert should be passed in the 4th parameter. For the first table joined
+   *   on a query, this value is ignored as the first table is taken as the base
+   *   table.
+   * @param $arguments
+   *   An array of arguments to replace into the $condition of this join.
+   * @return
+   *   The unique alias that was assigned for this table.
+   */
+  public function addJoin($type, $table, $alias = NULL, $condition = NULL, $arguments = array());
+
+  /**
+   * Orders the result set by a given field.
+   *
+   * If called multiple times, the query will order by each specified field in the
+   * order this method is called.
+   *
+   * @param $field
+   *   The field on which to order.
+   * @param $direction
+   *   The direction to sort. Legal values are "ASC" and "DESC".
+   * @return
+   *   The called object.
+   */
+  public function orderBy($field, $direction = 'ASC');
+
+  /**
+   * Restricts a query to a given range in the result set.
+   *
+   * If this method is called with no parameters, will remove any range
+   * directives that have been set.
+   *
+   * @param $start
+   *   The first record from the result set to return. If NULL, removes any
+   *   range directives that are set.
+   * @param $limit
+   *   The number of records to return from the result set.
+   * @return
+   *   The called object.
+   */
+  public function range($start = NULL, $length = NULL);
+
+  /**
+   * Groups the result set by the specified field.
+   *
+   * @param $field
+   *   The field on which to group. This should be the field as aliased.
+   * @return
+   *   The called object.
+   */
+  public function groupBy($field);
+
+  /**
+   * Get the equivalent COUNT query of this query as a new query object.
+   *
+   * @return
+   *   A new SelectQuery object with no fields or expressions besides COUNT(*).
+   */
+  public function countQuery();
+
+  /**
+   * Clone magic method.
+   *
+   * Select queries have dependent objects that must be deep-cloned.  The
+   * connection object itself, however, should not be cloned as that would
+   * duplicate the connection itself.
+   */
+  public function __clone();
+}
+
+/**
+ * The base extender class for Select queries.
+ */
+class SelectQueryExtender implements SelectQueryInterface {
+
+  /**
+   * The SelectQuery object we are extending/decorating.
+   *
+   * @var SelectQueryInterface
+   */
+  protected $query;
+
+  /**
+   * The connection object on which to run this query.
+   *
+   * @var DatabaseConnection
+   */
+  protected $connection;
+
+
+  public function __construct(SelectQueryInterface $query, DatabaseConnection $connection) {
+    $this->query = $query;
+    $this->connection = $connection;
+  }
+
+  /* Implementations of QueryAlterableInterface. */
+
+  public function addTag($tag) {
+    $this->query->addTag($tag);
+    return $this;
+  }
+
+  public function hasTag($tag) {
+    return $this->query->hasTag($tag);
+  }
+
+  public function hasAllTags() {
+    return call_user_func_array(array($this->query, 'hasAllTags', func_get_args()));
+  }
+
+  public function hasAnyTag() {
+    return call_user_func_array(array($this->query, 'hasAnyTags', func_get_args()));
+  }
+
+  public function addMetaData($key, $object) {
+    $this->query->addMetaData($key, $object);
+    return $this;
+  }
+
+  public function getMetaData($key) {
+    return $this->query->getMetaData($key);
+  }
+
+  /* Implementations of QueryConditionInterface for the WHERE clause. */
+
+  public function condition($field, $value = NULL, $operator = '=') {
+    $this->query->condition($field, $value, $operator);
+    return $this;
+  }
+
+  public function &conditions() {
+    return $this->query->conditions();
+  }
+
+  public function arguments() {
+    return $this->query->arguments();
+  }
+
+  public function where($snippet, $args = array()) {
+    $this->query->where($snippet, $args);
+    return $this;
+  }
+
+  public function compile(DatabaseConnection $connection) {
+    return $this->query->compile($connection);
+  }
+
+  /* Implmeentations of QueryConditionInterface for the HAVING clause. */
+
+  public function havingCondition($field, $value = NULL, $operator = '=') {
+    $this->query->condition($field, $value, $operator, $num_args);
+    return $this;
+  }
+
+  public function &havingConditions() {
+    return $this->having->conditions();
+  }
+
+  public function havingArguments() {
+    return $this->having->arguments();
+  }
+
+  public function having($snippet, $args = array()) {
+    $this->query->where($snippet, $args);
+    return $this;
+  }
+
+  public function havingCompile(DatabaseConnection $connection) {
+    return $this->query->havingCompile($connection);
+  }
+
+  /* Implementations of QueryExtendableInterface. */
+
+  public function extend($extender_name) {
+    $override_class = $this->connection->driver();
+    if (class_exists($override_class)) {
+      $extender_name = $override_class;
+    }
+    return new $extender_name($this, $this->connection);
+  }
+
+  /* Alter accessors to expose the query data to alter hooks. */
+
+  public function &getFields() {
+    return $this->query->getFields();
+  }
+
+  public function &getExpressions() {
+    return $this->query->getExpressions();
+  }
+
+  public function &getOrderBy() {
+    return $this->query->getOrderBy();
+  }
+
+  public function &getTables() {
+    return $this->query->getTables();
+  }
+
+  public function getArguments() {
+    return $this->query->getArguments();
+  }
+
+  public function execute() {
+    return $this->query->execute();
+  }
+
+  public function distinct($distinct = TRUE) {
+    $this->query->distinct($distinct);
+    return $this;
+  }
+
+  public function addField($table_alias, $field, $alias = NULL) {
+    return $this->query->addField($table_alias, $field, $alias);
+  }
+
+  public function fields($table_alias, array $fields = array()) {
+    $this->query->fields($table_alias, $fields);
+    return $this;
+  }
+
+  public function addExpression($expression, $alias = NULL, $arguments = array()) {
+    return $this->query->addExpression($expression, $alias, $arguments);
+  }
+
+  public function join($table, $alias = NULL, $condition = NULL, $arguments = array()) {
+    return $this->query->join($table, $alias, $condition, $arguments);
+  }
+
+  public function innerJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) {
+    return $this->query->innerJoin($table, $alias, $condition, $arguments);
+  }
+
+  public function leftJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) {
+    return $this->query->leftJoin($table, $alias, $condition, $arguments);
+  }
+
+  public function rightJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) {
+    return $this->query->rightJoin($table, $alias, $condition, $arguments);
+  }
+
+  public function addJoin($type, $table, $alias = NULL, $condition = NULL, $arguments = array()) {
+    return $this->query->addJoin($type, $table, $alias, $condition, $arguments);
+  }
+
+  public function orderBy($field, $direction = 'ASC') {
+    $this->query->orderBy($field, $direction);
+    return $this;
+  }
+
+  public function range($start = NULL, $length = NULL) {
+    $this->query->range($start, $length);
+    return $this;
+  }
+
+  public function groupBy($field) {
+    $this->query->groupBy($field);
+    return $this;
+  }
+
+  public function countQuery() {
+    // Create our new query object that we will mutate into a count query.
+    $count = clone($this);
+
+    // Zero-out existing fields and expressions.
+    $fields =& $count->getFields();
+    $fields = array();
+    $expressions =& $count->getExpressions();
+    $expressions = array();
+
+    // Ordering a count query is a waste of cycles, and breaks on some
+    // databases anyway.
+    $orders = &$count->getOrderBy();
+    $orders = array();
+
+    // COUNT() is an expression, so we add that back in.
+    $count->addExpression('COUNT(*)');
+
+    return $count;
+  }
+
+  public function __toString() {
+    return (string)$this->query;
+  }
+
+  public function __clone() {
+    // We need to deep-clone the query we're wrapping, which in turn may
+    // deep-clone other objects.  Exciting!
+    $this->query = clone($this->query);
+  }
+
+  /**
+   * Magic override for undefined methods.
+   *
+   * If one extender extends another extender, then methods in the inner extender
+   * will not be exposed on the outer extender.  That's because we cannot know
+   * in advance what those methods will be, so we cannot provide wrapping
+   * implementations as we do above.  Instead, we use this slower catch-all method
+   * to handle any additional methods.
+   */
+  public function __call($method, $args) {
+    $return = call_user_func_array(array($this->query, $method), $args);
+
+    // Some methods will return the called object as part of a fluent interface.
+    // Others will return some useful value.  If it's a value, then the caller
+    // probably wants that value.  If it's the called object, then we instead
+    // return this object.  That way we don't "lose" an extender layer when
+    // chaining methods together.
+    if ($return instanceof SelectQueryInterface) {
+      return $this;
+    }
+    else {
+      return $return;
+    }
+  }
+}
+
+/**
  * Query builder for SELECT statements.
  */
-class SelectQuery extends Query implements QueryConditionInterface, QueryAlterableInterface {
+class SelectQuery extends Query implements SelectQueryInterface {
 
   /**
    * The fields to SELECT.
@@ -104,6 +713,7 @@
 
   public function addTag($tag) {
     $this->alterTags[$tag] = 1;
+    return $this;
   }
 
   public function hasTag($tag) {
@@ -120,6 +730,7 @@
 
   public function addMetaData($key, $object) {
     $this->alterMetaData[$key] = $object;
+    return $this;
   }
 
   public function getMetaData($key) {
@@ -180,94 +791,34 @@
     return $this->having->compile($connection);
   }
 
+  /* Implementations of QueryExtendableInterface. */
+
+  public function extend($extender_name) {
+    $override_class = __CLASS__ . $this->connection->driver();
+    if (class_exists($override_class)) {
+      $extender_name = $override_class;
+    }
+    return new $extender_name($this, $this->connection);
+  }
+
   /* Alter accessors to expose the query data to alter hooks. */
 
-  /**
-   * Returns a reference to the fields array for this query.
-   *
-   * Because this method returns by reference, alter hooks may edit the fields
-   * array directly to make their changes. If just adding fields, however, the
-   * use of addField() is preferred.
-   *
-   * Note that this method must be called by reference as well:
-   *
-   * @code
-   * $fields =& $query->getFields();
-   * @endcode
-   *
-   * @return
-   *   A reference to the fields array structure.
-   */
   public function &getFields() {
     return $this->fields;
   }
 
-  /**
-   * Returns a reference to the expressions array for this query.
-   *
-   * Because this method returns by reference, alter hooks may edit the expressions
-   * array directly to make their changes. If just adding expressions, however, the
-   * use of addExpression() is preferred.
-   *
-   * Note that this method must be called by reference as well:
-   *
-   * @code
-   * $fields =& $query->getExpressions();
-   * @endcode
-   *
-   * @return
-   *   A reference to the expression array structure.
-   */
   public function &getExpressions() {
     return $this->expressions;
   }
 
-  /**
-   * Returns a reference to the order by array for this query.
-   *
-   * Because this method returns by reference, alter hooks may edit the order-by
-   * array directly to make their changes. If just adding additional ordering
-   * fields, however, the use of orderBy() is preferred.
-   *
-   * Note that this method must be called by reference as well:
-   *
-   * @code
-   * $fields =& $query->getOrderBy();
-   * @endcode
-   *
-   * @return
-   *   A reference to the expression array structure.
-   */
   public function &getOrderBy() {
     return $this->order;
   }
 
-  /**
-   * Returns a reference to the tables array for this query.
-   *
-   * Because this method returns by reference, alter hooks may edit the tables
-   * array directly to make their changes. If just adding tables, however, the
-   * use of the join() methods is preferred.
-   *
-   * Note that this method must be called by reference as well:
-   *
-   * @code
-   * $fields =& $query->getTables();
-   * @endcode
-   *
-   * @return
-   *   A reference to the tables array structure.
-   */
   public function &getTables() {
     return $this->tables;
   }
 
-  /**
-   * Compiles and returns an associative array of the arguments for this prepared statement.
-   *
-   * @return
-   *   An associative array of all placeholder arguments for this query.
-   */
   public function getArguments() {
     $this->where->compile($this->connection);
     $this->having->compile($this->connection);
@@ -307,36 +858,11 @@
     return $this->connection->query((string)$this, $args, $this->queryOptions);
   }
 
-  /**
-   * Sets this query to be DISTINCT.
-   *
-   * @param $distinct
-   *   TRUE to flag this query DISTINCT, FALSE to disable it.
-   * @return
-   *   The called object.
-   */
   public function distinct($distinct = TRUE) {
     $this->distinct = $distinct;
     return $this;
   }
 
-  /**
-   * Adds a field to the list to be SELECTed.
-   *
-   * @param $table_alias
-   *   The name of the table from which the field comes, as an alias. Generally
-   *   you will want to use the return value of join() here to ensure that it is
-   *   valid.
-   * @param $field
-   *   The name of the field.
-   * @param $alias
-   *   The alias for this field. If not specified, one will be generated
-   *   automatically based on the $table_alias and $field. The alias will be
-   *   checked for uniqueness, so the requested alias may not be the alias
-   *   that is assigned in all cases.
-   * @return
-   *   The unique alias that was assigned for this field.
-   */
   public function addField($table_alias, $field, $alias = NULL) {
     // If no alias is specified, first try the field name itself.
     if (empty($alias)) {
@@ -365,27 +891,6 @@
     return $alias;
   }
 
-  /**
-   * Add multiple fields from the same table to be SELECTed.
-   *
-   * This method does not return the aliases set for the passed fields. In the
-   * majority of cases that is not a problem, as the alias will be the field
-   * name. However, if you do need to know the alias you can call getFields()
-   * and examine the result to determine what alias was created. Alternatively,
-   * simply use addField() for the few fields you care about and this method for
-   * the rest.
-   *
-   * @param $table_alias
-   *   The name of the table from which the field comes, as an alias. Generally
-   *   you will want to use the return value of join() here to ensure that it is
-   *   valid.
-   * @param $fields
-   *   An indexed array of fields present in the specified table that should be
-   *   included in this query. If not specified, $table_alias.* will be generated
-   *   without any aliases.
-   * @return
-   *   The called object.
-   */
   public function fields($table_alias, array $fields = array()) {
 
     if ($fields) {
@@ -402,32 +907,6 @@
     return $this;
   }
 
-  /**
-   * Private list of aliases already attributed to expression fields.
-   *
-   * @var Array
-   */
-  private $expressionAliases = array();
-
-  /**
-   * Adds an expression to the list of "fields" to be SELECTed.
-   *
-   * An expression can be any arbitrary string that is valid SQL. That includes
-   * various functions, which may in some cases be database-dependent. This
-   * method makes no effort to correct for database-specific functions.
-   *
-   * @param $expression
-   *   The expression string. May contain placeholders.
-   * @param $alias
-   *   The alias for this expression. If not specified, one will be generated
-   *   automatically in the form "expression_#". The alias will be checked for
-   *   uniqueness, so the requested alias may not be the alias that is assigned
-   *   in all cases.
-   * @param $arguments
-   *   Any placeholder arguments needed for this expression.
-   * @return
-   *   The unique alias that was assigned for this expression.
-   */
   public function addExpression($expression, $alias = NULL, $arguments = array()) {
     if (empty($alias)) {
       $alias = 'expression';
@@ -449,127 +928,22 @@
     return $alias;
   }
 
-  /**
-   * Default Join against another table in the database.
-   *
-   * This method is a convenience method for innerJoin().
-   *
-   * @param $table
-   *   The table against which to join.
-   * @param $alias
-   *   The alias for the table. In most cases this should be the first letter
-   *   of the table, or the first letter of each "word" in the table.
-   * @param $condition
-   *   The condition on which to join this table. If the join requires values,
-   *   this clause should use a named placeholder and the value or values to
-   *   insert should be passed in the 4th parameter. For the first table joined
-   *   on a query, this value is ignored as the first table is taken as the base
-   *   table.
-   * @param $arguments
-   *   An array of arguments to replace into the $condition of this join.
-   * @return
-   *   The unique alias that was assigned for this table.
-   */
   public function join($table, $alias = NULL, $condition = NULL, $arguments = array()) {
     return $this->addJoin('INNER', $table, $alias, $condition, $arguments);
   }
 
-  /**
-   * Inner Join against another table in the database.
-   *
-   * @param $table
-   *   The table against which to join.
-   * @param $alias
-   *   The alias for the table. In most cases this should be the first letter
-   *   of the table, or the first letter of each "word" in the table.
-   * @param $condition
-   *   The condition on which to join this table. If the join requires values,
-   *   this clause should use a named placeholder and the value or values to
-   *   insert should be passed in the 4th parameter. For the first table joined
-   *   on a query, this value is ignored as the first table is taken as the base
-   *   table.
-   * @param $arguments
-   *   An array of arguments to replace into the $condition of this join.
-   * @return
-   *   The unique alias that was assigned for this table.
-   */
   public function innerJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) {
     return $this->addJoin('INNER', $table, $alias, $condition, $arguments);
   }
 
-  /**
-   * Left Outer Join against another table in the database.
-   *
-   * @param $table
-   *   The table against which to join.
-   * @param $alias
-   *   The alias for the table. In most cases this should be the first letter
-   *   of the table, or the first letter of each "word" in the table.
-   * @param $condition
-   *   The condition on which to join this table. If the join requires values,
-   *   this clause should use a named placeholder and the value or values to
-   *   insert should be passed in the 4th parameter. For the first table joined
-   *   on a query, this value is ignored as the first table is taken as the base
-   *   table.
-   * @param $arguments
-   *   An array of arguments to replace into the $condition of this join.
-   * @return
-   *   The unique alias that was assigned for this table.
-   */
   public function leftJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) {
     return $this->addJoin('LEFT OUTER', $table, $alias, $condition, $arguments);
   }
 
-  /**
-   * Right Outer Join against another table in the database.
-   *
-   * @param $table
-   *   The table against which to join.
-   * @param $alias
-   *   The alias for the table. In most cases this should be the first letter
-   *   of the table, or the first letter of each "word" in the table.
-   * @param $condition
-   *   The condition on which to join this table. If the join requires values,
-   *   this clause should use a named placeholder and the value or values to
-   *   insert should be passed in the 4th parameter. For the first table joined
-   *   on a query, this value is ignored as the first table is taken as the base
-   *   table.
-   * @param $arguments
-   *   An array of arguments to replace into the $condition of this join.
-   * @return
-   *   The unique alias that was assigned for this table.
-   */
   public function rightJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) {
     return $this->addJoin('RIGHT OUTER', $table, $alias, $condition, $arguments);
   }
 
-  /**
-   * Join against another table in the database.
-   *
-   * This method does the "hard" work of queuing up a table to be joined against.
-   * In some cases, that may include dipping into the Schema API to find the necessary
-   * fields on which to join.
-   *
-   * @param $type
-   *   The type of join. Typically one one of INNER, LEFT OUTER, and RIGHT OUTER.
-   * @param $table
-   *   The table against which to join. May be a string or another SelectQuery
-   *   object. If a query object is passed, it will be used as a subselect.
-   * @param $alias
-   *   The alias for the table. In most cases this should be the first letter
-   *   of the table, or the first letter of each "word" in the table. If omitted,
-   *   one will be dynamically generated.
-   * @param $condition
-   *   The condition on which to join this table. If the join requires values,
-   *   this clause should use a named placeholder and the value or values to
-   *   insert should be passed in the 4th parameter. For the first table joined
-   *   on a query, this value is ignored as the first table is taken as the base
-   *   table.
-   * @param $arguments
-   *   An array of arguments to replace into the $condition of this join.
-   * @return
-   *   The unique alias that was assigned for this table.
-   */
   public function addJoin($type, $table, $alias = NULL, $condition = NULL, $arguments = array()) {
 
     if (empty($alias)) {
@@ -599,64 +973,23 @@
     return $alias;
   }
 
-  /**
-   * Orders the result set by a given field.
-   *
-   * If called multiple times, the query will order by each specified field in the
-   * order this method is called.
-   *
-   * @param $field
-   *   The field on which to order.
-   * @param $direction
-   *   The direction to sort. Legal values are "ASC" and "DESC".
-   * @return
-   *   The called object.
-   */
   public function orderBy($field, $direction = 'ASC') {
     $this->order[$field] = $direction;
     return $this;
   }
 
-  /**
-   * Restricts a query to a given range in the result set.
-   *
-   * If this method is called with no parameters, will remove any range
-   * directives that have been set.
-   *
-   * @param $start
-   *   The first record from the result set to return. If NULL, removes any
-   *   range directives that are set.
-   * @param $limit
-   *   The number of records to return from the result set.
-   * @return
-   *   The called object.
-   */
   public function range($start = NULL, $length = NULL) {
     $this->range = func_num_args() ? array('start' => $start, 'length' => $length) : array();
     return $this;
   }
 
-  /**
-   * Groups the result set by the specified field.
-   *
-   * @param $field
-   *   The field on which to group. This should be the field as aliased.
-   * @return
-   *   The called object.
-   */
   public function groupBy($field) {
     $this->group[] = $field;
+    return $this;
   }
 
-  /**
-   * Get the equivalent COUNT query of this query as a new query object.
-   *
-   * @return
-   *   A new SelectQuery object with no fields or expressions besides COUNT(*).
-   */
   public function countQuery() {
-    // Shallow-clone this query. We don't want to duplicate any of the
-    // referenced objects, so a shallow query is all we need.
+    // Create our new query object that we will mutate into a count query.
     $count = clone($this);
 
     // Zero-out existing fields and expressions.

=== modified file 'includes/pager.inc'
--- includes/pager.inc	2008-12-06 09:01:58 +0000
+++ includes/pager.inc	2009-02-19 18:33:53 +0000
@@ -6,6 +6,150 @@
  * Functions to aid in presenting database results as a set of pages.
  */
 
+
+/**
+ * Query extender for pager queries.
+ *
+ * This is the "default" pager mechanism.  It creates a paged query with a fixed
+ * number of entries per page.
+ */
+class PagerDefault extends SelectQueryExtender {
+
+  /**
+   * The highest element we've autogenerated so far.
+   *
+   * @var int
+   */
+  static protected $maxElement = 0;
+
+  /**
+   * The number of elements per page to allow.
+   *
+   * @var int
+   */
+  protected $limit = 10;
+
+  /**
+   * The unique ID of this pager on this page.
+   *
+   * @var int
+   */
+  protected $element = NULL;
+
+  /**
+   * The count query that will be used for this pager.
+   *
+   * @var SelectQueryInterface
+   */
+  protected $customCountQuery = FALSE;
+
+  /**
+   * Override the execute method.
+   *
+   * Before we run the query, we need to add pager-based range() instructions
+   * to it.
+   */
+  public function execute() {
+    global $pager_page_array, $pager_total, $pager_total_items;
+
+    // A NULL limit is the "kill switch" for pager queries.
+    if (empty($this->limit)) {
+      return;
+    }
+    $this->ensureElement();
+
+    $page = isset($_GET['page']) ? $_GET['page'] : '';
+
+    // Convert comma-separated $page to an array, used by other functions.
+    $pager_page_array = explode(',', $page);
+
+    // We calculate the total of pages as ceil(items / limit).
+    $pager_total_items[$this->element] = $this->getCountQuery()->execute()->fetchField();
+    $pager_total[$this->element] = ceil($pager_total_items[$this->element] / $this->limit);
+    $pager_page_array[$this->element] = max(0, min((int)$pager_page_array[$this->element], ((int)$pager_total[$this->element]) - 1));
+    $this->range($pager_page_array[$this->element] * $this->limit, $this->limit);
+
+    // Now that we've added our pager-based range instructions, run the query normally.
+    return $this->query->execute();
+  }
+
+  /**
+   * Ensure that there is an element associated with this query.
+   *
+   * After running this query, access $this->element to get the element for this
+   * query.
+   */
+  protected function ensureElement() {
+    if (!empty($this->element)) {
+      return;
+    }
+
+    $this->element = self::$maxElement++;
+  }
+
+  /**
+   * Specify the count query object to use for this pager.
+   *
+   * You will rarely need to specify a count query directly.  If not specified,
+   * one is generated off of the pager query itself.
+   *
+   * @param SelectQueryInterface $query
+   *   The count query object.  It must return a single row with a single column,
+   *   which is the total number of records.
+   */
+  public function setCountQuery(SelectQueryInterface $query) {
+    $this->customCountQuery = $query;
+  }
+
+  /**
+   * Retrieve the count query for this pager.
+   *
+   * The count query may be specified manually or, by default, taken from the
+   * query we are extending.
+   *
+   * @return
+   *   A count SelectQueryInterface object.
+   */
+  protected function getCountQuery() {
+    if ($this->customCountQuery) {
+      return $this->customCountQuery;
+    }
+    else {
+      return $this->query->countQuery();
+    }
+  }
+
+  /**
+   * Specify the maximum number of elements per page for this query.
+   *
+   * The default if not specified is 10 items per page.
+   *
+   * @param $limit
+   *   An integer specifying the number of elements per page.  If passed a false
+   *   value (FALSE, 0, NULL), the pager is disabled.
+   */
+  public function limit($limit = 10) {
+    $this->limit = $limit;
+    return $this;
+  }
+
+  /**
+   * Specify the element ID for this pager query.
+   *
+   * The element is used to differentiate different pager queries on the same
+   * page so that they may be operated independently.  If you do not specify an
+   * element, every pager query on the page will get a unique element.  If for
+   * whatever reason you want to explicitly define an element for a given query,
+   * you may do so here.
+   *
+   * @param $element
+   */
+  public function element($element) {
+    $this->element = $element;
+    return $this;
+  }
+}
+
 /**
  * Perform a paged database query.
  *

=== modified file 'includes/tablesort.inc'
--- includes/tablesort.inc	2009-01-28 08:00:08 +0000
+++ includes/tablesort.inc	2009-02-19 18:33:53 +0000
@@ -9,6 +9,120 @@
  * column headers that the user can click on to sort the table by that column.
  */
 
+
+/**
+ * Query extender class for tablesort queries.
+ */
+class TableSort extends SelectQueryExtender {
+
+  /**
+   * The array of fields that can be sorted by.
+   *
+   * @var array
+   */
+  protected $header = array();
+
+  public function execute() {
+    $ts = $this->init();
+    if ($ts['sql']) {
+      // Based on code from db_escape_table(), but this can also contain a dot.
+      $field = preg_replace('/[^A-Za-z0-9_.]+/', '', $ts['sql']);
+
+      // Sort order can only be ASC or DESC.
+      $sort = drupal_strtoupper($ts['sort']);
+      $sort = in_array($sort, array('ASC', 'DESC')) ? $sort : '';
+      $this->orderBy($field, $sort);
+    }
+    return $this->query->execute();
+  }
+
+  public function setHeader(Array $header) {
+    $this->header = $header;
+    return $this;
+  }
+
+  /**
+   * Initialize the table sort context.
+   */
+  protected function init() {
+    $ts = $this->order();
+    $ts['sort'] = $this->getSort();
+    $ts['query_string'] = $this->getQueryString();
+    return $ts;
+  }
+
+  /**
+   * Determine the current sort direction.
+   *
+   * @param $headers
+   *   An array of column headers in the format described in theme_table().
+   * @return
+   *   The current sort direction ("asc" or "desc").
+   */
+  protected function getSort() {
+    if (isset($_GET['sort'])) {
+      return ($_GET['sort'] == 'desc') ? 'desc' : 'asc';
+    }
+    // User has not specified a sort. Use default if specified; otherwise use "asc".
+    else {
+      foreach ($this->header as $header) {
+        if (is_array($header) && array_key_exists('sort', $header)) {
+          return $header['sort'];
+        }
+      }
+    }
+    return 'asc';
+  }
+
+  /**
+   * Compose a query string to append to table sorting requests.
+   *
+   * @return
+   *   A query string that consists of all components of the current page request
+   *   except for those pertaining to table sorting.
+   */
+  protected function getQueryString() {
+    return drupal_query_string_encode($_REQUEST, array_merge(array('q', 'sort', 'order'), array_keys($_COOKIE)));
+  }
+
+  /**
+   * Determine the current sort criterion.
+   *
+   * @param $headers
+   *   An array of column headers in the format described in theme_table().
+   * @return
+   *   An associative array describing the criterion, containing the keys:
+   *   - "name": The localized title of the table column.
+   *   - "sql": The name of the database field to sort on.
+   */
+  protected function order() {
+    $order = isset($_GET['order']) ? $_GET['order'] : '';
+    foreach ($this->header as $header) {
+      if (isset($header['data']) && $order == $header['data']) {
+        return array('name' => $header['data'], 'sql' => isset($header['field']) ? $header['field'] : '');
+      }
+
+      if (isset($header['sort']) && ($header['sort'] == 'asc' || $header['sort'] == 'desc')) {
+        $default = array('name' => $header['data'], 'sql' => isset($header['field']) ? $header['field'] : '');
+      }
+    }
+
+    if (isset($default)) {
+      return $default;
+    }
+    else {
+      // The first column specified is initial 'order by' field unless otherwise specified
+      if (is_array($this->header[0])) {
+        $this->header[0] += array('data' => NULL, 'field' => NULL);
+        return array('name' => $this->header[0]['data'], 'sql' => $this->header[0]['field']);
+      }
+      else {
+        return array('name' => $this->header[0]);
+      }
+    }
+  }
+}
+
 /**
  * Initialize the table sort context.
  */

=== modified file 'modules/comment/comment.admin.inc'
--- modules/comment/comment.admin.inc	2009-02-18 16:00:09 +0000
+++ modules/comment/comment.admin.inc	2009-02-19 18:53:32 +0000
@@ -66,13 +66,26 @@
     'operations' => array('data' => t('Operations')),
   );
 
-  $result = pager_query('SELECT c.subject, c.nid, c.cid, c.comment, c.timestamp, c.status, c.name, c.homepage, u.name AS registered_name, u.uid, n.title as node_title FROM {comment} c INNER JOIN {user} u ON u.uid = c.uid INNER JOIN {node} n ON n.nid = c.nid WHERE c.status = %d' . tablesort_sql($header), 50, 0, NULL, $status);
+    $query = db_select('comment', 'c');
+  $query->join('user', 'u', 'u.uid = c.uid');
+  $query->join('node', 'n', 'n.nid = c.nid');
+  $query->addField('u', 'name', 'registered_name');
+  $query->addField('n', 'title', 'node_title');
+  $query
+    ->fields('c', array('subject', 'nid', 'cid', 'comment', 'timestamp', 'status', 'name', 'homepage'))
+    ->fields('u', array('uid'))
+    ->condition('c.status', $status)
+    ->extend('PagerDefault')->extend('TableSort')
+    ->limit(50)
+    ->setHeader($header);
+  $result = $query->execute();
+
 
   // Build a table listing the appropriate comments.
   $options = array();
   $destination = drupal_get_destination();
 
-  while ($comment = db_fetch_object($result)) {
+  foreach ($result as $comment) {
     $options[$comment->cid] = array(
       'subject' => l($comment->subject, 'node/' . $comment->nid, array('attributes' => array('title' => truncate_utf8($comment->comment, 128)), 'fragment' => 'comment-' . $comment->cid)),
       'author' => theme('username', $comment),

=== modified file 'modules/node/node.module'
--- modules/node/node.module	2009-02-18 16:00:09 +0000
+++ modules/node/node.module	2009-02-19 18:33:53 +0000
@@ -2024,7 +2024,18 @@
  * Menu callback; Generate a listing of promoted nodes.
  */
 function node_page_default() {
-  $nids = pager_query(db_rewrite_sql('SELECT n.nid, n.sticky, n.created FROM {node} n WHERE n.promote = 1 AND n.status = 1 ORDER BY n.sticky DESC, n.created DESC'), variable_get('default_nodes_main', 10))->fetchCol();
+  $select = db_select('node', 'n')
+    ->fields('n', array('nid'))
+    ->condition('promote', 1)
+    ->condition('status', 1)
+    ->orderBy('sticky', 'DESC')
+    ->orderBy('created', 'DESC')
+    ->extend('PagerDefault')
+    ->limit(variable_get('default_nodes_main', 10))
+    ->addTag('node_access');
+
+  $nids = $select->execute()->fetchCol();
+
   if (!empty($nids)) {
     $nodes = node_load_multiple($nids);
     $build = node_build_multiple($nodes);

=== modified file 'modules/simpletest/tests/database_test.module'
--- modules/simpletest/tests/database_test.module	2009-01-11 11:00:08 +0000
+++ modules/simpletest/tests/database_test.module	2009-02-20 09:35:54 +0000
@@ -48,6 +48,19 @@
     'access callback' => TRUE,
     'page callback' => 'database_test_db_query_temporary',
   );
+  $items['database_test/pager_query_even'] = array(
+    'access callback' => TRUE,
+    'page callback' => 'database_test_even_pager_query',
+  );
+  $items['database_test/pager_query_odd'] = array(
+    'access callback' => TRUE,
+    'page callback' => 'database_test_odd_pager_query',
+  );
+  $items['database_test/tablesort'] = array(
+    'access callback' => TRUE,
+    'page callback' => 'database_test_tablesort',
+  );
+
   return $items;
 }
 
@@ -66,3 +79,80 @@
   ));
   exit;
 }
+
+/**
+ * Run a pager query and return the results.
+ *
+ * This function does care about the page GET parameter, as set by the
+ * simpletest HTTP call.
+ */
+function database_test_even_pager_query($limit) {
+
+  $query = db_select('test', 't');
+  $query
+    ->fields('t', array('name'))
+    ->orderBy('age');
+
+  // This should result in 2 pages of results.
+  $query = $query->extend('PagerDefault')->limit($limit);
+
+  $names = $query->execute()->fetchCol();
+
+  drupal_json(array(
+    'names' => $names,
+  ));
+  exit;
+}
+
+/**
+ * Run a pager query and return the results.
+ *
+ * This function does care about the page GET parameter, as set by the
+ * simpletest HTTP call.
+ */
+function database_test_odd_pager_query($limit) {
+
+  $query = db_select('test_task', 't');
+  $query
+    ->fields('t', array('task'))
+    ->orderBy('pid');
+
+  // This should result in 4 pages of results.
+  $query = $query->extend('PagerDefault')->limit($limit);
+
+  $names = $query->execute()->fetchCol();
+
+  drupal_json(array(
+    'names' => $names,
+  ));
+  exit;
+}
+
+/**
+ * Run a tablesort query and return the results.
+ *
+ * This function does care about the page GET parameter, as set by the
+ * simpletest HTTP call.
+ */
+function database_test_tablesort() {
+  $header = array(
+    'tid' => array('data' => t('Task ID'), 'field' => 'tid', 'sort' => 'desc'),
+    'pid' => array('data' => t('Person ID'), 'field' => 'pid'),
+    'task' => array('data' => t('Task'), 'field' => 'task'),
+    'priority' => array('data' => t('Priority'), 'field' => 'priority', ),
+  );
+
+  $query = db_select('test_task', 't');
+  $query
+    ->fields('t', array('tid', 'pid', 'task', 'priority'));
+  
+  $query = $query->extend('TableSort')->setHeader($header);
+
+  // We need all the results at once to check the sort.
+  $tasks = $query->execute()->fetchAll();
+
+  drupal_json(array(
+    'tasks' => $tasks,
+  ));
+  exit;
+}
\ No newline at end of file

=== modified file 'modules/simpletest/tests/database_test.test'
--- modules/simpletest/tests/database_test.test	2009-02-18 16:00:09 +0000
+++ modules/simpletest/tests/database_test.test	2009-02-22 00:09:13 +0000
@@ -1478,6 +1478,125 @@
   }
 }
 
+class DatabaseSelectPagerDefaultTestCase extends DatabaseTestCase {
+
+  function getInfo() {
+    return array(
+      'name' => t('Pager query tests'),
+      'description' => t('Test the pager query extender.'),
+      'group' => t('Database'),
+    );
+  }
+
+  /**
+   * Confirm that a pager query returns the correct results.
+   *
+   * Note that we have to make an HTTP request to a test page handler
+   * because the pager depends on GET parameters.
+   */
+  function testEvenPagerQuery() {
+    // To keep the test from being too brittle, we determine up front
+    // what the page count should be dynamically, and pass the control
+    // information forward to the actual query on the other side of the
+    // HTTP request.
+    $limit = 2;
+    $count = db_query("SELECT COUNT(*) FROM {test}")->fetchField();
+
+    $correct_number = $limit;
+    $num_pages = floor($count / $limit);
+
+    // If there is no remainder from rounding, subtract 1 since we index from 0.
+    if (!($num_pages * $limit < $count)) {
+      $num_pages--;
+    }
+    
+    for ($page = 0; $page <= $num_pages; ++$page) {
+      $this->drupalGet('database_test/pager_query_even/' . $limit, array('query' => array('page' => $page)));
+      $data = json_decode($this->drupalGetContent());
+
+      if ($page == $num_pages) {
+        $correct_number = $count - ($limit * $page);
+      }
+
+      $this->assertEqual(count($data->names), $correct_number, t('Correct number of records returned by pager: @number', array('@number' => $correct_number)));
+    }
+  }
+
+  /**
+   * Confirm that a pager query returns the correct results.
+   *
+   * Note that we have to make an HTTP request to a test page handler
+   * because the pager depends on GET parameters.
+   */
+  function testOddPagerQuery() {
+    // To keep the test from being too brittle, we determine up front
+    // what the page count should be dynamically, and pass the control
+    // information forward to the actual query on the other side of the
+    // HTTP request.
+    $limit = 2;
+    $count = db_query("SELECT COUNT(*) FROM {test_task}")->fetchField();
+
+    $correct_number = $limit;
+    $num_pages = floor($count / $limit);
+
+    // If there is no remainder from rounding, subtract 1 since we index from 0.
+    if (!($num_pages * $limit < $count)) {
+      $num_pages--;
+    }
+
+    for ($page = 0; $page <= $num_pages; ++$page) {
+      $this->drupalGet('database_test/pager_query_odd/' . $limit, array('query' => array('page' => $page)));
+      $data = json_decode($this->drupalGetContent());
+
+      if ($page == $num_pages) {
+        $correct_number = $count - ($limit * $page);
+      }
+
+      $this->assertEqual(count($data->names), $correct_number, t('Correct number of records returned by pager: @number', array('@number' => $correct_number)));
+    }
+  }
+}
+
+
+class DatabaseSelectTableSortDefaultTestCase extends DatabaseTestCase {
+
+  function getInfo() {
+    return array(
+      'name' => t('Tablesort query tests'),
+      'description' => t('Test the tablesort query extender.'),
+      'group' => t('Database'),
+    );
+  }
+
+  /**
+   * Confirm that a tablesort query returns the correct results.
+   *
+   * Note that we have to make an HTTP request to a test page handler
+   * because the pager depends on GET parameters.
+   */
+  function testTableSortQuery() {
+    $sorts = array(
+      array('field' => t('Task ID'), 'sort' => 'desc', 'first' => 'perform at superbowl', 'last' => 'eat'),
+      array('field' => t('Task ID'), 'sort' => 'asc', 'first' => 'eat', 'last' => 'perform at superbowl'),
+      array('field' => t('Task'), 'sort' => 'asc', 'first' => 'code', 'last' => 'sleep'),
+      array('field' => t('Task'), 'sort' => 'desc', 'first' => 'sleep', 'last' => 'code'),
+      // more elements here
+
+    );
+
+    foreach ($sorts as $sort) {
+      $this->drupalGet('database_test/tablesort/', array('query' => array('order' => $sort['field'], 'sort' => $sort['sort'])));
+      $data = json_decode($this->drupalGetContent());
+
+      $first = array_shift($data->tasks);
+      $last = array_pop($data->tasks);
+
+      $this->assertEqual($first->task, $sort['first'], t('Items appear in the correct order.'));
+      $this->assertEqual($last->task, $sort['last'], t('Items appear in the correct order.'));
+    }
+  }
+}
+
 /**
  * Select tagging tests.
  *

