Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176

This commit is contained in:
Pantheon Automation 2015-08-17 17:00:26 -07:00 committed by Greg Anderson
commit 9921556621
13277 changed files with 1459781 additions and 0 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,13 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\ConnectionNotDefinedException.
*/
namespace Drupal\Core\Database;
/**
* Exception thrown if an undefined database connection is requested.
*/
class ConnectionNotDefinedException extends \RuntimeException {}

View file

@ -0,0 +1,531 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Database.
*/
namespace Drupal\Core\Database;
/**
* Primary front-controller for the database system.
*
* This class is uninstantiatable and un-extendable. It acts to encapsulate
* all control and shepherding of database connections into a single location
* without the use of globals.
*/
abstract class Database {
/**
* Flag to indicate a query call should simply return NULL.
*
* This is used for queries that have no reasonable return value anyway, such
* as INSERT statements to a table without a serial primary key.
*/
const RETURN_NULL = 0;
/**
* Flag to indicate a query call should return the prepared statement.
*/
const RETURN_STATEMENT = 1;
/**
* Flag to indicate a query call should return the number of affected rows.
*/
const RETURN_AFFECTED = 2;
/**
* Flag to indicate a query call should return the "last insert id".
*/
const RETURN_INSERT_ID = 3;
/**
* An nested array of all active connections. It is keyed by database name
* and target.
*
* @var array
*/
static protected $connections = array();
/**
* A processed copy of the database connection information from settings.php.
*
* @var array
*/
static protected $databaseInfo = array();
/**
* A list of key/target credentials to simply ignore.
*
* @var array
*/
static protected $ignoreTargets = array();
/**
* The key of the currently active database connection.
*
* @var string
*/
static protected $activeKey = 'default';
/**
* An array of active query log objects.
*
* Every connection has one and only one logger object for all targets and
* logging keys.
*
* array(
* '$db_key' => DatabaseLog object.
* );
*
* @var array
*/
static protected $logs = array();
/**
* Starts logging a given logging key on the specified connection.
*
* @param string $logging_key
* The logging key to log.
* @param string $key
* The database connection key for which we want to log.
*
* @return \Drupal\Core\Database\Log
* The query log object. Note that the log object does support richer
* methods than the few exposed through the Database class, so in some
* cases it may be desirable to access it directly.
*
* @see \Drupal\Core\Database\Log
*/
final public static function startLog($logging_key, $key = 'default') {
if (empty(self::$logs[$key])) {
self::$logs[$key] = new Log($key);
// Every target already active for this connection key needs to have the
// logging object associated with it.
if (!empty(self::$connections[$key])) {
foreach (self::$connections[$key] as $connection) {
$connection->setLogger(self::$logs[$key]);
}
}
}
self::$logs[$key]->start($logging_key);
return self::$logs[$key];
}
/**
* Retrieves the queries logged on for given logging key.
*
* This method also ends logging for the specified key. To get the query log
* to date without ending the logger request the logging object by starting
* it again (which does nothing to an open log key) and call methods on it as
* desired.
*
* @param string $logging_key
* The logging key to log.
* @param string $key
* The database connection key for which we want to log.
*
* @return array
* The query log for the specified logging key and connection.
*
* @see \Drupal\Core\Database\Log
*/
final public static function getLog($logging_key, $key = 'default') {
if (empty(self::$logs[$key])) {
return NULL;
}
$queries = self::$logs[$key]->get($logging_key);
self::$logs[$key]->end($logging_key);
return $queries;
}
/**
* Gets the connection object for the specified database key and target.
*
* @param string $target
* The database target name.
* @param string $key
* The database connection key. Defaults to NULL which means the active key.
*
* @return \Drupal\Core\Database\Connection
* The corresponding connection object.
*/
final public static function getConnection($target = 'default', $key = NULL) {
if (!isset($key)) {
// By default, we want the active connection, set in setActiveConnection.
$key = self::$activeKey;
}
// If the requested target does not exist, or if it is ignored, we fall back
// to the default target. The target is typically either "default" or
// "replica", indicating to use a replica SQL server if one is available. If
// it's not available, then the default/primary server is the correct server
// to use.
if (!empty(self::$ignoreTargets[$key][$target]) || !isset(self::$databaseInfo[$key][$target])) {
$target = 'default';
}
if (!isset(self::$connections[$key][$target])) {
// If necessary, a new connection is opened.
self::$connections[$key][$target] = self::openConnection($key, $target);
}
return self::$connections[$key][$target];
}
/**
* Determines if there is an active connection.
*
* Note that this method will return FALSE if no connection has been
* established yet, even if one could be.
*
* @return bool
* TRUE if there is at least one database connection established, FALSE
* otherwise.
*/
final public static function isActiveConnection() {
return !empty(self::$activeKey) && !empty(self::$connections) && !empty(self::$connections[self::$activeKey]);
}
/**
* Sets the active connection to the specified key.
*
* @return string|null
* The previous database connection key.
*/
final public static function setActiveConnection($key = 'default') {
if (!empty(self::$databaseInfo[$key])) {
$old_key = self::$activeKey;
self::$activeKey = $key;
return $old_key;
}
}
/**
* Process the configuration file for database information.
*
* @param array $info
* The database connection information, as defined in settings.php. The
* structure of this array depends on the database driver it is connecting
* to.
*/
final public static function parseConnectionInfo(array $info) {
// If there is no "driver" property, then we assume it's an array of
// possible connections for this target. Pick one at random. That allows
// us to have, for example, multiple replica servers.
if (empty($info['driver'])) {
$info = $info[mt_rand(0, count($info) - 1)];
}
// Parse the prefix information.
if (!isset($info['prefix'])) {
// Default to an empty prefix.
$info['prefix'] = array(
'default' => '',
);
}
elseif (!is_array($info['prefix'])) {
// Transform the flat form into an array form.
$info['prefix'] = array(
'default' => $info['prefix'],
);
}
return $info;
}
/**
* Adds database connection information for a given key/target.
*
* This method allows to add new connections at runtime.
*
* Under normal circumstances the preferred way to specify database
* credentials is via settings.php. However, this method allows them to be
* added at arbitrary times, such as during unit tests, when connecting to
* admin-defined third party databases, etc.
*
* If the given key/target pair already exists, this method will be ignored.
*
* @param string $key
* The database key.
* @param string $target
* The database target name.
* @param array $info
* The database connection information, as defined in settings.php. The
* structure of this array depends on the database driver it is connecting
* to.
*/
final public static function addConnectionInfo($key, $target, array $info) {
if (empty(self::$databaseInfo[$key][$target])) {
self::$databaseInfo[$key][$target] = self::parseConnectionInfo($info);
}
}
/**
* Gets information on the specified database connection.
*
* @param string $key
* (optional) The connection key for which to return information.
*
* @return array|null
*/
final public static function getConnectionInfo($key = 'default') {
if (!empty(self::$databaseInfo[$key])) {
return self::$databaseInfo[$key];
}
}
/**
* Gets connection information for all available databases.
*
* @return array
*/
final public static function getAllConnectionInfo() {
return self::$databaseInfo;
}
/**
* Sets connection information for multiple databases.
*
* @param array $databases
* A multi-dimensional array specifying database connection parameters, as
* defined in settings.php.
*/
final public static function setMultipleConnectionInfo(array $databases) {
foreach ($databases as $key => $targets) {
foreach ($targets as $target => $info) {
self::addConnectionInfo($key, $target, $info);
}
}
}
/**
* Rename a connection and its corresponding connection information.
*
* @param string $old_key
* The old connection key.
* @param string $new_key
* The new connection key.
*
* @return bool
* TRUE in case of success, FALSE otherwise.
*/
final public static function renameConnection($old_key, $new_key) {
if (!empty(self::$databaseInfo[$old_key]) && empty(self::$databaseInfo[$new_key])) {
// Migrate the database connection information.
self::$databaseInfo[$new_key] = self::$databaseInfo[$old_key];
unset(self::$databaseInfo[$old_key]);
// Migrate over the DatabaseConnection object if it exists.
if (isset(self::$connections[$old_key])) {
self::$connections[$new_key] = self::$connections[$old_key];
unset(self::$connections[$old_key]);
}
return TRUE;
}
else {
return FALSE;
}
}
/**
* Remove a connection and its corresponding connection information.
*
* @param string $key
* The connection key.
*
* @return bool
* TRUE in case of success, FALSE otherwise.
*/
final public static function removeConnection($key) {
if (isset(self::$databaseInfo[$key])) {
self::closeConnection(NULL, $key);
unset(self::$databaseInfo[$key]);
return TRUE;
}
else {
return FALSE;
}
}
/**
* Opens a connection to the server specified by the given key and target.
*
* @param string $key
* The database connection key, as specified in settings.php. The default is
* "default".
* @param string $target
* The database target to open.
*
* @throws \Drupal\Core\Database\ConnectionNotDefinedException
* @throws \Drupal\Core\Database\DriverNotSpecifiedException
*/
final protected static function openConnection($key, $target) {
// If the requested database does not exist then it is an unrecoverable
// error.
if (!isset(self::$databaseInfo[$key])) {
throw new ConnectionNotDefinedException('The specified database connection is not defined: ' . $key);
}
if (!$driver = self::$databaseInfo[$key][$target]['driver']) {
throw new DriverNotSpecifiedException('Driver not specified for this database connection: ' . $key);
}
if (!empty(self::$databaseInfo[$key][$target]['namespace'])) {
$driver_class = self::$databaseInfo[$key][$target]['namespace'] . '\\Connection';
}
else {
// Fallback for Drupal 7 settings.php.
$driver_class = "Drupal\\Core\\Database\\Driver\\{$driver}\\Connection";
}
$pdo_connection = $driver_class::open(self::$databaseInfo[$key][$target]);
$new_connection = new $driver_class($pdo_connection, self::$databaseInfo[$key][$target]);
$new_connection->setTarget($target);
$new_connection->setKey($key);
// If we have any active logging objects for this connection key, we need
// to associate them with the connection we just opened.
if (!empty(self::$logs[$key])) {
$new_connection->setLogger(self::$logs[$key]);
}
return $new_connection;
}
/**
* Closes a connection to the server specified by the given key and target.
*
* @param string $target
* The database target name. Defaults to NULL meaning that all target
* connections will be closed.
* @param string $key
* The database connection key. Defaults to NULL which means the active key.
*/
public static function closeConnection($target = NULL, $key = NULL) {
// Gets the active connection by default.
if (!isset($key)) {
$key = self::$activeKey;
}
// 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]);
}
}
/**
* Instructs the system to temporarily ignore a given key/target.
*
* At times we need to temporarily disable replica queries. To do so, call this
* method with the database key and the target to disable. That database key
* will then always fall back to 'default' for that key, even if it's defined.
*
* @param string $key
* The database connection key.
* @param string $target
* The target of the specified key to ignore.
*/
public static function ignoreTarget($key, $target) {
self::$ignoreTargets[$key][$target] = TRUE;
}
/**
* Converts a URL to a database connection info array.
*
* @param string $url
* The URL.
* @param string $root
* The root directory of the Drupal installation.
*
* @return array
* The database connection info.
*
* @throws \InvalidArgumentException
* Exception thrown when the provided URL does not meet the minimum
* requirements.
*/
public static function convertDbUrlToConnectionInfo($url, $root) {
$info = parse_url($url);
if (!isset($info['scheme'], $info['host'], $info['path'])) {
throw new \InvalidArgumentException('Minimum requirement: driver://host/database');
}
$info += array(
'user' => '',
'pass' => '',
'fragment' => '',
);
// A SQLite database path with two leading slashes indicates a system path.
// Otherwise the path is relative to the Drupal root.
if ($info['path'][0] === '/') {
$info['path'] = substr($info['path'], 1);
}
if ($info['scheme'] === 'sqlite' && $info['path'][0] !== '/') {
$info['path'] = $root . '/' . $info['path'];
}
$database = array(
'driver' => $info['scheme'],
'username' => $info['user'],
'password' => $info['pass'],
'host' => $info['host'],
'database' => $info['path'],
);
if (isset($info['port'])) {
$database['port'] = $info['port'];
}
return $database;
}
/**
* Gets database connection info as a URL.
*
* @param string $key
* (Optional) The database connection key.
*
* @return string
* The connection info as a URL.
*/
public static function getConnectionInfoAsUrl($key = 'default') {
$db_info = static::getConnectionInfo($key);
if ($db_info['default']['driver'] == 'sqlite') {
$db_url = 'sqlite://localhost/' . $db_info['default']['database'];
}
else {
$user = '';
if ($db_info['default']['username']) {
$user = $db_info['default']['username'];
if ($db_info['default']['password']) {
$user .= ':' . $db_info['default']['password'];
}
$user .= '@';
}
$db_url = $db_info['default']['driver'] . '://' . $user . $db_info['default']['host'];
if (isset($db_info['default']['port'])) {
$db_url .= ':' . $db_info['default']['port'];
}
$db_url .= '/' . $db_info['default']['database'];
}
if ($db_info['default']['prefix']['default']) {
$db_url .= '#' . $db_info['default']['prefix']['default'];
}
return $db_url;
}
}

View file

@ -0,0 +1,19 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\DatabaseException.
*/
namespace Drupal\Core\Database;
/**
* Interface for a database exception.
*
* All Database exceptions should implement this interface so that they can be
* caught collectively. Note that this applies only to Drupal-spawned
* exceptions. PDOException will not implement this interface and module
* developers should account for it separately.
*/
interface DatabaseException { }

View file

@ -0,0 +1,17 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\DatabaseExceptionWrapper.
*/
namespace Drupal\Core\Database;
/**
* This wrapper class serves only to provide additional debug information.
*
* This class will always wrap a PDOException.
*/
class DatabaseExceptionWrapper extends \RuntimeException implements DatabaseException {
}

View file

@ -0,0 +1,12 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\DatabaseNotFoundException.
*/
namespace Drupal\Core\Database;
/**
* Exception thrown if specified database is not found.
*/
class DatabaseNotFoundException extends \RuntimeException {}

View file

@ -0,0 +1,286 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\mysql\Connection.
*/
namespace Drupal\Core\Database\Driver\mysql;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\DatabaseNotFoundException;
use Drupal\Core\Database\TransactionCommitFailedException;
use Drupal\Core\Database\DatabaseException;
use Drupal\Core\Database\Connection as DatabaseConnection;
/**
* @addtogroup database
* @{
*/
class Connection extends DatabaseConnection {
/**
* Error code for "Unknown database" error.
*/
const DATABASE_NOT_FOUND = 1049;
/**
* Flag to indicate if the cleanup function in __destruct() should run.
*
* @var bool
*/
protected $needsCleanup = FALSE;
/**
* Constructs a Connection object.
*/
public function __construct(\PDO $connection, array $connection_options = array()) {
parent::__construct($connection, $connection_options);
// This driver defaults to transaction support, except if explicitly passed FALSE.
$this->transactionSupport = !isset($connection_options['transactions']) || ($connection_options['transactions'] !== FALSE);
// MySQL never supports transactional DDL.
$this->transactionalDDLSupport = FALSE;
$this->connectionOptions = $connection_options;
}
/**
* {@inheritdoc}
*/
public static function open(array &$connection_options = array()) {
// 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'];
}
else {
// 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=utf8mb4';
if (!empty($connection_options['database'])) {
$dsn .= ';dbname=' . $connection_options['database'];
}
// Allow PDO options to be overridden.
$connection_options += array(
'pdo' => array(),
);
$connection_options['pdo'] += array(
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
// So we don't have to mess around with cursors and unbuffered queries by default.
\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => TRUE,
// Make sure MySQL returns all matched rows on update queries including
// rows that actually didn't have to be updated because the values didn't
// change. This matches common behavior among other database systems.
\PDO::MYSQL_ATTR_FOUND_ROWS => TRUE,
// Because MySQL's prepared statements skip the query cache, because it's dumb.
\PDO::ATTR_EMULATE_PREPARES => TRUE,
);
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'] += [\PDO::MYSQL_ATTR_MULTI_STATEMENTS => FALSE];
}
$pdo = new \PDO($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
// 'utf8mb4_general_ci' for utf8mb4.
if (!empty($connection_options['collation'])) {
$pdo->exec('SET NAMES utf8mb4 COLLATE ' . $connection_options['collation']);
}
else {
$pdo->exec('SET NAMES utf8mb4');
}
// 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
// https://www.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 = 'ANSI,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,ONLY_FULL_GROUP_BY'",
);
// Execute initial commands.
foreach ($connection_options['init_commands'] as $sql) {
$pdo->exec($sql);
}
return $pdo;
}
/**
* {@inheritdoc}
*/
public function serialize() {
// Cleanup the connection, much like __destruct() does it as well.
if ($this->needsCleanup) {
$this->nextIdDelete();
}
$this->needsCleanup = FALSE;
return parent::serialize();
}
/**
* {@inheritdoc}
*/
public function __destruct() {
if ($this->needsCleanup) {
$this->nextIdDelete();
}
}
public function queryRange($query, $from, $count, array $args = array(), array $options = array()) {
return $this->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options);
}
public function queryTemporary($query, array $args = array(), array $options = array()) {
$tablename = $this->generateTemporaryTableName();
$this->query('CREATE TEMPORARY TABLE {' . $tablename . '} Engine=MEMORY ' . $query, $args, $options);
return $tablename;
}
public function driver() {
return 'mysql';
}
public function databaseType() {
return 'mysql';
}
/**
* Overrides \Drupal\Core\Database\Connection::createDatabase().
*
* @param string $database
* The name of the database to create.
*
* @throws \Drupal\Core\Database\DatabaseNotFoundException
*/
public function createDatabase($database) {
// Escape the database name.
$database = Database::getConnection()->escapeDatabase($database);
try {
// Create the database and set it as active.
$this->connection->exec("CREATE DATABASE $database");
$this->connection->exec("USE $database");
}
catch (\Exception $e) {
throw new DatabaseNotFoundException($e->getMessage());
}
}
public function mapConditionOperator($operator) {
// We don't want to override any of the defaults.
return NULL;
}
public function nextId($existing_id = 0) {
$new_id = $this->query('INSERT INTO {sequences} () VALUES ()', array(), array('return' => Database::RETURN_INSERT_ID));
// This should only happen after an import or similar event.
if ($existing_id >= $new_id) {
// If we INSERT a value manually into the sequences table, on the next
// INSERT, MySQL will generate a larger value. However, there is no way
// of knowing whether this value already exists in the table. MySQL
// provides an INSERT IGNORE which would work, but that can mask problems
// other than duplicate keys. Instead, we use INSERT ... ON DUPLICATE KEY
// UPDATE in such a way that the UPDATE does not do anything. This way,
// duplicate keys do not generate errors but everything else does.
$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));
}
$this->needsCleanup = TRUE;
return $new_id;
}
public function nextIdDelete() {
// While we want to clean up the table to keep it up from occupying too
// much storage and memory, we must keep the highest value in the table
// because InnoDB uses an in-memory auto-increment counter as long as the
// server runs. When the server is stopped and restarted, InnoDB
// reinitializes the counter for each table for the first INSERT to the
// table based solely on values from the table so deleting all values would
// be a problem in this case. Also, TRUNCATE resets the auto increment
// counter.
try {
$max_id = $this->query('SELECT MAX(value) FROM {sequences}')->fetchField();
// We know we are using MySQL here, no need for the slower db_delete().
$this->query('DELETE FROM {sequences} WHERE value < :value', array(':value' => $max_id));
}
// During testing, this function is called from shutdown with the
// simpletest prefix stored in $this->connection, and those tables are gone
// by the time shutdown is called so we need to ignore the database
// errors. There is no problem with completely ignoring errors here: if
// these queries fail, the sequence will work just fine, just use a bit
// more database storage and memory.
catch (DatabaseException $e) {
}
}
/**
* Overridden to work around issues to MySQL not supporting transactional DDL.
*/
protected function popCommittableTransactions() {
// Commit all the committable layers.
foreach (array_reverse($this->transactionLayers) as $name => $active) {
// Stop once we found an active transaction.
if ($active) {
break;
}
// If there are no more layers left then we should commit.
unset($this->transactionLayers[$name]);
if (empty($this->transactionLayers)) {
if (!$this->connection->commit()) {
throw new TransactionCommitFailedException();
}
}
else {
// Attempt to release this savepoint in the standard way.
try {
$this->query('RELEASE SAVEPOINT ' . $name);
}
catch (DatabaseExceptionWrapper $e) {
// However, in MySQL (InnoDB), savepoints are automatically committed
// when tables are altered or created (DDL transactions are not
// supported). This can cause exceptions due to trying to release
// savepoints which no longer exist.
//
// To avoid exceptions when no actual error has occurred, we silently
// succeed for MySQL error code 1305 ("SAVEPOINT does not exist").
if ($e->getPrevious()->errorInfo[1] == '1305') {
// If one SAVEPOINT was released automatically, then all were.
// Therefore, clean the transaction stack.
$this->transactionLayers = array();
// We also have to explain to PDO that the transaction stack has
// been cleaned-up.
$this->connection->commit();
}
else {
throw $e;
}
}
}
}
}
}
/**
* @} End of "addtogroup database".
*/

View file

@ -0,0 +1,12 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\mysql\Delete.
*/
namespace Drupal\Core\Database\Driver\mysql;
use Drupal\Core\Database\Query\Delete as QueryDelete;
class Delete extends QueryDelete { }

View file

@ -0,0 +1,86 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\mysql\Insert.
*/
namespace Drupal\Core\Database\Driver\mysql;
use Drupal\Core\Database\Query\Insert as QueryInsert;
class Insert extends QueryInsert {
public function execute() {
if (!$this->preExecute()) {
return NULL;
}
// 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)) {
$max_placeholder = 0;
$values = array();
foreach ($this->insertValues as $insert_values) {
foreach ($insert_values as $value) {
$values[':db_insert_placeholder_' . $max_placeholder++] = $value;
}
}
}
else {
$values = $this->fromQuery->getArguments();
}
$last_insert_id = $this->connection->query((string) $this, $values, $this->queryOptions);
// Re-initialize the values array so that we can re-use this query.
$this->insertValues = array();
return $last_insert_id;
}
public function __toString() {
// Create a sanitized comment string to prepend to the query.
$comments = $this->connection->makeComment($this->comments);
// 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)) {
$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 ';
$max_placeholder = 0;
$values = array();
if (count($this->insertValues)) {
foreach ($this->insertValues as $insert_values) {
$placeholders = array();
// Default fields aren't really placeholders, but this is the most convenient
// way to handle them.
$placeholders = array_pad($placeholders, count($this->defaultFields), 'default');
$new_placeholder = $max_placeholder + count($insert_values);
for ($i = $max_placeholder; $i < $new_placeholder; ++$i) {
$placeholders[] = ':db_insert_placeholder_' . $i;
}
$max_placeholder = $new_placeholder;
$values[] = '(' . implode(', ', $placeholders) . ')';
}
}
else {
// If there are no values, then this is a default-only query. We still need to handle that.
$placeholders = array_fill(0, count($this->defaultFields), 'default');
$values[] = '(' . implode(', ', $placeholders) . ')';
}
$query .= implode(', ', $values);
return $query;
}
}

View file

