diff options
author | Dries Buytaert <dries@buytaert.net> | 2009-02-22 16:53:41 +0000 |
---|---|---|
committer | Dries Buytaert <dries@buytaert.net> | 2009-02-22 16:53:41 +0000 |
commit | b3e36d655c831c63c26a710eb3c8bd82ca3b6fc5 (patch) | |
tree | af24f8462bd347de36cd7375fc78dffb48e7d0b9 | |
parent | e1652e99b61577ac42d6d6618420c872c0e42435 (diff) | |
download | brdo-b3e36d655c831c63c26a710eb3c8bd82ca3b6fc5.tar.gz brdo-b3e36d655c831c63c26a710eb3c8bd82ca3b6fc5.tar.bz2 |
- Patch #299267 by Crell: add extender support to the SELECT query builder.
-rw-r--r-- | includes/database/query.inc | 12 | ||||
-rw-r--r-- | includes/database/select.inc | 915 | ||||
-rw-r--r-- | includes/pager.inc | 144 | ||||
-rw-r--r-- | includes/tablesort.inc | 114 | ||||
-rw-r--r-- | modules/comment/comment.admin.inc | 17 | ||||
-rw-r--r-- | modules/node/node.module | 13 | ||||
-rw-r--r-- | modules/simpletest/tests/database_test.module | 90 | ||||
-rw-r--r-- | modules/simpletest/tests/database_test.test | 119 |
8 files changed, 1128 insertions, 296 deletions
diff --git a/includes/database/query.inc b/includes/database/query.inc index 7186c5a6a..dd6a4da9a 100644 --- a/includes/database/query.inc +++ b/includes/database/query.inc @@ -108,6 +108,8 @@ interface QueryAlterableInterface { * * @param $tag * The tag to add. + * @return + * The called object. */ public function addTag($tag); @@ -154,7 +156,8 @@ interface QueryAlterableInterface { * 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 class Query { 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(); } diff --git a/includes/database/select.inc b/includes/database/select.inc index 532b2e5a1..bef03f756 100644 --- a/includes/database/select.inc +++ b/includes/database/select.inc @@ -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 @@ class SelectQuery extends Query implements QueryConditionInterface, QueryAlterab public function addTag($tag) { $this->alterTags[$tag] = 1; + return $this; } public function hasTag($tag) { @@ -120,6 +730,7 @@ class SelectQuery extends Query implements QueryConditionInterface, QueryAlterab public function addMetaData($key, $object) { $this->alterMetaData[$key] = $object; + return $this; } public function getMetaData($key) { @@ -180,94 +791,34 @@ class SelectQuery extends Query implements QueryConditionInterface, QueryAlterab 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 @@ class SelectQuery extends Query implements QueryConditionInterface, QueryAlterab 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 @@ class SelectQuery extends Query implements QueryConditionInterface, QueryAlterab 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 @@ class SelectQuery extends Query implements QueryConditionInterface, QueryAlterab 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 @@ class SelectQuery extends Query implements QueryConditionInterface, QueryAlterab 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 @@ class SelectQuery extends Query implements QueryConditionInterface, QueryAlterab 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. diff --git a/includes/pager.inc b/includes/pager.inc index 4b219a8ca..a3c80ae5e 100644 --- a/includes/pager.inc +++ b/includes/pager.inc @@ -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. * diff --git a/includes/tablesort.inc b/includes/tablesort.inc index 36f01607f..382014fa8 100644 --- a/includes/tablesort.inc +++ b/includes/tablesort.inc @@ -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. */ diff --git a/modules/comment/comment.admin.inc b/modules/comment/comment.admin.inc index 9a35014b9..8d88ec77e 100644 --- a/modules/comment/comment.admin.inc +++ b/modules/comment/comment.admin.inc @@ -66,13 +66,26 @@ function comment_admin_overview($type = 'new', $arg) { '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), diff --git a/modules/node/node.module b/modules/node/node.module index 2f1d38bec..e601f2260 100644 --- a/modules/node/node.module +++ b/modules/node/node.module @@ -2024,7 +2024,18 @@ function node_build_multiple($nodes, $teaser = TRUE, $weight = 0) { * 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); diff --git a/modules/simpletest/tests/database_test.module b/modules/simpletest/tests/database_test.module index 573075078..2c1d22597 100644 --- a/modules/simpletest/tests/database_test.module +++ b/modules/simpletest/tests/database_test.module @@ -48,6 +48,19 @@ function database_test_menu() { '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 @@ function database_test_db_query_temporary() { )); 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 diff --git a/modules/simpletest/tests/database_test.test b/modules/simpletest/tests/database_test.test index faaeb01ab..09d209bc4 100644 --- a/modules/simpletest/tests/database_test.test +++ b/modules/simpletest/tests/database_test.test @@ -1530,6 +1530,125 @@ class DatabaseSelectComplexTestCase extends DatabaseTestCase { } } +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. * |