diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index 027e070a93af167a30ff25ae2fd535cd3c813985..52a8cb6a45b8d8c0067448d57295a6d48fd7c0ef 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -1,5 +1,6 @@ <?php +use Drupal\Core\Database\Database; use Symfony\Component\ClassLoader\UniversalClassLoader; use Symfony\Component\ClassLoader\ApcUniversalClassLoader; diff --git a/core/includes/common.inc b/core/includes/common.inc index 757a0c642086f541288afba4329138f0902b1ab1..17f83ad63656ed349250af6f9e98d3a901003d0a 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -1,5 +1,7 @@ <?php +use Drupal\Core\Database\Database; + /** * @file * Common functions that many Drupal modules will need to reference. diff --git a/core/includes/database/database.inc b/core/includes/database/database.inc index 7c96e070b006504551fd2373ed269b71703bdff7..c2df94988ffd6602810cb96ca3623a9d8deab81e 100644 --- a/core/includes/database/database.inc +++ b/core/includes/database/database.inc @@ -1,5 +1,8 @@ <?php +use Drupal\Core\Database\Database; +use Drupal\Core\Database\Query\Condition; + /** * @file * Core systems for the database layer. @@ -171,2109 +174,6 @@ */ -/** - * Base Database API class. - * - * This class provides a Drupal-specific extension of the PDO database - * abstraction class in PHP. Every database driver implementation must provide a - * concrete implementation of it to support special handling required by that - * database. - * - * @see http://php.net/manual/en/book.pdo.php - */ -abstract class DatabaseConnection extends PDO { - - /** - * The database target this connection is for. - * - * We need this information for later auditing and logging. - * - * @var string - */ - protected $target = NULL; - - /** - * The key representing this connection. - * - * The key is a unique string which identifies a database connection. A - * connection can be a single server or a cluster of master and slaves (use - * target to pick between master and slave). - * - * @var string - */ - protected $key = NULL; - - /** - * The current database logging object for this connection. - * - * @var DatabaseLog - */ - protected $logger = NULL; - - /** - * Tracks the number of "layers" of transactions currently active. - * - * On many databases transactions cannot nest. Instead, we track - * nested calls to transactions and collapse them into a single - * transaction. - * - * @var array - */ - protected $transactionLayers = array(); - - /** - * Index of what driver-specific class to use for various operations. - * - * @var array - */ - protected $driverClasses = array(); - - /** - * The name of the Statement class for this connection. - * - * @var string - */ - protected $statementClass = 'DatabaseStatementBase'; - - /** - * Whether this database connection supports transactions. - * - * @var bool - */ - protected $transactionSupport = TRUE; - - /** - * Whether this database connection supports transactional DDL. - * - * Set to FALSE by default because few databases support this feature. - * - * @var bool - */ - protected $transactionalDDLSupport = FALSE; - - /** - * An index used to generate unique temporary table names. - * - * @var integer - */ - protected $temporaryNameIndex = 0; - - /** - * The connection information for this connection object. - * - * @var array - */ - protected $connectionOptions = array(); - - /** - * The schema object for this connection. - * - * @var object - */ - protected $schema = NULL; - - /** - * The prefixes used by this database connection. - * - * @var array - */ - protected $prefixes = array(); - - /** - * List of search values for use in prefixTables(). - * - * @var array - */ - protected $prefixSearch = array(); - - /** - * List of replacement values for use in prefixTables(). - * - * @var array - */ - protected $prefixReplace = array(); - - function __construct($dsn, $username, $password, $driver_options = array()) { - // Initialize and prepare the connection prefix. - $this->setPrefix(isset($this->connectionOptions['prefix']) ? $this->connectionOptions['prefix'] : ''); - - // Because the other methods don't seem to work right. - $driver_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION; - - // Call PDO::__construct and PDO::setAttribute. - parent::__construct($dsn, $username, $password, $driver_options); - - // Set a specific PDOStatement class if the driver requires that. - if (!empty($this->statementClass)) { - $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array($this->statementClass, array($this))); - } - } - - /** - * Returns the default query options for any given query. - * - * A given query can be customized with a number of option flags in an - * associative array: - * - target: The database "target" against which to execute a query. Valid - * values are "default" or "slave". The system will first try to open a - * connection to a database specified with the user-supplied key. If one - * is not available, it will silently fall back to the "default" target. - * If multiple databases connections are specified with the same target, - * one will be selected at random for the duration of the request. - * - fetch: This element controls how rows from a result set will be - * returned. Legal values include PDO::FETCH_ASSOC, PDO::FETCH_BOTH, - * PDO::FETCH_OBJ, PDO::FETCH_NUM, or a string representing the name of a - * class. If a string is specified, each record will be fetched into a new - * object of that class. The behavior of all other values is defined by PDO. - * See http://php.net/manual/pdostatement.fetch.php - * - return: Depending on the type of query, different return values may be - * meaningful. This directive instructs the system which type of return - * value is desired. The system will generally set the correct value - * automatically, so it is extremely rare that a module developer will ever - * need to specify this value. Setting it incorrectly will likely lead to - * unpredictable results or fatal errors. Legal values include: - * - Database::RETURN_STATEMENT: Return the prepared statement object for - * the query. This is usually only meaningful for SELECT queries, where - * the statement object is how one accesses the result set returned by the - * query. - * - Database::RETURN_AFFECTED: Return the number of rows affected by an - * UPDATE or DELETE query. Be aware that means the number of rows actually - * changed, not the number of rows matched by the WHERE clause. - * - Database::RETURN_INSERT_ID: Return the sequence ID (primary key) - * created by an INSERT statement on a table that contains a serial - * column. - * - Database::RETURN_NULL: Do not return anything, as there is no - * meaningful value to return. That is the case for INSERT queries on - * tables that do not contain a serial column. - * - throw_exception: By default, the database system will catch any errors - * on a query as an Exception, log it, and then rethrow it so that code - * further up the call chain can take an appropriate action. To suppress - * that behavior and simply return NULL on failure, set this option to - * FALSE. - * - * @return - * An array of default query options. - */ - protected function defaultOptions() { - return array( - 'target' => 'default', - 'fetch' => PDO::FETCH_OBJ, - 'return' => Database::RETURN_STATEMENT, - 'throw_exception' => TRUE, - ); - } - - /** - * Returns the connection information for this connection object. - * - * Note that Database::getConnectionInfo() is for requesting information - * about an arbitrary database connection that is defined. This method - * is for requesting the connection information of this specific - * open connection object. - * - * @return - * An array of the connection information. The exact list of - * properties is driver-dependent. - */ - public function getConnectionOptions() { - return $this->connectionOptions; - } - - /** - * Set the list of prefixes used by this database connection. - * - * @param $prefix - * The prefixes, in any of the multiple forms documented in - * default.settings.php. - */ - protected function setPrefix($prefix) { - if (is_array($prefix)) { - $this->prefixes = $prefix + array('default' => ''); - } - else { - $this->prefixes = array('default' => $prefix); - } - - // Set up variables for use in prefixTables(). Replace table-specific - // prefixes first. - $this->prefixSearch = array(); - $this->prefixReplace = array(); - foreach ($this->prefixes as $key => $val) { - if ($key != 'default') { - $this->prefixSearch[] = '{' . $key . '}'; - $this->prefixReplace[] = $val . $key; - } - } - // Then replace remaining tables with the default prefix. - $this->prefixSearch[] = '{'; - $this->prefixReplace[] = $this->prefixes['default']; - $this->prefixSearch[] = '}'; - $this->prefixReplace[] = ''; - } - - /** - * Appends a database prefix to all tables in a query. - * - * Queries sent to Drupal should wrap all table names in curly brackets. This - * function searches for this syntax and adds Drupal's table prefix to all - * tables, allowing Drupal to coexist with other systems in the same database - * and/or schema if necessary. - * - * @param $sql - * A string containing a partial or entire SQL query. - * - * @return - * The properly-prefixed string. - */ - public function prefixTables($sql) { - return str_replace($this->prefixSearch, $this->prefixReplace, $sql); - } - - /** - * Find the prefix for a table. - * - * This function is for when you want to know the prefix of a table. This - * is not used in prefixTables due to performance reasons. - */ - public function tablePrefix($table = 'default') { - if (isset($this->prefixes[$table])) { - return $this->prefixes[$table]; - } - else { - return $this->prefixes['default']; - } - } - - /** - * Prepares a query string and returns the prepared statement. - * - * This method caches prepared statements, reusing them when - * possible. It also prefixes tables names enclosed in curly-braces. - * - * @param $query - * The query string as SQL, with curly-braces surrounding the - * table names. - * - * @return DatabaseStatementInterface - * A PDO prepared statement ready for its execute() method. - */ - public function prepareQuery($query) { - $query = $this->prefixTables($query); - - // Call PDO::prepare. - return parent::prepare($query); - } - - /** - * Tells this connection object what its target value is. - * - * This is needed for logging and auditing. It's sloppy to do in the - * constructor because the constructor for child classes has a different - * signature. We therefore also ensure that this function is only ever - * called once. - * - * @param $target - * The target this connection is for. Set to NULL (default) to disable - * logging entirely. - */ - public function setTarget($target = NULL) { - if (!isset($this->target)) { - $this->target = $target; - } - } - - /** - * Returns the target this connection is associated with. - * - * @return - * The target string of this connection. - */ - public function getTarget() { - return $this->target; - } - - /** - * Tells this connection object what its key is. - * - * @param $target - * The key this connection is for. - */ - public function setKey($key) { - if (!isset($this->key)) { - $this->key = $key; - } - } - - /** - * Returns the key this connection is associated with. - * - * @return - * The key of this connection. - */ - public function getKey() { - return $this->key; - } - - /** - * Associates a logging object with this connection. - * - * @param $logger - * The logging object we want to use. - */ - public function setLogger(DatabaseLog $logger) { - $this->logger = $logger; - } - - /** - * Gets the current logging object for this connection. - * - * @return DatabaseLog - * The current logging object for this connection. If there isn't one, - * NULL is returned. - */ - public function getLogger() { - return $this->logger; - } - - /** - * Creates the appropriate sequence name for a given table and serial field. - * - * This information is exposed to all database drivers, although it is only - * useful on some of them. This method is table prefix-aware. - * - * @param $table - * The table name to use for the sequence. - * @param $field - * The field name to use for the sequence. - * - * @return - * A table prefix-parsed string for the sequence name. - */ - public function makeSequenceName($table, $field) { - return $this->prefixTables('{' . $table . '}_' . $field . '_seq'); - } - - /** - * Flatten an array of query comments into a single comment string. - * - * The comment string will be sanitized to avoid SQL injection attacks. - * - * @param $comments - * An array of query comment strings. - * - * @return - * A sanitized comment string. - */ - public function makeComment($comments) { - if (empty($comments)) - return ''; - - // Flatten the array of comments. - $comment = implode('; ', $comments); - - // Sanitize the comment string so as to avoid SQL injection attacks. - return '/* ' . $this->filterComment($comment) . ' */ '; - } - - /** - * Sanitize a query comment string. - * - * Ensure a query comment does not include strings such as "* /" that might - * terminate the comment early. This avoids SQL injection attacks via the - * query comment. The comment strings in this example are separated by a - * space to avoid PHP parse errors. - * - * For example, the comment: - * @code - * db_update('example') - * ->condition('id', $id) - * ->fields(array('field2' => 10)) - * ->comment('Exploit * / DROP TABLE node; --') - * ->execute() - * @endcode - * - * Would result in the following SQL statement being generated: - * @code - * "/ * Exploit * / DROP TABLE node; -- * / UPDATE example SET field2=..." - * @endcode - * - * Unless the comment is sanitised first, the SQL server would drop the - * node table and ignore the rest of the SQL statement. - * - * @param $comment - * A query comment string. - * - * @return - * A sanitized version of the query comment string. - */ - protected function filterComment($comment = '') { - return preg_replace('/(\/\*\s*)|(\s*\*\/)/', '', $comment); - } - - /** - * Executes a query string against the database. - * - * This method provides a central handler for the actual execution of every - * query. All queries executed by Drupal are executed as PDO prepared - * statements. - * - * @param $query - * The query to execute. In most cases this will be a string containing - * an SQL query with placeholders. An already-prepared instance of - * DatabaseStatementInterface may also be passed in order to allow calling - * code to manually bind variables to a query. If a - * DatabaseStatementInterface is passed, the $args array will be ignored. - * It is extremely rare that module code will need to pass a statement - * object to this method. It is used primarily for database drivers for - * databases that require special LOB field handling. - * @param $args - * An array of arguments for the prepared statement. If the prepared - * statement uses ? placeholders, this array must be an indexed array. - * If it contains named placeholders, it must be an associative array. - * @param $options - * An associative array of options to control how the query is run. See - * the documentation for DatabaseConnection::defaultOptions() for details. - * - * @return DatabaseStatementInterface - * This method will return one of: the executed statement, the number of - * rows affected by the query (not the number matched), or the generated - * insert IT of the last query, depending on the value of - * $options['return']. Typically that value will be set by default or a - * query builder and should not be set by a user. If there is an error, - * this method will return NULL and may throw an exception if - * $options['throw_exception'] is TRUE. - * - * @throws PDOException - */ - public function query($query, array $args = array(), $options = array()) { - - // Use default values if not already set. - $options += $this->defaultOptions(); - - try { - // We allow either a pre-bound statement object or a literal string. - // In either case, we want to end up with an executed statement object, - // which we pass to PDOStatement::execute. - if ($query instanceof DatabaseStatementInterface) { - $stmt = $query; - $stmt->execute(NULL, $options); - } - else { - $this->expandArguments($query, $args); - $stmt = $this->prepareQuery($query); - $stmt->execute($args, $options); - } - - // Depending on the type of query we may need to return a different value. - // See DatabaseConnection::defaultOptions() for a description of each - // value. - switch ($options['return']) { - case Database::RETURN_STATEMENT: - return $stmt; - case Database::RETURN_AFFECTED: - return $stmt->rowCount(); - case Database::RETURN_INSERT_ID: - return $this->lastInsertId(); - case Database::RETURN_NULL: - return; - default: - throw new PDOException('Invalid return directive: ' . $options['return']); - } - } - catch (PDOException $e) { - if ($options['throw_exception']) { - // Add additional debug information. - if ($query instanceof DatabaseStatementInterface) { - $e->query_string = $stmt->getQueryString(); - } - else { - $e->query_string = $query; - } - $e->args = $args; - throw $e; - } - return NULL; - } - } - - /** - * Expands out shorthand placeholders. - * - * Drupal supports an alternate syntax for doing arrays of values. We - * therefore need to expand them out into a full, executable query string. - * - * @param $query - * The query string to modify. - * @param $args - * The arguments for the query. - * - * @return - * TRUE if the query was modified, FALSE otherwise. - */ - protected function expandArguments(&$query, &$args) { - $modified = FALSE; - - // If the placeholder value to insert is an array, assume that we need - // to expand it out into a comma-delimited set of placeholders. - foreach (array_filter($args, 'is_array') as $key => $data) { - $new_keys = array(); - foreach ($data as $i => $value) { - // This assumes that there are no other placeholders that use the same - // name. For example, if the array placeholder is defined as :example - // and there is already an :example_2 placeholder, this will generate - // a duplicate key. We do not account for that as the calling code - // is already broken if that happens. - $new_keys[$key . '_' . $i] = $value; - } - - // Update the query with the new placeholders. - // preg_replace is necessary to ensure the replacement does not affect - // placeholders that start with the same exact text. For example, if the - // query contains the placeholders :foo and :foobar, and :foo has an - // array of values, using str_replace would affect both placeholders, - // but using the following preg_replace would only affect :foo because - // it is followed by a non-word character. - $query = preg_replace('#' . $key . '\b#', implode(', ', array_keys($new_keys)), $query); - - // Update the args array with the new placeholders. - unset($args[$key]); - $args += $new_keys; - - $modified = TRUE; - } - - return $modified; - } - - /** - * Gets the driver-specific override class if any for the specified class. - * - * @param string $class - * The class for which we want the potentially driver-specific class. - * @param array $files - * The name of the files in which the driver-specific class can be. - * @param $use_autoload - * If TRUE, attempt to load classes using PHP's autoload capability - * as well as the manual approach here. - * @return string - * The name of the class that should be used for this driver. - */ - public function getDriverClass($class, array $files = array(), $use_autoload = FALSE) { - if (empty($this->driverClasses[$class])) { - $driver = $this->driver(); - $this->driverClasses[$class] = $class . '_' . $driver; - Database::loadDriverFile($driver, $files); - if (!class_exists($this->driverClasses[$class], $use_autoload)) { - $this->driverClasses[$class] = $class; - } - } - return $this->driverClasses[$class]; - } - - /** - * Prepares and returns a SELECT query object. - * - * @param $table - * The base table for this query, that is, the first table in the FROM - * clause. This table will also be used as the "base" table for query_alter - * hook implementations. - * @param $alias - * The alias of the base table of this query. - * @param $options - * An array of options on the query. - * - * @return SelectQueryInterface - * An appropriate SelectQuery object for this database connection. Note that - * it may be a driver-specific subclass of SelectQuery, depending on the - * driver. - * - * @see SelectQuery - */ - public function select($table, $alias = NULL, array $options = array()) { - $class = $this->getDriverClass('SelectQuery', array('query.inc', 'select.inc')); - return new $class($table, $alias, $this, $options); - } - - /** - * Prepares and returns an INSERT query object. - * - * @param $options - * An array of options on the query. - * - * @return InsertQuery - * A new InsertQuery object. - * - * @see InsertQuery - */ - public function insert($table, array $options = array()) { - $class = $this->getDriverClass('InsertQuery', array('query.inc')); - return new $class($this, $table, $options); - } - - /** - * Prepares and returns a MERGE query object. - * - * @param $options - * An array of options on the query. - * - * @return MergeQuery - * A new MergeQuery object. - * - * @see MergeQuery - */ - public function merge($table, array $options = array()) { - $class = $this->getDriverClass('MergeQuery', array('query.inc')); - return new $class($this, $table, $options); - } - - - /** - * Prepares and returns an UPDATE query object. - * - * @param $options - * An array of options on the query. - * - * @return UpdateQuery - * A new UpdateQuery object. - * - * @see UpdateQuery - */ - public function update($table, array $options = array()) { - $class = $this->getDriverClass('UpdateQuery', array('query.inc')); - return new $class($this, $table, $options); - } - - /** - * Prepares and returns a DELETE query object. - * - * @param $options - * An array of options on the query. - * - * @return DeleteQuery - * A new DeleteQuery object. - * - * @see DeleteQuery - */ - public function delete($table, array $options = array()) { - $class = $this->getDriverClass('DeleteQuery', array('query.inc')); - return new $class($this, $table, $options); - } - - /** - * Prepares and returns a TRUNCATE query object. - * - * @param $options - * An array of options on the query. - * - * @return TruncateQuery - * A new TruncateQuery object. - * - * @see TruncateQuery - */ - public function truncate($table, array $options = array()) { - $class = $this->getDriverClass('TruncateQuery', array('query.inc')); - return new $class($this, $table, $options); - } - - /** - * Returns a DatabaseSchema object for manipulating the schema. - * - * This method will lazy-load the appropriate schema library file. - * - * @return DatabaseSchema - * The DatabaseSchema object for this connection. - */ - public function schema() { - if (empty($this->schema)) { - $class = $this->getDriverClass('DatabaseSchema', array('schema.inc')); - if (class_exists($class)) { - $this->schema = new $class($this); - } - } - return $this->schema; - } - - /** - * Escapes a table name string. - * - * Force all table names to be strictly alphanumeric-plus-underscore. - * For some database drivers, it may also wrap the table name in - * database-specific escape characters. - * - * @return - * The sanitized table name string. - */ - public function escapeTable($table) { - return preg_replace('/[^A-Za-z0-9_.]+/', '', $table); - } - - /** - * Escapes a field name string. - * - * Force all field names to be strictly alphanumeric-plus-underscore. - * For some database drivers, it may also wrap the field name in - * database-specific escape characters. - * - * @return - * The sanitized field name string. - */ - public function escapeField($field) { - return preg_replace('/[^A-Za-z0-9_.]+/', '', $field); - } - - /** - * Escapes an alias name string. - * - * Force all alias names to be strictly alphanumeric-plus-underscore. In - * contrast to DatabaseConnection::escapeField() / - * DatabaseConnection::escapeTable(), this doesn't allow the period (".") - * because that is not allowed in aliases. - * - * @return - * The sanitized field name string. - */ - public function escapeAlias($field) { - return preg_replace('/[^A-Za-z0-9_]+/', '', $field); - } - - /** - * Escapes characters that work as wildcard characters in a LIKE pattern. - * - * The wildcard characters "%" and "_" as well as backslash are prefixed with - * a backslash. Use this to do a search for a verbatim string without any - * wildcard behavior. - * - * For example, the following does a case-insensitive query for all rows whose - * name starts with $prefix: - * @code - * $result = db_query( - * 'SELECT * FROM person WHERE name LIKE :pattern', - * array(':pattern' => db_like($prefix) . '%') - * ); - * @endcode - * - * Backslash is defined as escape character for LIKE patterns in - * DatabaseCondition::mapConditionOperator(). - * - * @param $string - * The string to escape. - * - * @return - * The escaped string. - */ - public function escapeLike($string) { - return addcslashes($string, '\%_'); - } - - /** - * Determines if there is an active transaction open. - * - * @return - * TRUE if we're currently in a transaction, FALSE otherwise. - */ - public function inTransaction() { - return ($this->transactionDepth() > 0); - } - - /** - * Determines current transaction depth. - */ - public function transactionDepth() { - return count($this->transactionLayers); - } - - /** - * Returns a new DatabaseTransaction object on this connection. - * - * @param $name - * Optional name of the savepoint. - * - * @see DatabaseTransaction - */ - public function startTransaction($name = '') { - $class = $this->getDriverClass('DatabaseTransaction'); - return new $class($this, $name); - } - - /** - * Rolls back the transaction entirely or to a named savepoint. - * - * This method throws an exception if no transaction is active. - * - * @param $savepoint_name - * The name of the savepoint. The default, 'drupal_transaction', will roll - * the entire transaction back. - * - * @throws DatabaseTransactionNoActiveException - * - * @see DatabaseTransaction::rollback() - */ - public function rollback($savepoint_name = 'drupal_transaction') { - if (!$this->supportsTransactions()) { - return; - } - if (!$this->inTransaction()) { - throw new DatabaseTransactionNoActiveException(); - } - // A previous rollback to an earlier savepoint may mean that the savepoint - // in question has already been accidentally committed. - if (!isset($this->transactionLayers[$savepoint_name])) { - throw new DatabaseTransactionNoActiveException(); - } - - // We need to find the point we're rolling back to, all other savepoints - // before are no longer needed. If we rolled back other active savepoints, - // we need to throw an exception. - $rolled_back_other_active_savepoints = FALSE; - while ($savepoint = array_pop($this->transactionLayers)) { - if ($savepoint == $savepoint_name) { - // If it is the last the transaction in the stack, then it is not a - // savepoint, it is the transaction itself so we will need to roll back - // the transaction rather than a savepoint. - if (empty($this->transactionLayers)) { - break; - } - $this->query('ROLLBACK TO SAVEPOINT ' . $savepoint); - $this->popCommittableTransactions(); - if ($rolled_back_other_active_savepoints) { - throw new DatabaseTransactionOutOfOrderException(); - } - return; - } - else { - $rolled_back_other_active_savepoints = TRUE; - } - } - parent::rollBack(); - if ($rolled_back_other_active_savepoints) { - throw new DatabaseTransactionOutOfOrderException(); - } - } - - /** - * Increases the depth of transaction nesting. - * - * If no transaction is already active, we begin a new transaction. - * - * @throws DatabaseTransactionNameNonUniqueException - * - * @see DatabaseTransaction - */ - public function pushTransaction($name) { - if (!$this->supportsTransactions()) { - return; - } - if (isset($this->transactionLayers[$name])) { - throw new DatabaseTransactionNameNonUniqueException($name . " is already in use."); - } - // If we're already in a transaction then we want to create a savepoint - // rather than try to create another transaction. - if ($this->inTransaction()) { - $this->query('SAVEPOINT ' . $name); - } - else { - parent::beginTransaction(); - } - $this->transactionLayers[$name] = $name; - } - - /** - * Decreases the depth of transaction nesting. - * - * If we pop off the last transaction layer, then we either commit or roll - * back the transaction as necessary. If no transaction is active, we return - * because the transaction may have manually been rolled back. - * - * @param $name - * The name of the savepoint - * - * @throws DatabaseTransactionNoActiveException - * @throws DatabaseTransactionCommitFailedException - * - * @see DatabaseTransaction - */ - public function popTransaction($name) { - if (!$this->supportsTransactions()) { - return; - } - // The transaction has already been committed earlier. There is nothing we - // need to do. If this transaction was part of an earlier out-of-order - // rollback, an exception would already have been thrown by - // Database::rollback(). - if (!isset($this->transactionLayers[$name])) { - return; - } - - // Mark this layer as committable. - $this->transactionLayers[$name] = FALSE; - $this->popCommittableTransactions(); - } - - /** - * Internal function: commit all the transaction layers that can commit. - */ - 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 (!parent::commit()) { - throw new DatabaseTransactionCommitFailedException(); - } - } - else { - $this->query('RELEASE SAVEPOINT ' . $name); - } - } - } - - /** - * Runs a limited-range query on this database object. - * - * Use this as a substitute for ->query() when a subset of the query is to be - * returned. User-supplied arguments to the query should be passed in as - * separate parameters so that they can be properly escaped to avoid SQL - * injection attacks. - * - * @param $query - * A string containing an SQL query. - * @param $args - * An array of values to substitute into the query at placeholder markers. - * @param $from - * The first result row to return. - * @param $count - * The maximum number of result rows to return. - * @param $options - * An array of options on the query. - * - * @return DatabaseStatementInterface - * A database query result resource, or NULL if the query was not executed - * correctly. - */ - abstract public function queryRange($query, $from, $count, array $args = array(), array $options = array()); - - /** - * Generates a temporary table name. - * - * @return - * A table name. - */ - protected function generateTemporaryTableName() { - return "db_temporary_" . $this->temporaryNameIndex++; - } - - /** - * Runs a SELECT query and stores its results in a temporary table. - * - * Use this as a substitute for ->query() when the results need to stored - * in a temporary table. Temporary tables exist for the duration of the page - * request. User-supplied arguments to the query should be passed in as - * separate parameters so that they can be properly escaped to avoid SQL - * injection attacks. - * - * Note that if you need to know how many results were returned, you should do - * a SELECT COUNT(*) on the temporary table afterwards. - * - * @param $query - * A string containing a normal SELECT SQL query. - * @param $args - * An array of values to substitute into the query at placeholder markers. - * @param $options - * An associative array of options to control how the query is run. See - * the documentation for DatabaseConnection::defaultOptions() for details. - * - * @return - * The name of the temporary table. - */ - abstract function queryTemporary($query, array $args = array(), array $options = array()); - - /** - * Returns the type of database driver. - * - * This is not necessarily the same as the type of the database itself. For - * instance, there could be two MySQL drivers, mysql and mysql_mock. This - * function would return different values for each, but both would return - * "mysql" for databaseType(). - */ - abstract public function driver(); - - /** - * Returns the version of the database server. - */ - public function version() { - return $this->getAttribute(PDO::ATTR_SERVER_VERSION); - } - - /** - * Determines if this driver supports transactions. - * - * @return - * TRUE if this connection supports transactions, FALSE otherwise. - */ - public function supportsTransactions() { - return $this->transactionSupport; - } - - /** - * Determines if this driver supports transactional DDL. - * - * DDL queries are those that change the schema, such as ALTER queries. - * - * @return - * TRUE if this connection supports transactions for DDL queries, FALSE - * otherwise. - */ - public function supportsTransactionalDDL() { - return $this->transactionalDDLSupport; - } - - /** - * Returns the name of the PDO driver for this connection. - */ - abstract public function databaseType(); - - - /** - * 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. Database connections should define only - * those operators they wish to be handled differently than the default. - * - * @param $operator - * The condition operator, such as "IN", "BETWEEN", etc. Case-sensitive. - * - * @return - * The extra handling directives for the specified operator, or NULL. - * - * @see DatabaseCondition::compile() - */ - abstract public function mapConditionOperator($operator); - - /** - * Throws an exception to deny direct access to transaction commits. - * - * We do not want to allow users to commit transactions at any time, only - * by destroying the transaction object or allowing it to go out of scope. - * A direct commit bypasses all of the safety checks we've built on top of - * PDO's transaction routines. - * - * @throws DatabaseTransactionExplicitCommitNotAllowedException - * - * @see DatabaseTransaction - */ - public function commit() { - throw new DatabaseTransactionExplicitCommitNotAllowedException(); - } - - /** - * Retrieves an unique id from a given sequence. - * - * Use this function if for some reason you can't use a serial field. For - * example, MySQL has no ways of reading of the current value of a sequence - * and PostgreSQL can not advance the sequence to be larger than a given - * value. Or sometimes you just need a unique integer. - * - * @param $existing_id - * After a database import, it might be that the sequences table is behind, - * so by passing in the maximum existing id, it can be assured that we - * never issue the same id. - * - * @return - * An integer number larger than any number returned by earlier calls and - * also larger than the $existing_id if one was passed in. - */ - abstract public function nextId($existing_id = 0); -} - -/** - * 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 = NULL; - - /** - * 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 $logging_key - * The logging key to log. - * @param $key - * The database connection key for which we want to log. - * - * @return DatabaseLog - * 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 DatabaseLog - */ - final public static function startLog($logging_key, $key = 'default') { - if (empty(self::$logs[$key])) { - self::$logs[$key] = new DatabaseLog($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 $logging_key - * The logging key to log. - * @param $key - * The database connection key for which we want to log. - * - * @return array - * The query log for the specified logging key and connection. - * - * @see DatabaseLog - */ - 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 $target - * The database target name. - * @param $key - * The database connection key. Defaults to NULL which means the active key. - * - * @return DatabaseConnection - * 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 - // "slave", indicating to use a slave SQL server if one is available. If - // it's not available, then the default/master 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 - * 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 - * The previous database connection key. - */ - final public static function setActiveConnection($key = 'default') { - if (empty(self::$databaseInfo)) { - self::parseConnectionInfo(); - } - - if (!empty(self::$databaseInfo[$key])) { - $old_key = self::$activeKey; - self::$activeKey = $key; - return $old_key; - } - } - - /** - * Process the configuration file for database information. - */ - final public static function parseConnectionInfo() { - global $databases; - - $database_info = is_array($databases) ? $databases : array(); - foreach ($database_info as $index => $info) { - foreach ($database_info[$index] as $target => $value) { - // 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 slave servers. - if (empty($value['driver'])) { - $database_info[$index][$target] = $database_info[$index][$target][mt_rand(0, count($database_info[$index][$target]) - 1)]; - } - - // Parse the prefix information. - if (!isset($database_info[$index][$target]['prefix'])) { - // Default to an empty prefix. - $database_info[$index][$target]['prefix'] = array( - 'default' => '', - ); - } - elseif (!is_array($database_info[$index][$target]['prefix'])) { - // Transform the flat form into an array form. - $database_info[$index][$target]['prefix'] = array( - 'default' => $database_info[$index][$target]['prefix'], - ); - } - } - } - - if (!is_array(self::$databaseInfo)) { - self::$databaseInfo = $database_info; - } - - // Merge the new $database_info into the existing. - // array_merge_recursive() cannot be used, as it would make multiple - // database, user, and password keys in the same database array. - else { - foreach ($database_info as $database_key => $database_values) { - foreach ($database_values as $target => $target_values) { - self::$databaseInfo[$database_key][$target] = $target_values; - } - } - } - } - - /** - * Adds database connection information for a given key/target. - * - * This method allows the addition of new connection credentials 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 $key - * The database key. - * @param $target - * The database target name. - * @param $info - * The database connection information, as it would be defined in - * settings.php. Note that the structure of this array will depend on the - * database driver it is connecting to. - */ - public static function addConnectionInfo($key, $target, $info) { - if (empty(self::$databaseInfo[$key][$target])) { - self::$databaseInfo[$key][$target] = $info; - } - } - - /** - * Gets information on the specified database connection. - * - * @param $connection - * The connection key for which we want information. - */ - final public static function getConnectionInfo($key = 'default') { - if (empty(self::$databaseInfo)) { - self::parseConnectionInfo(); - } - - if (!empty(self::$databaseInfo[$key])) { - return self::$databaseInfo[$key]; - } - } - - /** - * Rename a connection and its corresponding connection information. - * - * @param $old_key - * The old connection key. - * @param $new_key - * The new connection key. - * @return - * TRUE in case of success, FALSE otherwise. - */ - final public static function renameConnection($old_key, $new_key) { - if (empty(self::$databaseInfo)) { - self::parseConnectionInfo(); - } - - 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 $key - * The connection key. - * @return - * TRUE in case of success, FALSE otherwise. - */ - final public static function removeConnection($key) { - if (isset(self::$databaseInfo[$key])) { - unset(self::$databaseInfo[$key]); - unset(self::$connections[$key]); - return TRUE; - } - else { - return FALSE; - } - } - - /** - * Opens a connection to the server specified by the given key and target. - * - * @param $key - * The database connection key, as specified in settings.php. The default is - * "default". - * @param $target - * The database target to open. - * - * @throws DatabaseConnectionNotDefinedException - * @throws DatabaseDriverNotSpecifiedException - */ - final protected static function openConnection($key, $target) { - if (empty(self::$databaseInfo)) { - self::parseConnectionInfo(); - } - - // If the requested database does not exist then it is an unrecoverable - // error. - if (!isset(self::$databaseInfo[$key])) { - throw new DatabaseConnectionNotDefinedException('The specified database connection is not defined: ' . $key); - } - - if (!$driver = self::$databaseInfo[$key][$target]['driver']) { - throw new DatabaseDriverNotSpecifiedException('Driver not specified for this database connection: ' . $key); - } - - // We cannot rely on the registry yet, because the registry requires an - // open database connection. - $driver_class = 'DatabaseConnection_' . $driver; - require_once DRUPAL_ROOT . '/core/includes/database/' . $driver . '/database.inc'; - $new_connection = new $driver_class(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 $target - * The database target name. Defaults to NULL meaning that all target - * connections will be closed. - * @param $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 the connection, we need to unset the static variable. - if (isset($target)) { - unset(self::$connections[$key][$target]); - } - else { - unset(self::$connections[$key]); - } - } - - /** - * Instructs the system to temporarily ignore a given key/target. - * - * At times we need to temporarily disable slave 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 $key - * The database connection key. - * @param $target - * The target of the specified key to ignore. - */ - public static function ignoreTarget($key, $target) { - self::$ignoreTargets[$key][$target] = TRUE; - } - - /** - * Load a file for the database that might hold a class. - * - * @param $driver - * The name of the driver. - * @param array $files - * The name of the files the driver specific class can be. - */ - public static function loadDriverFile($driver, array $files = array()) { - static $base_path; - - if (empty($base_path)) { - $base_path = dirname(realpath(__FILE__)); - } - - $driver_base_path = "$base_path/$driver"; - foreach ($files as $file) { - // Load the base file first so that classes extending base classes will - // have the base class loaded. - foreach (array("$base_path/$file", "$driver_base_path/$file") as $filename) { - // The OS caches file_exists() and PHP caches require_once(), so - // we'll let both of those take care of performance here. - if (file_exists($filename)) { - require_once $filename; - } - } - } - } -} - -/** - * Exception for when popTransaction() is called with no active transaction. - */ -class DatabaseTransactionNoActiveException extends Exception { } - -/** - * Exception thrown when a savepoint or transaction name occurs twice. - */ -class DatabaseTransactionNameNonUniqueException extends Exception { } - -/** - * Exception thrown when a commit() function fails. - */ -class DatabaseTransactionCommitFailedException extends Exception { } - -/** - * 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 DatabaseTransactionExplicitCommitNotAllowedException extends Exception { } - -/** - * Exception thrown when a rollback() resulted in other active transactions being rolled-back. - */ -class DatabaseTransactionOutOfOrderException extends Exception { } - -/** - * 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 Exception {} - -/** - * 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 Exception {} - -/** - * Exception thrown if an insert query doesn't specify insert or default fields. - */ -class NoFieldsException extends Exception {} - -/** - * Exception thrown if an undefined database connection is requested. - */ -class DatabaseConnectionNotDefinedException extends Exception {} - -/** - * Exception thrown if no driver is specified for a database connection. - */ -class DatabaseDriverNotSpecifiedException extends Exception {} - - -/** - * 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 DatabaseTransaction { - - /** - * The connection object for this transaction. - * - * @var DatabaseConnection - */ - protected $connection; - - /** - * A boolean value to indicate whether this transaction has been rolled back. - * - * @var Boolean - */ - 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(DatabaseConnection &$connection, $name = NULL) { - $this->connection = &$connection; - // If there is no transaction depth, then no transaction has started. Name - // the transaction 'drupal_transaction'. - if (!$depth = $connection->transactionDepth()) { - $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 DatabaseConnection::rollback() - * @see watchdog_exception() - */ - public function rollback() { - $this->rolledBack = TRUE; - $this->connection->rollback($this->name); - } -} - -/** - * 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 DatabaseStatement_oracle extends PDOStatement implements DatabaseStatementInterface {} - * @endcode - * or define their own class. If defining their own class, they will also have - * to implement either the Iterator or IteratorAggregate interface before - * DatabaseStatementInterface: - * @code - * class DatabaseStatement_oracle implements Iterator, DatabaseStatementInterface {} - * @endcode - */ -interface DatabaseStatementInterface extends Traversable { - - /** - * 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. - */ - public function rowCount(); - - /** - * Sets the default fetch mode for this statement. - * - * See http://php.net/manual/en/pdo.constants.php for the definition of the - * 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/en/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 DatabaseStatementInterface::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); -} - -/** - * Default implementation of DatabaseStatementInterface. - * - * 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://us.php.net/pdostatement - */ -class DatabaseStatementBase extends PDOStatement implements DatabaseStatementInterface { - - /** - * Reference to the database connection object for this statement. - * - * The name $dbh is inherited from PDOStatement. - * - * @var DatabaseConnection - */ - public $dbh; - - protected function __construct($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'])) { - // 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 http://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); - } - - $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); - } -} - -/** - * 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 SearchQuery - */ -class DatabaseStatementEmpty implements Iterator, DatabaseStatementInterface { - - public function execute($args = array(), $options = array()) { - return FALSE; - } - - public function getQueryString() { - return ''; - } - - public function rowCount() { - return 0; - } - - 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; - } -} - /** * The following utility functions are simply convenience wrappers. * @@ -2630,28 +530,28 @@ function db_next_id($existing_id = 0) { /** * Returns a new DatabaseCondition, set to "OR" all conditions together. * - * @return DatabaseCondition + * @return Condition */ function db_or() { - return new DatabaseCondition('OR'); + return new Condition('OR'); } /** * Returns a new DatabaseCondition, set to "AND" all conditions together. * - * @return DatabaseCondition + * @return Condition */ function db_and() { - return new DatabaseCondition('AND'); + return new Condition('AND'); } /** * Returns a new DatabaseCondition, set to "XOR" all conditions together. * - * @return DatabaseCondition + * @return Condition */ function db_xor() { - return new DatabaseCondition('XOR'); + return new Condition('XOR'); } /** @@ -2662,10 +562,10 @@ function db_xor() { * * @param $conjunction * The conjunction to use for query conditions (AND, OR or XOR). - * @return DatabaseCondition + * @return Condition */ function db_condition($conjunction) { - return new DatabaseCondition($conjunction); + return new Condition($conjunction); } /** diff --git a/core/includes/database/query.inc b/core/includes/database/query.inc deleted file mode 100644 index 0effedae19cd82f367a8d61c28fc45a8734904e3..0000000000000000000000000000000000000000 --- a/core/includes/database/query.inc +++ /dev/null @@ -1,1956 +0,0 @@ -<?php - -/** - * @ingroup database - * @{ - */ - -/** - * @file - * Non-specific Database query code. Used by all engines. - */ - -/** - * Interface for a conditional clause in a query. - */ -interface QueryConditionInterface { - - /** - * 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 IN if $value is an array and = otherwise. - * - * Do not use this method to test for NULL values. Instead, use - * QueryConditionInterface::isNull() or QueryConditionInterface::isNotNull(). - * - * @param $field - * The name of the field to check. If you would like to add a more complex - * condition involving operators or functions, use where(). - * @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, and = otherwise. - * - * @return QueryConditionInterface - * The called object. - * - * @see QueryConditionInterface::isNull() - * @see QueryConditionInterface::isNotNull() - */ - public function condition($field, $value = NULL, $operator = NULL); - - /** - * 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 QueryConditionInterface - * 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 QueryConditionInterface - * 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 QueryConditionInterface - * The called object. - */ - public function isNotNull($field); - - /** - * Sets a condition that the specified subquery returns values. - * - * @param SelectQueryInterface $select - * The subquery that must contain results. - * - * @return QueryConditionInterface - * The called object. - */ - public function exists(SelectQueryInterface $select); - - /** - * Sets a condition that the specified subquery returns no values. - * - * @param SelectQueryInterface $select - * The subquery that must not contain results. - * - * @return QueryConditionInterface - * The called object. - */ - public function notExists(SelectQueryInterface $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(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder); - - /** - * Check whether a condition has been previously compiled. - * - * @return - * TRUE if the condition has been previously compiled. - */ - public function compiled(); -} - - -/** - * Interface for a query that can be manipulated via an alter hook. - */ -interface QueryAlterableInterface { - - /** - * 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 QueryAlterableInterface - * 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 QueryAlterableInterface - * 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); -} - -/** - * Interface for a query that accepts placeholders. - */ -interface QueryPlaceholderInterface { - - /** - * 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(); -} - -/** - * 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 QueryPlaceholderInterface { - - /** - * The connection object on which to run this query. - * - * @var DatabaseConnection - */ - 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 DatabaseConnection $connection - * Database connection object. - * @param array $options - * Array of query options. - */ - public function __construct(DatabaseConnection $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. - */ - 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 - * 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 - * 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 Query - * The called object. - */ - 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 - * A reference to the comments array structure. - */ - public function &getComments() { - return $this->comments; - } -} - -/** - * General class for an abstracted INSERT query. - */ -class InsertQuery 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 SelectQueryInterface - */ - protected $fromQuery; - - /** - * Constructs an InsertQuery object. - * - * @param DatabaseConnection $connection - * A DatabaseConnection 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 InsertQuery - * 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 InsertQuery - * 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 InsertQuery - * The called object. - */ - public function useDefaults(array $fields) { - $this->defaultFields = $fields; - return $this; - } - - /** - * Sets the fromQuery on this InsertQuery object. - * - * @param SelectQueryInterface $query - * The query to fetch the rows that should be inserted. - * - * @return InsertQuery - * The called object. - */ - public function from(SelectQueryInterface $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 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 FieldsOverlapException - * @throws 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()))); - } - - // 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; - } -} - -/** - * General class for an abstracted DELETE operation. - */ -class DeleteQuery extends Query implements QueryConditionInterface { - - /** - * The table from which to delete. - * - * @var string - */ - protected $table; - - /** - * The condition object for this query. - * - * Condition handling is handled via composition. - * - * @var DatabaseCondition - */ - protected $condition; - - /** - * Constructs a DeleteQuery object. - * - * @param DatabaseConnection $connection - * A DatabaseConnection object. - * @param string $table - * Name of the table to associate with this query. - * @param array $options - * Array of database options. - */ - public function __construct(DatabaseConnection $connection, $table, array $options = array()) { - $options['return'] = Database::RETURN_AFFECTED; - parent::__construct($connection, $options); - $this->table = $table; - - $this->condition = new DatabaseCondition('AND'); - } - - /** - * Implements QueryConditionInterface::condition(). - */ - public function condition($field, $value = NULL, $operator = NULL) { - $this->condition->condition($field, $value, $operator); - return $this; - } - - /** - * Implements QueryConditionInterface::isNull(). - */ - public function isNull($field) { - $this->condition->isNull($field); - return $this; - } - - /** - * Implements QueryConditionInterface::isNotNull(). - */ - public function isNotNull($field) { - $this->condition->isNotNull($field); - return $this; - } - - /** - * Implements QueryConditionInterface::exists(). - */ - public function exists(SelectQueryInterface $select) { - $this->condition->exists($select); - return $this; - } - - /** - * Implements QueryConditionInterface::notExists(). - */ - public function notExists(SelectQueryInterface $select) { - $this->condition->notExists($select); - return $this; - } - - /** - * Implements QueryConditionInterface::conditions(). - */ - public function &conditions() { - return $this->condition->conditions(); - } - - /** - * Implements QueryConditionInterface::arguments(). - */ - public function arguments() { - return $this->condition->arguments(); - } - - /** - * Implements QueryConditionInterface::where(). - */ - public function where($snippet, $args = array()) { - $this->condition->where($snippet, $args); - return $this; - } - - /** - * Implements QueryConditionInterface::compile(). - */ - public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) { - return $this->condition->compile($connection, $queryPlaceholder); - } - - /** - * Implements QueryConditionInterface::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; - } -} - - -/** - * General class for an abstracted TRUNCATE operation. - */ -class TruncateQuery extends Query { - - /** - * The table to truncate. - * - * @var string - */ - protected $table; - - /** - * Constructs a TruncateQuery object. - * - * @param DatabaseConnection $connection - * A DatabaseConnection object. - * @param string $table - * Name of the table to associate with this query. - * @param array $options - * Array of database options. - */ - public function __construct(DatabaseConnection $connection, $table, array $options = array()) { - $options['return'] = Database::RETURN_AFFECTED; - parent::__construct($connection, $options); - $this->table = $table; - } - - /** - * Implements QueryConditionInterface::compile(). - */ - public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) { - return $this->condition->compile($connection, $queryPlaceholder); - } - - /** - * Implements QueryConditionInterface::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); - - return $comments . 'TRUNCATE {' . $this->connection->escapeTable($this->table) . '} '; - } -} - -/** - * General class for an abstracted UPDATE operation. - */ -class UpdateQuery extends Query implements QueryConditionInterface { - - /** - * 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 DatabaseCondition - */ - 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 UpdateQuery object. - * - * @param DatabaseConnection $connection - * A DatabaseConnection object. - * @param string $table - * Name of the table to associate with this query. - * @param array $options - * Array of database options. - */ - public function __construct(DatabaseConnection $connection, $table, array $options = array()) { - $options['return'] = Database::RETURN_AFFECTED; - parent::__construct($connection, $options); - $this->table = $table; - - $this->condition = new DatabaseCondition('AND'); - } - - /** - * Implements QueryConditionInterface::condition(). - */ - public function condition($field, $value = NULL, $operator = NULL) { - $this->condition->condition($field, $value, $operator); - return $this; - } - - /** - * Implements QueryConditionInterface::isNull(). - */ - public function isNull($field) { - $this->condition->isNull($field); - return $this; - } - - /** - * Implements QueryConditionInterface::isNotNull(). - */ - public function isNotNull($field) { - $this->condition->isNotNull($field); - return $this; - } - - /** - * Implements QueryConditionInterface::exists(). - */ - public function exists(SelectQueryInterface $select) { - $this->condition->exists($select); - return $this; - } - - /** - * Implements QueryConditionInterface::notExists(). - */ - public function notExists(SelectQueryInterface $select) { - $this->condition->notExists($select); - return $this; - } - - /** - * Implements QueryConditionInterface::conditions(). - */ - public function &conditions() { - return $this->condition->conditions(); - } - - /** - * Implements QueryConditionInterface::arguments(). - */ - public function arguments() { - return $this->condition->arguments(); - } - - /** - * Implements QueryConditionInterface::where(). - */ - public function where($snippet, $args = array()) { - $this->condition->where($snippet, $args); - return $this; - } - - /** - * Implements QueryConditionInterface::compile(). - */ - public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) { - return $this->condition->compile($connection, $queryPlaceholder); - } - - /** - * Implements QueryConditionInterface::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 UpdateQuery - * 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 UpdateQuery - * 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 affected by the update. - */ - 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']; - } - 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 $field => $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) { - $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; - } - -} - -/** - * 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 convinient - * 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 MergeQuery extends Query implements QueryConditionInterface { - /** - * 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 boolean - */ - protected $needsUpdate = FALSE; - - /** - * Constructs a MergeQuery object. - * - * @param DatabaseConnection $connection - * A DatabaseConnection object. - * @param string $table - * Name of the table to associate with this query. - * @param array $options - * Array of database options. - */ - public function __construct(DatabaseConnection $connection, $table, array $options = array()) { - $options['return'] = Database::RETURN_AFFECTED; - parent::__construct($connection, $options); - $this->table = $table; - $this->conditionTable = $table; - $this->condition = new DatabaseCondition('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 SelectQuery object to - * pass in a subquery. - * - * @return MergeQuery - * 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 MergeQuery - * 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 MergeQuery - * 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 MergeQuery - * 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 MergeQuery - * 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 MergeQuery - * 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 field(s) 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 MergeQuery - * The called object. - */ - public function key(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; - } - - /** - * Implements QueryConditionInterface::condition(). - */ - public function condition($field, $value = NULL, $operator = NULL) { - $this->condition->condition($field, $value, $operator); - return $this; - } - - /** - * Implements QueryConditionInterface::isNull(). - */ - public function isNull($field) { - $this->condition->isNull($field); - return $this; - } - - /** - * Implements QueryConditionInterface::isNotNull(). - */ - public function isNotNull($field) { - $this->condition->isNotNull($field); - return $this; - } - - /** - * Implements QueryConditionInterface::exists(). - */ - public function exists(SelectQueryInterface $select) { - $this->condition->exists($select); - return $this; - } - - /** - * Implements QueryConditionInterface::notExists(). - */ - public function notExists(SelectQueryInterface $select) { - $this->condition->notExists($select); - return $this; - } - - /** - * Implements QueryConditionInterface::conditions(). - */ - public function &conditions() { - return $this->condition->conditions(); - } - - /** - * Implements QueryConditionInterface::arguments(). - */ - public function arguments() { - return $this->condition->arguments(); - } - - /** - * Implements QueryConditionInterface::where(). - */ - public function where($snippet, $args = array()) { - $this->condition->where($snippet, $args); - return $this; - } - - /** - * Implements QueryConditionInterface::compile(). - */ - public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) { - return $this->condition->compile($connection, $queryPlaceholder); - } - - /** - * Implements QueryConditionInterface::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() { - // Wrap multiple queries in a transaction, if the database supports it. - $transaction = $this->connection->startTransaction(); - try { - if (!count($this->condition)) { - throw new InvalidMergeQueryException(t('Invalid merge query: no conditions')); - } - $select = $this->connection->select($this->conditionTable) - ->condition($this->condition) - ->forUpdate(); - $select->addExpression('1'); - if (!$select->execute()->fetchField()) { - try { - $insert = $this->connection->insert($this->table)->fields($this->insertFields); - if ($this->defaultFields) { - $insert->useDefaults($this->defaultFields); - } - $insert->execute(); - return MergeQuery::STATUS_INSERT; - } - catch (Exception $e) { - // The insert query failed, maybe it's because a racing insert query - // beat us in inserting the same row. Retry the select query, if it - // returns a row, ignore the error and continue with the update - // query below. - if (!$select->execute()->fetchField()) { - throw $e; - } - } - } - if ($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 MergeQuery::STATUS_UPDATE; - } - } - catch (Exception $e) { - // Something really wrong happened here, bubble up the exception to the - // caller. - $transaction->rollback(); - throw $e; - } - // Transaction commits here where $transaction looses scope. - } -} - -/** - * Generic class for a series of conditions in a query. - */ -class DatabaseCondition implements QueryConditionInterface, 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 DataBaseCondition 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 the - * conjunction. - */ - public function count() { - return count($this->conditions) - 1; - } - - /** - * Implements QueryConditionInterface::condition(). - */ - public function condition($field, $value = NULL, $operator = NULL) { - if (!isset($operator)) { - if (is_array($value)) { - $operator = 'IN'; - } - else { - $operator = '='; - } - } - $this->conditions[] = array( - 'field' => $field, - 'value' => $value, - 'operator' => $operator, - ); - - $this->changed = TRUE; - - return $this; - } - - /** - * Implements QueryConditionInterface::where(). - */ - public function where($snippet, $args = array()) { - $this->conditions[] = array( - 'field' => $snippet, - 'value' => $args, - 'operator' => NULL, - ); - $this->changed = TRUE; - - return $this; - } - - /** - * Implements QueryConditionInterface::isNull(). - */ - public function isNull($field) { - return $this->condition($field, NULL, 'IS NULL'); - } - - /** - * Implements QueryConditionInterface::isNotNull(). - */ - public function isNotNull($field) { - return $this->condition($field, NULL, 'IS NOT NULL'); - } - - /** - * Implements QueryConditionInterface::exists(). - */ - public function exists(SelectQueryInterface $select) { - return $this->condition('', $select, 'EXISTS'); - } - - /** - * Implements QueryConditionInterface::notExists(). - */ - public function notExists(SelectQueryInterface $select) { - return $this->condition('', $select, 'NOT EXISTS'); - } - - /** - * Implements QueryConditionInterface::conditions(). - */ - public function &conditions() { - return $this->conditions; - } - - /** - * Implements QueryConditionInterface::arguments(). - */ - public function arguments() { - // If the caller forgot to call compile() first, refuse to run. - if ($this->changed) { - return NULL; - } - return $this->arguments; - } - - /** - * Implements QueryConditionInterface::compile(). - */ - public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $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 QueryConditionInterface) { - // 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 SelectQueryInterface) { - $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']) { - $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 QueryConditionInterface::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 NULL; - } - return $this->stringVersion; - } - - /** - * PHP magic __clone() method. - * - * Only copies fields that implement QueryConditionInterface. Also sets - * $this->changed to TRUE. - */ - function __clone() { - $this->changed = TRUE; - foreach ($this->conditions as $key => $condition) { - if ($condition['field'] instanceOf QueryConditionInterface) { - $this->conditions[$key]['field'] = clone($condition['field']); - } - } - } - - /** - * 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 - * The extra handling directives for the specified operator, or NULL. - */ - 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 drupal_strtoupper because SQL statements are ASCII. - $operator = strtoupper($operator); - $return = isset($specials[$operator]) ? $specials[$operator] : array(); - } - - $return += array('operator' => $operator); - - return $return; - } - -} - -/** - * @} End of "ingroup database". - */ diff --git a/core/includes/database/select.inc b/core/includes/database/select.inc deleted file mode 100644 index 9bc6b92e1d908c0d97bef2976df4873867628d1f..0000000000000000000000000000000000000000 --- a/core/includes/database/select.inc +++ /dev/null @@ -1,1609 +0,0 @@ -<?php - -/** - * @ingroup database - * @{ - */ - -require_once __DIR__ . '/query.inc'; - -/** - * Interface for extendable query objects. - * - * "Extenders" follow the "Decorator" OOP design pattern. That is, they wrap - * and "decorate" another object. In our case, they implement the same interface - * as select queries and wrap a select query, to which they delegate almost all - * operations. Subclasses of this class may implement additional methods or - * override existing methods as appropriate. Extenders may also wrap other - * extender objects, allowing for arbitrarily complex "enhanced" queries. - */ -interface QueryExtendableInterface { - - /** - * Enhance this object by wrapping it in an extender object. - * - * @param $extender_name - * The base name of the extending class. The base name will be checked - * against the current database connection to allow driver-specific subclasses - * as well, using the same logic as the query objects themselves. For example, - * PagerDefault_mysql is the MySQL-specific override for PagerDefault. - * @return QueryExtendableInterface - * The extender object, which now contains a reference to this object. - */ - public function extend($extender_name); -} - -/** - * Interface definition for a Select Query object. - */ -interface SelectQueryInterface extends QueryConditionInterface, QueryAlterableInterface, QueryExtendableInterface, QueryPlaceholderInterface { - - /* 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(); - - /** - * 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(QueryPlaceholderInterface $queryPlaceholder = NULL); - - /* Query building operations */ - - /** - * Sets this query to be DISTINCT. - * - * @param $distinct - * TRUE to flag this query DISTINCT, FALSE to disable it. - * @return SelectQueryInterface - * 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 SelectQueryInterface - * The called object. - */ - public function fields($table_alias, array $fields = array()); - - /** - * Adds an expression to the list of "fields" to be SELECTed. - * - * An expression can be any arbitrary string that is valid SQL. That includes - * various functions, which may in some cases be database-dependent. This - * method makes no effort to correct for database-specific functions. - * - * @param $expression - * The expression string. May contain placeholders. - * @param $alias - * The alias for this expression. If not specified, one will be generated - * automatically in the form "expression_#". The alias will be checked for - * uniqueness, so the requested alias may not be the alias that is assigned - * in all cases. - * @param $arguments - * Any placeholder arguments needed for this expression. - * @return - * The unique alias that was assigned for this expression. - */ - public function addExpression($expression, $alias = NULL, $arguments = array()); - - /** - * Default Join against another table in the database. - * - * This method is a convenience method for innerJoin(). - * - * @param $table - * The table against which to join. - * @param $alias - * The alias for the table. In most cases this should be the first letter - * of the table, or the first letter of each "word" in the table. - * @param $condition - * The condition on which to join this table. If the join requires values, - * this clause should use a named placeholder and the value or values to - * insert should be passed in the 4th parameter. For the first table joined - * on a query, this value is ignored as the first table is taken as the base - * table. 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. - * @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. - * @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. - * @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. - * @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. - * @param $direction - * The direction to sort. Legal values are "ASC" and "DESC". - * @return SelectQueryInterface - * 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 SelectQueryInterface - * 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 SelectQueryInterface - * 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 SelectQueryInterface - * The called object. - */ - public function union(SelectQueryInterface $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 SelectQueryInterface - * The called object. - */ - public function groupBy($field); - - /** - * Get the equivalent COUNT query of this query as a new query object. - * - * @return SelectQueryInterface - * 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(SelectQueryInterface $query = NULL); - - /** - * 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 QueryConditionInterface - * The called object. - */ - public function havingCondition($field, $value = NULL, $operator = NULL); - - /** - * 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 QueryConditionInterface - * The called object. - */ - public function forUpdate($set = TRUE); -} - -/** - * The base extender class for Select queries. - */ -class SelectQueryExtender implements SelectQueryInterface { - - /** - * The SelectQuery object we are extending/decorating. - * - * @var SelectQueryInterface - */ - protected $query; - - /** - * The connection object on which to run this query. - * - * @var DatabaseConnection - */ - protected $connection; - - /** - * A unique identifier for this query object. - */ - protected $uniqueIdentifier; - - /** - * The placeholder counter. - */ - protected $placeholder = 0; - - public function __construct(SelectQueryInterface $query, DatabaseConnection $connection) { - $this->uniqueIdentifier = uniqid('', TRUE); - $this->query = $query; - $this->connection = $connection; - } - - /** - * Implements QueryPlaceholderInterface::uniqueIdentifier(). - */ - public function uniqueIdentifier() { - return $this->uniqueIdentifier; - } - - /** - * Implements QueryPlaceholderInterface::nextPlaceholder(). - */ - public function nextPlaceholder() { - return $this->placeholder++; - } - - /* Implementations of QueryAlterableInterface. */ - - public function addTag($tag) { - $this->query->addTag($tag); - return $this; - } - - public function hasTag($tag) { - return $this->query->hasTag($tag); - } - - public function hasAllTags() { - return call_user_func_array(array($this->query, 'hasAllTags'), func_get_args()); - } - - public function hasAnyTag() { - return call_user_func_array(array($this->query, 'hasAnyTags'), func_get_args()); - } - - public function addMetaData($key, $object) { - $this->query->addMetaData($key, $object); - return $this; - } - - public function getMetaData($key) { - return $this->query->getMetaData($key); - } - - /* Implementations of QueryConditionInterface for the WHERE clause. */ - - public function condition($field, $value = NULL, $operator = NULL) { - $this->query->condition($field, $value, $operator); - return $this; - } - - public function &conditions() { - return $this->query->conditions(); - } - - public function arguments() { - return $this->query->arguments(); - } - - public function where($snippet, $args = array()) { - $this->query->where($snippet, $args); - return $this; - } - - public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) { - return $this->query->compile($connection, $queryPlaceholder); - } - - public function compiled() { - return $this->query->compiled(); - } - - /* Implementations of QueryConditionInterface 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(DatabaseConnection $connection) { - return $this->query->havingCompile($connection); - } - - /* Implementations of QueryExtendableInterface. */ - - public function extend($extender_name) { - // The extender can be anywhere so this needs to go to the registry, which - // is surely loaded by now. - $class = $this->connection->getDriverClass($extender_name, array(), TRUE); - 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(); - } - - public function getArguments(QueryPlaceholderInterface $queryPlaceholder = NULL) { - return $this->query->getArguments($queryPlaceholder); - } - - public function isPrepared() { - return $this->query->isPrepared(); - } - - public function preExecute(SelectQueryInterface $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(SelectQueryInterface $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(SelectQueryInterface $select) { - $this->query->exists($select); - return $this; - } - - public function notExists(SelectQueryInterface $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 SelectQueryInterface) { - return $this; - } - else { - return $return; - } - } -} - -/** - * Query builder for SELECT statements. - */ -class SelectQuery extends Query implements SelectQueryInterface { - - /** - * 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 SelectQuery 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 DatabaseCondition - */ - protected $where; - - /** - * The conditional object for the HAVING clause. - * - * @var DatabaseCondition - */ - protected $having; - - /** - * Whether or not this query should be DISTINCT - * - * @var boolean - */ - 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 boolean - */ - protected $prepared = FALSE; - - /** - * The FOR UPDATE status - */ - protected $forUpdate = FALSE; - - public function __construct($table, $alias = NULL, DatabaseConnection $connection, $options = array()) { - $options['return'] = Database::RETURN_STATEMENT; - parent::__construct($connection, $options); - $this->where = new DatabaseCondition('AND'); - $this->having = new DatabaseCondition('AND'); - $this->addJoin(NULL, $table, $alias); - } - - /* Implementations of QueryAlterableInterface. */ - - public function addTag($tag) { - $this->alterTags[$tag] = 1; - return $this; - } - - public function hasTag($tag) { - return isset($this->alterTags[$tag]); - } - - public function hasAllTags() { - return !(boolean)array_diff(func_get_args(), array_keys($this->alterTags)); - } - - public function hasAnyTag() { - return (boolean)array_intersect(func_get_args(), array_keys($this->alterTags)); - } - - public function addMetaData($key, $object) { - $this->alterMetaData[$key] = $object; - return $this; - } - - public function getMetaData($key) { - return isset($this->alterMetaData[$key]) ? $this->alterMetaData[$key] : NULL; - } - - /* Implementations of QueryConditionInterface for the WHERE clause. */ - - public function condition($field, $value = NULL, $operator = NULL) { - $this->where->condition($field, $value, $operator); - return $this; - } - - public function &conditions() { - return $this->where->conditions(); - } - - 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 SelectQueryInterface) { - $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; - } - - public function where($snippet, $args = array()) { - $this->where->where($snippet, $args); - return $this; - } - - public function isNull($field) { - $this->where->isNull($field); - return $this; - } - - public function isNotNull($field) { - $this->where->isNotNull($field); - return $this; - } - - public function exists(SelectQueryInterface $select) { - $this->where->exists($select); - return $this; - } - - public function notExists(SelectQueryInterface $select) { - $this->where->notExists($select); - return $this; - } - - public function compile(DatabaseConnection $connection, QueryPlaceholderInterface $queryPlaceholder) { - $this->where->compile($connection, $queryPlaceholder); - $this->having->compile($connection, $queryPlaceholder); - - foreach ($this->tables as $table) { - // If this table is a subquery, compile it recursively. - if ($table['table'] instanceof SelectQueryInterface) { - $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); - } - } - - 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 SelectQueryInterface) { - if (!$table['table']->compiled()) { - return FALSE; - } - } - } - - foreach ($this->union as $union) { - if (!$union['query']->compiled()) { - return FALSE; - } - } - - return TRUE; - } - - /* Implementations of QueryConditionInterface for the HAVING clause. */ - - public function havingCondition($field, $value = NULL, $operator = NULL) { - $this->having->condition($field, $value, $operator); - return $this; - } - - public function &havingConditions() { - return $this->having->conditions(); - } - - public function havingArguments() { - return $this->having->arguments(); - } - - public function having($snippet, $args = array()) { - $this->having->where($snippet, $args); - return $this; - } - - public function havingCompile(DatabaseConnection $connection) { - return $this->having->compile($connection, $this); - } - - /* Implementations of QueryExtendableInterface. */ - - 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); - } - - public function havingIsNull($field) { - $this->having->isNull($field); - return $this; - } - - public function havingIsNotNull($field) { - $this->having->isNotNull($field); - return $this; - } - - public function havingExists(SelectQueryInterface $select) { - $this->having->exists($select); - return $this; - } - - public function havingNotExists(SelectQueryInterface $select) { - $this->having->notExists($select); - return $this; - } - - public function forUpdate($set = TRUE) { - if (isset($set)) { - $this->forUpdate = $set; - } - return $this; - } - - /* Alter accessors to expose the query data to alter hooks. */ - - public function &getFields() { - return $this->fields; - } - - public function &getExpressions() { - return $this->expressions; - } - - public function &getOrderBy() { - return $this->order; - } - - public function &getGroupBy() { - return $this->group; - } - - public function &getTables() { - return $this->tables; - } - - public function &getUnion() { - return $this->union; - } - - public function getArguments(QueryPlaceholderInterface $queryPlaceholder = NULL) { - if (!isset($queryPlaceholder)) { - $queryPlaceholder = $this; - } - $this->compile($this->connection, $queryPlaceholder); - return $this->arguments(); - } - - /** - * Indicates if preExecute() has already been called on that object. - */ - public function isPrepared() { - return $this->prepared; - } - - /** - * Generic preparation and validation for a SELECT query. - * - * @return - * TRUE if the validation was successful, FALSE if not. - */ - public function preExecute(SelectQueryInterface $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_alter($hooks, $query); - } - - $this->prepared = TRUE; - - // Now also prepare any sub-queries. - foreach ($this->tables as $table) { - if ($table['table'] instanceof SelectQueryInterface) { - $table['table']->preExecute(); - } - } - - foreach ($this->union as $union) { - $union['query']->preExecute(); - } - - return $this->prepared; - } - - 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); - } - - public function distinct($distinct = TRUE) { - $this->distinct = $distinct; - return $this; - } - - 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; - } - - 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; - } - - 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; - } - - public function join($table, $alias = NULL, $condition = NULL, $arguments = array()) { - return $this->addJoin('INNER', $table, $alias, $condition, $arguments); - } - - public function innerJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) { - return $this->addJoin('INNER', $table, $alias, $condition, $arguments); - } - - public function leftJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) { - return $this->addJoin('LEFT OUTER', $table, $alias, $condition, $arguments); - } - - public function rightJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) { - return $this->addJoin('RIGHT OUTER', $table, $alias, $condition, $arguments); - } - - public function addJoin($type, $table, $alias = NULL, $condition = NULL, $arguments = array()) { - - if (empty($alias)) { - if ($table instanceof SelectQueryInterface) { - $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; - } - - public function orderBy($field, $direction = 'ASC') { - $this->order[$field] = $direction; - return $this; - } - - public function orderRandom() { - $alias = $this->addExpression('RAND()', 'random_field'); - $this->orderBy($alias); - return $this; - } - - public function range($start = NULL, $length = NULL) { - $this->range = func_num_args() ? array('start' => $start, 'length' => $length) : array(); - return $this; - } - - public function union(SelectQueryInterface $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; - } - - public function groupBy($field) { - $this->group[$field] = $field; - return $this; - } - - public function countQuery() { - // 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 $alias => &$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; - } - - $query = $this->connection->select($count); - $query->addExpression('COUNT(*)'); - - return $query; - } - - 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 $alias => $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 $alias => $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 $alias => $table) { - $query .= "\n"; - if (isset($table['join type'])) { - $query .= $table['join type'] . ' JOIN '; - } - - // If the table is a subquery, compile it and integrate it into this query. - if ($table['table'] instanceof SelectQueryInterface) { - // Run preparation steps on this sub-query before converting to string. - $subquery = $table['table']; - $subquery->preExecute(); - $table_string = '(' . (string) $subquery . ')'; - } - else { - $table_string = '{' . $this->connection->escapeTable($table['table']) . '}'; - } - - // Don't use the AS keyword for table aliases, as some - // databases don't support it (e.g., Oracle). - $query .= $table_string . ' ' . $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[] = $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; - } - - 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']); - } - } -} - -/** - * @} End of "ingroup database". - */ diff --git a/core/includes/database/sqlite/query.inc b/core/includes/database/sqlite/query.inc deleted file mode 100644 index 6b8a72f2ab46aed8da9f908a7f66f4f71d27b11a..0000000000000000000000000000000000000000 --- a/core/includes/database/sqlite/query.inc +++ /dev/null @@ -1,160 +0,0 @@ -<?php - -/** - * @file - * Query code for SQLite embedded database engine. - */ - -/** - * @ingroup database - * @{ - */ - -/** - * 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 InsertQuery_sqlite extends InsertQuery { - - public function execute() { - if (!$this->preExecute()) { - return NULL; - } - if (count($this->insertFields)) { - 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_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)) { - return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $this->insertFields) . ') ' . $this->fromQuery; - } - - return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $this->insertFields) . ') VALUES (' . implode(', ', $placeholders) . ')'; - } - -} - -/** - * SQLite specific implementation of UpdateQuery. - * - * SQLite counts all the rows that match the conditions as modified, even if they - * will not be affected by the query. We workaround this by ensuring that - * we don't select those rows. - * - * A query like this one: - * UPDATE test SET name = 'newname' WHERE tid = 1 - * will become: - * UPDATE test SET name = 'newname' WHERE tid = 1 AND name <> 'newname' - */ -class UpdateQuery_sqlite extends UpdateQuery { - /** - * Helper function that removes the fields that are already in a condition. - * - * @param $fields - * The fields. - * @param QueryConditionInterface $condition - * A database condition. - */ - protected function removeFieldsInCondition(&$fields, QueryConditionInterface $condition) { - foreach ($condition->conditions() as $child_condition) { - if ($child_condition['field'] instanceof QueryConditionInterface) { - $this->removeFieldsInCondition($fields, $child_condition['field']); - } - else { - unset($fields[$child_condition['field']]); - } - } - } - - public function execute() { - if (!empty($this->queryOptions['sqlite_return_matched_rows'])) { - return parent::execute(); - } - - // Get the fields used in the update query, and remove those that are already - // in the condition. - $fields = $this->expressionFields + $this->fields; - $this->removeFieldsInCondition($fields, $this->condition); - - // Add the inverse of the fields to the condition. - $condition = new DatabaseCondition('OR'); - foreach ($fields as $field => $data) { - if (is_array($data)) { - // The field is an expression. - $condition->where($field . ' <> ' . $data['expression']); - $condition->isNull($field); - } - elseif (!isset($data)) { - // The field will be set to NULL. - $condition->isNotNull($field); - } - else { - $condition->condition($field, $data, '<>'); - $condition->isNull($field); - } - } - if (count($condition)) { - $condition->compile($this->connection, $this); - $this->condition->where((string) $condition, $condition->arguments()); - } - return parent::execute(); - } - -} - -/** - * SQLite specific implementation of DeleteQuery. - * - * When the WHERE is omitted from a DELETE statement and the table being deleted - * has no triggers, SQLite uses an optimization to erase the entire table content - * without having to visit each row of the table individually. - * - * Prior to SQLite 3.6.5, SQLite does not return the actual number of rows deleted - * by that optimized "truncate" optimization. - */ -class DeleteQuery_sqlite extends DeleteQuery { - public function execute() { - if (!count($this->condition)) { - $total_rows = $this->connection->query('SELECT COUNT(*) FROM {' . $this->connection->escapeTable($this->table) . '}')->fetchField(); - parent::execute(); - return $total_rows; - } - else { - return parent::execute(); - } - } -} - -/** - * 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 TruncateQuery_sqlite extends TruncateQuery { - 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) . '} '; - } -} - -/** - * @} End of "ingroup database". - */ diff --git a/core/includes/database/sqlite/select.inc b/core/includes/database/sqlite/select.inc deleted file mode 100644 index fb926ef04d3124d7cb8efe35a7e51a0e81301f6c..0000000000000000000000000000000000000000 --- a/core/includes/database/sqlite/select.inc +++ /dev/null @@ -1,27 +0,0 @@ -<?php - -/** - * @file - * Select builder for SQLite embedded database engine. - */ - -/** - * @ingroup database - * @{ - */ - -/** - * SQLite specific query builder for SELECT statements. - */ -class SelectQuery_sqlite extends SelectQuery { - public function forUpdate($set = TRUE) { - // SQLite does not support FOR UPDATE so nothing to do. - return $this; - } -} - -/** - * @} End of "ingroup database". - */ - - diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index ef7eafe4f917d7364c80ba5b5a17499dd5826962..f16e094d8e6bf7c57301b82c8c7136a5978ebe84 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -1,5 +1,8 @@ <?php +use Drupal\Core\Database\Database; +use Drupal\Core\Database\Install\TaskException; + /** * @file * API functions for installing Drupal. @@ -223,6 +226,27 @@ function install_begin_request(&$install_state) { // Allow command line scripts to override server variables used by Drupal. require_once DRUPAL_ROOT . '/core/includes/bootstrap.inc'; + + // Ensure that the class loader is available so that we can leverage classes + // as part of the install routine. + $loader = drupal_classloader(); + + // Register explicit vendor namespaces. + $loader->registerNamespaces(array( + // All Symfony-borrowed code lives in /core/includes/Symfony. + 'Symfony' => DRUPAL_ROOT . '/core/vendor', + )); + // Register the Drupal namespace for classes in core as a fallback. + // This allows to register additional namespaces within the Drupal namespace + // (e.g., for modules) and avoids an additional file_exists() on the Drupal + // core namespace, since the class loader can already determine the best + // namespace match based on a string comparison. It further allows modules to + // register/overload namespaces in Drupal core. + $loader->registerNamespaceFallbacks(array( + // All Drupal-namespaced code in core lives in /core/includes/Drupal. + 'Drupal' => DRUPAL_ROOT . '/core/lib', + )); + if (!$install_state['interactive']) { drupal_override_server_variables($install_state['server']); } @@ -961,7 +985,7 @@ function install_database_errors($database, $settings_file) { try { db_run_tasks($driver); } - catch (DatabaseTaskException $e) { + catch (TaskException $e) { // These are generic errors, so we do not have any specific key of the // database connection array to attach them to; therefore, we just put // them in the error array with standard numeric keys. diff --git a/core/includes/install.inc b/core/includes/install.inc index 533678f0f2ff770915c6021d947a069fb4889e4c..8a25693a1c2a0dce3c827f9d17827912b19d5967 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -1,5 +1,7 @@ <?php +use Drupal\Core\Database\Database; + /** * Indicates that a module has not been installed yet. */ @@ -246,6 +248,7 @@ function drupal_detect_database_types() { */ function drupal_get_database_types() { $databases = array(); + $drivers = array(); // We define a driver as a directory in /core/includes/database that in turn // contains a database.inc file. That allows us to drop in additional drivers @@ -253,8 +256,8 @@ function drupal_get_database_types() { // Because we have no registry yet, we need to also include the install.inc // file for the driver explicitly. require_once DRUPAL_ROOT . '/core/includes/database/database.inc'; - foreach (file_scan_directory(DRUPAL_ROOT . '/core/includes/database', '/^[a-z]*$/i', array('recurse' => FALSE)) as $file) { - if (file_exists($file->uri . '/database.inc') && file_exists($file->uri . '/install.inc')) { + foreach (file_scan_directory(DRUPAL_ROOT . '/core/lib/Drupal/Core/Database/Driver', '/^[a-z]*$/i', array('recurse' => FALSE)) as $file) { + if (file_exists($file->uri . '/Install/Tasks.php')) { $drivers[$file->filename] = $file->uri; } } @@ -276,305 +279,6 @@ function drupal_get_database_types() { return $databases; } -/** - * Database installer structure. - * - * Defines basic Drupal requirements for databases. - */ -abstract class DatabaseTasks { - - /** - * 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 DatabaseTaskException(st("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 .= '<p class="error">' . $result . '</p>'; - } - } - if (!empty($message)) { - $message = '<p>In order for Drupal to work, and to continue with the installation process, you must resolve all issues reported below. For more help with configuring your database server, see the <a href="http://drupal.org/getting-started/install">installation handbook</a>. If you are unsure what any of this means you should probably contact your hosting provider.</p>' . $message; - throw new DatabaseTaskException($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(st('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 { - db_query($query); - $this->pass(st($pass)); - } - catch (Exception $e) { - $this->fail(st($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(st("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($database) { - $form['database'] = array( - '#type' => 'textfield', - '#title' => st('Database name'), - '#default_value' => empty($database['database']) ? '' : $database['database'], - '#size' => 45, - '#required' => TRUE, - '#description' => st('The name of the database your @drupal data will be stored in. It must exist on your server before @drupal can be installed.', array('@drupal' => drupal_install_profile_distribution_name())), - ); - - $form['username'] = array( - '#type' => 'textfield', - '#title' => st('Database username'), - '#default_value' => empty($database['username']) ? '' : $database['username'], - '#required' => TRUE, - '#size' => 45, - ); - - $form['password'] = array( - '#type' => 'password', - '#title' => st('Database password'), - '#default_value' => empty($database['password']) ? '' : $database['password'], - '#required' => FALSE, - '#size' => 45, - ); - - $form['advanced_options'] = array( - '#type' => 'fieldset', - '#title' => st('Advanced options'), - '#collapsible' => TRUE, - '#collapsed' => TRUE, - '#description' => st("These options are only necessary for some sites. If you're not sure what you should enter here, leave the default settings or check with your hosting provider."), - '#weight' => 10, - ); - - $profile = drupal_get_profile(); - $db_prefix = ($profile == 'standard') ? 'drupal_' : $profile . '_'; - $form['advanced_options']['db_prefix'] = array( - '#type' => 'textfield', - '#title' => st('Table prefix'), - '#default_value' => '', - '#size' => 45, - '#description' => st('If more than one application will be sharing this database, enter a table prefix such as %prefix for your @drupal site here.', array('@drupal' => drupal_install_profile_distribution_name(), '%prefix' => $db_prefix)), - '#weight' => 10, - ); - - $form['advanced_options']['host'] = array( - '#type' => 'textfield', - '#title' => st('Database host'), - '#default_value' => empty($database['host']) ? 'localhost' : $database['host'], - '#size' => 45, - // Hostnames can be 255 characters long. - '#maxlength' => 255, - '#required' => TRUE, - '#description' => st('If your database is located on a different server, change this.'), - ); - - $form['advanced_options']['port'] = array( - '#type' => 'textfield', - '#title' => st('Database port'), - '#default_value' => empty($database['port']) ? '' : $database['port'], - '#size' => 45, - // The maximum port number is 65536, 5 digits. - '#maxlength' => 5, - '#description' => st('If your database server is listening to a non-standard port, enter its number.'), - ); - - 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'] . '][advanced_options][db_prefix'] = st('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'])); - } - - // Verify the database port. - if (!empty($database['port']) && !is_numeric($database['port'])) { - $errors[$database['driver'] . '][advanced_options][port'] = st('Database port must be a number.'); - } - - return $errors; - } - -} - -/** - * Exception thrown if the database installer fails. - */ -class DatabaseTaskException extends Exception { -} - /** * Replace values in settings.php with values in the submitted array. * @@ -1300,7 +1004,8 @@ function db_run_tasks($driver) { * The name of the driver. */ function db_installer_object($driver) { - Database::loadDriverFile($driver, array('install.inc')); - $task_class = 'DatabaseTasks_' . $driver; + // We cannot use Database::getConnection->getDriverClass() here, because + // the connection object is not yet functional. + $task_class = "Drupal\\Core\\Database\\Driver\\{$driver}\\Install\\Tasks"; return new $task_class(); } diff --git a/core/includes/pager.inc b/core/includes/pager.inc index c060d0e7cda526b514e3ac999a01e789151ca412..c579e7e64809c81f9df08dfbbef9424a291a15a5 100644 --- a/core/includes/pager.inc +++ b/core/includes/pager.inc @@ -1,18 +1,21 @@ <?php +use Drupal\Core\Database\Connection; +use Drupal\Core\Database\Query\SelectExtender; +use Drupal\Core\Database\Query\SelectInterface; + /** * @file * Functions to aid in presenting database results as a set of pages. */ - /** * Query extender for pager queries. * * This is the "default" pager mechanism. It creates a paged query with a fixed * number of entries per page. */ -class PagerDefault extends SelectQueryExtender { +class PagerDefault extends SelectExtender { /** * The highest element we've autogenerated so far. @@ -42,7 +45,7 @@ class PagerDefault extends SelectQueryExtender { */ protected $customCountQuery = FALSE; - public function __construct(SelectQueryInterface $query, DatabaseConnection $connection) { + 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 @@ -103,7 +106,7 @@ protected function ensureElement() { * The count query object. It must return a single row with a single column, * which is the total number of records. */ - public function setCountQuery(SelectQueryInterface $query) { + public function setCountQuery(SelectInterface $query) { $this->customCountQuery = $query; } diff --git a/core/includes/registry.inc b/core/includes/registry.inc index 2ddd1f7f70a8d90333911d10a20f91d49a12420f..7ac296017942aca8c1d0245829dfda4333399681 100644 --- a/core/includes/registry.inc +++ b/core/includes/registry.inc @@ -1,5 +1,7 @@ <?php +use Drupal\Core\Database\Database; + /** * @file * This file contains the code registry parser engine. @@ -20,18 +22,12 @@ */ function _registry_update() { - // The registry serves as a central autoloader for all classes, including - // the database query builders. However, the registry rebuild process - // requires write ability to the database, which means having access to the - // query builders that require the registry in order to be loaded. That - // causes a fatal race condition. Therefore we manually include the - // appropriate query builders for the currently active database before the - // registry rebuild process runs. + // The registry serves as a central autoloader for all non-namespaced classes. + // It is backed by the database, but the database system is autoloaded using + // a PSR-0 class loader. That avoids a fata circular dependency here, since + // the other class loader will be able to load the database for us. $connection_info = Database::getConnectionInfo(); $driver = $connection_info['default']['driver']; - require_once DRUPAL_ROOT . '/core/includes/database/query.inc'; - require_once DRUPAL_ROOT . '/core/includes/database/select.inc'; - require_once DRUPAL_ROOT . '/core/includes/database/' . $driver . '/query.inc'; // Get current list of modules and their files. $modules = db_query("SELECT * FROM {system} WHERE type = 'module'")->fetchAll(); diff --git a/core/includes/tablesort.inc b/core/includes/tablesort.inc index 7873cdb2db42f2aba2a6f0ae876f7ffb7c02def2..3c70b965c30a0df9757022dd3cb2dfa2d79d0d29 100644 --- a/core/includes/tablesort.inc +++ b/core/includes/tablesort.inc @@ -1,5 +1,9 @@ <?php +use Drupal\Core\Database\Connection; +use Drupal\Core\Database\Query\SelectExtender; +use Drupal\Core\Database\Query\SelectInterface; + /** * @file * Functions to aid in the creation of sortable tables. @@ -8,11 +12,10 @@ * column headers that the user can click on to sort the table by that column. */ - /** * Query extender class for tablesort queries. */ -class TableSort extends SelectQueryExtender { +class TableSort extends SelectExtender { /** * The array of fields that can be sorted by. @@ -21,7 +24,7 @@ class TableSort extends SelectQueryExtender { */ protected $header = array(); - public function __construct(SelectQueryInterface $query, DatabaseConnection $connection) { + 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 diff --git a/core/lib/Drupal/Core/Database/Connection.php b/core/lib/Drupal/Core/Database/Connection.php new file mode 100644 index 0000000000000000000000000000000000000000..6e9e44aec14605fc8b6f902a49e2937ef0b03126 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Connection.php @@ -0,0 +1,1126 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\Connection + */ + +namespace Drupal\Core\Database; + +use Drupal\Core\Database\TransactionNoActiveException; +use Drupal\Core\Database\TransactionOutOfOrderException; + +use PDO; +use PDOException; + +/** + * Base Database API class. + * + * This class provides a Drupal-specific extension of the PDO database + * abstraction class in PHP. Every database driver implementation must provide a + * concrete implementation of it to support special handling required by that + * database. + * + * @see http://php.net/manual/en/book.pdo.php + */ +abstract class Connection extends PDO { + + /** + * The database target this connection is for. + * + * We need this information for later auditing and logging. + * + * @var string + */ + protected $target = NULL; + + /** + * The key representing this connection. + * + * The key is a unique string which identifies a database connection. A + * connection can be a single server or a cluster of master and slaves (use + * target to pick between master and slave). + * + * @var string + */ + protected $key = NULL; + + /** + * The current database logging object for this connection. + * + * @var Log + */ + protected $logger = NULL; + + /** + * Tracks the number of "layers" of transactions currently active. + * + * On many databases transactions cannot nest. Instead, we track + * nested calls to transactions and collapse them into a single + * transaction. + * + * @var array + */ + protected $transactionLayers = array(); + + /** + * Index of what driver-specific class to use for various operations. + * + * @var array + */ + protected $driverClasses = array(); + + /** + * The name of the Statement class for this connection. + * + * @var string + */ + protected $statementClass = 'Drupal\Core\Database\Statement'; + + /** + * Whether this database connection supports transactions. + * + * @var bool + */ + protected $transactionSupport = TRUE; + + /** + * Whether this database connection supports transactional DDL. + * + * Set to FALSE by default because few databases support this feature. + * + * @var bool + */ + protected $transactionalDDLSupport = FALSE; + + /** + * An index used to generate unique temporary table names. + * + * @var integer + */ + protected $temporaryNameIndex = 0; + + /** + * The connection information for this connection object. + * + * @var array + */ + protected $connectionOptions = array(); + + /** + * The schema object for this connection. + * + * @var object + */ + protected $schema = NULL; + + /** + * The prefixes used by this database connection. + * + * @var array + */ + protected $prefixes = array(); + + /** + * List of search values for use in prefixTables(). + * + * @var array + */ + protected $prefixSearch = array(); + + /** + * List of replacement values for use in prefixTables(). + * + * @var array + */ + protected $prefixReplace = array(); + + function __construct($dsn, $username, $password, $driver_options = array()) { + // Initialize and prepare the connection prefix. + $this->setPrefix(isset($this->connectionOptions['prefix']) ? $this->connectionOptions['prefix'] : ''); + + // Because the other methods don't seem to work right. + $driver_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION; + + // Call PDO::__construct and PDO::setAttribute. + parent::__construct($dsn, $username, $password, $driver_options); + + // Set a specific PDOStatement class if the driver requires that. + if (!empty($this->statementClass)) { + $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array($this->statementClass, array($this))); + } + } + + /** + * Returns the default query options for any given query. + * + * A given query can be customized with a number of option flags in an + * associative array: + * - target: The database "target" against which to execute a query. Valid + * values are "default" or "slave". The system will first try to open a + * connection to a database specified with the user-supplied key. If one + * is not available, it will silently fall back to the "default" target. + * If multiple databases connections are specified with the same target, + * one will be selected at random for the duration of the request. + * - fetch: This element controls how rows from a result set will be + * returned. Legal values include PDO::FETCH_ASSOC, PDO::FETCH_BOTH, + * PDO::FETCH_OBJ, PDO::FETCH_NUM, or a string representing the name of a + * class. If a string is specified, each record will be fetched into a new + * object of that class. The behavior of all other values is defined by PDO. + * See http://php.net/manual/pdostatement.fetch.php + * - return: Depending on the type of query, different return values may be + * meaningful. This directive instructs the system which type of return + * value is desired. The system will generally set the correct value + * automatically, so it is extremely rare that a module developer will ever + * need to specify this value. Setting it incorrectly will likely lead to + * unpredictable results or fatal errors. Legal values include: + * - Database::RETURN_STATEMENT: Return the prepared statement object for + * the query. This is usually only meaningful for SELECT queries, where + * the statement object is how one accesses the result set returned by the + * query. + * - Database::RETURN_AFFECTED: Return the number of rows affected by an + * UPDATE or DELETE query. Be aware that means the number of rows actually + * changed, not the number of rows matched by the WHERE clause. + * - Database::RETURN_INSERT_ID: Return the sequence ID (primary key) + * created by an INSERT statement on a table that contains a serial + * column. + * - Database::RETURN_NULL: Do not return anything, as there is no + * meaningful value to return. That is the case for INSERT queries on + * tables that do not contain a serial column. + * - throw_exception: By default, the database system will catch any errors + * on a query as an Exception, log it, and then rethrow it so that code + * further up the call chain can take an appropriate action. To suppress + * that behavior and simply return NULL on failure, set this option to + * FALSE. + * + * @return + * An array of default query options. + */ + protected function defaultOptions() { + return array( + 'target' => 'default', + 'fetch' => PDO::FETCH_OBJ, + 'return' => Database::RETURN_STATEMENT, + 'throw_exception' => TRUE, + ); + } + + /** + * Returns the connection information for this connection object. + * + * Note that Database::getConnectionInfo() is for requesting information + * about an arbitrary database connection that is defined. This method + * is for requesting the connection information of this specific + * open connection object. + * + * @return + * An array of the connection information. The exact list of + * properties is driver-dependent. + */ + public function getConnectionOptions() { + return $this->connectionOptions; + } + + /** + * Set the list of prefixes used by this database connection. + * + * @param $prefix + * The prefixes, in any of the multiple forms documented in + * default.settings.php. + */ + protected function setPrefix($prefix) { + if (is_array($prefix)) { + $this->prefixes = $prefix + array('default' => ''); + } + else { + $this->prefixes = array('default' => $prefix); + } + + // Set up variables for use in prefixTables(). Replace table-specific + // prefixes first. + $this->prefixSearch = array(); + $this->prefixReplace = array(); + foreach ($this->prefixes as $key => $val) { + if ($key != 'default') { + $this->prefixSearch[] = '{' . $key . '}'; + $this->prefixReplace[] = $val . $key; + } + } + // Then replace remaining tables with the default prefix. + $this->prefixSearch[] = '{'; + $this->prefixReplace[] = $this->prefixes['default']; + $this->prefixSearch[] = '}'; + $this->prefixReplace[] = ''; + } + + /** + * Appends a database prefix to all tables in a query. + * + * Queries sent to Drupal should wrap all table names in curly brackets. This + * function searches for this syntax and adds Drupal's table prefix to all + * tables, allowing Drupal to coexist with other systems in the same database + * and/or schema if necessary. + * + * @param $sql + * A string containing a partial or entire SQL query. + * + * @return + * The properly-prefixed string. + */ + public function prefixTables($sql) { + return str_replace($this->prefixSearch, $this->prefixReplace, $sql); + } + + /** + * Find the prefix for a table. + * + * This function is for when you want to know the prefix of a table. This + * is not used in prefixTables due to performance reasons. + */ + public function tablePrefix($table = 'default') { + if (isset($this->prefixes[$table])) { + return $this->prefixes[$table]; + } + else { + return $this->prefixes['default']; + } + } + + /** + * Prepares a query string and returns the prepared statement. + * + * This method caches prepared statements, reusing them when + * possible. It also prefixes tables names enclosed in curly-braces. + * + * @param $query + * The query string as SQL, with curly-braces surrounding the + * table names. + * + * @return Drupal\Core\Database\StatementInterface + * A PDO prepared statement ready for its execute() method. + */ + public function prepareQuery($query) { + $query = $this->prefixTables($query); + + // Call PDO::prepare. + return parent::prepare($query); + } + + /** + * Tells this connection object what its target value is. + * + * This is needed for logging and auditing. It's sloppy to do in the + * constructor because the constructor for child classes has a different + * signature. We therefore also ensure that this function is only ever + * called once. + * + * @param $target + * The target this connection is for. Set to NULL (default) to disable + * logging entirely. + */ + public function setTarget($target = NULL) { + if (!isset($this->target)) { + $this->target = $target; + } + } + + /** + * Returns the target this connection is associated with. + * + * @return + * The target string of this connection. + */ + public function getTarget() { + return $this->target; + } + + /** + * Tells this connection object what its key is. + * + * @param $target + * The key this connection is for. + */ + public function setKey($key) { + if (!isset($this->key)) { + $this->key = $key; + } + } + + /** + * Returns the key this connection is associated with. + * + * @return + * The key of this connection. + */ + public function getKey() { + return $this->key; + } + + /** + * Associates a logging object with this connection. + * + * @param $logger + * The logging object we want to use. + */ + public function setLogger(Log $logger) { + $this->logger = $logger; + } + + /** + * Gets the current logging object for this connection. + * + * @return DatabaseLog + * The current logging object for this connection. If there isn't one, + * NULL is returned. + */ + public function getLogger() { + return $this->logger; + } + + /** + * Creates the appropriate sequence name for a given table and serial field. + * + * This information is exposed to all database drivers, although it is only + * useful on some of them. This method is table prefix-aware. + * + * @param $table + * The table name to use for the sequence. + * @param $field + * The field name to use for the sequence. + * + * @return + * A table prefix-parsed string for the sequence name. + */ + public function makeSequenceName($table, $field) { + return $this->prefixTables('{' . $table . '}_' . $field . '_seq'); + } + + /** + * Flatten an array of query comments into a single comment string. + * + * The comment string will be sanitized to avoid SQL injection attacks. + * + * @param $comments + * An array of query comment strings. + * + * @return + * A sanitized comment string. + */ + public function makeComment($comments) { + if (empty($comments)) + return ''; + + // Flatten the array of comments. + $comment = implode('; ', $comments); + + // Sanitize the comment string so as to avoid SQL injection attacks. + return '/* ' . $this->filterComment($comment) . ' */ '; + } + + /** + * Sanitize a query comment string. + * + * Ensure a query comment does not include strings such as "* /" that might + * terminate the comment early. This avoids SQL injection attacks via the + * query comment. The comment strings in this example are separated by a + * space to avoid PHP parse errors. + * + * For example, the comment: + * @code + * db_update('example') + * ->condition('id', $id) + * ->fields(array('field2' => 10)) + * ->comment('Exploit * / DROP TABLE node; --') + * ->execute() + * @endcode + * + * Would result in the following SQL statement being generated: + * @code + * "/ * Exploit * / DROP TABLE node; -- * / UPDATE example SET field2=..." + * @endcode + * + * Unless the comment is sanitised first, the SQL server would drop the + * node table and ignore the rest of the SQL statement. + * + * @param $comment + * A query comment string. + * + * @return + * A sanitized version of the query comment string. + */ + protected function filterComment($comment = '') { + return preg_replace('/(\/\*\s*)|(\s*\*\/)/', '', $comment); + } + + /** + * Executes a query string against the database. + * + * This method provides a central handler for the actual execution of every + * query. All queries executed by Drupal are executed as PDO prepared + * statements. + * + * @param $query + * The query to execute. In most cases this will be a string containing + * an SQL query with placeholders. An already-prepared instance of + * DatabaseStatementInterface may also be passed in order to allow calling + * code to manually bind variables to a query. If a + * DatabaseStatementInterface is passed, the $args array will be ignored. + * It is extremely rare that module code will need to pass a statement + * object to this method. It is used primarily for database drivers for + * databases that require special LOB field handling. + * @param $args + * An array of arguments for the prepared statement. If the prepared + * statement uses ? placeholders, this array must be an indexed array. + * If it contains named placeholders, it must be an associative array. + * @param $options + * An associative array of options to control how the query is run. See + * the documentation for DatabaseConnection::defaultOptions() for details. + * + * @return Drupal\Core\Database\StatementInterface + * This method will return one of: the executed statement, the number of + * rows affected by the query (not the number matched), or the generated + * insert IT of the last query, depending on the value of + * $options['return']. Typically that value will be set by default or a + * query builder and should not be set by a user. If there is an error, + * this method will return NULL and may throw an exception if + * $options['throw_exception'] is TRUE. + * + * @throws PDOException + */ + public function query($query, array $args = array(), $options = array()) { + + // Use default values if not already set. + $options += $this->defaultOptions(); + + try { + // We allow either a pre-bound statement object or a literal string. + // In either case, we want to end up with an executed statement object, + // which we pass to PDOStatement::execute. + if ($query instanceof DatabaseStatementInterface) { + $stmt = $query; + $stmt->execute(NULL, $options); + } + else { + $this->expandArguments($query, $args); + $stmt = $this->prepareQuery($query); + $stmt->execute($args, $options); + } + + // Depending on the type of query we may need to return a different value. + // See DatabaseConnection::defaultOptions() for a description of each + // value. + switch ($options['return']) { + case Database::RETURN_STATEMENT: + return $stmt; + case Database::RETURN_AFFECTED: + return $stmt->rowCount(); + case Database::RETURN_INSERT_ID: + return $this->lastInsertId(); + case Database::RETURN_NULL: + return; + default: + throw new PDOException('Invalid return directive: ' . $options['return']); + } + } + catch (PDOException $e) { + if ($options['throw_exception']) { + // Add additional debug information. + if ($query instanceof DatabaseStatementInterface) { + $e->query_string = $stmt->getQueryString(); + } + else { + $e->query_string = $query; + } + $e->args = $args; + throw $e; + } + return NULL; + } + } + + /** + * Expands out shorthand placeholders. + * + * Drupal supports an alternate syntax for doing arrays of values. We + * therefore need to expand them out into a full, executable query string. + * + * @param $query + * The query string to modify. + * @param $args + * The arguments for the query. + * + * @return + * TRUE if the query was modified, FALSE otherwise. + */ + protected function expandArguments(&$query, &$args) { + $modified = FALSE; + + // If the placeholder value to insert is an array, assume that we need + // to expand it out into a comma-delimited set of placeholders. + foreach (array_filter($args, 'is_array') as $key => $data) { + $new_keys = array(); + foreach ($data as $i => $value) { + // This assumes that there are no other placeholders that use the same + // name. For example, if the array placeholder is defined as :example + // and there is already an :example_2 placeholder, this will generate + // a duplicate key. We do not account for that as the calling code + // is already broken if that happens. + $new_keys[$key . '_' . $i] = $value; + } + + // Update the query with the new placeholders. + // preg_replace is necessary to ensure the replacement does not affect + // placeholders that start with the same exact text. For example, if the + // query contains the placeholders :foo and :foobar, and :foo has an + // array of values, using str_replace would affect both placeholders, + // but using the following preg_replace would only affect :foo because + // it is followed by a non-word character. + $query = preg_replace('#' . $key . '\b#', implode(', ', array_keys($new_keys)), $query); + + // Update the args array with the new placeholders. + unset($args[$key]); + $args += $new_keys; + + $modified = TRUE; + } + + return $modified; + } + + /** + * Gets the driver-specific override class if any for the specified class. + * + * @param string $class + * The class for which we want the potentially driver-specific class. + * @return string + * The name of the class that should be used for this driver. + */ + public function getDriverClass($class) { + if (empty($this->driverClasses[$class])) { + $driver = $this->driver(); + $driver_class = "Drupal\\Core\\Database\\Driver\\{$driver}\\{$class}"; + $this->driverClasses[$class] = class_exists($driver_class) ? $driver_class : $class; + } + return $this->driverClasses[$class]; + } + + /** + * Prepares and returns a SELECT query object. + * + * @param $table + * The base table for this query, that is, the first table in the FROM + * clause. This table will also be used as the "base" table for query_alter + * hook implementations. + * @param $alias + * The alias of the base table of this query. + * @param $options + * An array of options on the query. + * + * @return Drupal\Core\Database\Query\SelectInterface + * An appropriate SelectQuery object for this database connection. Note that + * it may be a driver-specific subclass of SelectQuery, depending on the + * driver. + * + * @see Drupal\Core\Database\Query\Select + */ + public function select($table, $alias = NULL, array $options = array()) { + $class = $this->getDriverClass('Select'); + return new $class($table, $alias, $this, $options); + } + + /** + * Prepares and returns an INSERT query object. + * + * @param $options + * An array of options on the query. + * + * @return Drupal\Core\Database\Query\Insert + * A new Insert query object. + * + * @see Drupal\Core\Database\Query\Insert + */ + public function insert($table, array $options = array()) { + $class = $this->getDriverClass('Insert'); + return new $class($this, $table, $options); + } + + /** + * Prepares and returns a MERGE query object. + * + * @param $options + * An array of options on the query. + * + * @return Drupal\Core\Database\Query\Merge + * A new Merge query object. + * + * @see Drupal\Core\Database\Query\Merge + */ + public function merge($table, array $options = array()) { + $class = $this->getDriverClass('Merge'); + return new $class($this, $table, $options); + } + + + /** + * Prepares and returns an UPDATE query object. + * + * @param $options + * An array of options on the query. + * + * @return Drupal\Core\Database\Query\Update + * A new Update query object. + * + * @see Drupal\Core\Database\Query\Update + */ + public function update($table, array $options = array()) { + $class = $this->getDriverClass('Update'); + return new $class($this, $table, $options); + } + + /** + * Prepares and returns a DELETE query object. + * + * @param $options + * An array of options on the query. + * + * @return Drupal\Core\Database\Query\Delete + * A new Delete query object. + * + * @see Drupal\Core\Database\Query\Delete + */ + public function delete($table, array $options = array()) { + $class = $this->getDriverClass('Delete'); + return new $class($this, $table, $options); + } + + /** + * Prepares and returns a TRUNCATE query object. + * + * @param $options + * An array of options on the query. + * + * @return Drupal\Core\Database\Query\Truncate + * A new Truncate query object. + * + * @see Drupal\Core\Database\Query\Truncate + */ + public function truncate($table, array $options = array()) { + $class = $this->getDriverClass('Truncate'); + return new $class($this, $table, $options); + } + + /** + * Returns a DatabaseSchema object for manipulating the schema. + * + * This method will lazy-load the appropriate schema library file. + * + * @return Drupal\Core\Database\Schema + * The database Schema object for this connection. + */ + public function schema() { + if (empty($this->schema)) { + $class = $this->getDriverClass('Schema'); + $this->schema = new $class($this); + } + return $this->schema; + } + + /** + * Escapes a table name string. + * + * Force all table names to be strictly alphanumeric-plus-underscore. + * For some database drivers, it may also wrap the table name in + * database-specific escape characters. + * + * @return + * The sanitized table name string. + */ + public function escapeTable($table) { + return preg_replace('/[^A-Za-z0-9_.]+/', '', $table); + } + + /** + * Escapes a field name string. + * + * Force all field names to be strictly alphanumeric-plus-underscore. + * For some database drivers, it may also wrap the field name in + * database-specific escape characters. + * + * @return + * The sanitized field name string. + */ + public function escapeField($field) { + return preg_replace('/[^A-Za-z0-9_.]+/', '', $field); + } + + /** + * Escapes an alias name string. + * + * Force all alias names to be strictly alphanumeric-plus-underscore. In + * contrast to DatabaseConnection::escapeField() / + * DatabaseConnection::escapeTable(), this doesn't allow the period (".") + * because that is not allowed in aliases. + * + * @return + * The sanitized field name string. + */ + public function escapeAlias($field) { + return preg_replace('/[^A-Za-z0-9_]+/', '', $field); + } + + /** + * Escapes characters that work as wildcard characters in a LIKE pattern. + * + * The wildcard characters "%" and "_" as well as backslash are prefixed with + * a backslash. Use this to do a search for a verbatim string without any + * wildcard behavior. + * + * For example, the following does a case-insensitive query for all rows whose + * name starts with $prefix: + * @code + * $result = db_query( + * 'SELECT * FROM person WHERE name LIKE :pattern', + * array(':pattern' => db_like($prefix) . '%') + * ); + * @endcode + * + * Backslash is defined as escape character for LIKE patterns in + * Drupal\Core\Database\Query\Condition::mapConditionOperator(). + * + * @param $string + * The string to escape. + * + * @return + * The escaped string. + */ + public function escapeLike($string) { + return addcslashes($string, '\%_'); + } + + /** + * Determines if there is an active transaction open. + * + * @return + * TRUE if we're currently in a transaction, FALSE otherwise. + */ + public function inTransaction() { + return ($this->transactionDepth() > 0); + } + + /** + * Determines current transaction depth. + */ + public function transactionDepth() { + return count($this->transactionLayers); + } + + /** + * Returns a new DatabaseTransaction object on this connection. + * + * @param $name + * Optional name of the savepoint. + * + * @see Drupal\Core\Database\Transaction + */ + public function startTransaction($name = '') { + $class = $this->getDriverClass('Transaction'); + return new $class($this, $name); + } + + /** + * Rolls back the transaction entirely or to a named savepoint. + * + * This method throws an exception if no transaction is active. + * + * @param $savepoint_name + * The name of the savepoint. The default, 'drupal_transaction', will roll + * the entire transaction back. + * + * @throws Drupal\Core\Database\TransactionNoActiveException + * + * @see DatabaseTransaction::rollback() + */ + public function rollback($savepoint_name = 'drupal_transaction') { + if (!$this->supportsTransactions()) { + return; + } + if (!$this->inTransaction()) { + throw new TransactionNoActiveException(); + } + // A previous rollback to an earlier savepoint may mean that the savepoint + // in question has already been accidentally committed. + if (!isset($this->transactionLayers[$savepoint_name])) { + throw new TransactionNoActiveException(); + } + + // We need to find the point we're rolling back to, all other savepoints + // before are no longer needed. If we rolled back other active savepoints, + // we need to throw an exception. + $rolled_back_other_active_savepoints = FALSE; + while ($savepoint = array_pop($this->transactionLayers)) { + if ($savepoint == $savepoint_name) { + // If it is the last the transaction in the stack, then it is not a + // savepoint, it is the transaction itself so we will need to roll back + // the transaction rather than a savepoint. + if (empty($this->transactionLayers)) { + break; + } + $this->query('ROLLBACK TO SAVEPOINT ' . $savepoint); + $this->popCommittableTransactions(); + if ($rolled_back_other_active_savepoints) { + throw new TransactionOutOfOrderException(); + } + return; + } + else { + $rolled_back_other_active_savepoints = TRUE; + } + } + parent::rollBack(); + if ($rolled_back_other_active_savepoints) { + throw new TransactionOutOfOrderException(); + } + } + + /** + * Increases the depth of transaction nesting. + * + * If no transaction is already active, we begin a new transaction. + * + * @throws Drupal\Core\Database\TransactionNameNonUniqueException + * + * @see Drupal\Core\Database\Transaction + */ + public function pushTransaction($name) { + if (!$this->supportsTransactions()) { + return; + } + if (isset($this->transactionLayers[$name])) { + throw new TransactionNameNonUniqueException($name . " is already in use."); + } + // If we're already in a transaction then we want to create a savepoint + // rather than try to create another transaction. + if ($this->inTransaction()) { + $this->query('SAVEPOINT ' . $name); + } + else { + parent::beginTransaction(); + } + $this->transactionLayers[$name] = $name; + } + + /** + * Decreases the depth of transaction nesting. + * + * If we pop off the last transaction layer, then we either commit or roll + * back the transaction as necessary. If no transaction is active, we return + * because the transaction may have manually been rolled back. + * + * @param $name + * The name of the savepoint + * + * @throws Drupal\Core\Database\TransactionNoActiveException + * @throws Drupal\Core\Database\TransactionCommitFailedException + * + * @see DatabaseTransaction + */ + public function popTransaction($name) { + if (!$this->supportsTransactions()) { + return; + } + // The transaction has already been committed earlier. There is nothing we + // need to do. If this transaction was part of an earlier out-of-order + // rollback, an exception would already have been thrown by + // Database::rollback(). + if (!isset($this->transactionLayers[$name])) { + return; + } + + // Mark this layer as committable. + $this->transactionLayers[$name] = FALSE; + $this->popCommittableTransactions(); + } + + /** + * Internal function: commit all the transaction layers that can commit. + */ + 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 (!parent::commit()) { + throw new TransactionCommitFailedException(); + } + } + else { + $this->query('RELEASE SAVEPOINT ' . $name); + } + } + } + + /** + * Runs a limited-range query on this database object. + * + * Use this as a substitute for ->query() when a subset of the query is to be + * returned. User-supplied arguments to the query should be passed in as + * separate parameters so that they can be properly escaped to avoid SQL + * injection attacks. + * + * @param $query + * A string containing an SQL query. + * @param $args + * An array of values to substitute into the query at placeholder markers. + * @param $from + * The first result row to return. + * @param $count + * The maximum number of result rows to return. + * @param $options + * An array of options on the query. + * + * @return Drupal\Core\Database\StatementInterface + * A database query result resource, or NULL if the query was not executed + * correctly. + */ + abstract public function queryRange($query, $from, $count, array $args = array(), array $options = array()); + + /** + * Generates a temporary table name. + * + * @return + * A table name. + */ + protected function generateTemporaryTableName() { + return "db_temporary_" . $this->temporaryNameIndex++; + } + + /** + * Runs a SELECT query and stores its results in a temporary table. + * + * Use this as a substitute for ->query() when the results need to stored + * in a temporary table. Temporary tables exist for the duration of the page + * request. User-supplied arguments to the query should be passed in as + * separate parameters so that they can be properly escaped to avoid SQL + * injection attacks. + * + * Note that if you need to know how many results were returned, you should do + * a SELECT COUNT(*) on the temporary table afterwards. + * + * @param $query + * A string containing a normal SELECT SQL query. + * @param $args + * An array of values to substitute into the query at placeholder markers. + * @param $options + * An associative array of options to control how the query is run. See + * the documentation for DatabaseConnection::defaultOptions() for details. + * + * @return + * The name of the temporary table. + */ + abstract function queryTemporary($query, array $args = array(), array $options = array()); + + /** + * Returns the type of database driver. + * + * This is not necessarily the same as the type of the database itself. For + * instance, there could be two MySQL drivers, mysql and mysql_mock. This + * function would return different values for each, but both would return + * "mysql" for databaseType(). + */ + abstract public function driver(); + + /** + * Returns the version of the database server. + */ + public function version() { + return $this->getAttribute(PDO::ATTR_SERVER_VERSION); + } + + /** + * Determines if this driver supports transactions. + * + * @return + * TRUE if this connection supports transactions, FALSE otherwise. + */ + public function supportsTransactions() { + return $this->transactionSupport; + } + + /** + * Determines if this driver supports transactional DDL. + * + * DDL queries are those that change the schema, such as ALTER queries. + * + * @return + * TRUE if this connection supports transactions for DDL queries, FALSE + * otherwise. + */ + public function supportsTransactionalDDL() { + return $this->transactionalDDLSupport; + } + + /** + * Returns the name of the PDO driver for this connection. + */ + abstract public function databaseType(); + + + /** + * 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. Database connections should define only + * those operators they wish to be handled differently than the default. + * + * @param $operator + * The condition operator, such as "IN", "BETWEEN", etc. Case-sensitive. + * + * @return + * The extra handling directives for the specified operator, or NULL. + * + * @see Drupal\Core\Database\Query\Condition::compile() + */ + abstract public function mapConditionOperator($operator); + + /** + * Throws an exception to deny direct access to transaction commits. + * + * We do not want to allow users to commit transactions at any time, only + * by destroying the transaction object or allowing it to go out of scope. + * A direct commit bypasses all of the safety checks we've built on top of + * PDO's transaction routines. + * + * @throws Drupal\Core\Database\TransactionExplicitCommitNotAllowedException + * + * @see Drupal\Core\Database\Transaction + */ + public function commit() { + throw new TransactionExplicitCommitNotAllowedException(); + } + + /** + * Retrieves an unique id from a given sequence. + * + * Use this function if for some reason you can't use a serial field. For + * example, MySQL has no ways of reading of the current value of a sequence + * and PostgreSQL can not advance the sequence to be larger than a given + * value. Or sometimes you just need a unique integer. + * + * @param $existing_id + * After a database import, it might be that the sequences table is behind, + * so by passing in the maximum existing id, it can be assured that we + * never issue the same id. + * + * @return + * An integer number larger than any number returned by earlier calls and + * also larger than the $existing_id if one was passed in. + */ + abstract public function nextId($existing_id = 0); +} diff --git a/core/lib/Drupal/Core/Database/ConnectionNotDefinedException.php b/core/lib/Drupal/Core/Database/ConnectionNotDefinedException.php new file mode 100644 index 0000000000000000000000000000000000000000..d14535822fe3c0f4a033612c428ecdf8285eab63 --- /dev/null +++ b/core/lib/Drupal/Core/Database/ConnectionNotDefinedException.php @@ -0,0 +1,15 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\ConnectionNotDefinedException + */ + +namespace Drupal\Core\Database; + +use RuntimeException; + +/** + * Exception thrown if an undefined database connection is requested. + */ +class ConnectionNotDefinedException extends RuntimeException {} diff --git a/core/lib/Drupal/Core/Database/Database.php b/core/lib/Drupal/Core/Database/Database.php new file mode 100644 index 0000000000000000000000000000000000000000..298c0115ab6a7699ba1eb687dc65238cedf71aa3 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Database.php @@ -0,0 +1,431 @@ +<?php + +/** + * @file + * Definition of 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 = NULL; + + /** + * 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 $logging_key + * The logging key to log. + * @param $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 $logging_key + * The logging key to log. + * @param $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 $target + * The database target name. + * @param $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 + // "slave", indicating to use a slave SQL server if one is available. If + // it's not available, then the default/master 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 + * 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 + * The previous database connection key. + */ + final public static function setActiveConnection($key = 'default') { + if (empty(self::$databaseInfo)) { + self::parseConnectionInfo(); + } + + if (!empty(self::$databaseInfo[$key])) { + $old_key = self::$activeKey; + self::$activeKey = $key; + return $old_key; + } + } + + /** + * Process the configuration file for database information. + */ + final public static function parseConnectionInfo() { + global $databases; + + $database_info = is_array($databases) ? $databases : array(); + foreach ($database_info as $index => $info) { + foreach ($database_info[$index] as $target => $value) { + // 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 slave servers. + if (empty($value['driver'])) { + $database_info[$index][$target] = $database_info[$index][$target][mt_rand(0, count($database_info[$index][$target]) - 1)]; + } + + // Parse the prefix information. + if (!isset($database_info[$index][$target]['prefix'])) { + // Default to an empty prefix. + $database_info[$index][$target]['prefix'] = array( + 'default' => '', + ); + } + elseif (!is_array($database_info[$index][$target]['prefix'])) { + // Transform the flat form into an array form. + $database_info[$index][$target]['prefix'] = array( + 'default' => $database_info[$index][$target]['prefix'], + ); + } + } + } + + if (!is_array(self::$databaseInfo)) { + self::$databaseInfo = $database_info; + } + + // Merge the new $database_info into the existing. + // array_merge_recursive() cannot be used, as it would make multiple + // database, user, and password keys in the same database array. + else { + foreach ($database_info as $database_key => $database_values) { + foreach ($database_values as $target => $target_values) { + self::$databaseInfo[$database_key][$target] = $target_values; + } + } + } + } + + /** + * Adds database connection information for a given key/target. + * + * This method allows the addition of new connection credentials 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 $key + * The database key. + * @param $target + * The database target name. + * @param $info + * The database connection information, as it would be defined in + * settings.php. Note that the structure of this array will depend on the + * database driver it is connecting to. + */ + public static function addConnectionInfo($key, $target, $info) { + if (empty(self::$databaseInfo[$key][$target])) { + self::$databaseInfo[$key][$target] = $info; + } + } + + /** + * Gets information on the specified database connection. + * + * @param $connection + * The connection key for which we want information. + */ + final public static function getConnectionInfo($key = 'default') { + if (empty(self::$databaseInfo)) { + self::parseConnectionInfo(); + } + + if (!empty(self::$databaseInfo[$key])) { + return self::$databaseInfo[$key]; + } + } + + /** + * Rename a connection and its corresponding connection information. + * + * @param $old_key + * The old connection key. + * @param $new_key + * The new connection key. + * @return + * TRUE in case of success, FALSE otherwise. + */ + final public static function renameConnection($old_key, $new_key) { + if (empty(self::$databaseInfo)) { + self::parseConnectionInfo(); + } + + 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 $key + * The connection key. + * @return + * TRUE in case of success, FALSE otherwise. + */ + final public static function removeConnection($key) { + if (isset(self::$databaseInfo[$key])) { + unset(self::$databaseInfo[$key]); + unset(self::$connections[$key]); + return TRUE; + } + else { + return FALSE; + } + } + + /** + * Opens a connection to the server specified by the given key and target. + * + * @param $key + * The database connection key, as specified in settings.php. The default is + * "default". + * @param $target + * The database target to open. + * + * @throws Drupal\Core\Database\ConnectionNotDefinedException + * @throws Drupal\Core\Database\DriverNotSpecifiedException + */ + final protected static function openConnection($key, $target) { + if (empty(self::$databaseInfo)) { + self::parseConnectionInfo(); + } + + // 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); + } + + // We cannot rely on the registry yet, because the registry requires an + // open database connection. + $driver_class = "Drupal\\Core\\Database\\Driver\\{$driver}\\Connection"; + $new_connection = new $driver_class(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 $target + * The database target name. Defaults to NULL meaning that all target + * connections will be closed. + * @param $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 the connection, we need to unset the static variable. + if (isset($target)) { + unset(self::$connections[$key][$target]); + } + else { + unset(self::$connections[$key]); + } + } + + /** + * Instructs the system to temporarily ignore a given key/target. + * + * At times we need to temporarily disable slave 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 $key + * The database connection key. + * @param $target + * The target of the specified key to ignore. + */ + public static function ignoreTarget($key, $target) { + self::$ignoreTargets[$key][$target] = TRUE; + } +} diff --git a/core/lib/Drupal/Core/Database/DatabaseException.php b/core/lib/Drupal/Core/Database/DatabaseException.php new file mode 100644 index 0000000000000000000000000000000000000000..019d656166664d5731f735e16f346a8a6c500b64 --- /dev/null +++ b/core/lib/Drupal/Core/Database/DatabaseException.php @@ -0,0 +1,19 @@ +<?php + +/** + * @file + * Definition of 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 { } + diff --git a/core/includes/database/mysql/database.inc b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php similarity index 95% rename from core/includes/database/mysql/database.inc rename to core/lib/Drupal/Core/Database/Driver/mysql/Connection.php index e024a7f396829a5a63e68deeeeae526ecf76725b..3b62fa703a09b99f94f8bf510ebea33cbe9bbfe8 100644 --- a/core/includes/database/mysql/database.inc +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php @@ -2,15 +2,24 @@ /** * @file - * Database interface code for MySQL database servers. + * Definition of Drupal\Core\Database\Driver\mysql\Connection */ +namespace Drupal\Core\Database\Driver\mysql; + +use Drupal\Core\Database\Database; +use Drupal\Core\Database\TransactionCommitFailedException; +use Drupal\Core\Database\Connection as DatabaseConnection; + +use PDO; +use PDOException; + /** * @ingroup database * @{ */ -class DatabaseConnection_mysql extends DatabaseConnection { +class Connection extends DatabaseConnection { /** * Flag to indicate if we have registered the nextID cleanup function. @@ -165,7 +174,7 @@ protected function popCommittableTransactions() { unset($this->transactionLayers[$name]); if (empty($this->transactionLayers)) { if (!PDO::commit()) { - throw new DatabaseTransactionCommitFailedException(); + throw new TransactionCommitFailedException(); } } else { diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Delete.php b/core/lib/Drupal/Core/Database/Driver/mysql/Delete.php new file mode 100644 index 0000000000000000000000000000000000000000..de0db11ae586767b302769eba5969d5094baffb7 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Delete.php @@ -0,0 +1,12 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\Driver\mysql\Delete + */ + +namespace Drupal\Core\Database\Driver\mysql; + +use Drupal\Core\Database\Query\Delete as QueryDelete; + +class Delete extends QueryDelete { } diff --git a/core/includes/database/mysql/query.inc b/core/lib/Drupal/Core/Database/Driver/mysql/Insert.php similarity index 76% rename from core/includes/database/mysql/query.inc rename to core/lib/Drupal/Core/Database/Driver/mysql/Insert.php index 888b6a5a450e613b172814e53b0f53f27e76cf69..233118c5aa28931d3ba351502d492f58b7ac701b 100644 --- a/core/includes/database/mysql/query.inc +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Insert.php @@ -1,17 +1,15 @@ <?php -/** - * @ingroup database - * @{ - */ - /** * @file - * Query code for MySQL embedded database engine. + * Definition of Drupal\Core\Database\Driver\mysql\Insert */ +namespace Drupal\Core\Database\Driver\mysql; + +use Drupal\Core\Database\Query\Insert as QueryInsert; -class InsertQuery_mysql extends InsertQuery { +class Insert extends QueryInsert { public function execute() { if (!$this->preExecute()) { @@ -85,23 +83,3 @@ public function __toString() { return $query; } } - -class TruncateQuery_mysql extends TruncateQuery { - public function __toString() { - // TRUNCATE is actually a DDL statement on MySQL, and DDL statements are - // not transactional, and result in an implicit COMMIT. When we are in a - // transaction, fallback to the slower, but transactional, DELETE. - if ($this->connection->inTransaction()) { - // Create a comment string to prepend to the query. - $comments = $this->connection->makeComment($this->comments); - return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '}'; - } - else { - return parent::__toString(); - } - } -} - -/** - * @} End of "ingroup database". - */ diff --git a/core/includes/database/mysql/install.inc b/core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php similarity index 71% rename from core/includes/database/mysql/install.inc rename to core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php index 75f2ae390504361204acf9bc9c8c4ceb476327b4..4835103de489adca72df34830f525ada49a1da11 100644 --- a/core/includes/database/mysql/install.inc +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php @@ -2,13 +2,17 @@ /** * @file - * Installation code for MySQL embedded database engine. + * Definition of Drupal\Core\Database\Driver\mysql\Install\Tasks */ +namespace Drupal\Core\Database\Driver\mysql\Install; + +use Drupal\Core\Database\Install\Tasks as InstallTasks; + /** * Specifies installation tasks for MySQL and equivalent databases. */ -class DatabaseTasks_mysql extends DatabaseTasks { +class Tasks extends InstallTasks { /** * The PDO driver name for MySQL and equivalent databases. * @@ -30,4 +34,3 @@ public function minimumVersion() { return '5.0.15'; } } - diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Merge.php b/core/lib/Drupal/Core/Database/Driver/mysql/Merge.php new file mode 100644 index 0000000000000000000000000000000000000000..c48b830a566592a6bc4f5e2898a13d18630d5616 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Merge.php @@ -0,0 +1,12 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\Driver\mysql\Merge + */ + +namespace Drupal\Core\Database\Driver\mysql; + +use Drupal\Core\Database\Query\Merge as QueryMerge; + +class Merge extends QueryMerge { } diff --git a/core/includes/database/mysql/schema.inc b/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php similarity index 85% rename from core/includes/database/mysql/schema.inc rename to core/lib/Drupal/Core/Database/Driver/mysql/Schema.php index 4e88fa169ebeb6dca90fdf66a55f6ffe10f3bc06..24c3b4a6b11ac9125df1e8c1dab57df603773b41 100644 --- a/core/includes/database/mysql/schema.inc +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php @@ -2,16 +2,25 @@ /** * @file - * Database schema code for MySQL database servers. + * Definition of 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 Exception; /** * @ingroup schemaapi * @{ */ -class DatabaseSchema_mysql extends DatabaseSchema { +class Schema extends DatabaseSchema { /** * Maximum length of a table comment in MySQL. @@ -59,7 +68,7 @@ protected function buildTableNameCondition($table_name, $operator = '=', $add_pr $table_info = $this->getPrefixInfo($table_name, $add_prefix); - $condition = new DatabaseCondition('AND'); + $condition = new Condition('AND'); $condition->condition('table_schema', $table_info['database']); $condition->condition('table_name', $table_info['table'], $operator); return $condition; @@ -296,10 +305,10 @@ protected function createKeysSqlHelper($fields) { public function renameTable($table, $new_name) { if (!$this->tableExists($table)) { - throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot rename %table to %table_new: table %table doesn't exist.", array('%table' => $table, '%table_new' => $new_name))); + throw new 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 DatabaseSchemaObjectExistsException(t("Cannot rename %table to %table_new: table %table_new already exists.", array('%table' => $table, '%table_new' => $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); @@ -317,10 +326,10 @@ public function dropTable($table) { public function addField($table, $field, $spec, $keys_new = array()) { if (!$this->tableExists($table)) { - throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field %table.%field: table doesn't exist.", array('%field' => $field, '%table' => $table))); + throw new SchemaObjectDoesNotExistException(t("Cannot add field %table.%field: table doesn't exist.", array('%field' => $field, '%table' => $table))); } if ($this->fieldExists($table, $field)) { - throw new DatabaseSchemaObjectExistsException(t("Cannot add field %table.%field: field already exists.", array('%field' => $field, '%table' => $table))); + throw new SchemaObjectExistsException(t("Cannot add field %table.%field: field already exists.", array('%field' => $field, '%table' => $table))); } $fixnull = FALSE; @@ -356,7 +365,7 @@ public function dropField($table, $field) { public function fieldSetDefault($table, $field, $default) { if (!$this->fieldExists($table, $field)) { - throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot set default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field))); + throw new SchemaObjectDoesNotExistException(t("Cannot set default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field))); } if (!isset($default)) { @@ -371,7 +380,7 @@ public function fieldSetDefault($table, $field, $default) { public function fieldSetNoDefault($table, $field) { if (!$this->fieldExists($table, $field)) { - throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot remove default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field))); + throw new 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'); @@ -386,10 +395,10 @@ public function indexExists($table, $name) { public function addPrimaryKey($table, $fields) { if (!$this->tableExists($table)) { - throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add primary key to table %table: table doesn't exist.", array('%table' => $table))); + throw new SchemaObjectDoesNotExistException(t("Cannot add primary key to table %table: table doesn't exist.", array('%table' => $table))); } if ($this->indexExists($table, 'PRIMARY')) { - throw new DatabaseSchemaObjectExistsException(t("Cannot add primary key to table %table: primary key already exists.", array('%table' => $table))); + throw new 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) . ')'); @@ -406,10 +415,10 @@ public function dropPrimaryKey($table) { public function addUniqueKey($table, $name, $fields) { if (!$this->tableExists($table)) { - throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add unique key %name to table %table: table doesn't exist.", array('%table' => $table, '%name' => $name))); + throw new 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 DatabaseSchemaObjectExistsException(t("Cannot add unique key %name to table %table: unique key already exists.", array('%table' => $table, '%name' => $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) . ')'); @@ -426,10 +435,10 @@ public function dropUniqueKey($table, $name) { public function addIndex($table, $name, $fields) { if (!$this->tableExists($table)) { - throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add index %name to table %table: table doesn't exist.", array('%table' => $table, '%name' => $name))); + throw new 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 DatabaseSchemaObjectExistsException(t("Cannot add index %name to table %table: index already exists.", array('%table' => $table, '%name' => $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) . ')'); @@ -446,10 +455,10 @@ public function dropIndex($table, $name) { public function changeField($table, $field, $field_new, $spec, $keys_new = array()) { if (!$this->fieldExists($table, $field)) { - throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot change the definition of field %table.%name: field doesn't exist.", array('%table' => $table, '%name' => $field))); + throw new 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 DatabaseSchemaObjectExistsException(t("Cannot rename field %table.%name to %name_new: target field already exists.", array('%table' => $table, '%name' => $field, '%name_new' => $field_new))); + throw new 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)); diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Select.php b/core/lib/Drupal/Core/Database/Driver/mysql/Select.php new file mode 100644 index 0000000000000000000000000000000000000000..aecae55a2fbaa017be7289c1c328af52c1c3a0aa --- /dev/null +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Select.php @@ -0,0 +1,12 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\Driver\mysql\Select + */ + +namespace Drupal\Core\Database\Driver\mysql; + +use Drupal\Core\Database\Query\Select as QuerySelect; + +class Select extends QuerySelect { } diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Transaction.php b/core/lib/Drupal/Core/Database/Driver/mysql/Transaction.php new file mode 100644 index 0000000000000000000000000000000000000000..570a1300f3b0a9ef096124b488eedcdda0e70f9f --- /dev/null +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Transaction.php @@ -0,0 +1,12 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\Driver\mysql\Transaction + */ + +namespace Drupal\Core\Database\Driver\mysql; + +use Drupal\Core\Database\Transaction as DatabaseTransaction; + +class Transaction extends DatabaseTransaction { } diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Truncate.php b/core/lib/Drupal/Core/Database/Driver/mysql/Truncate.php new file mode 100644 index 0000000000000000000000000000000000000000..d0adfad10f2b4f26dcb17fc96ef3d87fe784b3da --- /dev/null +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Truncate.php @@ -0,0 +1,26 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\Driver\mysql\Truncate + */ + +namespace Drupal\Core\Database\Driver\mysql; + +use Drupal\Core\Database\Query\Truncate as QueryTruncate; + +class Truncate extends QueryTruncate { + public function __toString() { + // TRUNCATE is actually a DDL statement on MySQL, and DDL statements are + // not transactional, and result in an implicit COMMIT. When we are in a + // transaction, fallback to the slower, but transactional, DELETE. + if ($this->connection->inTransaction()) { + // Create a comment string to prepend to the query. + $comments = $this->connection->makeComment($this->comments); + return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '}'; + } + else { + return parent::__toString(); + } + } +} diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Update.php b/core/lib/Drupal/Core/Database/Driver/mysql/Update.php new file mode 100644 index 0000000000000000000000000000000000000000..5b3bda68b74e3d46c5050d4c2e4412b5a2e037ae --- /dev/null +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Update.php @@ -0,0 +1,12 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\Driver\mysql\Update + */ + +namespace Drupal\Core\Database\Driver\mysql; + +use Drupal\Core\Database\Query\Update as QueryUpdate; + +class Update extends QueryUpdate { } diff --git a/core/includes/database/pgsql/database.inc b/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php similarity index 89% rename from core/includes/database/pgsql/database.inc rename to core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php index 68300e55368e4bfd742e397f8fa3311df4bc19fb..6903a141991d02d143e89f6589a60c73ed8ea731 100644 --- a/core/includes/database/pgsql/database.inc +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php @@ -2,20 +2,29 @@ /** * @file - * Database interface code for PostgreSQL database servers. + * Definition of 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\StatementInterface; + +use PDO; +use PDOException; + /** * @ingroup database * @{ */ -/** - * The name by which to obtain a lock for retrive the next insert id. - */ -const POSTGRESQL_NEXTID_LOCK = 1000; +class Connection extends DatabaseConnection { -class DatabaseConnection_pgsql extends DatabaseConnection { + /** + * The name by which to obtain a lock for retrive the next insert id. + */ + const POSTGRESQL_NEXTID_LOCK = 1000; public function __construct(array $connection_options = array()) { // This driver defaults to transaction support, except if explicitly passed FALSE. @@ -92,7 +101,7 @@ public function query($query, array $args = array(), $options = array()) { } try { - if ($query instanceof DatabaseStatementInterface) { + if ($query instanceof StatementInterface) { $stmt = $query; $stmt->execute(NULL, $options); } @@ -118,7 +127,7 @@ public function query($query, array $args = array(), $options = array()) { catch (PDOException $e) { if ($options['throw_exception']) { // Add additional debug information. - if ($query instanceof DatabaseStatementInterface) { + if ($query instanceof StatementInterface) { $e->query_string = $stmt->getQueryString(); } else { @@ -187,13 +196,13 @@ public function nextId($existing = 0) { // PostgreSQL advisory locks are simply locks to be used by an // application such as Drupal. This will prevent other Drupal proccesses // from altering the sequence while we are. - $this->query("SELECT pg_advisory_lock(" . POSTGRESQL_NEXTID_LOCK . ")"); + $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(" . POSTGRESQL_NEXTID_LOCK . ")"); + $this->query("SELECT pg_advisory_unlock(" . self::POSTGRESQL_NEXTID_LOCK . ")"); return $id; } @@ -203,7 +212,7 @@ public function nextId($existing = 0) { // Retrive 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(" . POSTGRESQL_NEXTID_LOCK . ")"); + $this->query("SELECT pg_advisory_unlock(" . self::POSTGRESQL_NEXTID_LOCK . ")"); return $id; } diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Delete.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Delete.php new file mode 100644 index 0000000000000000000000000000000000000000..e9914ca97d9b94a6105368bb13f48f709d4838f0 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Delete.php @@ -0,0 +1,12 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\Driver\pgsql\Delete + */ + +namespace Drupal\Core\Database\Driver\pgsql; + +use Drupal\Core\Database\Query\Delete as QueryDelete; + +class Delete extends QueryDelete { } diff --git a/core/includes/database/pgsql/query.inc b/core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php similarity index 71% rename from core/includes/database/pgsql/query.inc rename to core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php index f3783a9ca8f9ceb4f745c650fa826ee3271e1df4..dffa1fdcbacd8d24a0039adca0372d7b2a092b86 100644 --- a/core/includes/database/pgsql/query.inc +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php @@ -1,17 +1,23 @@ <?php /** - * @ingroup database - * @{ + * @file + * Definition of 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; + +use PDO; + /** - * @file - * Query code for PostgreSQL embedded database engine. + * @ingroup database + * @{ */ - -class InsertQuery_pgsql extends InsertQuery { +class Insert extends QueryInsert { public function execute() { if (!$this->preExecute()) { @@ -146,64 +152,3 @@ public function __toString() { return $query; } } - -class UpdateQuery_pgsql extends UpdateQuery { - 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; - $expression_fields = array(); - 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]); - } - } - 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; - $this->connection->query($stmt, $options); - - return $stmt->rowCount(); - } -} diff --git a/core/includes/database/pgsql/install.inc b/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php similarity index 95% rename from core/includes/database/pgsql/install.inc rename to core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php index c350634ec40058a19c7ae470f2d5c8a03bc71c4c..c3ddffb4b78a31e5cec9c6a997d675ae8ccd20d5 100644 --- a/core/includes/database/pgsql/install.inc +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php @@ -2,13 +2,17 @@ /** * @file - * Install functions for PostgreSQL embedded database engine. + * Definition of Drupal\Core\Database\Driver\pgsql\Install\Tasks */ +namespace Drupal\Core\Database\Driver\pgsql\Install; -// PostgreSQL specific install functions +use Drupal\Core\Database\Install\Tasks as InstallTasks; -class DatabaseTasks_pgsql extends DatabaseTasks { +/** + * PostgreSQL specific install functions + */ +class Tasks extends InstallTasks { protected $pdoDriver = 'pgsql'; public function __construct() { diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Merge.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Merge.php new file mode 100644 index 0000000000000000000000000000000000000000..c3a2ae0938b837400df9f3e123ea9aa5a12f3593 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Merge.php @@ -0,0 +1,12 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\Driver\pgsql\Merge + */ + +namespace Drupal\Core\Database\Driver\pgsql; + +use Drupal\Core\Database\Query\Merge as QueryMerge; + +class Merge extends QueryMerge { } diff --git a/core/includes/database/pgsql/schema.inc b/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php similarity index 88% rename from core/includes/database/pgsql/schema.inc rename to core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php index 49adbf9077627e33fa354b7521cd642c4db9c8ef..7206e178dba0a37da6b061eb94064c6e84d79484 100644 --- a/core/includes/database/pgsql/schema.inc +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php @@ -2,15 +2,25 @@ /** * @file - * Database schema code for PostgreSQL database servers. + * Definition of Drupal\Core\Database\Driver\pgsql\Schema */ +namespace Drupal\Core\Database\Driver\pgsql; + +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 Exception; + /** * @ingroup schemaapi * @{ */ -class DatabaseSchema_pgsql extends DatabaseSchema { +class Schema extends DatabaseSchema { /** * A cache of information about blob columns and sequences of tables. @@ -314,10 +324,10 @@ protected function _createKeySql($fields) { function renameTable($table, $new_name) { if (!$this->tableExists($table)) { - throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot rename %table to %table_new: table %table doesn't exist.", array('%table' => $table, '%table_new' => $new_name))); + throw new 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 DatabaseSchemaObjectExistsException(t("Cannot rename %table to %table_new: table %table_new already exists.", array('%table' => $table, '%table_new' => $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. @@ -351,10 +361,10 @@ public function dropTable($table) { public function addField($table, $field, $spec, $new_keys = array()) { if (!$this->tableExists($table)) { - throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field %table.%field: table doesn't exist.", array('%field' => $field, '%table' => $table))); + throw new SchemaObjectDoesNotExistException(t("Cannot add field %table.%field: table doesn't exist.", array('%field' => $field, '%table' => $table))); } if ($this->fieldExists($table, $field)) { - throw new DatabaseSchemaObjectExistsException(t("Cannot add field %table.%field: field already exists.", array('%field' => $field, '%table' => $table))); + throw new SchemaObjectExistsException(t("Cannot add field %table.%field: field already exists.", array('%field' => $field, '%table' => $table))); } $fixnull = FALSE; @@ -393,7 +403,7 @@ public function dropField($table, $field) { public function fieldSetDefault($table, $field, $default) { if (!$this->fieldExists($table, $field)) { - throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot set default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field))); + throw new SchemaObjectDoesNotExistException(t("Cannot set default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field))); } if (!isset($default)) { @@ -408,7 +418,7 @@ public function fieldSetDefault($table, $field, $default) { public function fieldSetNoDefault($table, $field) { if (!$this->fieldExists($table, $field)) { - throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot remove default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field))); + throw new 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'); @@ -435,10 +445,10 @@ protected function constraintExists($table, $name) { public function addPrimaryKey($table, $fields) { if (!$this->tableExists($table)) { - throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add primary key to table %table: table doesn't exist.", array('%table' => $table))); + throw new SchemaObjectDoesNotExistException(t("Cannot add primary key to table %table: table doesn't exist.", array('%table' => $table))); } if ($this->constraintExists($table, 'pkey')) { - throw new DatabaseSchemaObjectExistsException(t("Cannot add primary key to table %table: primary key already exists.", array('%table' => $table))); + throw new 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 (' . implode(',', $fields) . ')'); @@ -455,10 +465,10 @@ public function dropPrimaryKey($table) { function addUniqueKey($table, $name, $fields) { if (!$this->tableExists($table)) { - throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add unique key %name to table %table: table doesn't exist.", array('%table' => $table, '%name' => $name))); + throw new 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 DatabaseSchemaObjectExistsException(t("Cannot add unique key %name to table %table: unique key already exists.", array('%table' => $table, '%name' => $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 CONSTRAINT "' . $this->prefixNonTable($table, $name, 'key') . '" UNIQUE (' . implode(',', $fields) . ')'); @@ -475,10 +485,10 @@ public function dropUniqueKey($table, $name) { public function addIndex($table, $name, $fields) { if (!$this->tableExists($table)) { - throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add index %name to table %table: table doesn't exist.", array('%table' => $table, '%name' => $name))); + throw new 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 DatabaseSchemaObjectExistsException(t("Cannot add index %name to table %table: index already exists.", array('%table' => $table, '%name' => $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)); @@ -495,10 +505,10 @@ public function dropIndex($table, $name) { public function changeField($table, $field, $field_new, $spec, $new_keys = array()) { if (!$this->fieldExists($table, $field)) { - throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot change the definition of field %table.%name: field doesn't exist.", array('%table' => $table, '%name' => $field))); + throw new 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 DatabaseSchemaObjectExistsException(t("Cannot rename field %table.%name to %name_new: target field already exists.", array('%table' => $table, '%name' => $field, '%name_new' => $field_new))); + throw new 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); @@ -615,3 +625,7 @@ public function getComment($table, $column = NULL) { } } } + +/** + * @} End of "ingroup database". + */ diff --git a/core/includes/database/pgsql/select.inc b/core/lib/Drupal/Core/Database/Driver/pgsql/Select.php similarity index 94% rename from core/includes/database/pgsql/select.inc rename to core/lib/Drupal/Core/Database/Driver/pgsql/Select.php index d1d83828118d9bbbd3de7cfedd506a8b862fedd8..a9226b26dbcfb7416546685c221c402e9a297a39 100644 --- a/core/includes/database/pgsql/select.inc +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Select.php @@ -2,15 +2,19 @@ /** * @file - * Select builder for PostgreSQL database engine. + * Definition of Drupal\Core\Database\Driver\pgsql\Select */ +namespace Drupal\Core\Database\Driver\pgsql; + +use Drupal\Core\Database\Query\Select as QuerySelect; + /** * @ingroup database * @{ */ -class SelectQuery_pgsql extends SelectQuery { +class Select extends QuerySelect { public function orderRandom() { $alias = $this->addExpression('RANDOM()', 'random_field'); @@ -105,4 +109,3 @@ public function orderBy($field, $direction = 'ASC') { /** * @} End of "ingroup database". */ - diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Transaction.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Transaction.php new file mode 100644 index 0000000000000000000000000000000000000000..028672243569869a610df096e9d0a7b6da1a1f15 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Transaction.php @@ -0,0 +1,12 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\Driver\pgsql\Transaction + */ + +namespace Drupal\Core\Database\Driver\mysql; + +use Drupal\Core\Database\Transaction as DatabaseTransaction; + +class Transaction extends DatabaseTransaction { } diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Truncate.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Truncate.php new file mode 100644 index 0000000000000000000000000000000000000000..e2fa83dac7c40f2ec6de932847de8301e78ab3fd --- /dev/null +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Truncate.php @@ -0,0 +1,12 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\Driver\pgsql\Truncate + */ + +namespace Drupal\Core\Database\Driver\pgsql; + +use Drupal\Core\Database\Query\Truncate as QueryTruncate; + +class Truncate extends QueryTruncate { } diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Update.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Update.php new file mode 100644 index 0000000000000000000000000000000000000000..de738bfda996cf0a1db53f8e2e99d7e8488ecdc1 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Update.php @@ -0,0 +1,75 @@ +<?php + +/** + * @file + * Definition of 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 PDO; + +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; + $expression_fields = array(); + 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]); + } + } + 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; + $this->connection->query($stmt, $options); + + return $stmt->rowCount(); + } +} diff --git a/core/includes/database/sqlite/database.inc b/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php similarity index 67% rename from core/includes/database/sqlite/database.inc rename to core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php index 5de219e8d79841d5a359316542645d5f0fffa28a..1587a632dd0812e27e694a503ae9956ffaff3cd4 100644 --- a/core/includes/database/sqlite/database.inc +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php @@ -2,20 +2,25 @@ /** * @file - * Database interface code for SQLite embedded database engine. + * Definition of Drupal\Core\Database\Driver\sqlite\Connection */ -/** - * @ingroup database - * @{ - */ +namespace Drupal\Core\Database\Driver\sqlite; -include_once DRUPAL_ROOT . '/core/includes/database/prefetch.inc'; +use Drupal\Core\Database\Database; +use Drupal\Core\Database\TransactionNoActiveException; +use Drupal\Core\Database\TransactionNameNonUniqueException; +use Drupal\Core\Database\TransactionCommitFailedException; +use Drupal\Core\Database\Driver\sqlite\Statement; +use Drupal\Core\Database\Connection as DatabaseConnection; + +use PDO; +use Exception; /** * Specific SQLite implementation of DatabaseConnection. */ -class DatabaseConnection_sqlite extends DatabaseConnection { +class Connection extends DatabaseConnection { /** * Whether this database connection supports savepoints. @@ -37,7 +42,7 @@ class DatabaseConnection_sqlite extends DatabaseConnection { /** * All databases attached to the current database. This is used to allow * prefixes to be safely handled without locking the table - * + * * @var array */ protected $attachedDatabases = array(); @@ -46,10 +51,10 @@ class DatabaseConnection_sqlite extends DatabaseConnection { * Whether or not a table has been dropped this request: the destructor will * only try to get rid of unnecessary databases if there is potential of them * being empty. - * - * This variable is set to public because DatabaseSchema_sqlite needs to + * + * This variable is set to public because Schema needs to * access it. However, it should not be manually set. - * + * * @var boolean */ var $tableDropped = FALSE; @@ -217,11 +222,11 @@ public function sqlFunctionRand($seed = NULL) { * SQLite-specific implementation of DatabaseConnection::prepare(). * * We don't use prepared statements at all at this stage. We just create - * a DatabaseStatement_sqlite object, that will create a PDOStatement + * a Statement object, that will create a PDOStatement * using the semi-private PDOPrepare() method below. */ public function prepare($query, $options = array()) { - return new DatabaseStatement_sqlite($this, $query, $options); + return new Statement($this, $query, $options); } /** @@ -232,9 +237,9 @@ public function prepare($query, $options = array()) { * destructor is called and SQLite does not allow data change (INSERT, * UPDATE etc) on a table which has open SELECT statements, you should never * call this function and keep a PDOStatement object alive as that can lead - * to a deadlock. This really, really should be private, but as - * DatabaseStatement_sqlite needs to call it, we have no other choice but to - * expose this function to the world. + * to a deadlock. This really, really should be private, but as Statement + * needs to call it, we have no other choice but to expose this function to + * the world. */ public function PDOPrepare($query, array $options = array()) { return parent::prepare($query, $options); @@ -304,7 +309,7 @@ public function rollback($savepoint_name = 'drupal_transaction') { } if (!$this->inTransaction()) { - throw new DatabaseTransactionNoActiveException(); + throw new TransactionNoActiveException(); } // A previous rollback to an earlier savepoint may mean that the savepoint // in question has already been rolled back. @@ -340,7 +345,7 @@ public function pushTransaction($name) { return; } if (isset($this->transactionLayers[$name])) { - throw new DatabaseTransactionNameNonUniqueException($name . " is already in use."); + throw new TransactionNameNonUniqueException($name . " is already in use."); } if (!$this->inTransaction()) { PDO::beginTransaction(); @@ -356,7 +361,7 @@ public function popTransaction($name) { return; } if (!$this->inTransaction()) { - throw new DatabaseTransactionNoActiveException(); + throw new TransactionNoActiveException(); } // Commit everything since SAVEPOINT $name. @@ -371,7 +376,7 @@ public function popTransaction($name) { PDO::rollBack(); } elseif (!PDO::commit()) { - throw new DatabaseTransactionCommitFailedException(); + throw new TransactionCommitFailedException(); } } else { @@ -381,141 +386,3 @@ public function popTransaction($name) { } } - -/** - * Specific SQLite implementation of DatabaseConnection. - * - * See DatabaseConnection_sqlite::PDOPrepare() for reasons why we must prefetch - * the data instead of using PDOStatement. - * - * @see DatabaseConnection_sqlite::PDOPrepare() - */ -class DatabaseStatement_sqlite extends DatabaseStatementPrefetch implements Iterator, DatabaseStatementInterface { - - /** - * 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->dbh->PDOPrepare($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; - } -} - -/** - * @} End of "ingroup database". - */ diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Delete.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Delete.php new file mode 100644 index 0000000000000000000000000000000000000000..c0056cbe3816a78843c491c1ac04cabf0796566e --- /dev/null +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Delete.php @@ -0,0 +1,33 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\Driver\sqlite\Delete + */ + +namespace Drupal\Core\Database\Driver\sqlite; + +use Drupal\Core\Database\Query\Delete as QueryDelete; + +/** + * SQLite specific implementation of DeleteQuery. + * + * When the WHERE is omitted from a DELETE statement and the table being deleted + * has no triggers, SQLite uses an optimization to erase the entire table content + * without having to visit each row of the table individually. + * + * Prior to SQLite 3.6.5, SQLite does not return the actual number of rows deleted + * by that optimized "truncate" optimization. + */ +class Delete extends QueryDelete { + public function execute() { + if (!count($this->condition)) { + $total_rows = $this->connection->query('SELECT COUNT(*) FROM {' . $this->connection->escapeTable($this->table) . '}')->fetchField(); + parent::execute(); + return $total_rows; + } + else { + return parent::execute(); + } + } +} diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Insert.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Insert.php new file mode 100644 index 0000000000000000000000000000000000000000..e584943430bcd65e70dafb954f4322781869c058 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Insert.php @@ -0,0 +1,49 @@ +<?php + +/** + * @file + * Definition of 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)) { + 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_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)) { + return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $this->insertFields) . ') ' . $this->fromQuery; + } + + return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $this->insertFields) . ') VALUES (' . implode(', ', $placeholders) . ')'; + } + +} \ No newline at end of file diff --git a/core/includes/database/sqlite/install.inc b/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php similarity index 87% rename from core/includes/database/sqlite/install.inc rename to core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php index 62cbac381f17c5cad67688540e0ff9a58905caf2..8866ef22a53d1c38ce6b0c7241b0784e1d69aae4 100644 --- a/core/includes/database/sqlite/install.inc +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php @@ -2,10 +2,16 @@ /** * @file - * SQLite specific install functions + * Definition of Drupal\Core\Database\Driver\sqlite\Install\Tasks */ -class DatabaseTasks_sqlite extends DatabaseTasks { +namespace Drupal\Core\Database\Driver\sqlite\Install; + +use Drupal\Core\Database\Install\Tasks as InstallTasks; + +use SplFileInfo; + +class Tasks extends InstallTasks { protected $pdoDriver = 'sqlite'; public function name() { diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Merge.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Merge.php new file mode 100644 index 0000000000000000000000000000000000000000..3bbc3de8c176ac1d11dd43dfcfccb3f10a9aa266 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Merge.php @@ -0,0 +1,12 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\Driver\sqlite\Merge + */ + +namespace Drupal\Core\Database\Driver\sqlite; + +use Drupal\Core\Database\Query\Merge as QueryMerge; + +class Merge extends QueryMerge { } diff --git a/core/includes/database/sqlite/schema.inc b/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php similarity index 88% rename from core/includes/database/sqlite/schema.inc rename to core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php index c5882f12715d9c31933961c94b4f009ed773cccd..823cd137a166ef298786a083b817db8fd3290795 100644 --- a/core/includes/database/sqlite/schema.inc +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php @@ -2,16 +2,24 @@ /** * @file - * Database schema code for SQLite databases. + * Definition of Drupal\Core\Database\Driver\sqlite\Schema */ +namespace Drupal\Core\Database\Driver\sqlite; + +use Drupal\Core\Database\SchemaObjectExistsException; +use Drupal\Core\Database\SchemaObjectDoesNotExistException; +use Drupal\Core\Database\Schema as DatabaseSchema; + +use Exception; + /** * @ingroup schemaapi * @{ */ -class DatabaseSchema_sqlite extends DatabaseSchema { +class Schema extends DatabaseSchema { /** * Override DatabaseSchema::$defaultSchema @@ -232,10 +240,10 @@ public function getFieldTypeMap() { public function renameTable($table, $new_name) { if (!$this->tableExists($table)) { - throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot rename %table to %table_new: table %table doesn't exist.", array('%table' => $table, '%table_new' => $new_name))); + throw new 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 DatabaseSchemaObjectExistsException(t("Cannot rename %table to %table_new: table %table_new already exists.", array('%table' => $table, '%table_new' => $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); @@ -278,10 +286,10 @@ public function dropTable($table) { public function addField($table, $field, $specification, $keys_new = array()) { if (!$this->tableExists($table)) { - throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field %table.%field: table doesn't exist.", array('%field' => $field, '%table' => $table))); + throw new SchemaObjectDoesNotExistException(t("Cannot add field %table.%field: table doesn't exist.", array('%field' => $field, '%table' => $table))); } if ($this->fieldExists($table, $field)) { - throw new DatabaseSchemaObjectExistsException(t("Cannot add field %table.%field: field already exists.", array('%field' => $field, '%table' => $table))); + throw new 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 @@ -494,10 +502,10 @@ public function dropField($table, $field) { public function changeField($table, $field, $field_new, $spec, $keys_new = array()) { if (!$this->fieldExists($table, $field)) { - throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot change the definition of field %table.%name: field doesn't exist.", array('%table' => $table, '%name' => $field))); + throw new 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 DatabaseSchemaObjectExistsException(t("Cannot rename field %table.%name to %name_new: target field already exists.", array('%table' => $table, '%name' => $field, '%name_new' => $field_new))); + throw new 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); @@ -559,10 +567,10 @@ protected function mapKeyDefinition(array $key_definition, array $mapping) { public function addIndex($table, $name, $fields) { if (!$this->tableExists($table)) { - throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add index %name to table %table: table doesn't exist.", array('%table' => $table, '%name' => $name))); + throw new 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 DatabaseSchemaObjectExistsException(t("Cannot add index %name to table %table: index already exists.", array('%table' => $table, '%name' => $name))); + throw new SchemaObjectExistsException(t("Cannot add index %name to table %table: index already exists.", array('%table' => $table, '%name' => $name))); } $schema['indexes'][$name] = $fields; @@ -591,10 +599,10 @@ public function dropIndex($table, $name) { public function addUniqueKey($table, $name, $fields) { if (!$this->tableExists($table)) { - throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add unique key %name to table %table: table doesn't exist.", array('%table' => $table, '%name' => $name))); + throw new 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 DatabaseSchemaObjectExistsException(t("Cannot add unique key %name to table %table: unique key already exists.", array('%table' => $table, '%name' => $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; @@ -617,14 +625,14 @@ public function dropUniqueKey($table, $name) { public function addPrimaryKey($table, $fields) { if (!$this->tableExists($table)) { - throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add primary key to table %table: table doesn't exist.", array('%table' => $table))); + throw new 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 DatabaseSchemaObjectExistsException(t("Cannot add primary key to table %table: primary key already exists.", array('%table' => $table))); + throw new SchemaObjectExistsException(t("Cannot add primary key to table %table: primary key already exists.", array('%table' => $table))); } $new_schema['primary key'] = $fields; @@ -646,7 +654,7 @@ public function dropPrimaryKey($table) { public function fieldSetDefault($table, $field, $default) { if (!$this->fieldExists($table, $field)) { - throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot set default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field))); + throw new SchemaObjectDoesNotExistException(t("Cannot set default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field))); } $old_schema = $this->introspectSchema($table); @@ -658,7 +666,7 @@ public function fieldSetDefault($table, $field, $default) { public function fieldSetNoDefault($table, $field) { if (!$this->fieldExists($table, $field)) { - throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot remove default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field))); + throw new SchemaObjectDoesNotExistException(t("Cannot remove default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field))); } $old_schema = $this->introspectSchema($table); @@ -680,4 +688,4 @@ public function findTables($table_expression) { )); return $result->fetchAllKeyed(0, 0); } -} +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Select.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Select.php new file mode 100644 index 0000000000000000000000000000000000000000..5403611003c94908fd8d866717b0caea9e47ff59 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Select.php @@ -0,0 +1,17 @@ +<?php + +/** + * @file + * Definition of 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; + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Statement.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Statement.php new file mode 100644 index 0000000000000000000000000000000000000000..531b6d08b249732fb0b40db064198b5a22a27e54 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Statement.php @@ -0,0 +1,148 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\Driver\sqlite\Statement + */ + +namespace Drupal\Core\Database\Driver\sqlite; + +use Drupal\Core\Database\StatementPrefetch; +use Drupal\Core\Database\StatementInterface; + +use Iterator; +use PDOException; + +/** + * Specific SQLite implementation of DatabaseConnection. + * + * See DatabaseConnection_sqlite::PDOPrepare() for reasons why we must prefetch + * the data instead of using PDOStatement. + * + * @see DatabaseConnection_sqlite::PDOPrepare() + */ +class Statement extends StatementPrefetch implements Iterator, 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->dbh->PDOPrepare($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; + } +} diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Transaction.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Transaction.php new file mode 100644 index 0000000000000000000000000000000000000000..ae73ed8c98799984a4878bd53d56ef770f17fcda --- /dev/null +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Transaction.php @@ -0,0 +1,12 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\Driver\sqlite\Transaction + */ + +namespace Drupal\Core\Database\Driver\sqlite; + +use Drupal\Core\Database\Transaction as DatabaseTransaction; + +class Transaction extends DatabaseTransaction { } diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Truncate.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Truncate.php new file mode 100644 index 0000000000000000000000000000000000000000..bc635404447f94f1e880f09265e27797f35f05ff --- /dev/null +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Truncate.php @@ -0,0 +1,25 @@ +<?php + +/** + * @file + * Definition of 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) . '} '; + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Update.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Update.php new file mode 100644 index 0000000000000000000000000000000000000000..18332e7d5578eddf430da2a57604d34583c050e0 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Update.php @@ -0,0 +1,80 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\Driver\sqlite\Update + */ + +namespace Drupal\Core\Database\Driver\sqlite; + +use Drupal\Core\Database\Query\Condition; +use Drupal\Core\Database\Query\ConditionInterface; +use Drupal\Core\Database\Query\Update as QueryUpdate; + +/** + * SQLite specific implementation of UpdateQuery. + * + * SQLite counts all the rows that match the conditions as modified, even if they + * will not be affected by the query. We workaround this by ensuring that + * we don't select those rows. + * + * A query like this one: + * UPDATE test SET name = 'newname' WHERE tid = 1 + * will become: + * UPDATE test SET name = 'newname' WHERE tid = 1 AND name <> 'newname' + */ +class Update extends QueryUpdate { + /** + * Helper function that removes the fields that are already in a condition. + * + * @param $fields + * The fields. + * @param QueryConditionInterface $condition + * A database condition. + */ + protected function removeFieldsInCondition(&$fields, ConditionInterface $condition) { + foreach ($condition->conditions() as $child_condition) { + if ($child_condition['field'] instanceof ConditionInterface) { + $this->removeFieldsInCondition($fields, $child_condition['field']); + } + else { + unset($fields[$child_condition['field']]); + } + } + } + + public function execute() { + if (!empty($this->queryOptions['sqlite_return_matched_rows'])) { + return parent::execute(); + } + + // Get the fields used in the update query, and remove those that are already + // in the condition. + $fields = $this->expressionFields + $this->fields; + $this->removeFieldsInCondition($fields, $this->condition); + + // Add the inverse of the fields to the condition. + $condition = new Condition('OR'); + foreach ($fields as $field => $data) { + if (is_array($data)) { + // The field is an expression. + $condition->where($field . ' <> ' . $data['expression']); + $condition->isNull($field); + } + elseif (!isset($data)) { + // The field will be set to NULL. + $condition->isNotNull($field); + } + else { + $condition->condition($field, $data, '<>'); + $condition->isNull($field); + } + } + if (count($condition)) { + $condition->compile($this->connection, $this); + $this->condition->where((string) $condition, $condition->arguments()); + } + return parent::execute(); + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Database/DriverNotSpecifiedException.php b/core/lib/Drupal/Core/Database/DriverNotSpecifiedException.php new file mode 100644 index 0000000000000000000000000000000000000000..946393142cc09e2c06ff2c95b466474728905f4e --- /dev/null +++ b/core/lib/Drupal/Core/Database/DriverNotSpecifiedException.php @@ -0,0 +1,15 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\DriverNotSpecifiedException + */ + +namespace Drupal\Core\Database; + +use RuntimeException; + +/** + * Exception thrown if no driver is specified for a database connection. + */ +class DriverNotSpecifiedException extends RuntimeException {} diff --git a/core/lib/Drupal/Core/Database/Install/TaskException.php b/core/lib/Drupal/Core/Database/Install/TaskException.php new file mode 100644 index 0000000000000000000000000000000000000000..d93b736c00210ea914cbdc2489cfdef0fba28e16 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Install/TaskException.php @@ -0,0 +1,15 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\Install\TaskException + */ + +namespace Drupal\Core\Database\Install; + +use RuntimeException; + +/** + * Exception thrown if the database installer fails. + */ +class TaskException extends RuntimeException { } diff --git a/core/lib/Drupal/Core/Database/Install/Tasks.php b/core/lib/Drupal/Core/Database/Install/Tasks.php new file mode 100644 index 0000000000000000000000000000000000000000..ece3c7c251931d458802fbd340da2275fb1ce837 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Install/Tasks.php @@ -0,0 +1,305 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\Install\Tasks + */ + +namespace Drupal\Core\Database\Install; + +use Drupal\Core\Database\Database; + +use PDO; + +/** + * Database installer structure. + * + * Defines basic Drupal requirements for databases. + */ +abstract class Tasks { + + /** + * 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(st("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 .= '<p class="error">' . $result . '</p>'; + } + } + if (!empty($message)) { + $message = '<p>In order for Drupal to work, and to continue with the installation process, you must resolve all issues reported below. For more help with configuring your database server, see the <a href="http://drupal.org/getting-started/install">installation handbook</a>. If you are unsure what any of this means you should probably contact your hosting provider.</p>' . $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(st('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 { + db_query($query); + $this->pass(st($pass)); + } + catch (Exception $e) { + $this->fail(st($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(st("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($database) { + $form['database'] = array( + '#type' => 'textfield', + '#title' => st('Database name'), + '#default_value' => empty($database['database']) ? '' : $database['database'], + '#size' => 45, + '#required' => TRUE, + '#description' => st('The name of the database your @drupal data will be stored in. It must exist on your server before @drupal can be installed.', array('@drupal' => drupal_install_profile_distribution_name())), + ); + + $form['username'] = array( + '#type' => 'textfield', + '#title' => st('Database username'), + '#default_value' => empty($database['username']) ? '' : $database['username'], + '#required' => TRUE, + '#size' => 45, + ); + + $form['password'] = array( + '#type' => 'password', + '#title' => st('Database password'), + '#default_value' => empty($database['password']) ? '' : $database['password'], + '#required' => FALSE, + '#size' => 45, + ); + + $form['advanced_options'] = array( + '#type' => 'fieldset', + '#title' => st('Advanced options'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#description' => st("These options are only necessary for some sites. If you're not sure what you should enter here, leave the default settings or check with your hosting provider."), + '#weight' => 10, + ); + + $profile = drupal_get_profile(); + $db_prefix = ($profile == 'standard') ? 'drupal_' : $profile . '_'; + $form['advanced_options']['db_prefix'] = array( + '#type' => 'textfield', + '#title' => st('Table prefix'), + '#default_value' => '', + '#size' => 45, + '#description' => st('If more than one application will be sharing this database, enter a table prefix such as %prefix for your @drupal site here.', array('@drupal' => drupal_install_profile_distribution_name(), '%prefix' => $db_prefix)), + '#weight' => 10, + ); + + $form['advanced_options']['host'] = array( + '#type' => 'textfield', + '#title' => st('Database host'), + '#default_value' => empty($database['host']) ? 'localhost' : $database['host'], + '#size' => 45, + // Hostnames can be 255 characters long. + '#maxlength' => 255, + '#required' => TRUE, + '#description' => st('If your database is located on a different server, change this.'), + ); + + $form['advanced_options']['port'] = array( + '#type' => 'textfield', + '#title' => st('Database port'), + '#default_value' => empty($database['port']) ? '' : $database['port'], + '#size' => 45, + // The maximum port number is 65536, 5 digits. + '#maxlength' => 5, + '#description' => st('If your database server is listening to a non-standard port, enter its number.'), + ); + + 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'] . '][advanced_options][db_prefix'] = st('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'])); + } + + // Verify the database port. + if (!empty($database['port']) && !is_numeric($database['port'])) { + $errors[$database['driver'] . '][advanced_options][port'] = st('Database port must be a number.'); + } + + return $errors; + } + +} diff --git a/core/includes/database/log.inc b/core/lib/Drupal/Core/Database/Log.php similarity index 80% rename from core/includes/database/log.inc rename to core/lib/Drupal/Core/Database/Log.php index ec27ef8e6332ad6543ce7b489b0048b4aa3a8034..5d5d560c152157491cdd3868f75413422b3dedc1 100644 --- a/core/includes/database/log.inc +++ b/core/lib/Drupal/Core/Database/Log.php @@ -2,9 +2,11 @@ /** * @file - * Logging classes for the database layer. + * Definition of Drupal\Core\Database\Log */ +namespace Drupal\Core\Database; + /** * Database query logger. * @@ -16,7 +18,7 @@ * Every connection has one and only one logging object on it for all targets * and logging keys. */ -class DatabaseLog { +class Log { /** * Cache of logged queries. This will only be used if the query logger is enabled. @@ -112,7 +114,7 @@ public function end($logging_key) { * @param $time * The time in milliseconds the query took to execute. */ - public function log(DatabaseStatementInterface $statement, $args, $time) { + public function log(StatementInterface $statement, $args, $time) { foreach (array_keys($this->queryLog) as $key) { $this->queryLog[$key][] = array( 'query' => $statement->getQueryString(), @@ -128,9 +130,12 @@ public function log(DatabaseStatementInterface $statement, $args, $time) { * Determine the routine that called this query. * * We define "the routine that called this query" as the first entry in - * the call stack that is not inside includes/database. That makes the - * climbing logic very simple, and handles the variable stack depth caused - * by the query builders. + * the call stack that is not inside the includes/Drupal/Database directory + * and does not begin with db_. That makes the climbing logic very simple, and + * handles the variable stack depth caused by the query builders. + * + * @todo Revisit this logic to not be dependent on file path, so that we can + * split most of the DB layer out of Drupal. * * @link http://www.php.net/debug_backtrace * @return @@ -142,9 +147,14 @@ public function log(DatabaseStatementInterface $statement, $args, $time) { */ public function findCaller() { $stack = debug_backtrace(); - $stack_count = count($stack); - for ($i = 0; $i < $stack_count; ++$i) { - if (strpos($stack[$i]['file'], 'includes' . DIRECTORY_SEPARATOR . 'database') === FALSE) { + 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) { return array( 'file' => $stack[$i]['file'], 'line' => $stack[$i]['line'], diff --git a/core/lib/Drupal/Core/Database/Query/AlterableInterface.php b/core/lib/Drupal/Core/Database/Query/AlterableInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..66a41e2e32719d31243220a27ddfdf028ed8c70c --- /dev/null +++ b/core/lib/Drupal/Core/Database/Query/AlterableInterface.php @@ -0,0 +1,95 @@ +<?php + +/** + * @file + * Definition of 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); +} diff --git a/core/lib/Drupal/Core/Database/Query/Condition.php b/core/lib/Drupal/Core/Database/Query/Condition.php new file mode 100644 index 0000000000000000000000000000000000000000..9faabc6fb3b5b3d2722713def33f0af0045c4114 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Query/Condition.php @@ -0,0 +1,317 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\Query\Condition + */ + +namespace Drupal\Core\Database\Query; + +use Drupal\Core\Database\Connection; + +use Countable; + +/** + * 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 the + * conjunction. + */ + public function count() { + return count($this->conditions) - 1; + } + + /** + * Implements Drupal\Core\Database\Query\ConditionInterface::condition(). + */ + public function condition($field, $value = NULL, $operator = NULL) { + if (!isset($operator)) { + if (is_array($value)) { + $operator = 'IN'; + } + else { + $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($GLOBALS['lg'])) debug($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']) { + $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 ($condition['field'] instanceOf ConditionInterface) { + $this->conditions[$key]['field'] = clone($condition['field']); + } + } + } + + /** + * 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 + * The extra handling directives for the specified operator, or NULL. + */ + 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 drupal_strtoupper because SQL statements are ASCII. + $operator = strtoupper($operator); + $return = isset($specials[$operator]) ? $specials[$operator] : array(); + } + + $return += array('operator' => $operator); + + return $return; + } + +} diff --git a/core/lib/Drupal/Core/Database/Query/ConditionInterface.php b/core/lib/Drupal/Core/Database/Query/ConditionInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..70c23c785443dab1996f15f70842b5963b755eda --- /dev/null +++ b/core/lib/Drupal/Core/Database/Query/ConditionInterface.php @@ -0,0 +1,159 @@ +<?php + +/** + * @file + * Definition of 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 IN if $value is an array and = otherwise. + * + * Do not use this method to test for NULL values. Instead, use + * QueryConditionInterface::isNull() or QueryConditionInterface::isNotNull(). + * + * @param $field + * The name of the field to check. If you would like to add a more complex + * condition involving operators or functions, use where(). + * @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, and = otherwise. + * + * @return QueryConditionInterface + * The called object. + * + * @see Drupal\Core\Database\Query\ConditionInterface::isNull() + * @see Drupal\Core\Database\Query\ConditionInterface::isNotNull() + */ + public function condition($field, $value = NULL, $operator = NULL); + + /** + * 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(); +} diff --git a/core/lib/Drupal/Core/Database/Query/Delete.php b/core/lib/Drupal/Core/Database/Query/Delete.php new file mode 100644 index 0000000000000000000000000000000000000000..3ecd0407d75121aa071472dbdb60d7c0a171b6c2 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Query/Delete.php @@ -0,0 +1,164 @@ +<?php + +/** + * @file + * Definition of 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. + */ +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 DatabaseConnection 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 = NULL) { + $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; + } +} diff --git a/core/lib/Drupal/Core/Database/Query/ExtendableInterface.php b/core/lib/Drupal/Core/Database/Query/ExtendableInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..1ebac45ac1983852b1bf15012ddfcafd6bc9660a --- /dev/null +++ b/core/lib/Drupal/Core/Database/Query/ExtendableInterface.php @@ -0,0 +1,34 @@ +<?php + +/** + * @file + * Definition of 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. For example, + * PagerDefault_mysql is the MySQL-specific override for PagerDefault. + * @return Drupal\Core\Database\Query\ExtendableInterface + * The extender object, which now contains a reference to this object. + */ + public function extend($extender_name); +} diff --git a/core/lib/Drupal/Core/Database/Query/FieldsOverlapException.php b/core/lib/Drupal/Core/Database/Query/FieldsOverlapException.php new file mode 100644 index 0000000000000000000000000000000000000000..f9d93728b158059da4e1ab5cd1559c8837afe187 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Query/FieldsOverlapException.php @@ -0,0 +1,20 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\Query\FieldsOverlapExceptoin + */ + +namespace Drupal\Core\Database\Query; + +use Drupal\Core\Database\DatabaseException; + +use InvalidArgumentException; + +/** + * 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 {} diff --git a/core/lib/Drupal/Core/Database/Query/Insert.php b/core/lib/Drupal/Core/Database/Query/Insert.php new file mode 100644 index 0000000000000000000000000000000000000000..e19de45cfa75319eca5abdc471bb9c3ef781da2d --- /dev/null +++ b/core/lib/Drupal/Core/Database/Query/Insert.php @@ -0,0 +1,301 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\Query\Insert + */ + +namespace Drupal\Core\Database\Query; + +use Drupal\Core\Database\Database; + +/** + * General class for an abstracted INSERT query. + */ +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 SelectQueryInterface + */ + protected $fromQuery; + + /** + * Constructs an Insert object. + * + * @param Drupal\Core\Database\Connection $connection + * A DatabaseConnection 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 SelectQueryInterface $query + * The query to fetch the rows that should be inserted. + * + * @return InsertQuery + * 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 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()))); + } + + // 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; + } +} diff --git a/core/lib/Drupal/Core/Database/Query/InvalidMergeQueryException.php b/core/lib/Drupal/Core/Database/Query/InvalidMergeQueryException.php new file mode 100644 index 0000000000000000000000000000000000000000..59ad1c9c3037e637b050c42d9b1fd87cc0364f13 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Query/InvalidMergeQueryException.php @@ -0,0 +1,20 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\Query\InvalidMergeQueryException + */ + +namespace Drupal\Core\Database\Query; + +use Drupal\Core\Database\DatabaseException; + +use InvalidArgumentException; + +/** + * 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 {} diff --git a/core/lib/Drupal/Core/Database/Query/Merge.php b/core/lib/Drupal/Core/Database/Query/Merge.php new file mode 100644 index 0000000000000000000000000000000000000000..547a22323b2d90bfa52205378a7d0a0c22b4598c --- /dev/null +++ b/core/lib/Drupal/Core/Database/Query/Merge.php @@ -0,0 +1,457 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\Query\Merge + */ + +namespace Drupal\Core\Database\Query; + +use Drupal\Core\Database\Database; +use Drupal\Core\Database\Connection; + +use Exception; + +/** + * 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 convinient + * 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 boolean + */ + protected $needsUpdate = FALSE; + + /** + * Constructs a Merge object. + * + * @param Drupal\Core\Database\Connection $connection + * A Drupal\Core\Database\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 field(s) 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 Drupal\Core\Database\Query\Merge + * The called object. + */ + public function key(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; + } + + /** + * Implements Drupal\Core\Database\Query\ConditionInterface::condition(). + */ + public function condition($field, $value = NULL, $operator = NULL) { + $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() { + // Wrap multiple queries in a transaction, if the database supports it. + $transaction = $this->connection->startTransaction(); + try { + if (!count($this->condition)) { + throw new InvalidMergeQueryException(t('Invalid merge query: no conditions')); + } + $select = $this->connection->select($this->conditionTable) + ->condition($this->condition) + ->forUpdate(); + $select->addExpression('1'); + if (!$select->execute()->fetchField()) { + try { + $insert = $this->connection->insert($this->table)->fields($this->insertFields); + if ($this->defaultFields) { + $insert->useDefaults($this->defaultFields); + } + $insert->execute(); + return self::STATUS_INSERT; + } + catch (Exception $e) { + // The insert query failed, maybe it's because a racing insert query + // beat us in inserting the same row. Retry the select query, if it + // returns a row, ignore the error and continue with the update + // query below. + if (!$select->execute()->fetchField()) { + throw $e; + } + } + } + if ($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) { + // Something really wrong happened here, bubble up the exception to the + // caller. + $transaction->rollback(); + throw $e; + } + // Transaction commits here where $transaction looses scope. + } +} diff --git a/core/lib/Drupal/Core/Database/Query/NoFieldsException.php b/core/lib/Drupal/Core/Database/Query/NoFieldsException.php new file mode 100644 index 0000000000000000000000000000000000000000..18851fdce52151188d0f29e5b56ed82ad29e4ff0 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Query/NoFieldsException.php @@ -0,0 +1,17 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\Query\NoFieldsException + */ + +namespace Drupal\Core\Database\Query; + +use Drupal\Core\Database\DatabaseException; + +use InvalidArgumentException; + +/** + * Exception thrown if an insert query doesn't specify insert or default fields. + */ +class NoFieldsException extends InvalidArgumentException implements DatabaseException {} diff --git a/core/lib/Drupal/Core/Database/Query/PlaceholderInterface.php b/core/lib/Drupal/Core/Database/Query/PlaceholderInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..68b802e676cb9fdf5d0c9886bbeac492393f16dd --- /dev/null +++ b/core/lib/Drupal/Core/Database/Query/PlaceholderInterface.php @@ -0,0 +1,27 @@ +<?php + +/** + * @file + * Definition of 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(); +} diff --git a/core/lib/Drupal/Core/Database/Query/Query.php b/core/lib/Drupal/Core/Database/Query/Query.php new file mode 100644 index 0000000000000000000000000000000000000000..95ab4b9225dc2c5729ebafc7b47e40c5289408a9 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Query/Query.php @@ -0,0 +1,180 @@ +<?php + +/** + * @file + * Definition of 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. + */ + 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 + * 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 + * 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 Drupal\Core\Database\Query\Query + * The called object. + */ + 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 + * A reference to the comments array structure. + */ + public function &getComments() { + return $this->comments; + } +} diff --git a/core/lib/Drupal/Core/Database/Query/Select.php b/core/lib/Drupal/Core/Database/Query/Select.php new file mode 100644 index 0000000000000000000000000000000000000000..2d3159c36e7bde7c0ed981ac7e334283057343fe --- /dev/null +++ b/core/lib/Drupal/Core/Database/Query/Select.php @@ -0,0 +1,765 @@ +<?php + +/** + * @file + * Definition of 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. + */ +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 boolean + */ + 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 boolean + */ + protected $prepared = FALSE; + + /** + * The FOR UPDATE status + */ + protected $forUpdate = FALSE; + + public function __construct($table, $alias = NULL, Connection $connection, $options = array()) { + $options['return'] = Database::RETURN_STATEMENT; + parent::__construct($connection, $options); + $this->where = new Condition('AND'); + $this->having = new Condition('AND'); + $this->addJoin(NULL, $table, $alias); + } + + /* Implementations of Drupal\Core\Database\Query\AlterableInterface. */ + + public function addTag($tag) { + $this->alterTags[$tag] = 1; + return $this; + } + + public function hasTag($tag) { + return isset($this->alterTags[$tag]); + } + + public function hasAllTags() { + return !(boolean)array_diff(func_get_args(), array_keys($this->alterTags)); + } + + public function hasAnyTag() { + return (boolean)array_intersect(func_get_args(), array_keys($this->alterTags)); + } + + public function addMetaData($key, $object) { + $this->alterMetaData[$key] = $object; + return $this; + } + + public function getMetaData($key) { + return isset($this->alterMetaData[$key]) ? $this->alterMetaData[$key] : NULL; + } + + /* Implementations of Drupal\Core\Database\Query\ConditionInterface for the WHERE clause. */ + + public function condition($field, $value = NULL, $operator = NULL) { + $this->where->condition($field, $value, $operator); + return $this; + } + + public function &conditions() { + return $this->where->conditions(); + } + + 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; + } + + public function where($snippet, $args = array()) { + $this->where->where($snippet, $args); + return $this; + } + + public function isNull($field) { + $this->where->isNull($field); + return $this; + } + + public function isNotNull($field) { + $this->where->isNotNull($field); + return $this; + } + + public function exists(SelectInterface $select) { + $this->where->exists($select); + return $this; + } + + public function notExists(SelectInterface $select) { + $this->where->notExists($select); + return $this; + } + + 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); + } + } + + 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; + } + + /* Implementations of Drupal\Core\Database\Query\ConditionInterface for the HAVING clause. */ + + public function havingCondition($field, $value = NULL, $operator = NULL) { + $this->having->condition($field, $value, $operator); + return $this; + } + + public function &havingConditions() { + return $this->having->conditions(); + } + + public function havingArguments() { + return $this->having->arguments(); + } + + public function having($snippet, $args = array()) { + $this->having->where($snippet, $args); + return $this; + } + + public function havingCompile(Connection $connection) { + return $this->having->compile($connection, $this); + } + + /* Implementations of Drupal\Core\Database\Query\ExtendableInterface. */ + + 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); + } + + public function havingIsNull($field) { + $this->having->isNull($field); + return $this; + } + + public function havingIsNotNull($field) { + $this->having->isNotNull($field); + return $this; + } + + public function havingExists(SelectInterface $select) { + $this->having->exists($select); + return $this; + } + + public function havingNotExists(SelectInterface $select) { + $this->having->notExists($select); + return $this; + } + + public function forUpdate($set = TRUE) { + if (isset($set)) { + $this->forUpdate = $set; + } + return $this; + } + + /* Alter accessors to expose the query data to alter hooks. */ + + public function &getFields() { + return $this->fields; + } + + public function &getExpressions() { + return $this->expressions; + } + + public function &getOrderBy() { + return $this->order; + } + + public function &getGroupBy() { + return $this->group; + } + + public function &getTables() { + return $this->tables; + } + + public function &getUnion() { + return $this->union; + } + + public function getArguments(PlaceholderInterface $queryPlaceholder = NULL) { + if (!isset($queryPlaceholder)) { + $queryPlaceholder = $this; + } + $this->compile($this->connection, $queryPlaceholder); + return $this->arguments(); + } + + /** + * Indicates if preExecute() has already been called on that object. + */ + public function isPrepared() { + return $this->prepared; + } + + /** + * Generic preparation and validation for a SELECT query. + * + * @return + * TRUE if the validation was successful, FALSE if not. + */ + 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_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; + } + + 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); + } + + public function distinct($distinct = TRUE) { + $this->distinct = $distinct; + return $this; + } + + 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; + } + + 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; + } + + 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; + } + + public function join($table, $alias = NULL, $condition = NULL, $arguments = array()) { + return $this->addJoin('INNER', $table, $alias, $condition, $arguments); + } + + public function innerJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) { + return $this->addJoin('INNER', $table, $alias, $condition, $arguments); + } + + public function leftJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) { + return $this->addJoin('LEFT OUTER', $table, $alias, $condition, $arguments); + } + + public function rightJoin($table, $alias = NULL, $condition = NULL, $arguments = array()) { + return $this->addJoin('RIGHT OUTER', $table, $alias, $condition, $arguments); + } + + 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; + } + + public function orderBy($field, $direction = 'ASC') { + $this->order[$field] = $direction; + return $this; + } + + public function orderRandom() { + $alias = $this->addExpression('RAND()', 'random_field'); + $this->orderBy($alias); + return $this; + } + + public function range($start = NULL, $length = NULL) { + $this->range = func_num_args() ? array('start' => $start, 'length' => $length) : array(); + return $this; + } + + 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; + } + + public function groupBy($field) { + $this->group[$field] = $field; + return $this; + } + + public function countQuery() { + // 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 $alias => &$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; + } + + $query = $this->connection->select($count); + $query->addExpression('COUNT(*)'); + + return $query; + } + + 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 $alias => $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 $alias => $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 $alias => $table) { + $query .= "\n"; + if (isset($table['join type'])) { + $query .= $table['join type'] . ' JOIN '; + } + + // If the table is a subquery, compile it and integrate it into this query. + if ($table['table'] instanceof 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']) . '}'; + } + + // 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[] = $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; + } + + 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']); + } + } +} diff --git a/core/lib/Drupal/Core/Database/Query/SelectExtender.php b/core/lib/Drupal/Core/Database/Query/SelectExtender.php new file mode 100644 index 0000000000000000000000000000000000000000..2f27d1b4b3b156a831af95c5cf2210b771ee053a --- /dev/null +++ b/core/lib/Drupal/Core/Database/Query/SelectExtender.php @@ -0,0 +1,330 @@ +<?php + +/** + * @file + * Definition of 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 DatabaseConnection + */ + 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, 'hasAnyTags'), func_get_args()); + } + + public function addMetaData($key, $object) { + $this->query->addMetaData($key, $object); + return $this; + } + + public function getMetaData($key) { + return $this->query->getMetaData($key); + } + + /* Implementations of Drupal\Core\Database\Query\ConditionInterface for the WHERE clause. */ + + public function condition($field, $value = NULL, $operator = NULL) { + $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); + } + + /* 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(); + } + + 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; + } + } +} diff --git a/core/lib/Drupal/Core/Database/Query/SelectInterface.php b/core/lib/Drupal/Core/Database/Query/SelectInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..779e69d2f74f397d5b703b1d4dacf181a0c57013 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Query/SelectInterface.php @@ -0,0 +1,504 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\Query\SelectInterface + */ + +namespace Drupal\Core\Database\Query; + +/** + * Interface definition for a Select Query object. + */ +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(); + + /** + * 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 SelectQueryInterface + * 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. + * @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. + * @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. + * @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. + * @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. + * @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. + * @param $direction + * The direction to sort. Legal values are "ASC" and "DESC". + * @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); + + /** + * 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); + + /** + * 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); +} diff --git a/core/lib/Drupal/Core/Database/Query/Truncate.php b/core/lib/Drupal/Core/Database/Query/Truncate.php new file mode 100644 index 0000000000000000000000000000000000000000..263acf50ab60a5ce875c1312b6c575ec24f76df5 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Query/Truncate.php @@ -0,0 +1,78 @@ +<?php + +/** + * @file + * Definition of 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 DatabaseConnection $connection + * A DatabaseConnection 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); + + return $comments . 'TRUNCATE {' . $this->connection->escapeTable($this->table) . '} '; + } +} diff --git a/core/lib/Drupal/Core/Database/Query/Update.php b/core/lib/Drupal/Core/Database/Query/Update.php new file mode 100644 index 0000000000000000000000000000000000000000..5ffdb33dba244ff239eb785379dbc7e1b15d3f51 --- /dev/null +++ b/core/lib/Drupal/Core/Database/Query/Update.php @@ -0,0 +1,268 @@ +<?php + +/** + * @file + * Definition of 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. + */ +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 = NULL) { + $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 affected by the update. + */ + 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']; + } + 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 $field => $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) { + $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; + } + +} diff --git a/core/includes/database/schema.inc b/core/lib/Drupal/Core/Database/Schema.php similarity index 93% rename from core/includes/database/schema.inc rename to core/lib/Drupal/Core/Database/Schema.php index 8905d3cecc83b860ae14d202b72e896fc7c1d088..9e2f22e38833bac3f4d6bdd61a1ab151d9294546 100644 --- a/core/includes/database/schema.inc +++ b/core/lib/Drupal/Core/Database/Schema.php @@ -2,10 +2,14 @@ /** * @file - * Generic Database schema code. + * Definition of Drupal\Core\Database\Schema */ -require_once __DIR__ . '/query.inc'; +namespace Drupal\Core\Database; + +use Drupal\Core\Database\SchemaObjectExistsException; +use Drupal\Core\Database\Query\Condition; +use Drupal\Core\Database\Query\PlaceholderInterface; /** * @defgroup schemaapi Schema API @@ -157,7 +161,7 @@ * @see drupal_install_schema() */ -abstract class DatabaseSchema implements QueryPlaceholderInterface { +abstract class Schema implements PlaceholderInterface { protected $connection; @@ -195,14 +199,14 @@ public function __clone() { } /** - * Implements QueryPlaceHolderInterface::uniqueIdentifier(). + * Implements PlaceHolderInterface::uniqueIdentifier(). */ public function uniqueIdentifier() { return $this->uniqueIdentifier; } /** - * Implements QueryPlaceHolderInterface::nextPlaceholder(). + * Implements PlaceHolderInterface::nextPlaceholder(). */ public function nextPlaceholder() { return $this->placeholder++; @@ -279,8 +283,8 @@ function prefixNonTable($table) { * @param $add_prefix * Boolean to indicate whether the table name needs to be prefixed. * - * @return QueryConditionInterface - * A DatabaseCondition object. + * @return Drupal\Core\Database\Query\ConditionInterface + * A Drupal\Core\Database\Query\Condition object. */ protected function buildTableNameCondition($table_name, $operator = '=', $add_prefix = TRUE) { $info = $this->connection->getConnectionOptions(); @@ -288,7 +292,7 @@ protected function buildTableNameCondition($table_name, $operator = '=', $add_pr // Retrive the table name and schema $table_info = $this->getPrefixInfo($table_name, $add_prefix); - $condition = new DatabaseCondition('AND'); + $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); @@ -380,9 +384,9 @@ abstract public function getFieldTypeMap(); * @param $new_name * The new name for the table. * - * @throws DatabaseSchemaObjectDoesNotExistException + * @throws Drupal\Core\Database\SchemaObjectDoesNotExistException * If the specified table doesn't exist. - * @throws DatabaseSchemaObjectExistsException + * @throws Drupal\Core\Database\SchemaObjectExistsException * If a table with the specified new name already exists. */ abstract public function renameTable($table, $new_name); @@ -420,9 +424,9 @@ abstract public function dropTable($table); * or index including it in this array. See db_change_field() for more * explanation why. * - * @throws DatabaseSchemaObjectDoesNotExistException + * @throws Drupal\Core\Database\SchemaObjectDoesNotExistException * If the specified table doesn't exist. - * @throws DatabaseSchemaObjectExistsException + * @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()); @@ -451,7 +455,7 @@ abstract public function dropField($table, $field); * @param $default * Default value to be set. NULL for 'default NULL'. * - * @throws DatabaseSchemaObjectDoesNotExistException + * @throws Drupal\Core\Database\SchemaObjectDoesNotExistException * If the specified table or field doesn't exist. */ abstract public function fieldSetDefault($table, $field, $default); @@ -464,7 +468,7 @@ abstract public function fieldSetDefault($table, $field, $default); * @param $field * The field to be altered. * - * @throws DatabaseSchemaObjectDoesNotExistException + * @throws Drupal\Core\Database\SchemaObjectDoesNotExistException * If the specified table or field doesn't exist. */ abstract public function fieldSetNoDefault($table, $field); @@ -490,9 +494,9 @@ abstract public function indexExists($table, $name); * @param $fields * Fields for the primary key. * - * @throws DatabaseSchemaObjectDoesNotExistException + * @throws Drupal\Core\Database\SchemaObjectDoesNotExistException * If the specified table doesn't exist. - * @throws DatabaseSchemaObjectExistsException + * @throws Drupal\Core\Database\SchemaObjectExistsException * If the specified table already has a primary key. */ abstract public function addPrimaryKey($table, $fields); @@ -519,9 +523,9 @@ abstract public function dropPrimaryKey($table); * @param $fields * An array of field names. * - * @throws DatabaseSchemaObjectDoesNotExistException + * @throws Drupal\Core\Database\SchemaObjectDoesNotExistException * If the specified table doesn't exist. - * @throws DatabaseSchemaObjectExistsException + * @throws Drupal\Core\Database\SchemaObjectExistsException * If the specified table already has a key by that name. */ abstract public function addUniqueKey($table, $name, $fields); @@ -550,9 +554,9 @@ abstract public function dropUniqueKey($table, $name); * @param $fields * An array of field names. * - * @throws DatabaseSchemaObjectDoesNotExistException + * @throws Drupal\Core\Database\SchemaObjectDoesNotExistException * If the specified table doesn't exist. - * @throws DatabaseSchemaObjectExistsException + * @throws Drupal\Core\Database\SchemaObjectExistsException * If the specified table already has an index by that name. */ abstract public function addIndex($table, $name, $fields); @@ -646,12 +650,12 @@ abstract public function changeField($table, $field, $field_new, $spec, $keys_ne * @param $table * A Schema API table definition array. * - * @throws DatabaseSchemaObjectExistsException + * @throws Drupal\Core\Database\SchemaObjectExistsException * If the specified table already exists. */ public function createTable($name, $table) { if ($this->tableExists($name)) { - throw new DatabaseSchemaObjectExistsException(t('Table %name already exists.', array('%name' => $name))); + throw new SchemaObjectExistsException(t('Table %name already exists.', array('%name' => $name))); } $statements = $this->createTableSql($name, $table); foreach ($statements as $statement) { @@ -699,26 +703,3 @@ public function prepareComment($comment, $length = NULL) { return $this->connection->quote($comment); } } - -/** - * 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 DatabaseSchemaObjectExistsException extends Exception {} - -/** - * 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 DatabaseSchemaObjectDoesNotExistException extends Exception {} - -/** - * @} End of "defgroup schemaapi". - */ - diff --git a/core/lib/Drupal/Core/Database/SchemaException.php b/core/lib/Drupal/Core/Database/SchemaException.php new file mode 100644 index 0000000000000000000000000000000000000000..3ae72b491489670a08c7c4f8f4aee961bc36414c --- /dev/null +++ b/core/lib/Drupal/Core/Database/SchemaException.php @@ -0,0 +1,15 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\SchemaException + */ + +namespace Drupal\Core\Database; + +use RuntimeException; + +/** + * Base exception for Schema-related errors. + */ +class SchemaException extends RuntimeException implements DatabaseException { } diff --git a/core/lib/Drupal/Core/Database/SchemaObjectDoesNotExistException.php b/core/lib/Drupal/Core/Database/SchemaObjectDoesNotExistException.php new file mode 100644 index 0000000000000000000000000000000000000000..ac5a1f8828b958164bd34b901e1f7981e680e5fc --- /dev/null +++ b/core/lib/Drupal/Core/Database/SchemaObjectDoesNotExistException.php @@ -0,0 +1,17 @@ +<?php + +/** + * @file + * Definition of 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 { } diff --git a/core/lib/Drupal/Core/Database/SchemaObjectExistsException.php b/core/lib/Drupal/Core/Database/SchemaObjectExistsException.php new file mode 100644 index 0000000000000000000000000000000000000000..e8221b5267c2f6e41e93e24a328d4746d964d1ea --- /dev/null +++ b/core/lib/Drupal/Core/Database/SchemaObjectExistsException.php @@ -0,0 +1,17 @@ +<?php + +/** + * @file + * Definition of 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 { } diff --git a/core/lib/Drupal/Core/Database/Statement.php b/core/lib/Drupal/Core/Database/Statement.php new file mode 100644 index 0000000000000000000000000000000000000000..7b23ed3d6a04e29f8032e6713fe0821ba1a6313d --- /dev/null +++ b/core/lib/Drupal/Core/Database/Statement.php @@ -0,0 +1,113 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\StatementBase + */ + +namespace Drupal\Core\Database; + +use PDO; +use PDOStatement; + +/** + * Default implementation of DatabaseStatementInterface. + * + * 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://us.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 DatabaseConnection + */ + public $dbh; + + protected function __construct($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'])) { + // 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 http://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); + } + + $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); + } +} diff --git a/core/lib/Drupal/Core/Database/StatementEmpty.php b/core/lib/Drupal/Core/Database/StatementEmpty.php new file mode 100644 index 0000000000000000000000000000000000000000..84ccb6a2ab5843630af916a9b99d561d07806708 --- /dev/null +++ b/core/lib/Drupal/Core/Database/StatementEmpty.php @@ -0,0 +1,94 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\StatementEmpty + */ + +namespace Drupal\Core\Database; + +use Iterator; + +/** + * 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 SearchQuery + */ +class StatementEmpty implements Iterator, StatementInterface { + + public function execute($args = array(), $options = array()) { + return FALSE; + } + + public function getQueryString() { + return ''; + } + + public function rowCount() { + return 0; + } + + 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; + } +} diff --git a/core/lib/Drupal/Core/Database/StatementInterface.php b/core/lib/Drupal/Core/Database/StatementInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..2fb57fcf14a752b4452c38d021c6737a78a3c024 --- /dev/null +++ b/core/lib/Drupal/Core/Database/StatementInterface.php @@ -0,0 +1,199 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\StatementInterface + */ + +namespace Drupal\Core\Database; + +use Traversable; + +/** + * 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 + */ +interface StatementInterface extends Traversable { + + /** + * 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. + */ + public function rowCount(); + + /** + * Sets the default fetch mode for this statement. + * + * See http://php.net/manual/en/pdo.constants.php for the definition of the + * 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/en/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 DatabaseStatementInterface::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); +} diff --git a/core/includes/database/prefetch.inc b/core/lib/Drupal/Core/Database/StatementPrefetch.php similarity index 95% rename from core/includes/database/prefetch.inc rename to core/lib/Drupal/Core/Database/StatementPrefetch.php index 4f2b19d1f3d1882c19dbef6264cfb52511ca149a..f860a9ff0909bf9e0d67bdba91a3a2afe13e7068 100644 --- a/core/includes/database/prefetch.inc +++ b/core/lib/Drupal/Core/Database/StatementPrefetch.php @@ -2,16 +2,15 @@ /** * @file - * Database interface code for engines that need complete control over their - * result sets. For example, 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. + * Definition of Drupal\Core\Database\StatementPrefetch */ -/** - * @ingroup database - * @{ - */ +namespace Drupal\Core\Database; + +use Drupal\Core\Database\Connection; +use Iterator; +use PDO; +use PDOException; /** * An implementation of DatabaseStatementInterface that prefetches all data. @@ -19,7 +18,7 @@ * This class behaves very similar to a PDOStatement but as it always fetches * every row it is possible to manipulate those results. */ -class DatabaseStatementPrefetch implements Iterator, DatabaseStatementInterface { +class StatementPrefetch implements Iterator, StatementInterface { /** * The query string. @@ -40,7 +39,7 @@ class DatabaseStatementPrefetch implements Iterator, DatabaseStatementInterface * * The name $dbh is inherited from PDOStatement. * - * @var DatabaseConnection + * @var Drupal\Core\Database\Connection */ public $dbh; @@ -125,7 +124,7 @@ class DatabaseStatementPrefetch implements Iterator, DatabaseStatementInterface 'column' => 0, ); - public function __construct(DatabaseConnection $connection, $query, array $driver_options = array()) { + public function __construct(Connection $connection, $query, array $driver_options = array()) { $this->dbh = $connection; $this->queryString = $query; $this->driverOptions = $driver_options; @@ -500,8 +499,3 @@ public function fetchAllAssoc($key, $fetch_style = NULL) { } } - -/** - * @} End of "ingroup database". - */ - diff --git a/core/lib/Drupal/Core/Database/Transaction.php b/core/lib/Drupal/Core/Database/Transaction.php new file mode 100644 index 0000000000000000000000000000000000000000..10adadb7111f08cb2a191f2d9f48a4f3f3f5fdac --- /dev/null +++ b/core/lib/Drupal/Core/Database/Transaction.php @@ -0,0 +1,101 @@ +<?php + +/** + * @file + * Definition of 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 Boolean + */ + 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); + } +} diff --git a/core/lib/Drupal/Core/Database/TransactionCommitFailedException.php b/core/lib/Drupal/Core/Database/TransactionCommitFailedException.php new file mode 100644 index 0000000000000000000000000000000000000000..6a5309ea823746aacf5f11f275bb3e286e06cd40 --- /dev/null +++ b/core/lib/Drupal/Core/Database/TransactionCommitFailedException.php @@ -0,0 +1,13 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\TransactionCommitFailedException + */ + +namespace Drupal\Core\Database; + +/** + * Exception thrown when a commit() function fails. + */ +class TransactionCommitFailedException extends TransactionException implements DatabaseException { } diff --git a/core/lib/Drupal/Core/Database/TransactionException.php b/core/lib/Drupal/Core/Database/TransactionException.php new file mode 100644 index 0000000000000000000000000000000000000000..25079f5f23c01e771bc42732b1f4f657373f269c --- /dev/null +++ b/core/lib/Drupal/Core/Database/TransactionException.php @@ -0,0 +1,15 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\TransactionException + */ + +namespace Drupal\Core\Database; + +use RuntimeException; + +/** + * Exception thrown by an error in a database transaction. + */ +class TransactionException extends RuntimeException implements DatabaseException { } diff --git a/core/lib/Drupal/Core/Database/TransactionExplicitCommitNotAllowedException.php b/core/lib/Drupal/Core/Database/TransactionExplicitCommitNotAllowedException.php new file mode 100644 index 0000000000000000000000000000000000000000..fd4bf23438048fe161abb8af018e835c4b4337a7 --- /dev/null +++ b/core/lib/Drupal/Core/Database/TransactionExplicitCommitNotAllowedException.php @@ -0,0 +1,16 @@ +<?php + +/** + * @file + * Definition of 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 { } diff --git a/core/lib/Drupal/Core/Database/TransactionNameNonUniqueException.php b/core/lib/Drupal/Core/Database/TransactionNameNonUniqueException.php new file mode 100644 index 0000000000000000000000000000000000000000..aef40cb81deb522dac38c5b7df82e5ac79859fe6 --- /dev/null +++ b/core/lib/Drupal/Core/Database/TransactionNameNonUniqueException.php @@ -0,0 +1,13 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\TransactionNameNonUniqueException + */ + +namespace Drupal\Core\Database; + +/** + * Exception thrown when a savepoint or transaction name occurs twice. + */ +class TransactionNameNonUniqueException extends TransactionException implements DatabaseException { } diff --git a/core/lib/Drupal/Core/Database/TransactionNoActiveException.php b/core/lib/Drupal/Core/Database/TransactionNoActiveException.php new file mode 100644 index 0000000000000000000000000000000000000000..44acede1d55fe2d0c3231583cafcde25fc58736b --- /dev/null +++ b/core/lib/Drupal/Core/Database/TransactionNoActiveException.php @@ -0,0 +1,13 @@ +<?php + +/** + * @file + * Definition of Drupal\Core\Database\TransactionNoActiveException + */ + +namespace Drupal\Core\Database; + +/** + * Exception for when popTransaction() is called with no active transaction. + */ +class TransactionNoActiveException extends TransactionException implements DatabaseException { } diff --git a/core/lib/Drupal/Core/Database/TransactionOutOfOrderException.php b/core/lib/Drupal/Core/Database/TransactionOutOfOrderException.php new file mode 100644 index 0000000000000000000000000000000000000000..2e2fe7a90d45760951c75726197a7ee3a3a278a0 --- /dev/null +++ b/core/lib/Drupal/Core/Database/TransactionOutOfOrderException.php @@ -0,0 +1,13 @@ +<?php + +/** + * @file + * Definition of 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 { } diff --git a/core/modules/dblog/dblog.module b/core/modules/dblog/dblog.module index 1f6c8c2f7f1aa4539514b2710cac57a995f464af..9c54ddfd32918695162ec8584d99e780cae7edeb 100644 --- a/core/modules/dblog/dblog.module +++ b/core/modules/dblog/dblog.module @@ -1,5 +1,7 @@ <?php +use Drupal\Core\Database\Database; + /** * @file * System monitoring and logging for administrators. diff --git a/core/modules/entity/entity.query.inc b/core/modules/entity/entity.query.inc index fde912376993cadd679afa0d00509e27ec8f1345..fb7ebf06ca38cb7532207100983781297ce649f6 100644 --- a/core/modules/entity/entity.query.inc +++ b/core/modules/entity/entity.query.inc @@ -1,5 +1,7 @@ <?php +use Drupal\Core\Database\Query\Select; + /** * @file * Entity query API. @@ -940,7 +942,7 @@ function finishQuery($select_query, $id_key = 'entity_id') { * HAVING or WHERE. This is necessary because SQL can't handle WHERE * conditions on aliased columns. */ - public function addCondition(SelectQuery $select_query, $sql_field, $condition, $having = FALSE) { + public function addCondition(Select $select_query, $sql_field, $condition, $having = FALSE) { $method = $having ? 'havingCondition' : 'condition'; $like_prefix = ''; switch ($condition['operator']) { diff --git a/core/modules/field/modules/field_sql_storage/field_sql_storage.module b/core/modules/field/modules/field_sql_storage/field_sql_storage.module index 561eab6eba22d8ece593dde29e48577bd8586900..adc50bd61114661d7259808029aa6a2128a1fb08 100644 --- a/core/modules/field/modules/field_sql_storage/field_sql_storage.module +++ b/core/modules/field/modules/field_sql_storage/field_sql_storage.module @@ -1,5 +1,8 @@ <?php +use Drupal\Core\Database\Database; +use Drupal\Core\Database\Query\Select; + /** * @file * Default implementation of the field storage API. @@ -592,7 +595,7 @@ function field_sql_storage_field_storage_query(EntityFieldQuery $query) { * @return * The name of the entity base table joined in. */ -function _field_sql_storage_query_join_entity(SelectQuery $select_query, $entity_type, $field_base_table) { +function _field_sql_storage_query_join_entity(Select $select_query, $entity_type, $field_base_table) { $entity_info = entity_get_info($entity_type); $entity_base_table = $entity_info['base table']; $entity_field = $entity_info['entity keys']['id']; @@ -615,7 +618,7 @@ function _field_sql_storage_query_join_entity(SelectQuery $select_query, $entity * A callback that should return the column name to be used for the field * conditions. Accepts a field name and a field column name as parameters. */ -function _field_sql_storage_query_field_conditions(EntityFieldQuery $query, SelectQuery $select_query, $conditions, $table_aliases, $column_callback) { +function _field_sql_storage_query_field_conditions(EntityFieldQuery $query, Select $select_query, $conditions, $table_aliases, $column_callback) { $groups = &drupal_static(__FUNCTION__, array()); foreach ($conditions as $key => $condition) { $table_alias = $table_aliases[$key]; diff --git a/core/modules/field/modules/field_sql_storage/field_sql_storage.test b/core/modules/field/modules/field_sql_storage/field_sql_storage.test index b87227023a0c733cfa78e0b6a1f42641b89cc651..d6c766e9f9462934fe3ab1f830bce59afddc51c5 100644 --- a/core/modules/field/modules/field_sql_storage/field_sql_storage.test +++ b/core/modules/field/modules/field_sql_storage/field_sql_storage.test @@ -1,5 +1,7 @@ <?php +use Drupal\Core\Database\Database; + /** * @file * Tests for field_sql_storage.module. diff --git a/core/modules/node/node.admin.inc b/core/modules/node/node.admin.inc index d78ebf7588cea26bdbc2f207561dc0a6686499e2..1a33174c67cf587933a8bb78ad308479160b59cd 100644 --- a/core/modules/node/node.admin.inc +++ b/core/modules/node/node.admin.inc @@ -1,5 +1,7 @@ <?php +use Drupal\Core\Database\Query\SelectInterface; + /** * @file * Content administration and module settings UI. @@ -125,7 +127,7 @@ function node_filters() { * @param $query * A SelectQuery to which the filters should be applied. */ -function node_build_filter_query(SelectQueryInterface $query) { +function node_build_filter_query(SelectInterface $query) { // Build query $filter_data = isset($_SESSION['node_overview_filter']) ? $_SESSION['node_overview_filter'] : array(); foreach ($filter_data as $index => $filter) { diff --git a/core/modules/node/node.module b/core/modules/node/node.module index 152360f48151adce03be7744d9a9da02fd4312e4..80d0897d193a6ce7df404109007f3f98920c591c 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -1,5 +1,9 @@ <?php +use Drupal\Core\Database\Query\AlterableInterface; +use Drupal\Core\Database\Query\SelectExtender; +use Drupal\Core\Database\Query\SelectInterface; + /** * @file * The core module that allows content to be submitted to the site. @@ -1637,7 +1641,7 @@ function node_permission() { * @param $query * A query object that has been extended with the Search DB Extender. */ -function _node_rankings(SelectQueryExtender $query) { +function _node_rankings(SelectExtender $query) { if ($ranking = module_invoke_all('ranking')) { $tables = &$query->getTables(); foreach ($ranking as $rank => $values) { @@ -3281,7 +3285,7 @@ function node_access_view_all_nodes($account = NULL) { * the 'op' meta-data (or 'view' if not provided; other possible values are * 'update' and 'delete'). */ -function node_query_node_access_alter(QueryAlterableInterface $query) { +function node_query_node_access_alter(AlterableInterface $query) { _node_query_node_access_alter($query, 'node'); } @@ -3292,7 +3296,7 @@ function node_query_node_access_alter(QueryAlterableInterface $query) { * node_query_node_access_alter() for the SQL field storage engine. Node access * conditions are added for field values belonging to nodes only. */ -function node_query_entity_field_access_alter(QueryAlterableInterface $query) { +function node_query_entity_field_access_alter(AlterableInterface $query) { _node_query_node_access_alter($query, 'entity'); } @@ -3336,7 +3340,7 @@ function _node_query_node_access_alter($query, $type) { if (!$base_table) { $fallback = ''; foreach ($tables as $alias => $table_info) { - if (!($table_info instanceof SelectQueryInterface)) { + if (!($table_info instanceof SelectInterface)) { $table = $table_info['table']; // If the node table is in the query, it wins immediately. if ($table == 'node') { @@ -3408,7 +3412,7 @@ function _node_query_node_access_alter($query, $type) { } foreach ($tables as $nalias => $tableinfo) { $table = $tableinfo['table']; - if (!($table instanceof SelectQueryInterface) && $table == $base_table) { + if (!($table instanceof SelectInterface) && $table == $base_table) { // The node_access table has the access grants for any given node so JOIN // it to the table containing the nid which can be either the node diff --git a/core/modules/node/node.test b/core/modules/node/node.test index 02001c713f29f2a658eb3c530d36fdd10c3aa19f..96d4e01e340cf7206de3bec2d132293667f2119a 100644 --- a/core/modules/node/node.test +++ b/core/modules/node/node.test @@ -1,5 +1,7 @@ <?php +use Drupal\Core\Database\Database; + /** * @file * Tests for node.module. diff --git a/core/modules/search/search.extender.inc b/core/modules/search/search.extender.inc index ad4b86e896137799cb0ed2dddf4af1e85c47f1e7..73f783652f6dba592d189337d45e5d4496915535 100644 --- a/core/modules/search/search.extender.inc +++ b/core/modules/search/search.extender.inc @@ -1,5 +1,8 @@ <?php +use Drupal\Core\Database\Query\SelectExtender; +use Drupal\Core\Database\StatementEmpty; + /** * @file * Search query extender and helper functions. @@ -24,7 +27,7 @@ * The used query object has the tag 'search_$module' and can be further * extended with hook_query_alter(). */ -class SearchQuery extends SelectQueryExtender { +class SearchQuery extends SelectExtender { /** * The search query that is used for searching. * @@ -433,7 +436,7 @@ public function execute() $this->executeFirstPass(); } if (!$this->normalize) { - return new DatabaseStatementEmpty(); + return new StatementEmpty(); } // Add conditions to query. diff --git a/core/modules/simpletest/drupal_web_test_case.php b/core/modules/simpletest/drupal_web_test_case.php index 2852fca9d1f323595dbdba095774a7f0c01e99a9..e5d36c338ba7809dbc12e60566402df657aa41fb 100644 --- a/core/modules/simpletest/drupal_web_test_case.php +++ b/core/modules/simpletest/drupal_web_test_case.php @@ -1,5 +1,8 @@ <?php +use Drupal\Core\Database\Database; +use Drupal\Core\Database\ConnectionNotDefinedException; + /** * Global variable that holds information about the tests being run. * @@ -152,7 +155,7 @@ protected function assert($status, $message = '', $group = 'Other', array $calle try { $connection = Database::getConnection('default', 'simpletest_original_default'); } - catch (DatabaseConnectionNotDefinedException $e) { + catch (ConnectionNotDefinedException $e) { // If the test was not set up, the simpletest_original_default // connection does not exist. $connection = Database::getConnection('default', 'default'); diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module index 831ea859ec5c1d64f1513d6fc434fb0b78ca8cd4..080b621b3b7b2a4ff9c370a598efe793a7faac7a 100644 --- a/core/modules/simpletest/simpletest.module +++ b/core/modules/simpletest/simpletest.module @@ -1,5 +1,7 @@ <?php +use Drupal\Core\Database\Database; + /** * @file * Provides testing functionality. diff --git a/core/modules/simpletest/tests/database_test.module b/core/modules/simpletest/tests/database_test.module index 6fac31919c7362d950415cb6f1ddefa6941c15fd..0ab51d80747ac345162d2eb9fd925ea770b1062d 100644 --- a/core/modules/simpletest/tests/database_test.module +++ b/core/modules/simpletest/tests/database_test.module @@ -1,9 +1,11 @@ <?php +use Drupal\Core\Database\Query\AlterableInterface; + /** * Implements hook_query_alter(). */ -function database_test_query_alter(QueryAlterableInterface $query) { +function database_test_query_alter(AlterableInterface $query) { if ($query->hasTag('database_test_alter_add_range')) { $query->range(0, 2); @@ -37,7 +39,7 @@ function database_test_query_alter(QueryAlterableInterface $query) { * * Called by DatabaseTestCase::testAlterRemoveRange. */ -function database_test_query_database_test_alter_remove_range_alter(QueryAlterableInterface $query) { +function database_test_query_database_test_alter_remove_range_alter(AlterableInterface $query) { $query->range(); } diff --git a/core/modules/simpletest/tests/database_test.test b/core/modules/simpletest/tests/database_test.test index 16c09c654c4fb3e9257bba878d0da10ce885ef1a..1d38ebb3766bef2aa2bf2bfe3f917be89f936238 100644 --- a/core/modules/simpletest/tests/database_test.test +++ b/core/modules/simpletest/tests/database_test.test @@ -1,5 +1,14 @@ <?php +use Drupal\Core\Database\Database; +use Drupal\Core\Database\StatementEmpty; +use Drupal\Core\Database\StatementInterface; +use Drupal\Core\Database\TransactionOutOfOrderException; +use Drupal\Core\Database\TransactionNoActiveException; +use Drupal\Core\Database\Query\Merge; +use Drupal\Core\Database\Query\InvalidMergeQueryException; +use Drupal\Core\Database\Query\NoFieldsException; + /** * Dummy class for fetching into a class. * @@ -307,7 +316,7 @@ class DatabaseFetchTestCase extends DatabaseTestCase { function testQueryFetchDefault() { $records = array(); $result = db_query('SELECT name FROM {test} WHERE age = :age', array(':age' => 25)); - $this->assertTrue($result instanceof DatabaseStatementInterface, t('Result set is a Drupal statement object.')); + $this->assertTrue($result instanceof StatementInterface, t('Result set is a Drupal statement object.')); foreach ($result as $record) { $records[] = $record; $this->assertTrue(is_object($record), t('Record is an object.')); @@ -1090,7 +1099,7 @@ class DatabaseMergeTestCase extends DatabaseTestCase { )) ->execute(); - $this->assertEqual($result, MergeQuery::STATUS_INSERT, t('Insert status returned.')); + $this->assertEqual($result, Merge::STATUS_INSERT, t('Insert status returned.')); $num_records_after = db_query('SELECT COUNT(*) FROM {test_people}')->fetchField(); $this->assertEqual($num_records_before + 1, $num_records_after, t('Merge inserted properly.')); @@ -1115,7 +1124,7 @@ class DatabaseMergeTestCase extends DatabaseTestCase { )) ->execute(); - $this->assertEqual($result, MergeQuery::STATUS_UPDATE, t('Update status returned.')); + $this->assertEqual($result, Merge::STATUS_UPDATE, t('Update status returned.')); $num_records_after = db_query('SELECT COUNT(*) FROM {test_people}')->fetchField(); $this->assertEqual($num_records_before, $num_records_after, t('Merge updated properly.')); @@ -3512,7 +3521,7 @@ class DatabaseTransactionTestCase extends DatabaseTestCase { // "ROLLBACK" fails silently in MySQL if there is no transaction active. // $this->fail(t('Rolling back a transaction containing DDL should fail.')); } - catch (DatabaseTransactionNoActiveException $e) { + catch (TransactionNoActiveException $e) { $this->pass(t('Rolling back a transaction containing DDL should fail.')); } $this->assertRowPresent('row'); @@ -3680,7 +3689,7 @@ class DatabaseTransactionTestCase extends DatabaseTestCase { unset($transaction); $this->fail(t('Rolling back the outer transaction while the inner transaction is active resulted in an exception.')); } - catch (DatabaseTransactionOutOfOrderException $e) { + catch (TransactionOutOfOrderException $e) { $this->pass(t('Rolling back the outer transaction while the inner transaction is active resulted in an exception.')); } $this->assertFalse($database->inTransaction(), t('No more in a transaction after rolling back the outer transaction')); @@ -3693,7 +3702,7 @@ class DatabaseTransactionTestCase extends DatabaseTestCase { unset($transaction2); $this->fail(t('Trying to commit an inner transaction resulted in an exception.')); } - catch (DatabaseTransactionNoActiveException $e) { + catch (TransactionNoActiveException $e) { $this->pass(t('Trying to commit an inner transaction resulted in an exception.')); } $this->assertRowAbsent('outer'); @@ -3746,9 +3755,9 @@ class DatabaseEmptyStatementTestCase extends DrupalWebTestCase { * Test that the empty result set behaves as empty. */ function testEmpty() { - $result = new DatabaseStatementEmpty(); + $result = new StatementEmpty(); - $this->assertTrue($result instanceof DatabaseStatementInterface, t('Class implements expected interface')); + $this->assertTrue($result instanceof StatementInterface, t('Class implements expected interface')); $this->assertNull($result->fetchObject(), t('Null result returned.')); } @@ -3756,7 +3765,7 @@ class DatabaseEmptyStatementTestCase extends DrupalWebTestCase { * Test that the empty result set iterates safely. */ function testEmptyIteration() { - $result = new DatabaseStatementEmpty(); + $result = new StatementEmpty(); foreach ($result as $record) { $this->fail(t('Iterating empty result set should not iterate.')); @@ -3770,7 +3779,7 @@ class DatabaseEmptyStatementTestCase extends DrupalWebTestCase { * Test that the empty result set mass-fetches in an expected way. */ function testEmptyFetchAll() { - $result = new DatabaseStatementEmpty(); + $result = new StatementEmpty(); $this->assertEqual($result->fetchAll(), array(), t('Empty array returned from empty result set.')); } diff --git a/core/modules/simpletest/tests/schema.test b/core/modules/simpletest/tests/schema.test index 8945117cbbdba4b13b70f9fd50ffbb4fce8a1460..5a105678fc7c9845032b994e787ef83138dbcb57 100644 --- a/core/modules/simpletest/tests/schema.test +++ b/core/modules/simpletest/tests/schema.test @@ -1,5 +1,7 @@ <?php +use Drupal\Core\Database\Database; + /** * @file * Tests for the Database Schema API. diff --git a/core/modules/simpletest/tests/upgrade/upgrade.test b/core/modules/simpletest/tests/upgrade/upgrade.test index 381816c72e84485f26a6e47b9c5714aa87dd045f..aafb642cfd4799a40b2a011358b79a8698360025 100644 --- a/core/modules/simpletest/tests/upgrade/upgrade.test +++ b/core/modules/simpletest/tests/upgrade/upgrade.test @@ -1,5 +1,7 @@ <?php +use Drupal\Core\Database\Database; + /** * Perform end-to-end tests of the upgrade path. */ diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php index 4d225d5b35709a83ac6c696d6b8e04a193814826..907b7b5f18a5704e9d91ecbedce1cf12d409129b 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -2746,10 +2746,10 @@ function hook_schema_alter(&$schema) { * * @see hook_query_TAG_alter() * @see node_query_node_access_alter() - * @see QueryAlterableInterface - * @see SelectQueryInterface + * @see AlterableInterface + * @see SelectInterface */ -function hook_query_alter(QueryAlterableInterface $query) { +function hook_query_alter(Drupal\Database\Query\AlterableInterface $query) { if ($query->hasTag('micro_limit')) { $query->range(0, 2); } @@ -2763,10 +2763,10 @@ function hook_query_alter(QueryAlterableInterface $query) { * * @see hook_query_alter() * @see node_query_node_access_alter() - * @see QueryAlterableInterface - * @see SelectQueryInterface + * @see AlterableInterface + * @see SelectInterface */ -function hook_query_TAG_alter(QueryAlterableInterface $query) { +function hook_query_TAG_alter(Drupal\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. diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 117cfa9506943bdb067dca6575dd3e4fda2602d9..d562bf68e286e94b7f1c9ab4d179af8684649fde 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -1,5 +1,7 @@ <?php +use Drupal\Core\Database\Database; + /** * @file * Install, update and uninstall functions for the system module. @@ -180,7 +182,7 @@ function system_requirements($phase) { } else { // Database information. - $class = 'DatabaseTasks_' . Database::getConnection()->driver(); + $class = Database::getConnection()->getDriverClass('Install\\Tasks'); $tasks = new $class(); $requirements['database_system'] = array( 'title' => $t('Database system'), diff --git a/core/modules/system/system.test b/core/modules/system/system.test index 8d6566709454f6114533dad91779eded531df5f2..64774c458146e553b481ff7db1381f81dcf2d1ce 100644 --- a/core/modules/system/system.test +++ b/core/modules/system/system.test @@ -1,5 +1,7 @@ <?php +use Drupal\Core\Database\Database; + /** * @file * Tests for system.module. diff --git a/core/modules/user/user.module b/core/modules/user/user.module index f4479ec4c2c9f5a4bbca5241939a3283a5db20c8..3ac4cd682313d0f4a76bdaf4ab163dddeb4ded60 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -1,5 +1,7 @@ <?php +use Drupal\Core\Database\Query\SelectInterface; + /** * @file * Enables the user registration and login system. @@ -3377,7 +3379,7 @@ function user_filters() { * @param $query * Query object that should be filtered. */ -function user_build_filter_query(SelectQuery $query) { +function user_build_filter_query(SelectInterface $query) { $filters = user_filters(); // Extend Query with filter conditions. foreach (isset($_SESSION['user_overview_filter']) ? $_SESSION['user_overview_filter'] : array() as $filter) {