@ -0,0 +1,124 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\mysql\Install\Tasks.
*/
namespace Drupal\Core\Database\Driver\mysql\Install;
use Drupal\Core\Database\Install\Tasks as InstallTasks;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Driver\mysql\Connection;
use Drupal\Core\Database\DatabaseNotFoundException;
/**
* Specifies installation tasks for MySQL and equivalent databases.
*/
class Tasks extends InstallTasks {
/**
* The PDO driver name for MySQL and equivalent databases.
*
* @var string
*/
protected $pdoDriver = 'mysql';
/**
* Constructs a \Drupal\Core\Database\Driver\mysql\Install\Tasks object.
*/
public function __construct() {
$this->tasks[] = array(
'arguments' => array(
'SET NAMES utf8mb4',
'The %name database server supports utf8mb4 character encoding.',
'The %name database server must support utf8mb4 character encoding to work with Drupal. Make sure to use a database server that supports utf8mb4 character encoding, such as MySQL/MariaDB/Percona versions 5.5.3 and up.',
),
);
$this->tasks[] = array(
'arguments' => array(),
'function' => 'ensureInnoDbAvailable',
);
}
/**
* {@inheritdoc}
*/
public function name() {
return t('MySQL, MariaDB, Percona Server, or equivalent');
}
/**
* {@inheritdoc}
*/
public function minimumVersion() {
return '5.5.3';
}
/**
* {@inheritdoc}
*/
protected function connect() {
try {
// This doesn't actually test the connection.
db_set_active();
// Now actually do a check.
Database::getConnection();
$this->pass('Drupal can CONNECT to the database ok.');
}
catch (\Exception $e) {
// Attempt to create the database if it is not found.
if ($e->getCode() == Connection::DATABASE_NOT_FOUND) {
// Remove the database string from connection info.
$connection_info = Database::getConnectionInfo();
$database = $connection_info['default']['database'];
unset($connection_info['default']['database']);
// In order to change the Database::$databaseInfo array, need to remove
// the active connection, then re-add it with the new info.
Database::removeConnection('default');
Database::addConnectionInfo('default', 'default', $connection_info['default']);
try {
// Now, attempt the connection again; if it's successful, attempt to
// create the database.
Database::getConnection()->createDatabase($database);
}
catch (DatabaseNotFoundException $e) {
// Still no dice; probably a permission issue. Raise the error to the
// installer.
$this->fail(t('Database %database not found. The server reports the following message when attempting to create the database: %error.', array('%database' => $database, '%error' => $e->getMessage())));
}
}
else {
// Database connection failed for some other reason than the database
// not existing.
$this->fail(t('Failed to connect to your database server. The server reports the following message: %error.<ul><li>Is the database server running?</li><li>Does the database exist or does the database user have sufficient privileges to create the database?</li><li>Have you entered the correct database name?</li><li>Have you entered the correct username and password?</li><li>Have you entered the correct database hostname?</li></ul>', array('%error' => $e->getMessage())));
return FALSE;
}
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getFormOptions(array $database) {
$form = parent::getFormOptions($database);
if (empty($form['advanced_options']['port']['#default_value'])) {
$form['advanced_options']['port']['#default_value'] = '3306';
}
return $form;
}
/**
* Ensure that InnoDB is available.
*/
function ensureInnoDbAvailable() {
$engines = Database::getConnection()->query('SHOW ENGINES')->fetchAllKeyed();
if (isset($engines['MyISAM']) && $engines['MyISAM'] == 'DEFAULT' && !isset($engines['InnoDB'])) {
$this->fail(t('The MyISAM storage engine is not supported.'));
}
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\mysql\Merge.
*/
namespace Drupal\Core\Database\Driver\mysql;
use Drupal\Core\Database\Query\Merge as QueryMerge;
class Merge extends QueryMerge { }

View file

@ -0,0 +1,590 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\mysql\Schema.
*/
namespace Drupal\Core\Database\Driver\mysql;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Database\SchemaObjectExistsException;
use Drupal\Core\Database\SchemaObjectDoesNotExistException;
use Drupal\Core\Database\Schema as DatabaseSchema;
use Drupal\Component\Utility\Unicode;
/**
* @addtogroup schemaapi
* @{
*/
class Schema extends DatabaseSchema {
/**
* Maximum length of a table comment in MySQL.
*/
const COMMENT_MAX_TABLE = 60;
/**
* Maximum length of a column comment in MySQL.
*/
const COMMENT_MAX_COLUMN = 255;
/**
* @var array
* List of MySQL string types.
*/
protected $mysqlStringTypes = array(
'VARCHAR',
'CHAR',
'TINYTEXT',
'MEDIUMTEXT',
'LONGTEXT',
'TEXT',
);
/**
* Get information about the table and database name from the prefix.
*
* @return
* A keyed array with information about the database, table name and prefix.
*/
protected function getPrefixInfo($table = 'default', $add_prefix = TRUE) {
$info = array('prefix' => $this->connection->tablePrefix($table));
if ($add_prefix) {
$table = $info['prefix'] . $table;
}
if (($pos = strpos($table, '.')) !== FALSE) {
$info['database'] = substr($table, 0, $pos);
$info['table'] = substr($table, ++$pos);
}
else {
$db_info = Database::getConnectionInfo();
$info['database'] = $db_info[$this->connection->getTarget()]['database'];
$info['table'] = $table;
}
return $info;
}
/**
* Build a condition to match a table name against a standard information_schema.
*
* MySQL uses databases like schemas rather than catalogs so when we build
* a condition to query the information_schema.tables, we set the default
* database as the schema unless specified otherwise, and exclude table_catalog
* from the condition criteria.
*/
protected function buildTableNameCondition($table_name, $operator = '=', $add_prefix = TRUE) {
$table_info = $this->getPrefixInfo($table_name, $add_prefix);
$condition = new Condition('AND');
$condition->condition('table_schema', $table_info['database']);
$condition->condition('table_name', $table_info['table'], $operator);
return $condition;
}
/**
* Generate SQL to create a new table from a Drupal schema definition.
*
* @param $name
* The name of the table to create.
* @param $table
* A Schema API table definition array.
* @return
* An array of SQL statements to create the table.
*/
protected function createTableSql($name, $table) {
$info = $this->connection->getConnectionOptions();
// Provide defaults if needed.
$table += array(
'mysql_engine' => 'InnoDB',
'mysql_character_set' => 'utf8mb4',
);
$sql = "CREATE TABLE {" . $name . "} (\n";
// Add the SQL statement for each field.
foreach ($table['fields'] as $field_name => $field) {
$sql .= $this->createFieldSql($field_name, $this->processField($field)) . ", \n";
}
// Process keys & indexes.
$keys = $this->createKeysSql($table);
if (count($keys)) {
$sql .= implode(", \n", $keys) . ", \n";
}
// Remove the last comma and space.
$sql = substr($sql, 0, -3) . "\n) ";
$sql .= 'ENGINE = ' . $table['mysql_engine'] . ' DEFAULT CHARACTER SET ' . $table['mysql_character_set'];
// By default, MySQL uses the default collation for new tables, which is
// 'utf8mb4_general_ci' for utf8mb4. If an alternate collation has been
// set, it needs to be explicitly specified.
// @see DatabaseConnection_mysql
if (!empty($info['collation'])) {
$sql .= ' COLLATE ' . $info['collation'];
}
// Add table comment.
if (!empty($table['description'])) {
$sql .= ' COMMENT ' . $this->prepareComment($table['description'], self::COMMENT_MAX_TABLE);
}
return array($sql);
}
/**
* Create an SQL string for a field to be used in table creation or alteration.
*
* Before passing a field out of a schema definition into this function it has
* to be processed by _db_process_field().
*
* @param string $name
* Name of the field.
* @param array $spec
* The field specification, as per the schema data structure format.
*/
protected function createFieldSql($name, $spec) {
$sql = "`" . $name . "` " . $spec['mysql_type'];
if (in_array($spec['mysql_type'], $this->mysqlStringTypes)) {
if (isset($spec['length'])) {
$sql .= '(' . $spec['length'] . ')';
}
if (!empty($spec['binary'])) {
$sql .= ' BINARY';
}
// Note we check for the "type" key here. "mysql_type" is VARCHAR:
if (isset($spec['type']) && $spec['type'] == 'varchar_ascii') {
$sql .= ' CHARACTER SET ascii COLLATE ascii_general_ci';
}
}
elseif (isset($spec['precision']) && isset($spec['scale'])) {
$sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')';
}
if (!empty($spec['unsigned'])) {
$sql .= ' unsigned';
}
if (isset($spec['not null'])) {
if ($spec['not null']) {
$sql .= ' NOT NULL';
}
else {
$sql .= ' NULL';
}
}
if (!empty($spec['auto_increment'])) {
$sql .= ' auto_increment';
}
// $spec['default'] can be NULL, so we explicitly check for the key here.
if (array_key_exists('default', $spec)) {
$sql .= ' DEFAULT ' . $this->escapeDefaultValue($spec['default']);
}
if (empty($spec['not null']) && !isset($spec['default'])) {
$sql .= ' DEFAULT NULL';
}
// Add column comment.
if (!empty($spec['description'])) {
$sql .= ' COMMENT ' . $this->prepareComment($spec['description'], self::COMMENT_MAX_COLUMN);
}
return $sql;
}
/**
* Set database-engine specific properties for a field.
*
* @param $field
* A field description array, as specified in the schema documentation.
*/
protected function processField($field) {
if (!isset($field['size'])) {
$field['size'] = 'normal';
}
// Set the correct database-engine specific datatype.
// In case one is already provided, force it to uppercase.
if (isset($field['mysql_type'])) {
$field['mysql_type'] = Unicode::strtoupper($field['mysql_type']);
}
else {
$map = $this->getFieldTypeMap();
$field['mysql_type'] = $map[$field['type'] . ':' . $field['size']];
}
if (isset($field['type']) && $field['type'] == 'serial') {
$field['auto_increment'] = TRUE;
}
return $field;
}
public function getFieldTypeMap() {
// Put :normal last so it gets preserved by array_flip. This makes
// it much easier for modules (such as schema.module) to map
// database types back into schema types.
// $map does not use drupal_static as its value never changes.
static $map = array(
'varchar_ascii:normal' => 'VARCHAR',
'varchar:normal' => 'VARCHAR',
'char:normal' => 'CHAR',
'text:tiny' => 'TINYTEXT',
'text:small' => 'TINYTEXT',
'text:medium' => 'MEDIUMTEXT',
'text:big' => 'LONGTEXT',
'text:normal' => 'TEXT',
'serial:tiny' => 'TINYINT',
'serial:small' => 'SMALLINT',
'serial:medium' => 'MEDIUMINT',
'serial:big' => 'BIGINT',
'serial:normal' => 'INT',
'int:tiny' => 'TINYINT',
'int:small' => 'SMALLINT',
'int:medium' => 'MEDIUMINT',
'int:big' => 'BIGINT',
'int:normal' => 'INT',
'float:tiny' => 'FLOAT',
'float:small' => 'FLOAT',
'float:medium' => 'FLOAT',
'float:big' => 'DOUBLE',
'float:normal' => 'FLOAT',
'numeric:normal' => 'DECIMAL',
'blob:big' => 'LONGBLOB',
'blob:normal' => 'BLOB',
);
return $map;
}
protected function createKeysSql($spec) {
$keys = array();
if (!empty($spec['primary key'])) {
$keys[] = 'PRIMARY KEY (' . $this->createKeySql($spec['primary key']) . ')';
}
if (!empty($spec['unique keys'])) {
foreach ($spec['unique keys'] as $key => $fields) {
$keys[] = 'UNIQUE KEY `' . $key . '` (' . $this->createKeySql($fields) . ')';
}
}
if (!empty($spec['indexes'])) {
$indexes = $this->getNormalizedIndexes($spec);
foreach ($indexes as $index => $fields) {
$keys[] = 'INDEX `' . $index . '` (' . $this->createKeySql($fields) . ')';
}
}
return $keys;
}
/**
* Gets normalized indexes from a table specification.
*
* Shortens indexes to 191 characters if they apply to utf8mb4-encoded
* fields, in order to comply with the InnoDB index limitation of 756 bytes.
*
* @param $spec
* The table specification.
*
* @return array
* List of shortened indexes.
*/
protected function getNormalizedIndexes($spec) {
$indexes = $spec['indexes'];
foreach ($indexes as $index_name => $index_fields) {
foreach ($index_fields as $index_key => $index_field) {
// Get the name of the field from the index specification.
$field_name = is_array($index_field) ? $index_field[0] : $index_field;
// Check whether the field is defined in the table specification.
if (isset($spec['fields'][$field_name])) {
// Get the MySQL type from the processed field.
$mysql_field = $this->processField($spec['fields'][$field_name]);
if (in_array($mysql_field['mysql_type'], $this->mysqlStringTypes)) {
// Check whether we need to shorten the index.
if ((!isset($mysql_field['type']) || $mysql_field['type'] != 'varchar_ascii') && (!isset($mysql_field['length']) || $mysql_field['length'] > 191)) {
// Limit the index length to 191 characters.
$this->shortenIndex($indexes[$index_name][$index_key]);
}
}
}
}
}
return $indexes;
}
/**
* Helper function for normalizeIndexes().
*
* Shortens an index to 191 characters.
*
* @param array $index
* The index array to be used in createKeySql.
*
* @see Drupal\Core\Database\Driver\mysql\Schema::createKeySql()
* @see Drupal\Core\Database\Driver\mysql\Schema::normalizeIndexes()
*/
protected function shortenIndex(&$index) {
if (is_array($index)) {
if ($index[1] > 191) {
$index[1] = 191;
}
}
else {
$index = array($index, 191);
}
}
protected function createKeySql($fields) {
$return = array();
foreach ($fields as $field) {
if (is_array($field)) {
$return[] = '`' . $field[0] . '`(' . $field[1] . ')';
}
else {
$return[] = '`' . $field . '`';
}
}
return implode(', ', $return);
}
public function renameTable($table, $new_name) {
if (!$this->tableExists($table)) {
throw new SchemaObjectDoesNotExistException(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 SchemaObjectExistsException(t("Cannot rename @table to @table_new: table @table_new already exists.", array('@table' => $table, '@table_new' => $new_name)));
}
$info = $this->getPrefixInfo($new_name);
return $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO `' . $info['table'] . '`');
}
public function dropTable($table) {
if (!$this->tableExists($table)) {
return FALSE;
}
$this->connection->query('DROP TABLE {' . $table . '}');
return TRUE;
}
public function addField($table, $field, $spec, $keys_new = array()) {
if (!$this->tableExists($table)) {
throw new SchemaObjectDoesNotExistException(t("Cannot add field @table.@field: table doesn't exist.", array('@field' => $field, '@table' => $table)));
}
if ($this->fieldExists($table, $field)) {
throw new SchemaObjectExistsException(t("Cannot add field @table.@field: field already exists.", array('@field' => $field, '@table' => $table)));
}
$fixnull = FALSE;
if (!empty($spec['not null']) && !isset($spec['default'])) {
$fixnull = TRUE;
$spec['not null'] = FALSE;
}
$query = 'ALTER TABLE {' . $table . '} ADD ';
$query .= $this->createFieldSql($field, $this->processField($spec));
if ($keys_sql = $this->createKeysSql($keys_new)) {
$query .= ', ADD ' . implode(', ADD ', $keys_sql);
}
$this->connection->query($query);
if (isset($spec['initial'])) {
$this->connection->update($table)
->fields(array($field => $spec['initial']))
->execute();
}
if ($fixnull) {
$spec['not null'] = TRUE;
$this->changeField($table, $field, $field, $spec);
}
}
public function dropField($table, $field) {
if (!$this->fieldExists($table, $field)) {
return FALSE;
}
$this->connection->query('ALTER TABLE {' . $table . '} DROP `' . $field . '`');
return TRUE;
}
public function fieldSetDefault($table, $field, $default) {
if (!$this->fieldExists($table, $field)) {
throw new SchemaObjectDoesNotExistException(t("Cannot set default value of field @table.@field: field doesn't exist.", array('@table' => $table, '@field' => $field)));
}
$this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN `' . $field . '` SET DEFAULT ' . $this->escapeDefaultValue($default));
}
public function fieldSetNoDefault($table, $field) {
if (!$this->fieldExists($table, $field)) {
throw new SchemaObjectDoesNotExistException(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');
}
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 = ' . $this->connection->quote($name))->fetchAssoc();
return isset($row['Key_name']);
}
public function addPrimaryKey($table, $fields) {
if (!$this->tableExists($table)) {
throw new SchemaObjectDoesNotExistException(t("Cannot add primary key to table @table: table doesn't exist.", array('@table' => $table)));
}
if ($this->indexExists($table, 'PRIMARY')) {
throw new SchemaObjectExistsException(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) . ')');
}
public function dropPrimaryKey($table) {
if (!$this->indexExists($table, 'PRIMARY')) {
return FALSE;
}
$this->connection->query('ALTER TABLE {' . $table . '} DROP PRIMARY KEY');
return TRUE;
}
public function addUniqueKey($table, $name, $fields) {
if (!$this->tableExists($table)) {
throw new SchemaObjectDoesNotExistException(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 SchemaObjectExistsException(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) . ')');
}
public function dropUniqueKey($table, $name) {
if (!$this->indexExists($table, $name)) {
return FALSE;
}
$this->connection->query('ALTER TABLE {' . $table . '} DROP KEY `' . $name . '`');
return TRUE;
}
public function addIndex($table, $name, $fields) {
if (!$this->tableExists($table)) {
throw new SchemaObjectDoesNotExistException(t("Cannot add index @name to table @table: table doesn't exist.", array('@table' => $table, '@name' => $name)));
}
if ($this->indexExists($table, $name)) {
throw new SchemaObjectExistsException(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) . ')');
}
public function dropIndex($table, $name) {
if (!$this->indexExists($table, $name)) {
return FALSE;
}
$this->connection->query('ALTER TABLE {' . $table . '} DROP INDEX `' . $name . '`');
return TRUE;
}
public function changeField($table, $field, $field_new, $spec, $keys_new = array()) {
if (!$this->fieldExists($table, $field)) {
throw new SchemaObjectDoesNotExistException(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 SchemaObjectExistsException(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));
if ($keys_sql = $this->createKeysSql($keys_new)) {
$sql .= ', ADD ' . implode(', ADD ', $keys_sql);
}
$this->connection->query($sql);
}
public function prepareComment($comment, $length = NULL) {
// Truncate comment to maximum comment length.
if (isset($length)) {
// Add table prefixes before truncating.
$comment = Unicode::truncate($this->connection->prefixTables($comment), $length, TRUE, TRUE);
}
return $this->connection->quote($comment);
}
/**
* Retrieve a table or column comment.
*/
public function getComment($table, $column = NULL) {
$condition = $this->buildTableNameCondition($table);
if (isset($column)) {
$condition->condition('column_name', $column);
$condition->compile($this->connection, $this);
// Don't use {} around information_schema.columns table.
return $this->connection->query("SELECT column_comment FROM information_schema.columns WHERE " . (string) $condition, $condition->arguments())->fetchField();
}
$condition->compile($this->connection, $this);
// Don't use {} around information_schema.tables table.
$comment = $this->connection->query("SELECT table_comment FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchField();
// Work-around for MySQL 5.0 bug http://bugs.mysql.com/bug.php?id=11379
return preg_replace('/; InnoDB free:.*$/', '', $comment);
}
public function tableExists($table) {
// The information_schema table is very slow to query under MySQL 5.0.
// Instead, we try to select from the table in question. If it fails,
// the most likely reason is that it does not exist. That is dramatically
// faster than using information_schema.
// @link http://bugs.mysql.com/bug.php?id=19588
// @todo This override should be removed once we require a version of MySQL
// that has that bug fixed.
try {
$this->connection->queryRange("SELECT 1 FROM {" . $table . "}", 0, 1);
return TRUE;
}
catch (\Exception $e) {
return FALSE;
}
}
public function fieldExists($table, $column) {
// The information_schema table is very slow to query under MySQL 5.0.
// Instead, we try to select from the table and field in question. If it
// fails, the most likely reason is that it does not exist. That is
// dramatically faster than using information_schema.
// @link http://bugs.mysql.com/bug.php?id=19588
// @todo This override should be removed once we require a version of MySQL
// that has that bug fixed.
try {
$this->connection->queryRange("SELECT $column FROM {" . $table . "}", 0, 1);
return TRUE;
}
catch (\Exception $e) {
return FALSE;
}
}
}
/**
* @} End of "addtogroup schemaapi".
*/

View file

@ -0,0 +1,12 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\mysql\Select.
*/
namespace Drupal\Core\Database\Driver\mysql;
use Drupal\Core\Database\Query\Select as QuerySelect;
class Select extends QuerySelect { }

View file

@ -0,0 +1,12 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\mysql\Transaction.
*/
namespace Drupal\Core\Database\Driver\mysql;
use Drupal\Core\Database\Transaction as DatabaseTransaction;
class Transaction extends DatabaseTransaction { }

View file

@ -0,0 +1,12 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\mysql\Truncate.
*/
namespace Drupal\Core\Database\Driver\mysql;
use Drupal\Core\Database\Query\Truncate as QueryTruncate;
class Truncate extends QueryTruncate { }

View file

@ -0,0 +1,12 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\mysql\Update.
*/
namespace Drupal\Core\Database\Driver\mysql;
use Drupal\Core\Database\Query\Update as QueryUpdate;
class Update extends QueryUpdate { }

View file

@ -0,0 +1,357 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\pgsql\Connection.
*/
namespace Drupal\Core\Database\Driver\pgsql;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Connection as DatabaseConnection;
use Drupal\Core\Database\DatabaseNotFoundException;
use Drupal\Core\Database\StatementInterface;
use Drupal\Core\Database\IntegrityConstraintViolationException;
use Drupal\Core\Database\DatabaseExceptionWrapper;
/**
* @addtogroup database
* @{
*/
class Connection extends DatabaseConnection {
/**
* The name by which to obtain a lock for retrieve the next insert id.
*/
const POSTGRESQL_NEXTID_LOCK = 1000;
/**
* Error code for "Unknown database" error.
*/
const DATABASE_NOT_FOUND = 7;
/**
* Constructs a connection object.
*/
public function __construct(\PDO $connection, array $connection_options) {
parent::__construct($connection, $connection_options);
// This driver defaults to transaction support, except if explicitly passed FALSE.
$this->transactionSupport = !isset($connection_options['transactions']) || ($connection_options['transactions'] !== FALSE);
// Transactional DDL is always available in PostgreSQL,
// but we'll only enable it if standard transactions are.
$this->transactionalDDLSupport = $this->transactionSupport;
$this->connectionOptions = $connection_options;
// Force PostgreSQL to use the UTF-8 character set by default.
$this->connection->exec("SET NAMES 'UTF8'");
// Execute PostgreSQL init_commands.
if (isset($connection_options['init_commands'])) {
$this->connection->exec(implode('; ', $connection_options['init_commands']));
}
}
/**
* {@inheritdoc}
*/
public static function open(array &$connection_options = array()) {
// Default to TCP connection on port 5432.
if (empty($connection_options['port'])) {
$connection_options['port'] = 5432;
}
// PostgreSQL in trust mode doesn't require a password to be supplied.
if (empty($connection_options['password'])) {
$connection_options['password'] = NULL;
}
// If the password contains a backslash it is treated as an escape character
// http://bugs.php.net/bug.php?id=53217
// so backslashes in the password need to be doubled up.
// The bug was reported against pdo_pgsql 1.0.2, backslashes in passwords
// will break on this doubling up when the bug is fixed, so check the version
//elseif (phpversion('pdo_pgsql') < 'version_this_was_fixed_in') {
else {
$connection_options['password'] = str_replace('\\', '\\\\', $connection_options['password']);
}
$connection_options['database'] = (!empty($connection_options['database']) ? $connection_options['database'] : 'template1');
$dsn = 'pgsql:host=' . $connection_options['host'] . ' dbname=' . $connection_options['database'] . ' port=' . $connection_options['port'];
// Allow PDO options to be overridden.
$connection_options += array(
'pdo' => array(),
);
$connection_options['pdo'] += array(
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
// Prepared statements are most effective for performance when queries
// are recycled (used several times). However, if they are not re-used,
// prepared statements become inefficient. Since most of Drupal's
// prepared queries are not re-used, it should be faster to emulate
// the preparation than to actually ready statements for re-use. If in
// doubt, reset to FALSE and measure performance.
\PDO::ATTR_EMULATE_PREPARES => TRUE,
// Convert numeric values to strings when fetching.
\PDO::ATTR_STRINGIFY_FETCHES => TRUE,
);
$pdo = new \PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
return $pdo;
}
/**
* {@inheritdoc}
*/
public function query($query, array $args = array(), $options = array()) {
$options += $this->defaultOptions();
// The PDO PostgreSQL driver has a bug which doesn't type cast booleans
// correctly when parameters are bound using associative arrays.
// @see http://bugs.php.net/bug.php?id=48383
foreach ($args as &$value) {
if (is_bool($value)) {
$value = (int) $value;
}
}
return parent::query($query, $args, $options);
}
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 queryRange($query, $from, $count, array $args = array(), array $options = array()) {
return $this->query($query . ' LIMIT ' . (int) $count . ' OFFSET ' . (int) $from, $args, $options);
}
public function queryTemporary($query, array $args = array(), array $options = array()) {
$tablename = $this->generateTemporaryTableName();
$this->query('CREATE TEMPORARY TABLE {' . $tablename . '} AS ' . $query, $args, $options);
return $tablename;
}
/**
* {@inheritdoc}
*/
public function escapeField($field) {
$escaped = parent::escapeField($field);
// Remove any invalid start character.
$escaped = preg_replace('/^[^A-Za-z0-9_]/', '', $escaped);
// The pgsql database driver does not support field names that contain
// periods (supported by PostgreSQL server) because this method may be
// called by a field with a table alias as part of SQL conditions or
// order by statements. This will consider a period as a table alias
// identifier, and split the string at the first period.
if (preg_match('/^([A-Za-z0-9_]+)"?[.]"?([A-Za-z0-9_.]+)/', $escaped, $parts)) {
$table = $parts[1];
$column = $parts[2];
// Use escape alias because escapeField may contain multiple periods that
// need to be escaped.
$escaped = $this->escapeTable($table) . '.' . $this->escapeAlias($column);
}
elseif (preg_match('/[A-Z]/', $escaped)) {
// Quote the field name for case-sensitivity.
$escaped = '"' . $escaped . '"';
}
return $escaped;
}
/**
* {@inheritdoc}
*/
public function escapeAlias($field) {
$escaped = preg_replace('/[^A-Za-z0-9_]+/', '', $field);
// Escape the alias in quotes for case-sensitivity.
if (preg_match('/[A-Z]/', $escaped)) {
$escaped = '"' . $escaped . '"';
}
return $escaped;
}
/**
* {@inheritdoc}
*/
public function escapeTable($table) {
$escaped = parent::escapeTable($table);
// Quote identifier to make it case-sensitive.
if (preg_match('/[A-Z]/', $escaped)) {
$escaped = '"' . $escaped . '"';
}
return $escaped;
}
public function driver() {
return 'pgsql';
}
public function databaseType() {
return 'pgsql';
}
/**
* Overrides \Drupal\Core\Database\Connection::createDatabase().
*
* @param string $database
* The name of the database to create.
*
* @throws \Drupal\Core\Database\DatabaseNotFoundException
*/
public function createDatabase($database) {
// Escape the database name.
$database = Database::getConnection()->escapeDatabase($database);
// If the PECL intl extension is installed, use it to determine the proper
// locale. Otherwise, fall back to en_US.
if (class_exists('Locale')) {
$locale = \Locale::getDefault();
}
else {
$locale = 'en_US';
}
try {
// Create the database and set it as active.
$this->connection->exec("CREATE DATABASE $database WITH TEMPLATE template0 ENCODING='utf8' LC_CTYPE='$locale.utf8' LC_COLLATE='$locale.utf8'");
}
catch (\Exception $e) {
throw new DatabaseNotFoundException($e->getMessage());
}
}
public function mapConditionOperator($operator) {
static $specials = array(
// In PostgreSQL, 'LIKE' is case-sensitive. For case-insensitive LIKE
// statements, we need to use ILIKE instead.
'LIKE' => array('operator' => 'ILIKE'),
'LIKE BINARY' => array('operator' => 'LIKE'),
'NOT LIKE' => array('operator' => 'NOT ILIKE'),
'REGEXP' => array('operator' => '~*'),
);
return isset($specials[$operator]) ? $specials[$operator] : NULL;
}
/**
* Retrieve a the next id in a sequence.
*
* PostgreSQL has built in sequences. We'll use these instead of inserting
* and updating a sequences table.
*/
public function nextId($existing = 0) {
// Retrieve the name of the sequence. This information cannot be cached
// because the prefix may change, for example, like it does in simpletests.
$sequence_name = $this->makeSequenceName('sequences', 'value');
// When PostgreSQL gets a value too small then it will lock the table,
// retry the INSERT and if it's still too small then alter the sequence.
$id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
if ($id > $existing) {
return $id;
}
// PostgreSQL advisory locks are simply locks to be used by an
// application such as Drupal. This will prevent other Drupal processes
// from altering the sequence while we are.
$this->query("SELECT pg_advisory_lock(" . self::POSTGRESQL_NEXTID_LOCK . ")");
// While waiting to obtain the lock, the sequence may have been altered
// so lets try again to obtain an adequate value.
$id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
if ($id > $existing) {
$this->query("SELECT pg_advisory_unlock(" . self::POSTGRESQL_NEXTID_LOCK . ")");
return $id;
}
// Reset the sequence to a higher value than the existing id.
$this->query("ALTER SEQUENCE " . $sequence_name . " RESTART WITH " . ($existing + 1));
// Retrieve the next id. We know this will be as high as we want it.
$id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
$this->query("SELECT pg_advisory_unlock(" . self::POSTGRESQL_NEXTID_LOCK . ")");
return $id;
}
/**
* {@inheritdoc}
*/
public function getFullQualifiedTableName($table) {
$options = $this->getConnectionOptions();
$prefix = $this->tablePrefix($table);
// The fully qualified table name in PostgreSQL is in the form of
// <database>.<schema>.<table>, so we have to include the 'public' schema in
// the return value.
return $options['database'] . '.public.' . $prefix . $table;
}
/**
* Add a new savepoint with an unique name.
*
* The main use for this method is to mimic InnoDB functionality, which
* provides an inherent savepoint before any query in a transaction.
*
* @param $savepoint_name
* A string representing the savepoint name. By default,
* "mimic_implicit_commit" is used.
*
* @see Drupal\Core\Database\Connection::pushTransaction().
*/
public function addSavepoint($savepoint_name = 'mimic_implicit_commit') {
if ($this->inTransaction()) {
$this->pushTransaction($savepoint_name);
}
}
/**
* Release a savepoint by name.
*
* @param $savepoint_name
* A string representing the savepoint name. By default,
* "mimic_implicit_commit" is used.
*
* @see Drupal\Core\Database\Connection::popTransaction().
*/
public function releaseSavepoint($savepoint_name = 'mimic_implicit_commit') {
if (isset($this->transactionLayers[$savepoint_name])) {
$this->popTransaction($savepoint_name);
}
}
/**
* Rollback a savepoint by name if it exists.
*
* @param $savepoint_name
* A string representing the savepoint name. By default,
* "mimic_implicit_commit" is used.
*/
public function rollbackSavepoint($savepoint_name = 'mimic_implicit_commit') {
if (isset($this->transactionLayers[$savepoint_name])) {
$this->rollback($savepoint_name);
}
}
}
/**
* @} End of "addtogroup database".
*/

View file

@ -0,0 +1,31 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\pgsql\Delete.
*/
namespace Drupal\Core\Database\Driver\pgsql;
use Drupal\Core\Database\Query\Delete as QueryDelete;
class Delete extends QueryDelete {
/**
* {@inheritdoc}
*/
public function execute() {
$this->connection->addSavepoint();
try {
$result = parent::execute();
}
catch (\Exception $e) {
$this->connection->rollbackSavepoint();
throw $e;
}
$this->connection->releaseSavepoint();
return $result;
}
}

View file

@ -0,0 +1,157 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\pgsql\Insert.
*/
namespace Drupal\Core\Database\Driver\pgsql;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Query\Insert as QueryInsert;
/**
* @ingroup database
* @{
*/
class Insert extends QueryInsert {
public function execute() {
if (!$this->preExecute()) {
return NULL;
}
$stmt = $this->connection->prepareQuery((string) $this);
// Fetch the list of blobs and sequences used on that table.
$table_information = $this->connection->schema()->queryTableInformation($this->table);
$max_placeholder = 0;
$blobs = array();
$blob_count = 0;
foreach ($this->insertValues as $insert_values) {
foreach ($this->insertFields as $idx => $field) {
if (isset($table_information->blob_fields[$field])) {
$blobs[$blob_count] = fopen('php://memory', 'a');
fwrite($blobs[$blob_count], $insert_values[$idx]);
rewind($blobs[$blob_count]);
$stmt->bindParam(':db_insert_placeholder_' . $max_placeholder++, $blobs[$blob_count], \PDO::PARAM_LOB);
// Pre-increment is faster in PHP than increment.
++$blob_count;
}
else {
$stmt->bindParam(':db_insert_placeholder_' . $max_placeholder++, $insert_values[$idx]);
}
}
// Check if values for a serial field has been passed.
if (!empty($table_information->serial_fields)) {
foreach ($table_information->serial_fields as $index => $serial_field) {
$serial_key = array_search($serial_field, $this->insertFields);
if ($serial_key !== FALSE) {
$serial_value = $insert_values[$serial_key];
// Force $last_insert_id to the specified value. This is only done
// if $index is 0.
if ($index == 0) {
$last_insert_id = $serial_value;
}
// Sequences must be greater than or equal to 1.
if ($serial_value === NULL || !$serial_value) {
$serial_value = 1;
}
// Set the sequence to the bigger value of either the passed
// value or the max value of the column. It can happen that another
// thread calls nextval() which could lead to a serial number being
// used twice. However, trying to insert a value into a serial
// column should only be done in very rare cases and is not thread
// safe by definition.
$this->connection->query("SELECT setval('" . $table_information->sequences[$index] . "', GREATEST(MAX(" . $serial_field . "), :serial_value)) FROM {" . $this->table . "}", array(':serial_value' => (int)$serial_value));
}
}
}
}
if (!empty($this->fromQuery)) {
// bindParam stores only a reference to the variable that is followed when
// the statement is executed. We pass $arguments[$key] instead of $value
// because the second argument to bindParam is passed by reference and
// the foreach statement assigns the element to the existing reference.
$arguments = $this->fromQuery->getArguments();
foreach ($arguments as $key => $value) {
$stmt->bindParam($key, $arguments[$key]);
}
}
// PostgreSQL requires the table name to be specified explicitly
// when requesting the last insert ID, so we pass that in via
// the options array.
$options = $this->queryOptions;
if (!empty($table_information->sequences)) {
$options['sequence_name'] = $table_information->sequences[0];
}
// If there are no sequences then we can't get a last insert id.
elseif ($options['return'] == Database::RETURN_INSERT_ID) {
$options['return'] = Database::RETURN_NULL;
}
// Only use the returned last_insert_id if it is not already set.
if (!empty($last_insert_id)) {
$this->connection->query($stmt, array(), $options);
}
else {
$last_insert_id = $this->connection->query($stmt, array(), $options);
}
// Re-initialize the values array so that we can re-use this query.
$this->insertValues = array();
return $last_insert_id;
}
public function __toString() {
// Create a sanitized comment string to prepend to the query.
$comments = $this->connection->makeComment($this->comments);
// 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)) {
$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 ';
$max_placeholder = 0;
$values = array();
if (count($this->insertValues)) {
foreach ($this->insertValues as $insert_values) {
$placeholders = array();
// Default fields aren't really placeholders, but this is the most convenient
// way to handle them.
$placeholders = array_pad($placeholders, count($this->defaultFields), 'default');
$new_placeholder = $max_placeholder + count($insert_values);
for ($i = $max_placeholder; $i < $new_placeholder; ++$i) {
$placeholders[] = ':db_insert_placeholder_' . $i;
}
$max_placeholder = $new_placeholder;
$values[] = '(' . implode(', ', $placeholders) . ')';
}
}
else {
// If there are no values, then this is a default-only query. We still need to handle that.
$placeholders = array_fill(0, count($this->defaultFields), 'default');
$values[] = '(' . implode(', ', $placeholders) . ')';
}
$query .= implode(', ', $values);
return $query;
}
}

View file

@ -0,0 +1,258 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\pgsql\Install\Tasks.
*/
namespace Drupal\Core\Database\Driver\pgsql\Install;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Install\Tasks as InstallTasks;
use Drupal\Core\Database\Driver\pgsql\Connection;
use Drupal\Core\Database\DatabaseNotFoundException;
/**
* Specifies installation tasks for PostgreSQL databases.
*/
class Tasks extends InstallTasks {
/**
* {@inheritdoc}
*/
protected $pdoDriver = 'pgsql';
/**
* Constructs a \Drupal\Core\Database\Driver\pgsql\Install\Tasks object.
*/
public function __construct() {
$this->tasks[] = array(
'function' => 'checkEncoding',
'arguments' => array(),
);
$this->tasks[] = array(
'function' => 'checkBinaryOutput',
'arguments' => array(),
);
$this->tasks[] = array(
'function' => 'initializeDatabase',
'arguments' => array(),
);
}
/**
* {@inheritdoc}
*/
public function name() {
return t('PostgreSQL');
}
/**
* {@inheritdoc}
*/
public function minimumVersion() {
return '8.3';
}
/**
* {@inheritdoc}
*/
protected function connect() {
try {
// This doesn't actually test the connection.
db_set_active();
// Now actually do a check.
Database::getConnection();
$this->pass('Drupal can CONNECT to the database ok.');
}
catch (\Exception $e) {
// Attempt to create the database if it is not found.
if ($e->getCode() == Connection::DATABASE_NOT_FOUND) {
// Remove the database string from connection info.
$connection_info = Database::getConnectionInfo();
$database = $connection_info['default']['database'];
unset($connection_info['default']['database']);
// In order to change the Database::$databaseInfo array, need to remove
// the active connection, then re-add it with the new info.
Database::removeConnection('default');
Database::addConnectionInfo('default', 'default', $connection_info['default']);
try {
// Now, attempt the connection again; if it's successful, attempt to
// create the database.
Database::getConnection()->createDatabase($database);
Database::closeConnection();
// Now, restore the database config.
Database::removeConnection('default');
$connection_info['default']['database'] = $database;
Database::addConnectionInfo('default', 'default', $connection_info['default']);
// Check the database connection.
Database::getConnection();
$this->pass('Drupal can CONNECT to the database ok.');
}
catch (DatabaseNotFoundException $e) {
// Still no dice; probably a permission issue. Raise the error to the
// installer.
$this->fail(t('Database %database not found. The server reports the following message when attempting to create the database: %error.', array('%database' => $database, '%error' => $e->getMessage())));
}
}
else {
// Database connection failed for some other reason than the database
// not existing.
$this->fail(t('Failed to connect to your database server. The server reports the following message: %error.<ul><li>Is the database server running?</li><li>Does the database exist, and have you entered the correct database name?</li><li>Have you entered the correct username and password?</li><li>Have you entered the correct database hostname?</li></ul>', array('%error' => $e->getMessage())));
return FALSE;
}
}
return TRUE;
}
/**
* Check encoding is UTF8.
*/
protected function checkEncoding() {
try {
if (db_query('SHOW server_encoding')->fetchField() == 'UTF8') {
$this->pass(t('Database is encoded in UTF-8'));
}
else {
$this->fail(t('The %driver database must use %encoding encoding to work with Drupal. Recreate the database with %encoding encoding. See !link for more details.', array(
'%encoding' => 'UTF8',
'%driver' => $this->name(),
'!link' => '<a href="INSTALL.pgsql.txt">INSTALL.pgsql.txt</a>'
)));
}
}
catch (\Exception $e) {
$this->fail(t('Drupal could not determine the encoding of the database was set to UTF-8'));
}
}
/**
* Check Binary Output.
*
* Unserializing does not work on Postgresql 9 when bytea_output is 'hex'.
*/
function checkBinaryOutput() {
// PostgreSQL < 9 doesn't support bytea_output, so verify we are running
// at least PostgreSQL 9.
$database_connection = Database::getConnection();
if (version_compare($database_connection->version(), '9') >= 0) {
if (!$this->checkBinaryOutputSuccess()) {
// First try to alter the database. If it fails, raise an error telling
// the user to do it themselves.
$connection_options = $database_connection->getConnectionOptions();
// It is safe to include the database name directly here, because this
// code is only called when a connection to the database is already
// established, thus the database name is guaranteed to be a correct
// value.
$query = "ALTER DATABASE \"" . $connection_options['database'] . "\" SET bytea_output = 'escape';";
try {
db_query($query);
}
catch (\Exception $e) {
// Ignore possible errors when the user doesn't have the necessary
// privileges to ALTER the database.
}
// Close the database connection so that the configuration parameter
// is applied to the current connection.
db_close();
// Recheck, if it fails, finally just rely on the end user to do the
// right thing.
if (!$this->checkBinaryOutputSuccess()) {
$replacements = array(
'%setting' => 'bytea_output',
'%current_value' => 'hex',
'%needed_value' => 'escape',
'!query' => "<code>" . $query . "</code>",
);
$this->fail(t("The %setting setting is currently set to '%current_value', but needs to be '%needed_value'. Change this by running the following query: !query", $replacements));
}
}
}
}
/**
* Verify that a binary data roundtrip returns the original string.
*/
protected function checkBinaryOutputSuccess() {
$bytea_output = db_query("SELECT 'encoding'::bytea AS output")->fetchField();
return ($bytea_output == 'encoding');
}
/**
* Make PostgreSQL Drupal friendly.
*/
function initializeDatabase() {
// We create some functions using global names instead of prefixing them
// like we do with table names. This is so that we don't double up if more
// than one instance of Drupal is running on a single database. We therefore
// avoid trying to create them again in that case.
try {
// Create functions.
db_query('CREATE OR REPLACE FUNCTION "greatest"(numeric, numeric) RETURNS numeric AS
\'SELECT CASE WHEN (($1 > $2) OR ($2 IS NULL)) THEN $1 ELSE $2 END;\'
LANGUAGE \'sql\''
);
db_query('CREATE OR REPLACE FUNCTION "greatest"(numeric, numeric, numeric) RETURNS numeric AS
\'SELECT greatest($1, greatest($2, $3));\'
LANGUAGE \'sql\''
);
// Don't use {} around pg_proc table.
if (!db_query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'rand'")->fetchField()) {
db_query('CREATE OR REPLACE FUNCTION "rand"() RETURNS float AS
\'SELECT random();\'
LANGUAGE \'sql\''
);
}
db_query('CREATE OR REPLACE FUNCTION "substring_index"(text, text, integer) RETURNS text AS
\'SELECT array_to_string((string_to_array($1, $2)) [1:$3], $2);\'
LANGUAGE \'sql\''
);
// Using || to concatenate in Drupal is not recommended because there are
// database drivers for Drupal that do not support the syntax, however
// they do support CONCAT(item1, item2) which we can replicate in
// PostgreSQL. PostgreSQL requires the function to be defined for each
// different argument variation the function can handle.
db_query('CREATE OR REPLACE FUNCTION "concat"(anynonarray, anynonarray) RETURNS text AS
\'SELECT CAST($1 AS text) || CAST($2 AS text);\'
LANGUAGE \'sql\'
');
db_query('CREATE OR REPLACE FUNCTION "concat"(text, anynonarray) RETURNS text AS
\'SELECT $1 || CAST($2 AS text);\'
LANGUAGE \'sql\'
');
db_query('CREATE OR REPLACE FUNCTION "concat"(anynonarray, text) RETURNS text AS
\'SELECT CAST($1 AS text) || $2;\'
LANGUAGE \'sql\'
');
db_query('CREATE OR REPLACE FUNCTION "concat"(text, text) RETURNS text AS
\'SELECT $1 || $2;\'
LANGUAGE \'sql\'
');
$this->pass(t('PostgreSQL has initialized itself.'));
}
catch (\Exception $e) {
$this->fail(t('Drupal could not be correctly setup with the existing database. Revise any errors.'));
}
}
/**
* {@inheritdoc}
*/
public function getFormOptions(array $database) {
$form = parent::getFormOptions($database);
if (empty($form['advanced_options']['port']['#default_value'])) {
$form['advanced_options']['port']['#default_value'] = '5432';
}
return $form;
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\pgsql\Merge.
*/
namespace Drupal\Core\Database\Driver\pgsql;
use Drupal\Core\Database\Query\Merge as QueryMerge;
class Merge extends QueryMerge { }

View file

@ -0,0 +1,820 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\pgsql\Schema.
*/
namespace Drupal\Core\Database\Driver\pgsql;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Database\SchemaObjectExistsException;
use Drupal\Core\Database\SchemaObjectDoesNotExistException;
use Drupal\Core\Database\Schema as DatabaseSchema;
/**
* @addtogroup schemaapi
* @{
*/
class Schema extends DatabaseSchema {
/**
* A cache of information about blob columns and sequences of tables.
*
* This is collected by DatabaseConnection_pgsql->queryTableInformation(),
* by introspecting the database.
*
* @see DatabaseConnection_pgsql->queryTableInformation()
* @var array
*/
protected $tableInformation = array();
/**
* The maximum allowed length for index, primary key and constraint names.
*
* Value will usually be set to a 63 chars limit but PostgreSQL allows
* to higher this value before compiling, so we need to check for that.
*
* @var integer
*/
protected $maxIdentifierLength;
/**
* Make sure to limit identifiers according to PostgreSQL compiled in length.
*
* PostgreSQL allows in standard configuration no longer identifiers than 63
* chars for table/relation names, indexes, primary keys, and constraints. So
* we map all identifiers that are too long to drupal_base64hash_tag, where
* tag is one of:
* - idx for indexes
* - key for constraints
* - pkey for primary keys
*
* @param $identifiers
* The arguments to build the identifier string
* @return
* The index/constraint/pkey identifier
*/
protected function ensureIdentifiersLength($identifier) {
$args = func_get_args();
$info = $this->getPrefixInfo($identifier);
$args[0] = $info['table'];
$identifierName = implode('__', $args);
// Retrieve the max identifier length which is usually 63 characters
// but can be altered before PostgreSQL is compiled so we need to check.
$this->maxIdentifierLength = $this->connection->query("SHOW max_identifier_length")->fetchField();
if (strlen($identifierName) > $this->maxIdentifierLength) {
$saveIdentifier = '"drupal_' . $this->hashBase64($identifierName) . '_' . $args[2] . '"';
}
else {
$saveIdentifier = $identifierName;
}
return $saveIdentifier;
}
/**
* Fetch the list of blobs and sequences used on a table.
*
* We introspect the database to collect the information required by insert
* and update queries.
*
* @param $table_name
* The non-prefixed name of the table.
* @return
* An object with two member variables:
* - 'blob_fields' that lists all the blob fields in the table.
* - 'sequences' that lists the sequences used in that table.
*/
public function queryTableInformation($table) {
// Generate a key to reference this table's information on.
$key = $this->connection->prefixTables('{' . $table . '}');
if (strpos($key, '.') === FALSE) {
$key = 'public.' . $key;
}
if (!isset($this->tableInformation[$key])) {
// Split the key into schema and table for querying.
list($schema, $table_name) = explode('.', $key);
$table_information = (object) array(
'blob_fields' => array(),
'sequences' => array(),
);
// Don't use {} around information_schema.columns table.
$this->connection->addSavepoint();
try {
// Check if the table information exists in the PostgreSQL metadata.
$table_information_exists = (bool) $this->connection->query("SELECT 1 FROM pg_class WHERE relname = :table", array(':table' => $table_name))->fetchField();
// If the table information does not yet exist in the PostgreSQL
// metadata, then return the default table information here, so that it
// will not be cached.
if (!$table_information_exists) {
$this->connection->releaseSavepoint();
return $table_information;
}
else {
$result = $this->connection->query("SELECT column_name, data_type, column_default FROM information_schema.columns WHERE table_schema = :schema AND table_name = :table AND (data_type = 'bytea' OR (numeric_precision IS NOT NULL AND column_default LIKE :default))", array(
':schema' => $schema,
':table' => $table_name,
':default' => '%nextval%',
));
}
}
catch (\Exception $e) {
$this->connection->rollbackSavepoint();
throw $e;
}
$this->connection->releaseSavepoint();
foreach ($result as $column) {
if ($column->data_type == 'bytea') {
$table_information->blob_fields[$column->column_name] = TRUE;
}
elseif (preg_match("/nextval\('([^']+)'/", $column->column_default, $matches)) {
// We must know of any sequences in the table structure to help us
// return the last insert id. If there is more than 1 sequences the
// first one (index 0 of the sequences array) will be used.
$table_information->sequences[] = $matches[1];
$table_information->serial_fields[] = $column->column_name;
}
}
$this->tableInformation[$key] = $table_information;
}
return $this->tableInformation[$key];
}
/**
* Resets information about table blobs, sequences and serial fields.
*
* @param $table
* The non-prefixed name of the table.
*/
protected function resetTableInformation($table) {
$key = $this->connection->prefixTables('{' . $table . '}');
if (strpos($key, '.') === FALSE) {
$key = 'public.' . $key;
}
unset($this->tableInformation[$key]);
}
/**
* Fetch the list of CHECK constraints used on a field.
*
* We introspect the database to collect the information required by field
* alteration.
*
* @param $table
* The non-prefixed name of the table.
* @param $field
* The name of the field.
* @return
* An array of all the checks for the field.
*/
public function queryFieldInformation($table, $field) {
$prefixInfo = $this->getPrefixInfo($table, TRUE);
// Split the key into schema and table for querying.
$schema = $prefixInfo['schema'];
$table_name = $prefixInfo['table'];
$this->connection->addSavepoint();
try {
$checks = $this->connection->query("SELECT conname FROM pg_class cl INNER JOIN pg_constraint co ON co.conrelid = cl.oid INNER JOIN pg_attribute attr ON attr.attrelid = cl.oid AND attr.attnum = ANY (co.conkey) INNER JOIN pg_namespace ns ON cl.relnamespace = ns.oid WHERE co.contype = 'c' AND ns.nspname = :schema AND cl.relname = :table AND attr.attname = :column", array(
':schema' => $schema,
':table' => $table_name,
':column' => $field,
));
}
catch (\Exception $e) {
$this->connection->rollbackSavepoint();
throw $e;
}
$this->connection->releaseSavepoint();
$field_information = $checks->fetchCol();
return $field_information;
}
/**
* Generate SQL to create a new table from a Drupal schema definition.
*
* @param $name
* The name of the table to create.
* @param $table
* A Schema API table definition array.
* @return
* An array of SQL statements to create the table.
*/
protected function createTableSql($name, $table) {
$sql_fields = array();
foreach ($table['fields'] as $field_name => $field) {
$sql_fields[] = $this->createFieldSql($field_name, $this->processField($field));
}
$sql_keys = array();
if (isset($table['primary key']) && is_array($table['primary key'])) {
$sql_keys[] = 'CONSTRAINT ' . $this->ensureIdentifiersLength($name, '', 'pkey') . ' PRIMARY KEY (' . $this->createPrimaryKeySql($table['primary key']) . ')';
}
if (isset($table['unique keys']) && is_array($table['unique keys'])) {
foreach ($table['unique keys'] as $key_name => $key) {
$sql_keys[] = 'CONSTRAINT ' . $this->ensureIdentifiersLength($name, $key_name, 'key') . ' UNIQUE (' . implode(', ', $key) . ')';
}
}
$sql = "CREATE TABLE {" . $name . "} (\n\t";
$sql .= implode(",\n\t", $sql_fields);
if (count($sql_keys) > 0) {
$sql .= ",\n\t";
}
$sql .= implode(",\n\t", $sql_keys);
$sql .= "\n)";
$statements[] = $sql;
if (isset($table['indexes']) && is_array($table['indexes'])) {
foreach ($table['indexes'] as $key_name => $key) {
$statements[] = $this->_createIndexSql($name, $key_name, $key);
}
}
// Add table comment.
if (!empty($table['description'])) {
$statements[] = 'COMMENT ON TABLE {' . $name . '} IS ' . $this->prepareComment($table['description']);
}
// Add column comments.
foreach ($table['fields'] as $field_name => $field) {
if (!empty($field['description'])) {
$statements[] = 'COMMENT ON COLUMN {' . $name . '}.' . $field_name . ' IS ' . $this->prepareComment($field['description']);
}
}
return $statements;
}
/**
* Create an SQL string for a field to be used in table creation or
* alteration.
*
* Before passing a field out of a schema definition into this
* function it has to be processed by _db_process_field().
*
* @param $name
* Name of the field.
* @param $spec
* The field specification, as per the schema data structure format.
*/
protected function createFieldSql($name, $spec) {
// The PostgreSQL server converts names into lowercase, unless quoted.
$sql = '"' . $name . '" ' . $spec['pgsql_type'];
if (isset($spec['type']) && $spec['type'] == 'serial') {
unset($spec['not null']);
}
if (in_array($spec['pgsql_type'], array('varchar', 'character')) && isset($spec['length'])) {
$sql .= '(' . $spec['length'] . ')';
}
elseif (isset($spec['precision']) && isset($spec['scale'])) {
$sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')';
}
if (!empty($spec['unsigned'])) {
$sql .= " CHECK ($name >= 0)";
}
if (isset($spec['not null'])) {
if ($spec['not null']) {
$sql .= ' NOT NULL';
}
else {
$sql .= ' NULL';
}
}
if (array_key_exists('default', $spec)) {
$default = $this->escapeDefaultValue($spec['default']);
$sql .= " default $default";
}
return $sql;
}
/**
* Set database-engine specific properties for a field.
*
* @param $field
* A field description array, as specified in the schema documentation.
*/
protected function processField($field) {
if (!isset($field['size'])) {
$field['size'] = 'normal';
}
// Set the correct database-engine specific datatype.
// In case one is already provided, force it to lowercase.
if (isset($field['pgsql_type'])) {
$field['pgsql_type'] = Unicode::strtolower($field['pgsql_type']);
}
else {
$map = $this->getFieldTypeMap();
$field['pgsql_type'] = $map[$field['type'] . ':' . $field['size']];
}
if (!empty($field['unsigned'])) {
// Unsigned datatypes are not supported in PostgreSQL 8.3. In MySQL,
// they are used to ensure a positive number is inserted and it also
// doubles the maximum integer size that can be stored in a field.
// The PostgreSQL schema in Drupal creates a check constraint
// to ensure that a value inserted is >= 0. To provide the extra
// integer capacity, here, we bump up the column field size.
if (!isset($map)) {
$map = $this->getFieldTypeMap();
}
switch ($field['pgsql_type']) {
case 'smallint':
$field['pgsql_type'] = $map['int:medium'];
break;
case 'int' :
$field['pgsql_type'] = $map['int:big'];
break;
}
}
if (isset($field['type']) && $field['type'] == 'serial') {
unset($field['not null']);
}
return $field;
}
/**
* This maps a generic data type in combination with its data size
* to the engine-specific data type.
*/
function getFieldTypeMap() {
// Put :normal last so it gets preserved by array_flip. This makes
// it much easier for modules (such as schema.module) to map
// database types back into schema types.
// $map does not use drupal_static as its value never changes.
static $map = array(
'varchar_ascii:normal' => 'varchar',
'varchar:normal' => 'varchar',
'char:normal' => 'character',
'text:tiny' => 'text',
'text:small' => 'text',
'text:medium' => 'text',
'text:big' => 'text',
'text:normal' => 'text',
'int:tiny' => 'smallint',
'int:small' => 'smallint',
'int:medium' => 'int',
'int:big' => 'bigint',
'int:normal' => 'int',
'float:tiny' => 'real',
'float:small' => 'real',
'float:medium' => 'real',
'float:big' => 'double precision',
'float:normal' => 'real',
'numeric:normal' => 'numeric',
'blob:big' => 'bytea',
'blob:normal' => 'bytea',
'serial:tiny' => 'serial',
'serial:small' => 'serial',
'serial:medium' => 'serial',
'serial:big' => 'bigserial',
'serial:normal' => 'serial',
);
return $map;
}
protected function _createKeySql($fields) {
$return = array();
foreach ($fields as $field) {
if (is_array($field)) {
$return[] = 'substr(' . $field[0] . ', 1, ' . $field[1] . ')';
}
else {
$return[] = '"' . $field . '"';
}
}
return implode(', ', $return);
}
/**
* Create the SQL expression for primary keys.
*
* Postgresql does not support key length. It does support fillfactor, but
* that requires a separate database lookup for each column in the key. The
* key length defined in the schema is ignored.
*/
protected function createPrimaryKeySql($fields) {
$return = array();
foreach ($fields as $field) {
if (is_array($field)) {
$return[] = '"' . $field[0] . '"';
}
else {
$return[] = '"' . $field . '"';
}
}
return implode(', ', $return);
}
/**
* {@inheritdoc}
*/
public function tableExists($table) {
$prefixInfo = $this->getPrefixInfo($table, TRUE);
return (bool) $this->connection->query("SELECT 1 FROM pg_tables WHERE schemaname = :schema AND tablename = :table", array(':schema' => $prefixInfo['schema'], ':table' => $prefixInfo['table']))->fetchField();
}
function renameTable($table, $new_name) {
if (!$this->tableExists($table)) {
throw new SchemaObjectDoesNotExistException(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 SchemaObjectExistsException(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.
$old_full_name = $this->connection->prefixTables('{' . $table . '}');
list($old_schema, $old_table_name) = strpos($old_full_name, '.') ? explode('.', $old_full_name) : array('public', $old_full_name);
// Index names and constraint names are global in PostgreSQL, so we need to
// 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) {
// Get the index type by suffix, e.g. idx/key/pkey
$index_type = substr($index->indexname, strrpos($index->indexname, '_') + 1);
// If the index is already rewritten by ensureIdentifiersLength() to not
// exceed the 63 chars limit of PostgreSQL, we need to take care of that.
// Example (drupal_Gk7Su_T1jcBHVuvSPeP22_I3Ni4GrVEgTYlIYnBJkro_idx).
if (strpos($index->indexname, 'drupal_') !== FALSE) {
preg_match('/^drupal_(.*)_' . preg_quote($index_type) . '/', $index->indexname, $matches);
$index_name = $matches[1];
}
else {
// Make sure to remove the suffix from index names, because
// $this->ensureIdentifiersLength() will add the suffix again and thus
// would result in a wrong index name.
preg_match('/^' . preg_quote($old_full_name) . '__(.*)__' . preg_quote($index_type) . '/', $index->indexname, $matches);
$index_name = $matches[1];
}
$this->connection->query('ALTER INDEX "' . $index->indexname . '" RENAME TO ' . $this->ensureIdentifiersLength($new_name, $index_name, $index_type) . '');
}
// Ensure the new table name does not include schema syntax.
$prefixInfo = $this->getPrefixInfo($new_name);
// Rename sequences if there's a serial fields.
$info = $this->queryTableInformation($table);
if (!empty($info->serial_fields)) {
foreach ($info->serial_fields as $field) {
$old_sequence = $this->prefixNonTable($table, $field, 'seq');
$new_sequence = $this->prefixNonTable($new_name, $field, 'seq');
$this->connection->query('ALTER SEQUENCE ' . $old_sequence . ' RENAME TO ' . $new_sequence);
}
}
// Now rename the table.
$this->connection->query('ALTER TABLE {' . $table . '} RENAME TO ' . $prefixInfo['table']);
$this->resetTableInformation($table);
}
public function dropTable($table) {
if (!$this->tableExists($table)) {
return FALSE;
}
$this->connection->query('DROP TABLE {' . $table . '}');
$this->resetTableInformation($table);
return TRUE;
}
public function addField($table, $field, $spec, $new_keys = array()) {
if (!$this->tableExists($table)) {
throw new SchemaObjectDoesNotExistException(t("Cannot add field @table.@field: table doesn't exist.", array('@field' => $field, '@table' => $table)));
}
if ($this->fieldExists($table, $field)) {
throw new SchemaObjectExistsException(t("Cannot add field @table.@field: field already exists.", array('@field' => $field, '@table' => $table)));
}
$fixnull = FALSE;
if (!empty($spec['not null']) && !isset($spec['default'])) {
$fixnull = TRUE;
$spec['not null'] = FALSE;
}
$query = 'ALTER TABLE {' . $table . '} ADD COLUMN ';
$query .= $this->createFieldSql($field, $this->processField($spec));
$this->connection->query($query);
if (isset($spec['initial'])) {
$this->connection->update($table)
->fields(array($field => $spec['initial']))
->execute();
}
if ($fixnull) {
$this->connection->query("ALTER TABLE {" . $table . "} ALTER $field SET NOT NULL");
}
if (isset($new_keys)) {
$this->_createKeys($table, $new_keys);
}
// Add column comment.
if (!empty($spec['description'])) {
$this->connection->query('COMMENT ON COLUMN {' . $table . '}.' . $field . ' IS ' . $this->prepareComment($spec['description']));
}
$this->resetTableInformation($table);
}
public function dropField($table, $field) {
if (!$this->fieldExists($table, $field)) {
return FALSE;
}
$this->connection->query('ALTER TABLE {' . $table . '} DROP COLUMN "' . $field . '"');
$this->resetTableInformation($table);
return TRUE;
}
public function fieldSetDefault($table, $field, $default) {
if (!$this->fieldExists($table, $field)) {
throw new SchemaObjectDoesNotExistException(t("Cannot set default value of field @table.@field: field doesn't exist.", array('@table' => $table, '@field' => $field)));
}
$default = $this->escapeDefaultValue($default);
$this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN "' . $field . '" SET DEFAULT ' . $default);
}
public function fieldSetNoDefault($table, $field) {
if (!$this->fieldExists($table, $field)) {
throw new SchemaObjectDoesNotExistException(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');
}
public function indexExists($table, $name) {
// Details http://www.postgresql.org/docs/8.3/interactive/view-pg-indexes.html
$index_name = $this->ensureIdentifiersLength($table, $name, 'idx');
// Remove leading and trailing quotes because the index name is in a WHERE
// clause and not used as an identifier.
$index_name = str_replace('"', '', $index_name);
return (bool) $this->connection->query("SELECT 1 FROM pg_indexes WHERE indexname = '$index_name'")->fetchField();
}
/**
* Helper function: check if a constraint (PK, FK, UK) exists.
*
* @param $table
* The name of the table.
* @param $name
* The name of the constraint (typically 'pkey' or '[constraint]_key').
*/
public function constraintExists($table, $name) {
$constraint_name = $this->ensureIdentifiersLength($table, $name);
// Remove leading and trailing quotes because the index name is in a WHERE
// clause and not used as an identifier.
$constraint_name = str_replace('"', '', $constraint_name);
return (bool) $this->connection->query("SELECT 1 FROM pg_constraint WHERE conname = '$constraint_name'")->fetchField();
}
public function addPrimaryKey($table, $fields) {
if (!$this->tableExists($table)) {
throw new SchemaObjectDoesNotExistException(t("Cannot add primary key to table @table: table doesn't exist.", array('@table' => $table)));
}
if ($this->constraintExists($table, 'pkey')) {
throw new SchemaObjectExistsException(t("Cannot add primary key to table @table: primary key already exists.", array('@table' => $table)));
}
$this->connection->query('ALTER TABLE {' . $table . '} ADD CONSTRAINT ' . $this->ensureIdentifiersLength($table, '', 'pkey') . ' PRIMARY KEY (' . $this->createPrimaryKeySql($fields) . ')');
$this->resetTableInformation($table);
}
public function dropPrimaryKey($table) {
if (!$this->constraintExists($table, 'pkey')) {
return FALSE;
}
$this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT ' . $this->ensureIdentifiersLength($table, '', 'pkey'));
$this->resetTableInformation($table);
return TRUE;
}
function addUniqueKey($table, $name, $fields) {
if (!$this->tableExists($table)) {
throw new SchemaObjectDoesNotExistException(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 SchemaObjectExistsException(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->ensureIdentifiersLength($table, $name, 'key') . ' UNIQUE (' . implode(',', $fields) . ')');
$this->resetTableInformation($table);
}
public function dropUniqueKey($table, $name) {
if (!$this->constraintExists($table, $name . '__key')) {
return FALSE;
}
$this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT ' . $this->ensureIdentifiersLength($table, $name, 'key'));
$this->resetTableInformation($table);
return TRUE;
}
public function addIndex($table, $name, $fields) {
if (!$this->tableExists($table)) {
throw new SchemaObjectDoesNotExistException(t("Cannot add index @name to table @table: table doesn't exist.", array('@table' => $table, '@name' => $name)));
}
if ($this->indexExists($table, $name)) {
throw new SchemaObjectExistsException(t("Cannot add index @name to table @table: index already exists.", array('@table' => $table, '@name' => $name)));
}
$this->connection->query($this->_createIndexSql($table, $name, $fields));
$this->resetTableInformation($table);
}
public function dropIndex($table, $name) {
if (!$this->indexExists($table, $name)) {
return FALSE;
}
$this->connection->query('DROP INDEX ' . $this->ensureIdentifiersLength($table, $name, 'idx'));
$this->resetTableInformation($table);
return TRUE;
}
public function changeField($table, $field, $field_new, $spec, $new_keys = array()) {
if (!$this->fieldExists($table, $field)) {
throw new SchemaObjectDoesNotExistException(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 SchemaObjectExistsException(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);
// Type 'serial' is known to PostgreSQL, but only during table creation,
// not when altering. Because of that, we create it here as an 'int'. After
// we create it we manually re-apply the sequence.
if (in_array($spec['pgsql_type'], array('serial', 'bigserial'))) {
$field_def = 'int';
}
else {
$field_def = $spec['pgsql_type'];
}
if (in_array($spec['pgsql_type'], array('varchar', 'character', 'text')) && isset($spec['length'])) {
$field_def .= '(' . $spec['length'] . ')';
}
elseif (isset($spec['precision']) && isset($spec['scale'])) {
$field_def .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')';
}
// Remove old check constraints.
$field_info = $this->queryFieldInformation($table, $field);
foreach ($field_info as $check) {
$this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT "' . $check . '"');
}
// Remove old default.
$this->fieldSetNoDefault($table, $field);
// Convert field type.
// Usually, we do this via a simple typecast 'USING fieldname::type'. But
// the typecast does not work for conversions to bytea.
// @see http://www.postgresql.org/docs/current/static/datatype-binary.html
if ($spec['pgsql_type'] != 'bytea') {
$this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $field_def . ' USING "' . $field . '"::' . $field_def);
}
else {
// Do not attempt to convert a field that is bytea already.
$table_information = $this->queryTableInformation($table);
if (!in_array($field, $table_information->blob_fields)) {
// Convert to a bytea type by using the SQL replace() function to
// convert any single backslashes in the field content to double
// backslashes ('\' to '\\').
$this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $field_def . ' USING decode(replace("' . $field . '"' . ", '\\', '\\\\'), 'escape');");
}
}
if (isset($spec['not null'])) {
if ($spec['not null']) {
$nullaction = 'SET NOT NULL';
}
else {
$nullaction = 'DROP NOT NULL';
}
$this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" ' . $nullaction);
}
if (in_array($spec['pgsql_type'], array('serial', 'bigserial'))) {
// Type "serial" is known to PostgreSQL, but *only* during table creation,
// not when altering. Because of that, the sequence needs to be created
// and initialized by hand.
$seq = "{" . $table . "}_" . $field_new . "_seq";
$this->connection->query("CREATE SEQUENCE " . $seq);
// Set sequence to maximal field value to not conflict with existing
// entries.
$this->connection->query("SELECT setval('" . $seq . "', MAX(\"" . $field . '")) FROM {' . $table . "}");
$this->connection->query('ALTER TABLE {' . $table . '} ALTER ' . $field . ' SET DEFAULT nextval(' . $this->connection->quote($seq) . ')');
}
// Rename the column if necessary.
if ($field != $field_new) {
$this->connection->query('ALTER TABLE {' . $table . '} RENAME "' . $field . '" TO "' . $field_new . '"');
}
// Add unsigned check if necessary.
if (!empty($spec['unsigned'])) {
$this->connection->query('ALTER TABLE {' . $table . '} ADD CHECK ("' . $field_new . '" >= 0)');
}
// Add default if necessary.
if (isset($spec['default'])) {
$this->fieldSetDefault($table, $field_new, $spec['default']);
}
// Change description if necessary.
if (!empty($spec['description'])) {
$this->connection->query('COMMENT ON COLUMN {' . $table . '}."' . $field_new . '" IS ' . $this->prepareComment($spec['description']));
}
if (isset($new_keys)) {
$this->_createKeys($table, $new_keys);
}
$this->resetTableInformation($table);
}
protected function _createIndexSql($table, $name, $fields) {
$query = 'CREATE INDEX ' . $this->ensureIdentifiersLength($table, $name, 'idx') . ' ON {' . $table . '} (';
$query .= $this->_createKeySql($fields) . ')';
return $query;
}
protected function _createKeys($table, $new_keys) {
if (isset($new_keys['primary key'])) {
$this->addPrimaryKey($table, $new_keys['primary key']);
}
if (isset($new_keys['unique keys'])) {
foreach ($new_keys['unique keys'] as $name => $fields) {
$this->addUniqueKey($table, $name, $fields);
}
}
if (isset($new_keys['indexes'])) {
foreach ($new_keys['indexes'] as $name => $fields) {
$this->addIndex($table, $name, $fields);
}
}
}
/**
* Retrieve a table or column comment.
*/
public function getComment($table, $column = NULL) {
$info = $this->getPrefixInfo($table);
// Don't use {} around pg_class, pg_attribute tables.
if (isset($column)) {
return $this->connection->query('SELECT col_description(oid, attnum) FROM pg_class, pg_attribute WHERE attrelid = oid AND relname = ? AND attname = ?', array($info['table'], $column))->fetchField();
}
else {
return $this->connection->query('SELECT obj_description(oid, ?) FROM pg_class WHERE relname = ?', array('pg_class', $info['table']))->fetchField();
}
}
/**
* Calculates a base-64 encoded, PostgreSQL-safe sha-256 hash per PostgreSQL
* documentation: 4.1. Lexical Structure.
*
* @param $data
* String to be hashed.
* @return string
* A base-64 encoded sha-256 hash, with + and / replaced with _ and any =
* padding characters removed.
*/
protected function hashBase64($data) {
$hash = base64_encode(hash('sha256', $data, TRUE));
// Modify the hash so it's safe to use in PostgreSQL identifiers.
return strtr($hash, array('+' => '_', '/' => '_', '=' => ''));
}
}
/**
* @} End of "addtogroup schemaapi".
*/

View file

@ -0,0 +1,161 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\pgsql\Select.
*/
namespace Drupal\Core\Database\Driver\pgsql;
use Drupal\Core\Database\Query\Select as QuerySelect;
/**
* @addtogroup database
* @{
*/
class Select extends QuerySelect {
public function orderRandom() {
$alias = $this->addExpression('RANDOM()', 'random_field');
$this->orderBy($alias);
return $this;
}
/**
* Overrides SelectQuery::orderBy().
*
* PostgreSQL adheres strictly to the SQL-92 standard and requires that when
* using DISTINCT or GROUP BY conditions, fields and expressions that are
* ordered on also need to be selected. This is a best effort implementation
* to handle the cases that can be automated by adding the field if it is not
* yet selected.
*
* @code
* $query = db_select('example', 'e');
* $query->join('example_revision', 'er', 'e.vid = er.vid');
* $query
* ->distinct()
* ->fields('e')
* ->orderBy('timestamp');
* @endcode
*
* In this query, it is not possible (without relying on the schema) to know
* whether timestamp belongs to example_revision and needs to be added or
* belongs to node and is already selected. Queries like this will need to be
* corrected in the original query by adding an explicit call to
* SelectQuery::addField() or SelectQuery::fields().
*
* Since this has a small performance impact, both by the additional
* processing in this function and in the database that needs to return the
* additional fields, this is done as an override instead of implementing it
* directly in SelectQuery::orderBy().
*/
public function orderBy($field, $direction = 'ASC') {
// Only allow ASC and DESC, default to ASC.
// Emulate MySQL default behavior to sort NULL values first for ascending,
// and last for descending.
// @see http://www.postgresql.org/docs/9.3/static/queries-order.html
$direction = strtoupper($direction) == 'DESC' ? 'DESC NULLS LAST' : 'ASC NULLS FIRST';
$this->order[$field] = $direction;
if ($this->hasTag('entity_query')) {
return $this;
}
// If there is a table alias specified, split it up.
if (strpos($field, '.') !== FALSE) {
list($table, $table_field) = explode('.', $field);
}
// Figure out if the field has already been added.
foreach ($this->fields as $existing_field) {
if (!empty($table)) {
// If table alias is given, check if field and table exists.
if ($existing_field['table'] == $table && $existing_field['field'] == $table_field) {
return $this;
}
}
else {
// If there is no table, simply check if the field exists as a field or
// an aliased field.
if ($existing_field['alias'] == $field) {
return $this;
}
}
}
// Also check expression aliases.
foreach ($this->expressions as $expression) {
if ($expression['alias'] == $this->connection->escapeAlias($field)) {
return $this;
}
}
// If a table loads all fields, it can not be added again. It would
// result in an ambiguous alias error because that field would be loaded
// twice: Once through table_alias.* and once directly. If the field
// actually belongs to a different table, it must be added manually.
foreach ($this->tables as $table) {
if (!empty($table['all_fields'])) {
return $this;
}
}
// If $field contains an characters which are not allowed in a field name
// it is considered an expression, these can't be handled automatically
// either.
if ($this->connection->escapeField($field) != $field) {
return $this;
}
// This is a case that can be handled automatically, add the field.
$this->addField(NULL, $field);
return $this;
}
/**
* {@inheritdoc}
*/
public function addExpression($expression, $alias = NULL, $arguments = array()) {
if (empty($alias)) {
$alias = 'expression';
}
// This implements counting in the same manner as the parent method.
$alias_candidate = $alias;
$count = 2;
while (!empty($this->expressions[$alias_candidate])) {
$alias_candidate = $alias . '_' . $count++;
}
$alias = $alias_candidate;
$this->expressions[$alias] = array(
'expression' => $expression,
'alias' => $this->connection->escapeAlias($alias_candidate),
'arguments' => $arguments,
);
return $alias;
}
/**
* {@inheritdoc}
*/
public function execute() {
$this->connection->addSavepoint();
try {
$result = parent::execute();
}
catch (\Exception $e) {
$this->connection->rollbackSavepoint();
throw $e;
}
$this->connection->releaseSavepoint();
return $result;
}
}
/**
* @} End of "addtogroup database".
*/

View file

@ -0,0 +1,12 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\pgsql\Transaction.
*/
namespace Drupal\Core\Database\Driver\pgsql;
use Drupal\Core\Database\Transaction as DatabaseTransaction;
class Transaction extends DatabaseTransaction { }

View file

@ -0,0 +1,30 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\pgsql\Truncate.
*/
namespace Drupal\Core\Database\Driver\pgsql;
use Drupal\Core\Database\Query\Truncate as QueryTruncate;
class Truncate extends QueryTruncate {
/**
* {@inheritdoc}
*/
public function execute() {
$this->connection->addSavepoint();
try {
$result = parent::execute();
}
catch (\Exception $e) {
$this->connection->rollbackSavepoint();
throw $e;
}
$this->connection->releaseSavepoint();
return $result;
}
}

View file

@ -0,0 +1,90 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\pgsql\Update.
*/
namespace Drupal\Core\Database\Driver\pgsql;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Query\Update as QueryUpdate;
use Drupal\Core\Database\Query\SelectInterface;
class Update extends QueryUpdate {
public function execute() {
$max_placeholder = 0;
$blobs = array();
$blob_count = 0;
// Because we filter $fields the same way here and in __toString(), the
// placeholders will all match up properly.
$stmt = $this->connection->prepareQuery((string) $this);
// Fetch the list of blobs and sequences used on that table.
$table_information = $this->connection->schema()->queryTableInformation($this->table);
// Expressions take priority over literal fields, so we process those first
// and remove any literal fields that conflict.
$fields = $this->fields;
foreach ($this->expressionFields as $field => $data) {
if (!empty($data['arguments'])) {
foreach ($data['arguments'] as $placeholder => $argument) {
// We assume that an expression will never happen on a BLOB field,
// which is a fairly safe assumption to make since in most cases
// it would be an invalid query anyway.
$stmt->bindParam($placeholder, $data['arguments'][$placeholder]);
}
}
if ($data['expression'] instanceof SelectInterface) {
$data['expression']->compile($this->connection, $this);
$select_query_arguments = $data['expression']->arguments();
foreach ($select_query_arguments as $placeholder => $argument) {
$stmt->bindParam($placeholder, $select_query_arguments[$placeholder]);
}
}
unset($fields[$field]);
}
foreach ($fields as $field => $value) {
$placeholder = ':db_update_placeholder_' . ($max_placeholder++);
if (isset($table_information->blob_fields[$field])) {
$blobs[$blob_count] = fopen('php://memory', 'a');
fwrite($blobs[$blob_count], $value);
rewind($blobs[$blob_count]);
$stmt->bindParam($placeholder, $blobs[$blob_count], \PDO::PARAM_LOB);
++$blob_count;
}
else {
$stmt->bindParam($placeholder, $fields[$field]);
}
}
if (count($this->condition)) {
$this->condition->compile($this->connection, $this);
$arguments = $this->condition->arguments();
foreach ($arguments as $placeholder => $value) {
$stmt->bindParam($placeholder, $arguments[$placeholder]);
}
}
$options = $this->queryOptions;
$options['already_prepared'] = TRUE;
$options['return'] = Database::RETURN_AFFECTED;
$this->connection->addSavepoint();
try {
$result = $this->connection->query($stmt, array(), $options);
$this->connection->releaseSavepoint();
return $result;
}
catch (\Exception $e) {
$this->connection->rollbackSavepoint();
throw $e;
}
}
}

View file

@ -0,0 +1,406 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\sqlite\Connection.
*/
namespace Drupal\Core\Database\Driver\sqlite;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\DatabaseNotFoundException;
use Drupal\Core\Database\Connection as DatabaseConnection;
/**
* Specific SQLite implementation of DatabaseConnection.
*/
class Connection extends DatabaseConnection {
/**
* Error code for "Unable to open database file" error.
*/
const DATABASE_NOT_FOUND = 14;
/**
* Whether or not the active transaction (if any) will be rolled back.
*
* @var bool
*/
protected $willRollback;
/**
* 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();
/**
* 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 Schema needs to
* access it. However, it should not be manually set.
*
* @var bool
*/
var $tableDropped = FALSE;
/**
* Constructs a \Drupal\Core\Database\Driver\sqlite\Connection object.
*/
public function __construct(\PDO $connection, array $connection_options) {
// We don't need a specific PDOStatement class here, we simulate it in
// static::prepare().
$this->statementClass = NULL;
parent::__construct($connection, $connection_options);
// This driver defaults to transaction support, except if explicitly passed FALSE.
$this->transactionSupport = $this->transactionalDDLSupport = !isset($connection_options['transactions']) || $connection_options['transactions'] !== FALSE;
$this->connectionOptions = $connection_options;
// Attach one database for each registered prefix.
$prefixes = $this->prefixes;
foreach ($prefixes as &$prefix) {
// Empty prefix means query the main database -- no need to attach anything.
if (!empty($prefix)) {
// Only attach the database once.
if (!isset($this->attachedDatabases[$prefix])) {
$this->attachedDatabases[$prefix] = $prefix;
if ($connection_options['database'] === ':memory:') {
// In memory database use ':memory:' as database name. According to
// http://www.sqlite.org/inmemorydb.html it will open a unique
// database so attaching it twice is not a problem.
$this->query('ATTACH DATABASE :database AS :prefix', array(':database' => $connection_options['database'], ':prefix' => $prefix));
}
else {
$this->query('ATTACH DATABASE :database AS :prefix', array(':database' => $connection_options['database'] . '-' . $prefix, ':prefix' => $prefix));
}
}
// Add a ., so queries become prefix.table, which is proper syntax for
// querying an attached database.
$prefix .= '.';
}
}
// Regenerate the prefixes replacement table.
$this->setPrefix($prefixes);
}
/**
* {@inheritdoc}
*/
public static function open(array &$connection_options = array()) {
// Allow PDO options to be overridden.
$connection_options += array(
'pdo' => array(),
);
$connection_options['pdo'] += array(
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
// Convert numeric values to strings when fetching.
\PDO::ATTR_STRINGIFY_FETCHES => TRUE,
);
$pdo = new \PDO('sqlite:' . $connection_options['database'], '', '', $connection_options['pdo']);
// Create functions needed by SQLite.
$pdo->sqliteCreateFunction('if', array(__CLASS__, 'sqlFunctionIf'));
$pdo->sqliteCreateFunction('greatest', array(__CLASS__, 'sqlFunctionGreatest'));
$pdo->sqliteCreateFunction('pow', 'pow', 2);
$pdo->sqliteCreateFunction('exp', 'exp', 1);
$pdo->sqliteCreateFunction('length', 'strlen', 1);
$pdo->sqliteCreateFunction('md5', 'md5', 1);
$pdo->sqliteCreateFunction('concat', array(__CLASS__, 'sqlFunctionConcat'));
$pdo->sqliteCreateFunction('concat_ws', array(__CLASS__, 'sqlFunctionConcatWs'));
$pdo->sqliteCreateFunction('substring', array(__CLASS__, 'sqlFunctionSubstring'), 3);
$pdo->sqliteCreateFunction('substring_index', array(__CLASS__, 'sqlFunctionSubstringIndex'), 3);
$pdo->sqliteCreateFunction('rand', array(__CLASS__, 'sqlFunctionRand'));
$pdo->sqliteCreateFunction('regexp', array(__CLASS__, 'sqlFunctionRegexp'));
// SQLite does not support the LIKE BINARY operator, so we overload the
// non-standard GLOB operator for case-sensitive matching. Another option
// would have been to override another non-standard operator, MATCH, but
// that does not support the NOT keyword prefix.
$pdo->sqliteCreateFunction('glob', array(__CLASS__, 'sqlFunctionLikeBinary'));
// Create a user-space case-insensitive collation with UTF-8 support.
$pdo->sqliteCreateCollation('NOCASE_UTF8', array('Drupal\Component\Utility\Unicode', 'strcasecmp'));
// Execute sqlite init_commands.
if (isset($connection_options['init_commands'])) {
$pdo->exec(implode('; ', $connection_options['init_commands']));
}
return $pdo;
}
/**
* Destructor for the SQLite connection.
*
* We prune empty databases on destruct, but only if tables have been
* dropped. This is especially needed when running the test suite, which
* creates and destroy databases several times in a row.
*/
public function __destruct() {
if ($this->tableDropped && !empty($this->attachedDatabases)) {
foreach ($this->attachedDatabases as $prefix) {
// Check if the database is now empty, ignore the internal SQLite tables.
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 doesn't have any tables.
if ($count == 0) {
// Detach the database.
$this->query('DETACH DATABASE :schema', array(':schema' => $prefix));
// Destroy the database file.
unlink($this->connectionOptions['database'] . '-' . $prefix);
}
}
catch (\Exception $e) {
// Ignore the exception and continue. There is nothing we can do here
// to report the error or fail safe.
}
}
}
}
/**
* SQLite compatibility implementation for the IF() SQL function.
*/
public static function sqlFunctionIf($condition, $expr1, $expr2 = NULL) {
return $condition ? $expr1 : $expr2;
}
/**
* SQLite compatibility implementation for the GREATEST() SQL function.
*/
public static function sqlFunctionGreatest() {
$args = func_get_args();
foreach ($args as $v) {
if (!isset($v)) {
unset($args);
}
}
if (count($args)) {
return max($args);
}
else {
return NULL;
}
}
/**
* SQLite compatibility implementation for the CONCAT() SQL function.
*/
public static function sqlFunctionConcat() {
$args = func_get_args();
return implode('', $args);
}
/**
* SQLite compatibility implementation for the CONCAT_WS() SQL function.
*
* @see http://dev.mysql.com/doc/refman/5.6/en/string-functions.html#function_concat-ws
*/
public static function sqlFunctionConcatWs() {
$args = func_get_args();
$separator = array_shift($args);
// If the separator is NULL, the result is NULL.
if ($separator === FALSE || is_null($separator)) {
return NULL;
}
// Skip any NULL values after the separator argument.
$args = array_filter($args, function ($value) {
return !is_null($value);
});
return implode($separator, $args);
}
/**
* SQLite compatibility implementation for the SUBSTRING() SQL function.
*/
public static function sqlFunctionSubstring($string, $from, $length) {
return substr($string, $from - 1, $length);
}
/**
* SQLite compatibility implementation for the SUBSTRING_INDEX() SQL function.
*/
public static function sqlFunctionSubstringIndex($string, $delimiter, $count) {
// If string is empty, simply return an empty string.
if (empty($string)) {
return '';
}
$end = 0;
for ($i = 0; $i < $count; $i++) {
$end = strpos($string, $delimiter, $end + 1);
if ($end === FALSE) {
$end = strlen($string);
}
}
return substr($string, 0, $end);
}
/**
* SQLite compatibility implementation for the RAND() SQL function.
*/
public static function sqlFunctionRand($seed = NULL) {
if (isset($seed)) {
mt_srand($seed);
}
return mt_rand() / mt_getrandmax();
}
/**
* SQLite compatibility implementation for the REGEXP SQL operator.
*
* The REGEXP operator is natively known, but not implemented by default.
*
* @see http://www.sqlite.org/lang_expr.html#regexp
*/
public static function sqlFunctionRegexp($pattern, $subject) {
// preg_quote() cannot be used here, since $pattern may contain reserved
// regular expression characters already (such as ^, $, etc). Therefore,
// use a rare character as PCRE delimiter.
$pattern = '#' . addcslashes($pattern, '#') . '#i';
return preg_match($pattern, $subject);
}
/**
* SQLite compatibility implementation for the LIKE BINARY SQL operator.
*
* SQLite supports case-sensitive LIKE operations through the
* 'case_sensitive_like' PRAGMA statement, but only for ASCII characters, so
* we have to provide our own implementation with UTF-8 support.
*
* @see https://sqlite.org/pragma.html#pragma_case_sensitive_like
* @see https://sqlite.org/lang_expr.html#like
*/
public static function sqlFunctionLikeBinary($pattern, $subject) {
// Replace the SQL LIKE wildcard meta-characters with the equivalent regular
// expression meta-characters and escape the delimiter that will be used for
// matching.
$pattern = str_replace(array('%', '_'), array('.*?', '.'), preg_quote($pattern, '/'));
return preg_match('/^' . $pattern . '$/', $subject);
}
/**
* {@inheritdoc}
*/
public function prepare($statement, array $driver_options = array()) {
return new Statement($this->connection, $this, $statement, $driver_options);
}
/**
* {@inheritdoc}
*/
protected function handleQueryException(\PDOException $e, $query, array $args = array(), $options = array()) {
// The database schema might be changed by another process in between the
// time that the statement was prepared and the time the statement was run
// (e.g. usually happens when running tests). In this case, we need to
// re-run the query.
// @see http://www.sqlite.org/faq.html#q15
// @see http://www.sqlite.org/rescode.html#schema
if (!empty($e->errorInfo[1]) && $e->errorInfo[1] === 17) {
return $this->query($query, $args, $options);
}
parent::handleQueryException($e, $query, $args, $options);
}
public function queryRange($query, $from, $count, array $args = array(), array $options = array()) {
return $this->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options);
}
public function queryTemporary($query, array $args = array(), array $options = array()) {
// Generate a new temporary table name and protect it from prefixing.
// SQLite requires that temporary tables to be non-qualified.
$tablename = $this->generateTemporaryTableName();
$prefixes = $this->prefixes;
$prefixes[$tablename] = '';
$this->setPrefix($prefixes);
$this->query('CREATE TEMPORARY TABLE ' . $tablename . ' AS ' . $query, $args, $options);
return $tablename;
}
public function driver() {
return 'sqlite';
}
public function databaseType() {
return 'sqlite';
}
/**
* Overrides \Drupal\Core\Database\Connection::createDatabase().
*
* @param string $database
* The name of the database to create.
*
* @throws \Drupal\Core\Database\DatabaseNotFoundException
*/
public function createDatabase($database) {
// Verify the database is writable.
$db_directory = new \SplFileInfo(dirname($database));
if (!$db_directory->isDir() && !drupal_mkdir($db_directory->getPathName(), 0755, TRUE)) {
throw new DatabaseNotFoundException('Unable to create database directory ' . $db_directory->getPathName());
}
}
public function mapConditionOperator($operator) {
// We don't want to override any of the defaults.
static $specials = array(
'LIKE' => array('postfix' => " ESCAPE '\\'"),
'NOT LIKE' => array('postfix' => " ESCAPE '\\'"),
'LIKE BINARY' => array('postfix' => " ESCAPE '\\'", 'operator' => 'GLOB'),
'NOT LIKE BINARY' => array('postfix' => " ESCAPE '\\'", 'operator' => 'NOT GLOB'),
);
return isset($specials[$operator]) ? $specials[$operator] : NULL;
}
/**
* {@inheritdoc}
*/
public function prepareQuery($query) {
return $this->prepare($this->prefixTables($query));
}
public function nextId($existing_id = 0) {
$this->startTransaction();
// We can safely use literal queries here instead of the slower query
// builder because if a given database breaks here then it can simply
// override nextId. However, this is unlikely as we deal with short strings
// and integers and no known databases require special handling for those
// simple cases. If another transaction wants to write the same row, it will
// wait until this transaction commits. Also, the return value needs to be
// set to RETURN_AFFECTED as if it were a real update() query otherwise it
// is not possible to get the row count properly.
$affected = $this->query('UPDATE {sequences} SET value = GREATEST(value, :existing_id) + 1', array(
':existing_id' => $existing_id,
), array('return' => Database::RETURN_AFFECTED));
if (!$affected) {
$this->query('INSERT INTO {sequences} (value) VALUES (:existing_id + 1)', array(
':existing_id' => $existing_id,
));
}
// The transaction gets committed when the transaction object gets destroyed
// because it gets out of scope.
return $this->query('SELECT value FROM {sequences}')->fetchField();
}
/**
* {@inheritdoc}
*/
public function getFullQualifiedTableName($table) {
$prefix = $this->tablePrefix($table);
// Don't include the SQLite database file name as part of the table name.
return $prefix . $table;
}
}

View file

@ -0,0 +1,15 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\sqlite\Delete.
*/
namespace Drupal\Core\Database\Driver\sqlite;
use Drupal\Core\Database\Query\Delete as QueryDelete;
/**
* SQLite specific implementation of \Drupal\Core\Database\Query\Delete.
*/
class Delete extends QueryDelete { }

View file

@ -0,0 +1,53 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\sqlite\Insert.
*/
namespace Drupal\Core\Database\Driver\sqlite;
use Drupal\Core\Database\Query\Insert as QueryInsert;
/**
* SQLite specific implementation of InsertQuery.
*
* We ignore all the default fields and use the clever SQLite syntax:
* INSERT INTO table DEFAULT VALUES
* for degenerated "default only" queries.
*/
class Insert extends QueryInsert {
public function execute() {
if (!$this->preExecute()) {
return NULL;
}
if (count($this->insertFields) || !empty($this->fromQuery)) {
return parent::execute();
}
else {
return $this->connection->query('INSERT INTO {' . $this->table . '} DEFAULT VALUES', array(), $this->queryOptions);
}
}
public function __toString() {
// Create a sanitized comment string to prepend to the query.
$comments = $this->connection->makeComment($this->comments);
// Produce as many generic placeholders as necessary.
$placeholders = array();
if (!empty($this->insertFields)) {
$placeholders = array_fill(0, count($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)) {
$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) . ')';
}
}

View file

@ -0,0 +1,111 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\sqlite\Install\Tasks.
*/
namespace Drupal\Core\Database\Driver\sqlite\Install;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Driver\sqlite\Connection;
use Drupal\Core\Database\DatabaseNotFoundException;
use Drupal\Core\Database\Install\Tasks as InstallTasks;
/**
* Specifies installation tasks for SQLite databases.
*/
class Tasks extends InstallTasks {
/**
* {@inheritdoc}
*/
protected $pdoDriver = 'sqlite';
/**
* {@inheritdoc}
*/
public function name() {
return t('SQLite');
}
/**
* {@inheritdoc}
*/
public function minimumVersion() {
return '3.6.8';
}
/**
* {@inheritdoc}
*/
public function getFormOptions(array $database) {
$form = parent::getFormOptions($database);
// Remove the options that only apply to client/server style databases.
unset($form['username'], $form['password'], $form['advanced_options']['host'], $form['advanced_options']['port']);
// Make the text more accurate for SQLite.
$form['database']['#title'] = t('Database file');
$form['database']['#description'] = t('The absolute path to the file where @drupal data will be stored. This must be writable by the web server and should exist outside of the web root.', array('@drupal' => drupal_install_profile_distribution_name()));
$default_database = \Drupal::service('site.path') . '/files/.ht.sqlite';
$form['database']['#default_value'] = empty($database['database']) ? $default_database : $database['database'];
return $form;
}
/**
* {@inheritdoc}
*/
protected function connect() {
try {
// This doesn't actually test the connection.
db_set_active();
// Now actually do a check.
Database::getConnection();
$this->pass('Drupal can CONNECT to the database ok.');
}
catch (\Exception $e) {
// Attempt to create the database if it is not found.
if ($e->getCode() == Connection::DATABASE_NOT_FOUND) {
// Remove the database string from connection info.
$connection_info = Database::getConnectionInfo();
$database = $connection_info['default']['database'];
// We cannot use file_directory_temp() here because we haven't yet
// successfully connected to the database.
$connection_info['default']['database'] = drupal_tempnam(sys_get_temp_dir(), 'sqlite');
// In order to change the Database::$databaseInfo array, need to remove
// the active connection, then re-add it with the new info.
Database::removeConnection('default');
Database::addConnectionInfo('default', 'default', $connection_info['default']);
try {
Database::getConnection()->createDatabase($database);
Database::closeConnection();
// Now, restore the database config.
Database::removeConnection('default');
$connection_info['default']['database'] = $database;
Database::addConnectionInfo('default', 'default', $connection_info['default']);
// Check the database connection.
Database::getConnection();
$this->pass('Drupal can CONNECT to the database ok.');
}
catch (DatabaseNotFoundException $e) {
// Still no dice; probably a permission issue. Raise the error to the
// installer.
$this->fail(t('Database %database not found. The server reports the following message when attempting to create the database: %error.', array('%database' => $database, '%error' => $e->getMessage())));
}
}
else {
// Database connection failed for some other reason than the database
// not existing.
$this->fail(t('Failed to connect to your database server. The server reports the following message: %error.<ul><li>Is the database server running?</li><li>Does the database exist, and have you entered the correct database name?</li><li>Have you entered the correct username and password?</li><li>Have you entered the correct database hostname?</li></ul>', array('%error' => $e->getMessage())));
return FALSE;
}
}
return TRUE;
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\sqlite\Merge.
*/
namespace Drupal\Core\Database\Driver\sqlite;
use Drupal\Core\Database\Query\Merge as QueryMerge;
class Merge extends QueryMerge { }

View file

@ -0,0 +1,708 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\sqlite\Schema.
*/
namespace Drupal\Core\Database\Driver\sqlite;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Database\SchemaObjectExistsException;
use Drupal\Core\Database\SchemaObjectDoesNotExistException;
use Drupal\Core\Database\Schema as DatabaseSchema;
/**
* @ingroup schemaapi
* @{
*/
class Schema extends DatabaseSchema {
/**
* Override DatabaseSchema::$defaultSchema
*/
protected $defaultSchema = 'main';
public function tableExists($table) {
$info = $this->getPrefixInfo($table);
// Don't use {} around sqlite_master table.
return (bool) $this->connection->query('SELECT 1 FROM ' . $info['schema'] . '.sqlite_master WHERE type = :type AND name = :name', array(':type' => 'table', ':name' => $info['table']))->fetchField();
}
public function fieldExists($table, $column) {
$schema = $this->introspectSchema($table);
return !empty($schema['fields'][$column]);
}
/**
* Generate SQL to create a new table from a Drupal schema definition.
*
* @param $name
* The name of the table to create.
* @param $table
* A Schema API table definition array.
* @return
* An array of SQL statements to create the table.
*/
public function createTableSql($name, $table) {
$sql = array();
$sql[] = "CREATE TABLE {" . $name . "} (\n" . $this->createColumnsSql($name, $table) . "\n);\n";
return array_merge($sql, $this->createIndexSql($name, $table));
}
/**
* Build the SQL expression for indexes.
*/
protected function createIndexSql($tablename, $schema) {
$sql = array();
$info = $this->getPrefixInfo($tablename);
if (!empty($schema['unique keys'])) {
foreach ($schema['unique keys'] as $key => $fields) {
$sql[] = 'CREATE UNIQUE INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $key . ' ON ' . $info['table'] . ' (' . $this->createKeySql($fields) . "); \n";
}
}
if (!empty($schema['indexes'])) {
foreach ($schema['indexes'] as $key => $fields) {
$sql[] = 'CREATE INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $key . ' ON ' . $info['table'] . ' (' . $this->createKeySql($fields) . "); \n";
}
}
return $sql;
}
/**
* Build the SQL expression for creating columns.
*/
protected function createColumnsSql($tablename, $schema) {
$sql_array = array();
// Add the SQL statement for each field.
foreach ($schema['fields'] as $name => $field) {
if (isset($field['type']) && $field['type'] == 'serial') {
if (isset($schema['primary key']) && ($key = array_search($name, $schema['primary key'])) !== FALSE) {
unset($schema['primary key'][$key]);
}
}
$sql_array[] = $this->createFieldSql($name, $this->processField($field));
}
// Process keys.
if (!empty($schema['primary key'])) {
$sql_array[] = " PRIMARY KEY (" . $this->createKeySql($schema['primary key']) . ")";
}
return implode(", \n", $sql_array);
}
/**
* Build the SQL expression for keys.
*/
protected function createKeySql($fields) {
$return = array();
foreach ($fields as $field) {
if (is_array($field)) {
$return[] = $field[0];
}
else {
$return[] = $field;
}
}
return implode(', ', $return);
}
/**
* Set database-engine specific properties for a field.
*
* @param $field
* A field description array, as specified in the schema documentation.
*/
protected function processField($field) {
if (!isset($field['size'])) {
$field['size'] = 'normal';
}
// Set the correct database-engine specific datatype.
// In case one is already provided, force it to uppercase.
if (isset($field['sqlite_type'])) {
$field['sqlite_type'] = Unicode::strtoupper($field['sqlite_type']);
}
else {
$map = $this->getFieldTypeMap();
$field['sqlite_type'] = $map[$field['type'] . ':' . $field['size']];
// Numeric fields with a specified scale have to be stored as floats.
if ($field['sqlite_type'] === 'NUMERIC' && isset($field['scale'])) {
$field['sqlite_type'] = 'FLOAT';
}
}
if (isset($field['type']) && $field['type'] == 'serial') {
$field['auto_increment'] = TRUE;
}
return $field;
}
/**
* Create an SQL string for a field to be used in table creation or alteration.
*
* Before passing a field out of a schema definition into this function it has
* to be processed by db_processField().
*
* @param $name
* Name of the field.
* @param $spec
* The field specification, as per the schema data structure format.
*/
protected function createFieldSql($name, $spec) {
if (!empty($spec['auto_increment'])) {
$sql = $name . " INTEGER PRIMARY KEY AUTOINCREMENT";
if (!empty($spec['unsigned'])) {
$sql .= ' CHECK (' . $name . '>= 0)';
}
}
else {
$sql = $name . ' ' . $spec['sqlite_type'];
if (in_array($spec['sqlite_type'], array('VARCHAR', 'TEXT'))) {
if (isset($spec['length'])) {
$sql .= '(' . $spec['length'] . ')';
}
if (isset($spec['binary']) && $spec['binary'] === FALSE) {
$sql .= ' COLLATE NOCASE_UTF8';
}
}
if (isset($spec['not null'])) {
if ($spec['not null']) {
$sql .= ' NOT NULL';
}
else {
$sql .= ' NULL';
}
}
if (!empty($spec['unsigned'])) {
$sql .= ' CHECK (' . $name . '>= 0)';
}
if (isset($spec['default'])) {
if (is_string($spec['default'])) {
$spec['default'] = $this->connection->quote($spec['default']);
}
$sql .= ' DEFAULT ' . $spec['default'];
}
if (empty($spec['not null']) && !isset($spec['default'])) {
$sql .= ' DEFAULT NULL';
}
}
return $sql;
}
/**
* This maps a generic data type in combination with its data size
* to the engine-specific data type.
*/
public function getFieldTypeMap() {
// Put :normal last so it gets preserved by array_flip. This makes
// it much easier for modules (such as schema.module) to map
// database types back into schema types.
// $map does not use drupal_static as its value never changes.
static $map = array(
'varchar_ascii:normal' => 'VARCHAR',
'varchar:normal' => 'VARCHAR',
'char:normal' => 'CHAR',
'text:tiny' => 'TEXT',
'text:small' => 'TEXT',
'text:medium' => 'TEXT',
'text:big' => 'TEXT',
'text:normal' => 'TEXT',
'serial:tiny' => 'INTEGER',
'serial:small' => 'INTEGER',
'serial:medium' => 'INTEGER',
'serial:big' => 'INTEGER',
'serial:normal' => 'INTEGER',
'int:tiny' => 'INTEGER',
'int:small' => 'INTEGER',
'int:medium' => 'INTEGER',
'int:big' => 'INTEGER',
'int:normal' => 'INTEGER',
'float:tiny' => 'FLOAT',
'float:small' => 'FLOAT',
'float:medium' => 'FLOAT',
'float:big' => 'FLOAT',
'float:normal' => 'FLOAT',
'numeric:normal' => 'NUMERIC',
'blob:big' => 'BLOB',
'blob:normal' => 'BLOB',
);
return $map;
}
public function renameTable($table, $new_name) {
if (!$this->tableExists($table)) {
throw new SchemaObjectDoesNotExistException(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 SchemaObjectExistsException(t("Cannot rename @table to @table_new: table @table_new already exists.", array('@table' => $table, '@table_new' => $new_name)));
}
$schema = $this->introspectSchema($table);
// SQLite doesn't allow you to rename tables outside of the current
// database. So the syntax '... RENAME TO database.table' would fail.
// So we must determine the full table name here rather than surrounding
// the table with curly braces in case the db_prefix contains a reference
// to a database outside of our existing database.
$info = $this->getPrefixInfo($new_name);
$this->connection->query('ALTER TABLE {' . $table . '} RENAME TO ' . $info['table']);
// Drop the indexes, there is no RENAME INDEX command in SQLite.
if (!empty($schema['unique keys'])) {
foreach ($schema['unique keys'] as $key => $fields) {
$this->dropIndex($table, $key);
}
}
if (!empty($schema['indexes'])) {
foreach ($schema['indexes'] as $index => $fields) {
$this->dropIndex($table, $index);
}
}
// Recreate the indexes.
$statements = $this->createIndexSql($new_name, $schema);
foreach ($statements as $statement) {
$this->connection->query($statement);
}
}
public function dropTable($table) {
if (!$this->tableExists($table)) {
return FALSE;
}
$this->connection->tableDropped = TRUE;
$this->connection->query('DROP TABLE {' . $table . '}');
return TRUE;
}
public function addField($table, $field, $specification, $keys_new = array()) {
if (!$this->tableExists($table)) {
throw new SchemaObjectDoesNotExistException(t("Cannot add field @table.@field: table doesn't exist.", array('@field' => $field, '@table' => $table)));
}
if ($this->fieldExists($table, $field)) {
throw new SchemaObjectExistsException(t("Cannot add field @table.@field: field already exists.", array('@field' => $field, '@table' => $table)));
}
// SQLite doesn't have a full-featured ALTER TABLE statement. It only
// supports adding new fields to a table, in some simple cases. In most
// cases, we have to create a new table and copy the data over.
if (empty($keys_new) && (empty($specification['not null']) || isset($specification['default']))) {
// When we don't have to create new keys and we are not creating a
// NOT NULL column without a default value, we can use the quicker version.
$query = 'ALTER TABLE {' . $table . '} ADD ' . $this->createFieldSql($field, $this->processField($specification));
$this->connection->query($query);
// Apply the initial value if set.
if (isset($specification['initial'])) {
$this->connection->update($table)
->fields(array($field => $specification['initial']))
->execute();
}
}
else {
// We cannot add the field directly. Use the slower table alteration
// method, starting from the old schema.
$old_schema = $this->introspectSchema($table);
$new_schema = $old_schema;
// Add the new field.
$new_schema['fields'][$field] = $specification;
// Build the mapping between the old fields and the new fields.
$mapping = array();
if (isset($specification['initial'])) {
// If we have a initial value, copy it over.
$mapping[$field] = array(
'expression' => ':newfieldinitial',
'arguments' => array(':newfieldinitial' => $specification['initial']),
);
}
else {
// Else use the default of the field.
$mapping[$field] = NULL;
}
// Add the new indexes.
$new_schema += $keys_new;
$this->alterTable($table, $old_schema, $new_schema, $mapping);
}
}
/**
* Create a table with a new schema containing the old content.
*
* As SQLite does not support ALTER TABLE (with a few exceptions) it is
* necessary to create a new table and copy over the old content.
*
* @param $table
* Name of the table to be altered.
* @param $old_schema
* The old schema array for the table.
* @param $new_schema
* The new schema array for the table.
* @param $mapping
* An optional mapping between the fields of the old specification and the
* fields of the new specification. An associative array, whose keys are
* the fields of the new table, and values can take two possible forms:
* - a simple string, which is interpreted as the name of a field of the
* old table,
* - an associative array with two keys 'expression' and 'arguments',
* that will be used as an expression field.
*/
protected function alterTable($table, $old_schema, $new_schema, array $mapping = array()) {
$i = 0;
do {
$new_table = $table . '_' . $i++;
} while ($this->tableExists($new_table));
$this->createTable($new_table, $new_schema);
// Build a SQL query to migrate the data from the old table to the new.
$select = $this->connection->select($table);
// Complete the mapping.
$possible_keys = array_keys($new_schema['fields']);
$mapping += array_combine($possible_keys, $possible_keys);
// Now add the fields.
foreach ($mapping as $field_alias => $field_source) {
// Just ignore this field (ie. use it's default value).
if (!isset($field_source)) {
continue;
}
if (is_array($field_source)) {
$select->addExpression($field_source['expression'], $field_alias, $field_source['arguments']);
}
else {
$select->addField($table, $field_source, $field_alias);
}
}
// Execute the data migration query.
$this->connection->insert($new_table)
->from($select)
->execute();
$old_count = $this->connection->query('SELECT COUNT(*) FROM {' . $table . '}')->fetchField();
$new_count = $this->connection->query('SELECT COUNT(*) FROM {' . $new_table . '}')->fetchField();
if ($old_count == $new_count) {
$this->dropTable($table);
$this->renameTable($new_table, $table);
}
}
/**
* Find out the schema of a table.
*
* This function uses introspection methods provided by the database to
* create a schema array. This is useful, for example, during update when
* the old schema is not available.
*
* @param $table
* Name of the table.
* @return
* An array representing the schema, from drupal_get_schema().
*/
protected function introspectSchema($table) {
$mapped_fields = array_flip($this->getFieldTypeMap());
$schema = array(
'fields' => array(),
'primary key' => array(),
'unique keys' => array(),
'indexes' => array(),
);
$info = $this->getPrefixInfo($table);
$result = $this->connection->query('PRAGMA ' . $info['schema'] . '.table_info(' . $info['table'] . ')');
foreach ($result as $row) {
if (preg_match('/^([^(]+)\((.*)\)$/', $row->type, $matches)) {
$type = $matches[1];
$length = $matches[2];
}
else {
$type = $row->type;
$length = NULL;
}
if (isset($mapped_fields[$type])) {
list($type, $size) = explode(':', $mapped_fields[$type]);
$schema['fields'][$row->name] = array(
'type' => $type,
'size' => $size,
'not null' => !empty($row->notnull),
'default' => trim($row->dflt_value, "'"),
);
if ($length) {
$schema['fields'][$row->name]['length'] = $length;
}
if ($row->pk) {
$schema['primary key'][] = $row->name;
}
}
else {
new \Exception("Unable to parse the column type " . $row->type);
}
}
$indexes = array();
$result = $this->connection->query('PRAGMA ' . $info['schema'] . '.index_list(' . $info['table'] . ')');
foreach ($result as $row) {
if (strpos($row->name, 'sqlite_autoindex_') !== 0) {
$indexes[] = array(
'schema_key' => $row->unique ? 'unique keys' : 'indexes',
'name' => $row->name,
);
}
}
foreach ($indexes as $index) {
$name = $index['name'];
// Get index name without prefix.
$index_name = substr($name, strlen($info['table']) + 1);
$result = $this->connection->query('PRAGMA ' . $info['schema'] . '.index_info(' . $name . ')');
foreach ($result as $row) {
$schema[$index['schema_key']][$index_name][] = $row->name;
}
}
return $schema;
}
public function dropField($table, $field) {
if (!$this->fieldExists($table, $field)) {
return FALSE;
}
$old_schema = $this->introspectSchema($table);
$new_schema = $old_schema;
unset($new_schema['fields'][$field]);
// Handle possible primary key changes.
if (isset($new_schema['primary key']) && ($key = array_search($field, $new_schema['primary key'])) !== FALSE) {
unset($new_schema['primary key'][$key]);
}
// Handle possible index changes.
foreach ($new_schema['indexes'] as $index => $fields) {
foreach ($fields as $key => $field_name) {
if ($field_name == $field) {
unset($new_schema['indexes'][$index][$key]);
}
}
// If this index has no more fields then remove it.
if (empty($new_schema['indexes'][$index])) {
unset($new_schema['indexes'][$index]);
}
}
$this->alterTable($table, $old_schema, $new_schema);
return TRUE;
}
public function changeField($table, $field, $field_new, $spec, $keys_new = array()) {
if (!$this->fieldExists($table, $field)) {
throw new SchemaObjectDoesNotExistException(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 SchemaObjectExistsException(t("Cannot rename field @table.@name to @name_new: target field already exists.", array('@table' => $table, '@name' => $field, '@name_new' => $field_new)));
}
$old_schema = $this->introspectSchema($table);
$new_schema = $old_schema;
// Map the old field to the new field.
if ($field != $field_new) {
$mapping[$field_new] = $field;
}
else {
$mapping = array();
}
// Remove the previous definition and swap in the new one.
unset($new_schema['fields'][$field]);
$new_schema['fields'][$field_new] = $spec;
// Map the former indexes to the new column name.
$new_schema['primary key'] = $this->mapKeyDefinition($new_schema['primary key'], $mapping);
foreach (array('unique keys', 'indexes') as $k) {
foreach ($new_schema[$k] as &$key_definition) {
$key_definition = $this->mapKeyDefinition($key_definition, $mapping);
}
}
// Add in the keys from $keys_new.
if (isset($keys_new['primary key'])) {
$new_schema['primary key'] = $keys_new['primary key'];
}
foreach (array('unique keys', 'indexes') as $k) {
if (!empty($keys_new[$k])) {
$new_schema[$k] = $keys_new[$k] + $new_schema[$k];
}
}
$this->alterTable($table, $old_schema, $new_schema, $mapping);
}
/**
* Utility method: rename columns in an index definition according to a new mapping.
*
* @param $key_definition
* The key definition.
* @param $mapping
* The new mapping.
*/
protected function mapKeyDefinition(array $key_definition, array $mapping) {
foreach ($key_definition as &$field) {
// The key definition can be an array($field, $length).
if (is_array($field)) {
$field = &$field[0];
}
if (isset($mapping[$field])) {
$field = $mapping[$field];
}
}
return $key_definition;
}
public function addIndex($table, $name, $fields) {
if (!$this->tableExists($table)) {
throw new SchemaObjectDoesNotExistException(t("Cannot add index @name to table @table: table doesn't exist.", array('@table' => $table, '@name' => $name)));
}
if ($this->indexExists($table, $name)) {
throw new SchemaObjectExistsException(t("Cannot add index @name to table @table: index already exists.", array('@table' => $table, '@name' => $name)));
}
$schema['indexes'][$name] = $fields;
$statements = $this->createIndexSql($table, $schema);
foreach ($statements as $statement) {
$this->connection->query($statement);
}
}
public function indexExists($table, $name) {
$info = $this->getPrefixInfo($table);
return $this->connection->query('PRAGMA ' . $info['schema'] . '.index_info(' . $info['table'] . '_' . $name . ')')->fetchField() != '';
}
public function dropIndex($table, $name) {
if (!$this->indexExists($table, $name)) {
return FALSE;
}
$info = $this->getPrefixInfo($table);
$this->connection->query('DROP INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $name);
return TRUE;
}
public function addUniqueKey($table, $name, $fields) {
if (!$this->tableExists($table)) {
throw new SchemaObjectDoesNotExistException(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 SchemaObjectExistsException(t("Cannot add unique key @name to table @table: unique key already exists.", array('@table' => $table, '@name' => $name)));
}
$schema['unique keys'][$name] = $fields;
$statements = $this->createIndexSql($table, $schema);
foreach ($statements as $statement) {
$this->connection->query($statement);
}
}
public function dropUniqueKey($table, $name) {
if (!$this->indexExists($table, $name)) {
return FALSE;
}
$info = $this->getPrefixInfo($table);
$this->connection->query('DROP INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $name);
return TRUE;
}
public function addPrimaryKey($table, $fields) {
if (!$this->tableExists($table)) {
throw new SchemaObjectDoesNotExistException(t("Cannot add primary key to table @table: table doesn't exist.", array('@table' => $table)));
}
$old_schema = $this->introspectSchema($table);
$new_schema = $old_schema;
if (!empty($new_schema['primary key'])) {
throw new SchemaObjectExistsException(t("Cannot add primary key to table @table: primary key already exists.", array('@table' => $table)));
}
$new_schema['primary key'] = $fields;
$this->alterTable($table, $old_schema, $new_schema);
}
public function dropPrimaryKey($table) {
$old_schema = $this->introspectSchema($table);
$new_schema = $old_schema;
if (empty($new_schema['primary key'])) {
return FALSE;
}
unset($new_schema['primary key']);
$this->alterTable($table, $old_schema, $new_schema);
return TRUE;
}
public function fieldSetDefault($table, $field, $default) {
if (!$this->fieldExists($table, $field)) {
throw new SchemaObjectDoesNotExistException(t("Cannot set default value of field @table.@field: field doesn't exist.", array('@table' => $table, '@field' => $field)));
}
$old_schema = $this->introspectSchema($table);
$new_schema = $old_schema;
$new_schema['fields'][$field]['default'] = $default;
$this->alterTable($table, $old_schema, $new_schema);
}
public function fieldSetNoDefault($table, $field) {
if (!$this->fieldExists($table, $field)) {
throw new SchemaObjectDoesNotExistException(t("Cannot remove default value of field @table.@field: field doesn't exist.", array('@table' => $table, '@field' => $field)));
}
$old_schema = $this->introspectSchema($table);
$new_schema = $old_schema;
unset($new_schema['fields'][$field]['default']);
$this->alterTable($table, $old_schema, $new_schema);
}
public function findTables($table_expression) {
// Don't add the prefix, $table_expression already includes the prefix.
$info = $this->getPrefixInfo($table_expression, FALSE);
// Can't use query placeholders for the schema because the query would have
// to be :prefixsqlite_master, which does not work.
$result = db_query("SELECT name FROM " . $info['schema'] . ".sqlite_master WHERE type = :type AND name LIKE :table_name", array(
':type' => 'table',
':table_name' => $info['table'],
));
return $result->fetchAllKeyed(0, 0);
}
}

View file

@ -0,0 +1,17 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\sqlite\Select.
*/
namespace Drupal\Core\Database\Driver\sqlite;
use Drupal\Core\Database\Query\Select as QuerySelect;
class Select extends QuerySelect {
public function forUpdate($set = TRUE) {
// SQLite does not support FOR UPDATE so nothing to do.
return $this;
}
}

View file

@ -0,0 +1,146 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\sqlite\Statement.
*/
namespace Drupal\Core\Database\Driver\sqlite;
use Drupal\Core\Database\StatementPrefetch;
use Drupal\Core\Database\StatementInterface;
/**
* Specific SQLite implementation of DatabaseConnection.
*
* The PDO SQLite driver only closes SELECT statements when the PDOStatement
* destructor is called and SQLite does not allow data change (INSERT,
* UPDATE etc) on a table which has open SELECT statements. This is a
* user-space mock of PDOStatement that buffers all the data and doesn't
* have those limitations.
*/
class Statement extends StatementPrefetch implements StatementInterface {
/**
* SQLite specific implementation of getStatement().
*
* The PDO SQLite layer doesn't replace numeric placeholders in queries
* correctly, and this makes numeric expressions (such as COUNT(*) >= :count)
* fail. We replace numeric placeholders in the query ourselves to work
* around this bug.
*
* See http://bugs.php.net/bug.php?id=45259 for more details.
*/
protected function getStatement($query, &$args = array()) {
if (count($args)) {
// Check if $args is a simple numeric array.
if (range(0, count($args) - 1) === array_keys($args)) {
// In that case, we have unnamed placeholders.
$count = 0;
$new_args = array();
foreach ($args as $value) {
if (is_float($value) || is_int($value)) {
if (is_float($value)) {
// Force the conversion to float so as not to loose precision
// in the automatic cast.
$value = sprintf('%F', $value);
}
$query = substr_replace($query, $value, strpos($query, '?'), 1);
}
else {
$placeholder = ':db_statement_placeholder_' . $count++;
$query = substr_replace($query, $placeholder, strpos($query, '?'), 1);
$new_args[$placeholder] = $value;
}
}
$args = $new_args;
}
else {
// Else, this is using named placeholders.
foreach ($args as $placeholder => $value) {
if (is_float($value) || is_int($value)) {
if (is_float($value)) {
// Force the conversion to float so as not to loose precision
// in the automatic cast.
$value = sprintf('%F', $value);
}
// We will remove this placeholder from the query as PDO throws an
// exception if the number of placeholders in the query and the
// arguments does not match.
unset($args[$placeholder]);
// PDO allows placeholders to not be prefixed by a colon. See
// http://marc.info/?l=php-internals&m=111234321827149&w=2 for
// more.
if ($placeholder[0] != ':') {
$placeholder = ":$placeholder";
}
// When replacing the placeholders, make sure we search for the
// exact placeholder. For example, if searching for
// ':db_placeholder_1', do not replace ':db_placeholder_11'.
$query = preg_replace('/' . preg_quote($placeholder) . '\b/', $value, $query);
}
}
}
}
return $this->pdoConnection->prepare($query);
}
public function execute($args = array(), $options = array()) {
try {
$return = parent::execute($args, $options);
}
catch (\PDOException $e) {
if (!empty($e->errorInfo[1]) && $e->errorInfo[1] === 17) {
// The schema has changed. SQLite specifies that we must resend the query.
$return = parent::execute($args, $options);
}
else {
// Rethrow the exception.
throw $e;
}
}
// In some weird cases, SQLite will prefix some column names by the name
// of the table. We post-process the data, by renaming the column names
// using the same convention as MySQL and PostgreSQL.
$rename_columns = array();
foreach ($this->columnNames as $k => $column) {
// In some SQLite versions, SELECT DISTINCT(field) will return "(field)"
// instead of "field".
if (preg_match("/^\((.*)\)$/", $column, $matches)) {
$rename_columns[$column] = $matches[1];
$this->columnNames[$k] = $matches[1];
$column = $matches[1];
}
// Remove "table." prefixes.
if (preg_match("/^.*\.(.*)$/", $column, $matches)) {
$rename_columns[$column] = $matches[1];
$this->columnNames[$k] = $matches[1];
}
}
if ($rename_columns) {
// DatabaseStatementPrefetch already extracted the first row,
// put it back into the result set.
if (isset($this->currentRow)) {
$this->data[0] = &$this->currentRow;
}
// Then rename all the columns across the result set.
foreach ($this->data as $k => $row) {
foreach ($rename_columns as $old_column => $new_column) {
$this->data[$k][$new_column] = $this->data[$k][$old_column];
unset($this->data[$k][$old_column]);
}
}
// Finally, extract the first row again.
$this->currentRow = $this->data[0];
unset($this->data[0]);
}
return $return;
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\sqlite\Transaction.
*/
namespace Drupal\Core\Database\Driver\sqlite;
use Drupal\Core\Database\Transaction as DatabaseTransaction;
class Transaction extends DatabaseTransaction { }

View file

@ -0,0 +1,25 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\sqlite\Truncate.
*/
namespace Drupal\Core\Database\Driver\sqlite;
use Drupal\Core\Database\Query\Truncate as QueryTruncate;
/**
* SQLite specific implementation of TruncateQuery.
*
* SQLite doesn't support TRUNCATE, but a DELETE query with no condition has
* exactly the effect (it is implemented by DROPing the table).
*/
class Truncate extends QueryTruncate {
public function __toString() {
// Create a sanitized comment string to prepend to the query.
$comments = $this->connection->makeComment($this->comments);
return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '} ';
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Driver\sqlite\Update.
*/
namespace Drupal\Core\Database\Driver\sqlite;
use Drupal\Core\Database\Query\Update as QueryUpdate;
class Update extends QueryUpdate { }

View file

@ -0,0 +1,13 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\DriverNotSpecifiedException.
*/
namespace Drupal\Core\Database;
/**
* Exception thrown if no driver is specified for a database connection.
*/
class DriverNotSpecifiedException extends \RuntimeException {}

View file

@ -0,0 +1,13 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Install\TaskException.
*/
namespace Drupal\Core\Database\Install;
/**
* Exception thrown if the database installer fails.
*/
class TaskException extends \RuntimeException { }

View file

@ -0,0 +1,309 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Install\Tasks.
*/
namespace Drupal\Core\Database\Install;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Database\Database;
/**
* Database installer structure.
*
* Defines basic Drupal requirements for databases.
*/
abstract class Tasks {
/**
* The name of the PDO driver this database type requires.
*
* @var string
*/
protected $pdoDriver;
/**
* Structure that describes each task to run.
*
* @var array
*
* Each value of the tasks array is an associative array defining the function
* to call (optional) and any arguments to be passed to the function.
*/
protected $tasks = array(
array(
'function' => 'checkEngineVersion',
'arguments' => array(),
),
array(
'arguments' => array(
'CREATE TABLE {drupal_install_test} (id int NULL)',
'Drupal can use CREATE TABLE database commands.',
'Failed to <strong>CREATE</strong> a test table on your database server with the command %query. The server reports the following message: %error.<p>Are you sure the configured username has the necessary permissions to create tables in the database?</p>',
TRUE,
),
),
array(
'arguments' => array(
'INSERT INTO {drupal_install_test} (id) VALUES (1)',
'Drupal can use INSERT database commands.',
'Failed to <strong>INSERT</strong> a value into a test table on your database server. We tried inserting a value with the command %query and the server reported the following error: %error.',
),
),
array(
'arguments' => array(
'UPDATE {drupal_install_test} SET id = 2',
'Drupal can use UPDATE database commands.',
'Failed to <strong>UPDATE</strong> a value in a test table on your database server. We tried updating a value with the command %query and the server reported the following error: %error.',
),
),
array(
'arguments' => array(
'DELETE FROM {drupal_install_test}',
'Drupal can use DELETE database commands.',
'Failed to <strong>DELETE</strong> a value from a test table on your database server. We tried deleting a value with the command %query and the server reported the following error: %error.',
),
),
array(
'arguments' => array(
'DROP TABLE {drupal_install_test}',
'Drupal can use DROP TABLE database commands.',
'Failed to <strong>DROP</strong> a test table from your database server. We tried dropping a table with the command %query and the server reported the following error %error.',
),
),
);
/**
* Results from tasks.
*
* @var array
*/
protected $results = array();
/**
* Ensure the PDO driver is supported by the version of PHP in use.
*/
protected function hasPdoDriver() {
return in_array($this->pdoDriver, \PDO::getAvailableDrivers());
}
/**
* Assert test as failed.
*/
protected function fail($message) {
$this->results[$message] = FALSE;
}
/**
* Assert test as a pass.
*/
protected function pass($message) {
$this->results[$message] = TRUE;
}
/**
* Check whether Drupal is installable on the database.
*/
public function installable() {
return $this->hasPdoDriver() && empty($this->error);
}
/**
* Return the human-readable name of the driver.
*/
abstract public function name();
/**
* Return the minimum required version of the engine.
*
* @return
* A version string. If not NULL, it will be checked against the version
* reported by the Database engine using version_compare().
*/
public function minimumVersion() {
return NULL;
}
/**
* Run database tasks and tests to see if Drupal can run on the database.
*/
public function runTasks() {
// We need to establish a connection before we can run tests.
if ($this->connect()) {
foreach ($this->tasks as $task) {
if (!isset($task['function'])) {
$task['function'] = 'runTestQuery';
}
if (method_exists($this, $task['function'])) {
// Returning false is fatal. No other tasks can run.
if (FALSE === call_user_func_array(array($this, $task['function']), $task['arguments'])) {
break;
}
}
else {
throw new TaskException(t("Failed to run all tasks against the database server. The task %task wasn't found.", array('%task' => $task['function'])));
}
}
}
// Check for failed results and compile message
$message = '';
foreach ($this->results as $result => $success) {
if (!$success) {
$message = SafeMarkup::isSafe($result) ? $result : SafeMarkup::checkPlain($result);
}
}
if (!empty($message)) {
$message = SafeMarkup::set('Resolve all issues below to continue the installation. For help configuring your database server, see the <a href="https://www.drupal.org/getting-started/install">installation handbook</a>, or contact your hosting provider.' . $message);
throw new TaskException($message);
}
}
/**
* Check if we can connect to the database.
*/
protected function connect() {
try {
// This doesn't actually test the connection.
db_set_active();
// Now actually do a check.
Database::getConnection();
$this->pass('Drupal can CONNECT to the database ok.');
}
catch (\Exception $e) {
$this->fail(t('Failed to connect to your database server. The server reports the following message: %error.<ul><li>Is the database server running?</li><li>Does the database exist, and have you entered the correct database name?</li><li>Have you entered the correct username and password?</li><li>Have you entered the correct database hostname?</li></ul>', array('%error' => $e->getMessage())));
return FALSE;
}
return TRUE;
}
/**
* Run SQL tests to ensure the database can execute commands with the current user.
*/
protected function runTestQuery($query, $pass, $fail, $fatal = FALSE) {
try {
Database::getConnection()->query($query);
$this->pass(t($pass));
}
catch (\Exception $e) {
$this->fail(t($fail, array('%query' => $query, '%error' => $e->getMessage(), '%name' => $this->name())));
return !$fatal;
}
}
/**
* Check the engine version.
*/
protected function checkEngineVersion() {
if ($this->minimumVersion() && version_compare(Database::getConnection()->version(), $this->minimumVersion(), '<')) {
$this->fail(t("The database version %version is less than the minimum required version %minimum_version.", array('%version' => Database::getConnection()->version(), '%minimum_version' => $this->minimumVersion())));
}
}
/**
* Return driver specific configuration options.
*
* @param $database
* An array of driver specific configuration options.
*
* @return
* The options form array.
*/
public function getFormOptions(array $database) {
$form['database'] = array(
'#type' => 'textfield',
'#title' => t('Database name'),
'#default_value' => empty($database['database']) ? '' : $database['database'],
'#size' => 45,
'#required' => TRUE,
'#states' => array(
'required' => array(
':input[name=driver]' => array('value' => $this->pdoDriver),
),
),
);
$form['username'] = array(
'#type' => 'textfield',
'#title' => t('Database username'),
'#default_value' => empty($database['username']) ? '' : $database['username'],
'#size' => 45,
'#required' => TRUE,
'#states' => array(
'required' => array(
':input[name=driver]' => array('value' => $this->pdoDriver),
),
),
);
$form['password'] = array(
'#type' => 'password',
'#title' => t('Database password'),
'#default_value' => empty($database['password']) ? '' : $database['password'],
'#required' => FALSE,
'#size' => 45,
);
$form['advanced_options'] = array(
'#type' => 'details',
'#title' => t('Advanced options'),
'#weight' => 10,
);
$profile = drupal_get_profile();
$db_prefix = ($profile == 'standard') ? 'drupal_' : $profile . '_';
$form['advanced_options']['prefix'] = array(
'#type' => 'textfield',
'#title' => t('Table name prefix'),
'#default_value' => empty($database['prefix']) ? '' : $database['prefix'],
'#size' => 45,
'#description' => t('If more than one application will be sharing this database, a unique table name prefix such as %prefix will prevent collisions.', array('%prefix' => $db_prefix)),
'#weight' => 10,
);
$form['advanced_options']['host'] = array(
'#type' => 'textfield',
'#title' => t('Host'),
'#default_value' => empty($database['host']) ? 'localhost' : $database['host'],
'#size' => 45,
// Hostnames can be 255 characters long.
'#maxlength' => 255,
'#required' => TRUE,
);
$form['advanced_options']['port'] = array(
'#type' => 'number',
'#title' => t('Port number'),
'#default_value' => empty($database['port']) ? '' : $database['port'],
'#min' => 0,
'#max' => 65535,
);
return $form;
}
/**
* Validates driver specific configuration settings.
*
* Checks to ensure correct basic database settings and that a proper
* connection to the database can be established.
*
* @param $database
* An array of driver specific configuration options.
*
* @return
* An array of driver configuration errors, keyed by form element name.
*/
public function validateDatabaseSettings($database) {
$errors = array();
// Verify the table prefix.
if (!empty($database['prefix']) && is_string($database['prefix']) && !preg_match('/^[A-Za-z0-9_.]+$/', $database['prefix'])) {
$errors[$database['driver'] . '][prefix'] = t('The database table prefix you have entered, %prefix, is invalid. The table prefix can only contain alphanumeric characters, periods, or underscores.', array('%prefix' => $database['prefix']));
}
return $errors;
}
}

View file

@ -0,0 +1,16 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\IntegrityConstraintViolationException.
*/
namespace Drupal\Core\Database;
/**
* Exception thrown if a query would violate an integrity constraint.
*
* This exception is thrown e.g. when trying to insert a row that would violate
* a unique key constraint.
*/
class IntegrityConstraintViolationException extends \RuntimeException implements DatabaseException { }

View file

@ -0,0 +1,16 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\InvalidQueryException.
*/
namespace Drupal\Core\Database;
/**
* Exception thrown if a query would be invalid.
*
* This exception is thrown e.g. when trying to have an IN condition with an
* empty array.
*/
class InvalidQueryException extends \InvalidArgumentException implements DatabaseException { }

View file

@ -0,0 +1,169 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Log.
*/
namespace Drupal\Core\Database;
/**
* Database query logger.
*
* We log queries in a separate object rather than in the connection object
* because we want to be able to see all queries sent to a given database, not
* database target. If we logged the queries in each connection object we
* would not be able to track what queries went to which target.
*
* Every connection has one and only one logging object on it for all targets
* and logging keys.
*/
class Log {
/**
* Cache of logged queries. This will only be used if the query logger is enabled.
*
* The structure for the logging array is as follows:
*
* array(
* $logging_key = array(
* array(query => '', args => array(), caller => '', target => '', time => 0),
* array(query => '', args => array(), caller => '', target => '', time => 0),
* ),
* );
*
* @var array
*/
protected $queryLog = array();
/**
* The connection key for which this object is logging.
*
* @var string
*/
protected $connectionKey = 'default';
/**
* Constructor.
*
* @param $key
* The database connection key for which to enable logging.
*/
public function __construct($key = 'default') {
$this->connectionKey = $key;
}
/**
* Begin logging queries to the specified connection and logging key.
*
* If the specified logging key is already running this method does nothing.
*
* @param $logging_key
* The identification key for this log request. By specifying different
* logging keys we are able to start and stop multiple logging runs
* simultaneously without them colliding.
*/
public function start($logging_key) {
if (empty($this->queryLog[$logging_key])) {
$this->clear($logging_key);
}
}
/**
* Retrieve the query log for the specified logging key so far.
*
* @param $logging_key
* The logging key to fetch.
* @return
* An indexed array of all query records for this logging key.
*/
public function get($logging_key) {
return $this->queryLog[$logging_key];
}
/**
* Empty the query log for the specified logging key.
*
* This method does not stop logging, it simply clears the log. To stop
* logging, use the end() method.
*
* @param $logging_key
* The logging key to empty.
*/
public function clear($logging_key) {
$this->queryLog[$logging_key] = array();
}
/**
* Stop logging for the specified logging key.
*
* @param $logging_key
* The logging key to stop.
*/
public function end($logging_key) {
unset($this->queryLog[$logging_key]);
}
/**
* Log a query to all active logging keys.
*
* @param $statement
* The prepared statement object to log.
* @param $args
* The arguments passed to the statement object.
* @param $time
* The time in milliseconds the query took to execute.
*/
public function log(StatementInterface $statement, $args, $time) {
foreach (array_keys($this->queryLog) as $key) {
$this->queryLog[$key][] = array(
'query' => $statement->getQueryString(),
'args' => $args,
'target' => $statement->dbh->getTarget(),
'caller' => $this->findCaller(),
'time' => $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 the includes/Drupal/Database directory,
* does not begin with db_ 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
* This method returns a stack trace entry similar to that generated by
* debug_backtrace(). However, it flattens the trace entry and the trace
* entry before it so that we get the function and args of the function that
* called into the database system, not the function and args of the
* database call itself.
*/
public function findCaller() {
$stack = debug_backtrace();
for ($i = 0, $stack_count = count($stack); $i < $stack_count; ++$i) {
// If the call was made from a function, 'class' will be empty. It's
// just easier to give it a default value than to try and integrate
// that into the if statement below.
if (empty($stack[$i]['class'])) {
$stack[$i]['class'] = '';
}
if (strpos($stack[$i]['class'], __NAMESPACE__) === FALSE && strpos($stack[$i + 1]['function'], 'db_') === FALSE && !empty($stack[$i]['file'])) {
$stack[$i] += array('file' => '?', 'line' => '?', 'args' => array());
return array(
'file' => $stack[$i]['file'],
'line' => $stack[$i]['line'],
'function' => $stack[$i + 1]['function'],
'class' => isset($stack[$i + 1]['class']) ? $stack[$i + 1]['class'] : NULL,
'type' => isset($stack[$i + 1]['type']) ? $stack[$i + 1]['type'] : NULL,
'args' => $stack[$i + 1]['args'],
);
}
}
}
}

View file

@ -0,0 +1,95 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Query\AlterableInterface.
*/
namespace Drupal\Core\Database\Query;
/**
* Interface for a query that can be manipulated via an alter hook.
*/
interface AlterableInterface {
/**
* Adds a tag to a query.
*
* Tags are strings that identify a query. A query may have any number of
* tags. Tags are used to mark a query so that alter hooks may decide if they
* wish to take action. Tags should be all lower-case and contain only
* letters, numbers, and underscore, and start with a letter. That is, they
* should follow the same rules as PHP identifiers in general.
*
* @param $tag
* The tag to add.
*
* @return \Drupal\Core\Database\Query\AlterableInterface
* The called object.
*/
public function addTag($tag);
/**
* Determines if a given query has a given tag.
*
* @param $tag
* The tag to check.
*
* @return
* TRUE if this query has been marked with this tag, FALSE otherwise.
*/
public function hasTag($tag);
/**
* Determines if a given query has all specified tags.
*
* @param $tags
* A variable number of arguments, one for each tag to check.
*
* @return
* TRUE if this query has been marked with all specified tags, FALSE
* otherwise.
*/
public function hasAllTags();
/**
* Determines if a given query has any specified tag.
*
* @param $tags
* A variable number of arguments, one for each tag to check.
*
* @return
* TRUE if this query has been marked with at least one of the specified
* tags, FALSE otherwise.
*/
public function hasAnyTag();
/**
* Adds additional metadata to the query.
*
* Often, a query may need to provide additional contextual data to alter
* hooks. Alter hooks may then use that information to decide if and how
* to take action.
*
* @param $key
* The unique identifier for this piece of metadata. Must be a string that
* 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 \Drupal\Core\Database\Query\AlterableInterface
* The called object.
*/
public function addMetaData($key, $object);
/**
* Retrieves a given piece of metadata.
*
* @param $key
* The unique identifier for the piece of metadata to retrieve.
*
* @return
* The previously attached metadata object, or NULL if one doesn't exist.
*/
public function getMetaData($key);
}

View file

@ -0,0 +1,340 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Query\Condition.
*/
namespace Drupal\Core\Database\Query;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\InvalidQueryException;
/**
* Generic class for a series of conditions in a query.
*/
class Condition implements ConditionInterface, \Countable {
/**
* Array of conditions.
*
* @var array
*/
protected $conditions = array();
/**
* Array of arguments.
*
* @var array
*/
protected $arguments = array();
/**
* Whether the conditions have been changed.
*
* TRUE if the condition has been changed since the last compile.
* FALSE if the condition has been compiled and not changed.
*
* @var bool
*/
protected $changed = TRUE;
/**
* The identifier of the query placeholder this condition has been compiled against.
*/
protected $queryPlaceholderIdentifier;
/**
* Constructs a Condition object.
*
* @param string $conjunction
* The operator to use to combine conditions: 'AND' or 'OR'.
*/
public function __construct($conjunction) {
$this->conditions['#conjunction'] = $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
* conjunction.
*/
public function count() {
return count($this->conditions) - 1;
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::condition().
*/
public function condition($field, $value = NULL, $operator = '=') {
if (empty($operator)) {
$operator = '=';
}
if (empty($value) && is_array($value)) {
throw new InvalidQueryException(sprintf("Query condition '%s %s ()' cannot be empty.", $field, $operator));
}
$this->conditions[] = array(
'field' => $field,
'value' => $value,
'operator' => $operator,
);
$this->changed = TRUE;
return $this;
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::where().
*/
public function where($snippet, $args = array()) {
$this->conditions[] = array(
'field' => $snippet,
'value' => $args,
'operator' => NULL,
);
$this->changed = TRUE;
return $this;
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::isNull().
*/
public function isNull($field) {
return $this->condition($field, NULL, 'IS NULL');
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::isNotNull().
*/
public function isNotNull($field) {
return $this->condition($field, NULL, 'IS NOT NULL');
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::exists().
*/
public function exists(SelectInterface $select) {
return $this->condition('', $select, 'EXISTS');
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::notExists().
*/
public function notExists(SelectInterface $select) {
return $this->condition('', $select, 'NOT EXISTS');
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::conditions().
*/
public function &conditions() {
return $this->conditions;
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::arguments().
*/
public function arguments() {
// If the caller forgot to call compile() first, refuse to run.
if ($this->changed) {
return NULL;
}
return $this->arguments;
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::compile().
*/
public function compile(Connection $connection, PlaceholderInterface $queryPlaceholder) {
// Re-compile if this condition changed or if we are compiled against a
// different query placeholder object.
if ($this->changed || isset($this->queryPlaceholderIdentifier) && ($this->queryPlaceholderIdentifier != $queryPlaceholder->uniqueIdentifier())) {
$this->queryPlaceholderIdentifier = $queryPlaceholder->uniqueIdentifier();
$condition_fragments = array();
$arguments = array();
$conditions = $this->conditions;
$conjunction = $conditions['#conjunction'];
unset($conditions['#conjunction']);
foreach ($conditions as $condition) {
if (empty($condition['operator'])) {
// This condition is a literal string, so let it through as is.
$condition_fragments[] = ' (' . $condition['field'] . ') ';
$arguments += $condition['value'];
}
else {
// It's a structured condition, so parse it out accordingly.
// Note that $condition['field'] will only be an object for a dependent
// DatabaseCondition object, not for a dependent subquery.
if ($condition['field'] instanceof ConditionInterface) {
// Compile the sub-condition recursively and add it to the list.
$condition['field']->compile($connection, $queryPlaceholder);
$condition_fragments[] = '(' . (string) $condition['field'] . ')';
$arguments += $condition['field']->arguments();
}
else {
// For simplicity, we treat all operators as the same data structure.
// In the typical degenerate case, this won't get changed.
$operator_defaults = array(
'prefix' => '',
'postfix' => '',
'delimiter' => '',
'operator' => $condition['operator'],
'use_value' => TRUE,
);
$operator = $connection->mapConditionOperator($condition['operator']);
if (!isset($operator)) {
$operator = $this->mapConditionOperator($condition['operator']);
}
$operator += $operator_defaults;
$placeholders = array();
if ($condition['value'] instanceof SelectInterface) {
$condition['value']->compile($connection, $queryPlaceholder);
$placeholders[] = (string) $condition['value'];
$arguments += $condition['value']->arguments();
// Subqueries are the actual value of the operator, we don't
// need to add another below.
$operator['use_value'] = FALSE;
}
// We assume that if there is a delimiter, then the value is an
// array. If not, it is a scalar. For simplicity, we first convert
// up to an array so that we can build the placeholders in the same way.
elseif (!$operator['delimiter'] && !is_array($condition['value'])) {
$condition['value'] = array($condition['value']);
}
if ($operator['use_value']) {
foreach ($condition['value'] as $value) {
$placeholder = ':db_condition_placeholder_' . $queryPlaceholder->nextPlaceholder();
$arguments[$placeholder] = $value;
$placeholders[] = $placeholder;
}
}
$condition_fragments[] = ' (' . $connection->escapeField($condition['field']) . ' ' . $operator['operator'] . ' ' . $operator['prefix'] . implode($operator['delimiter'], $placeholders) . $operator['postfix'] . ') ';
}
}
}
$this->changed = FALSE;
$this->stringVersion = implode($conjunction, $condition_fragments);
$this->arguments = $arguments;
}
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::compiled().
*/
public function compiled() {
return !$this->changed;
}
/**
* Implements PHP magic __toString method to convert the conditions to string.
*
* @return string
* A string version of the conditions.
*/
public function __toString() {
// If the caller forgot to call compile() first, refuse to run.
if ($this->changed) {
return '';
}
return $this->stringVersion;
}
/**
* PHP magic __clone() method.
*
* Only copies fields that implement Drupal\Core\Database\Query\ConditionInterface. Also sets
* $this->changed to TRUE.
*/
function __clone() {
$this->changed = TRUE;
foreach ($this->conditions as $key => $condition) {
if ($key !== '#conjunction') {
if ($condition['field'] instanceOf ConditionInterface) {
$this->conditions[$key]['field'] = clone($condition['field']);
}
if ($condition['value'] instanceOf SelectInterface) {
$this->conditions[$key]['value'] = clone($condition['value']);
}
}
}
}
/**
* Gets any special processing requirements for the condition operator.
*
* Some condition types require special processing, such as IN, because
* the value data they pass in is not a simple value. This is a simple
* overridable lookup function.
*
* @param $operator
* The condition operator, such as "IN", "BETWEEN", etc. Case-sensitive.
*
* @return array
* The extra handling directives for the specified operator or an empty
* array if there are no extra handling directives.
*/
protected function mapConditionOperator($operator) {
// $specials does not use drupal_static as its value never changes.
static $specials = array(
'BETWEEN' => array('delimiter' => ' AND '),
'IN' => array('delimiter' => ', ', 'prefix' => ' (', 'postfix' => ')'),
'NOT IN' => array('delimiter' => ', ', 'prefix' => ' (', 'postfix' => ')'),
'EXISTS' => array('prefix' => ' (', 'postfix' => ')'),
'NOT EXISTS' => array('prefix' => ' (', 'postfix' => ')'),
'IS NULL' => array('use_value' => FALSE),
'IS NOT NULL' => array('use_value' => FALSE),
// Use backslash for escaping wildcard characters.
'LIKE' => array('postfix' => " ESCAPE '\\\\'"),
'NOT LIKE' => array('postfix' => " ESCAPE '\\\\'"),
// These ones are here for performance reasons.
'=' => array(),
'<' => array(),
'>' => array(),
'>=' => array(),
'<=' => array(),
);
if (isset($specials[$operator])) {
$return = $specials[$operator];
}
else {
// We need to upper case because PHP index matches are case sensitive but
// do not need the more expensive Unicode::strtoupper() because SQL statements are ASCII.
$operator = strtoupper($operator);
$return = isset($specials[$operator]) ? $specials[$operator] : array();
}
$return += array('operator' => $operator);
return $return;
}
/**
* {@inheritdoc}
*/
public function conditionGroupFactory($conjunction = 'AND') {
return new Condition($conjunction);
}
/**
* {@inheritdoc}
*/
public function andConditionGroup() {
return $this->conditionGroupFactory('AND');
}
/**
* {@inheritdoc}
*/
public function orConditionGroup() {
return $this->conditionGroupFactory('OR');
}
}

View file

@ -0,0 +1,199 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Query\ConditionInterface.
*/
namespace Drupal\Core\Database\Query;
use Drupal\Core\Database\Connection;
/**
* Interface for a conditional clause in a query.
*/
interface ConditionInterface {
/**
* Helper function: builds the most common conditional clauses.
*
* This method can take a variable number of parameters. If called with two
* parameters, they are taken as $field and $value with $operator having a
* value of =.
*
* Do not use this method to test for NULL values. Instead, use
* QueryConditionInterface::isNull() or QueryConditionInterface::isNotNull().
*
* Drupal considers LIKE case insensitive and the following is often used
* to tell the database that case insensitive equivalence is desired:
* @code
* db_select('users')
* ->condition('name', db_like($name), 'LIKE')
* @endcode
* Use 'LIKE BINARY' instead of 'LIKE' for case sensitive queries.
*
* Note: When using MySQL, the exact behavior also depends on the used
* collation. if the field is set to binary, then a LIKE condition will also
* be case sensitive and when a case insensitive collation is used, the =
* operator will also be case insensitive.
*
* @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().
* @param $value
* The value to test the field against. In most cases, this is a scalar.
* For more complex options, it is an array. The meaning of each element in
* the array is dependent on the $operator.
* @param $operator
* The comparison operator, such as =, <, or >=. It also accepts more
* complex options such as IN, LIKE, LIKE BINARY, or BETWEEN. Defaults to =.
*
* @return \Drupal\Core\Database\Query\ConditionInterface
* The called object.
*
* @see \Drupal\Core\Database\Query\ConditionInterface::isNull()
* @see \Drupal\Core\Database\Query\ConditionInterface::isNotNull()
*/
public function condition($field, $value = NULL, $operator = '=');
/**
* Adds an arbitrary WHERE clause to the query.
*
* @param $snippet
* A portion of a WHERE clause as a prepared statement. It must use named
* placeholders, not ? placeholders.
* @param $args
* An associative array of arguments.
*
* @return \Drupal\Core\Database\Query\ConditionInterface
* The called object.
*/
public function where($snippet, $args = array());
/**
* Sets a condition that the specified field be NULL.
*
* @param $field
* The name of the field to check.
*
* @return \Drupal\Core\Database\Query\ConditionInterface
* The called object.
*/
public function isNull($field);
/**
* Sets a condition that the specified field be NOT NULL.
*
* @param $field
* The name of the field to check.
*
* @return \Drupal\Core\Database\Query\ConditionInterface
* The called object.
*/
public function isNotNull($field);
/**
* Sets a condition that the specified subquery returns values.
*
* @param \Drupal\Core\Database\Query\SelectInterface $select
* The subquery that must contain results.
*
* @return \Drupal\Core\Database\Query\ConditionInterface
* The called object.
*/
public function exists(SelectInterface $select);
/**
* Sets a condition that the specified subquery returns no values.
*
* @param \Drupal\Core\Database\Query\SelectInterface $select
* The subquery that must not contain results.
*
* @return \Drupal\Core\Database\Query\ConditionInterface
* The called object.
*/
public function notExists(SelectInterface $select);
/**
* Gets a complete list of all conditions in this conditional clause.
*
* This method returns by reference. That allows alter hooks to access the
* data structure directly and manipulate it before it gets compiled.
*
* The data structure that is returned is an indexed array of entries, where
* each entry looks like the following:
* @code
* array(
* 'field' => $field,
* 'value' => $value,
* 'operator' => $operator,
* );
* @endcode
*
* In the special case that $operator is NULL, the $field is taken as a raw
* SQL snippet (possibly containing a function) and $value is an associative
* array of placeholders for the snippet.
*
* There will also be a single array entry of #conjunction, which is the
* conjunction that will be applied to the array, such as AND.
*/
public function &conditions();
/**
* Gets a complete list of all values to insert into the prepared statement.
*
* @return
* An associative array of placeholders and values.
*/
public function arguments();
/**
* Compiles the saved conditions for later retrieval.
*
* This method does not return anything, but simply prepares data to be
* retrieved via __toString() and arguments().
*
* @param $connection
* The database connection for which to compile the conditionals.
* @param $queryPlaceholder
* The query this condition belongs to. If not given, the current query is
* used.
*/
public function compile(Connection $connection, PlaceholderInterface $queryPlaceholder);
/**
* Check whether a condition has been previously compiled.
*
* @return
* TRUE if the condition has been previously compiled.
*/
public function compiled();
/**
* Creates an object holding a group of conditions.
*
* See andConditionGroup() and orConditionGroup() for more.
*
* @param $conjunction
* - AND (default): this is the equivalent of andConditionGroup().
* - OR: this is the equivalent of andConditionGroup().
*
* @return \Drupal\Core\Database\Query\ConditionInterface
* An object holding a group of conditions.
*/
public function conditionGroupFactory($conjunction = 'AND');
/**
* Creates a new group of conditions ANDed together.
*
* @return \Drupal\Core\Database\Query\ConditionInterface
*/
public function andConditionGroup();
/**
* Creates a new group of conditions ORed together.
*
* @return \Drupal\Core\Database\Query\ConditionInterface
*/
public function orConditionGroup();
}

View file

@ -0,0 +1,166 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Query\Delete.
*/
namespace Drupal\Core\Database\Query;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Connection;
/**
* General class for an abstracted DELETE operation.
*
* @ingroup database
*/
class Delete extends Query implements ConditionInterface {
/**
* The table from which to delete.
*
* @var string
*/
protected $table;
/**
* The condition object for this query.
*
* Condition handling is handled via composition.
*
* @var Condition
*/
protected $condition;
/**
* Constructs a Delete object.
*
* @param \Drupal\Core\Database\Connection $connection
* A Connection object.
* @param string $table
* Name of the table to associate with this query.
* @param array $options
* Array of database options.
*/
public function __construct(Connection $connection, $table, array $options = array()) {
$options['return'] = Database::RETURN_AFFECTED;
parent::__construct($connection, $options);
$this->table = $table;
$this->condition = new Condition('AND');
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::condition().
*/
public function condition($field, $value = NULL, $operator = '=') {
$this->condition->condition($field, $value, $operator);
return $this;
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::isNull().
*/
public function isNull($field) {
$this->condition->isNull($field);
return $this;
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::isNotNull().
*/
public function isNotNull($field) {
$this->condition->isNotNull($field);
return $this;
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::exists().
*/
public function exists(SelectInterface $select) {
$this->condition->exists($select);
return $this;
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::notExists().
*/
public function notExists(SelectInterface $select) {
$this->condition->notExists($select);
return $this;
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::conditions().
*/
public function &conditions() {
return $this->condition->conditions();
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::arguments().
*/
public function arguments() {
return $this->condition->arguments();
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::where().
*/
public function where($snippet, $args = array()) {
$this->condition->where($snippet, $args);
return $this;
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::compile().
*/
public function compile(Connection $connection, PlaceholderInterface $queryPlaceholder) {
return $this->condition->compile($connection, $queryPlaceholder);
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::compiled().
*/
public function compiled() {
return $this->condition->compiled();
}
/**
* Executes the DELETE query.
*
* @return
* The return value is dependent on the database connection.
*/
public function execute() {
$values = array();
if (count($this->condition)) {
$this->condition->compile($this->connection, $this);
$values = $this->condition->arguments();
}
return $this->connection->query((string) $this, $values, $this->queryOptions);
}
/**
* Implements PHP magic __toString method to convert the query to a string.
*
* @return string
* The prepared statement.
*/
public function __toString() {
// Create a sanitized comment string to prepend to the query.
$comments = $this->connection->makeComment($this->comments);
$query = $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '} ';
if (count($this->condition)) {
$this->condition->compile($this->connection, $this);
$query .= "\nWHERE " . $this->condition;
}
return $query;
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Query\ExtendableInterface.
*/
namespace Drupal\Core\Database\Query;
/**
* Interface for extendable query objects.
*
* "Extenders" follow the "Decorator" OOP design pattern. That is, they wrap
* and "decorate" another object. In our case, they implement the same interface
* as select queries and wrap a select query, to which they delegate almost all
* operations. Subclasses of this class may implement additional methods or
* override existing methods as appropriate. Extenders may also wrap other
* extender objects, allowing for arbitrarily complex "enhanced" queries.
*/
interface ExtendableInterface {
/**
* Enhance this object by wrapping it in an extender object.
*
* @param $extender_name
* The base name of the extending class. The base name will be checked
* against the current database connection to allow driver-specific subclasses
* as well, using the same logic as the query objects themselves.
* @return \Drupal\Core\Database\Query\ExtendableInterface
* The extender object, which now contains a reference to this object.
*/
public function extend($extender_name);
}

View file

@ -0,0 +1,18 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Query\FieldsOverlapException.
*/
namespace Drupal\Core\Database\Query;
use Drupal\Core\Database\DatabaseException;
/**
* Exception thrown if an insert query specifies a field twice.
*
* It is not allowed to specify a field as default and insert field, this
* exception is thrown if that is the case.
*/
class FieldsOverlapException extends \InvalidArgumentException implements DatabaseException {}

View file

@ -0,0 +1,304 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Query\Insert.
*/
namespace Drupal\Core\Database\Query;
use Drupal\Core\Database\Database;
/**
* General class for an abstracted INSERT query.
*
* @ingroup database
*/
class Insert extends Query {
/**
* The table on which to insert.
*
* @var string
*/
protected $table;
/**
* An array of fields on which to insert.
*
* @var array
*/
protected $insertFields = array();
/**
* An array of fields that should be set to their database-defined defaults.
*
* @var array
*/
protected $defaultFields = array();
/**
* A nested array of values to insert.
*
* $insertValues is an array of arrays. Each sub-array is either an
* associative array whose keys are field names and whose values are field
* values to insert, or a non-associative array of values in the same order
* as $insertFields.
*
* Whether multiple insert sets will be run in a single query or multiple
* queries is left to individual drivers to implement in whatever manner is
* most appropriate. The order of values in each sub-array must match the
* order of fields in $insertFields.
*
* @var array
*/
protected $insertValues = array();
/**
* A SelectQuery object to fetch the rows that should be inserted.
*
* @var \Drupal\Core\Database\Query\SelectInterface
*/
protected $fromQuery;
/**
* Constructs an Insert object.
*
* @param \Drupal\Core\Database\Connection $connection
* A Connection object.
* @param string $table
* Name of the table to associate with this query.
* @param array $options
* Array of database options.
*/
public function __construct($connection, $table, array $options = array()) {
if (!isset($options['return'])) {
$options['return'] = Database::RETURN_INSERT_ID;
}
parent::__construct($connection, $options);
$this->table = $table;
}
/**
* Adds a set of field->value pairs to be inserted.
*
* This method may only be called once. Calling it a second time will be
* ignored. To queue up multiple sets of values to be inserted at once,
* use the values() method.
*
* @param $fields
* An array of fields on which to insert. This array may be indexed or
* associative. If indexed, the array is taken to be the list of fields.
* If associative, the keys of the array are taken to be the fields and
* the values are taken to be corresponding values to insert. If a
* $values argument is provided, $fields must be indexed.
* @param $values
* An array of fields to insert into the database. The values must be
* specified in the same order as the $fields array.
*
* @return \Drupal\Core\Database\Query\Insert
* The called object.
*/
public function fields(array $fields, array $values = array()) {
if (empty($this->insertFields)) {
if (empty($values)) {
if (!is_numeric(key($fields))) {
$values = array_values($fields);
$fields = array_keys($fields);
}
}
$this->insertFields = $fields;
if (!empty($values)) {
$this->insertValues[] = $values;
}
}
return $this;
}
/**
* Adds another set of values to the query to be inserted.
*
* If $values is a numeric-keyed array, it will be assumed to be in the same
* order as the original fields() call. If it is associative, it may be
* in any order as long as the keys of the array match the names of the
* fields.
*
* @param $values
* An array of values to add to the query.
*
* @return \Drupal\Core\Database\Query\Insert
* The called object.
*/
public function values(array $values) {
if (is_numeric(key($values))) {
$this->insertValues[] = $values;
}
else {
// Reorder the submitted values to match the fields array.
foreach ($this->insertFields as $key) {
$insert_values[$key] = $values[$key];
}
// For consistency, the values array is always numerically indexed.
$this->insertValues[] = array_values($insert_values);
}
return $this;
}
/**
* Specifies fields for which the database defaults should be used.
*
* If you want to force a given field to use the database-defined default,
* not NULL or undefined, use this method to instruct the database to use
* default values explicitly. In most cases this will not be necessary
* unless you are inserting a row that is all default values, as you cannot
* specify no values in an INSERT query.
*
* Specifying a field both in fields() and in useDefaults() is an error
* and will not execute.
*
* @param $fields
* An array of values for which to use the default values
* specified in the table definition.
*
* @return \Drupal\Core\Database\Query\Insert
* The called object.
*/
public function useDefaults(array $fields) {
$this->defaultFields = $fields;
return $this;
}
/**
* Sets the fromQuery on this InsertQuery object.
*
* @param \Drupal\Core\Database\Query\SelectInterface $query
* The query to fetch the rows that should be inserted.
*
* @return \Drupal\Core\Database\Query\Insert
* The called object.
*/
public function from(SelectInterface $query) {
$this->fromQuery = $query;
return $this;
}
/**
* Executes the insert query.
*
* @return
* The last insert ID of the query, if one exists. If the query was given
* multiple sets of values to insert, the return value is undefined. If no
* fields are specified, this method will do nothing and return NULL. That
* That makes it safe to use in multi-insert loops.
*/
public function execute() {
// If validation fails, simply return NULL. Note that validation routines
// in preExecute() may throw exceptions instead.
if (!$this->preExecute()) {
return NULL;
}
// 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)) {
$sql = (string) $this;
// The SelectQuery may contain arguments, load and pass them through.
return $this->connection->query($sql, $this->fromQuery->getArguments(), $this->queryOptions);
}
$last_insert_id = 0;
// Each insert happens in its own query in the degenerate case. However,
// we wrap it in a transaction so that it is atomic where possible. On many
// databases, such as SQLite, this is also a notable performance boost.
$transaction = $this->connection->startTransaction();
try {
$sql = (string) $this;
foreach ($this->insertValues as $insert_values) {
$last_insert_id = $this->connection->query($sql, $insert_values, $this->queryOptions);
}
}
catch (\Exception $e) {
// One of the INSERTs failed, rollback the whole batch.
$transaction->rollback();
// Rethrow the exception for the calling code.
throw $e;
}
// Re-initialize the values array so that we can re-use this query.
$this->insertValues = array();
// Transaction commits here where $transaction looses scope.
return $last_insert_id;
}
/**
* Implements PHP magic __toString method to convert the query to a string.
*
* @return string
* The prepared statement.
*/
public function __toString() {
// Create a sanitized comment string to prepend to the query.
$comments = $this->connection->makeComment($this->comments);
// Default fields are always placed first for consistency.
$insert_fields = array_merge($this->defaultFields, $this->insertFields);
if (!empty($this->fromQuery)) {
return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') ' . $this->fromQuery;
}
// For simplicity, we will use the $placeholders array to inject
// default keywords even though they are not, strictly speaking,
// placeholders for prepared statements.
$placeholders = array();
$placeholders = array_pad($placeholders, count($this->defaultFields), 'default');
$placeholders = array_pad($placeholders, count($this->insertFields), '?');
return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES (' . implode(', ', $placeholders) . ')';
}
/**
* Preprocesses and validates the query.
*
* @return
* TRUE if the validation was successful, FALSE if not.
*
* @throws \Drupal\Core\Database\Query\FieldsOverlapException
* @throws \Drupal\Core\Database\Query\NoFieldsException
*/
public function preExecute() {
// Confirm that the user did not try to specify an identical
// field and default field.
if (array_intersect($this->insertFields, $this->defaultFields)) {
throw new FieldsOverlapException('You may not specify the same field to have a value and a schema-default value.');
}
if (!empty($this->fromQuery)) {
// We have to assume that the used aliases match the insert fields.
// Regular fields are added to the query before expressions, maintain the
// same order for the insert fields.
// This behavior can be overridden by calling fields() manually as only the
// first call to fields() does have an effect.
$this->fields(array_merge(array_keys($this->fromQuery->getFields()), array_keys($this->fromQuery->getExpressions())));
}
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
// if values are added conditionally, so we don't want to throw an
// exception.
if (!isset($this->insertValues[0]) && count($this->insertFields) > 0 && empty($this->fromQuery)) {
return FALSE;
}
return TRUE;
}
}

View file

@ -0,0 +1,18 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Query\InvalidMergeQueryException.
*/
namespace Drupal\Core\Database\Query;
use Drupal\Core\Database\DatabaseException;
/**
* Exception thrown for merge queries that do not make semantic sense.
*
* There are many ways that a merge query could be malformed. They should all
* throw this exception and set an appropriately descriptive message.
*/
class InvalidMergeQueryException extends \InvalidArgumentException implements DatabaseException {}

View file

@ -0,0 +1,484 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Query\Merge.
*/
namespace Drupal\Core\Database\Query;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\IntegrityConstraintViolationException;
/**
* General class for an abstracted MERGE query operation.
*
* An ANSI SQL:2003 compatible database would run the following query:
*
* @code
* MERGE INTO table_name_1 USING table_name_2 ON (condition)
* WHEN MATCHED THEN
* UPDATE SET column1 = value1 [, column2 = value2 ...]
* WHEN NOT MATCHED THEN
* INSERT (column1 [, column2 ...]) VALUES (value1 [, value2 ...
* @endcode
*
* Other databases (most notably MySQL, PostgreSQL and SQLite) will emulate
* this statement by running a SELECT and then INSERT or UPDATE.
*
* By default, the two table names are identical and they are passed into the
* the constructor. table_name_2 can be specified by the
* MergeQuery::conditionTable() method. It can be either a string or a
* subquery.
*
* The condition is built exactly like SelectQuery or UpdateQuery conditions,
* the UPDATE query part is built similarly like an UpdateQuery and finally the
* INSERT query part is built similarly like an InsertQuery. However, both
* UpdateQuery and InsertQuery has a fields method so
* MergeQuery::updateFields() and MergeQuery::insertFields() needs to be called
* instead. MergeQuery::fields() can also be called which calls both of these
* methods as the common case is to use the same column-value pairs for both
* INSERT and UPDATE. However, this is not mandatory. Another convenient
* wrapper is MergeQuery::key() which adds the same column-value pairs to the
* condition and the INSERT query part.
*
* Several methods (key(), fields(), insertFields()) can be called to set a
* key-value pair for the INSERT query part. Subsequent calls for the same
* fields override the earlier ones. The same is true for UPDATE and key(),
* fields() and updateFields().
*/
class Merge extends Query implements ConditionInterface {
/**
* Returned by execute() if an INSERT query has been executed.
*/
const STATUS_INSERT = 1;
/**
* Returned by execute() if an UPDATE query has been executed.
*/
const STATUS_UPDATE = 2;
/**
* The table to be used for INSERT and UPDATE.
*
* @var string
*/
protected $table;
/**
* The table or subquery to be used for the condition.
*/
protected $conditionTable;
/**
* An array of fields on which to insert.
*
* @var array
*/
protected $insertFields = array();
/**
* An array of fields which should be set to their database-defined defaults.
*
* Used on INSERT.
*
* @var array
*/
protected $defaultFields = array();
/**
* An array of values to be inserted.
*
* @var string
*/
protected $insertValues = array();
/**
* An array of fields that will be updated.
*
* @var array
*/
protected $updateFields = array();
/**
* Array of fields to update to an expression in case of a duplicate record.
*
* This variable is a nested array in the following format:
* @code
* <some field> => array(
* 'condition' => <condition to execute, as a string>,
* 'arguments' => <array of arguments for condition, or NULL for none>,
* );
* @endcode
*
* @var array
*/
protected $expressionFields = array();
/**
* Flag indicating whether an UPDATE is necessary.
*
* @var bool
*/
protected $needsUpdate = FALSE;
/**
* Constructs a Merge object.
*
* @param \Drupal\Core\Database\Connection $connection
* A Connection object.
* @param string $table
* Name of the table to associate with this query.
* @param array $options
* Array of database options.
*/
public function __construct(Connection $connection, $table, array $options = array()) {
$options['return'] = Database::RETURN_AFFECTED;
parent::__construct($connection, $options);
$this->table = $table;
$this->conditionTable = $table;
$this->condition = new Condition('AND');
}
/**
* Sets the table or subquery to be used for the condition.
*
* @param $table
* The table name or the subquery to be used. Use a Select query object to
* pass in a subquery.
*
* @return \Drupal\Core\Database\Query\Merge
* The called object.
*/
protected function conditionTable($table) {
$this->conditionTable = $table;
return $this;
}
/**
* Adds a set of field->value pairs to be updated.
*
* @param $fields
* An associative array of fields to write into the database. The array keys
* are the field names and the values are the values to which to set them.
*
* @return \Drupal\Core\Database\Query\Merge
* The called object.
*/
public function updateFields(array $fields) {
$this->updateFields = $fields;
$this->needsUpdate = TRUE;
return $this;
}
/**
* Specifies fields to be updated as an expression.
*
* Expression fields are cases such as counter = counter + 1. This method
* takes precedence over MergeQuery::updateFields() and it's wrappers,
* MergeQuery::key() and MergeQuery::fields().
*
* @param $field
* The field to set.
* @param $expression
* The field will be set to the value of this expression. This parameter
* may include named placeholders.
* @param $arguments
* If specified, this is an array of key/value pairs for named placeholders
* corresponding to the expression.
*
* @return \Drupal\Core\Database\Query\Merge
* The called object.
*/
public function expression($field, $expression, array $arguments = NULL) {
$this->expressionFields[$field] = array(
'expression' => $expression,
'arguments' => $arguments,
);
$this->needsUpdate = TRUE;
return $this;
}
/**
* Adds a set of field->value pairs to be inserted.
*
* @param $fields
* An array of fields on which to insert. This array may be indexed or
* associative. If indexed, the array is taken to be the list of fields.
* If associative, the keys of the array are taken to be the fields and
* the values are taken to be corresponding values to insert. If a
* $values argument is provided, $fields must be indexed.
* @param $values
* An array of fields to insert into the database. The values must be
* specified in the same order as the $fields array.
*
* @return \Drupal\Core\Database\Query\Merge
* The called object.
*/
public function insertFields(array $fields, array $values = array()) {
if ($values) {
$fields = array_combine($fields, $values);
}
$this->insertFields = $fields;
return $this;
}
/**
* Specifies fields for which the database-defaults should be used.
*
* If you want to force a given field to use the database-defined default,
* not NULL or undefined, use this method to instruct the database to use
* default values explicitly. In most cases this will not be necessary
* unless you are inserting a row that is all default values, as you cannot
* specify no values in an INSERT query.
*
* Specifying a field both in fields() and in useDefaults() is an error
* and will not execute.
*
* @param $fields
* An array of values for which to use the default values
* specified in the table definition.
*
* @return \Drupal\Core\Database\Query\Merge
* The called object.
*/
public function useDefaults(array $fields) {
$this->defaultFields = $fields;
return $this;
}
/**
* Sets common field-value pairs in the INSERT and UPDATE query parts.
*
* This method should only be called once. It may be called either
* with a single associative array or two indexed arrays. If called
* with an associative array, the keys are taken to be the fields
* and the values are taken to be the corresponding values to set.
* If called with two arrays, the first array is taken as the fields
* and the second array is taken as the corresponding values.
*
* @param $fields
* An array of fields to insert, or an associative array of fields and
* values. The keys of the array are taken to be the fields and the values
* are taken to be corresponding values to insert.
* @param $values
* An array of values to set into the database. The values must be
* specified in the same order as the $fields array.
*
* @return \Drupal\Core\Database\Query\Merge
* The called object.
*/
public function fields(array $fields, array $values = array()) {
if ($values) {
$fields = array_combine($fields, $values);
}
foreach ($fields as $key => $value) {
$this->insertFields[$key] = $value;
$this->updateFields[$key] = $value;
}
$this->needsUpdate = TRUE;
return $this;
}
/**
* Sets the key fields to be used as conditions for this query.
*
* This method should only be called once. It may be called either
* with a single associative array or two indexed arrays. If called
* with an associative array, the keys are taken to be the fields
* and the values are taken to be the corresponding values to set.
* If called with two arrays, the first array is taken as the fields
* and the second array is taken as the corresponding values.
*
* The fields are copied to the condition of the query and the INSERT part.
* If no other method is called, the UPDATE will become a no-op.
*
* @param $fields
* An array of fields to set, or an associative array of fields and values.
* @param $values
* An array of values to set into the database. The values must be
* specified in the same order as the $fields array.
*
* @return $this
*/
public function keys(array $fields, array $values = array()) {
if ($values) {
$fields = array_combine($fields, $values);
}
foreach ($fields as $key => $value) {
$this->insertFields[$key] = $value;
$this->condition($key, $value);
}
return $this;
}
/**
* Sets a single key field to be used as condition for this query.
*
* Same as \Drupal\Core\Database\Query\Merge::keys() but offering a signature
* that is more natural for the case of a single key.
*
* @param string $field
* The name of the field to set.
* @param mixed $value
* The value to set into the database.
*
* @return $this
*
* @see \Drupal\Core\Database\Query\Merge::keys()
*/
public function key($field, $value = NULL) {
// @todo D9: Remove this backwards-compatibility shim.
if (is_array($field)) {
$this->keys($field, isset($value) ? $value : array());
}
else {
$this->keys(array($field => $value));
}
return $this;
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::condition().
*/
public function condition($field, $value = NULL, $operator = '=') {
$this->condition->condition($field, $value, $operator);
return $this;
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::isNull().
*/
public function isNull($field) {
$this->condition->isNull($field);
return $this;
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::isNotNull().
*/
public function isNotNull($field) {
$this->condition->isNotNull($field);
return $this;
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::exists().
*/
public function exists(SelectInterface $select) {
$this->condition->exists($select);
return $this;
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::notExists().
*/
public function notExists(SelectInterface $select) {
$this->condition->notExists($select);
return $this;
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::conditions().
*/
public function &conditions() {
return $this->condition->conditions();
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::arguments().
*/
public function arguments() {
return $this->condition->arguments();
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::where().
*/
public function where($snippet, $args = array()) {
$this->condition->where($snippet, $args);
return $this;
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::compile().
*/
public function compile(Connection $connection, PlaceholderInterface $queryPlaceholder) {
return $this->condition->compile($connection, $queryPlaceholder);
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::compiled().
*/
public function compiled() {
return $this->condition->compiled();
}
/**
* Implements PHP magic __toString method to convert the query to a string.
*
* In the degenerate case, there is no string-able query as this operation
* is potentially two queries.
*
* @return string
* The prepared query statement.
*/
public function __toString() {
}
public function execute() {
// Default options for merge queries.
$this->queryOptions += array(
'throw_exception' => TRUE,
);
try {
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;
}
catch (IntegrityConstraintViolationException $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 ($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;
}
}
catch (\Exception $e) {
if ($this->queryOptions['throw_exception']) {
throw $e;
}
else {
return NULL;
}
}
}
}

View file

@ -0,0 +1,15 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Query\NoFieldsException.
*/
namespace Drupal\Core\Database\Query;
use Drupal\Core\Database\DatabaseException;
/**
* Exception thrown if an insert query doesn't specify insert or default fields.
*/
class NoFieldsException extends \InvalidArgumentException implements DatabaseException {}

View file

@ -0,0 +1,177 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Query\PagerSelectExtender.
*/
namespace Drupal\Core\Database\Query;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\SelectExtender;
use Drupal\Core\Database\Query\SelectInterface;
/**
* Query extender for pager queries.
*
* This is the "default" pager mechanism. It creates a paged query with a fixed
* number of entries per page.
*
* When adding this extender along with other extenders, be sure to add
* PagerSelectExtender last, so that its range and count are based on the full
* query.
*/
class PagerSelectExtender extends SelectExtender {
/**
* The highest element we've autogenerated so far.
*
* @var int
*/
static $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 \Drupal\Core\Database\Query\SelectInterface
*/
protected $customCountQuery = FALSE;
public function __construct(SelectInterface $query, Connection $connection) {
parent::__construct($query, $connection);
// Add pager tag. Do this here to ensure that it is always added before
// preExecute() is called.
$this->addTag('pager');
}
/**
* Override the execute method.
*
* Before we run the query, we need to add pager-based range() instructions
* to it.
*/
public function execute() {
// 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();
$total_items = $this->getCountQuery()->execute()->fetchField();
$current_page = pager_default_initialize($total_items, $this->limit, $this->element);
$this->range($current_page * $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.
* If an element was not specified previously, then the value of the
* $maxElement counter is taken, after which the counter is incremented.
*
* After running this method, access $this->element to get the element for this
* query.
*/
protected function ensureElement() {
if (!isset($this->element)) {
$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 \Drupal\Core\Database\Query\SelectInterface $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(SelectInterface $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 \Drupal\Core\Database\Query\SelectInterface
* A count query object.
*/
public 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 int|false $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.
*
* Setting the element here also increments the static $maxElement counter,
* which is used for determining the $element when there's none specified.
*
* Note that no collision detection is done when setting an element ID
* explicitly, so it is possible for two pagers to end up using the same ID
* if both are set explicitly.
*
* @param $element
* Element ID that is used to differentiate different pager queries.
*/
public function element($element) {
$this->element = $element;
if ($element >= self::$maxElement) {
self::$maxElement = $element + 1;
}
return $this;
}
}

View file

@ -0,0 +1,27 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Query\PlaceholderInterface.
*/
namespace Drupal\Core\Database\Query;
/**
* Interface for a query that accepts placeholders.
*/
interface PlaceholderInterface {
/**
* Returns a unique identifier for this object.
*/
public function uniqueIdentifier();
/**
* Returns the next placeholder ID for the query.
*
* @return
* The next available placeholder ID as an integer.
*/
public function nextPlaceholder();
}

View file

@ -0,0 +1,204 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Query\Query.
*/
namespace Drupal\Core\Database\Query;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Connection;
/**
* Base class for query builders.
*
* Note that query builders use PHP's magic __toString() method to compile the
* query object into a prepared statement.
*/
abstract class Query implements PlaceholderInterface {
/**
* The connection object on which to run this query.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* The target of the connection object.
*
* @var string
*/
protected $connectionTarget;
/**
* The key of the connection object.
*
* @var string
*/
protected $connectionKey;
/**
* The query options to pass on to the connection object.
*
* @var array
*/
protected $queryOptions;
/**
* A unique identifier for this query object.
*/
protected $uniqueIdentifier;
/**
* The placeholder counter.
*/
protected $nextPlaceholder = 0;
/**
* An array of comments that can be prepended to a query.
*
* @var array
*/
protected $comments = array();
/**
* Constructs a Query object.
*
* @param \Drupal\Core\Database\Connection $connection
* Database connection object.
* @param array $options
* Array of query options.
*/
public function __construct(Connection $connection, $options) {
$this->uniqueIdentifier = uniqid('', TRUE);
$this->connection = $connection;
$this->connectionKey = $this->connection->getKey();
$this->connectionTarget = $this->connection->getTarget();
$this->queryOptions = $options;
}
/**
* Implements the magic __sleep function to disconnect from the database.
*/
public function __sleep() {
$keys = get_object_vars($this);
unset($keys['connection']);
return array_keys($keys);
}
/**
* Implements the magic __wakeup function to reconnect to the database.
*/
public function __wakeup() {
$this->connection = Database::getConnection($this->connectionTarget, $this->connectionKey);
}
/**
* Implements the magic __clone function.
*/
public function __clone() {
$this->uniqueIdentifier = uniqid('', TRUE);
}
/**
* Runs the query against the database.
*
* @return \Drupal\Core\Database\StatementInterface|null
* A prepared statement, or NULL if the query is not valid.
*/
abstract protected function execute();
/**
* Implements PHP magic __toString method to convert the query to a string.
*
* The toString operation is how we compile a query object to a prepared
* statement.
*
* @return string
* A prepared statement query string for this object.
*/
abstract public function __toString();
/**
* Returns a unique identifier for this object.
*/
public function uniqueIdentifier() {
return $this->uniqueIdentifier;
}
/**
* Gets the next placeholder value for this query object.
*
* @return int
* The next placeholder value.
*/
public function nextPlaceholder() {
return $this->nextPlaceholder++;
}
/**
* Adds a comment to the query.
*
* By adding a comment to a query, you can more easily find it in your
* query log or the list of active queries on an SQL server. This allows
* for easier debugging and allows you to more easily find where a query
* with a performance problem is being generated.
*
* The comment string will be sanitized to remove * / and other characters
* that may terminate the string early so as to avoid SQL injection attacks.
*
* @param $comment
* The comment string to be inserted into the query.
*
* @return $this
*/
public function comment($comment) {
$this->comments[] = $comment;
return $this;
}
/**
* Returns a reference to the comments array for the query.
*
* Because this method returns by reference, alter hooks may edit the comments
* array directly to make their changes. If just adding comments, however, the
* use of comment() is preferred.
*
* Note that this method must be called by reference as well:
* @code
* $comments =& $query->getComments();
* @endcode
*
* @return array
* A reference to the comments array structure.
*/
public function &getComments() {
return $this->comments;
}
/**
* {@inheritdoc}
*/
public function conditionGroupFactory($conjunction = 'AND') {
return new Condition($conjunction);
}
/**
* {@inheritdoc}
*/
public function andConditionGroup() {
return $this->conditionGroupFactory('AND');
}
/**
* {@inheritdoc}
*/
public function orConditionGroup() {
return $this->conditionGroupFactory('OR');
}
}

View file

@ -0,0 +1,953 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Query\Select.
*/
namespace Drupal\Core\Database\Query;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Connection;
/**
* Query builder for SELECT statements.
*
* @ingroup database
*/
class Select extends Query implements SelectInterface {
/**
* The fields to SELECT.
*
* @var array
*/
protected $fields = array();
/**
* The expressions to SELECT as virtual fields.
*
* @var array
*/
protected $expressions = array();
/**
* The tables against which to JOIN.
*
* This property is a nested array. Each entry is an array representing
* a single table against which to join. The structure of each entry is:
*
* array(
* 'type' => $join_type (one of INNER, LEFT OUTER, RIGHT OUTER),
* 'table' => $table,
* 'alias' => $alias_of_the_table,
* 'condition' => $condition_clause_on_which_to_join,
* 'arguments' => $array_of_arguments_for_placeholders_in_the condition.
* 'all_fields' => TRUE to SELECT $alias.*, FALSE or NULL otherwise.
* )
*
* If $table is a string, it is taken as the name of a table. If it is
* a Select query object, it is taken as a subquery.
*
* @var array
*/
protected $tables = array();
/**
* The fields by which to order this query.
*
* This is an associative array. The keys are the fields to order, and the value
* is the direction to order, either ASC or DESC.
*
* @var array
*/
protected $order = array();
/**
* The fields by which to group.
*
* @var array
*/
protected $group = array();
/**
* The conditional object for the WHERE clause.
*
* @var \Drupal\Core\Database\Query\Condition
*/
protected $where;
/**
* The conditional object for the HAVING clause.
*
* @var \Drupal\Core\Database\Query\Condition
*/
protected $having;
/**
* Whether or not this query should be DISTINCT
*
* @var bool
*/
protected $distinct = FALSE;
/**
* The range limiters for this query.
*
* @var array
*/
protected $range;
/**
* An array whose elements specify a query to UNION, and the UNION type. The
* 'type' key may be '', 'ALL', or 'DISTINCT' to represent a 'UNION',
* 'UNION ALL', or 'UNION DISTINCT' statement, respectively.
*
* All entries in this array will be applied from front to back, with the
* first query to union on the right of the original query, the second union
* to the right of the first, etc.
*
* @var array
*/
protected $union = array();
/**
* Indicates if preExecute() has already been called.
* @var bool
*/
protected $prepared = FALSE;
/**
* The FOR UPDATE status
*/
protected $forUpdate = FALSE;
/**
* Constructs a Select object.
*
* @param string $table
* The name of the table that is being queried.
* @param string $alias
* The alias for the table.
* @param \Drupal\Core\Database\Connection $connection
* Database connection object.
* @param array $options
* Array of query options.
*/
public function __construct($table, $alias = NULL, Connection $connection, $options = array()) {
$options['return'] = Database::RETURN_STATEMENT;
parent::__construct($connection, $options);
$conjunction = isset($options['conjunction']) ? $options['conjunction'] : 'AND';
$this->where = new Condition($conjunction);
$this->having = new Condition($conjunction);
$this->addJoin(NULL, $table, $alias);
}
/**
* {@inheritdoc}
*/
public function addTag($tag) {
$this->alterTags[$tag] = 1;
return $this;
}
/**
* {@inheritdoc}
*/
public function hasTag($tag) {
return isset($this->alterTags[$tag]);
}
/**
* {@inheritdoc}
*/
public function hasAllTags() {
return !(boolean)array_diff(func_get_args(), array_keys($this->alterTags));
}
/**
* {@inheritdoc}
*/
public function hasAnyTag() {
return (boolean)array_intersect(func_get_args(), array_keys($this->alterTags));
}
/**
* {@inheritdoc}
*/
public function addMetaData($key, $object) {
$this->alterMetaData[$key] = $object;
return $this;
}
/**
* {@inheritdoc}
*/
public function getMetaData($key) {
return isset($this->alterMetaData[$key]) ? $this->alterMetaData[$key] : NULL;
}
/**
* {@inheritdoc}
*/
public function condition($field, $value = NULL, $operator = '=') {
$this->where->condition($field, $value, $operator);
return $this;
}
/**
* {@inheritdoc}
*/
public function &conditions() {
return $this->where->conditions();
}
/**
* {@inheritdoc}
*/
public function arguments() {
if (!$this->compiled()) {
return NULL;
}
$args = $this->where->arguments() + $this->having->arguments();
foreach ($this->tables as $table) {
if ($table['arguments']) {
$args += $table['arguments'];
}
// If this table is a subquery, grab its arguments recursively.
if ($table['table'] instanceof SelectInterface) {
$args += $table['table']->arguments();
}
}
foreach ($this->expressions as $expression) {
if ($expression['arguments']) {
$args += $expression['arguments'];
}
}
// If there are any dependent queries to UNION,
// incorporate their arguments recursively.
foreach ($this->union as $union) {
$args += $union['query']->arguments();
}
return $args;
}
/**
* {@inheritdoc}
*/
public function where($snippet, $args = array()) {
$this->where->where($snippet, $args);
return $this;
}
/**
* {@inheritdoc}
*/
public function isNull($field) {
$this->where->isNull($field);
return $this;
}
/**
* {@inheritdoc}
*/
public function isNotNull($field) {
$this->where->isNotNull($field);
return $this;
}
/**
* {@inheritdoc}
*/
public function exists(SelectInterface $select) {
$this->where->exists($select);
return $this;
}
/**
* {@inheritdoc}
*/
public function notExists(SelectInterface $select) {
$this->where->notExists($select);
return $this;
}
/**
* {@inheritdoc}
*/
public function compile(Connection $connection, PlaceholderInterface $queryPlaceholder) {
$this->where->compile($connection, $queryPlaceholder);
$this->having->compile($connection, $queryPlaceholder);
foreach ($this->tables as $table) {
// If this table is a subquery, compile it recursively.
if ($table['table'] instanceof SelectInterface) {
$table['table']->compile($connection, $queryPlaceholder);
}
}
// If there are any dependent queries to UNION, compile it recursively.
foreach ($this->union as $union) {
$union['query']->compile($connection, $queryPlaceholder);
}
}
/**
* {@inheritdoc}
*/
public function compiled() {
if (!$this->where->compiled() || !$this->having->compiled()) {
return FALSE;
}
foreach ($this->tables as $table) {
// If this table is a subquery, check its status recursively.
if ($table['table'] instanceof SelectInterface) {
if (!$table['table']->compiled()) {
return FALSE;
}
}
}
foreach ($this->union as $union) {
if (!$union['query']->compiled()) {
return FALSE;
}
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function havingCondition($field, $value = NULL, $operator = NULL) {
$this->having->condition($field, $value, $operator);
return $this;
}
/**
* {@inheritdoc}
*/
public function &havingConditions() {
return $this->having->conditions();
}
/**
* {@inheritdoc}
*/
public function havingArguments() {
return $this->having->arguments();
}
/**
* {@inheritdoc}
*/
public function having($snippet, $args = array()) {
$this->having->where($snippet, $args);
return $this;
}
/**
* {@inheritdoc}
*/
public function havingCompile(Connection $connection) {
$this->having->compile($connection, $this);
}
/**
* {@inheritdoc}
*/
public function extend($extender_name) {
$override_class = $extender_name . '_' . $this->connection->driver();
if (class_exists($override_class)) {
$extender_name = $override_class;
}
return new $extender_name($this, $this->connection);
}
/**
* {@inheritdoc}
*/
public function havingIsNull($field) {
$this->having->isNull($field);
return $this;
}
/**
* {@inheritdoc}
*/
public function havingIsNotNull($field) {
$this->having->isNotNull($field);
return $this;
}
/**
* {@inheritdoc}
*/
public function havingExists(SelectInterface $select) {
$this->having->exists($select);
return $this;
}
/**
* {@inheritdoc}
*/
public function havingNotExists(SelectInterface $select) {
$this->having->notExists($select);
return $this;
}
/**
* {@inheritdoc}
*/
public function forUpdate($set = TRUE) {
if (isset($set)) {
$this->forUpdate = $set;
}
return $this;
}
/**
* {@inheritdoc}
*/
public function &getFields() {
return $this->fields;
}
/**
* {@inheritdoc}
*/
public function &getExpressions() {
return $this->expressions;
}
/**
* {@inheritdoc}
*/
public function &getOrderBy() {
return $this->order;
}
/**
* {@inheritdoc}
*/
public function &getGroupBy() {
return $this->group;
}
/**
* {@inheritdoc}
*/
public function &getTables() {
return $this->tables;
}
/**
* {@inheritdoc}
*/
public function &getUnion() {
return $this->union;
}
/**
* {@inheritdoc}
*/
public function escapeLike($string) {
return $this->connection->escapeLike($string);
}
/**
* {@inheritdoc}
*/
public function getArguments(PlaceholderInterface $queryPlaceholder = NULL) {
if (!isset($queryPlaceholder)) {
$queryPlaceholder = $this;
}
$this->compile($this->connection, $queryPlaceholder);
return $this->arguments();
}
/**
* {@inheritdoc}
*/
public function isPrepared() {
return $this->prepared;
}
/**
* {@inheritdoc}
*/
public function preExecute(SelectInterface $query = NULL) {
// If no query object is passed in, use $this.
if (!isset($query)) {
$query = $this;
}
// Only execute this once.
if ($query->isPrepared()) {
return TRUE;
}
// Modules may alter all queries or only those having a particular tag.
if (isset($this->alterTags)) {
$hooks = array('query');
foreach ($this->alterTags as $tag => $value) {
$hooks[] = 'query_' . $tag;
}
\Drupal::moduleHandler()->alter($hooks, $query);
}
$this->prepared = TRUE;
// Now also prepare any sub-queries.
foreach ($this->tables as $table) {
if ($table['table'] instanceof SelectInterface) {
$table['table']->preExecute();
}
}
foreach ($this->union as $union) {
$union['query']->preExecute();
}
return $this->prepared;
}
/**
* {@inheritdoc}
*/
public function execute() {
// If validation fails, simply return NULL.
// Note that validation routines in preExecute() may throw exceptions instead.
if (!$this->preExecute()) {
return NULL;
}
$args = $this->getArguments();
return $this->connection->query((string) $this, $args, $this->queryOptions);
}
/**
* {@inheritdoc}
*/
public function distinct($distinct = TRUE) {
$this->distinct = $distinct;
return $this;
}
/**
* {@inheritdoc}
*/
public function addField($table_alias, $field, $alias = NULL) {
// If no alias is specified, first try the field name itself.
if (empty($alias)) {
$alias = $field;
}
// If that's already in use, try the table name and field name.
if (!empty($this->fields[$alias])) {
$alias = $table_alias . '_' . $field;
}
// If that is already used, just add a counter until we find an unused alias.
$alias_candidate = $alias;
$count = 2;
while (!empty($this->fields[$alias_candidate])) {
$alias_candidate = $alias . '_' . $count++;
}
$alias = $alias_candidate;
$this->fields[$alias] = array(
'field' => $field,
'table' => $table_alias,
'alias' => $alias,
);
return $alias;
}
/**
* {@inheritdoc}
*/
public function fields($table_alias, array $fields = array()) {
if ($fields) {
foreach ($fields as $field) {
// We don't care what alias was assigned.
$this->addField($table_alias, $field);
}
}
else {
// We want all fields from this table.
$this->tables[$table_alias]['all_fields'] = TRUE;
}
return $this;
}
/**
* {@inheritdoc}
*/
public function addExpression($expression, $alias = NULL, $arguments = array()) {
if (empty($alias)) {
$alias = 'expression';
}
$alias_candidate = $alias;
$count = 2;
while (!empty($this->expressions[$alias_candidate])) {
$alias_candidate = $alias . '_' . $count++;
}
$alias = $alias_candidate;
$this->expressions[$alias] = array(
'expression' => $expression,
'alias' => $alias,
'arguments' => $arguments,
);
return $alias;
}
/**
* {@inheritdoc}
*/
public function join($table, $alias = NULL, $condition = NULL, $arguments = array()) {
return $this->addJoin('INNER', $table, $alias, $condition, $arguments);
}
/**
* {@inheritdoc}
*/
public function innerJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) {
return $this->addJoin('INNER', $table, $alias, $condition, $arguments);
}
/**
* {@inheritdoc}
*/
public function leftJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) {
return $this->addJoin('LEFT OUTER', $table, $alias, $condition, $arguments);
}
/**
* {@inheritdoc}
*/
public function rightJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) {
return $this->addJoin('RIGHT OUTER', $table, $alias, $condition, $arguments);
}
/**
* {@inheritdoc}
*/
public function addJoin($type, $table, $alias = NULL, $condition = NULL, $arguments = array()) {
if (empty($alias)) {
if ($table instanceof SelectInterface) {
$alias = 'subquery';
}
else {
$alias = $table;
}
}
$alias_candidate = $alias;
$count = 2;
while (!empty($this->tables[$alias_candidate])) {
$alias_candidate = $alias . '_' . $count++;
}
$alias = $alias_candidate;
if (is_string($condition)) {
$condition = str_replace('%alias', $alias, $condition);
}
$this->tables[$alias] = array(
'join type' => $type,
'table' => $table,
'alias' => $alias,
'condition' => $condition,
'arguments' => $arguments,
);
return $alias;
}
/**
* {@inheritdoc}
*/
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;
}
/**
* {@inheritdoc}
*/
public function orderRandom() {
$alias = $this->addExpression('RAND()', 'random_field');
$this->orderBy($alias);
return $this;
}
/**
* {@inheritdoc}
*/
public function range($start = NULL, $length = NULL) {
$this->range = func_num_args() ? array('start' => $start, 'length' => $length) : array();
return $this;
}
/**
* {@inheritdoc}
*/
public function union(SelectInterface $query, $type = '') {
// Handle UNION aliasing.
switch ($type) {
// Fold UNION DISTINCT to UNION for better cross database support.
case 'DISTINCT':
case '':
$type = 'UNION';
break;
case 'ALL':
$type = 'UNION ALL';
default:
}
$this->union[] = array(
'type' => $type,
'query' => $query,
);
return $this;
}
/**
* {@inheritdoc}
*/
public function groupBy($field) {
$this->group[$field] = $field;
return $this;
}
/**
* {@inheritdoc}
*/
public function countQuery() {
$count = $this->prepareCountQuery();
$query = $this->connection->select($count, NULL, $this->queryOptions);
$query->addExpression('COUNT(*)');
return $query;
}
/**
* Prepares a count query from the current query object.
*
* @return \Drupal\Core\Database\Query\Select
* A new query object ready to have COUNT(*) performed on it.
*/
protected function prepareCountQuery() {
// Create our new query object that we will mutate into a count query.
$count = clone($this);
$group_by = $count->getGroupBy();
$having = $count->havingConditions();
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 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])) {
unset($expressions[$field]);
}
}
// Also remove 'all_fields' statements, which are expanded into tablename.*
// when the query is executed.
foreach ($count->tables as &$table) {
unset($table['all_fields']);
}
}
// If we've just removed all fields from the query, make sure there is at
// least one so that the query still runs.
$count->addExpression('1');
// Ordering a count query is a waste of cycles, and breaks on some
// databases anyway.
$orders = &$count->getOrderBy();
$orders = array();
if ($count->distinct && !empty($group_by)) {
// If the query is distinct and contains a GROUP BY, we need to remove the
// distinct because SQL99 does not support counting on distinct multiple fields.
$count->distinct = FALSE;
}
// If there are any dependent queries to UNION, prepare each of those for
// the count query also.
foreach ($count->union as &$union) {
$union['query'] = $union['query']->prepareCountQuery();
}
return $count;
}
/**
* {@inheritdoc}
*/
public function __toString() {
// For convenience, we compile the query ourselves if the caller forgot
// to do it. This allows constructs like "(string) $query" to work. When
// the query will be executed, it will be recompiled using the proper
// placeholder generator anyway.
if (!$this->compiled()) {
$this->compile($this->connection, $this);
}
// Create a sanitized comment string to prepend to the query.
$comments = $this->connection->makeComment($this->comments);
// SELECT
$query = $comments . 'SELECT ';
if ($this->distinct) {
$query .= 'DISTINCT ';
}
// FIELDS and EXPRESSIONS
$fields = array();
foreach ($this->tables as $alias => $table) {
if (!empty($table['all_fields'])) {
$fields[] = $this->connection->escapeTable($alias) . '.*';
}
}
foreach ($this->fields as $field) {
// Always use the AS keyword for field aliases, as some
// databases require it (e.g., PostgreSQL).
$fields[] = (isset($field['table']) ? $this->connection->escapeTable($field['table']) . '.' : '') . $this->connection->escapeField($field['field']) . ' AS ' . $this->connection->escapeAlias($field['alias']);
}
foreach ($this->expressions as $expression) {
$fields[] = $expression['expression'] . ' AS ' . $this->connection->escapeAlias($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 $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 SelectInterface) {
// 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']);
// Do not attempt prefixing cross database / schema queries.
if (strpos($table_string, '.') === FALSE) {
$table_string = '{' . $table_string . '}';
}
}
// Don't use the AS keyword for table aliases, as some
// databases don't support it (e.g., Oracle).
$query .= $table_string . ' ' . $this->connection->escapeTable($table['alias']);
if (!empty($table['condition'])) {
$query .= ' ON ' . $table['condition'];
}
}
// WHERE
if (count($this->where)) {
// 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)) {
// 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[] = $this->connection->escapeField($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 " . (int) $this->range['length'] . " OFFSET " . (int) $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'];
}
}
if ($this->forUpdate) {
$query .= ' FOR UPDATE';
}
return $query;
}
/**
* {@inheritdoc}
*/
public function __clone() {
// On cloning, also clone the dependent objects. However, we do not
// want to clone the database connection object as that would duplicate the
// connection itself.
$this->where = clone($this->where);
$this->having = clone($this->having);
foreach ($this->union as $key => $aggregate) {
$this->union[$key]['query'] = clone($aggregate['query']);
}
}
}

View file

@ -0,0 +1,390 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Query\SelectExtender.
*/
namespace Drupal\Core\Database\Query;
use Drupal\Core\Database\Connection;
/**
* The base extender class for Select queries.
*/
class SelectExtender implements SelectInterface {
/**
* The Select query object we are extending/decorating.
*
* @var \Drupal\Core\Database\Query\SelectInterface
*/
protected $query;
/**
* The connection object on which to run this query.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* A unique identifier for this query object.
*/
protected $uniqueIdentifier;
/**
* The placeholder counter.
*/
protected $placeholder = 0;
public function __construct(SelectInterface $query, Connection $connection) {
$this->uniqueIdentifier = uniqid('', TRUE);
$this->query = $query;
$this->connection = $connection;
}
/**
* Implements Drupal\Core\Database\Query\PlaceholderInterface::uniqueIdentifier().
*/
public function uniqueIdentifier() {
return $this->uniqueIdentifier;
}
/**
* Implements Drupal\Core\Database\Query\PlaceholderInterface::nextPlaceholder().
*/
public function nextPlaceholder() {
return $this->placeholder++;
}
/* Implementations of Drupal\Core\Database\Query\AlterableInterface. */
public function addTag($tag) {
$this->query->addTag($tag);
return $this;
}
public function hasTag($tag) {
return $this->query->hasTag($tag);
}
public function hasAllTags() {
return call_user_func_array(array($this->query, 'hasAllTags'), func_get_args());
}
public function hasAnyTag() {
return call_user_func_array(array($this->query, 'hasAnyTag'), func_get_args());
}
public function addMetaData($key, $object) {
$this->query->addMetaData($key, $object);
return $this;
}
public function getMetaData($key) {
return $this->query->getMetaData($key);
}
/* Implementations of Drupal\Core\Database\Query\ConditionInterface for the WHERE clause. */
public function condition($field, $value = NULL, $operator = '=') {
$this->query->condition($field, $value, $operator);
return $this;
}
public function &conditions() {
return $this->query->conditions();
}
public function arguments() {
return $this->query->arguments();
}
public function where($snippet, $args = array()) {
$this->query->where($snippet, $args);
return $this;
}
public function compile(Connection $connection, PlaceholderInterface $queryPlaceholder) {
return $this->query->compile($connection, $queryPlaceholder);
}
public function compiled() {
return $this->query->compiled();
}
/* Implementations of Drupal\Core\Database\Query\ConditionInterface for the HAVING clause. */
public function havingCondition($field, $value = NULL, $operator = '=') {
$this->query->havingCondition($field, $value, $operator);
return $this;
}
public function &havingConditions() {
return $this->query->havingConditions();
}
public function havingArguments() {
return $this->query->havingArguments();
}
public function having($snippet, $args = array()) {
$this->query->having($snippet, $args);
return $this;
}
public function havingCompile(Connection $connection) {
return $this->query->havingCompile($connection);
}
/**
* {@inheritdoc}
*/
public function havingIsNull($field) {
$this->query->havingIsNull($field);
return $this;
}
/**
* {@inheritdoc}
*/
public function havingIsNotNull($field) {
$this->query->havingIsNotNull($field);
return $this;
}
/**
* {@inheritdoc}
*/
public function havingExists(SelectInterface $select) {
$this->query->havingExists($select);
return $this;
}
/**
* {@inheritdoc}
*/
public function havingNotExists(SelectInterface $select) {
$this->query->havingNotExists($select);
return $this;
}
/* Implementations of Drupal\Core\Database\Query\ExtendableInterface. */
public function extend($extender_name) {
$class = $this->connection->getDriverClass($extender_name);
return new $class($this, $this->connection);
}
/* Alter accessors to expose the query data to alter hooks. */
public function &getFields() {
return $this->query->getFields();
}
public function &getExpressions() {
return $this->query->getExpressions();
}
public function &getOrderBy() {
return $this->query->getOrderBy();
}
public function &getGroupBy() {
return $this->query->getGroupBy();
}
public function &getTables() {
return $this->query->getTables();
}
public function &getUnion() {
return $this->query->getUnion();
}
/**
* {@inheritdoc}
*/
public function escapeLike($string) {
return $this->query->escapeLike($string);
}
public function getArguments(PlaceholderInterface $queryPlaceholder = NULL) {
return $this->query->getArguments($queryPlaceholder);
}
public function isPrepared() {
return $this->query->isPrepared();
}
public function preExecute(SelectInterface $query = NULL) {
// If no query object is passed in, use $this.
if (!isset($query)) {
$query = $this;
}
return $this->query->preExecute($query);
}
public function execute() {
// By calling preExecute() here, we force it to preprocess the extender
// object rather than just the base query object. That means
// hook_query_alter() gets access to the extended object.
if (!$this->preExecute($this)) {
return NULL;
}
return $this->query->execute();
}
public function distinct($distinct = TRUE) {
$this->query->distinct($distinct);
return $this;
}
public function addField($table_alias, $field, $alias = NULL) {
return $this->query->addField($table_alias, $field, $alias);
}
public function fields($table_alias, array $fields = array()) {
$this->query->fields($table_alias, $fields);
return $this;
}
public function addExpression($expression, $alias = NULL, $arguments = array()) {
return $this->query->addExpression($expression, $alias, $arguments);
}
public function join($table, $alias = NULL, $condition = NULL, $arguments = array()) {
return $this->query->join($table, $alias, $condition, $arguments);
}
public function innerJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) {
return $this->query->innerJoin($table, $alias, $condition, $arguments);
}
public function leftJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) {
return $this->query->leftJoin($table, $alias, $condition, $arguments);
}
public function rightJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) {
return $this->query->rightJoin($table, $alias, $condition, $arguments);
}
public function addJoin($type, $table, $alias = NULL, $condition = NULL, $arguments = array()) {
return $this->query->addJoin($type, $table, $alias, $condition, $arguments);
}
public function orderBy($field, $direction = 'ASC') {
$this->query->orderBy($field, $direction);
return $this;
}
public function orderRandom() {
$this->query->orderRandom();
return $this;
}
public function range($start = NULL, $length = NULL) {
$this->query->range($start, $length);
return $this;
}
public function union(SelectInterface $query, $type = '') {
$this->query->union($query, $type);
return $this;
}
public function groupBy($field) {
$this->query->groupBy($field);
return $this;
}
public function forUpdate($set = TRUE) {
$this->query->forUpdate($set);
return $this;
}
public function countQuery() {
return $this->query->countQuery();
}
function isNull($field) {
$this->query->isNull($field);
return $this;
}
function isNotNull($field) {
$this->query->isNotNull($field);
return $this;
}
public function exists(SelectInterface $select) {
$this->query->exists($select);
return $this;
}
public function notExists(SelectInterface $select) {
$this->query->notExists($select);
return $this;
}
public function __toString() {
return (string) $this->query;
}
public function __clone() {
$this->uniqueIdentifier = uniqid('', TRUE);
// We need to deep-clone the query we're wrapping, which in turn may
// deep-clone other objects. Exciting!
$this->query = clone($this->query);
}
/**
* Magic override for undefined methods.
*
* If one extender extends another extender, then methods in the inner extender
* will not be exposed on the outer extender. That's because we cannot know
* in advance what those methods will be, so we cannot provide wrapping
* implementations as we do above. Instead, we use this slower catch-all method
* to handle any additional methods.
*/
public function __call($method, $args) {
$return = call_user_func_array(array($this->query, $method), $args);
// Some methods will return the called object as part of a fluent interface.
// Others will return some useful value. If it's a value, then the caller
// probably wants that value. If it's the called object, then we instead
// return this object. That way we don't "lose" an extender layer when
// chaining methods together.
if ($return instanceof SelectInterface) {
return $this;
}
else {
return $return;
}
}
/**
* {@inheritdoc}
*/
public function conditionGroupFactory($conjunction = 'AND') {
return new Condition($conjunction);
}
/**
* {@inheritdoc}
*/
public function andConditionGroup() {
return $this->conditionGroupFactory('AND');
}
/**
* {@inheritdoc}
*/
public function orConditionGroup() {
return $this->conditionGroupFactory('OR');
}
}

View file

@ -0,0 +1,635 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Query\SelectInterface.
*/
namespace Drupal\Core\Database\Query;
use Drupal\Core\Database\Connection;
/**
* Interface definition for a Select Query object.
*
* @ingroup database
*/
interface SelectInterface extends ConditionInterface, AlterableInterface, ExtendableInterface, PlaceholderInterface {
/* Alter accessors to expose the query data to alter hooks. */
/**
* Returns a reference to the fields array for this query.
*
* Because this method returns by reference, alter hooks may edit the fields
* array directly to make their changes. If just adding fields, however, the
* use of addField() is preferred.
*
* Note that this method must be called by reference as well:
*
* @code
* $fields =& $query->getFields();
* @endcode
*
* @return
* A reference to the fields array structure.
*/
public function &getFields();
/**
* Returns a reference to the expressions array for this query.
*
* Because this method returns by reference, alter hooks may edit the expressions
* array directly to make their changes. If just adding expressions, however, the
* use of addExpression() is preferred.
*
* Note that this method must be called by reference as well:
*
* @code
* $fields =& $query->getExpressions();
* @endcode
*
* @return
* A reference to the expression array structure.
*/
public function &getExpressions();
/**
* Returns a reference to the order by array for this query.
*
* Because this method returns by reference, alter hooks may edit the order-by
* array directly to make their changes. If just adding additional ordering
* fields, however, the use of orderBy() is preferred.
*
* Note that this method must be called by reference as well:
*
* @code
* $fields =& $query->getOrderBy();
* @endcode
*
* @return
* A reference to the expression array structure.
*/
public function &getOrderBy();
/**
* Returns a reference to the group-by array for this query.
*
* Because this method returns by reference, alter hooks may edit the group-by
* array directly to make their changes. If just adding additional grouping
* fields, however, the use of groupBy() is preferred.
*
* Note that this method must be called by reference as well:
*
* @code
* $fields =& $query->getGroupBy();
* @endcode
*
* @return
* A reference to the group-by array structure.
*/
public function &getGroupBy();
/**
* Returns a reference to the tables array for this query.
*
* Because this method returns by reference, alter hooks may edit the tables
* array directly to make their changes. If just adding tables, however, the
* use of the join() methods is preferred.
*
* Note that this method must be called by reference as well:
*
* @code
* $fields =& $query->getTables();
* @endcode
*
* @return
* A reference to the tables array structure.
*/
public function &getTables();
/**
* Returns a reference to the union queries for this query. This include
* queries for UNION, UNION ALL, and UNION DISTINCT.
*
* Because this method returns by reference, alter hooks may edit the tables
* array directly to make their changes. If just adding union queries,
* however, the use of the union() method is preferred.
*
* Note that this method must be called by reference as well:
*
* @code
* $fields =& $query->getUnion();
* @endcode
*
* @return
* A reference to the union query array structure.
*/
public function &getUnion();
/**
* Escapes characters that work as wildcard characters in a LIKE pattern.
*
* @param $string
* The string to escape.
*
* @return string
* The escaped string.
*
* @see \Drupal\Core\Database\Connection::escapeLike()
*/
public function escapeLike($string);
/**
* Compiles and returns an associative array of the arguments for this prepared statement.
*
* @param $queryPlaceholder
* When collecting the arguments of a subquery, the main placeholder
* object should be passed as this parameter.
*
* @return
* An associative array of all placeholder arguments for this query.
*/
public function getArguments(PlaceholderInterface $queryPlaceholder = NULL);
/* Query building operations */
/**
* Sets this query to be DISTINCT.
*
* @param $distinct
* TRUE to flag this query DISTINCT, FALSE to disable it.
* @return \Drupal\Core\Database\Query\SelectInterface
* The called object.
*/
public function distinct($distinct = TRUE);
/**
* Adds a field to the list to be SELECTed.
*
* @param $table_alias
* The name of the table from which the field comes, as an alias. Generally
* you will want to use the return value of join() here to ensure that it is
* valid.
* @param $field
* The name of the field.
* @param $alias
* The alias for this field. If not specified, one will be generated
* automatically based on the $table_alias and $field. The alias will be
* checked for uniqueness, so the requested alias may not be the alias
* that is assigned in all cases.
* @return
* The unique alias that was assigned for this field.
*/
public function addField($table_alias, $field, $alias = NULL);
/**
* Add multiple fields from the same table to be SELECTed.
*
* This method does not return the aliases set for the passed fields. In the
* majority of cases that is not a problem, as the alias will be the field
* name. However, if you do need to know the alias you can call getFields()
* and examine the result to determine what alias was created. Alternatively,
* simply use addField() for the few fields you care about and this method for
* the rest.
*
* @param $table_alias
* The name of the table from which the field comes, as an alias. Generally
* you will want to use the return value of join() here to ensure that it is
* valid.
* @param $fields
* An indexed array of fields present in the specified table that should be
* included in this query. If not specified, $table_alias.* will be generated
* without any aliases.
* @return \Drupal\Core\Database\Query\SelectInterface
* The called object.
*/
public function fields($table_alias, array $fields = array());
/**
* Adds an expression to the list of "fields" to be SELECTed.
*
* An expression can be any arbitrary string that is valid SQL. That includes
* various functions, which may in some cases be database-dependent. This
* method makes no effort to correct for database-specific functions.
*
* @param $expression
* The expression string. May contain placeholders.
* @param $alias
* The alias for this expression. If not specified, one will be generated
* automatically in the form "expression_#". The alias will be checked for
* uniqueness, so the requested alias may not be the alias that is assigned
* in all cases.
* @param $arguments
* Any placeholder arguments needed for this expression.
* @return
* The unique alias that was assigned for this expression.
*/
public function addExpression($expression, $alias = NULL, $arguments = array());
/**
* Default Join against another table in the database.
*
* This method is a convenience method for innerJoin().
*
* @param $table
* The table against which to join. May be a string or another SelectQuery
* object. If a query object is passed, it will be used as a subselect.
* Unless the table name starts with the database / schema name and a dot
* it will be prefixed.
* @param $alias
* The alias for the table. In most cases this should be the first letter
* of the table, or the first letter of each "word" in the table.
* @param $condition
* The condition on which to join this table. If the join requires values,
* this clause should use a named placeholder and the value or values to
* insert should be passed in the 4th parameter. For the first table joined
* on a query, this value is ignored as the first table is taken as the base
* table. The token %alias can be used in this string to be replaced with
* the actual alias. This is useful when $alias is modified by the database
* system, for example, when joining the same table more than once.
* @param $arguments
* An array of arguments to replace into the $condition of this join.
* @return
* The unique alias that was assigned for this table.
*/
public function join($table, $alias = NULL, $condition = NULL, $arguments = array());
/**
* Inner Join against another table in the database.
*
* @param $table
* The table against which to join. May be a string or another SelectQuery
* object. If a query object is passed, it will be used as a subselect.
* Unless the table name starts with the database / schema name and a dot
* it will be prefixed.
* @param $alias
* The alias for the table. In most cases this should be the first letter
* of the table, or the first letter of each "word" in the table.
* @param $condition
* The condition on which to join this table. If the join requires values,
* this clause should use a named placeholder and the value or values to
* insert should be passed in the 4th parameter. For the first table joined
* on a query, this value is ignored as the first table is taken as the base
* table. The token %alias can be used in this string to be replaced with
* the actual alias. This is useful when $alias is modified by the database
* system, for example, when joining the same table more than once.
* @param $arguments
* An array of arguments to replace into the $condition of this join.
* @return
* The unique alias that was assigned for this table.
*/
public function innerJoin($table, $alias = NULL, $condition = NULL, $arguments = array());
/**
* Left Outer Join against another table in the database.
*
* @param $table
* The table against which to join. May be a string or another SelectQuery
* object. If a query object is passed, it will be used as a subselect.
* Unless the table name starts with the database / schema name and a dot
* it will be prefixed.
* @param $alias
* The alias for the table. In most cases this should be the first letter
* of the table, or the first letter of each "word" in the table.
* @param $condition
* The condition on which to join this table. If the join requires values,
* this clause should use a named placeholder and the value or values to
* insert should be passed in the 4th parameter. For the first table joined
* on a query, this value is ignored as the first table is taken as the base
* table. The token %alias can be used in this string to be replaced with
* the actual alias. This is useful when $alias is modified by the database
* system, for example, when joining the same table more than once.
* @param $arguments
* An array of arguments to replace into the $condition of this join.
* @return
* The unique alias that was assigned for this table.
*/
public function leftJoin($table, $alias = NULL, $condition = NULL, $arguments = array());
/**
* Right Outer Join against another table in the database.
*
* @param $table
* The table against which to join. May be a string or another SelectQuery
* object. If a query object is passed, it will be used as a subselect.
* Unless the table name starts with the database / schema name and a dot
* it will be prefixed.
* @param $alias
* The alias for the table. In most cases this should be the first letter
* of the table, or the first letter of each "word" in the table.
* @param $condition
* The condition on which to join this table. If the join requires values,
* this clause should use a named placeholder and the value or values to
* insert should be passed in the 4th parameter. For the first table joined
* on a query, this value is ignored as the first table is taken as the base
* table. The token %alias can be used in this string to be replaced with
* the actual alias. This is useful when $alias is modified by the database
* system, for example, when joining the same table more than once.
* @param $arguments
* An array of arguments to replace into the $condition of this join.
* @return
* The unique alias that was assigned for this table.
*/
public function rightJoin($table, $alias = NULL, $condition = NULL, $arguments = array());
/**
* Join against another table in the database.
*
* This method does the "hard" work of queuing up a table to be joined against.
* In some cases, that may include dipping into the Schema API to find the necessary
* fields on which to join.
*
* @param $type
* The type of join. Typically one one of INNER, LEFT OUTER, and RIGHT OUTER.
* @param $table
* The table against which to join. May be a string or another SelectQuery
* object. If a query object is passed, it will be used as a subselect.
* Unless the table name starts with the database / schema name and a dot
* it will be prefixed.
* @param $alias
* The alias for the table. In most cases this should be the first letter
* of the table, or the first letter of each "word" in the table. If omitted,
* one will be dynamically generated.
* @param $condition
* The condition on which to join this table. If the join requires values,
* this clause should use a named placeholder and the value or values to
* insert should be passed in the 4th parameter. For the first table joined
* on a query, this value is ignored as the first table is taken as the base
* table. The token %alias can be used in this string to be replaced with
* the actual alias. This is useful when $alias is modified by the database
* system, for example, when joining the same table more than once.
* @param $arguments
* An array of arguments to replace into the $condition of this join.
* @return
* The unique alias that was assigned for this table.
*/
public function addJoin($type, $table, $alias = NULL, $condition = NULL, $arguments = array());
/**
* Orders the result set by a given field.
*
* If called multiple times, the query will order by each specified field in the
* order this method is called.
*
* If the query uses DISTINCT or GROUP BY conditions, fields or expressions
* that are used for the order must be selected to be compatible with some
* databases like PostgreSQL. The PostgreSQL driver can handle simple cases
* automatically but it is suggested to explicitly specify them. Additionally,
* when ordering on an alias, the alias must be added before orderBy() is
* called.
*
* @param $field
* The field on which to order. The field is escaped for security so only
* valid field and alias names are possible. To order by an expression, add
* the expression with addExpression() first and then use the alias to order
* on.
*
* Example:
* <code>
* $query->addExpression('SUBSTRING(thread, 1, (LENGTH(thread) - 1))', 'order_field');
* $query->orderBy('order_field', 'ASC');
* </code>
* @param $direction
* The direction to sort. Legal values are "ASC" and "DESC". Any other value
* will be converted to "ASC".
* @return \Drupal\Core\Database\Query\SelectInterface
* The called object.
*/
public function orderBy($field, $direction = 'ASC');
/**
* Orders the result set by a random value.
*
* This may be stacked with other orderBy() calls. If so, the query will order
* by each specified field, including this one, in the order called. Although
* this method may be called multiple times on the same query, doing so
* is not particularly useful.
*
* Note: The method used by most drivers may not scale to very large result
* sets. If you need to work with extremely large data sets, you may create
* your own database driver by subclassing off of an existing driver and
* implementing your own randomization mechanism. See
*
* http://jan.kneschke.de/projects/mysql/order-by-rand/
*
* for an example of such an alternate sorting mechanism.
*
* @return \Drupal\Core\Database\Query\SelectInterface
* The called object
*/
public function orderRandom();
/**
* Restricts a query to a given range in the result set.
*
* If this method is called with no parameters, will remove any range
* directives that have been set.
*
* @param $start
* The first record from the result set to return. If NULL, removes any
* range directives that are set.
* @param $length
* The number of records to return from the result set.
* @return \Drupal\Core\Database\Query\SelectInterface
* The called object.
*/
public function range($start = NULL, $length = NULL);
/**
* Add another Select query to UNION to this one.
*
* Union queries consist of two or more queries whose
* results are effectively concatenated together. Queries
* will be UNIONed in the order they are specified, with
* this object's query coming first. Duplicate columns will
* be discarded. All forms of UNION are supported, using
* the second '$type' argument.
*
* Note: All queries UNIONed together must have the same
* field structure, in the same order. It is up to the
* caller to ensure that they match properly. If they do
* not, an SQL syntax error will result.
*
* @param $query
* The query to UNION to this query.
* @param $type
* The type of UNION to add to the query. Defaults to plain
* UNION.
* @return \Drupal\Core\Database\Query\SelectInterface
* The called object.
*/
public function union(SelectInterface $query, $type = '');
/**
* Groups the result set by the specified field.
*
* @param $field
* The field on which to group. This should be the field as aliased.
* @return \Drupal\Core\Database\Query\SelectInterface
* The called object.
*/
public function groupBy($field);
/**
* Get the equivalent COUNT query of this query as a new query object.
*
* @return \Drupal\Core\Database\Query\SelectInterface
* A new SelectQuery object with no fields or expressions besides COUNT(*).
*/
public function countQuery();
/**
* Indicates if preExecute() has already been called on that object.
*
* @return
* TRUE is this query has already been prepared, FALSE otherwise.
*/
public function isPrepared();
/**
* Generic preparation and validation for a SELECT query.
*
* @return
* TRUE if the validation was successful, FALSE if not.
*/
public function preExecute(SelectInterface $query = NULL);
/**
* Runs the query against the database.
*
* @return \Drupal\Core\Database\StatementInterface|null
* A prepared statement, or NULL if the query is not valid.
*/
public function execute();
/**
* Helper function to build most common HAVING conditional clauses.
*
* This method can take a variable number of parameters. If called with two
* parameters, they are taken as $field and $value with $operator having a value
* of IN if $value is an array and = otherwise.
*
* @param $field
* The name of the field to check. If you would like to add a more complex
* condition involving operators or functions, use having().
* @param $value
* The value to test the field against. In most cases, this is a scalar. For more
* complex options, it is an array. The meaning of each element in the array is
* dependent on the $operator.
* @param $operator
* The comparison operator, such as =, <, or >=. It also accepts more complex
* options such as IN, LIKE, or BETWEEN. Defaults to IN if $value is an array
* = otherwise.
* @return \Drupal\Core\Database\Query\ConditionInterface
* The called object.
*/
public function havingCondition($field, $value = NULL, $operator = NULL);
/**
* Gets a list of all conditions in the HAVING clause.
*
* This method returns by reference. That allows alter hooks to access the
* data structure directly and manipulate it before it gets compiled.
*
* @return array
* An array of conditions.
*
* @see \Drupal\Core\Database\Query\ConditionInterface::conditions()
*/
public function &havingConditions();
/**
* Gets a list of all values to insert into the HAVING clause.
*
* @return array
* An associative array of placeholders and values.
*/
public function havingArguments();
/**
* Adds an arbitrary HAVING clause to the query.
*
* @param $snippet
* A portion of a HAVING clause as a prepared statement. It must use named
* placeholders, not ? placeholders.
* @param $args
* (optional) An associative array of arguments.
*
* @return $this
*/
public function having($snippet, $args = array());
/**
* Compiles the HAVING clause for later retrieval.
*
* @param $connection
* The database connection for which to compile the clause.
*/
public function havingCompile(Connection $connection);
/**
* Sets a condition in the HAVING clause that the specified field be NULL.
*
* @param $field
* The name of the field to check.
*
* @return $this
*/
public function havingIsNull($field);
/**
* Sets a condition in the HAVING clause that the specified field be NOT NULL.
*
* @param $field
* The name of the field to check.
*
* @return $this
*/
public function havingIsNotNull($field);
/**
* Sets a HAVING condition that the specified subquery returns values.
*
* @param \Drupal\Core\Database\Query\SelectInterface $select
* The subquery that must contain results.
*
* @return $this
*/
public function havingExists(SelectInterface $select);
/**
* Sets a HAVING condition that the specified subquery returns no values.
*
* @param \Drupal\Core\Database\Query\SelectInterface $select
* The subquery that must contain results.
*
* @return $this
*/
public function havingNotExists(SelectInterface $select);
/**
* Clone magic method.
*
* Select queries have dependent objects that must be deep-cloned. The
* connection object itself, however, should not be cloned as that would
* duplicate the connection itself.
*/
public function __clone();
/**
* Add FOR UPDATE to the query.
*
* FOR UPDATE prevents the rows retrieved by the SELECT statement from being
* modified or deleted by other transactions until the current transaction
* ends. Other transactions that attempt UPDATE, DELETE, or SELECT FOR UPDATE
* of these rows will be blocked until the current transaction ends.
*
* @param $set
* IF TRUE, FOR UPDATE will be added to the query, if FALSE then it won't.
*
* @return \Drupal\Core\Database\Query\ConditionInterface
* The called object.
*/
public function forUpdate($set = TRUE);
}

View file

@ -0,0 +1,104 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Query\TableSortExtender.
*/
namespace Drupal\Core\Database\Query;
use Drupal\Core\Database\Connection;
/**
* Query extender class for tablesort queries.
*/
class TableSortExtender extends SelectExtender {
/**
* The array of fields that can be sorted by.
*/
protected $header = array();
public function __construct(SelectInterface $query, Connection $connection) {
parent::__construct($query, $connection);
// 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.
$this->addTag('tablesort');
}
/**
* Order the query based on a header array.
*
* @param array $header
* Table header array.
*
* @return \Drupal\Core\Database\Query\SelectInterface
* The called object.
*
* @see table.html.twig
*/
public function orderByHeader(array $header) {
$this->header = $header;
$ts = $this->init();
if (!empty($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']);
// orderBy() will ensure that only ASC/DESC values are accepted, so we
// don't need to sanitize that here.
$this->orderBy($field, $ts['sort']);
}
return $this;
}
/**
* Initialize the table sort context.
*/
protected function init() {
$ts = $this->order();
$ts['sort'] = $this->getSort();
$ts['query'] = $this->getQueryParameters();
return $ts;
}
/**
* Determine the current sort direction.
*
* @return
* The current sort direction ("asc" or "desc").
*
* @see tablesort_get_sort()
*/
protected function getSort() {
return tablesort_get_sort($this->header);
}
/**
* Compose a URL query parameter array to append to table sorting requests.
*
* @return
* A URL query parameter array that consists of all components of the current
* page request except for those pertaining to table sorting.
*
* @see tablesort_get_query_parameters()
*/
protected function getQueryParameters() {
return tablesort_get_query_parameters();
}
/**
* Determine the current sort criterion.
*
* @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.
*
* @see tablesort_get_order()
*/
protected function order() {
return tablesort_get_order($this->header);
}
}

View file

@ -0,0 +1,88 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Query\Truncate.
*/
namespace Drupal\Core\Database\Query;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Connection;
/**
* General class for an abstracted TRUNCATE operation.
*/
class Truncate extends Query {
/**
* The table to truncate.
*
* @var string
*/
protected $table;
/**
* Constructs a Truncate query object.
*
* @param \Drupal\Core\Database\Connection $connection
* A Connection object.
* @param string $table
* Name of the table to associate with this query.
* @param array $options
* Array of database options.
*/
public function __construct(Connection $connection, $table, array $options = array()) {
$options['return'] = Database::RETURN_AFFECTED;
parent::__construct($connection, $options);
$this->table = $table;
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::compile().
*/
public function compile(Connection $connection, PlaceholderInterface $queryPlaceholder) {
return $this->condition->compile($connection, $queryPlaceholder);
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::compiled().
*/
public function compiled() {
return $this->condition->compiled();
}
/**
* Executes the TRUNCATE query.
*
* @return
* Return value is dependent on the database type.
*/
public function execute() {
return $this->connection->query((string) $this, array(), $this->queryOptions);
}
/**
* Implements PHP magic __toString method to convert the query to a string.
*
* @return string
* The prepared statement.
*/
public function __toString() {
// Create a sanitized comment string to prepend to the query.
$comments = $this->connection->makeComment($this->comments);
// 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) . '} ';
}
}
}

View file

@ -0,0 +1,279 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Query\Update.
*/
namespace Drupal\Core\Database\Query;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Connection;
/**
* General class for an abstracted UPDATE operation.
*
* @ingroup database
*/
class Update extends Query implements ConditionInterface {
/**
* The table to update.
*
* @var string
*/
protected $table;
/**
* An array of fields that will be updated.
*
* @var array
*/
protected $fields = array();
/**
* An array of values to update to.
*
* @var array
*/
protected $arguments = array();
/**
* The condition object for this query.
*
* Condition handling is handled via composition.
*
* @var \Drupal\Core\Database\Query\Condition
*/
protected $condition;
/**
* Array of fields to update to an expression in case of a duplicate record.
*
* This variable is a nested array in the following format:
* @code
* <some field> => array(
* 'condition' => <condition to execute, as a string>,
* 'arguments' => <array of arguments for condition, or NULL for none>,
* );
* @endcode
*
* @var array
*/
protected $expressionFields = array();
/**
* Constructs an Update query object.
*
* @param \Drupal\Core\Database\Connection $connection
* A Connection object.
* @param string $table
* Name of the table to associate with this query.
* @param array $options
* Array of database options.
*/
public function __construct(Connection $connection, $table, array $options = array()) {
$options['return'] = Database::RETURN_AFFECTED;
parent::__construct($connection, $options);
$this->table = $table;
$this->condition = new Condition('AND');
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::condition().
*/
public function condition($field, $value = NULL, $operator = '=') {
$this->condition->condition($field, $value, $operator);
return $this;
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::isNull().
*/
public function isNull($field) {
$this->condition->isNull($field);
return $this;
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::isNotNull().
*/
public function isNotNull($field) {
$this->condition->isNotNull($field);
return $this;
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::exists().
*/
public function exists(SelectInterface $select) {
$this->condition->exists($select);
return $this;
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::notExists().
*/
public function notExists(SelectInterface $select) {
$this->condition->notExists($select);
return $this;
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::conditions().
*/
public function &conditions() {
return $this->condition->conditions();
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::arguments().
*/
public function arguments() {
return $this->condition->arguments();
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::where().
*/
public function where($snippet, $args = array()) {
$this->condition->where($snippet, $args);
return $this;
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::compile().
*/
public function compile(Connection $connection, PlaceholderInterface $queryPlaceholder) {
return $this->condition->compile($connection, $queryPlaceholder);
}
/**
* Implements Drupal\Core\Database\Query\ConditionInterface::compiled().
*/
public function compiled() {
return $this->condition->compiled();
}
/**
* Adds a set of field->value pairs to be updated.
*
* @param $fields
* An associative array of fields to write into the database. The array keys
* are the field names and the values are the values to which to set them.
*
* @return \Drupal\Core\Database\Query\Update
* The called object.
*/
public function fields(array $fields) {
$this->fields = $fields;
return $this;
}
/**
* Specifies fields to be updated as an expression.
*
* Expression fields are cases such as counter=counter+1. This method takes
* precedence over fields().
*
* @param $field
* The field to set.
* @param $expression
* The field will be set to the value of this expression. This parameter
* may include named placeholders.
* @param $arguments
* If specified, this is an array of key/value pairs for named placeholders
* corresponding to the expression.
*
* @return \Drupal\Core\Database\Query\Update
* The called object.
*/
public function expression($field, $expression, array $arguments = NULL) {
$this->expressionFields[$field] = array(
'expression' => $expression,
'arguments' => $arguments,
);
return $this;
}
/**
* Executes the UPDATE query.
*
* @return
* The number of rows matched by the update query. This includes rows that
* actually didn't have to be updated because the values didn't change.
*/
public function execute() {
// Expressions take priority over literal fields, so we process those first
// and remove any literal fields that conflict.
$fields = $this->fields;
$update_values = array();
foreach ($this->expressionFields as $field => $data) {
if (!empty($data['arguments'])) {
$update_values += $data['arguments'];
}
if ($data['expression'] instanceof SelectInterface) {
$data['expression']->compile($this->connection, $this);
$update_values += $data['expression']->arguments();
}
unset($fields[$field]);
}
// Because we filter $fields the same way here and in __toString(), the
// placeholders will all match up properly.
$max_placeholder = 0;
foreach ($fields as $value) {
$update_values[':db_update_placeholder_' . ($max_placeholder++)] = $value;
}
if (count($this->condition)) {
$this->condition->compile($this->connection, $this);
$update_values = array_merge($update_values, $this->condition->arguments());
}
return $this->connection->query((string) $this, $update_values, $this->queryOptions);
}
/**
* Implements PHP magic __toString method to convert the query to a string.
*
* @return string
* The prepared statement.
*/
public function __toString() {
// Create a sanitized comment string to prepend to the query.
$comments = $this->connection->makeComment($this->comments);
// 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) {
if ($data['expression'] instanceof SelectInterface) {
// Compile and cast expression subquery to a string.
$data['expression']->compile($this->connection, $this);
$data['expression'] = ' (' . $data['expression'] . ')';
}
$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 {' . $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;
}
}

View file

@ -0,0 +1,22 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\RowCountException.
*/
namespace Drupal\Core\Database;
/**
* Exception thrown if a SELECT query trying to execute rowCount() on result.
*/
class RowCountException extends \RuntimeException implements DatabaseException {
public function __construct($message = NULL, $code = 0, \Exception $previous = NULL) {
if (!isset($message)) {
$message = "rowCount() is supported for DELETE, INSERT, or UPDATE statements performed with structured query builders only, since they would not be portable across database engines otherwise. If the query builders are not sufficient, set the 'return' option to Database::RETURN_AFFECTED to get the number of affected rows.";
}
parent::__construct($message, $code, $previous);
}
}

View file

@ -0,0 +1,582 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Schema.
*/
namespace Drupal\Core\Database;
use Drupal\Core\Database\SchemaObjectExistsException;
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Database\Query\PlaceholderInterface;
/**
* Provides a base implementation for Database Schema.
*/
abstract class Schema implements PlaceholderInterface {
protected $connection;
/**
* The placeholder counter.
*/
protected $placeholder = 0;
/**
* Definition of prefixInfo array structure.
*
* Rather than redefining DatabaseSchema::getPrefixInfo() for each driver,
* by defining the defaultSchema variable only MySQL has to re-write the
* method.
*
* @see DatabaseSchema::getPrefixInfo()
*/
protected $defaultSchema = 'public';
/**
* A unique identifier for this query object.
*/
protected $uniqueIdentifier;
public function __construct($connection) {
$this->uniqueIdentifier = uniqid('', TRUE);
$this->connection = $connection;
}
/**
* Implements the magic __clone function.
*/
public function __clone() {
$this->uniqueIdentifier = uniqid('', TRUE);
}
/**
* Implements PlaceHolderInterface::uniqueIdentifier().
*/
public function uniqueIdentifier() {
return $this->uniqueIdentifier;
}
/**
* Implements PlaceHolderInterface::nextPlaceholder().
*/
public function nextPlaceholder() {
return $this->placeholder++;
}
/**
* Get information about the table name and schema from the prefix.
*
* @param
* Name of table to look prefix up for. Defaults to 'default' because that's
* default key for prefix.
* @param $add_prefix
* Boolean that indicates whether the given table name should be prefixed.
*
* @return
* A keyed array with information about the schema, table name and prefix.
*/
protected function getPrefixInfo($table = 'default', $add_prefix = TRUE) {
$info = array(
'schema' => $this->defaultSchema,
'prefix' => $this->connection->tablePrefix($table),
);
if ($add_prefix) {
$table = $info['prefix'] . $table;
}
// If the prefix contains a period in it, then that means the prefix also
// contains a schema reference in which case we will change the schema key
// to the value before the period in the prefix. Everything after the dot
// will be prefixed onto the front of the table.
if (($pos = strpos($table, '.')) !== FALSE) {
// Grab everything before the period.
$info['schema'] = substr($table, 0, $pos);
// Grab everything after the dot.
$info['table'] = substr($table, ++$pos);
}
else {
$info['table'] = $table;
}
return $info;
}
/**
* Create names for indexes, primary keys and constraints.
*
* This prevents using {} around non-table names like indexes and keys.
*/
function prefixNonTable($table) {
$args = func_get_args();
$info = $this->getPrefixInfo($table);
$args[0] = $info['table'];
return implode('_', $args);
}
/**
* Build a condition to match a table name against a standard information_schema.
*
* The information_schema is a SQL standard that provides information about the
* database server and the databases, schemas, tables, columns and users within
* it. This makes information_schema a useful tool to use across the drupal
* database drivers and is used by a few different functions. The function below
* describes the conditions to be meet when querying information_schema.tables
* for drupal tables or information associated with drupal tables. Even though
* this is the standard method, not all databases follow standards and so this
* method should be overwritten by a database driver if the database provider
* uses alternate methods. Because information_schema.tables is used in a few
* different functions, a database driver will only need to override this function
* to make all the others work. For example see
* core/includes/databases/mysql/schema.inc.
*
* @param $table_name
* The name of the table in question.
* @param $operator
* The operator to apply on the 'table' part of the condition.
* @param $add_prefix
* Boolean to indicate whether the table name needs to be prefixed.
*
* @return \Drupal\Core\Database\Query\Condition
* A Condition object.
*/
protected function buildTableNameCondition($table_name, $operator = '=', $add_prefix = TRUE) {
$info = $this->connection->getConnectionOptions();
// Retrieve the table name and schema
$table_info = $this->getPrefixInfo($table_name, $add_prefix);
$condition = new Condition('AND');
$condition->condition('table_catalog', $info['database']);
$condition->condition('table_schema', $table_info['schema']);
$condition->condition('table_name', $table_info['table'], $operator);
return $condition;
}
/**
* Check if a table exists.
*
* @param $table
* The name of the table in drupal (no prefixing).
*
* @return
* TRUE if the given table exists, otherwise FALSE.
*/
public function tableExists($table) {
$condition = $this->buildTableNameCondition($table);
$condition->compile($this->connection, $this);
// Normally, we would heartily discourage the use of string
// concatenation for conditionals like this however, we
// couldn't use db_select() here because it would prefix
// information_schema.tables and the query would fail.
// Don't use {} around information_schema.tables table.
return (bool) $this->connection->query("SELECT 1 FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchField();
}
/**
* Find all tables that are like the specified base table name.
*
* @param $table_expression
* An SQL expression, for example "simpletest%" (without the quotes).
* BEWARE: this is not prefixed, the caller should take care of that.
*
* @return
* Array, both the keys and the values are the matching tables.
*/
public function findTables($table_expression) {
$condition = $this->buildTableNameCondition($table_expression, 'LIKE', FALSE);
$condition->compile($this->connection, $this);
// Normally, we would heartily discourage the use of string
// concatenation for conditionals like this however, we
// couldn't use db_select() here because it would prefix
// information_schema.tables and the query would fail.
// Don't use {} around information_schema.tables table.
return $this->connection->query("SELECT table_name FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchAllKeyed(0, 0);
}
/**
* Check if a column exists in the given table.
*
* @param $table
* The name of the table in drupal (no prefixing).
* @param $name
* The name of the column.
*
* @return
* TRUE if the given column exists, otherwise FALSE.
*/
public function fieldExists($table, $column) {
$condition = $this->buildTableNameCondition($table);
$condition->condition('column_name', $column);
$condition->compile($this->connection, $this);
// Normally, we would heartily discourage the use of string
// concatenation for conditionals like this however, we
// couldn't use db_select() here because it would prefix
// information_schema.tables and the query would fail.
// Don't use {} around information_schema.columns table.
return (bool) $this->connection->query("SELECT 1 FROM information_schema.columns WHERE " . (string) $condition, $condition->arguments())->fetchField();
}
/**
* Returns a mapping of Drupal schema field names to DB-native field types.
*
* Because different field types do not map 1:1 between databases, Drupal has
* its own normalized field type names. This function returns a driver-specific
* mapping table from Drupal names to the native names for each database.
*
* @return array
* An array of Schema API field types to driver-specific field types.
*/
abstract public function getFieldTypeMap();
/**
* Rename a table.
*
* @param $table
* The table to be renamed.
* @param $new_name
* The new name for the table.
*
* @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
* If the specified table doesn't exist.
* @throws \Drupal\Core\Database\SchemaObjectExistsException
* If a table with the specified new name already exists.
*/
abstract public function renameTable($table, $new_name);
/**
* Drop a table.
*
* @param $table
* The table to be dropped.
*
* @return
* TRUE if the table was successfully dropped, FALSE if there was no table
* by that name to begin with.
*/
abstract public function dropTable($table);
/**
* Add a new field to a table.
*
* @param $table
* Name of the table to be altered.
* @param $field
* Name of the field to be added.
* @param $spec
* The field specification array, as taken from a schema definition.
* The specification may also contain the key 'initial', the newly
* created field 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 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 db_change_field() for more
* explanation why.
*
* @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
* If the specified table doesn't exist.
* @throws \Drupal\Core\Database\SchemaObjectExistsException
* If the specified table already has a field by that name.
*/
abstract public function addField($table, $field, $spec, $keys_new = array());
/**
* Drop a field.
*
* @param $table
* The table to be altered.
* @param $field
* The field to be dropped.
*
* @return
* TRUE if the field was successfully dropped, FALSE if there was no field
* by that name to begin with.
*/
abstract public function dropField($table, $field);
/**
* Set the default value for a field.
*
* @param $table
* The table to be altered.
* @param $field
* The field to be altered.
* @param $default
* Default value to be set. NULL for 'default NULL'.
*
* @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
* If the specified table or field doesn't exist.
*/
abstract public function fieldSetDefault($table, $field, $default);
/**
* Set a field to have no default value.
*
* @param $table
* The table to be altered.
* @param $field
* The field to be altered.
*
* @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
* If the specified table or field doesn't exist.
*/
abstract public function fieldSetNoDefault($table, $field);
/**
* Checks if an index exists in the given table.
*
* @param $table
* The name of the table in drupal (no prefixing).
* @param $name
* The name of the index in drupal (no prefixing).
*
* @return
* TRUE if the given index exists, otherwise FALSE.
*/
abstract public function indexExists($table, $name);
/**
* Add a primary key.
*
* @param $table
* The table to be altered.
* @param $fields
* Fields for the primary key.
*
* @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
* If the specified table doesn't exist.
* @throws \Drupal\Core\Database\SchemaObjectExistsException
* If the specified table already has a primary key.
*/
abstract public function addPrimaryKey($table, $fields);
/**
* Drop the primary key.
*
* @param $table
* The table to be altered.
*
* @return
* TRUE if the primary key was successfully dropped, FALSE if there was no
* primary key on this table to begin with.
*/
abstract public function dropPrimaryKey($table);
/**
* Add a unique key.
*
* @param $table
* The table to be altered.
* @param $name
* The name of the key.
* @param $fields
* An array of field names.
*
* @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
* If the specified table doesn't exist.
* @throws \Drupal\Core\Database\SchemaObjectExistsException
* If the specified table already has a key by that name.
*/
abstract public function addUniqueKey($table, $name, $fields);
/**
* Drop a unique key.
*
* @param $table
* The table to be altered.
* @param $name
* The name of the key.
*
* @return
* TRUE if the key was successfully dropped, FALSE if there was no key by
* that name to begin with.
*/
abstract public function dropUniqueKey($table, $name);
/**
* Add an index.
*
* @param $table
* The table to be altered.
* @param $name
* The name of the index.
* @param $fields
* An array of field names or field information; if field information is
* passed, it's an array whose first element is the field name and whose
* second is the maximum length in the index. For example, the following
* will use the full length of the `foo` field, but limit the `bar` field to
* 4 characters:
* @code
* $fields = ['foo', ['bar', 4]];
* @endcode
*
* @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
* If the specified table doesn't exist.
* @throws \Drupal\Core\Database\SchemaObjectExistsException
* If the specified table already has an index by that name.
*/
abstract public function addIndex($table, $name, $fields);
/**
* Drop an index.
*
* @param $table
* The table to be altered.
* @param $name
* The name of the index.
*
* @return
* TRUE if the index was successfully dropped, FALSE if there was no index
* by that name to begin with.
*/
abstract public function dropIndex($table, $name);
/**
* Change a field definition.
*
* IMPORTANT NOTE: To maintain database portability, you have to explicitly
* recreate all indices and primary keys that are using the changed field.
*
* That means that you have to drop all affected keys and indexes with
* db_drop_{primary_key,unique_key,index}() before calling db_change_field().
* To recreate the keys and indices, pass the key definitions as the
* optional $keys_new argument directly to db_change_field().
*
* For example, suppose you have:
* @code
* $schema['foo'] = array(
* 'fields' => array(
* 'bar' => array('type' => 'int', 'not null' => TRUE)
* ),
* 'primary key' => array('bar')
* );
* @endcode
* and you want to change foo.bar to be type serial, leaving it as the
* primary key. The correct sequence is:
* @code
* db_drop_primary_key('foo');
* db_change_field('foo', 'bar', 'bar',
* array('type' => 'serial', 'not null' => TRUE),
* array('primary key' => array('bar')));
* @endcode
*
* The reasons for this are due to the different database engines:
*
* On PostgreSQL, changing a field definition involves adding a new field
* and dropping an old one which* causes any indices, primary keys and
* sequences (from serial-type fields) that use the changed field to be dropped.
*
* On MySQL, all type 'serial' fields must be part of at least one key
* or index as soon as they are created. You cannot use
* db_add_{primary_key,unique_key,index}() for this purpose because
* the ALTER TABLE command will fail to add the column without a key
* or index specification. The solution is to use the optional
* $keys_new argument to create the key or index at the same time as
* field.
*
* You could use db_add_{primary_key,unique_key,index}() in all cases
* unless you are converting a field to be type serial. You can use
* the $keys_new argument in all cases.
*
* @param $table
* Name of the table.
* @param $field
* Name of the field to change.
* @param $field_new
* New name for the field (set to the same as $field if you don't want to change the 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 with changing the field. The format is the same as a
* table specification but without the 'fields' element.
*
* @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
* If the specified table or source field doesn't exist.
* @throws \Drupal\Core\Database\SchemaObjectExistsException
* If the specified destination field already exists.
*/
abstract public function changeField($table, $field, $field_new, $spec, $keys_new = array());
/**
* Create a new table from a Drupal table definition.
*
* @param $name
* The name of the table to create.
* @param $table
* A Schema API table definition array.
*
* @throws \Drupal\Core\Database\SchemaObjectExistsException
* If the specified table already exists.
*/
public function createTable($name, $table) {
if ($this->tableExists($name)) {
throw new SchemaObjectExistsException(t('Table @name already exists.', array('@name' => $name)));
}
$statements = $this->createTableSql($name, $table);
foreach ($statements as $statement) {
$this->connection->query($statement);
}
}
/**
* Return an array of field names from an array of key/index column specifiers.
*
* This is usually an identity function but if a key/index uses a column prefix
* specification, this function extracts just the name.
*
* @param $fields
* An array of key/index column specifiers.
*
* @return
* An array of field names.
*/
public function fieldNames($fields) {
$return = array();
foreach ($fields as $field) {
if (is_array($field)) {
$return[] = $field[0];
}
else {
$return[] = $field;
}
}
return $return;
}
/**
* Prepare a table or column comment for database query.
*
* @param $comment
* The comment string to prepare.
* @param $length
* Optional upper limit on the returned string length.
*
* @return
* The prepared comment.
*/
public function prepareComment($comment, $length = NULL) {
return $this->connection->quote($comment);
}
/**
* Return an escaped version of its parameter to be used as a default value
* on a column.
*
* @param mixed $value
* The value to be escaped (int, float, null or string).
*
* @return string|int|float
* The escaped value.
*/
protected function escapeDefaultValue($value) {
if (is_null($value)) {
return 'NULL';
}
return is_string($value) ? $this->connection->quote($value) : $value;
}
}

View file

@ -0,0 +1,13 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\SchemaException.
*/
namespace Drupal\Core\Database;
/**
* Base exception for Schema-related errors.
*/
class SchemaException extends \RuntimeException implements DatabaseException { }

View file

@ -0,0 +1,17 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\SchemaObjectDoesNotExistException.
*/
namespace Drupal\Core\Database;
/**
* Exception thrown if an object being modified doesn't exist yet.
*
* For example, this exception should be thrown whenever there is an attempt to
* modify a database table, field, or index that does not currently exist in
* the database schema.
*/
class SchemaObjectDoesNotExistException extends SchemaException implements DatabaseException { }

View file

@ -0,0 +1,17 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\SchemaObjectExistsException.
*/
namespace Drupal\Core\Database;
/**
* Exception thrown if an object being created already exists.
*
* For example, this exception should be thrown whenever there is an attempt to
* create a new database table, field, or index that already exists in the
* database schema.
*/
class SchemaObjectExistsException extends SchemaException implements DatabaseException { }

View file

@ -0,0 +1,130 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Statement.
*/
namespace Drupal\Core\Database;
/**
* Default implementation of StatementInterface.
*
* \PDO allows us to extend the \PDOStatement class to provide additional
* functionality beyond that offered by default. We do need extra
* functionality. By default, this class is not driver-specific. If a given
* driver needs to set a custom statement class, it may do so in its
* constructor.
*
* @see http://php.net/pdostatement
*/
class Statement extends \PDOStatement implements StatementInterface {
/**
* Reference to the database connection object for this statement.
*
* The name $dbh is inherited from \PDOStatement.
*
* @var \Drupal\Core\Database\Connection
*/
public $dbh;
/**
* Is rowCount() execution allowed.
*
* @var bool
*/
public $allowRowCount = FALSE;
protected function __construct(Connection $dbh) {
$this->dbh = $dbh;
$this->setFetchMode(\PDO::FETCH_OBJ);
}
public function execute($args = array(), $options = array()) {
if (isset($options['fetch'])) {
if (is_string($options['fetch'])) {
// \PDO::FETCH_PROPS_LATE tells __construct() to run before properties
// are added to the object.
$this->setFetchMode(\PDO::FETCH_CLASS | \PDO::FETCH_PROPS_LATE, $options['fetch']);
}
else {
$this->setFetchMode($options['fetch']);
}
}
$logger = $this->dbh->getLogger();
if (!empty($logger)) {
$query_start = microtime(TRUE);
}
$return = parent::execute($args);
if (!empty($logger)) {
$query_end = microtime(TRUE);
$logger->log($this, $args, $query_end - $query_start);
}
return $return;
}
public function getQueryString() {
return $this->queryString;
}
public function fetchCol($index = 0) {
return $this->fetchAll(\PDO::FETCH_COLUMN, $index);
}
public function fetchAllAssoc($key, $fetch = NULL) {
$return = array();
if (isset($fetch)) {
if (is_string($fetch)) {
$this->setFetchMode(\PDO::FETCH_CLASS, $fetch);
}
else {
$this->setFetchMode($fetch);
}
}
foreach ($this as $record) {
$record_key = is_object($record) ? $record->$key : $record[$key];
$return[$record_key] = $record;
}
return $return;
}
public function fetchAllKeyed($key_index = 0, $value_index = 1) {
$return = array();
$this->setFetchMode(\PDO::FETCH_NUM);
foreach ($this as $record) {
$return[$record[$key_index]] = $record[$value_index];
}
return $return;
}
public function fetchField($index = 0) {
// Call \PDOStatement::fetchColumn to fetch the field.
return $this->fetchColumn($index);
}
public function fetchAssoc() {
// Call \PDOStatement::fetch to fetch the row.
return $this->fetch(\PDO::FETCH_ASSOC);
}
/**
* {@inheritdoc}
*/
public function rowCount() {
// SELECT query should not use the method.
if ($this->allowRowCount) {
return parent::rowCount();
}
else {
throw new RowCountException();
}
}
}

View file

@ -0,0 +1,103 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\StatementEmpty.
*/
namespace Drupal\Core\Database;
/**
* Empty implementation of a database statement.
*
* This class satisfies the requirements of being a database statement/result
* object, but does not actually contain data. It is useful when developers
* need to safely return an "empty" result set without connecting to an actual
* database. Calling code can then treat it the same as if it were an actual
* result set that happens to contain no records.
*
* @see \Drupal\search\SearchQuery
*/
class StatementEmpty implements \Iterator, StatementInterface {
/**
* Is rowCount() execution allowed.
*
* @var bool
*/
public $allowRowCount = FALSE;
public function execute($args = array(), $options = array()) {
return FALSE;
}
public function getQueryString() {
return '';
}
public function rowCount() {
if ($this->allowRowCount) {
return 0;
}
throw new RowCountException();
}
public function setFetchMode($mode, $a1 = NULL, $a2 = array()) {
return;
}
public function fetch($mode = NULL, $cursor_orientation = NULL, $cursor_offset = NULL) {
return NULL;
}
public function fetchField($index = 0) {
return NULL;
}
public function fetchObject() {
return NULL;
}
public function fetchAssoc() {
return NULL;
}
function fetchAll($mode = NULL, $column_index = NULL, array $constructor_arguments = array()) {
return array();
}
public function fetchCol($index = 0) {
return array();
}
public function fetchAllKeyed($key_index = 0, $value_index = 1) {
return array();
}
public function fetchAllAssoc($key, $fetch = NULL) {
return array();
}
/* Implementations of Iterator. */
public function current() {
return NULL;
}
public function key() {
return NULL;
}
public function rewind() {
// Nothing to do: our DatabaseStatement can't be rewound.
}
public function next() {
// Do nothing, since this is an always-empty implementation.
}
public function valid() {
return FALSE;
}
}

View file

@ -0,0 +1,221 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\StatementInterface.
*/
namespace Drupal\Core\Database;
/**
* Represents a prepared statement.
*
* 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. See http://bugs.php.net/bug.php?id=42452 for more details.
*
* Child implementations should either extend PDOStatement:
* @code
* class Drupal\Core\Database\Driver\oracle\Statement extends PDOStatement implements Drupal\Core\Database\StatementInterface {}
* @endcode
* or define their own class. If defining their own class, they will also have
* to implement either the Iterator or IteratorAggregate interface before
* Drupal\Core\Database\StatementInterface:
* @code
* class Drupal\Core\Database\Driver\oracle\Statement implements Iterator, Drupal\Core\Database\StatementInterface {}
* @endcode
*
* @ingroup database
*/
interface StatementInterface extends \Traversable {
/**
* Constructs a new PDOStatement object.
*
* The PDO manual does not document this constructor, but when overriding the
* PDOStatement class with a custom without this constructor, PDO will throw
* the internal exception/warning:
*
* "PDO::query(): SQLSTATE[HY000]: General error: user-supplied statement does
* not accept constructor arguments"
*
* PDO enforces that the access type of this constructor must be protected,
* and lastly, it also enforces that a custom PDOStatement interface (like
* this) omits the constructor (declaring it results in fatal errors
* complaining about "the access type must not be public" if it is public, and
* "the access type must be omitted" if it is protected; i.e., conflicting
* statements). The access type has to be protected.
*/
//protected function __construct(Connection $dbh);
/**
* Executes a prepared statement
*
* @param $args
* An array of values with as many elements as there are bound parameters in
* the SQL statement being executed.
* @param $options
* An array of options for this query.
*
* @return
* TRUE on success, or FALSE on failure.
*/
public function execute($args = array(), $options = array());
/**
* Gets the query string of this statement.
*
* @return
* The query string, in its form with placeholders.
*/
public function getQueryString();
/**
* Returns the number of rows affected by the last SQL statement.
*
* @return
* The number of rows affected by the last DELETE, INSERT, or UPDATE
* statement executed or throws \Drupal\Core\Database\RowCountException
* if the last executed statement was SELECT.
*
* @throws \Drupal\Core\Database\RowCountException
*/
public function rowCount();
/**
* Sets the default fetch mode for this statement.
*
* See http://php.net/manual/pdo.constants.php for the definition of the
* constants used.
*
* @param $mode
* One of the PDO::FETCH_* constants.
* @param $a1
* An option depending of the fetch mode specified by $mode:
* - for PDO::FETCH_COLUMN, the index of the column to fetch
* - for PDO::FETCH_CLASS, the name of the class to create
* - for PDO::FETCH_INTO, the object to add the data to
* @param $a2
* If $mode is PDO::FETCH_CLASS, the optional arguments to pass to the
* constructor.
*/
// public function setFetchMode($mode, $a1 = NULL, $a2 = array());
/**
* Fetches the next row from a result set.
*
* See http://php.net/manual/pdo.constants.php for the definition of the
* constants used.
*
* @param $mode
* One of the PDO::FETCH_* constants.
* Default to what was specified by setFetchMode().
* @param $cursor_orientation
* Not implemented in all database drivers, don't use.
* @param $cursor_offset
* Not implemented in all database drivers, don't use.
*
* @return
* A result, formatted according to $mode.
*/
// public function fetch($mode = NULL, $cursor_orientation = NULL, $cursor_offset = NULL);
/**
* Returns a single field from the next record of a result set.
*
* @param $index
* The numeric index of the field to return. Defaults to the first field.
*
* @return
* A single field from the next record, or FALSE if there is no next record.
*/
public function fetchField($index = 0);
/**
* Fetches the next row and returns it as an object.
*
* The object will be of the class specified by StatementInterface::setFetchMode()
* or stdClass if not specified.
*/
// public function fetchObject();
/**
* Fetches the next row and returns it as an associative array.
*
* This method corresponds to PDOStatement::fetchObject(), but for associative
* arrays. For some reason PDOStatement does not have a corresponding array
* helper method, so one is added.
*
* @return
* An associative array, or FALSE if there is no next row.
*/
public function fetchAssoc();
/**
* Returns an array containing all of the result set rows.
*
* @param $mode
* One of the PDO::FETCH_* constants.
* @param $column_index
* If $mode is PDO::FETCH_COLUMN, the index of the column to fetch.
* @param $constructor_arguments
* If $mode is PDO::FETCH_CLASS, the arguments to pass to the constructor.
*
* @return
* An array of results.
*/
// function fetchAll($mode = NULL, $column_index = NULL, array $constructor_arguments);
/**
* Returns an entire single column of a result set as an indexed array.
*
* Note that this method will run the result set to the end.
*
* @param $index
* The index of the column number to fetch.
*
* @return
* An indexed array, or an empty array if there is no result set.
*/
public function fetchCol($index = 0);
/**
* Returns the entire result set as a single associative array.
*
* This method is only useful for two-column result sets. It will return an
* associative array where the key is one column from the result set and the
* value is another field. In most cases, the default of the first two columns
* is appropriate.
*
* Note that this method will run the result set to the end.
*
* @param $key_index
* The numeric index of the field to use as the array key.
* @param $value_index
* The numeric index of the field to use as the array value.
*
* @return
* An associative array, or an empty array if there is no result set.
*/
public function fetchAllKeyed($key_index = 0, $value_index = 1);
/**
* Returns the result set as an associative array keyed by the given field.
*
* If the given key appears multiple times, later records will overwrite
* earlier ones.
*
* @param $key
* The name of the field on which to index the array.
* @param $fetch
* The fetchmode to use. If set to PDO::FETCH_ASSOC, PDO::FETCH_NUM, or
* PDO::FETCH_BOTH the returned value with be an array of arrays. For any
* other value it will be an array of objects. By default, the fetch mode
* set for the query will be used.
*
* @return
* An associative array, or an empty array if there is no result set.
*/
public function fetchAllAssoc($key, $fetch = NULL);
}

View file

@ -0,0 +1,515 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\StatementPrefetch.
*/
namespace Drupal\Core\Database;
/**
* An implementation of StatementInterface that prefetches all data.
*
* This class behaves very similar to a \PDOStatement but as it always fetches
* every row it is possible to manipulate those results.
*/
class StatementPrefetch implements \Iterator, StatementInterface {
/**
* The query string.
*
* @var string
*/
protected $queryString;
/**
* Driver-specific options. Can be used by child classes.
*
* @var Array
*/
protected $driverOptions;
/**
* Reference to the Drupal database connection object for this statement.
*
* @var \Drupal\Core\Database\Connection
*/
public $dbh;
/**
* Reference to the PDO connection object for this statement.
*
* @var \PDO
*/
protected $pdoConnection;
/**
* Main data store.
*
* @var Array
*/
protected $data = array();
/**
* The current row, retrieved in \PDO::FETCH_ASSOC format.
*
* @var Array
*/
protected $currentRow = NULL;
/**
* The key of the current row.
*
* @var int
*/
protected $currentKey = NULL;
/**
* The list of column names in this result set.
*
* @var Array
*/
protected $columnNames = NULL;
/**
* The number of rows affected by the last query.
*
* @var int
*/
protected $rowCount = NULL;
/**
* The number of rows in this result set.
*
* @var int
*/
protected $resultRowCount = 0;
/**
* Holds the current fetch style (which will be used by the next fetch).
* @see \PDOStatement::fetch()
*
* @var int
*/
protected $fetchStyle = \PDO::FETCH_OBJ;
/**
* Holds supplementary current fetch options (which will be used by the next fetch).
*
* @var Array
*/
protected $fetchOptions = array(
'class' => 'stdClass',
'constructor_args' => array(),
'object' => NULL,
'column' => 0,
);
/**
* Holds the default fetch style.
*
* @var int
*/
protected $defaultFetchStyle = \PDO::FETCH_OBJ;
/**
* Holds supplementary default fetch options.
*
* @var Array
*/
protected $defaultFetchOptions = array(
'class' => 'stdClass',
'constructor_args' => array(),
'object' => NULL,
'column' => 0,
);
/**
* Is rowCount() execution allowed.
*
* @var bool
*/
public $allowRowCount = FALSE;
public function __construct(\PDO $pdo_connection, Connection $connection, $query, array $driver_options = array()) {
$this->pdoConnection = $pdo_connection;
$this->dbh = $connection;
$this->queryString = $query;
$this->driverOptions = $driver_options;
}
/**
* Executes a prepared statement.
*
* @param $args
* An array of values with as many elements as there are bound parameters in the SQL statement being executed.
* @param $options
* An array of options for this query.
* @return
* TRUE on success, or FALSE on failure.
*/
public function execute($args = array(), $options = array()) {
if (isset($options['fetch'])) {
if (is_string($options['fetch'])) {
// Default to an object. Note: db fields will be added to the object
// before the constructor is run. If you need to assign fields after
// the constructor is run. See https://www.drupal.org/node/315092.
$this->setFetchMode(\PDO::FETCH_CLASS, $options['fetch']);
}
else {
$this->setFetchMode($options['fetch']);
}
}
$logger = $this->dbh->getLogger();
if (!empty($logger)) {
$query_start = microtime(TRUE);
}
// Prepare the query.
$statement = $this->getStatement($this->queryString, $args);
if (!$statement) {
$this->throwPDOException();
}
$return = $statement->execute($args);
if (!$return) {
$this->throwPDOException();
}
if ($options['return'] == Database::RETURN_AFFECTED) {
$this->rowCount = $statement->rowCount();
}
// Fetch all the data from the reply, in order to release any lock
// as soon as possible.
$this->data = $statement->fetchAll(\PDO::FETCH_ASSOC);
// Destroy the statement as soon as possible. See the documentation of
// \Drupal\Core\Database\Driver\sqlite\Statement for an explanation.
unset($statement);
$this->resultRowCount = count($this->data);
if ($this->resultRowCount) {
$this->columnNames = array_keys($this->data[0]);
}
else {
$this->columnNames = array();
}
if (!empty($logger)) {
$query_end = microtime(TRUE);
$logger->log($this, $args, $query_end - $query_start);
}
// Initialize the first row in $this->currentRow.
$this->next();
return $return;
}
/**
* Throw a PDO Exception based on the last PDO error.
*/
protected function throwPDOException() {
$error_info = $this->dbh->errorInfo();
// We rebuild a message formatted in the same way as PDO.
$exception = new \PDOException("SQLSTATE[" . $error_info[0] . "]: General error " . $error_info[1] . ": " . $error_info[2]);
$exception->errorInfo = $error_info;
throw $exception;
}
/**
* Grab a PDOStatement object from a given query and its arguments.
*
* Some drivers (including SQLite) will need to perform some preparation
* themselves to get the statement right.
*
* @param $query
* The query.
* @param array $args
* An array of arguments.
* @return \PDOStatement
* A PDOStatement object.
*/
protected function getStatement($query, &$args = array()) {
return $this->dbh->prepare($query);
}
/**
* Return the object's SQL query string.
*/
public function getQueryString() {
return $this->queryString;
}
/**
* @see \PDOStatement::setFetchMode()
*/
public function setFetchMode($fetchStyle, $a2 = NULL, $a3 = NULL) {
$this->defaultFetchStyle = $fetchStyle;
switch ($fetchStyle) {
case \PDO::FETCH_CLASS:
$this->defaultFetchOptions['class'] = $a2;
if ($a3) {
$this->defaultFetchOptions['constructor_args'] = $a3;
}
break;
case \PDO::FETCH_COLUMN:
$this->defaultFetchOptions['column'] = $a2;
break;
case \PDO::FETCH_INTO:
$this->defaultFetchOptions['object'] = $a2;
break;
}
// Set the values for the next fetch.
$this->fetchStyle = $this->defaultFetchStyle;
$this->fetchOptions = $this->defaultFetchOptions;
}
/**
* Return the current row formatted according to the current fetch style.
*
* This is the core method of this class. It grabs the value at the current
* array position in $this->data and format it according to $this->fetchStyle
* and $this->fetchMode.
*
* @return
* The current row formatted as requested.
*/
public function current() {
if (isset($this->currentRow)) {
switch ($this->fetchStyle) {
case \PDO::FETCH_ASSOC:
return $this->currentRow;
case \PDO::FETCH_BOTH:
// \PDO::FETCH_BOTH returns an array indexed by both the column name
// and the column number.
return $this->currentRow + array_values($this->currentRow);
case \PDO::FETCH_NUM:
return array_values($this->currentRow);
case \PDO::FETCH_LAZY:
// We do not do lazy as everything is fetched already. Fallback to
// \PDO::FETCH_OBJ.
case \PDO::FETCH_OBJ:
return (object) $this->currentRow;
case \PDO::FETCH_CLASS | \PDO::FETCH_CLASSTYPE:
$class_name = array_unshift($this->currentRow);
// Deliberate no break.
case \PDO::FETCH_CLASS:
if (!isset($class_name)) {
$class_name = $this->fetchOptions['class'];
}
if (count($this->fetchOptions['constructor_args'])) {
$reflector = new \ReflectionClass($class_name);
$result = $reflector->newInstanceArgs($this->fetchOptions['constructor_args']);
}
else {
$result = new $class_name();
}
foreach ($this->currentRow as $k => $v) {
$result->$k = $v;
}
return $result;
case \PDO::FETCH_INTO:
foreach ($this->currentRow as $k => $v) {
$this->fetchOptions['object']->$k = $v;
}
return $this->fetchOptions['object'];
case \PDO::FETCH_COLUMN:
if (isset($this->columnNames[$this->fetchOptions['column']])) {
return $this->currentRow[$k][$this->columnNames[$this->fetchOptions['column']]];
}
else {
return;
}
}
}
}
/* Implementations of Iterator. */
public function key() {
return $this->currentKey;
}
public function rewind() {
// Nothing to do: our DatabaseStatement can't be rewound.
}
public function next() {
if (!empty($this->data)) {
$this->currentRow = reset($this->data);
$this->currentKey = key($this->data);
unset($this->data[$this->currentKey]);
}
else {
$this->currentRow = NULL;
}
}
public function valid() {
return isset($this->currentRow);
}
/**
* {@inheritdoc}
*/
public function rowCount() {
// SELECT query should not use the method.
if ($this->allowRowCount) {
return $this->rowCount;
}
else {
throw new RowCountException();
}
}
public function fetch($fetch_style = NULL, $cursor_orientation = \PDO::FETCH_ORI_NEXT, $cursor_offset = NULL) {
if (isset($this->currentRow)) {
// Set the fetch parameter.
$this->fetchStyle = isset($fetch_style) ? $fetch_style : $this->defaultFetchStyle;
$this->fetchOptions = $this->defaultFetchOptions;
// Grab the row in the format specified above.
$return = $this->current();
// Advance the cursor.
$this->next();
// Reset the fetch parameters to the value stored using setFetchMode().
$this->fetchStyle = $this->defaultFetchStyle;
$this->fetchOptions = $this->defaultFetchOptions;
return $return;
}
else {
return FALSE;
}
}
public function fetchColumn($index = 0) {
if (isset($this->currentRow) && isset($this->columnNames[$index])) {
// We grab the value directly from $this->data, and format it.
$return = $this->currentRow[$this->columnNames[$index]];
$this->next();
return $return;
}
else {
return FALSE;
}
}
public function fetchField($index = 0) {
return $this->fetchColumn($index);
}
public function fetchObject($class_name = NULL, $constructor_args = array()) {
if (isset($this->currentRow)) {
if (!isset($class_name)) {
// Directly cast to an object to avoid a function call.
$result = (object) $this->currentRow;
}
else {
$this->fetchStyle = \PDO::FETCH_CLASS;
$this->fetchOptions = array('constructor_args' => $constructor_args);
// Grab the row in the format specified above.
$result = $this->current();
// Reset the fetch parameters to the value stored using setFetchMode().
$this->fetchStyle = $this->defaultFetchStyle;
$this->fetchOptions = $this->defaultFetchOptions;
}
$this->next();
return $result;
}
else {
return FALSE;
}
}
public function fetchAssoc() {
if (isset($this->currentRow)) {
$result = $this->currentRow;
$this->next();
return $result;
}
else {
return FALSE;
}
}
public function fetchAll($fetch_style = NULL, $fetch_column = NULL, $constructor_args = NULL) {
$this->fetchStyle = isset($fetch_style) ? $fetch_style : $this->defaultFetchStyle;
$this->fetchOptions = $this->defaultFetchOptions;
if (isset($fetch_column)) {
$this->fetchOptions['column'] = $fetch_column;
}
if (isset($constructor_args)) {
$this->fetchOptions['constructor_args'] = $constructor_args;
}
$result = array();
// Traverse the array as PHP would have done.
while (isset($this->currentRow)) {
// Grab the row in the format specified above.
$result[] = $this->current();
$this->next();
}
// Reset the fetch parameters to the value stored using setFetchMode().
$this->fetchStyle = $this->defaultFetchStyle;
$this->fetchOptions = $this->defaultFetchOptions;
return $result;
}
public function fetchCol($index = 0) {
if (isset($this->columnNames[$index])) {
$result = array();
// Traverse the array as PHP would have done.
while (isset($this->currentRow)) {
$result[] = $this->currentRow[$this->columnNames[$index]];
$this->next();
}
return $result;
}
else {
return array();
}
}
public function fetchAllKeyed($key_index = 0, $value_index = 1) {
if (!isset($this->columnNames[$key_index]) || !isset($this->columnNames[$value_index]))
return array();
$key = $this->columnNames[$key_index];
$value = $this->columnNames[$value_index];
$result = array();
// Traverse the array as PHP would have done.
while (isset($this->currentRow)) {
$result[$this->currentRow[$key]] = $this->currentRow[$value];
$this->next();
}
return $result;
}
public function fetchAllAssoc($key, $fetch_style = NULL) {
$this->fetchStyle = isset($fetch_style) ? $fetch_style : $this->defaultFetchStyle;
$this->fetchOptions = $this->defaultFetchOptions;
$result = array();
// Traverse the array as PHP would have done.
while (isset($this->currentRow)) {
// Grab the row in its raw \PDO::FETCH_ASSOC format.
$result_row = $this->current();
$result[$this->currentRow[$key]] = $result_row;
$this->next();
}
// Reset the fetch parameters to the value stored using setFetchMode().
$this->fetchStyle = $this->defaultFetchStyle;
$this->fetchOptions = $this->defaultFetchOptions;
return $result;
}
}

View file

@ -0,0 +1,101 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\Transaction.
*/
namespace Drupal\Core\Database;
/**
* A wrapper class for creating and managing database transactions.
*
* Not all databases or database configurations support transactions. For
* example, MySQL MyISAM tables do not. It is also easy to begin a transaction
* and then forget to commit it, which can lead to connection errors when
* another transaction is started.
*
* This class acts as a wrapper for transactions. To begin a transaction,
* simply instantiate it. When the object goes out of scope and is destroyed
* it will automatically commit. It also will check to see if the specified
* connection supports transactions. If not, it will simply skip any transaction
* commands, allowing user-space code to proceed normally. The only difference
* is that rollbacks won't actually do anything.
*
* In the vast majority of cases, you should not instantiate this class
* directly. Instead, call ->startTransaction(), from the appropriate connection
* object.
*/
class Transaction {
/**
* The connection object for this transaction.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* A boolean value to indicate whether this transaction has been rolled back.
*
* @var bool
*/
protected $rolledBack = FALSE;
/**
* The name of the transaction.
*
* This is used to label the transaction savepoint. It will be overridden to
* 'drupal_transaction' if there is no transaction depth.
*/
protected $name;
public function __construct(Connection $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()) {
$this->name = 'drupal_transaction';
}
// Within transactions, savepoints are used. Each savepoint requires a
// name. So if no name is present we need to create one.
elseif (!$name) {
$this->name = 'savepoint_' . $depth;
}
else {
$this->name = $name;
}
$this->connection->pushTransaction($this->name);
}
public function __destruct() {
// If we rolled back then the transaction would have already been popped.
if (!$this->rolledBack) {
$this->connection->popTransaction($this->name);
}
}
/**
* Retrieves the name of the transaction or savepoint.
*/
public function name() {
return $this->name;
}
/**
* Rolls back the current transaction.
*
* This is just a wrapper method to rollback whatever transaction stack we are
* currently in, which is managed by the connection object itself. Note that
* logging (preferable with watchdog_exception()) needs to happen after a
* transaction has been rolled back or the log messages will be rolled back
* too.
*
* @see \Drupal\Core\Database\Connection::rollback()
* @see watchdog_exception()
*/
public function rollback() {
$this->rolledBack = TRUE;
$this->connection->rollback($this->name);
}
}

View file

@ -0,0 +1,13 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\TransactionCommitFailedException.
*/
namespace Drupal\Core\Database;
/**
* Exception thrown when a commit() function fails.
*/
class TransactionCommitFailedException extends TransactionException implements DatabaseException { }

View file

@ -0,0 +1,13 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\TransactionException.
*/
namespace Drupal\Core\Database;
/**
* Exception thrown by an error in a database transaction.
*/
class TransactionException extends \RuntimeException implements DatabaseException { }

View file

@ -0,0 +1,16 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\TransactionExplicitCommitNotAllowedException.
*/
namespace Drupal\Core\Database;
/**
* Exception to deny attempts to explicitly manage transactions.
*
* This exception will be thrown when the PDO connection commit() is called.
* Code should never call this method directly.
*/
class TransactionExplicitCommitNotAllowedException extends TransactionException implements DatabaseException { }

View file

@ -0,0 +1,13 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\TransactionNameNonUniqueException.
*/
namespace Drupal\Core\Database;
/**
* Exception thrown when a savepoint or transaction name occurs twice.
*/
class TransactionNameNonUniqueException extends TransactionException implements DatabaseException { }

View file

@ -0,0 +1,13 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\TransactionNoActiveException.
*/
namespace Drupal\Core\Database;
/**
* Exception for when popTransaction() is called with no active transaction.
*/
class TransactionNoActiveException extends TransactionException implements DatabaseException { }

View file

@ -0,0 +1,13 @@
<?php
/**
* @file
* Contains \Drupal\Core\Database\TransactionOutOfOrderException.
*/
namespace Drupal\Core\Database;
/**
* Exception thrown when a rollback() resulted in other active transactions being rolled-back.
*/
class TransactionOutOfOrderException extends TransactionException implements DatabaseException { }

View file

@ -0,0 +1,537 @@
<?php
/**
* @file
* Hooks related to the Database system and the Schema API.
*/
/**
* @defgroup database Database abstraction layer
* @{
* Allow the use of different database servers using the same code base.
*
* @section sec_intro Overview
* Drupal's database abstraction layer provides a unified database query API
* that can query different underlying databases. It is built upon PHP's
* PDO (PHP Data Objects) database API, and inherits much of its syntax and
* semantics. Besides providing a unified API for database queries, the
* database abstraction layer also provides a structured way to construct
* complex queries, and it protects the database by using good security
* practices.
*
* For more detailed information on the database abstraction layer, see
* https://www.drupal.org/developing/api/database.
*
* @section sec_entity Querying entities
* Any query on Drupal entities or fields should use the Entity Query API. See
* the @link entity_api entity API topic @endlink for more information.
*
* @section sec_simple Simple SELECT database queries
* For simple SELECT queries that do not involve entities, the Drupal database
* abstraction layer provides the functions db_query() and db_query_range(),
* which execute SELECT queries (optionally with range limits) and return result
* sets that you can iterate over using foreach loops. (The result sets are
* objects implementing the \Drupal\Core\Database\StatementInterface interface.)
* You can use the simple query functions for query strings that are not
* dynamic (except for placeholders, see below), and that you are certain will
* work in any database engine. See @ref sec_dynamic below if you have a more
* complex query, or a query whose syntax would be different in some databases.
*
* As a note, db_query() and similar functions are wrappers on connection object
* methods. In most classes, you should use dependency injection and the
* database connection object instead of these wrappers; See @ref sec_connection
* below for details.
*
* To use the simple database query functions, you will need to make a couple of
* modifications to your bare SQL query:
* - Enclose your table name in {}. Drupal allows site builders to use
* database table name prefixes, so you cannot be sure what the actual
* name of the table will be. So, use the name that is in the hook_schema(),
* enclosed in {}, and Drupal will calculate the right name.
* - Instead of putting values for conditions into the query, use placeholders.
* The placeholders are named and start with :, and they take the place of
* putting variables directly into the query, to protect against SQL
* injection attacks.
* - LIMIT syntax differs between databases, so if you have a ranged query,
* use db_query_range() instead of db_query().
*
* For example, if the query you want to run is:
* @code
* SELECT e.id, e.title, e.created FROM example e WHERE e.uid = $uid
* ORDER BY e.created DESC LIMIT 0, 10;
* @endcode
* you would do it like this:
* @code
* $result = db_query_range('SELECT e.id, e.title, e.created
* FROM {example} e
* WHERE e.uid = :uid
* ORDER BY e.created DESC',
* 0, 10, array(':uid' => $uid));
* foreach ($result as $record) {
* // Perform operations on $record->title, etc. here.
* }
* @endcode
*
* Note that if your query has a string condition, like:
* @code
* WHERE e.my_field = 'foo'
* @endcode
* when you convert it to placeholders, omit the quotes:
* @code
* WHERE e.my_field = :my_field
* ... array(':my_field' => 'foo') ...
* @endcode
*
* @section sec_dynamic Dynamic SELECT queries
* For SELECT queries where the simple query API described in @ref sec_simple
* will not work well, you need to use the dynamic query API. However, you
* should still use the Entity Query API if your query involves entities or
* fields (see the @link entity_api Entity API topic @endlink for more on
* entity queries).
*
* As a note, db_select() and similar functions are wrappers on connection
* object methods. In most classes, you should use dependency injection and the
* database connection object instead of these wrappers; See @ref sec_connection
* below for details.
*
* The dynamic query API lets you build up a query dynamically using method
* calls. As an illustration, the query example from @ref sec_simple above
* would be:
* @code
* $result = db_select('example', 'e')
* ->fields('e', array('id', 'title', 'created'))
* ->condition('e.uid', $uid)
* ->orderBy('e.created', 'DESC')
* ->range(0, 10)
* ->execute();
* @endcode
*
* There are also methods to join to other tables, add fields with aliases,
* isNull() to have a @code WHERE e.foo IS NULL @endcode condition, etc. See
* https://www.drupal.org/developing/api/database for many more details.
*
* One note on chaining: It is common in the dynamic database API to chain
* method calls (as illustrated here), because most of the query methods modify
* the query object and then return the modified query as their return
* value. However, there are some important exceptions; these methods (and some
* others) do not support chaining:
* - join(), innerJoin(), etc.: These methods return the joined table alias.
* - addField(): This method returns the field alias.
* Check the documentation for the query method you are using to see if it
* returns the query or something else, and only chain methods that return the
* query.
*
* @section_insert INSERT, UPDATE, and DELETE queries
* INSERT, UPDATE, and DELETE queries need special care in order to behave
* consistently across databases; you should never use db_query() to run
* an INSERT, UPDATE, or DELETE query. Instead, use functions db_insert(),
* db_update(), and db_delete() to obtain a base query on your table, and then
* add dynamic conditions (as illustrated in @ref sec_dynamic above).
*
* As a note, db_insert() and similar functions are wrappers on connection
* object methods. In most classes, you should use dependency injection and the
* database connection object instead of these wrappers; See @ref sec_connection
* below for details.
*
* For example, if your query is:
* @code
* INSERT INTO example (id, uid, path, name) VALUES (1, 2, 'path', 'Name');
* @endcode
* You can execute it via:
* @code
* $fields = array('id' => 1, 'uid' => 2, 'path' => 'path', 'name' => 'Name');
* db_insert('example')
* ->fields($fields)
* ->execute();
* @endcode
*
* @section sec_transaction Transactions
* Drupal supports transactions, including a transparent fallback for
* databases that do not support transactions. To start a new transaction,
* call @code $txn = db_transaction(); @endcode The transaction will
* remain open for as long as the variable $txn remains in scope; when $txn is
* destroyed, the transaction will be committed. If your transaction is nested
* inside of another then Drupal will track each transaction and only commit
* the outer-most transaction when the last transaction object goes out out of
* scope (when all relevant queries have completed successfully).
*
* Example:
* @code
* function my_transaction_function() {
* // The transaction opens here.
* $txn = db_transaction();
*
* try {
* $id = db_insert('example')
* ->fields(array(
* 'field1' => 'mystring',
* 'field2' => 5,
* ))
* ->execute();
*
* my_other_function($id);
*
* return $id;
* }
* catch (Exception $e) {
* // Something went wrong somewhere, so roll back now.
* $txn->rollback();
* // Log the exception to watchdog.
* watchdog_exception('type', $e);
* }
*
* // $txn goes out of scope here. Unless the transaction was rolled back, it
* // gets automatically committed here.
* }
*
* function my_other_function($id) {
* // The transaction is still open here.
*
* if ($id % 2 == 0) {
* db_update('example')
* ->condition('id', $id)
* ->fields(array('field2' => 10))
* ->execute();
* }
* }
* @endcode
*
* @section sec_connection Database connection objects
* The examples here all use functions like db_select() and db_query(), which
* can be called from any Drupal method or function code. In some classes, you
* may already have a database connection object in a member variable, or it may
* be passed into a class constructor via dependency injection. If that is the
* case, you can look at the code for db_select() and the other functions to see
* how to get a query object from your connection variable. For example:
* @code
* $query = $connection->select('example', 'e');
* @endcode
* would be the equivalent of
* @code
* $query = db_select('example', 'e');
* @endcode
* if you had a connection object variable $connection available to use. See
* also the @link container Services and Dependency Injection topic. @endlink
*
* @see https://www.drupal.org/developing/api/database
* @see entity_api
* @see schemaapi
*
* @}
*/
/**
* @defgroup schemaapi Schema API
* @{
* API to handle database schemas.
*
* A Drupal schema definition is an array structure representing one or
* more tables and their related keys and indexes. A schema is defined by
* hook_schema(), which usually lives in a modulename.install file.
*
* By implementing hook_schema() and specifying the tables your module
* declares, you can easily create and drop these tables on all
* supported database engines. You don't have to deal with the
* different SQL dialects for table creation and alteration of the
* supported database engines.
*
* hook_schema() should return an array with a key for each table that
* the module defines.
*
* The following keys are defined:
* - 'description': A string in non-markup plain text describing this table
* and its purpose. References to other tables should be enclosed in
* curly-brackets. For example, the node_field_revision table
* description field might contain "Stores per-revision title and
* body data for each {node}."
* - 'fields': An associative array ('fieldname' => specification)
* that describes the table's database columns. The specification
* is also an array. The following specification parameters are defined:
* - 'description': A string in non-markup plain text describing this field
* and its purpose. References to other tables should be enclosed in
* curly-brackets. For example, the node table vid field
* description might contain "Always holds the largest (most
* recent) {node_field_revision}.vid value for this nid."
* - 'type': The generic datatype: 'char', 'varchar', 'text', 'blob', 'int',
* 'float', 'numeric', or 'serial'. Most types just map to the according
* database engine specific datatypes. Use 'serial' for auto incrementing
* fields. This will expand to 'INT auto_increment' on MySQL.
* A special 'varchar_ascii' type is also available for limiting machine
* name field to US ASCII characters.
* - 'mysql_type', 'pgsql_type', 'sqlite_type', etc.: If you need to
* use a record type not included in the officially supported list
* of types above, you can specify a type for each database
* backend. In this case, you can leave out the type parameter,
* but be advised that your schema will fail to load on backends that
* do not have a type specified. A possible solution can be to
* use the "text" type as a fallback.
* - 'serialize': A boolean indicating whether the field will be stored as
* a serialized string.
* - 'size': The data size: 'tiny', 'small', 'medium', 'normal',
* 'big'. This is a hint about the largest value the field will
* store and determines which of the database engine specific
* datatypes will be used (e.g. on MySQL, TINYINT vs. INT vs. BIGINT).
* 'normal', the default, selects the base type (e.g. on MySQL,
* INT, VARCHAR, BLOB, etc.).
* Not all sizes are available for all data types. See
* DatabaseSchema::getFieldTypeMap() for possible combinations.
* - 'not null': If true, no NULL values will be allowed in this
* database column. Defaults to false.
* - 'default': The field's default value. The PHP type of the
* value matters: '', '0', and 0 are all different. If you
* specify '0' as the default value for a type 'int' field it
* will not work because '0' is a string containing the
* character "zero", not an integer.
* - 'length': The maximal length of a type 'char', 'varchar' or 'text'
* field. Ignored for other field types.
* - 'unsigned': A boolean indicating whether a type 'int', 'float'
* and 'numeric' only is signed or unsigned. Defaults to
* FALSE. Ignored for other field types.
* - 'precision', 'scale': For type 'numeric' fields, indicates
* 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', 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' =>
* specification). Each specification is an array of one or more
* key column specifiers (see below) that form a unique key on the table.
* - 'foreign keys': An associative array of relations ('my_relation' =>
* 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').
* - '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
* table.
*
* A key column specifier is either a string naming a column or an
* array of two elements, column name and length, specifying a prefix
* of the named column.
*
* As an example, here is a SUBSET of the schema definition for
* Drupal's 'node' table. It show four fields (nid, vid, type, and
* title), the primary key on field 'nid', a unique key named 'vid' on
* field 'vid', and two indexes, one named 'nid' on field 'nid' and
* one named 'node_title_type' on the field 'title' and the first four
* bytes of the field 'type':
*
* @code
* $schema['node'] = array(
* 'description' => 'The base table for nodes.',
* 'fields' => array(
* 'nid' => array('type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE),
* 'vid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE,'default' => 0),
* 'type' => array('type' => 'varchar','length' => 32,'not null' => TRUE, 'default' => ''),
* 'language' => array('type' => 'varchar','length' => 12,'not null' => TRUE,'default' => ''),
* 'title' => array('type' => 'varchar','length' => 255,'not null' => TRUE, 'default' => ''),
* 'uid' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
* 'status' => array('type' => 'int', 'not null' => TRUE, 'default' => 1),
* 'created' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
* 'changed' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
* 'comment' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
* 'promote' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
* 'moderate' => array('type' => 'int', 'not null' => TRUE,'default' => 0),
* 'sticky' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
* 'translate' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
* ),
* 'indexes' => array(
* 'node_changed' => array('changed'),
* 'node_created' => array('created'),
* 'node_moderate' => array('moderate'),
* 'node_frontpage' => array('promote', 'status', 'sticky', 'created'),
* 'node_status_type' => array('status', 'type', 'nid'),
* 'node_title_type' => array('title', array('type', 4)),
* 'node_type' => array(array('type', 4)),
* 'uid' => array('uid'),
* 'translate' => array('translate'),
* ),
* 'unique keys' => array(
* 'vid' => array('vid'),
* ),
* 'foreign keys' => array(
* 'node_revision' => array(
* 'table' => 'node_field_revision',
* 'columns' => array('vid' => 'vid'),
* ),
* 'node_author' => array(
* 'table' => 'users',
* 'columns' => array('uid' => 'uid'),
* ),
* ),
* 'primary key' => array('nid'),
* );
* @endcode
*
* @see drupal_install_schema()
*
* @}
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Perform alterations to a structured query.
*
* Structured (aka dynamic) queries that have tags associated may be altered by any module
* before the query is executed.
*
* @param $query
* A Query object describing the composite parts of a SQL query.
*
* @see hook_query_TAG_alter()
* @see node_query_node_access_alter()
* @see AlterableInterface
* @see SelectInterface
*
* @ingroup database
*/
function hook_query_alter(Drupal\Core\Database\Query\AlterableInterface $query) {
if ($query->hasTag('micro_limit')) {
$query->range(0, 2);
}
}
/**
* Perform alterations to a structured query for a given tag.
*
* @param $query
* An Query object describing the composite parts of a SQL query.
*
* @see hook_query_alter()
* @see node_query_node_access_alter()
* @see AlterableInterface
* @see SelectInterface
*
* @ingroup database
*/
function hook_query_TAG_alter(Drupal\Core\Database\Query\AlterableInterface $query) {
// Skip the extra expensive alterations if site has no node access control modules.
if (!node_access_view_all_nodes()) {
// Prevent duplicates records.
$query->distinct();
// The recognized operations are 'view', 'update', 'delete'.
if (!$op = $query->getMetaData('op')) {
$op = 'view';
}
// Skip the extra joins and conditions for node admins.
if (!\Drupal::currentUser()->hasPermission('bypass node access')) {
// The node_access table has the access grants for any given node.
$access_alias = $query->join('node_access', 'na', '%alias.nid = n.nid');
$or = db_or();
// If any grant exists for the specified user, then user has access to the node for the specified operation.
foreach (node_access_grants($op, $query->getMetaData('account')) as $realm => $gids) {
foreach ($gids as $gid) {
$or->condition(db_and()
->condition($access_alias . '.gid', $gid)
->condition($access_alias . '.realm', $realm)
);
}
}
if (count($or->conditions())) {
$query->condition($or);
}
$query->condition($access_alias . 'grant_' . $op, 1, '>=');
}
}
}
/**
* Define the current version of the database schema.
*
* A Drupal schema definition is an array structure representing one or more
* tables and their related keys and indexes. A schema is defined by
* hook_schema() which must live in your module's .install file.
*
* The tables declared by this hook will be automatically created when the
* module is installed, and removed when the module is uninstalled. This happens
* before hook_install() is invoked, and after hook_uninstall() is invoked,
* respectively.
*
* By declaring the tables used by your module via an implementation of
* hook_schema(), these tables will be available on all supported database
* engines. You don't have to deal with the different SQL dialects for table
* creation and alteration of the supported database engines.
*
* See the Schema API Handbook at https://www.drupal.org/node/146843 for details
* on schema definition structures.
*
* @return array
* A schema definition structure array. For each element of the
* array, the key is a table name and the value is a table structure
* definition.
*
* @ingroup schemaapi
*/
function hook_schema() {
$schema['node'] = array(
// Example (partial) specification for table "node".
'description' => 'The base table for nodes.',
'fields' => array(
'nid' => array(
'description' => 'The primary identifier for a node.',
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
),
'vid' => array(
'description' => 'The current {node_field_revision}.vid version identifier.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'type' => array(
'description' => 'The type of this node.',
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
),
'title' => array(
'description' => 'The node title.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
),
),
'indexes' => array(
'node_changed' => array('changed'),
'node_created' => array('created'),
),
'unique keys' => array(
'nid_vid' => array('nid', 'vid'),
'vid' => array('vid'),
),
'foreign keys' => array(
'node_revision' => array(
'table' => 'node_field_revision',
'columns' => array('vid' => 'vid'),
),
'node_author' => array(
'table' => 'users',
'columns' => array('uid' => 'uid'),
),
),
'primary key' => array('nid'),
);
return $schema;
}
/**
* @} End of "addtogroup hooks".
*/