Index: includes/database/query.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/database/query.inc,v retrieving revision 1.49 diff -u -p -r1.49 query.inc --- includes/database/query.inc 15 May 2010 07:04:21 -0000 1.49 +++ includes/database/query.inc 21 May 2010 23:39:03 -0000 @@ -247,6 +247,13 @@ abstract class Query implements QueryPla */ protected $comments = array(); + /** + * Query hints or flags that can be set on a query. + * + * @var string + */ + protected $hints = ''; + public function __construct(DatabaseConnection $connection, $options) { $this->connection = $connection; $this->queryOptions = $options; @@ -308,6 +315,24 @@ abstract class Query implements QueryPla public function &getComments() { return $this->comments; } + + /** + * Sets the hints or flags on a query. + * + * Hints and flags directly affect the way in which the query is run + * on the database server. Their syntax is database specific, so they + * should be used internally in database-specific classes only. + * + * Because there is no standardisation of hints across databases, there + * is not currently any support for setting multiple hints via multiple + * calls to this method. Any new hint will override a previously set one. + * + * @param $hints + * The hint string to be inserted into the query. + */ + public function setHints($hints) { + $this->hints = $hints; + } } /** Index: includes/database/mysql/query.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/database/mysql/query.inc,v retrieving revision 1.17 diff -u -p -r1.17 query.inc --- includes/database/mysql/query.inc 15 May 2010 07:04:21 -0000 1.17 +++ includes/database/mysql/query.inc 21 May 2010 23:39:03 -0000 @@ -46,16 +46,19 @@ class InsertQuery_mysql extends InsertQu // Create a comments string to prepend to the query. $comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : ''; + // Create a hints string to include in the query. + $hints = (!empty($this->hints)) ? $this->hints . ' ' : ''; + // Default fields are always placed first for consistency. $insert_fields = array_merge($this->defaultFields, $this->insertFields); // If we're selecting from a SelectQuery, finish building the query and // pass it back, as any remaining options are irrelevant. if (!empty($this->fromQuery)) { - return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') ' . $this->fromQuery; + return $comments . 'INSERT ' . $hints . 'INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') ' . $this->fromQuery; } - $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES '; + $query = $comments . 'INSERT ' . $hints . 'INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES '; $max_placeholder = 0; $values = array(); @@ -143,11 +146,13 @@ class MergeQuery_mysql extends MergeQuer return $this->connection->query((string) $this, $values, $this->queryOptions); } - public function __toString() { // Create a comments string to prepend to the query. $comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : ''; + // Create a hints string to include in the query. + $hints = (!empty($this->hints)) ? $this->hints . ' ' : ''; + // Set defaults. if ($this->updateFields) { $update_fields = $this->updateFields; @@ -168,7 +173,7 @@ class MergeQuery_mysql extends MergeQuer $insert_fields = $this->insertFields + $this->keyFields; - $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', array_keys($insert_fields)) . ') VALUES '; + $query = $comments . 'INSERT ' . $hints . 'INTO {' . $this->table . '} (' . implode(', ', array_keys($insert_fields)) . ') VALUES '; $max_placeholder = 0; $values = array(); @@ -205,6 +210,290 @@ class MergeQuery_mysql extends MergeQuer } } +class DeleteQuery_mysql extends DeleteQuery { + + /** + * Override this for DeleteQuery_mysql to include support for query hints. + */ + public function __toString() { + // Create a comments string to prepend to the query. + $comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : ''; + + // Create a hints string to include in the query. + $hints = (!empty($this->hints)) ? $this->hints . ' ' : ''; + + $query = $comments . 'DELETE ' . $hints . 'FROM {' . $this->connection->escapeTable($this->table) . '} '; + + if (count($this->condition)) { + + $this->condition->compile($this->connection, $this); + $query .= "\nWHERE " . $this->condition; + } + + return $query; + } +} + +class TruncateQuery_mysql extends TruncateQuery { + + /** + * Override this for TruncateQuery_mysql to include support for query hints. + */ + public function __toString() { + // Create a comments string to prepend to the query. + $comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : ''; + + // Create a hints string to include in the query. + $hints = (!empty($this->hints)) ? $this->hints . ' ' : ''; + + return $comments . 'TRUNCATE ' . $hints . '{' . $this->connection->escapeTable($this->table) . '} '; + } +} + +class UpdateQuery_mysql extends UpdateQuery { + + /** + * Override this for UpdateQuery_mysql to include support for query hints. + */ + public function __toString() { + // Create a comments string to prepend to the query. + $comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : ''; + + // Create a hints string to include in the query. + $hints = (!empty($this->hints)) ? $this->hints . ' ' : ''; + + // Expressions take priority over literal fields, so we process those first + // and remove any literal fields that conflict. + $fields = $this->fields; + $update_fields = array(); + foreach ($this->expressionFields as $field => $data) { + $update_fields[] = $field . '=' . $data['expression']; + unset($fields[$field]); + } + + $max_placeholder = 0; + foreach ($fields as $field => $value) { + $update_fields[] = $field . '=:db_update_placeholder_' . ($max_placeholder++); + } + + $query = $comments . 'UPDATE ' . $hints . '{' . $this->connection->escapeTable($this->table) . '} SET ' . implode(', ', $update_fields); + + if (count($this->condition)) { + $this->condition->compile($this->connection, $this); + // There is an implicit string cast on $this->condition. + $query .= "\nWHERE " . $this->condition; + } + + return $query; + } +} + +class SelectQuery_mysql extends SelectQuery { + + /** + * Override this for SelectQuery_mysql to include support for query hints. + */ + public function __toString() { + // Create a comments string to prepend to the query. + $comments = (!empty($this->comments)) ? '/* ' . implode('; ', $this->comments) . ' */ ' : ''; + + // Create a hints string to include in the query. + $hints = (!empty($this->hints)) ? $this->hints . ' ' : ''; + + // SELECT + $query = $comments . 'SELECT ' . $hints; + + if ($this->distinct) { + $query .= 'DISTINCT '; + } + + // FIELDS and EXPRESSIONS + $fields = array(); + foreach ($this->tables as $alias => $table) { + if (!empty($table['all_fields'])) { + $fields[] = $alias . '.*'; + } + } + foreach ($this->fields as $alias => $field) { + // Always use the AS keyword for field aliases, as some + // databases require it (e.g., PostgreSQL). + $fields[] = (isset($field['table']) ? $field['table'] . '.' : '') . $field['field'] . ' AS ' . $field['alias']; + } + foreach ($this->expressions as $alias => $expression) { + $fields[] = $expression['expression'] . ' AS ' . $expression['alias']; + } + $query .= implode(', ', $fields); + + + // FROM - We presume all queries have a FROM, as any query that doesn't won't need the query builder anyway. + $query .= "\nFROM "; + foreach ($this->tables as $alias => $table) { + $query .= "\n"; + if (isset($table['join type'])) { + $query .= $table['join type'] . ' JOIN '; + } + + // If the table is a subquery, compile it and integrate it into this query. + if ($table['table'] instanceof SelectQueryInterface) { + // Run preparation steps on this sub-query before converting to string. + $subquery = $table['table']; + $subquery->preExecute(); + $table_string = '(' . (string) $subquery . ')'; + } + else { + $table_string = '{' . $this->connection->escapeTable($table['table']) . '}'; + } + + // Don't use the AS keyword for table aliases, as some + // databases don't support it (e.g., Oracle). + $query .= $table_string . ' ' . $table['alias']; + + if (!empty($table['condition'])) { + $query .= ' ON ' . $table['condition']; + } + } + + // WHERE + if (count($this->where)) { + $this->where->compile($this->connection, $this); + // There is an implicit string cast on $this->condition. + $query .= "\nWHERE " . $this->where; + } + + // GROUP BY + if ($this->group) { + $query .= "\nGROUP BY " . implode(', ', $this->group); + } + + // HAVING + if (count($this->having)) { + $this->having->compile($this->connection, $this); + // There is an implicit string cast on $this->having. + $query .= "\nHAVING " . $this->having; + } + + // ORDER BY + if ($this->order) { + $query .= "\nORDER BY "; + $fields = array(); + foreach ($this->order as $field => $direction) { + $fields[] = $field . ' ' . $direction; + } + $query .= implode(', ', $fields); + } + + // RANGE + // There is no universal SQL standard for handling range or limit clauses. + // Fortunately, all core-supported databases use the same range syntax. + // Databases that need a different syntax can override this method and + // do whatever alternate logic they need to. + if (!empty($this->range)) { + $query .= "\nLIMIT " . $this->range['length'] . " OFFSET " . $this->range['start']; + } + + // UNION is a little odd, as the select queries to combine are passed into + // this query, but syntactically they all end up on the same level. + if ($this->union) { + foreach ($this->union as $union) { + $query .= ' ' . $union['type'] . ' ' . (string) $union['query']; + } + } + + return $query; + } +} + + +/** + * Query extender for mysql pager queries. + * + * This is the mysql specific pager mechanism. It creates a paged query with a fixed + * number of entries per page and simultaneously counts the total number of rows. + */ +class PagerDefault_mysql extends PagerDefault { + + /** + * 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, $pager_limits; + + // Add convenience tag to mark that this is an extended query. We have to + // do this in the constructor to ensure that it is set before preExecute() + // gets called. + if (!$this->preExecute($this)) { + return NULL; + } + + // 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); + + if (!isset($pager_page_array[$this->element])) { + $pager_page_array[$this->element] = 0; + } + + // Rather than rely on the countQuery, add a query hint to prepare for a + // FOUND_ROWS() call a bit later on. That means we don't need to load the + // entire table index twice, which should see a nice speed increase when + // paging through large tables, esepecially on InnoDB. + $this->query->setHints('SQL_CALC_FOUND_ROWS'); + + // We usually calculate the total number of pages as ceil(items / limit) but + // now that we don't run a coutnQuery, we don't know the total number of rows + // before running the pager_query. For now we assume that the total items is + // the current page number multiplied by the limit. We recalculate when we + // know the correct total number. + $this->range((int)$page * $this->limit, $this->limit); + + // Now that we've added our pager-based range instructions, run the query + // normally. + $result = $this->query->execute(); + + // And now we can fetch the total number of results, had there not been + // a range. + $pager_total_items[$this->element] = $this->connection->query('SELECT FOUND_ROWS()')->fetchField(); + + // This does unfortunately mean that a user can override the page variable + // in the query string and end up with an empty pager. Previously this would + // make this default to the last page. We can emulate this behaviour by + // checking the total number of rows returned and re-run an adjusted query + // in that case. + // + // What we do now is check if our query range was out of bounds, which can + // happen if the user manually overrode the page variable in the query + // string. If no rows were returned and if we're not looking at the first + // page of this pager, adjust the range to return the last page of results + // only, just as we would normally do. + if (((int)$page * $this->limit) > $pager_total_items[$this->element] && (int)$page) { + // Calculate the new range and re-run the pager query to return the last + // page of results. + $page_max = ceil($pager_total_items[$this->element] / $this->limit) - 1; + $this->range($page_max * $this->limit, $this->limit); + // No need to re-count all rows, so remove the hint. + $this->query->setHints(''); + $result = $this->query->execute(); + } + + // Redo the page number calculation, because we now know the total humber of results. + $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)); + $pager_limits[$this->element] = $this->limit; + + return $result; + } +} + /** * @} End of "ingroup database". */