- Patch #299267 by Crell: add extender support to the SELECT query builder.
parent
e1652e99b6
commit
b3e36d655c
|
@ -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();
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue