Skip to content
......@@ -28,20 +28,23 @@
* Most Drupal database SELECT queries are performed by a call to db_query() or
* db_query_range(). Module authors should also consider using the PagerDefault
* Extender for queries that return results that need to be presented on
* multiple pages, and the Tablesort Extender for generating appropriate queries
* for sortable tables.
* multiple pages (see https://drupal.org/node/508796), and the TableSort
* Extender for generating appropriate queries for sortable tables
* (see https://drupal.org/node/1848372).
*
* For example, one might wish to return a list of the most recent 10 nodes
* authored by a given user. Instead of directly issuing the SQL query
* @code
* SELECT n.nid, n.title, n.created FROM node n WHERE n.uid = $uid LIMIT 0, 10;
* SELECT n.nid, n.title, n.created FROM node n WHERE n.uid = $uid
* ORDER BY n.created DESC LIMIT 0, 10;
* @endcode
* one would instead call the Drupal functions:
* @code
* $result = db_query_range('SELECT n.nid, n.title, n.created
* FROM {node} n WHERE n.uid = :uid', 0, 10, array(':uid' => $uid));
* FROM {node} n WHERE n.uid = :uid
* ORDER BY n.created DESC', 0, 10, array(':uid' => $uid));
* foreach ($result as $record) {
* // Perform operations on $node->title, etc. here.
* // Perform operations on $record->title, etc. here.
* }
* @endcode
* Curly braces are used around "node" to provide table prefixing via
......@@ -152,7 +155,7 @@
* }
*
* // $txn goes out of scope here. Unless the transaction was rolled back, it
* // gets automatically commited here.
* // gets automatically committed here.
* }
*
* function my_other_function($id) {
......@@ -167,7 +170,7 @@
* }
* @endcode
*
* @link http://drupal.org/developing/api/database
* @see http://drupal.org/developing/api/database
*/
......@@ -179,7 +182,7 @@
* concrete implementation of it to support special handling required by that
* database.
*
* @see http://php.net/manual/en/book.pdo.php
* @see http://php.net/manual/book.pdo.php
*/
abstract class DatabaseConnection extends PDO {
......@@ -194,7 +197,7 @@ abstract class DatabaseConnection extends PDO {
/**
* The key representing this connection.
*
*
* The key is a unique string which identifies a database connection. A
* connection can be a single server or a cluster of master and slaves (use
* target to pick between master and slave).
......@@ -293,6 +296,20 @@ abstract class DatabaseConnection extends PDO {
*/
protected $prefixReplace = array();
/**
* List of escaped database, table, and field names, keyed by unescaped names.
*
* @var array
*/
protected $escapedNames = array();
/**
* List of escaped aliases names, keyed by unescaped aliases.
*
* @var array
*/
protected $escapedAliases = array();
function __construct($dsn, $username, $password, $driver_options = array()) {
// Initialize and prepare the connection prefix.
$this->setPrefix(isset($this->connectionOptions['prefix']) ? $this->connectionOptions['prefix'] : '');
......@@ -303,12 +320,28 @@ function __construct($dsn, $username, $password, $driver_options = array()) {
// Call PDO::__construct and PDO::setAttribute.
parent::__construct($dsn, $username, $password, $driver_options);
// Set a specific PDOStatement class if the driver requires that.
// Set a Statement class, unless the driver opted out.
if (!empty($this->statementClass)) {
$this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array($this->statementClass, array($this)));
}
}
/**
* Destroys this Connection object.
*
* PHP does not destruct an object if it is still referenced in other
* variables. In case of PDO database connection objects, PHP only closes the
* connection when the PDO object is destructed, so any references to this
* object may cause the number of maximum allowed connections to be exceeded.
*/
public function destroy() {
// Destroy all references to this connection by setting them to NULL.
// The Statement class attribute only accepts a new value that presents a
// proper callable, so we reset it to PDOStatement.
$this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('PDOStatement', array()));
$this->schema = NULL;
}
/**
* Returns the default query options for any given query.
*
......@@ -607,7 +640,7 @@ public function makeComment($comments) {
* A sanitized version of the query comment string.
*/
protected function filterComment($comment = '') {
return preg_replace('/(\/\*\s*)|(\s*\*\/)/', '', $comment);
return strtr($comment, array('*' => ' * '));
}
/**
......@@ -637,7 +670,7 @@ protected function filterComment($comment = '') {
* @return DatabaseStatementInterface
* This method will return one of: the executed statement, the number of
* rows affected by the query (not the number matched), or the generated
* insert IT of the last query, depending on the value of
* insert ID of the last query, depending on the value of
* $options['return']. Typically that value will be set by default or a
* query builder and should not be set by a user. If there is an error,
* this method will return NULL and may throw an exception if
......@@ -717,7 +750,7 @@ protected function expandArguments(&$query, &$args) {
// to expand it out into a comma-delimited set of placeholders.
foreach (array_filter($args, 'is_array') as $key => $data) {
$new_keys = array();
foreach ($data as $i => $value) {
foreach (array_values($data) as $i => $value) {
// This assumes that there are no other placeholders that use the same
// name. For example, if the array placeholder is defined as :example
// and there is already an :example_2 placeholder, this will generate
......@@ -900,11 +933,14 @@ public function schema() {
* For some database drivers, it may also wrap the table name in
* database-specific escape characters.
*
* @return
* @return string
* The sanitized table name string.
*/
public function escapeTable($table) {
return preg_replace('/[^A-Za-z0-9_.]+/', '', $table);
if (!isset($this->escapedNames[$table])) {
$this->escapedNames[$table] = preg_replace('/[^A-Za-z0-9_.]+/', '', $table);
}
return $this->escapedNames[$table];
}
/**
......@@ -914,11 +950,14 @@ public function escapeTable($table) {
* For some database drivers, it may also wrap the field name in
* database-specific escape characters.
*
* @return
* @return string
* The sanitized field name string.
*/
public function escapeField($field) {
return preg_replace('/[^A-Za-z0-9_.]+/', '', $field);
if (!isset($this->escapedNames[$field])) {
$this->escapedNames[$field] = preg_replace('/[^A-Za-z0-9_.]+/', '', $field);
}
return $this->escapedNames[$field];
}
/**
......@@ -929,11 +968,14 @@ public function escapeField($field) {
* DatabaseConnection::escapeTable(), this doesn't allow the period (".")
* because that is not allowed in aliases.
*
* @return
* @return string
* The sanitized field name string.
*/
public function escapeAlias($field) {
return preg_replace('/[^A-Za-z0-9_]+/', '', $field);
if (!isset($this->escapedAliases[$field])) {
$this->escapedAliases[$field] = preg_replace('/[^A-Za-z0-9_]+/', '', $field);
}
return $this->escapedAliases[$field];
}
/**
......@@ -988,6 +1030,9 @@ public function transactionDepth() {
* @param $name
* Optional name of the savepoint.
*
* @return DatabaseTransaction
* A DatabaseTransaction object.
*
* @see DatabaseTransaction
*/
public function startTransaction($name = '') {
......@@ -1016,9 +1061,9 @@ public function rollback($savepoint_name = 'drupal_transaction') {
throw new DatabaseTransactionNoActiveException();
}
// A previous rollback to an earlier savepoint may mean that the savepoint
// in question has already been rolled back.
if (!in_array($savepoint_name, $this->transactionLayers)) {
return;
// in question has already been accidentally committed.
if (!isset($this->transactionLayers[$savepoint_name])) {
throw new DatabaseTransactionNoActiveException();
}
// We need to find the point we're rolling back to, all other savepoints
......@@ -1096,8 +1141,12 @@ public function popTransaction($name) {
if (!$this->supportsTransactions()) {
return;
}
// The transaction has already been committed earlier. There is nothing we
// need to do. If this transaction was part of an earlier out-of-order
// rollback, an exception would already have been thrown by
// Database::rollback().
if (!isset($this->transactionLayers[$name])) {
throw new DatabaseTransactionNoActiveException();
return;
}
// Mark this layer as committable.
......@@ -1287,6 +1336,39 @@ public function commit() {
* also larger than the $existing_id if one was passed in.
*/
abstract public function nextId($existing_id = 0);
/**
* Checks whether utf8mb4 support is configurable in settings.php.
*
* @return bool
*/
public function utf8mb4IsConfigurable() {
// Since 4 byte UTF-8 is not supported by default, there is nothing to
// configure.
return FALSE;
}
/**
* Checks whether utf8mb4 support is currently active.
*
* @return bool
*/
public function utf8mb4IsActive() {
// Since 4 byte UTF-8 is not supported by default, there is nothing to
// activate.
return FALSE;
}
/**
* Checks whether utf8mb4 support is available on the current database system.
*
* @return bool
*/
public function utf8mb4IsSupported() {
// By default we assume that the database backend may not support 4 byte
// UTF-8.
return FALSE;
}
}
/**
......@@ -1426,9 +1508,6 @@ final public static function getLog($logging_key, $key = 'default') {
/**
* Gets the connection object for the specified database key and target.
*
* Note: do not use the setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE) on the
* returned object because of http://bugs.php.net/bug.php?id=43139.
*
* @param $target
* The database target name.
* @param $key
......@@ -1623,8 +1702,8 @@ final public static function renameConnection($old_key, $new_key) {
*/
final public static function removeConnection($key) {
if (isset(self::$databaseInfo[$key])) {
self::closeConnection(NULL, $key);
unset(self::$databaseInfo[$key]);
unset(self::$connections[$key]);
return TRUE;
}
else {
......@@ -1690,11 +1769,24 @@ public static function closeConnection($target = NULL, $key = NULL) {
if (!isset($key)) {
$key = self::$activeKey;
}
// To close the connection, we need to unset the static variable.
// To close a connection, it needs to be set to NULL and removed from the
// static variable. In all cases, closeConnection() might be called for a
// connection that was not opened yet, in which case the key is not defined
// yet and we just ensure that the connection key is undefined.
if (isset($target)) {
if (isset(self::$connections[$key][$target])) {
self::$connections[$key][$target]->destroy();
self::$connections[$key][$target] = NULL;
}
unset(self::$connections[$key][$target]);
}
else {
if (isset(self::$connections[$key])) {
foreach (self::$connections[$key] as $target => $connection) {
self::$connections[$key][$target]->destroy();
self::$connections[$key][$target] = NULL;
}
}
unset(self::$connections[$key]);
}
}
......@@ -1848,8 +1940,8 @@ class DatabaseTransaction {
*/
protected $name;
public function __construct(DatabaseConnection &$connection, $name = NULL) {
$this->connection = &$connection;
public function __construct(DatabaseConnection $connection, $name = NULL) {
$this->connection = $connection;
// If there is no transaction depth, then no transaction has started. Name
// the transaction 'drupal_transaction'.
if (!$depth = $connection->transactionDepth()) {
......@@ -1899,21 +1991,19 @@ public function rollback() {
}
/**
* A prepared statement.
* Represents a prepared statement.
*
* Some methods in that class are purposely commented out. Due to a change in
* Some methods in that class are purposefully commented out. Due to a change in
* how PHP defines PDOStatement, we can't define a signature for those methods
* that will work the same way between versions older than 5.2.6 and later
* versions.
*
* Please refer to http://bugs.php.net/bug.php?id=42452 for more details.
* versions. See http://bugs.php.net/bug.php?id=42452 for more details.
*
* Child implementations should either extend PDOStatement:
* @code
* class DatabaseStatement_oracle extends PDOStatement implements DatabaseStatementInterface {}
* @endcode
* or implement their own class, but in that case they will also have to
* implement the Iterator or IteratorArray interfaces before
* or define their own class. If defining their own class, they will also have
* to implement either the Iterator or IteratorAggregate interface before
* DatabaseStatementInterface:
* @code
* class DatabaseStatement_oracle implements Iterator, DatabaseStatementInterface {}
......@@ -1955,7 +2045,7 @@ public function rowCount();
/**
* Sets the default fetch mode for this statement.
*
* See http://php.net/manual/en/pdo.constants.php for the definition of the
* See http://php.net/manual/pdo.constants.php for the definition of the
* constants used.
*
* @param $mode
......@@ -1974,7 +2064,7 @@ public function rowCount();
/**
* Fetches the next row from a result set.
*
* See http://php.net/manual/en/pdo.constants.php for the definition of the
* See http://php.net/manual/pdo.constants.php for the definition of the
* constants used.
*
* @param $mode
......@@ -1997,7 +2087,7 @@ public function rowCount();
* The numeric index of the field to return. Defaults to the first field.
*
* @return
* A single field from the next record.
* A single field from the next record, or FALSE if there is no next record.
*/
public function fetchField($index = 0);
......@@ -2017,7 +2107,7 @@ public function fetchField($index = 0);
* helper method, so one is added.
*
* @return
* An associative array.
* An associative array, or FALSE if there is no next row.
*/
public function fetchAssoc();
......@@ -2045,7 +2135,7 @@ public function fetchAssoc();
* The index of the column number to fetch.
*
* @return
* An indexed array.
* An indexed array, or an empty array if there is no result set.
*/
public function fetchCol($index = 0);
......@@ -2065,7 +2155,7 @@ public function fetchCol($index = 0);
* The numeric index of the field to use as the array value.
*
* @return
* An associative array.
* An associative array, or an empty array if there is no result set.
*/
public function fetchAllKeyed($key_index = 0, $value_index = 1);
......@@ -2084,7 +2174,7 @@ public function fetchAllKeyed($key_index = 0, $value_index = 1);
* set for the query will be used.
*
* @return
* An associative array.
* An associative array, or an empty array if there is no result set.
*/
public function fetchAllAssoc($key, $fetch = NULL);
}
......@@ -2349,14 +2439,14 @@ function db_query_range($query, $from, $count, array $args = array(), array $opt
}
/**
* Executes a query string and saves the result set to a temporary table.
* Executes a SELECT query string and saves the result set to a temporary table.
*
* The execution of the query string happens against the active database.
*
* @param $query
* The prepared statement query to run. Although it will accept both named and
* unnamed placeholders, named placeholders are strongly preferred as they are
* more self-documenting.
* The prepared SELECT statement query to run. Although it will accept both
* named and unnamed placeholders, named placeholders are strongly preferred
* as they are more self-documenting.
* @param $args
* An array of values to substitute into the query. If the query uses named
* placeholders, this is an associative array in any order. If the query uses
......@@ -2670,7 +2760,7 @@ function db_condition($conjunction) {
/**
* @ingroup schemaapi
* @addtogroup schemaapi
* @{
*/
......@@ -2767,7 +2857,7 @@ function _db_create_keys_sql($spec) {
* Renames a table.
*
* @param $table
* The table to be renamed.
* The current name of the table to be renamed.
* @param $new_name
* The new name for the table.
*/
......@@ -2798,7 +2888,7 @@ function db_drop_table($table) {
* will be set to the value of the key in all rows. This is most useful for
* creating NOT NULL columns with no default value in existing tables.
* @param $keys_new
* Optional keys and indexes specification to be created on the table along
* (optional) Keys and indexes specification to be created on the table along
* with adding the field. The format is the same as a table specification, but
* without the 'fields' element. If you are adding a type 'serial' field, you
* MUST specify at least one key or index including it in this array. See
......@@ -2978,7 +3068,7 @@ function db_drop_index($table, $name) {
* @param $spec
* The field specification for the new field.
* @param $keys_new
* Optional keys and indexes specification to be created on the table along
* (optional) Keys and indexes specification to be created on the table along
* with changing the field. The format is the same as a table specification
* but without the 'fields' element.
*/
......@@ -2987,7 +3077,7 @@ function db_change_field($table, $field, $field_new, $spec, $keys_new = array())
}
/**
* @} End of "ingroup schemaapi".
* @} End of "addtogroup schemaapi".
*/
/**
......
......@@ -128,9 +128,10 @@ public function log(DatabaseStatementInterface $statement, $args, $time) {
* Determine the routine that called this query.
*
* We define "the routine that called this query" as the first entry in
* the call stack that is not inside includes/database. That makes the
* climbing logic very simple, and handles the variable stack depth caused
* by the query builders.
* the call stack that is not inside includes/database and does have a file
* (which excludes call_user_func_array(), anonymous functions and similar).
* That makes the climbing logic very simple, and handles the variable stack
* depth caused by the query builders.
*
* @link http://www.php.net/debug_backtrace
* @return
......@@ -144,7 +145,8 @@ public function findCaller() {
$stack = debug_backtrace();
$stack_count = count($stack);
for ($i = 0; $i < $stack_count; ++$i) {
if (strpos($stack[$i]['file'], 'includes' . DIRECTORY_SEPARATOR . 'database') === FALSE) {
if (!empty($stack[$i]['file']) && strpos($stack[$i]['file'], 'includes' . DIRECTORY_SEPARATOR . 'database') === FALSE) {
$stack[$i] += array('args' => array());
return array(
'file' => $stack[$i]['file'],
'line' => $stack[$i]['line'],
......
......@@ -6,18 +6,18 @@
*/
/**
* @ingroup database
* @addtogroup database
* @{
*/
class DatabaseConnection_mysql extends DatabaseConnection {
/**
* Flag to indicate if we have registered the nextID cleanup function.
* Flag to indicate if the cleanup function in __destruct() should run.
*
* @var boolean
*/
protected $shutdownRegistered = FALSE;
protected $needsCleanup = FALSE;
public function __construct(array $connection_options = array()) {
// This driver defaults to transaction support, except if explicitly passed FALSE.
......@@ -28,6 +28,12 @@ public function __construct(array $connection_options = array()) {
$this->connectionOptions = $connection_options;
$charset = 'utf8';
// Check if the charset is overridden to utf8mb4 in settings.php.
if ($this->utf8mb4IsActive()) {
$charset = 'utf8mb4';
}
// The DSN should use either a socket or a host/port.
if (isset($connection_options['unix_socket'])) {
$dsn = 'mysql:unix_socket=' . $connection_options['unix_socket'];
......@@ -36,34 +42,63 @@ public function __construct(array $connection_options = array()) {
// Default to TCP connection on port 3306.
$dsn = 'mysql:host=' . $connection_options['host'] . ';port=' . (empty($connection_options['port']) ? 3306 : $connection_options['port']);
}
// Character set is added to dsn to ensure PDO uses the proper character
// set when escaping. This has security implications. See
// https://www.drupal.org/node/1201452 for further discussion.
$dsn .= ';charset=' . $charset;
$dsn .= ';dbname=' . $connection_options['database'];
parent::__construct($dsn, $connection_options['username'], $connection_options['password'], array(
// Allow PDO options to be overridden.
$connection_options += array(
'pdo' => array(),
);
$connection_options['pdo'] += array(
// So we don't have to mess around with cursors and unbuffered queries by default.
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => TRUE,
// Because MySQL's prepared statements skip the query cache, because it's dumb.
PDO::ATTR_EMULATE_PREPARES => TRUE,
// Force column names to lower case.
PDO::ATTR_CASE => PDO::CASE_LOWER,
));
);
if (defined('PDO::MYSQL_ATTR_MULTI_STATEMENTS')) {
// An added connection option in PHP 5.5.21+ to optionally limit SQL to a
// single statement like mysqli.
$connection_options['pdo'] += array(PDO::MYSQL_ATTR_MULTI_STATEMENTS => FALSE);
}
parent::__construct($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
// Force MySQL to use the UTF-8 character set. Also set the collation, if a
// certain one has been set; otherwise, MySQL defaults to 'utf8_general_ci'
// for UTF-8.
if (!empty($connection_options['collation'])) {
$this->exec('SET NAMES utf8 COLLATE ' . $connection_options['collation']);
$this->exec('SET NAMES ' . $charset . ' COLLATE ' . $connection_options['collation']);
}
else {
$this->exec('SET NAMES utf8');
$this->exec('SET NAMES ' . $charset);
}
// Set MySQL init_commands if not already defined. Default Drupal's MySQL
// behavior to conform more closely to SQL standards. This allows Drupal
// to run almost seamlessly on many different kinds of database systems.
// These settings force MySQL to behave the same as postgresql, or sqlite
// in regards to syntax interpretation and invalid data handling. See
// http://drupal.org/node/344575 for further discussion. Also, as MySQL 5.5
// changed the meaning of TRADITIONAL we need to spell out the modes one by
// one.
$connection_options += array(
'init_commands' => array(),
);
$connection_options['init_commands'] += array(
'sql_mode' => "SET sql_mode = 'REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER'",
);
// Execute initial commands.
foreach ($connection_options['init_commands'] as $sql) {
$this->exec($sql);
}
}
// Force MySQL's behavior to conform more closely to SQL standards.
// This allows Drupal to run almost seamlessly on many different
// kinds of database systems. These settings force MySQL to behave
// the same as postgresql, or sqlite in regards to syntax interpretation
// and invalid data handling. See http://drupal.org/node/344575 for
// further discussion. Also, as MySQL 5.5 changed the meaning of
// TRADITIONAL we need to spell out the modes one by one.
$this->exec("SET sql_mode='ANSI,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER'");
public function __destruct() {
if ($this->needsCleanup) {
$this->nextIdDelete();
}
}
public function queryRange($query, $from, $count, array $args = array(), array $options = array()) {
......@@ -72,7 +107,7 @@ public function queryRange($query, $from, $count, array $args = array(), array $
public function queryTemporary($query, array $args = array(), array $options = array()) {
$tablename = $this->generateTemporaryTableName();
$this->query(preg_replace('/^SELECT/i', 'CREATE TEMPORARY TABLE {' . $tablename . '} Engine=MEMORY SELECT', $query), $args, $options);
$this->query('CREATE TEMPORARY TABLE {' . $tablename . '} Engine=MEMORY ' . $query, $args, $options);
return $tablename;
}
......@@ -103,12 +138,7 @@ public function nextId($existing_id = 0) {
$this->query('INSERT INTO {sequences} (value) VALUES (:value) ON DUPLICATE KEY UPDATE value = value', array(':value' => $existing_id));
$new_id = $this->query('INSERT INTO {sequences} () VALUES ()', array(), array('return' => Database::RETURN_INSERT_ID));
}
if (!$this->shutdownRegistered) {
// Use register_shutdown_function() here to keep the database system
// independent of Drupal.
register_shutdown_function(array($this, 'nextIdDelete'));
$shutdownRegistered = TRUE;
}
$this->needsCleanup = TRUE;
return $new_id;
}
......@@ -169,8 +199,11 @@ protected function popCommittableTransactions() {
// succeed for MySQL error code 1305 ("SAVEPOINT does not exist").
if ($e->errorInfo[1] == '1305') {
// If one SAVEPOINT was released automatically, then all were.
// Therefore, we keep just the topmost transaction.
$this->transactionLayers = array('drupal_transaction' => 'drupal_transaction');
// Therefore, clean the transaction stack.
$this->transactionLayers = array();
// We also have to explain to PDO that the transaction stack has
// been cleaned-up.
PDO::commit();
}
else {
throw $e;
......@@ -179,9 +212,45 @@ protected function popCommittableTransactions() {
}
}
}
public function utf8mb4IsConfigurable() {
return TRUE;
}
public function utf8mb4IsActive() {
return isset($this->connectionOptions['charset']) && $this->connectionOptions['charset'] === 'utf8mb4';
}
public function utf8mb4IsSupported() {
// Ensure that the MySQL driver supports utf8mb4 encoding.
$version = $this->getAttribute(PDO::ATTR_CLIENT_VERSION);
if (strpos($version, 'mysqlnd') !== FALSE) {
// The mysqlnd driver supports utf8mb4 starting at version 5.0.9.
$version = preg_replace('/^\D+([\d.]+).*/', '$1', $version);
if (version_compare($version, '5.0.9', '<')) {
return FALSE;
}
}
else {
// The libmysqlclient driver supports utf8mb4 starting at version 5.5.3.
if (version_compare($version, '5.5.3', '<')) {
return FALSE;
}
}
// Ensure that the MySQL server supports large prefixes and utf8mb4.
try {
$this->query("CREATE TABLE {drupal_utf8mb4_test} (id VARCHAR(255), PRIMARY KEY(id(255))) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci ROW_FORMAT=DYNAMIC ENGINE=INNODB");
}
catch (Exception $e) {
return FALSE;
}
$this->query("DROP TABLE {drupal_utf8mb4_test}");
return TRUE;
}
}
/**
* @} End of "ingroup database".
* @} End of "addtogroup database".
*/
<?php
/**
* @ingroup database
* @addtogroup database
* @{
*/
......@@ -51,7 +51,8 @@ public function __toString() {
// 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;
$insert_fields_string = $insert_fields ? ' (' . implode(', ', $insert_fields) . ') ' : ' ';
return $comments . 'INSERT INTO {' . $this->table . '}' . $insert_fields_string . $this->fromQuery;
}
$query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
......@@ -86,22 +87,8 @@ public function __toString() {
}
}
class TruncateQuery_mysql extends TruncateQuery {
public function __toString() {
// TRUNCATE is actually a DDL statement on MySQL, and DDL statements are
// not transactional, and result in an implicit COMMIT. When we are in a
// transaction, fallback to the slower, but transactional, DELETE.
if ($this->connection->inTransaction()) {
// Create a comment string to prepend to the query.
$comments = $this->connection->makeComment($this->comments);
return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '}';
}
else {
return parent::__toString();
}
}
}
class TruncateQuery_mysql extends TruncateQuery { }
/**
* @} End of "ingroup database".
* @} End of "addtogroup database".
*/
......@@ -7,7 +7,7 @@
/**
* @ingroup schemaapi
* @addtogroup schemaapi
* @{
*/
......@@ -39,8 +39,8 @@ protected function getPrefixInfo($table = 'default', $add_prefix = TRUE) {
$info['table'] = substr($table, ++$pos);
}
else {
$db_info = Database::getConnectionInfo();
$info['database'] = $db_info['default']['database'];
$db_info = $this->connection->getConnectionOptions();
$info['database'] = $db_info['database'];
$info['table'] = $table;
}
return $info;
......@@ -81,7 +81,8 @@ protected function createTableSql($name, $table) {
// Provide defaults if needed.
$table += array(
'mysql_engine' => 'InnoDB',
'mysql_character_set' => 'utf8',
// Allow the default charset to be overridden in settings.php.
'mysql_character_set' => $this->connection->utf8mb4IsActive() ? 'utf8mb4' : 'utf8',
);
$sql = "CREATE TABLE {" . $name . "} (\n";
......@@ -109,6 +110,13 @@ protected function createTableSql($name, $table) {
$sql .= ' COLLATE ' . $info['collation'];
}
// The row format needs to be either DYNAMIC or COMPRESSED in order to allow
// for the innodb_large_prefix setting to take effect, see
// https://dev.mysql.com/doc/refman/5.6/en/create-table.html
if ($this->connection->utf8mb4IsActive()) {
$sql .= ' ROW_FORMAT=DYNAMIC';
}
// Add table comment.
if (!empty($table['description'])) {
$sql .= ' COMMENT ' . $this->prepareComment($table['description'], self::COMMENT_MAX_TABLE);
......@@ -131,8 +139,13 @@ protected function createTableSql($name, $table) {
protected function createFieldSql($name, $spec) {
$sql = "`" . $name . "` " . $spec['mysql_type'];
if (in_array($spec['mysql_type'], array('VARCHAR', 'CHAR', 'TINYTEXT', 'MEDIUMTEXT', 'LONGTEXT', 'TEXT')) && isset($spec['length'])) {
$sql .= '(' . $spec['length'] . ')';
if (in_array($spec['mysql_type'], array('VARCHAR', 'CHAR', 'TINYTEXT', 'MEDIUMTEXT', 'LONGTEXT', 'TEXT'))) {
if (isset($spec['length'])) {
$sql .= '(' . $spec['length'] . ')';
}
if (!empty($spec['binary'])) {
$sql .= ' BINARY';
}
}
elseif (isset($spec['precision']) && isset($spec['scale'])) {
$sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')';
......@@ -296,10 +309,10 @@ protected function createKeysSqlHelper($fields) {
public function renameTable($table, $new_name) {
if (!$this->tableExists($table)) {
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot rename %table to %table_new: table %table doesn't exist.", array('%table' => $table, '%table_new' => $new_name)));
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot rename @table to @table_new: table @table doesn't exist.", array('@table' => $table, '@table_new' => $new_name)));
}
if ($this->tableExists($new_name)) {
throw new DatabaseSchemaObjectExistsException(t("Cannot rename %table to %table_new: table %table_new already exists.", array('%table' => $table, '%table_new' => $new_name)));
throw new DatabaseSchemaObjectExistsException(t("Cannot rename @table to @table_new: table @table_new already exists.", array('@table' => $table, '@table_new' => $new_name)));
}
$info = $this->getPrefixInfo($new_name);
......@@ -317,10 +330,10 @@ public function dropTable($table) {
public function addField($table, $field, $spec, $keys_new = array()) {
if (!$this->tableExists($table)) {
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field %table.%field: table doesn't exist.", array('%field' => $field, '%table' => $table)));
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field @table.@field: table doesn't exist.", array('@field' => $field, '@table' => $table)));
}
if ($this->fieldExists($table, $field)) {
throw new DatabaseSchemaObjectExistsException(t("Cannot add field %table.%field: field already exists.", array('%field' => $field, '%table' => $table)));
throw new DatabaseSchemaObjectExistsException(t("Cannot add field @table.@field: field already exists.", array('@field' => $field, '@table' => $table)));
}
$fixnull = FALSE;
......@@ -356,7 +369,7 @@ public function dropField($table, $field) {
public function fieldSetDefault($table, $field, $default) {
if (!$this->fieldExists($table, $field)) {
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot set default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field)));
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot set default value of field @table.@field: field doesn't exist.", array('@table' => $table, '@field' => $field)));
}
if (!isset($default)) {
......@@ -371,7 +384,7 @@ public function fieldSetDefault($table, $field, $default) {
public function fieldSetNoDefault($table, $field) {
if (!$this->fieldExists($table, $field)) {
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot remove default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field)));
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot remove default value of field @table.@field: field doesn't exist.", array('@table' => $table, '@field' => $field)));
}
$this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN `' . $field . '` DROP DEFAULT');
......@@ -381,15 +394,15 @@ public function indexExists($table, $name) {
// Returns one row for each column in the index. Result is string or FALSE.
// Details at http://dev.mysql.com/doc/refman/5.0/en/show-index.html
$row = $this->connection->query('SHOW INDEX FROM {' . $table . "} WHERE key_name = '$name'")->fetchAssoc();
return isset($row['key_name']);
return isset($row['Key_name']);
}
public function addPrimaryKey($table, $fields) {
if (!$this->tableExists($table)) {
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add primary key to table %table: table doesn't exist.", array('%table' => $table)));
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add primary key to table @table: table doesn't exist.", array('@table' => $table)));
}
if ($this->indexExists($table, 'PRIMARY')) {
throw new DatabaseSchemaObjectExistsException(t("Cannot add primary key to table %table: primary key already exists.", array('%table' => $table)));
throw new DatabaseSchemaObjectExistsException(t("Cannot add primary key to table @table: primary key already exists.", array('@table' => $table)));
}
$this->connection->query('ALTER TABLE {' . $table . '} ADD PRIMARY KEY (' . $this->createKeySql($fields) . ')');
......@@ -406,10 +419,10 @@ public function dropPrimaryKey($table) {
public function addUniqueKey($table, $name, $fields) {
if (!$this->tableExists($table)) {
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add unique key %name to table %table: table doesn't exist.", array('%table' => $table, '%name' => $name)));
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add unique key @name to table @table: table doesn't exist.", array('@table' => $table, '@name' => $name)));
}
if ($this->indexExists($table, $name)) {
throw new DatabaseSchemaObjectExistsException(t("Cannot add unique key %name to table %table: unique key already exists.", array('%table' => $table, '%name' => $name)));
throw new DatabaseSchemaObjectExistsException(t("Cannot add unique key @name to table @table: unique key already exists.", array('@table' => $table, '@name' => $name)));
}
$this->connection->query('ALTER TABLE {' . $table . '} ADD UNIQUE KEY `' . $name . '` (' . $this->createKeySql($fields) . ')');
......@@ -426,10 +439,10 @@ public function dropUniqueKey($table, $name) {
public function addIndex($table, $name, $fields) {
if (!$this->tableExists($table)) {
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add index %name to table %table: table doesn't exist.", array('%table' => $table, '%name' => $name)));
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add index @name to table @table: table doesn't exist.", array('@table' => $table, '@name' => $name)));
}
if ($this->indexExists($table, $name)) {
throw new DatabaseSchemaObjectExistsException(t("Cannot add index %name to table %table: index already exists.", array('%table' => $table, '%name' => $name)));
throw new DatabaseSchemaObjectExistsException(t("Cannot add index @name to table @table: index already exists.", array('@table' => $table, '@name' => $name)));
}
$this->connection->query('ALTER TABLE {' . $table . '} ADD INDEX `' . $name . '` (' . $this->createKeySql($fields) . ')');
......@@ -446,10 +459,10 @@ public function dropIndex($table, $name) {
public function changeField($table, $field, $field_new, $spec, $keys_new = array()) {
if (!$this->fieldExists($table, $field)) {
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot change the definition of field %table.%name: field doesn't exist.", array('%table' => $table, '%name' => $field)));
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot change the definition of field @table.@name: field doesn't exist.", array('@table' => $table, '@name' => $field)));
}
if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
throw new DatabaseSchemaObjectExistsException(t("Cannot rename field %table.%name to %name_new: target field already exists.", array('%table' => $table, '%name' => $field, '%name_new' => $field_new)));
throw new DatabaseSchemaObjectExistsException(t("Cannot rename field @table.@name to @name_new: target field already exists.", array('@table' => $table, '@name' => $field, '@name_new' => $field_new)));
}
$sql = 'ALTER TABLE {' . $table . '} CHANGE `' . $field . '` ' . $this->createFieldSql($field_new, $this->processField($spec));
......@@ -527,5 +540,5 @@ public function fieldExists($table, $column) {
}
/**
* @} End of "ingroup schemaapi".
* @} End of "addtogroup schemaapi".
*/
......@@ -6,7 +6,7 @@
*/
/**
* @ingroup database
* @addtogroup database
* @{
*/
......@@ -47,7 +47,12 @@ public function __construct(array $connection_options = array()) {
$this->connectionOptions = $connection_options;
$dsn = 'pgsql:host=' . $connection_options['host'] . ' dbname=' . $connection_options['database'] . ' port=' . $connection_options['port'];
parent::__construct($dsn, $connection_options['username'], $connection_options['password'], array(
// Allow PDO options to be overridden.
$connection_options += array(
'pdo' => array(),
);
$connection_options['pdo'] += array(
// Prepared statements are most effective for performance when queries
// are recycled (used several times). However, if they are not re-used,
// prepared statements become ineffecient. Since most of Drupal's
......@@ -57,12 +62,27 @@ public function __construct(array $connection_options = array()) {
PDO::ATTR_EMULATE_PREPARES => TRUE,
// Convert numeric values to strings when fetching.
PDO::ATTR_STRINGIFY_FETCHES => TRUE,
// Force column names to lower case.
PDO::ATTR_CASE => PDO::CASE_LOWER,
));
);
parent::__construct($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
// Force PostgreSQL to use the UTF-8 character set by default.
$this->exec("SET NAMES 'UTF8'");
// Execute PostgreSQL init_commands.
if (isset($connection_options['init_commands'])) {
$this->exec(implode('; ', $connection_options['init_commands']));
}
}
public function prepareQuery($query) {
// mapConditionOperator converts LIKE operations to ILIKE for consistency
// with MySQL. However, Postgres does not support ILIKE on bytea (blobs)
// fields.
// To make the ILIKE operator work, we type-cast bytea fields into text.
// @todo This workaround only affects bytea fields, but the involved field
// types involved in the query are unknown, so there is no way to
// conditionally execute this for affected queries only.
return parent::prepareQuery(preg_replace('/ ([^ ]+) +(I*LIKE|NOT +I*LIKE) /i', ' ${1}::text ${2} ', $query));
}
public function query($query, array $args = array(), $options = array()) {
......@@ -126,7 +146,7 @@ public function queryRange($query, $from, $count, array $args = array(), array $
public function queryTemporary($query, array $args = array(), array $options = array()) {
$tablename = $this->generateTemporaryTableName();
$this->query(preg_replace('/^SELECT/i', 'CREATE TEMPORARY TABLE {' . $tablename . '} AS SELECT', $query), $args, $options);
$this->query('CREATE TEMPORARY TABLE {' . $tablename . '} AS ' . $query, $args, $options);
return $tablename;
}
......@@ -196,8 +216,16 @@ public function nextId($existing = 0) {
return $id;
}
public function utf8mb4IsActive() {
return TRUE;
}
public function utf8mb4IsSupported() {
return TRUE;
}
}
/**
* @} End of "ingroup database".
* @} End of "addtogroup database".
*/
......@@ -112,7 +112,8 @@ public function __toString() {
// 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;
$insert_fields_string = $insert_fields ? ' (' . implode(', ', $insert_fields) . ') ' : ' ';
return $comments . 'INSERT INTO {' . $this->table . '}' . $insert_fields_string . $this->fromQuery;
}
$query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
......
......@@ -314,10 +314,10 @@ protected function _createKeySql($fields) {
function renameTable($table, $new_name) {
if (!$this->tableExists($table)) {
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot rename %table to %table_new: table %table doesn't exist.", array('%table' => $table, '%table_new' => $new_name)));
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot rename @table to @table_new: table @table doesn't exist.", array('@table' => $table, '@table_new' => $new_name)));
}
if ($this->tableExists($new_name)) {
throw new DatabaseSchemaObjectExistsException(t("Cannot rename %table to %table_new: table %table_new already exists.", array('%table' => $table, '%table_new' => $new_name)));
throw new DatabaseSchemaObjectExistsException(t("Cannot rename @table to @table_new: table @table_new already exists.", array('@table' => $table, '@table_new' => $new_name)));
}
// Get the schema and tablename for the old table.
......@@ -328,9 +328,9 @@ function renameTable($table, $new_name) {
// rename them when renaming the table.
$indexes = $this->connection->query('SELECT indexname FROM pg_indexes WHERE schemaname = :schema AND tablename = :table', array(':schema' => $old_schema, ':table' => $old_table_name));
foreach ($indexes as $index) {
if (preg_match('/^' . preg_quote($old_full_name) . '_(.*)_idx$/', $index->indexname, $matches)) {
if (preg_match('/^' . preg_quote($old_full_name) . '_(.*)$/', $index->indexname, $matches)) {
$index_name = $matches[1];
$this->connection->query('ALTER INDEX ' . $index->indexname . ' RENAME TO {' . $new_name . '}_' . $index_name . '_idx');
$this->connection->query('ALTER INDEX ' . $index->indexname . ' RENAME TO {' . $new_name . '}_' . $index_name);
}
}
......@@ -351,10 +351,10 @@ public function dropTable($table) {
public function addField($table, $field, $spec, $new_keys = array()) {
if (!$this->tableExists($table)) {
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field %table.%field: table doesn't exist.", array('%field' => $field, '%table' => $table)));
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field @table.@field: table doesn't exist.", array('@field' => $field, '@table' => $table)));
}
if ($this->fieldExists($table, $field)) {
throw new DatabaseSchemaObjectExistsException(t("Cannot add field %table.%field: field already exists.", array('%field' => $field, '%table' => $table)));
throw new DatabaseSchemaObjectExistsException(t("Cannot add field @table.@field: field already exists.", array('@field' => $field, '@table' => $table)));
}
$fixnull = FALSE;
......@@ -393,7 +393,7 @@ public function dropField($table, $field) {
public function fieldSetDefault($table, $field, $default) {
if (!$this->fieldExists($table, $field)) {
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot set default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field)));
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot set default value of field @table.@field: field doesn't exist.", array('@table' => $table, '@field' => $field)));
}
if (!isset($default)) {
......@@ -408,7 +408,7 @@ public function fieldSetDefault($table, $field, $default) {
public function fieldSetNoDefault($table, $field) {
if (!$this->fieldExists($table, $field)) {
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot remove default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field)));
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot remove default value of field @table.@field: field doesn't exist.", array('@table' => $table, '@field' => $field)));
}
$this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN "' . $field . '" DROP DEFAULT');
......@@ -435,10 +435,10 @@ protected function constraintExists($table, $name) {
public function addPrimaryKey($table, $fields) {
if (!$this->tableExists($table)) {
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add primary key to table %table: table doesn't exist.", array('%table' => $table)));
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add primary key to table @table: table doesn't exist.", array('@table' => $table)));
}
if ($this->constraintExists($table, 'pkey')) {
throw new DatabaseSchemaObjectExistsException(t("Cannot add primary key to table %table: primary key already exists.", array('%table' => $table)));
throw new DatabaseSchemaObjectExistsException(t("Cannot add primary key to table @table: primary key already exists.", array('@table' => $table)));
}
$this->connection->query('ALTER TABLE {' . $table . '} ADD PRIMARY KEY (' . implode(',', $fields) . ')');
......@@ -455,10 +455,10 @@ public function dropPrimaryKey($table) {
function addUniqueKey($table, $name, $fields) {
if (!$this->tableExists($table)) {
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add unique key %name to table %table: table doesn't exist.", array('%table' => $table, '%name' => $name)));
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add unique key @name to table @table: table doesn't exist.", array('@table' => $table, '@name' => $name)));
}
if ($this->constraintExists($table, $name . '_key')) {
throw new DatabaseSchemaObjectExistsException(t("Cannot add unique key %name to table %table: unique key already exists.", array('%table' => $table, '%name' => $name)));
throw new DatabaseSchemaObjectExistsException(t("Cannot add unique key @name to table @table: unique key already exists.", array('@table' => $table, '@name' => $name)));
}
$this->connection->query('ALTER TABLE {' . $table . '} ADD CONSTRAINT "' . $this->prefixNonTable($table, $name, 'key') . '" UNIQUE (' . implode(',', $fields) . ')');
......@@ -475,10 +475,10 @@ public function dropUniqueKey($table, $name) {
public function addIndex($table, $name, $fields) {
if (!$this->tableExists($table)) {
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add index %name to table %table: table doesn't exist.", array('%table' => $table, '%name' => $name)));
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add index @name to table @table: table doesn't exist.", array('@table' => $table, '@name' => $name)));
}
if ($this->indexExists($table, $name)) {
throw new DatabaseSchemaObjectExistsException(t("Cannot add index %name to table %table: index already exists.", array('%table' => $table, '%name' => $name)));
throw new DatabaseSchemaObjectExistsException(t("Cannot add index @name to table @table: index already exists.", array('@table' => $table, '@name' => $name)));
}
$this->connection->query($this->_createIndexSql($table, $name, $fields));
......@@ -495,10 +495,10 @@ public function dropIndex($table, $name) {
public function changeField($table, $field, $field_new, $spec, $new_keys = array()) {
if (!$this->fieldExists($table, $field)) {
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot change the definition of field %table.%name: field doesn't exist.", array('%table' => $table, '%name' => $field)));
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot change the definition of field @table.@name: field doesn't exist.", array('@table' => $table, '@name' => $field)));
}
if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
throw new DatabaseSchemaObjectExistsException(t("Cannot rename field %table.%name to %name_new: target field already exists.", array('%table' => $table, '%name' => $field, '%name_new' => $field_new)));
throw new DatabaseSchemaObjectExistsException(t("Cannot rename field @table.@name to @name_new: target field already exists.", array('@table' => $table, '@name' => $field, '@name_new' => $field_new)));
}
$spec = $this->processField($spec);
......
......@@ -6,7 +6,7 @@
*/
/**
* @ingroup database
* @addtogroup database
* @{
*/
......@@ -103,6 +103,6 @@ public function orderBy($field, $direction = 'ASC') {
}
/**
* @} End of "ingroup database".
* @} End of "addtogroup database".
*/
......@@ -9,7 +9,7 @@
*/
/**
* @ingroup database
* @addtogroup database
* @{
*/
......@@ -502,6 +502,6 @@ public function fetchAllAssoc($key, $fetch_style = NULL) {
}
/**
* @} End of "ingroup database".
* @} End of "addtogroup database".
*/
<?php
/**
* @ingroup database
* @addtogroup database
* @{
*/
......@@ -22,6 +22,9 @@ interface QueryConditionInterface {
* parameters, they are taken as $field and $value with $operator having a
* value of IN if $value is an array and = otherwise.
*
* Do not use this method to test for NULL values. Instead, use
* QueryConditionInterface::isNull() or QueryConditionInterface::isNotNull().
*
* @param $field
* The name of the field to check. If you would like to add a more complex
* condition involving operators or functions, use where().
......@@ -36,6 +39,9 @@ interface QueryConditionInterface {
*
* @return QueryConditionInterface
* The called object.
*
* @see QueryConditionInterface::isNull()
* @see QueryConditionInterface::isNotNull()
*/
public function condition($field, $value = NULL, $operator = NULL);
......@@ -77,7 +83,7 @@ public function isNotNull($field);
/**
* Sets a condition that the specified subquery returns values.
*
*
* @param SelectQueryInterface $select
* The subquery that must contain results.
*
......@@ -85,10 +91,10 @@ public function isNotNull($field);
* The called object.
*/
public function exists(SelectQueryInterface $select);
/**
* Sets a condition that the specified subquery returns no values.
*
*
* @param SelectQueryInterface $select
* The subquery that must not contain results.
*
......@@ -96,7 +102,7 @@ public function exists(SelectQueryInterface $select);
* The called object.
*/
public function notExists(SelectQueryInterface $select);
/**
* Gets a complete list of all conditions in this conditional clause.
*
......@@ -277,14 +283,14 @@ abstract class Query implements QueryPlaceholderInterface {
/**
* The target of the connection object.
*
*
* @var string
*/
protected $connectionTarget;
/**
* The key of the connection object.
*
*
* @var string
*/
protected $connectionKey;
......@@ -704,10 +710,11 @@ public function preExecute() {
// first call to fields() does have an effect.
$this->fields(array_merge(array_keys($this->fromQuery->getFields()), array_keys($this->fromQuery->getExpressions())));
}
// Don't execute query without fields.
if (count($this->insertFields) + count($this->defaultFields) == 0) {
throw new NoFieldsException('There are no fields available to insert with.');
else {
// Don't execute query without fields.
if (count($this->insertFields) + count($this->defaultFields) == 0) {
throw new NoFieldsException('There are no fields available to insert with.');
}
}
// If no values have been added, silently ignore this query. This can happen
......@@ -798,7 +805,7 @@ public function notExists(SelectQueryInterface $select) {
$this->condition->notExists($select);
return $this;
}
/**
* Implements QueryConditionInterface::conditions().
*/
......@@ -839,7 +846,7 @@ public function compiled() {
* Executes the DELETE query.
*
* @return
* The return value is dependant on the database connection.
* The return value is dependent on the database connection.
*/
public function execute() {
$values = array();
......@@ -936,7 +943,17 @@ public function __toString() {
// Create a sanitized comment string to prepend to the query.
$comments = $this->connection->makeComment($this->comments);
return $comments . 'TRUNCATE {' . $this->connection->escapeTable($this->table) . '} ';
// In most cases, TRUNCATE is not a transaction safe statement as it is a
// DDL statement which results in an implicit COMMIT. When we are in a
// transaction, fallback to the slower, but transactional, DELETE.
// PostgreSQL also locks the entire table for a TRUNCATE strongly reducing
// the concurrency with other transactions.
if ($this->connection->inTransaction()) {
return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '}';
}
else {
return $comments . 'TRUNCATE {' . $this->connection->escapeTable($this->table) . '} ';
}
}
}
......@@ -1047,7 +1064,7 @@ public function notExists(SelectQueryInterface $select) {
$this->condition->notExists($select);
return $this;
}
/**
* Implements QueryConditionInterface::conditions().
*/
......@@ -1539,7 +1556,7 @@ public function notExists(SelectQueryInterface $select) {
$this->condition->notExists($select);
return $this;
}
/**
* Implements QueryConditionInterface::conditions().
*/
......@@ -1589,55 +1606,43 @@ public function __toString() {
}
public function execute() {
// Wrap multiple queries in a transaction, if the database supports it.
$transaction = $this->connection->startTransaction();
try {
if (!count($this->condition)) {
throw new InvalidMergeQueryException(t('Invalid merge query: no conditions'));
}
$select = $this->connection->select($this->conditionTable)
->condition($this->condition)
->forUpdate();
$select->addExpression('1');
if (!$select->execute()->fetchField()) {
try {
$insert = $this->connection->insert($this->table)->fields($this->insertFields);
if ($this->defaultFields) {
$insert->useDefaults($this->defaultFields);
}
$insert->execute();
return MergeQuery::STATUS_INSERT;
}
catch (Exception $e) {
// The insert query failed, maybe it's because a racing insert query
// beat us in inserting the same row. Retry the select query, if it
// returns a row, ignore the error and continue with the update
// query below.
if (!$select->execute()->fetchField()) {
throw $e;
}
if (!count($this->condition)) {
throw new InvalidMergeQueryException(t('Invalid merge query: no conditions'));
}
$select = $this->connection->select($this->conditionTable)
->condition($this->condition);
$select->addExpression('1');
if (!$select->execute()->fetchField()) {
try {
$insert = $this->connection->insert($this->table)->fields($this->insertFields);
if ($this->defaultFields) {
$insert->useDefaults($this->defaultFields);
}
$insert->execute();
return self::STATUS_INSERT;
}
if ($this->needsUpdate) {
$update = $this->connection->update($this->table)
->fields($this->updateFields)
->condition($this->condition);
if ($this->expressionFields) {
foreach ($this->expressionFields as $field => $data) {
$update->expression($field, $data['expression'], $data['arguments']);
}
catch (Exception $e) {
// The insert query failed, maybe it's because a racing insert query
// beat us in inserting the same row. Retry the select query, if it
// returns a row, ignore the error and continue with the update
// query below.
if (!$select->execute()->fetchField()) {
throw $e;
}
$update->execute();
return MergeQuery::STATUS_UPDATE;
}
}
catch (Exception $e) {
// Something really wrong happened here, bubble up the exception to the
// caller.
$transaction->rollback();
throw $e;
}
// Transaction commits here where $transaction looses scope.
if ($this->needsUpdate) {
$update = $this->connection->update($this->table)
->fields($this->updateFields)
->condition($this->condition);
if ($this->expressionFields) {
foreach ($this->expressionFields as $field => $data) {
$update->expression($field, $data['expression'], $data['arguments']);
}
}
$update->execute();
return self::STATUS_UPDATE;
}
}
}
......@@ -1689,7 +1694,7 @@ public function __construct($conjunction) {
* Implements Countable::count().
*
* Returns the size of this conditional. The size of the conditional is the
* size of its conditional array minus one, because one element is the the
* size of its conditional array minus one, because one element is the
* conjunction.
*/
public function count() {
......@@ -1756,14 +1761,14 @@ public function isNotNull($field) {
public function exists(SelectQueryInterface $select) {
return $this->condition('', $select, 'EXISTS');
}
/**
* Implements QueryConditionInterface::notExists().
*/
public function notExists(SelectQueryInterface $select) {
return $this->condition('', $select, 'NOT EXISTS');
}
/**
* Implements QueryConditionInterface::conditions().
*/
......@@ -1892,8 +1897,13 @@ public function __toString() {
function __clone() {
$this->changed = TRUE;
foreach ($this->conditions as $key => $condition) {
if ($condition['field'] instanceOf QueryConditionInterface) {
$this->conditions[$key]['field'] = clone($condition['field']);
if ($key !== '#conjunction') {
if ($condition['field'] instanceOf QueryConditionInterface) {
$this->conditions[$key]['field'] = clone($condition['field']);
}
if ($condition['value'] instanceOf SelectQueryInterface) {
$this->conditions[$key]['value'] = clone($condition['value']);
}
}
}
}
......@@ -1949,5 +1959,5 @@ protected function mapConditionOperator($operator) {
}
/**
* @} End of "ingroup database".
* @} End of "addtogroup database".
*/
......@@ -76,8 +76,13 @@
* the precision (total number of significant digits) and scale
* (decimal digits right of the decimal point). Both values are
* mandatory. Ignored for other field types.
* - 'binary': A boolean indicating that MySQL should force 'char',
* 'varchar' or 'text' fields to use case-sensitive binary collation.
* This has no effect on other database types for which case sensitivity
* is already the default behavior.
* All parameters apart from 'type' are optional except that type
* 'numeric' columns must specify 'precision' and 'scale'.
* 'numeric' columns must specify 'precision' and 'scale', and type
* 'varchar' must specify the 'length' parameter.
* - 'primary key': An array of one or more key column specifiers (see below)
* that form the primary key.
* - 'unique keys': An associative array of unique keys ('keyname' =>
......@@ -87,7 +92,8 @@
* specification). Each specification is an array containing the name of
* the referenced table ('table'), and an array of column mappings
* ('columns'). Column mappings are defined by key pairs ('source_column' =>
* 'referenced_column').
* 'referenced_column'). This key is for documentation purposes only; foreign
* keys are not created in the database, nor are they enforced by Drupal.
* - 'indexes': An associative array of indexes ('indexname' =>
* specification). Each specification is an array of one or more
* key column specifiers (see below) that form an index on the
......@@ -139,6 +145,8 @@
* 'unique keys' => array(
* 'vid' => array('vid'),
* ),
* // For documentation purposes only; foreign keys are not created in the
* // database.
* 'foreign keys' => array(
* 'node_revision' => array(
* 'table' => 'node_revision',
......@@ -411,7 +419,7 @@ abstract public function dropTable($table);
* This is most useful for creating NOT NULL columns with no default
* value in existing tables.
* @param $keys_new
* Optional keys and indexes specification to be created on the
* (optional) Keys and indexes specification to be created on the
* table along with adding the field. The format is the same as a
* table specification but without the 'fields' element. If you are
* adding a type 'serial' field, you MUST specify at least one key
......@@ -625,7 +633,7 @@ abstract public function dropIndex($table, $name);
* @param $spec
* The field specification for the new field.
* @param $keys_new
* Optional keys and indexes specification to be created on the
* (optional) Keys and indexes specification to be created on the
* table along with changing the field. The format is the same as a
* table specification but without the 'fields' element.
*
......@@ -649,7 +657,7 @@ abstract public function changeField($table, $field, $field_new, $spec, $keys_ne
*/
public function createTable($name, $table) {
if ($this->tableExists($name)) {
throw new DatabaseSchemaObjectExistsException(t('Table %name already exists.', array('%name' => $name)));
throw new DatabaseSchemaObjectExistsException(t('Table @name already exists.', array('@name' => $name)));
}
$statements = $this->createTableSql($name, $table);
foreach ($statements as $statement) {
......
<?php
/**
* @ingroup database
* @addtogroup database
* @{
*/
......@@ -377,7 +377,8 @@ public function addJoin($type, $table, $alias = NULL, $condition = NULL, $argume
* @param $field
* The field on which to order.
* @param $direction
* The direction to sort. Legal values are "ASC" and "DESC".
* The direction to sort. Legal values are "ASC" and "DESC". Any other value
* will be converted to "ASC".
* @return SelectQueryInterface
* The called object.
*/
......@@ -590,11 +591,13 @@ public function hasTag($tag) {
}
public function hasAllTags() {
return call_user_func_array(array($this->query, 'hasAllTags'), func_get_args());
$args = func_get_args();
return call_user_func_array(array($this->query, 'hasAllTags'), $args);
}
public function hasAnyTag() {
return call_user_func_array(array($this->query, 'hasAnyTags'), func_get_args());
$args = func_get_args();
return call_user_func_array(array($this->query, 'hasAnyTag'), $args);
}
public function addMetaData($key, $object) {
......@@ -637,16 +640,16 @@ public function compiled() {
/* Implementations of QueryConditionInterface for the HAVING clause. */
public function havingCondition($field, $value = NULL, $operator = '=') {
$this->query->condition($field, $value, $operator, $num_args);
$this->query->havingCondition($field, $value, $operator);
return $this;
}
public function &havingConditions() {
return $this->having->conditions();
return $this->query->havingConditions();
}
public function havingArguments() {
return $this->having->arguments();
return $this->query->havingArguments();
}
public function having($snippet, $args = array()) {
......@@ -790,31 +793,7 @@ public function forUpdate($set = TRUE) {
}
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();
// Also remove 'all_fields' statements, which are expanded into tablename.*
// when the query is executed.
$tables = &$count->getTables();
foreach ($tables as $alias => &$table) {
unset($table['all_fields']);
}
// 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;
return $this->query->countQuery();
}
function isNull($field) {
......@@ -836,7 +815,7 @@ public function notExists(SelectQueryInterface $select) {
$this->query->notExists($select);
return $this;
}
public function __toString() {
return (string) $this->query;
}
......@@ -1005,11 +984,13 @@ public function hasTag($tag) {
}
public function hasAllTags() {
return !(boolean)array_diff(func_get_args(), array_keys($this->alterTags));
$args = func_get_args();
return !(boolean)array_diff($args, array_keys($this->alterTags));
}
public function hasAnyTag() {
return (boolean)array_intersect(func_get_args(), array_keys($this->alterTags));
$args = func_get_args();
return (boolean)array_intersect($args, array_keys($this->alterTags));
}
public function addMetaData($key, $object) {
......@@ -1088,7 +1069,7 @@ public function notExists(SelectQueryInterface $select) {
$this->where->notExists($select);
return $this;
}
public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) {
$this->where->compile($connection, $queryPlaceholder);
$this->having->compile($connection, $queryPlaceholder);
......@@ -1172,17 +1153,17 @@ public function havingIsNotNull($field) {
$this->having->isNotNull($field);
return $this;
}
public function havingExists(SelectQueryInterface $select) {
$this->having->exists($select);
return $this;
}
public function havingNotExists(SelectQueryInterface $select) {
$this->having->notExists($select);
return $this;
}
public function forUpdate($set = TRUE) {
if (isset($set)) {
$this->forUpdate = $set;
......@@ -1250,6 +1231,21 @@ public function preExecute(SelectQueryInterface $query = NULL) {
// Modules may alter all queries or only those having a particular tag.
if (isset($this->alterTags)) {
// Many contrib modules assume that query tags used for access-checking
// purposes follow the pattern $entity_type . '_access'. But this is
// not the case for taxonomy terms, since core used to add term_access
// instead of taxonomy_term_access to its queries. Provide backwards
// compatibility by adding both tags here instead of attempting to fix
// all contrib modules in a coordinated effort.
// TODO:
// - Extract this mechanism into a hook as part of a public (non-security)
// issue.
// - Emit E_USER_DEPRECATED if term_access is used.
// https://www.drupal.org/node/2575081
$term_access_tags = array('term_access' => 1, 'taxonomy_term_access' => 1);
if (array_intersect_key($this->alterTags, $term_access_tags)) {
$this->alterTags += $term_access_tags;
}
$hooks = array('query');
foreach ($this->alterTags as $tag => $value) {
$hooks[] = 'query_' . $tag;
......@@ -1404,6 +1400,8 @@ public function addJoin($type, $table, $alias = NULL, $condition = NULL, $argume
}
public function orderBy($field, $direction = 'ASC') {
// Only allow ASC and DESC, default to ASC.
$direction = strtoupper($direction) == 'DESC' ? 'DESC' : 'ASC';
$this->order[$field] = $direction;
return $this;
}
......@@ -1451,17 +1449,20 @@ public function countQuery() {
$count = clone($this);
$group_by = $count->getGroupBy();
$having = $count->havingConditions();
if (!$count->distinct) {
if (!$count->distinct && !isset($having[0])) {
// When not executing a distinct query, we can zero-out existing fields
// and expressions that are not used by a GROUP BY. Fields listed in
// the GROUP BY clause need to be present in the query.
// and expressions that are not used by a GROUP BY or HAVING. Fields
// listed in a GROUP BY or HAVING clause need to be present in the
// query.
$fields =& $count->getFields();
foreach (array_keys($fields) as $field) {
if (empty($group_by[$field])) {
unset($fields[$field]);
}
}
$expressions =& $count->getExpressions();
foreach (array_keys($expressions) as $field) {
if (empty($group_by[$field])) {
......@@ -1626,5 +1627,5 @@ public function __clone() {
}
/**
* @} End of "ingroup database".
* @} End of "addtogroup database".
*/
......@@ -6,7 +6,7 @@
*/
/**
* @ingroup database
* @addtogroup database
* @{
*/
......@@ -37,7 +37,7 @@ class DatabaseConnection_sqlite extends DatabaseConnection {
/**
* All databases attached to the current database. This is used to allow
* prefixes to be safely handled without locking the table
*
*
* @var array
*/
protected $attachedDatabases = array();
......@@ -46,10 +46,10 @@ class DatabaseConnection_sqlite extends DatabaseConnection {
* Whether or not a table has been dropped this request: the destructor will
* only try to get rid of unnecessary databases if there is potential of them
* being empty.
*
*
* This variable is set to public because DatabaseSchema_sqlite needs to
* access it. However, it should not be manually set.
*
*
* @var boolean
*/
var $tableDropped = FALSE;
......@@ -59,16 +59,19 @@ public function __construct(array $connection_options = array()) {
$this->statementClass = NULL;
// This driver defaults to transaction support, except if explicitly passed FALSE.
$this->transactionSupport = !isset($connection_options['transactions']) || $connection_options['transactions'] !== FALSE;
$this->transactionSupport = $this->transactionalDDLSupport = !isset($connection_options['transactions']) || $connection_options['transactions'] !== FALSE;
$this->connectionOptions = $connection_options;
parent::__construct('sqlite:' . $connection_options['database'], '', '', array(
// Force column names to lower case.
PDO::ATTR_CASE => PDO::CASE_LOWER,
// Allow PDO options to be overridden.
$connection_options += array(
'pdo' => array(),
);
$connection_options['pdo'] += array(
// Convert numeric values to strings when fetching.
PDO::ATTR_STRINGIFY_FETCHES => TRUE,
));
);
parent::__construct('sqlite:' . $connection_options['database'], '', '', $connection_options['pdo']);
// Attach one database for each registered prefix.
$prefixes = $this->prefixes;
......@@ -103,6 +106,11 @@ public function __construct(array $connection_options = array()) {
$this->sqliteCreateFunction('substring', array($this, 'sqlFunctionSubstring'), 3);
$this->sqliteCreateFunction('substring_index', array($this, 'sqlFunctionSubstringIndex'), 3);
$this->sqliteCreateFunction('rand', array($this, 'sqlFunctionRand'));
// Execute sqlite init_commands.
if (isset($connection_options['init_commands'])) {
$this->exec(implode('; ', $connection_options['init_commands']));
}
}
/**
......@@ -119,7 +127,7 @@ public function __destruct() {
try {
$count = $this->query('SELECT COUNT(*) FROM ' . $prefix . '.sqlite_master WHERE type = :type AND name NOT LIKE :pattern', array(':type' => 'table', ':pattern' => 'sqlite_%'))->fetchField();
// We can prune the database file if it doens't have any tables.
// We can prune the database file if it doesn't have any tables.
if ($count == 0) {
// Detach the database.
$this->query('DETACH DATABASE :schema', array(':schema' => $prefix));
......@@ -242,7 +250,7 @@ public function queryTemporary($query, array $args = array(), array $options = a
$prefixes[$tablename] = '';
$this->setPrefix($prefixes);
$this->query(preg_replace('/^SELECT/i', 'CREATE TEMPORARY TABLE ' . $tablename . ' AS SELECT', $query), $args, $options);
$this->query('CREATE TEMPORARY TABLE ' . $tablename . ' AS ' . $query, $args, $options);
return $tablename;
}
......@@ -370,6 +378,14 @@ public function popTransaction($name) {
}
}
public function utf8mb4IsActive() {
return TRUE;
}
public function utf8mb4IsSupported() {
return TRUE;
}
}
/**
......@@ -507,5 +523,5 @@ public function execute($args = array(), $options = array()) {
}
/**
* @} End of "ingroup database".
* @} End of "addtogroup database".
*/
......@@ -14,8 +14,6 @@ public function name() {
/**
* Minimum engine version.
*
* @todo: consider upping to 3.6.8 in Drupal 8 to get SAVEPOINT support.
*/
public function minimumVersion() {
return '3.3.7';
......
......@@ -6,7 +6,7 @@
*/
/**
* @ingroup database
* @addtogroup database
* @{
*/
......@@ -41,7 +41,8 @@ public function __toString() {
// 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(', ', $this->insertFields) . ') ' . $this->fromQuery;
$insert_fields_string = $this->insertFields ? ' (' . implode(', ', $this->insertFields) . ') ' : ' ';
return $comments . 'INSERT INTO {' . $this->table . '}' . $insert_fields_string . $this->fromQuery;
}
return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $this->insertFields) . ') VALUES (' . implode(', ', $placeholders) . ')';
......@@ -57,39 +58,18 @@ public function __toString() {
* we don't select those rows.
*
* A query like this one:
* UPDATE test SET name = 'newname' WHERE tid = 1
* UPDATE test SET col1 = 'newcol1', col2 = 'newcol2' WHERE tid = 1
* will become:
* UPDATE test SET name = 'newname' WHERE tid = 1 AND name <> 'newname'
* UPDATE test SET col1 = 'newcol1', col2 = 'newcol2' WHERE tid = 1 AND (col1 <> 'newcol1' OR col2 <> 'newcol2')
*/
class UpdateQuery_sqlite extends UpdateQuery {
/**
* Helper function that removes the fields that are already in a condition.
*
* @param $fields
* The fields.
* @param QueryConditionInterface $condition
* A database condition.
*/
protected function removeFieldsInCondition(&$fields, QueryConditionInterface $condition) {
foreach ($condition->conditions() as $child_condition) {
if ($child_condition['field'] instanceof QueryConditionInterface) {
$this->removeFieldsInCondition($fields, $child_condition['field']);
}
else {
unset($fields[$child_condition['field']]);
}
}
}
public function execute() {
if (!empty($this->queryOptions['sqlite_return_matched_rows'])) {
return parent::execute();
}
// Get the fields used in the update query, and remove those that are already
// in the condition.
// Get the fields used in the update query.
$fields = $this->expressionFields + $this->fields;
$this->removeFieldsInCondition($fields, $this->condition);
// Add the inverse of the fields to the condition.
$condition = new DatabaseCondition('OR');
......@@ -156,5 +136,5 @@ public function __toString() {
}
/**
* @} End of "ingroup database".
* @} End of "addtogroup database".
*/
This diff is collapsed.
......@@ -6,7 +6,7 @@
*/
/**
* @ingroup database
* @addtogroup database
* @{
*/
......@@ -21,7 +21,7 @@ public function forUpdate($set = TRUE) {
}
/**
* @} End of "ingroup database".
* @} End of "addtogroup database".
*/
......@@ -2,7 +2,7 @@
/**
* @file
* Initialize the list of date formats and their locales.
* Initializes the list of date formats and their locales.
*/
/**
......
This diff is collapsed.