summaryrefslogtreecommitdiffstatshomepage
path: root/core/includes/database
diff options
context:
space:
mode:
Diffstat (limited to 'core/includes/database')
-rw-r--r--core/includes/database/database.inc3003
-rw-r--r--core/includes/database/log.inc159
-rw-r--r--core/includes/database/mysql/database.inc187
-rw-r--r--core/includes/database/mysql/install.inc33
-rw-r--r--core/includes/database/mysql/query.inc107
-rw-r--r--core/includes/database/mysql/schema.inc531
-rw-r--r--core/includes/database/pgsql/database.inc203
-rw-r--r--core/includes/database/pgsql/install.inc176
-rw-r--r--core/includes/database/pgsql/query.inc209
-rw-r--r--core/includes/database/pgsql/schema.inc617
-rw-r--r--core/includes/database/pgsql/select.inc108
-rw-r--r--core/includes/database/prefetch.inc507
-rw-r--r--core/includes/database/query.inc1953
-rw-r--r--core/includes/database/schema.inc722
-rw-r--r--core/includes/database/select.inc1630
-rw-r--r--core/includes/database/sqlite/database.inc511
-rw-r--r--core/includes/database/sqlite/install.inc51
-rw-r--r--core/includes/database/sqlite/query.inc160
-rw-r--r--core/includes/database/sqlite/schema.inc683
-rw-r--r--core/includes/database/sqlite/select.inc27
20 files changed, 11577 insertions, 0 deletions
diff --git a/core/includes/database/database.inc b/core/includes/database/database.inc
new file mode 100644
index 00000000000..bdd65cc5fde
--- /dev/null
+++ b/core/includes/database/database.inc
@@ -0,0 +1,3003 @@
+<?php
+
+/**
+ * @file
+ * Core systems for the database layer.
+ *
+ * Classes required for basic functioning of the database system should be
+ * placed in this file. All utility functions should also be placed in this
+ * file only, as they cannot auto-load the way classes can.
+ */
+
+/**
+ * @defgroup database Database abstraction layer
+ * @{
+ * Allow the use of different database servers using the same code base.
+ *
+ * Drupal provides a database abstraction layer to provide developers with
+ * the ability to support multiple database servers easily. The intent of
+ * this layer is to preserve the syntax and power of SQL as much as possible,
+ * but also allow developers a way to leverage more complex functionality in
+ * a unified way. It also provides a structured interface for dynamically
+ * constructing queries when appropriate, and enforcing security checks and
+ * similar good practices.
+ *
+ * The system is built atop PHP's PDO (PHP Data Objects) database API and
+ * inherits much of its syntax and semantics.
+ *
+ * Most Drupal database SELECT queries are performed by a call to db_query() or
+ * db_query_range(). Module authors should also consider using the PagerDefault
+ * Extender for queries that return results that need to be presented on
+ * multiple pages, and the Tablesort Extender for generating appropriate queries
+ * for sortable tables.
+ *
+ * For example, one might wish to return a list of the most recent 10 nodes
+ * authored by a given user. Instead of directly issuing the SQL query
+ * @code
+ * SELECT n.nid, n.title, n.created FROM node n WHERE n.uid = $uid LIMIT 0, 10;
+ * @endcode
+ * one would instead call the Drupal functions:
+ * @code
+ * $result = db_query_range('SELECT n.nid, n.title, n.created
+ * FROM {node} n WHERE n.uid = :uid', 0, 10, array(':uid' => $uid));
+ * foreach ($result as $record) {
+ * // Perform operations on $node->title, etc. here.
+ * }
+ * @endcode
+ * Curly braces are used around "node" to provide table prefixing via
+ * DatabaseConnection::prefixTables(). The explicit use of a user ID is pulled
+ * out into an argument passed to db_query() so that SQL injection attacks
+ * from user input can be caught and nullified. The LIMIT syntax varies between
+ * database servers, so that is abstracted into db_query_range() arguments.
+ * Finally, note the PDO-based ability to iterate over the result set using
+ * foreach ().
+ *
+ * All queries are passed as a prepared statement string. A
+ * prepared statement is a "template" of a query that omits literal or variable
+ * values in favor of placeholders. The values to place into those
+ * placeholders are passed separately, and the database driver handles
+ * inserting the values into the query in a secure fashion. That means you
+ * should never quote or string-escape a value to be inserted into the query.
+ *
+ * There are two formats for placeholders: named and unnamed. Named placeholders
+ * are strongly preferred in all cases as they are more flexible and
+ * self-documenting. Named placeholders should start with a colon ":" and can be
+ * followed by one or more letters, numbers or underscores.
+ *
+ * Named placeholders begin with a colon followed by a unique string. Example:
+ * @code
+ * SELECT nid, title FROM {node} WHERE uid=:uid;
+ * @endcode
+ *
+ * ":uid" is a placeholder that will be replaced with a literal value when
+ * the query is executed. A given placeholder label cannot be repeated in a
+ * given query, even if the value should be the same. When using named
+ * placeholders, the array of arguments to the query must be an associative
+ * array where keys are a placeholder label (e.g., :uid) and the value is the
+ * corresponding value to use. The array may be in any order.
+ *
+ * Unnamed placeholders are simply a question mark. Example:
+ * @code
+ * SELECT nid, title FROM {node} WHERE uid=?;
+ * @endcode
+ *
+ * In this case, the array of arguments must be an indexed array of values to
+ * use in the exact same order as the placeholders in the query.
+ *
+ * Note that placeholders should be a "complete" value. For example, when
+ * running a LIKE query the SQL wildcard character, %, should be part of the
+ * value, not the query itself. Thus, the following is incorrect:
+ * @code
+ * SELECT nid, title FROM {node} WHERE title LIKE :title%;
+ * @endcode
+ * It should instead read:
+ * @code
+ * SELECT nid, title FROM {node} WHERE title LIKE :title;
+ * @endcode
+ * and the value for :title should include a % as appropriate. Again, note the
+ * lack of quotation marks around :title. Because the value is not inserted
+ * into the query as one big string but as an explicitly separate value, the
+ * database server knows where the query ends and a value begins. That is
+ * considerably more secure against SQL injection than trying to remember
+ * which values need quotation marks and string escaping and which don't.
+ *
+ * INSERT, UPDATE, and DELETE queries need special care in order to behave
+ * consistently across all different databases. Therefore, they use a special
+ * object-oriented API for defining a query structurally. For example, rather
+ * than:
+ * @code
+ * INSERT INTO node (nid, title, body) VALUES (1, 'my title', 'my body');
+ * @endcode
+ * one would instead write:
+ * @code
+ * $fields = array('nid' => 1, 'title' => 'my title', 'body' => 'my body');
+ * db_insert('node')->fields($fields)->execute();
+ * @endcode
+ * This method allows databases that need special data type handling to do so,
+ * while also allowing optimizations such as multi-insert queries. UPDATE and
+ * DELETE queries have a similar pattern.
+ *
+ * Drupal also supports transactions, including a transparent fallback for
+ * databases that do not support transactions. To start a new transaction,
+ * simply call $txn = db_transaction(); in your own code. The transaction will
+ * remain open for as long as the variable $txn remains in scope. When $txn is
+ * destroyed, the transaction will be committed. If your transaction is nested
+ * inside of another then Drupal will track each transaction and only commit
+ * the outer-most transaction when the last transaction object goes out out of
+ * scope, that is, all relevant queries completed successfully.
+ *
+ * Example:
+ * @code
+ * function my_transaction_function() {
+ * // The transaction opens here.
+ * $txn = db_transaction();
+ *
+ * try {
+ * $id = db_insert('example')
+ * ->fields(array(
+ * 'field1' => 'mystring',
+ * 'field2' => 5,
+ * ))
+ * ->execute();
+ *
+ * my_other_function($id);
+ *
+ * return $id;
+ * }
+ * catch (Exception $e) {
+ * // Something went wrong somewhere, so roll back now.
+ * $txn->rollback();
+ * // Log the exception to watchdog.
+ * watchdog_exception('type', $e);
+ * }
+ *
+ * // $txn goes out of scope here. Unless the transaction was rolled back, it
+ * // gets automatically committed here.
+ * }
+ *
+ * function my_other_function($id) {
+ * // The transaction is still open here.
+ *
+ * if ($id % 2 == 0) {
+ * db_update('example')
+ * ->condition('id', $id)
+ * ->fields(array('field2' => 10))
+ * ->execute();
+ * }
+ * }
+ * @endcode
+ *
+ * @link http://drupal.org/developing/api/database
+ */
+
+
+/**
+ * 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 rolled back.
+ if (!in_array($savepoint_name, $this->transactionLayers)) {
+ return;
+ }
+
+ // 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;
+ }
+ if (!isset($this->transactionLayers[$name])) {
+ throw new DatabaseTransactionNoActiveException();
+ }
+
+ // 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 . '/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.
+ *
+ * They should never, ever have any database-specific code in them.
+ */
+
+/**
+ * Executes an arbitrary query string against the active database.
+ *
+ * Use this function for SELECT queries if it is just a simple query string.
+ * If the caller or other modules need to change the query, use db_select()
+ * instead.
+ *
+ * Do not use this function for INSERT, UPDATE, or DELETE queries. Those should
+ * be handled via db_insert(), db_update() and db_delete() respectively.
+ *
+ * @param $query
+ * The prepared statement query to run. Although it will accept both named and
+ * unnamed placeholders, named placeholders are strongly preferred as they are
+ * more self-documenting.
+ * @param $args
+ * An array of values to substitute into the query. If the query uses named
+ * placeholders, this is an associative array in any order. If the query uses
+ * unnamed placeholders (?), this is an indexed array and the order must match
+ * the order of placeholders in the query string.
+ * @param $options
+ * An array of options to control how the query operates.
+ *
+ * @return DatabaseStatementInterface
+ * A prepared statement object, already executed.
+ *
+ * @see DatabaseConnection::defaultOptions()
+ */
+function db_query($query, array $args = array(), array $options = array()) {
+ if (empty($options['target'])) {
+ $options['target'] = 'default';
+ }
+
+ return Database::getConnection($options['target'])->query($query, $args, $options);
+}
+
+/**
+ * Executes a query against the active database, restricted to a range.
+ *
+ * @param $query
+ * The prepared statement query to run. Although it will accept both named and
+ * unnamed placeholders, named placeholders are strongly preferred as they are
+ * more self-documenting.
+ * @param $from
+ * The first record from the result set to return.
+ * @param $count
+ * The number of records to return from the result set.
+ * @param $args
+ * An array of values to substitute into the query. If the query uses named
+ * placeholders, this is an associative array in any order. If the query uses
+ * unnamed placeholders (?), this is an indexed array and the order must match
+ * the order of placeholders in the query string.
+ * @param $options
+ * An array of options to control how the query operates.
+ *
+ * @return DatabaseStatementInterface
+ * A prepared statement object, already executed.
+ *
+ * @see DatabaseConnection::defaultOptions()
+ */
+function db_query_range($query, $from, $count, array $args = array(), array $options = array()) {
+ if (empty($options['target'])) {
+ $options['target'] = 'default';
+ }
+
+ return Database::getConnection($options['target'])->queryRange($query, $from, $count, $args, $options);
+}
+
+/**
+ * Executes a query string and saves the result set to a temporary table.
+ *
+ * The execution of the query string happens against the active database.
+ *
+ * @param $query
+ * The prepared statement query to run. Although it will accept both named and
+ * unnamed placeholders, named placeholders are strongly preferred as they are
+ * more self-documenting.
+ * @param $args
+ * An array of values to substitute into the query. If the query uses named
+ * placeholders, this is an associative array in any order. If the query uses
+ * unnamed placeholders (?), this is an indexed array and the order must match
+ * the order of placeholders in the query string.
+ * @param $options
+ * An array of options to control how the query operates.
+ *
+ * @return
+ * The name of the temporary table.
+ *
+ * @see DatabaseConnection::defaultOptions()
+ */
+function db_query_temporary($query, array $args = array(), array $options = array()) {
+ if (empty($options['target'])) {
+ $options['target'] = 'default';
+ }
+
+ return Database::getConnection($options['target'])->queryTemporary($query, $args, $options);
+}
+
+/**
+ * Returns a new InsertQuery object for the active database.
+ *
+ * @param $table
+ * The table into which to insert.
+ * @param $options
+ * An array of options to control how the query operates.
+ *
+ * @return InsertQuery
+ * A new InsertQuery object for this connection.
+ */
+function db_insert($table, array $options = array()) {
+ if (empty($options['target']) || $options['target'] == 'slave') {
+ $options['target'] = 'default';
+ }
+ return Database::getConnection($options['target'])->insert($table, $options);
+}
+
+/**
+ * Returns a new MergeQuery object for the active database.
+ *
+ * @param $table
+ * The table into which to merge.
+ * @param $options
+ * An array of options to control how the query operates.
+ *
+ * @return MergeQuery
+ * A new MergeQuery object for this connection.
+ */
+function db_merge($table, array $options = array()) {
+ if (empty($options['target']) || $options['target'] == 'slave') {
+ $options['target'] = 'default';
+ }
+ return Database::getConnection($options['target'])->merge($table, $options);
+}
+
+/**
+ * Returns a new UpdateQuery object for the active database.
+ *
+ * @param $table
+ * The table to update.
+ * @param $options
+ * An array of options to control how the query operates.
+ *
+ * @return UpdateQuery
+ * A new UpdateQuery object for this connection.
+ */
+function db_update($table, array $options = array()) {
+ if (empty($options['target']) || $options['target'] == 'slave') {
+ $options['target'] = 'default';
+ }
+ return Database::getConnection($options['target'])->update($table, $options);
+}
+
+/**
+ * Returns a new DeleteQuery object for the active database.
+ *
+ * @param $table
+ * The table from which to delete.
+ * @param $options
+ * An array of options to control how the query operates.
+ *
+ * @return DeleteQuery
+ * A new DeleteQuery object for this connection.
+ */
+function db_delete($table, array $options = array()) {
+ if (empty($options['target']) || $options['target'] == 'slave') {
+ $options['target'] = 'default';
+ }
+ return Database::getConnection($options['target'])->delete($table, $options);
+}
+
+/**
+ * Returns a new TruncateQuery object for the active database.
+ *
+ * @param $table
+ * The table from which to delete.
+ * @param $options
+ * An array of options to control how the query operates.
+ *
+ * @return TruncateQuery
+ * A new TruncateQuery object for this connection.
+ */
+function db_truncate($table, array $options = array()) {
+ if (empty($options['target']) || $options['target'] == 'slave') {
+ $options['target'] = 'default';
+ }
+ return Database::getConnection($options['target'])->truncate($table, $options);
+}
+
+/**
+ * Returns a new SelectQuery object for the active database.
+ *
+ * @param $table
+ * The base table for this query. 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 base table of this query.
+ * @param $options
+ * An array of options to control how the query operates.
+ *
+ * @return SelectQuery
+ * A new SelectQuery object for this connection.
+ */
+function db_select($table, $alias = NULL, array $options = array()) {
+ if (empty($options['target'])) {
+ $options['target'] = 'default';
+ }
+ return Database::getConnection($options['target'])->select($table, $alias, $options);
+}
+
+/**
+ * Returns a new transaction object for the active database.
+ *
+ * @param string $name
+ * Optional name of the transaction.
+ * @param array $options
+ * An array of options to control how the transaction operates:
+ * - target: The database target name.
+ *
+ * @return DatabaseTransaction
+ * A new DatabaseTransaction object for this connection.
+ */
+function db_transaction($name = NULL, array $options = array()) {
+ if (empty($options['target'])) {
+ $options['target'] = 'default';
+ }
+ return Database::getConnection($options['target'])->startTransaction($name);
+}
+
+/**
+ * Sets a new active database.
+ *
+ * @param $key
+ * The key in the $databases array to set as the default database.
+ *
+ * @return
+ * The key of the formerly active database.
+ */
+function db_set_active($key = 'default') {
+ return Database::setActiveConnection($key);
+}
+
+/**
+ * Restricts a dynamic table name to safe characters.
+ *
+ * Only keeps alphanumeric and underscores.
+ *
+ * @param $table
+ * The table name to escape.
+ *
+ * @return
+ * The escaped table name as a string.
+ */
+function db_escape_table($table) {
+ return Database::getConnection()->escapeTable($table);
+}
+
+/**
+ * Restricts a dynamic column or constraint name to safe characters.
+ *
+ * Only keeps alphanumeric and underscores.
+ *
+ * @param $field
+ * The field name to escape.
+ *
+ * @return
+ * The escaped field name as a string.
+ */
+function db_escape_field($field) {
+ return Database::getConnection()->escapeField($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.
+ */
+function db_like($string) {
+ return Database::getConnection()->escapeLike($string);
+}
+
+/**
+ * Retrieves the name of the currently active database driver.
+ *
+ * @return
+ * The name of the currently active database driver.
+ */
+function db_driver() {
+ return Database::getConnection()->driver();
+}
+
+/**
+ * Closes the active database connection.
+ *
+ * @param $options
+ * An array of options to control which connection is closed. Only the target
+ * key has any meaning in this case.
+ */
+function db_close(array $options = array()) {
+ if (empty($options['target'])) {
+ $options['target'] = NULL;
+ }
+ Database::closeConnection($options['target']);
+}
+
+/**
+ * Retrieves a unique id.
+ *
+ * Use this function if for some reason you can't use a serial field. Using a
+ * serial field is preferred, and InsertQuery::execute() returns the value of
+ * the last ID inserted.
+ *
+ * @param $existing_id
+ * After a database import, it might be that the sequences table is behind, so
+ * by passing in a minimum ID, it can be assured that we never issue the same
+ * ID.
+ *
+ * @return
+ * An integer number larger than any number returned before for this sequence.
+ */
+function db_next_id($existing_id = 0) {
+ return Database::getConnection()->nextId($existing_id);
+}
+
+/**
+ * Returns a new DatabaseCondition, set to "OR" all conditions together.
+ *
+ * @return DatabaseCondition
+ */
+function db_or() {
+ return new DatabaseCondition('OR');
+}
+
+/**
+ * Returns a new DatabaseCondition, set to "AND" all conditions together.
+ *
+ * @return DatabaseCondition
+ */
+function db_and() {
+ return new DatabaseCondition('AND');
+}
+
+/**
+ * Returns a new DatabaseCondition, set to "XOR" all conditions together.
+ *
+ * @return DatabaseCondition
+ */
+function db_xor() {
+ return new DatabaseCondition('XOR');
+}
+
+/**
+ * Returns a new DatabaseCondition, set to the specified conjunction.
+ *
+ * Internal API function call. The db_and(), db_or(), and db_xor()
+ * functions are preferred.
+ *
+ * @param $conjunction
+ * The conjunction to use for query conditions (AND, OR or XOR).
+ * @return DatabaseCondition
+ */
+function db_condition($conjunction) {
+ return new DatabaseCondition($conjunction);
+}
+
+/**
+ * @} End of "defgroup database".
+ */
+
+
+/**
+ * @ingroup schemaapi
+ * @{
+ */
+
+/**
+ * Creates a new table from a Drupal table definition.
+ *
+ * @param $name
+ * The name of the table to create.
+ * @param $table
+ * A Schema API table definition array.
+ */
+function db_create_table($name, $table) {
+ return Database::getConnection()->schema()->createTable($name, $table);
+}
+
+/**
+ * Returns an array of field names from an array of key/index column specifiers.
+ *
+ * This is usually an identity function but if a key/index uses a column prefix
+ * specification, this function extracts just the name.
+ *
+ * @param $fields
+ * An array of key/index column specifiers.
+ *
+ * @return
+ * An array of field names.
+ */
+function db_field_names($fields) {
+ return Database::getConnection()->schema()->fieldNames($fields);
+}
+
+/**
+ * Checks if an index exists in the given table.
+ *
+ * @param $table
+ * The name of the table in drupal (no prefixing).
+ * @param $name
+ * The name of the index in drupal (no prefixing).
+ *
+ * @return
+ * TRUE if the given index exists, otherwise FALSE.
+ */
+function db_index_exists($table, $name) {
+ return Database::getConnection()->schema()->indexExists($table, $name);
+}
+
+/**
+ * Checks if a table exists.
+ *
+ * @param $table
+ * The name of the table in drupal (no prefixing).
+ *
+ * @return
+ * TRUE if the given table exists, otherwise FALSE.
+ */
+function db_table_exists($table) {
+ return Database::getConnection()->schema()->tableExists($table);
+}
+
+/**
+ * Checks if a column exists in the given table.
+ *
+ * @param $table
+ * The name of the table in drupal (no prefixing).
+ * @param $field
+ * The name of the field.
+ *
+ * @return
+ * TRUE if the given column exists, otherwise FALSE.
+ */
+function db_field_exists($table, $field) {
+ return Database::getConnection()->schema()->fieldExists($table, $field);
+}
+
+/**
+ * Finds all tables that are like the specified base table name.
+ *
+ * @param $table_expression
+ * An SQL expression, for example "simpletest%" (without the quotes).
+ * BEWARE: this is not prefixed, the caller should take care of that.
+ *
+ * @return
+ * Array, both the keys and the values are the matching tables.
+ */
+function db_find_tables($table_expression) {
+ return Database::getConnection()->schema()->findTables($table_expression);
+}
+
+function _db_create_keys_sql($spec) {
+ return Database::getConnection()->schema()->createKeysSql($spec);
+}
+
+/**
+ * Renames a table.
+ *
+ * @param $table
+ * The table to be renamed.
+ * @param $new_name
+ * The new name for the table.
+ */
+function db_rename_table($table, $new_name) {
+ return Database::getConnection()->schema()->renameTable($table, $new_name);
+}
+
+/**
+ * Drops a table.
+ *
+ * @param $table
+ * The table to be dropped.
+ */
+function db_drop_table($table) {
+ return Database::getConnection()->schema()->dropTable($table);
+}
+
+/**
+ * Adds a new field to a table.
+ *
+ * @param $table
+ * Name of the table to be altered.
+ * @param $field
+ * Name of the field to be added.
+ * @param $spec
+ * The field specification array, as taken from a schema definition. The
+ * specification may also contain the key 'initial'; the newly-created field
+ * will be set to the value of the key in all rows. This is most useful for
+ * creating NOT NULL columns with no default value in existing tables.
+ * @param $keys_new
+ * Optional keys and indexes specification to be created on the table along
+ * with adding the field. The format is the same as a table specification, but
+ * without the 'fields' element. If you are adding a type 'serial' field, you
+ * MUST specify at least one key or index including it in this array. See
+ * db_change_field() for more explanation why.
+ *
+ * @see db_change_field()
+ */
+function db_add_field($table, $field, $spec, $keys_new = array()) {
+ return Database::getConnection()->schema()->addField($table, $field, $spec, $keys_new);
+}
+
+/**
+ * Drops a field.
+ *
+ * @param $table
+ * The table to be altered.
+ * @param $field
+ * The field to be dropped.
+ */
+function db_drop_field($table, $field) {
+ return Database::getConnection()->schema()->dropField($table, $field);
+}
+
+/**
+ * Sets the default value for a field.
+ *
+ * @param $table
+ * The table to be altered.
+ * @param $field
+ * The field to be altered.
+ * @param $default
+ * Default value to be set. NULL for 'default NULL'.
+ */
+function db_field_set_default($table, $field, $default) {
+ return Database::getConnection()->schema()->fieldSetDefault($table, $field, $default);
+}
+
+/**
+ * Sets a field to have no default value.
+ *
+ * @param $table
+ * The table to be altered.
+ * @param $field
+ * The field to be altered.
+ */
+function db_field_set_no_default($table, $field) {
+ return Database::getConnection()->schema()->fieldSetNoDefault($table, $field);
+}
+
+/**
+ * Adds a primary key to a database table.
+ *
+ * @param $table
+ * Name of the table to be altered.
+ * @param $fields
+ * Array of fields for the primary key.
+ */
+function db_add_primary_key($table, $fields) {
+ return Database::getConnection()->schema()->addPrimaryKey($table, $fields);
+}
+
+/**
+ * Drops the primary key of a database table.
+ *
+ * @param $table
+ * Name of the table to be altered.
+ */
+function db_drop_primary_key($table) {
+ return Database::getConnection()->schema()->dropPrimaryKey($table);
+}
+
+/**
+ * Adds a unique key.
+ *
+ * @param $table
+ * The table to be altered.
+ * @param $name
+ * The name of the key.
+ * @param $fields
+ * An array of field names.
+ */
+function db_add_unique_key($table, $name, $fields) {
+ return Database::getConnection()->schema()->addUniqueKey($table, $name, $fields);
+}
+
+/**
+ * Drops a unique key.
+ *
+ * @param $table
+ * The table to be altered.
+ * @param $name
+ * The name of the key.
+ */
+function db_drop_unique_key($table, $name) {
+ return Database::getConnection()->schema()->dropUniqueKey($table, $name);
+}
+
+/**
+ * Adds an index.
+ *
+ * @param $table
+ * The table to be altered.
+ * @param $name
+ * The name of the index.
+ * @param $fields
+ * An array of field names.
+ */
+function db_add_index($table, $name, $fields) {
+ return Database::getConnection()->schema()->addIndex($table, $name, $fields);
+}
+
+/**
+ * Drops an index.
+ *
+ * @param $table
+ * The table to be altered.
+ * @param $name
+ * The name of the index.
+ */
+function db_drop_index($table, $name) {
+ return Database::getConnection()->schema()->dropIndex($table, $name);
+}
+
+/**
+ * Changes a field definition.
+ *
+ * IMPORTANT NOTE: To maintain database portability, you have to explicitly
+ * recreate all indices and primary keys that are using the changed field.
+ *
+ * That means that you have to drop all affected keys and indexes with
+ * db_drop_{primary_key,unique_key,index}() before calling db_change_field().
+ * To recreate the keys and indices, pass the key definitions as the optional
+ * $keys_new argument directly to db_change_field().
+ *
+ * For example, suppose you have:
+ * @code
+ * $schema['foo'] = array(
+ * 'fields' => array(
+ * 'bar' => array('type' => 'int', 'not null' => TRUE)
+ * ),
+ * 'primary key' => array('bar')
+ * );
+ * @endcode
+ * and you want to change foo.bar to be type serial, leaving it as the primary
+ * key. The correct sequence is:
+ * @code
+ * db_drop_primary_key('foo');
+ * db_change_field('foo', 'bar', 'bar',
+ * array('type' => 'serial', 'not null' => TRUE),
+ * array('primary key' => array('bar')));
+ * @endcode
+ *
+ * The reasons for this are due to the different database engines:
+ *
+ * On PostgreSQL, changing a field definition involves adding a new field and
+ * dropping an old one which causes any indices, primary keys and sequences
+ * (from serial-type fields) that use the changed field to be dropped.
+ *
+ * On MySQL, all type 'serial' fields must be part of at least one key or index
+ * as soon as they are created. You cannot use
+ * db_add_{primary_key,unique_key,index}() for this purpose because the ALTER
+ * TABLE command will fail to add the column without a key or index
+ * specification. The solution is to use the optional $keys_new argument to
+ * create the key or index at the same time as field.
+ *
+ * You could use db_add_{primary_key,unique_key,index}() in all cases unless you
+ * are converting a field to be type serial. You can use the $keys_new argument
+ * in all cases.
+ *
+ * @param $table
+ * Name of the table.
+ * @param $field
+ * Name of the field to change.
+ * @param $field_new
+ * New name for the field (set to the same as $field if you don't want to
+ * change the name).
+ * @param $spec
+ * The field specification for the new field.
+ * @param $keys_new
+ * Optional keys and indexes specification to be created on the table along
+ * with changing the field. The format is the same as a table specification
+ * but without the 'fields' element.
+ */
+function db_change_field($table, $field, $field_new, $spec, $keys_new = array()) {
+ return Database::getConnection()->schema()->changeField($table, $field, $field_new, $spec, $keys_new);
+}
+
+/**
+ * @} End of "ingroup schemaapi".
+ */
+
+/**
+ * Sets a session variable specifying the lag time for ignoring a slave server.
+ */
+function db_ignore_slave() {
+ $connection_info = Database::getConnectionInfo();
+ // Only set ignore_slave_server if there are slave servers being used, which
+ // is assumed if there are more than one.
+ if (count($connection_info) > 1) {
+ // Five minutes is long enough to allow the slave to break and resume
+ // interrupted replication without causing problems on the Drupal site from
+ // the old data.
+ $duration = variable_get('maximum_replication_lag', 300);
+ // Set session variable with amount of time to delay before using slave.
+ $_SESSION['ignore_slave_server'] = REQUEST_TIME + $duration;
+ }
+}
diff --git a/core/includes/database/log.inc b/core/includes/database/log.inc
new file mode 100644
index 00000000000..ec27ef8e633
--- /dev/null
+++ b/core/includes/database/log.inc
@@ -0,0 +1,159 @@
+<?php
+
+/**
+ * @file
+ * Logging classes for the database layer.
+ */
+
+/**
+ * Database query logger.
+ *
+ * We log queries in a separate object rather than in the connection object
+ * because we want to be able to see all queries sent to a given database, not
+ * database target. If we logged the queries in each connection object we
+ * would not be able to track what queries went to which target.
+ *
+ * Every connection has one and only one logging object on it for all targets
+ * and logging keys.
+ */
+class DatabaseLog {
+
+ /**
+ * Cache of logged queries. This will only be used if the query logger is enabled.
+ *
+ * The structure for the logging array is as follows:
+ *
+ * array(
+ * $logging_key = array(
+ * array(query => '', args => array(), caller => '', target => '', time => 0),
+ * array(query => '', args => array(), caller => '', target => '', time => 0),
+ * ),
+ * );
+ *
+ * @var array
+ */
+ protected $queryLog = array();
+
+ /**
+ * The connection key for which this object is logging.
+ *
+ * @var string
+ */
+ protected $connectionKey = 'default';
+
+ /**
+ * Constructor.
+ *
+ * @param $key
+ * The database connection key for which to enable logging.
+ */
+ public function __construct($key = 'default') {
+ $this->connectionKey = $key;
+ }
+
+ /**
+ * Begin logging queries to the specified connection and logging key.
+ *
+ * If the specified logging key is already running this method does nothing.
+ *
+ * @param $logging_key
+ * The identification key for this log request. By specifying different
+ * logging keys we are able to start and stop multiple logging runs
+ * simultaneously without them colliding.
+ */
+ public function start($logging_key) {
+ if (empty($this->queryLog[$logging_key])) {
+ $this->clear($logging_key);
+ }
+ }
+
+ /**
+ * Retrieve the query log for the specified logging key so far.
+ *
+ * @param $logging_key
+ * The logging key to fetch.
+ * @return
+ * An indexed array of all query records for this logging key.
+ */
+ public function get($logging_key) {
+ return $this->queryLog[$logging_key];
+ }
+
+ /**
+ * Empty the query log for the specified logging key.
+ *
+ * This method does not stop logging, it simply clears the log. To stop
+ * logging, use the end() method.
+ *
+ * @param $logging_key
+ * The logging key to empty.
+ */
+ public function clear($logging_key) {
+ $this->queryLog[$logging_key] = array();
+ }
+
+ /**
+ * Stop logging for the specified logging key.
+ *
+ * @param $logging_key
+ * The logging key to stop.
+ */
+ public function end($logging_key) {
+ unset($this->queryLog[$logging_key]);
+ }
+
+ /**
+ * Log a query to all active logging keys.
+ *
+ * @param $statement
+ * The prepared statement object to log.
+ * @param $args
+ * The arguments passed to the statement object.
+ * @param $time
+ * The time in milliseconds the query took to execute.
+ */
+ public function log(DatabaseStatementInterface $statement, $args, $time) {
+ foreach (array_keys($this->queryLog) as $key) {
+ $this->queryLog[$key][] = array(
+ 'query' => $statement->getQueryString(),
+ 'args' => $args,
+ 'target' => $statement->dbh->getTarget(),
+ 'caller' => $this->findCaller(),
+ 'time' => $time,
+ );
+ }
+ }
+
+ /**
+ * Determine the routine that called this query.
+ *
+ * We define "the routine that called this query" as the first entry in
+ * the call stack that is not inside includes/database. That makes the
+ * climbing logic very simple, and handles the variable stack depth caused
+ * by the query builders.
+ *
+ * @link http://www.php.net/debug_backtrace
+ * @return
+ * This method returns a stack trace entry similar to that generated by
+ * debug_backtrace(). However, it flattens the trace entry and the trace
+ * entry before it so that we get the function and args of the function that
+ * called into the database system, not the function and args of the
+ * database call itself.
+ */
+ public function findCaller() {
+ $stack = debug_backtrace();
+ $stack_count = count($stack);
+ for ($i = 0; $i < $stack_count; ++$i) {
+ if (strpos($stack[$i]['file'], 'includes' . DIRECTORY_SEPARATOR . 'database') === FALSE) {
+ return array(
+ 'file' => $stack[$i]['file'],
+ 'line' => $stack[$i]['line'],
+ 'function' => $stack[$i + 1]['function'],
+ 'class' => isset($stack[$i + 1]['class']) ? $stack[$i + 1]['class'] : NULL,
+ 'type' => isset($stack[$i + 1]['type']) ? $stack[$i + 1]['type'] : NULL,
+ 'args' => $stack[$i + 1]['args'],
+ );
+ }
+ }
+ }
+}
diff --git a/core/includes/database/mysql/database.inc b/core/includes/database/mysql/database.inc
new file mode 100644
index 00000000000..7d5d85998db
--- /dev/null
+++ b/core/includes/database/mysql/database.inc
@@ -0,0 +1,187 @@
+<?php
+
+/**
+ * @file
+ * Database interface code for MySQL database servers.
+ */
+
+/**
+ * @ingroup database
+ * @{
+ */
+
+class DatabaseConnection_mysql extends DatabaseConnection {
+
+ /**
+ * Flag to indicate if we have registered the nextID cleanup function.
+ *
+ * @var boolean
+ */
+ protected $shutdownRegistered = FALSE;
+
+ public function __construct(array $connection_options = array()) {
+ // This driver defaults to transaction support, except if explicitly passed FALSE.
+ $this->transactionSupport = !isset($connection_options['transactions']) || ($connection_options['transactions'] !== FALSE);
+
+ // MySQL never supports transactional DDL.
+ $this->transactionalDDLSupport = FALSE;
+
+ $this->connectionOptions = $connection_options;
+
+ // The DSN should use either a socket or a host/port.
+ if (isset($connection_options['unix_socket'])) {
+ $dsn = 'mysql:unix_socket=' . $connection_options['unix_socket'];
+ }
+ else {
+ // Default to TCP connection on port 3306.
+ $dsn = 'mysql:host=' . $connection_options['host'] . ';port=' . (empty($connection_options['port']) ? 3306 : $connection_options['port']);
+ }
+ $dsn .= ';dbname=' . $connection_options['database'];
+ parent::__construct($dsn, $connection_options['username'], $connection_options['password'], array(
+ // So we don't have to mess around with cursors and unbuffered queries by default.
+ PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => TRUE,
+ // Because MySQL's prepared statements skip the query cache, because it's dumb.
+ PDO::ATTR_EMULATE_PREPARES => TRUE,
+ // Force column names to lower case.
+ PDO::ATTR_CASE => PDO::CASE_LOWER,
+ ));
+
+ // Force MySQL to use the UTF-8 character set. Also set the collation, if a
+ // certain one has been set; otherwise, MySQL defaults to 'utf8_general_ci'
+ // for UTF-8.
+ if (!empty($connection_options['collation'])) {
+ $this->exec('SET NAMES utf8 COLLATE ' . $connection_options['collation']);
+ }
+ else {
+ $this->exec('SET NAMES utf8');
+ }
+
+ // Force MySQL's behavior to conform more closely to SQL standards.
+ // This allows Drupal to run almost seamlessly on many different
+ // kinds of database systems. These settings force MySQL to behave
+ // the same as postgresql, or sqlite in regards to syntax interpretation
+ // and invalid data handling. See http://drupal.org/node/344575 for
+ // further discussion. Also, as MySQL 5.5 changed the meaning of
+ // TRADITIONAL we need to spell out the modes one by one.
+ $this->exec("SET sql_mode='ANSI,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER'");
+ }
+
+ public function queryRange($query, $from, $count, array $args = array(), array $options = array()) {
+ return $this->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options);
+ }
+
+ public function queryTemporary($query, array $args = array(), array $options = array()) {
+ $tablename = $this->generateTemporaryTableName();
+ $this->query(preg_replace('/^SELECT/i', 'CREATE TEMPORARY TABLE {' . $tablename . '} Engine=MEMORY SELECT', $query), $args, $options);
+ return $tablename;
+ }
+
+ public function driver() {
+ return 'mysql';
+ }
+
+ public function databaseType() {
+ return 'mysql';
+ }
+
+ public function mapConditionOperator($operator) {
+ // We don't want to override any of the defaults.
+ return NULL;
+ }
+
+ public function nextId($existing_id = 0) {
+ $new_id = $this->query('INSERT INTO {sequences} () VALUES ()', array(), array('return' => Database::RETURN_INSERT_ID));
+ // This should only happen after an import or similar event.
+ if ($existing_id >= $new_id) {
+ // If we INSERT a value manually into the sequences table, on the next
+ // INSERT, MySQL will generate a larger value. However, there is no way
+ // of knowing whether this value already exists in the table. MySQL
+ // provides an INSERT IGNORE which would work, but that can mask problems
+ // other than duplicate keys. Instead, we use INSERT ... ON DUPLICATE KEY
+ // UPDATE in such a way that the UPDATE does not do anything. This way,
+ // duplicate keys do not generate errors but everything else does.
+ $this->query('INSERT INTO {sequences} (value) VALUES (:value) ON DUPLICATE KEY UPDATE value = value', array(':value' => $existing_id));
+ $new_id = $this->query('INSERT INTO {sequences} () VALUES ()', array(), array('return' => Database::RETURN_INSERT_ID));
+ }
+ if (!$this->shutdownRegistered) {
+ // Use register_shutdown_function() here to keep the database system
+ // independent of Drupal.
+ register_shutdown_function(array($this, 'nextIdDelete'));
+ $shutdownRegistered = TRUE;
+ }
+ return $new_id;
+ }
+
+ public function nextIdDelete() {
+ // While we want to clean up the table to keep it up from occupying too
+ // much storage and memory, we must keep the highest value in the table
+ // because InnoDB uses an in-memory auto-increment counter as long as the
+ // server runs. When the server is stopped and restarted, InnoDB
+ // reinitializes the counter for each table for the first INSERT to the
+ // table based solely on values from the table so deleting all values would
+ // be a problem in this case. Also, TRUNCATE resets the auto increment
+ // counter.
+ try {
+ $max_id = $this->query('SELECT MAX(value) FROM {sequences}')->fetchField();
+ // We know we are using MySQL here, no need for the slower db_delete().
+ $this->query('DELETE FROM {sequences} WHERE value < :value', array(':value' => $max_id));
+ }
+ // During testing, this function is called from shutdown with the
+ // simpletest prefix stored in $this->connection, and those tables are gone
+ // by the time shutdown is called so we need to ignore the database
+ // errors. There is no problem with completely ignoring errors here: if
+ // these queries fail, the sequence will work just fine, just use a bit
+ // more database storage and memory.
+ catch (PDOException $e) {
+ }
+ }
+
+ /**
+ * Overridden to work around issues to MySQL not supporting transactional DDL.
+ */
+ protected function popCommittableTransactions() {
+ // Commit all the committable layers.
+ foreach (array_reverse($this->transactionLayers) as $name => $active) {
+ // Stop once we found an active transaction.
+ if ($active) {
+ break;
+ }
+
+ // If there are no more layers left then we should commit.
+ unset($this->transactionLayers[$name]);
+ if (empty($this->transactionLayers)) {
+ if (!PDO::commit()) {
+ throw new DatabaseTransactionCommitFailedException();
+ }
+ }
+ else {
+ // Attempt to release this savepoint in the standard way.
+ try {
+ $this->query('RELEASE SAVEPOINT ' . $name);
+ }
+ catch (PDOException $e) {
+ // However, in MySQL (InnoDB), savepoints are automatically committed
+ // when tables are altered or created (DDL transactions are not
+ // supported). This can cause exceptions due to trying to release
+ // savepoints which no longer exist.
+ //
+ // To avoid exceptions when no actual error has occurred, we silently
+ // succeed for MySQL error code 1305 ("SAVEPOINT does not exist").
+ if ($e->errorInfo[1] == '1305') {
+ // If one SAVEPOINT was released automatically, then all were.
+ // Therefore, we keep just the topmost transaction.
+ $this->transactionLayers = array('drupal_transaction' => 'drupal_transaction');
+ }
+ else {
+ throw $e;
+ }
+ }
+ }
+ }
+ }
+}
+
+
+/**
+ * @} End of "ingroup database".
+ */
diff --git a/core/includes/database/mysql/install.inc b/core/includes/database/mysql/install.inc
new file mode 100644
index 00000000000..75f2ae39050
--- /dev/null
+++ b/core/includes/database/mysql/install.inc
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * @file
+ * Installation code for MySQL embedded database engine.
+ */
+
+/**
+ * Specifies installation tasks for MySQL and equivalent databases.
+ */
+class DatabaseTasks_mysql extends DatabaseTasks {
+ /**
+ * The PDO driver name for MySQL and equivalent databases.
+ *
+ * @var string
+ */
+ protected $pdoDriver = 'mysql';
+
+ /**
+ * Returns a human-readable name string for MySQL and equivalent databases.
+ */
+ public function name() {
+ return st('MySQL, MariaDB, or equivalent');
+ }
+
+ /**
+ * Returns the minimum version for MySQL.
+ */
+ public function minimumVersion() {
+ return '5.0.15';
+ }
+}
+
diff --git a/core/includes/database/mysql/query.inc b/core/includes/database/mysql/query.inc
new file mode 100644
index 00000000000..888b6a5a450
--- /dev/null
+++ b/core/includes/database/mysql/query.inc
@@ -0,0 +1,107 @@
+<?php
+
+/**
+ * @ingroup database
+ * @{
+ */
+
+/**
+ * @file
+ * Query code for MySQL embedded database engine.
+ */
+
+
+class InsertQuery_mysql extends InsertQuery {
+
+ public function execute() {
+ if (!$this->preExecute()) {
+ return NULL;
+ }
+
+ // If we're selecting from a SelectQuery, finish building the query and
+ // pass it back, as any remaining options are irrelevant.
+ if (empty($this->fromQuery)) {
+ $max_placeholder = 0;
+ $values = array();
+ foreach ($this->insertValues as $insert_values) {
+ foreach ($insert_values as $value) {
+ $values[':db_insert_placeholder_' . $max_placeholder++] = $value;
+ }
+ }
+ }
+ else {
+ $values = $this->fromQuery->getArguments();
+ }
+
+ $last_insert_id = $this->connection->query((string) $this, $values, $this->queryOptions);
+
+ // Re-initialize the values array so that we can re-use this query.
+ $this->insertValues = array();
+
+ return $last_insert_id;
+ }
+
+ public function __toString() {
+ // Create a sanitized comment string to prepend to the query.
+ $comments = $this->connection->makeComment($this->comments);
+
+ // Default fields are always placed first for consistency.
+ $insert_fields = array_merge($this->defaultFields, $this->insertFields);
+
+ // If we're selecting from a SelectQuery, finish building the query and
+ // pass it back, as any remaining options are irrelevant.
+ if (!empty($this->fromQuery)) {
+ return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') ' . $this->fromQuery;
+ }
+
+ $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
+
+ $max_placeholder = 0;
+ $values = array();
+ if (count($this->insertValues)) {
+ foreach ($this->insertValues as $insert_values) {
+ $placeholders = array();
+
+ // Default fields aren't really placeholders, but this is the most convenient
+ // way to handle them.
+ $placeholders = array_pad($placeholders, count($this->defaultFields), 'default');
+
+ $new_placeholder = $max_placeholder + count($insert_values);
+ for ($i = $max_placeholder; $i < $new_placeholder; ++$i) {
+ $placeholders[] = ':db_insert_placeholder_' . $i;
+ }
+ $max_placeholder = $new_placeholder;
+ $values[] = '(' . implode(', ', $placeholders) . ')';
+ }
+ }
+ else {
+ // If there are no values, then this is a default-only query. We still need to handle that.
+ $placeholders = array_fill(0, count($this->defaultFields), 'default');
+ $values[] = '(' . implode(', ', $placeholders) . ')';
+ }
+
+ $query .= implode(', ', $values);
+
+ return $query;
+ }
+}
+
+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/schema.inc b/core/includes/database/mysql/schema.inc
new file mode 100644
index 00000000000..4e88fa169eb
--- /dev/null
+++ b/core/includes/database/mysql/schema.inc
@@ -0,0 +1,531 @@
+<?php
+
+/**
+ * @file
+ * Database schema code for MySQL database servers.
+ */
+
+
+/**
+ * @ingroup schemaapi
+ * @{
+ */
+
+class DatabaseSchema_mysql extends DatabaseSchema {
+
+ /**
+ * Maximum length of a table comment in MySQL.
+ */
+ const COMMENT_MAX_TABLE = 60;
+
+ /**
+ * Maximum length of a column comment in MySQL.
+ */
+ const COMMENT_MAX_COLUMN = 255;
+
+ /**
+ * Get information about the table and database name from the prefix.
+ *
+ * @return
+ * A keyed array with information about the database, table name and prefix.
+ */
+ protected function getPrefixInfo($table = 'default', $add_prefix = TRUE) {
+ $info = array('prefix' => $this->connection->tablePrefix($table));
+ if ($add_prefix) {
+ $table = $info['prefix'] . $table;
+ }
+ if (($pos = strpos($table, '.')) !== FALSE) {
+ $info['database'] = substr($table, 0, $pos);
+ $info['table'] = substr($table, ++$pos);
+ }
+ else {
+ $db_info = Database::getConnectionInfo();
+ $info['database'] = $db_info['default']['database'];
+ $info['table'] = $table;
+ }
+ return $info;
+ }
+
+ /**
+ * Build a condition to match a table name against a standard information_schema.
+ *
+ * MySQL uses databases like schemas rather than catalogs so when we build
+ * a condition to query the information_schema.tables, we set the default
+ * database as the schema unless specified otherwise, and exclude table_catalog
+ * from the condition criteria.
+ */
+ protected function buildTableNameCondition($table_name, $operator = '=', $add_prefix = TRUE) {
+ $info = $this->connection->getConnectionOptions();
+
+ $table_info = $this->getPrefixInfo($table_name, $add_prefix);
+
+ $condition = new DatabaseCondition('AND');
+ $condition->condition('table_schema', $table_info['database']);
+ $condition->condition('table_name', $table_info['table'], $operator);
+ return $condition;
+ }
+
+ /**
+ * Generate SQL to create a new table from a Drupal schema definition.
+ *
+ * @param $name
+ * The name of the table to create.
+ * @param $table
+ * A Schema API table definition array.
+ * @return
+ * An array of SQL statements to create the table.
+ */
+ protected function createTableSql($name, $table) {
+ $info = $this->connection->getConnectionOptions();
+
+ // Provide defaults if needed.
+ $table += array(
+ 'mysql_engine' => 'InnoDB',
+ 'mysql_character_set' => 'utf8',
+ );
+
+ $sql = "CREATE TABLE {" . $name . "} (\n";
+
+ // Add the SQL statement for each field.
+ foreach ($table['fields'] as $field_name => $field) {
+ $sql .= $this->createFieldSql($field_name, $this->processField($field)) . ", \n";
+ }
+
+ // Process keys & indexes.
+ $keys = $this->createKeysSql($table);
+ if (count($keys)) {
+ $sql .= implode(", \n", $keys) . ", \n";
+ }
+
+ // Remove the last comma and space.
+ $sql = substr($sql, 0, -3) . "\n) ";
+
+ $sql .= 'ENGINE = ' . $table['mysql_engine'] . ' DEFAULT CHARACTER SET ' . $table['mysql_character_set'];
+ // By default, MySQL uses the default collation for new tables, which is
+ // 'utf8_general_ci' for utf8. If an alternate collation has been set, it
+ // needs to be explicitly specified.
+ // @see DatabaseConnection_mysql
+ if (!empty($info['collation'])) {
+ $sql .= ' COLLATE ' . $info['collation'];
+ }
+
+ // Add table comment.
+ if (!empty($table['description'])) {
+ $sql .= ' COMMENT ' . $this->prepareComment($table['description'], self::COMMENT_MAX_TABLE);
+ }
+
+ return array($sql);
+ }
+
+ /**
+ * Create an SQL string for a field to be used in table creation or alteration.
+ *
+ * Before passing a field out of a schema definition into this function it has
+ * to be processed by _db_process_field().
+ *
+ * @param $name
+ * Name of the field.
+ * @param $spec
+ * The field specification, as per the schema data structure format.
+ */
+ protected function createFieldSql($name, $spec) {
+ $sql = "`" . $name . "` " . $spec['mysql_type'];
+
+ if (in_array($spec['mysql_type'], array('VARCHAR', 'CHAR', 'TINYTEXT', 'MEDIUMTEXT', 'LONGTEXT', 'TEXT')) && isset($spec['length'])) {
+ $sql .= '(' . $spec['length'] . ')';
+ }
+ elseif (isset($spec['precision']) && isset($spec['scale'])) {
+ $sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')';
+ }
+
+ if (!empty($spec['unsigned'])) {
+ $sql .= ' unsigned';
+ }
+
+ if (isset($spec['not null'])) {
+ if ($spec['not null']) {
+ $sql .= ' NOT NULL';
+ }
+ else {
+ $sql .= ' NULL';
+ }
+ }
+
+ if (!empty($spec['auto_increment'])) {
+ $sql .= ' auto_increment';
+ }
+
+ // $spec['default'] can be NULL, so we explicitly check for the key here.
+ if (array_key_exists('default', $spec)) {
+ if (is_string($spec['default'])) {
+ $spec['default'] = "'" . $spec['default'] . "'";
+ }
+ elseif (!isset($spec['default'])) {
+ $spec['default'] = 'NULL';
+ }
+ $sql .= ' DEFAULT ' . $spec['default'];
+ }
+
+ if (empty($spec['not null']) && !isset($spec['default'])) {
+ $sql .= ' DEFAULT NULL';
+ }
+
+ // Add column comment.
+ if (!empty($spec['description'])) {
+ $sql .= ' COMMENT ' . $this->prepareComment($spec['description'], self::COMMENT_MAX_COLUMN);
+ }
+
+ return $sql;
+ }
+
+ /**
+ * Set database-engine specific properties for a field.
+ *
+ * @param $field
+ * A field description array, as specified in the schema documentation.
+ */
+ protected function processField($field) {
+
+ if (!isset($field['size'])) {
+ $field['size'] = 'normal';
+ }
+
+ // Set the correct database-engine specific datatype.
+ // In case one is already provided, force it to uppercase.
+ if (isset($field['mysql_type'])) {
+ $field['mysql_type'] = drupal_strtoupper($field['mysql_type']);
+ }
+ else {
+ $map = $this->getFieldTypeMap();
+ $field['mysql_type'] = $map[$field['type'] . ':' . $field['size']];
+ }
+
+ if (isset($field['type']) && $field['type'] == 'serial') {
+ $field['auto_increment'] = TRUE;
+ }
+
+ return $field;
+ }
+
+ public function getFieldTypeMap() {
+ // Put :normal last so it gets preserved by array_flip. This makes
+ // it much easier for modules (such as schema.module) to map
+ // database types back into schema types.
+ // $map does not use drupal_static as its value never changes.
+ static $map = array(
+ 'varchar:normal' => 'VARCHAR',
+ 'char:normal' => 'CHAR',
+
+ 'text:tiny' => 'TINYTEXT',
+ 'text:small' => 'TINYTEXT',
+ 'text:medium' => 'MEDIUMTEXT',
+ 'text:big' => 'LONGTEXT',
+ 'text:normal' => 'TEXT',
+
+ 'serial:tiny' => 'TINYINT',
+ 'serial:small' => 'SMALLINT',
+ 'serial:medium' => 'MEDIUMINT',
+ 'serial:big' => 'BIGINT',
+ 'serial:normal' => 'INT',
+
+ 'int:tiny' => 'TINYINT',
+ 'int:small' => 'SMALLINT',
+ 'int:medium' => 'MEDIUMINT',
+ 'int:big' => 'BIGINT',
+ 'int:normal' => 'INT',
+
+ 'float:tiny' => 'FLOAT',
+ 'float:small' => 'FLOAT',
+ 'float:medium' => 'FLOAT',
+ 'float:big' => 'DOUBLE',
+ 'float:normal' => 'FLOAT',
+
+ 'numeric:normal' => 'DECIMAL',
+
+ 'blob:big' => 'LONGBLOB',
+ 'blob:normal' => 'BLOB',
+ );
+ return $map;
+ }
+
+ protected function createKeysSql($spec) {
+ $keys = array();
+
+ if (!empty($spec['primary key'])) {
+ $keys[] = 'PRIMARY KEY (' . $this->createKeysSqlHelper($spec['primary key']) . ')';
+ }
+ if (!empty($spec['unique keys'])) {
+ foreach ($spec['unique keys'] as $key => $fields) {
+ $keys[] = 'UNIQUE KEY `' . $key . '` (' . $this->createKeysSqlHelper($fields) . ')';
+ }
+ }
+ if (!empty($spec['indexes'])) {
+ foreach ($spec['indexes'] as $index => $fields) {
+ $keys[] = 'INDEX `' . $index . '` (' . $this->createKeysSqlHelper($fields) . ')';
+ }
+ }
+
+ return $keys;
+ }
+
+ protected function createKeySql($fields) {
+ $return = array();
+ foreach ($fields as $field) {
+ if (is_array($field)) {
+ $return[] = '`' . $field[0] . '`(' . $field[1] . ')';
+ }
+ else {
+ $return[] = '`' . $field . '`';
+ }
+ }
+ return implode(', ', $return);
+ }
+
+ protected function createKeysSqlHelper($fields) {
+ $return = array();
+ foreach ($fields as $field) {
+ if (is_array($field)) {
+ $return[] = '`' . $field[0] . '`(' . $field[1] . ')';
+ }
+ else {
+ $return[] = '`' . $field . '`';
+ }
+ }
+ return implode(', ', $return);
+ }
+
+ public function renameTable($table, $new_name) {
+ if (!$this->tableExists($table)) {
+ throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot rename %table to %table_new: table %table doesn't exist.", array('%table' => $table, '%table_new' => $new_name)));
+ }
+ if ($this->tableExists($new_name)) {
+ throw new DatabaseSchemaObjectExistsException(t("Cannot rename %table to %table_new: table %table_new already exists.", array('%table' => $table, '%table_new' => $new_name)));
+ }
+
+ $info = $this->getPrefixInfo($new_name);
+ return $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO `' . $info['table'] . '`');
+ }
+
+ public function dropTable($table) {
+ if (!$this->tableExists($table)) {
+ return FALSE;
+ }
+
+ $this->connection->query('DROP TABLE {' . $table . '}');
+ return TRUE;
+ }
+
+ public function addField($table, $field, $spec, $keys_new = array()) {
+ if (!$this->tableExists($table)) {
+ throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field %table.%field: table doesn't exist.", array('%field' => $field, '%table' => $table)));
+ }
+ if ($this->fieldExists($table, $field)) {
+ throw new DatabaseSchemaObjectExistsException(t("Cannot add field %table.%field: field already exists.", array('%field' => $field, '%table' => $table)));
+ }
+
+ $fixnull = FALSE;
+ if (!empty($spec['not null']) && !isset($spec['default'])) {
+ $fixnull = TRUE;
+ $spec['not null'] = FALSE;
+ }
+ $query = 'ALTER TABLE {' . $table . '} ADD ';
+ $query .= $this->createFieldSql($field, $this->processField($spec));
+ if ($keys_sql = $this->createKeysSql($keys_new)) {
+ $query .= ', ADD ' . implode(', ADD ', $keys_sql);
+ }
+ $this->connection->query($query);
+ if (isset($spec['initial'])) {
+ $this->connection->update($table)
+ ->fields(array($field => $spec['initial']))
+ ->execute();
+ }
+ if ($fixnull) {
+ $spec['not null'] = TRUE;
+ $this->changeField($table, $field, $field, $spec);
+ }
+ }
+
+ public function dropField($table, $field) {
+ if (!$this->fieldExists($table, $field)) {
+ return FALSE;
+ }
+
+ $this->connection->query('ALTER TABLE {' . $table . '} DROP `' . $field . '`');
+ return TRUE;
+ }
+
+ public function fieldSetDefault($table, $field, $default) {
+ if (!$this->fieldExists($table, $field)) {
+ throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot set default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field)));
+ }
+
+ if (!isset($default)) {
+ $default = 'NULL';
+ }
+ else {
+ $default = is_string($default) ? "'$default'" : $default;
+ }
+
+ $this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN `' . $field . '` SET DEFAULT ' . $default);
+ }
+
+ public function fieldSetNoDefault($table, $field) {
+ if (!$this->fieldExists($table, $field)) {
+ throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot remove default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field)));
+ }
+
+ $this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN `' . $field . '` DROP DEFAULT');
+ }
+
+ public function indexExists($table, $name) {
+ // Returns one row for each column in the index. Result is string or FALSE.
+ // Details at http://dev.mysql.com/doc/refman/5.0/en/show-index.html
+ $row = $this->connection->query('SHOW INDEX FROM {' . $table . "} WHERE key_name = '$name'")->fetchAssoc();
+ return isset($row['key_name']);
+ }
+
+ 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)));
+ }
+ if ($this->indexExists($table, 'PRIMARY')) {
+ throw new DatabaseSchemaObjectExistsException(t("Cannot add primary key to table %table: primary key already exists.", array('%table' => $table)));
+ }
+
+ $this->connection->query('ALTER TABLE {' . $table . '} ADD PRIMARY KEY (' . $this->createKeySql($fields) . ')');
+ }
+
+ public function dropPrimaryKey($table) {
+ if (!$this->indexExists($table, 'PRIMARY')) {
+ return FALSE;
+ }
+
+ $this->connection->query('ALTER TABLE {' . $table . '} DROP PRIMARY KEY');
+ return TRUE;
+ }
+
+ public function addUniqueKey($table, $name, $fields) {
+ if (!$this->tableExists($table)) {
+ throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add unique key %name to table %table: table doesn't exist.", array('%table' => $table, '%name' => $name)));
+ }
+ if ($this->indexExists($table, $name)) {
+ throw new DatabaseSchemaObjectExistsException(t("Cannot add unique key %name to table %table: unique key already exists.", array('%table' => $table, '%name' => $name)));
+ }
+
+ $this->connection->query('ALTER TABLE {' . $table . '} ADD UNIQUE KEY `' . $name . '` (' . $this->createKeySql($fields) . ')');
+ }
+
+ public function dropUniqueKey($table, $name) {
+ if (!$this->indexExists($table, $name)) {
+ return FALSE;
+ }
+
+ $this->connection->query('ALTER TABLE {' . $table . '} DROP KEY `' . $name . '`');
+ return TRUE;
+ }
+
+ public function addIndex($table, $name, $fields) {
+ if (!$this->tableExists($table)) {
+ throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add index %name to table %table: table doesn't exist.", array('%table' => $table, '%name' => $name)));
+ }
+ if ($this->indexExists($table, $name)) {
+ throw new DatabaseSchemaObjectExistsException(t("Cannot add index %name to table %table: index already exists.", array('%table' => $table, '%name' => $name)));
+ }
+
+ $this->connection->query('ALTER TABLE {' . $table . '} ADD INDEX `' . $name . '` (' . $this->createKeySql($fields) . ')');
+ }
+
+ public function dropIndex($table, $name) {
+ if (!$this->indexExists($table, $name)) {
+ return FALSE;
+ }
+
+ $this->connection->query('ALTER TABLE {' . $table . '} DROP INDEX `' . $name . '`');
+ return TRUE;
+ }
+
+ public function changeField($table, $field, $field_new, $spec, $keys_new = array()) {
+ if (!$this->fieldExists($table, $field)) {
+ throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot change the definition of field %table.%name: field doesn't exist.", array('%table' => $table, '%name' => $field)));
+ }
+ if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
+ throw new DatabaseSchemaObjectExistsException(t("Cannot rename field %table.%name to %name_new: target field already exists.", array('%table' => $table, '%name' => $field, '%name_new' => $field_new)));
+ }
+
+ $sql = 'ALTER TABLE {' . $table . '} CHANGE `' . $field . '` ' . $this->createFieldSql($field_new, $this->processField($spec));
+ if ($keys_sql = $this->createKeysSql($keys_new)) {
+ $sql .= ', ADD ' . implode(', ADD ', $keys_sql);
+ }
+ $this->connection->query($sql);
+ }
+
+ public function prepareComment($comment, $length = NULL) {
+ // Work around a bug in some versions of PDO, see http://bugs.php.net/bug.php?id=41125
+ $comment = str_replace("'", '’', $comment);
+
+ // Truncate comment to maximum comment length.
+ if (isset($length)) {
+ // Add table prefixes before truncating.
+ $comment = truncate_utf8($this->connection->prefixTables($comment), $length, TRUE, TRUE);
+ }
+
+ return $this->connection->quote($comment);
+ }
+
+ /**
+ * Retrieve a table or column comment.
+ */
+ public function getComment($table, $column = NULL) {
+ $condition = $this->buildTableNameCondition($table);
+ if (isset($column)) {
+ $condition->condition('column_name', $column);
+ $condition->compile($this->connection, $this);
+ // Don't use {} around information_schema.columns table.
+ return $this->connection->query("SELECT column_comment FROM information_schema.columns WHERE " . (string) $condition, $condition->arguments())->fetchField();
+ }
+ $condition->compile($this->connection, $this);
+ // Don't use {} around information_schema.tables table.
+ $comment = $this->connection->query("SELECT table_comment FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchField();
+ // Work-around for MySQL 5.0 bug http://bugs.mysql.com/bug.php?id=11379
+ return preg_replace('/; InnoDB free:.*$/', '', $comment);
+ }
+
+ public function tableExists($table) {
+ // The information_schema table is very slow to query under MySQL 5.0.
+ // Instead, we try to select from the table in question. If it fails,
+ // the most likely reason is that it does not exist. That is dramatically
+ // faster than using information_schema.
+ // @link http://bugs.mysql.com/bug.php?id=19588
+ // @todo: This override should be removed once we require a version of MySQL
+ // that has that bug fixed.
+ try {
+ $this->connection->queryRange("SELECT 1 FROM {" . $table . "}", 0, 1);
+ return TRUE;
+ }
+ catch (Exception $e) {
+ return FALSE;
+ }
+ }
+
+ public function fieldExists($table, $column) {
+ // The information_schema table is very slow to query under MySQL 5.0.
+ // Instead, we try to select from the table and field in question. If it
+ // fails, the most likely reason is that it does not exist. That is
+ // dramatically faster than using information_schema.
+ // @link http://bugs.mysql.com/bug.php?id=19588
+ // @todo: This override should be removed once we require a version of MySQL
+ // that has that bug fixed.
+ try {
+ $this->connection->queryRange("SELECT $column FROM {" . $table . "}", 0, 1);
+ return TRUE;
+ }
+ catch (Exception $e) {
+ return FALSE;
+ }
+ }
+
+}
+
+/**
+ * @} End of "ingroup schemaapi".
+ */
diff --git a/core/includes/database/pgsql/database.inc b/core/includes/database/pgsql/database.inc
new file mode 100644
index 00000000000..39b4e9b6960
--- /dev/null
+++ b/core/includes/database/pgsql/database.inc
@@ -0,0 +1,203 @@
+<?php
+
+/**
+ * @file
+ * Database interface code for PostgreSQL database servers.
+ */
+
+/**
+ * @ingroup database
+ * @{
+ */
+
+/**
+ * The name by which to obtain a lock for retrive the next insert id.
+ */
+define('POSTGRESQL_NEXTID_LOCK', 1000);
+
+class DatabaseConnection_pgsql extends DatabaseConnection {
+
+ public function __construct(array $connection_options = array()) {
+ // This driver defaults to transaction support, except if explicitly passed FALSE.
+ $this->transactionSupport = !isset($connection_options['transactions']) || ($connection_options['transactions'] !== FALSE);
+
+ // Transactional DDL is always available in PostgreSQL,
+ // but we'll only enable it if standard transactions are.
+ $this->transactionalDDLSupport = $this->transactionSupport;
+
+ // Default to TCP connection on port 5432.
+ if (empty($connection_options['port'])) {
+ $connection_options['port'] = 5432;
+ }
+
+ // PostgreSQL in trust mode doesn't require a password to be supplied.
+ if (empty($connection_options['password'])) {
+ $connection_options['password'] = NULL;
+ }
+ // If the password contains a backslash it is treated as an escape character
+ // http://bugs.php.net/bug.php?id=53217
+ // so backslashes in the password need to be doubled up.
+ // The bug was reported against pdo_pgsql 1.0.2, backslashes in passwords
+ // will break on this doubling up when the bug is fixed, so check the version
+ //elseif (phpversion('pdo_pgsql') < 'version_this_was_fixed_in') {
+ else {
+ $connection_options['password'] = str_replace('\\', '\\\\', $connection_options['password']);
+ }
+
+ $this->connectionOptions = $connection_options;
+
+ $dsn = 'pgsql:host=' . $connection_options['host'] . ' dbname=' . $connection_options['database'] . ' port=' . $connection_options['port'];
+ parent::__construct($dsn, $connection_options['username'], $connection_options['password'], array(
+ // Prepared statements are most effective for performance when queries
+ // are recycled (used several times). However, if they are not re-used,
+ // prepared statements become ineffecient. Since most of Drupal's
+ // prepared queries are not re-used, it should be faster to emulate
+ // the preparation than to actually ready statements for re-use. If in
+ // doubt, reset to FALSE and measure performance.
+ PDO::ATTR_EMULATE_PREPARES => TRUE,
+ // Convert numeric values to strings when fetching.
+ PDO::ATTR_STRINGIFY_FETCHES => TRUE,
+ // Force column names to lower case.
+ PDO::ATTR_CASE => PDO::CASE_LOWER,
+ ));
+
+ // Force PostgreSQL to use the UTF-8 character set by default.
+ $this->exec("SET NAMES 'UTF8'");
+ }
+
+ public function query($query, array $args = array(), $options = array()) {
+
+ $options += $this->defaultOptions();
+
+ // The PDO PostgreSQL driver has a bug which
+ // doesn't type cast booleans correctly when
+ // parameters are bound using associative
+ // arrays.
+ // See http://bugs.php.net/bug.php?id=48383
+ foreach ($args as &$value) {
+ if (is_bool($value)) {
+ $value = (int) $value;
+ }
+ }
+
+ try {
+ if ($query instanceof DatabaseStatementInterface) {
+ $stmt = $query;
+ $stmt->execute(NULL, $options);
+ }
+ else {
+ $this->expandArguments($query, $args);
+ $stmt = $this->prepareQuery($query);
+ $stmt->execute($args, $options);
+ }
+
+ switch ($options['return']) {
+ case Database::RETURN_STATEMENT:
+ return $stmt;
+ case Database::RETURN_AFFECTED:
+ return $stmt->rowCount();
+ case Database::RETURN_INSERT_ID:
+ return $this->lastInsertId($options['sequence_name']);
+ 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;
+ }
+ }
+
+ public function queryRange($query, $from, $count, array $args = array(), array $options = array()) {
+ return $this->query($query . ' LIMIT ' . (int) $count . ' OFFSET ' . (int) $from, $args, $options);
+ }
+
+ public function queryTemporary($query, array $args = array(), array $options = array()) {
+ $tablename = $this->generateTemporaryTableName();
+ $this->query(preg_replace('/^SELECT/i', 'CREATE TEMPORARY TABLE {' . $tablename . '} AS SELECT', $query), $args, $options);
+ return $tablename;
+ }
+
+ public function driver() {
+ return 'pgsql';
+ }
+
+ public function databaseType() {
+ return 'pgsql';
+ }
+
+ public function mapConditionOperator($operator) {
+ static $specials;
+
+ // Function calls not allowed in static declarations, thus this method.
+ if (!isset($specials)) {
+ $specials = array(
+ // In PostgreSQL, 'LIKE' is case-sensitive. For case-insensitive LIKE
+ // statements, we need to use ILIKE instead.
+ 'LIKE' => array('operator' => 'ILIKE'),
+ 'NOT LIKE' => array('operator' => 'NOT ILIKE'),
+ );
+ }
+
+ return isset($specials[$operator]) ? $specials[$operator] : NULL;
+ }
+
+ /**
+ * Retrive a the next id in a sequence.
+ *
+ * PostgreSQL has built in sequences. We'll use these instead of inserting
+ * and updating a sequences table.
+ */
+ public function nextId($existing = 0) {
+
+ // Retrive the name of the sequence. This information cannot be cached
+ // because the prefix may change, for example, like it does in simpletests.
+ $sequence_name = $this->makeSequenceName('sequences', 'value');
+
+ // When PostgreSQL gets a value too small then it will lock the table,
+ // retry the INSERT and if it's still too small then alter the sequence.
+ $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
+ if ($id > $existing) {
+ return $id;
+ }
+
+ // PostgreSQL advisory locks are simply locks to be used by an
+ // application such as Drupal. This will prevent other Drupal proccesses
+ // from altering the sequence while we are.
+ $this->query("SELECT pg_advisory_lock(" . 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 . ")");
+ return $id;
+ }
+
+ // Reset the sequence to a higher value than the existing id.
+ $this->query("ALTER SEQUENCE " . $sequence_name . " RESTART WITH " . ($existing + 1));
+
+ // 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 . ")");
+
+ return $id;
+ }
+}
+
+/**
+ * @} End of "ingroup database".
+ */
diff --git a/core/includes/database/pgsql/install.inc b/core/includes/database/pgsql/install.inc
new file mode 100644
index 00000000000..c350634ec40
--- /dev/null
+++ b/core/includes/database/pgsql/install.inc
@@ -0,0 +1,176 @@
+<?php
+
+/**
+ * @file
+ * Install functions for PostgreSQL embedded database engine.
+ */
+
+
+// PostgreSQL specific install functions
+
+class DatabaseTasks_pgsql extends DatabaseTasks {
+ protected $pdoDriver = 'pgsql';
+
+ public function __construct() {
+ $this->tasks[] = array(
+ 'function' => 'checkEncoding',
+ 'arguments' => array(),
+ );
+ $this->tasks[] = array(
+ 'function' => 'checkBinaryOutput',
+ 'arguments' => array(),
+ );
+ $this->tasks[] = array(
+ 'function' => 'initializeDatabase',
+ 'arguments' => array(),
+ );
+ }
+
+ public function name() {
+ return st('PostgreSQL');
+ }
+
+ public function minimumVersion() {
+ return '8.3';
+ }
+
+ /**
+ * Check encoding is UTF8.
+ */
+ protected function checkEncoding() {
+ try {
+ if (db_query('SHOW server_encoding')->fetchField() == 'UTF8') {
+ $this->pass(st('Database is encoded in UTF-8'));
+ }
+ else {
+ $replacements = array(
+ '%encoding' => 'UTF8',
+ '%driver' => $this->name(),
+ '!link' => '<a href="INSTALL.pgsql.txt">INSTALL.pgsql.txt</a>'
+ );
+ $text = 'The %driver database must use %encoding encoding to work with Drupal.';
+ $text .= 'Recreate the database with %encoding encoding. See !link for more details.';
+ $this->fail(st($text, $replacements));
+ }
+ }
+ catch (Exception $e) {
+ $this->fail(st('Drupal could not determine the encoding of the database was set to UTF-8'));
+ }
+ }
+
+ /**
+ * Check Binary Output.
+ *
+ * Unserializing does not work on Postgresql 9 when bytea_output is 'hex'.
+ */
+ function checkBinaryOutput() {
+ // PostgreSQL < 9 doesn't support bytea_output, so verify we are running
+ // at least PostgreSQL 9.
+ $database_connection = Database::getConnection();
+ if (version_compare($database_connection->version(), '9') >= 0) {
+ if (!$this->checkBinaryOutputSuccess()) {
+ // First try to alter the database. If it fails, raise an error telling
+ // the user to do it themselves.
+ $connection_options = $database_connection->getConnectionOptions();
+ // It is safe to include the database name directly here, because this
+ // code is only called when a connection to the database is already
+ // established, thus the database name is guaranteed to be a correct
+ // value.
+ $query = "ALTER DATABASE \"" . $connection_options['database'] . "\" SET bytea_output = 'escape';";
+ try {
+ db_query($query);
+ }
+ catch (Exception $e) {
+ // Ignore possible errors when the user doesn't have the necessary
+ // privileges to ALTER the database.
+ }
+
+ // Close the database connection so that the configuration parameter
+ // is applied to the current connection.
+ db_close();
+
+ // Recheck, if it fails, finally just rely on the end user to do the
+ // right thing.
+ if (!$this->checkBinaryOutputSuccess()) {
+ $replacements = array(
+ '%setting' => 'bytea_output',
+ '%current_value' => 'hex',
+ '%needed_value' => 'escape',
+ '!query' => "<code>" . $query . "</code>",
+ );
+ $this->fail(st("The %setting setting is currently set to '%current_value', but needs to be '%needed_value'. Change this by running the following query: !query", $replacements));
+ }
+ }
+ }
+ }
+
+ /**
+ * Verify that a binary data roundtrip returns the original string.
+ */
+ protected function checkBinaryOutputSuccess() {
+ $bytea_output = db_query("SELECT 'encoding'::bytea AS output")->fetchField();
+ return ($bytea_output == 'encoding');
+ }
+
+ /**
+ * Make PostgreSQL Drupal friendly.
+ */
+ function initializeDatabase() {
+ // We create some functions using global names instead of prefixing them
+ // like we do with table names. This is so that we don't double up if more
+ // than one instance of Drupal is running on a single database. We therefore
+ // avoid trying to create them again in that case.
+
+ try {
+ // Create functions.
+ db_query('CREATE OR REPLACE FUNCTION "greatest"(numeric, numeric) RETURNS numeric AS
+ \'SELECT CASE WHEN (($1 > $2) OR ($2 IS NULL)) THEN $1 ELSE $2 END;\'
+ LANGUAGE \'sql\''
+ );
+ db_query('CREATE OR REPLACE FUNCTION "greatest"(numeric, numeric, numeric) RETURNS numeric AS
+ \'SELECT greatest($1, greatest($2, $3));\'
+ LANGUAGE \'sql\''
+ );
+ // Don't use {} around pg_proc table.
+ if (!db_query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'rand'")->fetchField()) {
+ db_query('CREATE OR REPLACE FUNCTION "rand"() RETURNS float AS
+ \'SELECT random();\'
+ LANGUAGE \'sql\''
+ );
+ }
+
+ db_query('CREATE OR REPLACE FUNCTION "substring_index"(text, text, integer) RETURNS text AS
+ \'SELECT array_to_string((string_to_array($1, $2)) [1:$3], $2);\'
+ LANGUAGE \'sql\''
+ );
+
+ // Using || to concatenate in Drupal is not recommeneded because there are
+ // database drivers for Drupal that do not support the syntax, however
+ // they do support CONCAT(item1, item2) which we can replicate in
+ // PostgreSQL. PostgreSQL requires the function to be defined for each
+ // different argument variation the function can handle.
+ db_query('CREATE OR REPLACE FUNCTION "concat"(anynonarray, anynonarray) RETURNS text AS
+ \'SELECT CAST($1 AS text) || CAST($2 AS text);\'
+ LANGUAGE \'sql\'
+ ');
+ db_query('CREATE OR REPLACE FUNCTION "concat"(text, anynonarray) RETURNS text AS
+ \'SELECT $1 || CAST($2 AS text);\'
+ LANGUAGE \'sql\'
+ ');
+ db_query('CREATE OR REPLACE FUNCTION "concat"(anynonarray, text) RETURNS text AS
+ \'SELECT CAST($1 AS text) || $2;\'
+ LANGUAGE \'sql\'
+ ');
+ db_query('CREATE OR REPLACE FUNCTION "concat"(text, text) RETURNS text AS
+ \'SELECT $1 || $2;\'
+ LANGUAGE \'sql\'
+ ');
+
+ $this->pass(st('PostgreSQL has initialized itself.'));
+ }
+ catch (Exception $e) {
+ $this->fail(st('Drupal could not be correctly setup with the existing database. Revise any errors.'));
+ }
+ }
+}
+
diff --git a/core/includes/database/pgsql/query.inc b/core/includes/database/pgsql/query.inc
new file mode 100644
index 00000000000..f3783a9ca8f
--- /dev/null
+++ b/core/includes/database/pgsql/query.inc
@@ -0,0 +1,209 @@
+<?php
+
+/**
+ * @ingroup database
+ * @{
+ */
+
+/**
+ * @file
+ * Query code for PostgreSQL embedded database engine.
+ */
+
+
+class InsertQuery_pgsql extends InsertQuery {
+
+ public function execute() {
+ if (!$this->preExecute()) {
+ return NULL;
+ }
+
+ $stmt = $this->connection->prepareQuery((string) $this);
+
+ // Fetch the list of blobs and sequences used on that table.
+ $table_information = $this->connection->schema()->queryTableInformation($this->table);
+
+ $max_placeholder = 0;
+ $blobs = array();
+ $blob_count = 0;
+ foreach ($this->insertValues as $insert_values) {
+ foreach ($this->insertFields as $idx => $field) {
+ if (isset($table_information->blob_fields[$field])) {
+ $blobs[$blob_count] = fopen('php://memory', 'a');
+ fwrite($blobs[$blob_count], $insert_values[$idx]);
+ rewind($blobs[$blob_count]);
+
+ $stmt->bindParam(':db_insert_placeholder_' . $max_placeholder++, $blobs[$blob_count], PDO::PARAM_LOB);
+
+ // Pre-increment is faster in PHP than increment.
+ ++$blob_count;
+ }
+ else {
+ $stmt->bindParam(':db_insert_placeholder_' . $max_placeholder++, $insert_values[$idx]);
+ }
+ }
+ // Check if values for a serial field has been passed.
+ if (!empty($table_information->serial_fields)) {
+ foreach ($table_information->serial_fields as $index => $serial_field) {
+ $serial_key = array_search($serial_field, $this->insertFields);
+ if ($serial_key !== FALSE) {
+ $serial_value = $insert_values[$serial_key];
+
+ // Force $last_insert_id to the specified value. This is only done
+ // if $index is 0.
+ if ($index == 0) {
+ $last_insert_id = $serial_value;
+ }
+ // Set the sequence to the bigger value of either the passed
+ // value or the max value of the column. It can happen that another
+ // thread calls nextval() which could lead to a serial number being
+ // used twice. However, trying to insert a value into a serial
+ // column should only be done in very rare cases and is not thread
+ // safe by definition.
+ $this->connection->query("SELECT setval('" . $table_information->sequences[$index] . "', GREATEST(MAX(" . $serial_field . "), :serial_value)) FROM {" . $this->table . "}", array(':serial_value' => (int)$serial_value));
+ }
+ }
+ }
+ }
+ if (!empty($this->fromQuery)) {
+ // bindParam stores only a reference to the variable that is followed when
+ // the statement is executed. We pass $arguments[$key] instead of $value
+ // because the second argument to bindParam is passed by reference and
+ // the foreach statement assigns the element to the existing reference.
+ $arguments = $this->fromQuery->getArguments();
+ foreach ($arguments as $key => $value) {
+ $stmt->bindParam($key, $arguments[$key]);
+ }
+ }
+
+ // PostgreSQL requires the table name to be specified explicitly
+ // when requesting the last insert ID, so we pass that in via
+ // the options array.
+ $options = $this->queryOptions;
+
+ if (!empty($table_information->sequences)) {
+ $options['sequence_name'] = $table_information->sequences[0];
+ }
+ // If there are no sequences then we can't get a last insert id.
+ elseif ($options['return'] == Database::RETURN_INSERT_ID) {
+ $options['return'] = Database::RETURN_NULL;
+ }
+ // Only use the returned last_insert_id if it is not already set.
+ if (!empty($last_insert_id)) {
+ $this->connection->query($stmt, array(), $options);
+ }
+ else {
+ $last_insert_id = $this->connection->query($stmt, array(), $options);
+ }
+
+ // Re-initialize the values array so that we can re-use this query.
+ $this->insertValues = array();
+
+ return $last_insert_id;
+ }
+
+ public function __toString() {
+ // Create a sanitized comment string to prepend to the query.
+ $comments = $this->connection->makeComment($this->comments);
+
+ // Default fields are always placed first for consistency.
+ $insert_fields = array_merge($this->defaultFields, $this->insertFields);
+
+ // If we're selecting from a SelectQuery, finish building the query and
+ // pass it back, as any remaining options are irrelevant.
+ if (!empty($this->fromQuery)) {
+ return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') ' . $this->fromQuery;
+ }
+
+ $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
+
+ $max_placeholder = 0;
+ $values = array();
+ if (count($this->insertValues)) {
+ foreach ($this->insertValues as $insert_values) {
+ $placeholders = array();
+
+ // Default fields aren't really placeholders, but this is the most convenient
+ // way to handle them.
+ $placeholders = array_pad($placeholders, count($this->defaultFields), 'default');
+
+ $new_placeholder = $max_placeholder + count($insert_values);
+ for ($i = $max_placeholder; $i < $new_placeholder; ++$i) {
+ $placeholders[] = ':db_insert_placeholder_' . $i;
+ }
+ $max_placeholder = $new_placeholder;
+ $values[] = '(' . implode(', ', $placeholders) . ')';
+ }
+ }
+ else {
+ // If there are no values, then this is a default-only query. We still need to handle that.
+ $placeholders = array_fill(0, count($this->defaultFields), 'default');
+ $values[] = '(' . implode(', ', $placeholders) . ')';
+ }
+
+ $query .= implode(', ', $values);
+
+ return $query;
+ }
+}
+
+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/schema.inc b/core/includes/database/pgsql/schema.inc
new file mode 100644
index 00000000000..9ed8a262032
--- /dev/null
+++ b/core/includes/database/pgsql/schema.inc
@@ -0,0 +1,617 @@
+<?php
+
+/**
+ * @file
+ * Database schema code for PostgreSQL database servers.
+ */
+
+/**
+ * @ingroup schemaapi
+ * @{
+ */
+
+class DatabaseSchema_pgsql extends DatabaseSchema {
+
+ /**
+ * A cache of information about blob columns and sequences of tables.
+ *
+ * This is collected by DatabaseConnection_pgsql->queryTableInformation(),
+ * by introspecting the database.
+ *
+ * @see DatabaseConnection_pgsql->queryTableInformation()
+ * @var array
+ */
+ protected $tableInformation = array();
+
+ /**
+ * Fetch the list of blobs and sequences used on a table.
+ *
+ * We introspect the database to collect the information required by insert
+ * and update queries.
+ *
+ * @param $table_name
+ * The non-prefixed name of the table.
+ * @return
+ * An object with two member variables:
+ * - 'blob_fields' that lists all the blob fields in the table.
+ * - 'sequences' that lists the sequences used in that table.
+ */
+ public function queryTableInformation($table) {
+ // Generate a key to reference this table's information on.
+ $key = $this->connection->prefixTables('{' . $table . '}');
+ if (!strpos($key, '.')) {
+ $key = 'public.' . $key;
+ }
+
+ if (!isset($this->tableInformation[$key])) {
+ // Split the key into schema and table for querying.
+ list($schema, $table_name) = explode('.', $key);
+ $table_information = (object) array(
+ 'blob_fields' => array(),
+ 'sequences' => array(),
+ );
+ // Don't use {} around information_schema.columns table.
+ $result = $this->connection->query("SELECT column_name, data_type, column_default FROM information_schema.columns WHERE table_schema = :schema AND table_name = :table AND (data_type = 'bytea' OR (numeric_precision IS NOT NULL AND column_default LIKE :default))", array(
+ ':schema' => $schema,
+ ':table' => $table_name,
+ ':default' => '%nextval%',
+ ));
+ foreach ($result as $column) {
+ if ($column->data_type == 'bytea') {
+ $table_information->blob_fields[$column->column_name] = TRUE;
+ }
+ elseif (preg_match("/nextval\('([^']+)'/", $column->column_default, $matches)) {
+ // We must know of any sequences in the table structure to help us
+ // return the last insert id. If there is more than 1 sequences the
+ // first one (index 0 of the sequences array) will be used.
+ $table_information->sequences[] = $matches[1];
+ $table_information->serial_fields[] = $column->column_name;
+ }
+ }
+ $this->tableInformation[$key] = $table_information;
+ }
+ return $this->tableInformation[$key];
+ }
+
+ /**
+ * Fetch the list of CHECK constraints used on a field.
+ *
+ * We introspect the database to collect the information required by field
+ * alteration.
+ *
+ * @param $table
+ * The non-prefixed name of the table.
+ * @param $field
+ * The name of the field.
+ * @return
+ * An array of all the checks for the field.
+ */
+ public function queryFieldInformation($table, $field) {
+ $prefixInfo = $this->getPrefixInfo($table, TRUE);
+
+ // Split the key into schema and table for querying.
+ $schema = $prefixInfo['schema'];
+ $table_name = $prefixInfo['table'];
+
+ $field_information = (object) array(
+ 'checks' => array(),
+ );
+ $checks = $this->connection->query("SELECT conname FROM pg_class cl INNER JOIN pg_constraint co ON co.conrelid = cl.oid INNER JOIN pg_attribute attr ON attr.attrelid = cl.oid AND attr.attnum = ANY (co.conkey) INNER JOIN pg_namespace ns ON cl.relnamespace = ns.oid WHERE co.contype = 'c' AND ns.nspname = :schema AND cl.relname = :table AND attr.attname = :column", array(
+ ':schema' => $schema,
+ ':table' => $table_name,
+ ':column' => $field,
+ ));
+ $field_information = $checks->fetchCol();
+
+ return $field_information;
+ }
+
+ /**
+ * Generate SQL to create a new table from a Drupal schema definition.
+ *
+ * @param $name
+ * The name of the table to create.
+ * @param $table
+ * A Schema API table definition array.
+ * @return
+ * An array of SQL statements to create the table.
+ */
+ protected function createTableSql($name, $table) {
+ $sql_fields = array();
+ foreach ($table['fields'] as $field_name => $field) {
+ $sql_fields[] = $this->createFieldSql($field_name, $this->processField($field));
+ }
+
+ $sql_keys = array();
+ if (isset($table['primary key']) && is_array($table['primary key'])) {
+ $sql_keys[] = 'PRIMARY KEY (' . implode(', ', $table['primary key']) . ')';
+ }
+ if (isset($table['unique keys']) && is_array($table['unique keys'])) {
+ foreach ($table['unique keys'] as $key_name => $key) {
+ $sql_keys[] = 'CONSTRAINT ' . $this->prefixNonTable($name, $key_name, 'key') . ' UNIQUE (' . implode(', ', $key) . ')';
+ }
+ }
+
+ $sql = "CREATE TABLE {" . $name . "} (\n\t";
+ $sql .= implode(",\n\t", $sql_fields);
+ if (count($sql_keys) > 0) {
+ $sql .= ",\n\t";
+ }
+ $sql .= implode(",\n\t", $sql_keys);
+ $sql .= "\n)";
+ $statements[] = $sql;
+
+ if (isset($table['indexes']) && is_array($table['indexes'])) {
+ foreach ($table['indexes'] as $key_name => $key) {
+ $statements[] = $this->_createIndexSql($name, $key_name, $key);
+ }
+ }
+
+ // Add table comment.
+ if (!empty($table['description'])) {
+ $statements[] = 'COMMENT ON TABLE {' . $name . '} IS ' . $this->prepareComment($table['description']);
+ }
+
+ // Add column comments.
+ foreach ($table['fields'] as $field_name => $field) {
+ if (!empty($field['description'])) {
+ $statements[] = 'COMMENT ON COLUMN {' . $name . '}.' . $field_name . ' IS ' . $this->prepareComment($field['description']);
+ }
+ }
+
+ return $statements;
+ }
+
+ /**
+ * Create an SQL string for a field to be used in table creation or
+ * alteration.
+ *
+ * Before passing a field out of a schema definition into this
+ * function it has to be processed by _db_process_field().
+ *
+ * @param $name
+ * Name of the field.
+ * @param $spec
+ * The field specification, as per the schema data structure format.
+ */
+ protected function createFieldSql($name, $spec) {
+ $sql = $name . ' ' . $spec['pgsql_type'];
+
+ if (isset($spec['type']) && $spec['type'] == 'serial') {
+ unset($spec['not null']);
+ }
+
+ if (in_array($spec['pgsql_type'], array('varchar', 'character', 'text')) && isset($spec['length'])) {
+ $sql .= '(' . $spec['length'] . ')';
+ }
+ elseif (isset($spec['precision']) && isset($spec['scale'])) {
+ $sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')';
+ }
+
+ if (!empty($spec['unsigned'])) {
+ $sql .= " CHECK ($name >= 0)";
+ }
+
+ if (isset($spec['not null'])) {
+ if ($spec['not null']) {
+ $sql .= ' NOT NULL';
+ }
+ else {
+ $sql .= ' NULL';
+ }
+ }
+ if (isset($spec['default'])) {
+ $default = is_string($spec['default']) ? "'" . $spec['default'] . "'" : $spec['default'];
+ $sql .= " default $default";
+ }
+
+ return $sql;
+ }
+
+ /**
+ * Set database-engine specific properties for a field.
+ *
+ * @param $field
+ * A field description array, as specified in the schema documentation.
+ */
+ protected function processField($field) {
+ if (!isset($field['size'])) {
+ $field['size'] = 'normal';
+ }
+
+ // Set the correct database-engine specific datatype.
+ // In case one is already provided, force it to lowercase.
+ if (isset($field['pgsql_type'])) {
+ $field['pgsql_type'] = drupal_strtolower($field['pgsql_type']);
+ }
+ else {
+ $map = $this->getFieldTypeMap();
+ $field['pgsql_type'] = $map[$field['type'] . ':' . $field['size']];
+ }
+
+ if (!empty($field['unsigned'])) {
+ // Unsigned datatypes are not supported in PostgreSQL 8.3. In MySQL,
+ // they are used to ensure a positive number is inserted and it also
+ // doubles the maximum integer size that can be stored in a field.
+ // The PostgreSQL schema in Drupal creates a check constraint
+ // to ensure that a value inserted is >= 0. To provide the extra
+ // integer capacity, here, we bump up the column field size.
+ if (!isset($map)) {
+ $map = $this->getFieldTypeMap();
+ }
+ switch ($field['pgsql_type']) {
+ case 'smallint':
+ $field['pgsql_type'] = $map['int:medium'];
+ break;
+ case 'int' :
+ $field['pgsql_type'] = $map['int:big'];
+ break;
+ }
+ }
+ if (isset($field['type']) && $field['type'] == 'serial') {
+ unset($field['not null']);
+ }
+ return $field;
+ }
+
+ /**
+ * This maps a generic data type in combination with its data size
+ * to the engine-specific data type.
+ */
+ function getFieldTypeMap() {
+ // Put :normal last so it gets preserved by array_flip. This makes
+ // it much easier for modules (such as schema.module) to map
+ // database types back into schema types.
+ // $map does not use drupal_static as its value never changes.
+ static $map = array(
+ 'varchar:normal' => 'varchar',
+ 'char:normal' => 'character',
+
+ 'text:tiny' => 'text',
+ 'text:small' => 'text',
+ 'text:medium' => 'text',
+ 'text:big' => 'text',
+ 'text:normal' => 'text',
+
+ 'int:tiny' => 'smallint',
+ 'int:small' => 'smallint',
+ 'int:medium' => 'int',
+ 'int:big' => 'bigint',
+ 'int:normal' => 'int',
+
+ 'float:tiny' => 'real',
+ 'float:small' => 'real',
+ 'float:medium' => 'real',
+ 'float:big' => 'double precision',
+ 'float:normal' => 'real',
+
+ 'numeric:normal' => 'numeric',
+
+ 'blob:big' => 'bytea',
+ 'blob:normal' => 'bytea',
+
+ 'serial:tiny' => 'serial',
+ 'serial:small' => 'serial',
+ 'serial:medium' => 'serial',
+ 'serial:big' => 'bigserial',
+ 'serial:normal' => 'serial',
+ );
+ return $map;
+ }
+
+ protected function _createKeySql($fields) {
+ $return = array();
+ foreach ($fields as $field) {
+ if (is_array($field)) {
+ $return[] = 'substr(' . $field[0] . ', 1, ' . $field[1] . ')';
+ }
+ else {
+ $return[] = '"' . $field . '"';
+ }
+ }
+ return implode(', ', $return);
+ }
+
+ 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)));
+ }
+ 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)));
+ }
+
+ // Get the schema and tablename for the old table.
+ $old_full_name = $this->connection->prefixTables('{' . $table . '}');
+ list($old_schema, $old_table_name) = strpos($old_full_name, '.') ? explode('.', $old_full_name) : array('public', $old_full_name);
+
+ // Index names and constraint names are global in PostgreSQL, so we need to
+ // rename them when renaming the table.
+ $indexes = $this->connection->query('SELECT indexname FROM pg_indexes WHERE schemaname = :schema AND tablename = :table', array(':schema' => $old_schema, ':table' => $old_table_name));
+ foreach ($indexes as $index) {
+ if (preg_match('/^' . preg_quote($old_full_name) . '_(.*)_idx$/', $index->indexname, $matches)) {
+ $index_name = $matches[1];
+ $this->connection->query('ALTER INDEX ' . $index->indexname . ' RENAME TO {' . $new_name . '}_' . $index_name . '_idx');
+ }
+ }
+
+ // Now rename the table.
+ // Ensure the new table name does not include schema syntax.
+ $prefixInfo = $this->getPrefixInfo($new_name);
+ $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO ' . $prefixInfo['table']);
+ }
+
+ public function dropTable($table) {
+ if (!$this->tableExists($table)) {
+ return FALSE;
+ }
+
+ $this->connection->query('DROP TABLE {' . $table . '}');
+ return TRUE;
+ }
+
+ public function addField($table, $field, $spec, $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)));
+ }
+ if ($this->fieldExists($table, $field)) {
+ throw new DatabaseSchemaObjectExistsException(t("Cannot add field %table.%field: field already exists.", array('%field' => $field, '%table' => $table)));
+ }
+
+ $fixnull = FALSE;
+ if (!empty($spec['not null']) && !isset($spec['default'])) {
+ $fixnull = TRUE;
+ $spec['not null'] = FALSE;
+ }
+ $query = 'ALTER TABLE {' . $table . '} ADD COLUMN ';
+ $query .= $this->createFieldSql($field, $this->processField($spec));
+ $this->connection->query($query);
+ if (isset($spec['initial'])) {
+ $this->connection->update($table)
+ ->fields(array($field => $spec['initial']))
+ ->execute();
+ }
+ if ($fixnull) {
+ $this->connection->query("ALTER TABLE {" . $table . "} ALTER $field SET NOT NULL");
+ }
+ if (isset($new_keys)) {
+ $this->_createKeys($table, $new_keys);
+ }
+ // Add column comment.
+ if (!empty($spec['description'])) {
+ $this->connection->query('COMMENT ON COLUMN {' . $table . '}.' . $field . ' IS ' . $this->prepareComment($spec['description']));
+ }
+ }
+
+ public function dropField($table, $field) {
+ if (!$this->fieldExists($table, $field)) {
+ return FALSE;
+ }
+
+ $this->connection->query('ALTER TABLE {' . $table . '} DROP COLUMN "' . $field . '"');
+ return TRUE;
+ }
+
+ 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)));
+ }
+
+ if (!isset($default)) {
+ $default = 'NULL';
+ }
+ else {
+ $default = is_string($default) ? "'$default'" : $default;
+ }
+
+ $this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN "' . $field . '" SET DEFAULT ' . $default);
+ }
+
+ public function fieldSetNoDefault($table, $field) {
+ if (!$this->fieldExists($table, $field)) {
+ throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot remove default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field)));
+ }
+
+ $this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN "' . $field . '" DROP DEFAULT');
+ }
+
+ public function indexExists($table, $name) {
+ // Details http://www.postgresql.org/docs/8.3/interactive/view-pg-indexes.html
+ $index_name = '{' . $table . '}_' . $name . '_idx';
+ return (bool) $this->connection->query("SELECT 1 FROM pg_indexes WHERE indexname = '$index_name'")->fetchField();
+ }
+
+ /**
+ * Helper function: check if a constraint (PK, FK, UK) exists.
+ *
+ * @param $table
+ * The name of the table.
+ * @param $name
+ * The name of the constraint (typically 'pkey' or '[constraint]_key').
+ */
+ protected function constraintExists($table, $name) {
+ $constraint_name = '{' . $table . '}_' . $name;
+ return (bool) $this->connection->query("SELECT 1 FROM pg_constraint WHERE conname = '$constraint_name'")->fetchField();
+ }
+
+ public function addPrimaryKey($table, $fields) {
+ if (!$this->tableExists($table)) {
+ throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add primary key to table %table: table doesn't exist.", array('%table' => $table)));
+ }
+ if ($this->constraintExists($table, 'pkey')) {
+ throw new DatabaseSchemaObjectExistsException(t("Cannot add primary key to table %table: primary key already exists.", array('%table' => $table)));
+ }
+
+ $this->connection->query('ALTER TABLE {' . $table . '} ADD PRIMARY KEY (' . implode(',', $fields) . ')');
+ }
+
+ public function dropPrimaryKey($table) {
+ if (!$this->constraintExists($table, 'pkey')) {
+ return FALSE;
+ }
+
+ $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT ' . $this->prefixNonTable($table, 'pkey'));
+ return TRUE;
+ }
+
+ 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)));
+ }
+ 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)));
+ }
+
+ $this->connection->query('ALTER TABLE {' . $table . '} ADD CONSTRAINT "' . $this->prefixNonTable($table, $name, 'key') . '" UNIQUE (' . implode(',', $fields) . ')');
+ }
+
+ public function dropUniqueKey($table, $name) {
+ if (!$this->constraintExists($table, $name . '_key')) {
+ return FALSE;
+ }
+
+ $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT "' . $this->prefixNonTable($table, $name, 'key') . '"');
+ return TRUE;
+ }
+
+ 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)));
+ }
+ if ($this->indexExists($table, $name)) {
+ throw new DatabaseSchemaObjectExistsException(t("Cannot add index %name to table %table: index already exists.", array('%table' => $table, '%name' => $name)));
+ }
+
+ $this->connection->query($this->_createIndexSql($table, $name, $fields));
+ }
+
+ public function dropIndex($table, $name) {
+ if (!$this->indexExists($table, $name)) {
+ return FALSE;
+ }
+
+ $this->connection->query('DROP INDEX ' . $this->prefixNonTable($table, $name, 'idx'));
+ return TRUE;
+ }
+
+ 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)));
+ }
+ 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)));
+ }
+
+ $spec = $this->processField($spec);
+
+ // We need to typecast the new column to best be able to transfer the data
+ // Schema_pgsql::getFieldTypeMap() will return possibilities that are not
+ // 'cast-able' such as 'serial' - so they need to be casted int instead.
+ if (in_array($spec['pgsql_type'], array('serial', 'bigserial', 'numeric'))) {
+ $typecast = 'int';
+ }
+ else {
+ $typecast = $spec['pgsql_type'];
+ }
+
+ if (in_array($spec['pgsql_type'], array('varchar', 'character', 'text')) && isset($spec['length'])) {
+ $typecast .= '(' . $spec['length'] . ')';
+ }
+ elseif (isset($spec['precision']) && isset($spec['scale'])) {
+ $typecast .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')';
+ }
+
+ // Remove old check constraints.
+ $field_info = $this->queryFieldInformation($table, $field);
+
+ foreach ($field_info as $check) {
+ $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT "' . $check . '"');
+ }
+
+ // Remove old default.
+ $this->fieldSetNoDefault($table, $field);
+
+ $this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $typecast . ' USING "' . $field . '"::' . $typecast);
+
+ if (isset($spec['not null'])) {
+ if ($spec['not null']) {
+ $nullaction = 'SET NOT NULL';
+ }
+ else {
+ $nullaction = 'DROP NOT NULL';
+ }
+ $this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" ' . $nullaction);
+ }
+
+ if (in_array($spec['pgsql_type'], array('serial', 'bigserial'))) {
+ // Type "serial" is known to PostgreSQL, but *only* during table creation,
+ // not when altering. Because of that, the sequence needs to be created
+ // and initialized by hand.
+ $seq = "{" . $table . "}_" . $field_new . "_seq";
+ $this->connection->query("CREATE SEQUENCE " . $seq);
+ // Set sequence to maximal field value to not conflict with existing
+ // entries.
+ $this->connection->query("SELECT setval('" . $seq . "', MAX(\"" . $field . '")) FROM {' . $table . "}");
+ $this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" SET DEFAULT nextval(\'' . $seq . '\')');
+ }
+
+ // Rename the column if necessary.
+ if ($field != $field_new) {
+ $this->connection->query('ALTER TABLE {' . $table . '} RENAME "' . $field . '" TO "' . $field_new . '"');
+ }
+
+ // Add unsigned check if necessary.
+ if (!empty($spec['unsigned'])) {
+ $this->connection->query('ALTER TABLE {' . $table . '} ADD CHECK ("' . $field_new . '" >= 0)');
+ }
+
+ // Add default if necessary.
+ if (isset($spec['default'])) {
+ $this->fieldSetDefault($table, $field_new, $spec['default']);
+ }
+
+ // Change description if necessary.
+ if (!empty($spec['description'])) {
+ $this->connection->query('COMMENT ON COLUMN {' . $table . '}."' . $field_new . '" IS ' . $this->prepareComment($spec['description']));
+ }
+
+ if (isset($new_keys)) {
+ $this->_createKeys($table, $new_keys);
+ }
+ }
+
+ protected function _createIndexSql($table, $name, $fields) {
+ $query = 'CREATE INDEX "' . $this->prefixNonTable($table, $name, 'idx') . '" ON {' . $table . '} (';
+ $query .= $this->_createKeySql($fields) . ')';
+ return $query;
+ }
+
+ protected function _createKeys($table, $new_keys) {
+ if (isset($new_keys['primary key'])) {
+ $this->addPrimaryKey($table, $new_keys['primary key']);
+ }
+ if (isset($new_keys['unique keys'])) {
+ foreach ($new_keys['unique keys'] as $name => $fields) {
+ $this->addUniqueKey($table, $name, $fields);
+ }
+ }
+ if (isset($new_keys['indexes'])) {
+ foreach ($new_keys['indexes'] as $name => $fields) {
+ $this->addIndex($table, $name, $fields);
+ }
+ }
+ }
+
+ /**
+ * Retrieve a table or column comment.
+ */
+ public function getComment($table, $column = NULL) {
+ $info = $this->getPrefixInfo($table);
+ // Don't use {} around pg_class, pg_attribute tables.
+ if (isset($column)) {
+ return $this->connection->query('SELECT col_description(oid, attnum) FROM pg_class, pg_attribute WHERE attrelid = oid AND relname = ? AND attname = ?', array($info['table'], $column))->fetchField();
+ }
+ else {
+ return $this->connection->query('SELECT obj_description(oid, ?) FROM pg_class WHERE relname = ?', array('pg_class', $info['table']))->fetchField();
+ }
+ }
+}
diff --git a/core/includes/database/pgsql/select.inc b/core/includes/database/pgsql/select.inc
new file mode 100644
index 00000000000..d1d83828118
--- /dev/null
+++ b/core/includes/database/pgsql/select.inc
@@ -0,0 +1,108 @@
+<?php
+
+/**
+ * @file
+ * Select builder for PostgreSQL database engine.
+ */
+
+/**
+ * @ingroup database
+ * @{
+ */
+
+class SelectQuery_pgsql extends SelectQuery {
+
+ public function orderRandom() {
+ $alias = $this->addExpression('RANDOM()', 'random_field');
+ $this->orderBy($alias);
+ return $this;
+ }
+
+ /**
+ * Overrides SelectQuery::orderBy().
+ *
+ * PostgreSQL adheres strictly to the SQL-92 standard and requires that when
+ * using DISTINCT or GROUP BY conditions, fields and expressions that are
+ * ordered on also need to be selected. This is a best effort implementation
+ * to handle the cases that can be automated by adding the field if it is not
+ * yet selected.
+ *
+ * @code
+ * $query = db_select('node', 'n');
+ * $query->join('node_revision', 'nr', 'n.vid = nr.vid');
+ * $query
+ * ->distinct()
+ * ->fields('n')
+ * ->orderBy('timestamp');
+ * @endcode
+ *
+ * In this query, it is not possible (without relying on the schema) to know
+ * whether timestamp belongs to node_revisions and needs to be added or
+ * belongs to node and is already selected. Queries like this will need to be
+ * corrected in the original query by adding an explicit call to
+ * SelectQuery::addField() or SelectQuery::fields().
+ *
+ * Since this has a small performance impact, both by the additional
+ * processing in this function and in the database that needs to return the
+ * additional fields, this is done as an override instead of implementing it
+ * directly in SelectQuery::orderBy().
+ */
+ public function orderBy($field, $direction = 'ASC') {
+ // Call parent function to order on this.
+ $return = parent::orderBy($field, $direction);
+
+ // If there is a table alias specified, split it up.
+ if (strpos($field, '.') !== FALSE) {
+ list($table, $table_field) = explode('.', $field);
+ }
+ // Figure out if the field has already been added.
+ foreach ($this->fields as $existing_field) {
+ if (!empty($table)) {
+ // If table alias is given, check if field and table exists.
+ if ($existing_field['table'] == $table && $existing_field['field'] == $table_field) {
+ return $return;
+ }
+ }
+ else {
+ // If there is no table, simply check if the field exists as a field or
+ // an aliased field.
+ if ($existing_field['alias'] == $field) {
+ return $return;
+ }
+ }
+ }
+
+ // Also check expression aliases.
+ foreach ($this->expressions as $expression) {
+ if ($expression['alias'] == $field) {
+ return $return;
+ }
+ }
+
+ // If a table loads all fields, it can not be added again. It would
+ // result in an ambigious alias error because that field would be loaded
+ // twice: Once through table_alias.* and once directly. If the field
+ // actually belongs to a different table, it must be added manually.
+ foreach ($this->tables as $table) {
+ if (!empty($table['all_fields'])) {
+ return $return;
+ }
+ }
+
+ // If $field contains an characters which are not allowed in a field name
+ // it is considered an expression, these can't be handeld automatically
+ // either.
+ if ($this->connection->escapeField($field) != $field) {
+ return $return;
+ }
+
+ // This is a case that can be handled automatically, add the field.
+ $this->addField(NULL, $field);
+ return $return;
+ }
+}
+
+/**
+ * @} End of "ingroup database".
+ */
+
diff --git a/core/includes/database/prefetch.inc b/core/includes/database/prefetch.inc
new file mode 100644
index 00000000000..4f2b19d1f3d
--- /dev/null
+++ b/core/includes/database/prefetch.inc
@@ -0,0 +1,507 @@
+<?php
+
+/**
+ * @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.
+ */
+
+/**
+ * @ingroup database
+ * @{
+ */
+
+/**
+ * An implementation of DatabaseStatementInterface that prefetches all data.
+ *
+ * This class behaves very similar to a PDOStatement but as it always fetches
+ * every row it is possible to manipulate those results.
+ */
+class DatabaseStatementPrefetch implements Iterator, DatabaseStatementInterface {
+
+ /**
+ * The query string.
+ *
+ * @var string
+ */
+ protected $queryString;
+
+ /**
+ * Driver-specific options. Can be used by child classes.
+ *
+ * @var Array
+ */
+ protected $driverOptions;
+
+ /**
+ * Reference to the database connection object for this statement.
+ *
+ * The name $dbh is inherited from PDOStatement.
+ *
+ * @var DatabaseConnection
+ */
+ public $dbh;
+
+ /**
+ * Main data store.
+ *
+ * @var Array
+ */
+ protected $data = array();
+
+ /**
+ * The current row, retrieved in PDO::FETCH_ASSOC format.
+ *
+ * @var Array
+ */
+ protected $currentRow = NULL;
+
+ /**
+ * The key of the current row.
+ *
+ * @var int
+ */
+ protected $currentKey = NULL;
+
+ /**
+ * The list of column names in this result set.
+ *
+ * @var Array
+ */
+ protected $columnNames = NULL;
+
+ /**
+ * The number of rows affected by the last query.
+ *
+ * @var int
+ */
+ protected $rowCount = NULL;
+
+ /**
+ * The number of rows in this result set.
+ *
+ * @var int
+ */
+ protected $resultRowCount = 0;
+
+ /**
+ * Holds the current fetch style (which will be used by the next fetch).
+ * @see PDOStatement::fetch()
+ *
+ * @var int
+ */
+ protected $fetchStyle = PDO::FETCH_OBJ;
+
+ /**
+ * Holds supplementary current fetch options (which will be used by the next fetch).
+ *
+ * @var Array
+ */
+ protected $fetchOptions = array(
+ 'class' => 'stdClass',
+ 'constructor_args' => array(),
+ 'object' => NULL,
+ 'column' => 0,
+ );
+
+ /**
+ * Holds the default fetch style.
+ *
+ * @var int
+ */
+ protected $defaultFetchStyle = PDO::FETCH_OBJ;
+
+ /**
+ * Holds supplementary default fetch options.
+ *
+ * @var Array
+ */
+ protected $defaultFetchOptions = array(
+ 'class' => 'stdClass',
+ 'constructor_args' => array(),
+ 'object' => NULL,
+ 'column' => 0,
+ );
+
+ public function __construct(DatabaseConnection $connection, $query, array $driver_options = array()) {
+ $this->dbh = $connection;
+ $this->queryString = $query;
+ $this->driverOptions = $driver_options;
+ }
+
+ /**
+ * Executes a prepared statement.
+ *
+ * @param $args
+ * An array of values with as many elements as there are bound parameters in the SQL statement being executed.
+ * @param $options
+ * An array of options for this query.
+ * @return
+ * TRUE on success, or FALSE on failure.
+ */
+ public function execute($args = array(), $options = array()) {
+ if (isset($options['fetch'])) {
+ if (is_string($options['fetch'])) {
+ // Default to an object. Note: db fields will be added to the object
+ // before the constructor is run. If you need to assign fields after
+ // the constructor is run, see 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);
+ }
+
+ // Prepare the query.
+ $statement = $this->getStatement($this->queryString, $args);
+ if (!$statement) {
+ $this->throwPDOException();
+ }
+
+ $return = $statement->execute($args);
+ if (!$return) {
+ $this->throwPDOException();
+ }
+
+ // Fetch all the data from the reply, in order to release any lock
+ // as soon as possible.
+ $this->rowCount = $statement->rowCount();
+ $this->data = $statement->fetchAll(PDO::FETCH_ASSOC);
+ // Destroy the statement as soon as possible. See
+ // DatabaseConnection_sqlite::PDOPrepare() for explanation.
+ unset($statement);
+
+ $this->resultRowCount = count($this->data);
+
+ if ($this->resultRowCount) {
+ $this->columnNames = array_keys($this->data[0]);
+ }
+ else {
+ $this->columnNames = array();
+ }
+
+ if (!empty($logger)) {
+ $query_end = microtime(TRUE);
+ $logger->log($this, $args, $query_end - $query_start);
+ }
+
+ // Initialize the first row in $this->currentRow.
+ $this->next();
+
+ return $return;
+ }
+
+ /**
+ * Throw a PDO Exception based on the last PDO error.
+ */
+ protected function throwPDOException() {
+ $error_info = $this->dbh->errorInfo();
+ // We rebuild a message formatted in the same way as PDO.
+ $exception = new PDOException("SQLSTATE[" . $error_info[0] . "]: General error " . $error_info[1] . ": " . $error_info[2]);
+ $exception->errorInfo = $error_info;
+ throw $exception;
+ }
+
+ /**
+ * Grab a PDOStatement object from a given query and its arguments.
+ *
+ * Some drivers (including SQLite) will need to perform some preparation
+ * themselves to get the statement right.
+ *
+ * @param $query
+ * The query.
+ * @param array $args
+ * An array of arguments.
+ * @return PDOStatement
+ * A PDOStatement object.
+ */
+ protected function getStatement($query, &$args = array()) {
+ return $this->dbh->prepare($query);
+ }
+
+ /**
+ * Return the object's SQL query string.
+ */
+ public function getQueryString() {
+ return $this->queryString;
+ }
+
+ /**
+ * @see PDOStatement::setFetchMode()
+ */
+ public function setFetchMode($fetchStyle, $a2 = NULL, $a3 = NULL) {
+ $this->defaultFetchStyle = $fetchStyle;
+ switch ($fetchStyle) {
+ case PDO::FETCH_CLASS:
+ $this->defaultFetchOptions['class'] = $a2;
+ if ($a3) {
+ $this->defaultFetchOptions['constructor_args'] = $a3;
+ }
+ break;
+ case PDO::FETCH_COLUMN:
+ $this->defaultFetchOptions['column'] = $a2;
+ break;
+ case PDO::FETCH_INTO:
+ $this->defaultFetchOptions['object'] = $a2;
+ break;
+ }
+
+ // Set the values for the next fetch.
+ $this->fetchStyle = $this->defaultFetchStyle;
+ $this->fetchOptions = $this->defaultFetchOptions;
+ }
+
+ /**
+ * Return the current row formatted according to the current fetch style.
+ *
+ * This is the core method of this class. It grabs the value at the current
+ * array position in $this->data and format it according to $this->fetchStyle
+ * and $this->fetchMode.
+ *
+ * @return
+ * The current row formatted as requested.
+ */
+ public function current() {
+ if (isset($this->currentRow)) {
+ switch ($this->fetchStyle) {
+ case PDO::FETCH_ASSOC:
+ return $this->currentRow;
+ case PDO::FETCH_BOTH:
+ // PDO::FETCH_BOTH returns an array indexed by both the column name
+ // and the column number.
+ return $this->currentRow + array_values($this->currentRow);
+ case PDO::FETCH_NUM:
+ return array_values($this->currentRow);
+ case PDO::FETCH_LAZY:
+ // We do not do lazy as everything is fetched already. Fallback to
+ // PDO::FETCH_OBJ.
+ case PDO::FETCH_OBJ:
+ return (object) $this->currentRow;
+ case PDO::FETCH_CLASS | PDO::FETCH_CLASSTYPE:
+ $class_name = array_unshift($this->currentRow);
+ // Deliberate no break.
+ case PDO::FETCH_CLASS:
+ if (!isset($class_name)) {
+ $class_name = $this->fetchOptions['class'];
+ }
+ if (count($this->fetchOptions['constructor_args'])) {
+ $reflector = new ReflectionClass($class_name);
+ $result = $reflector->newInstanceArgs($this->fetchOptions['constructor_args']);
+ }
+ else {
+ $result = new $class_name();
+ }
+ foreach ($this->currentRow as $k => $v) {
+ $result->$k = $v;
+ }
+ return $result;
+ case PDO::FETCH_INTO:
+ foreach ($this->currentRow as $k => $v) {
+ $this->fetchOptions['object']->$k = $v;
+ }
+ return $this->fetchOptions['object'];
+ case PDO::FETCH_COLUMN:
+ if (isset($this->columnNames[$this->fetchOptions['column']])) {
+ return $this->currentRow[$k][$this->columnNames[$this->fetchOptions['column']]];
+ }
+ else {
+ return;
+ }
+ }
+ }
+ }
+
+ /* Implementations of Iterator. */
+
+ public function key() {
+ return $this->currentKey;
+ }
+
+ public function rewind() {
+ // Nothing to do: our DatabaseStatement can't be rewound.
+ }
+
+ public function next() {
+ if (!empty($this->data)) {
+ $this->currentRow = reset($this->data);
+ $this->currentKey = key($this->data);
+ unset($this->data[$this->currentKey]);
+ }
+ else {
+ $this->currentRow = NULL;
+ }
+ }
+
+ public function valid() {
+ return isset($this->currentRow);
+ }
+
+ /* Implementations of DatabaseStatementInterface. */
+
+ public function rowCount() {
+ return $this->rowCount;
+ }
+
+ public function fetch($fetch_style = NULL, $cursor_orientation = PDO::FETCH_ORI_NEXT, $cursor_offset = NULL) {
+ if (isset($this->currentRow)) {
+ // Set the fetch parameter.
+ $this->fetchStyle = isset($fetch_style) ? $fetch_style : $this->defaultFetchStyle;
+ $this->fetchOptions = $this->defaultFetchOptions;
+
+ // Grab the row in the format specified above.
+ $return = $this->current();
+ // Advance the cursor.
+ $this->next();
+
+ // Reset the fetch parameters to the value stored using setFetchMode().
+ $this->fetchStyle = $this->defaultFetchStyle;
+ $this->fetchOptions = $this->defaultFetchOptions;
+ return $return;
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ public function fetchColumn($index = 0) {
+ if (isset($this->currentRow) && isset($this->columnNames[$index])) {
+ // We grab the value directly from $this->data, and format it.
+ $return = $this->currentRow[$this->columnNames[$index]];
+ $this->next();
+ return $return;
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ public function fetchField($index = 0) {
+ return $this->fetchColumn($index);
+ }
+
+ public function fetchObject($class_name = NULL, $constructor_args = array()) {
+ if (isset($this->currentRow)) {
+ if (!isset($class_name)) {
+ // Directly cast to an object to avoid a function call.
+ $result = (object) $this->currentRow;
+ }
+ else {
+ $this->fetchStyle = PDO::FETCH_CLASS;
+ $this->fetchOptions = array('constructor_args' => $constructor_args);
+ // Grab the row in the format specified above.
+ $result = $this->current();
+ // Reset the fetch parameters to the value stored using setFetchMode().
+ $this->fetchStyle = $this->defaultFetchStyle;
+ $this->fetchOptions = $this->defaultFetchOptions;
+ }
+
+ $this->next();
+
+ return $result;
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ public function fetchAssoc() {
+ if (isset($this->currentRow)) {
+ $result = $this->currentRow;
+ $this->next();
+ return $result;
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ public function fetchAll($fetch_style = NULL, $fetch_column = NULL, $constructor_args = NULL) {
+ $this->fetchStyle = isset($fetch_style) ? $fetch_style : $this->defaultFetchStyle;
+ $this->fetchOptions = $this->defaultFetchOptions;
+ if (isset($fetch_column)) {
+ $this->fetchOptions['column'] = $fetch_column;
+ }
+ if (isset($constructor_args)) {
+ $this->fetchOptions['constructor_args'] = $constructor_args;
+ }
+
+ $result = array();
+ // Traverse the array as PHP would have done.
+ while (isset($this->currentRow)) {
+ // Grab the row in the format specified above.
+ $result[] = $this->current();
+ $this->next();
+ }
+
+ // Reset the fetch parameters to the value stored using setFetchMode().
+ $this->fetchStyle = $this->defaultFetchStyle;
+ $this->fetchOptions = $this->defaultFetchOptions;
+ return $result;
+ }
+
+ public function fetchCol($index = 0) {
+ if (isset($this->columnNames[$index])) {
+ $column = $this->columnNames[$index];
+ $result = array();
+ // Traverse the array as PHP would have done.
+ while (isset($this->currentRow)) {
+ $result[] = $this->currentRow[$this->columnNames[$index]];
+ $this->next();
+ }
+ return $result;
+ }
+ else {
+ return array();
+ }
+ }
+
+ public function fetchAllKeyed($key_index = 0, $value_index = 1) {
+ if (!isset($this->columnNames[$key_index]) || !isset($this->columnNames[$value_index]))
+ return array();
+
+ $key = $this->columnNames[$key_index];
+ $value = $this->columnNames[$value_index];
+
+ $result = array();
+ // Traverse the array as PHP would have done.
+ while (isset($this->currentRow)) {
+ $result[$this->currentRow[$key]] = $this->currentRow[$value];
+ $this->next();
+ }
+ return $result;
+ }
+
+ public function fetchAllAssoc($key, $fetch_style = NULL) {
+ $this->fetchStyle = isset($fetch_style) ? $fetch_style : $this->defaultFetchStyle;
+ $this->fetchOptions = $this->defaultFetchOptions;
+
+ $result = array();
+ // Traverse the array as PHP would have done.
+ while (isset($this->currentRow)) {
+ // Grab the row in its raw PDO::FETCH_ASSOC format.
+ $row = $this->currentRow;
+ // Grab the row in the format specified above.
+ $result_row = $this->current();
+ $result[$this->currentRow[$key]] = $result_row;
+ $this->next();
+ }
+
+ // Reset the fetch parameters to the value stored using setFetchMode().
+ $this->fetchStyle = $this->defaultFetchStyle;
+ $this->fetchOptions = $this->defaultFetchOptions;
+ return $result;
+ }
+
+}
+
+/**
+ * @} End of "ingroup database".
+ */
+
diff --git a/core/includes/database/query.inc b/core/includes/database/query.inc
new file mode 100644
index 00000000000..c779687679a
--- /dev/null
+++ b/core/includes/database/query.inc
@@ -0,0 +1,1953 @@
+<?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.
+ *
+ * @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.
+ */
+ 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';
+ }
+ elseif (!isset($value)) {
+ $operator = 'IS NULL';
+ }
+ 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);
+ }
+
+ /**
+ * 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/schema.inc b/core/includes/database/schema.inc
new file mode 100644
index 00000000000..41c68021dcd
--- /dev/null
+++ b/core/includes/database/schema.inc
@@ -0,0 +1,722 @@
+<?php
+
+/**
+ * @file
+ * Generic Database schema code.
+ */
+
+require_once __DIR__ . '/query.inc';
+
+/**
+ * @defgroup schemaapi Schema API
+ * @{
+ * API to handle database schemas.
+ *
+ * A Drupal schema definition is an array structure representing one or
+ * more tables and their related keys and indexes. A schema is defined by
+ * hook_schema(), which usually lives in a modulename.install file.
+ *
+ * By implementing hook_schema() and specifying the tables your module
+ * declares, you can easily create and drop these tables on all
+ * supported database engines. You don't have to deal with the
+ * different SQL dialects for table creation and alteration of the
+ * supported database engines.
+ *
+ * hook_schema() should return an array with a key for each table that
+ * the module defines.
+ *
+ * The following keys are defined:
+ * - 'description': A string in non-markup plain text describing this table
+ * and its purpose. References to other tables should be enclosed in
+ * curly-brackets. For example, the node_revisions table
+ * description field might contain "Stores per-revision title and
+ * body data for each {node}."
+ * - 'fields': An associative array ('fieldname' => specification)
+ * that describes the table's database columns. The specification
+ * is also an array. The following specification parameters are defined:
+ * - 'description': A string in non-markup plain text describing this field
+ * and its purpose. References to other tables should be enclosed in
+ * curly-brackets. For example, the node table vid field
+ * description might contain "Always holds the largest (most
+ * recent) {node_revision}.vid value for this nid."
+ * - 'type': The generic datatype: 'char', 'varchar', 'text', 'blob', 'int',
+ * 'float', 'numeric', or 'serial'. Most types just map to the according
+ * database engine specific datatypes. Use 'serial' for auto incrementing
+ * fields. This will expand to 'INT auto_increment' on MySQL.
+ * - 'mysql_type', 'pgsql_type', 'sqlite_type', etc.: If you need to
+ * use a record type not included in the officially supported list
+ * of types above, you can specify a type for each database
+ * backend. In this case, you can leave out the type parameter,
+ * but be advised that your schema will fail to load on backends that
+ * do not have a type specified. A possible solution can be to
+ * use the "text" type as a fallback.
+ * - 'serialize': A boolean indicating whether the field will be stored as
+ * a serialized string.
+ * - 'size': The data size: 'tiny', 'small', 'medium', 'normal',
+ * 'big'. This is a hint about the largest value the field will
+ * store and determines which of the database engine specific
+ * datatypes will be used (e.g. on MySQL, TINYINT vs. INT vs. BIGINT).
+ * 'normal', the default, selects the base type (e.g. on MySQL,
+ * INT, VARCHAR, BLOB, etc.).
+ * Not all sizes are available for all data types. See
+ * DatabaseSchema::getFieldTypeMap() for possible combinations.
+ * - 'not null': If true, no NULL values will be allowed in this
+ * database column. Defaults to false.
+ * - 'default': The field's default value. The PHP type of the
+ * value matters: '', '0', and 0 are all different. If you
+ * specify '0' as the default value for a type 'int' field it
+ * will not work because '0' is a string containing the
+ * character "zero", not an integer.
+ * - 'length': The maximal length of a type 'char', 'varchar' or 'text'
+ * field. Ignored for other field types.
+ * - 'unsigned': A boolean indicating whether a type 'int', 'float'
+ * and 'numeric' only is signed or unsigned. Defaults to
+ * FALSE. Ignored for other field types.
+ * - 'precision', 'scale': For type 'numeric' fields, indicates
+ * the precision (total number of significant digits) and scale
+ * (decimal digits right of the decimal point). Both values are
+ * mandatory. Ignored for other field types.
+ * All parameters apart from 'type' are optional except that type
+ * 'numeric' columns must specify 'precision' and 'scale'.
+ * - 'primary key': An array of one or more key column specifiers (see below)
+ * that form the primary key.
+ * - 'unique keys': An associative array of unique keys ('keyname' =>
+ * specification). Each specification is an array of one or more
+ * key column specifiers (see below) that form a unique key on the table.
+ * - 'foreign keys': An associative array of relations ('my_relation' =>
+ * specification). Each specification is an array containing the name of
+ * the referenced table ('table'), and an array of column mappings
+ * ('columns'). Column mappings are defined by key pairs ('source_column' =>
+ * 'referenced_column').
+ * - 'indexes': An associative array of indexes ('indexname' =>
+ * specification). Each specification is an array of one or more
+ * key column specifiers (see below) that form an index on the
+ * table.
+ *
+ * A key column specifier is either a string naming a column or an
+ * array of two elements, column name and length, specifying a prefix
+ * of the named column.
+ *
+ * As an example, here is a SUBSET of the schema definition for
+ * Drupal's 'node' table. It show four fields (nid, vid, type, and
+ * title), the primary key on field 'nid', a unique key named 'vid' on
+ * field 'vid', and two indexes, one named 'nid' on field 'nid' and
+ * one named 'node_title_type' on the field 'title' and the first four
+ * bytes of the field 'type':
+ *
+ * @code
+ * $schema['node'] = array(
+ * 'description' => 'The base table for nodes.',
+ * 'fields' => array(
+ * 'nid' => array('type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE),
+ * 'vid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE,'default' => 0),
+ * 'type' => array('type' => 'varchar','length' => 32,'not null' => TRUE, 'default' => ''),
+ * 'language' => array('type' => 'varchar','length' => 12,'not null' => TRUE,'default' => ''),
+ * 'title' => array('type' => 'varchar','length' => 255,'not null' => TRUE, 'default' => ''),
+ * 'uid' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+ * 'status' => array('type' => 'int', 'not null' => TRUE, 'default' => 1),
+ * 'created' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+ * 'changed' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+ * 'comment' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+ * 'promote' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+ * 'moderate' => array('type' => 'int', 'not null' => TRUE,'default' => 0),
+ * 'sticky' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+ * 'tnid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
+ * 'translate' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+ * ),
+ * 'indexes' => array(
+ * 'node_changed' => array('changed'),
+ * 'node_created' => array('created'),
+ * 'node_moderate' => array('moderate'),
+ * 'node_frontpage' => array('promote', 'status', 'sticky', 'created'),
+ * 'node_status_type' => array('status', 'type', 'nid'),
+ * 'node_title_type' => array('title', array('type', 4)),
+ * 'node_type' => array(array('type', 4)),
+ * 'uid' => array('uid'),
+ * 'tnid' => array('tnid'),
+ * 'translate' => array('translate'),
+ * ),
+ * 'unique keys' => array(
+ * 'vid' => array('vid'),
+ * ),
+ * 'foreign keys' => array(
+ * 'node_revision' => array(
+ * 'table' => 'node_revision',
+ * 'columns' => array('vid' => 'vid'),
+ * ),
+ * 'node_author' => array(
+ * 'table' => 'users',
+ * 'columns' => array('uid' => 'uid'),
+ * ),
+ * ),
+ * 'primary key' => array('nid'),
+ * );
+ * @endcode
+ *
+ * @see drupal_install_schema()
+ */
+
+abstract class DatabaseSchema implements QueryPlaceholderInterface {
+
+ protected $connection;
+
+ /**
+ * The placeholder counter.
+ */
+ protected $placeholder = 0;
+
+ /**
+ * Definition of prefixInfo array structure.
+ *
+ * Rather than redefining DatabaseSchema::getPrefixInfo() for each driver,
+ * by defining the defaultSchema variable only MySQL has to re-write the
+ * method.
+ *
+ * @see DatabaseSchema::getPrefixInfo()
+ */
+ protected $defaultSchema = 'public';
+
+ /**
+ * A unique identifier for this query object.
+ */
+ protected $uniqueIdentifier;
+
+ public function __construct($connection) {
+ $this->uniqueIdentifier = uniqid('', TRUE);
+ $this->connection = $connection;
+ }
+
+ /**
+ * Implements the magic __clone function.
+ */
+ public function __clone() {
+ $this->uniqueIdentifier = uniqid('', TRUE);
+ }
+
+ /**
+ * Implements QueryPlaceHolderInterface::uniqueIdentifier().
+ */
+ public function uniqueIdentifier() {
+ return $this->uniqueIdentifier;
+ }
+
+ /**
+ * Implements QueryPlaceHolderInterface::nextPlaceholder().
+ */
+ public function nextPlaceholder() {
+ return $this->placeholder++;
+ }
+
+ /**
+ * Get information about the table name and schema from the prefix.
+ *
+ * @param
+ * Name of table to look prefix up for. Defaults to 'default' because thats
+ * default key for prefix.
+ * @param $add_prefix
+ * Boolean that indicates whether the given table name should be prefixed.
+ *
+ * @return
+ * A keyed array with information about the schema, table name and prefix.
+ */
+ protected function getPrefixInfo($table = 'default', $add_prefix = TRUE) {
+ $info = array(
+ 'schema' => $this->defaultSchema,
+ 'prefix' => $this->connection->tablePrefix($table),
+ );
+ if ($add_prefix) {
+ $table = $info['prefix'] . $table;
+ }
+ // If the prefix contains a period in it, then that means the prefix also
+ // contains a schema reference in which case we will change the schema key
+ // to the value before the period in the prefix. Everything after the dot
+ // will be prefixed onto the front of the table.
+ if (($pos = strpos($table, '.')) !== FALSE) {
+ // Grab everything before the period.
+ $info['schema'] = substr($table, 0, $pos);
+ // Grab everything after the dot.
+ $info['table'] = substr($table, ++$pos);
+ }
+ else {
+ $info['table'] = $table;
+ }
+ return $info;
+ }
+
+ /**
+ * Create names for indexes, primary keys and constraints.
+ *
+ * This prevents using {} around non-table names like indexes and keys.
+ */
+ function prefixNonTable($table) {
+ $args = func_get_args();
+ $info = $this->getPrefixInfo($table);
+ $args[0] = $info['table'];
+ return implode('_', $args);
+ }
+
+ /**
+ * Build a condition to match a table name against a standard information_schema.
+ *
+ * The information_schema is a SQL standard that provides information about the
+ * database server and the databases, schemas, tables, columns and users within
+ * it. This makes information_schema a useful tool to use across the drupal
+ * database drivers and is used by a few different functions. The function below
+ * describes the conditions to be meet when querying information_schema.tables
+ * for drupal tables or information associated with drupal tables. Even though
+ * this is the standard method, not all databases follow standards and so this
+ * method should be overwritten by a database driver if the database provider
+ * uses alternate methods. Because information_schema.tables is used in a few
+ * different functions, a database driver will only need to override this function
+ * to make all the others work. For example see includes/databases/mysql/schema.inc.
+ *
+ * @param $table_name
+ * The name of the table in question.
+ * @param $operator
+ * The operator to apply on the 'table' part of the condition.
+ * @param $add_prefix
+ * Boolean to indicate whether the table name needs to be prefixed.
+ *
+ * @return QueryConditionInterface
+ * A DatabaseCondition object.
+ */
+ protected function buildTableNameCondition($table_name, $operator = '=', $add_prefix = TRUE) {
+ $info = $this->connection->getConnectionOptions();
+
+ // Retrive the table name and schema
+ $table_info = $this->getPrefixInfo($table_name, $add_prefix);
+
+ $condition = new DatabaseCondition('AND');
+ $condition->condition('table_catalog', $info['database']);
+ $condition->condition('table_schema', $table_info['schema']);
+ $condition->condition('table_name', $table_info['table'], $operator);
+ return $condition;
+ }
+
+ /**
+ * Check if a table exists.
+ *
+ * @param $table
+ * The name of the table in drupal (no prefixing).
+ *
+ * @return
+ * TRUE if the given table exists, otherwise FALSE.
+ */
+ public function tableExists($table) {
+ $condition = $this->buildTableNameCondition($table);
+ $condition->compile($this->connection, $this);
+ // Normally, we would heartily discourage the use of string
+ // concatenation for conditionals like this however, we
+ // couldn't use db_select() here because it would prefix
+ // information_schema.tables and the query would fail.
+ // Don't use {} around information_schema.tables table.
+ return (bool) $this->connection->query("SELECT 1 FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchField();
+ }
+
+ /**
+ * Find all tables that are like the specified base table name.
+ *
+ * @param $table_expression
+ * An SQL expression, for example "simpletest%" (without the quotes).
+ * BEWARE: this is not prefixed, the caller should take care of that.
+ *
+ * @return
+ * Array, both the keys and the values are the matching tables.
+ */
+ public function findTables($table_expression) {
+ $condition = $this->buildTableNameCondition($table_expression, 'LIKE', FALSE);
+
+ $condition->compile($this->connection, $this);
+ // Normally, we would heartily discourage the use of string
+ // concatenation for conditionals like this however, we
+ // couldn't use db_select() here because it would prefix
+ // information_schema.tables and the query would fail.
+ // Don't use {} around information_schema.tables table.
+ return $this->connection->query("SELECT table_name FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchAllKeyed(0, 0);
+ }
+
+ /**
+ * Check if a column exists in the given table.
+ *
+ * @param $table
+ * The name of the table in drupal (no prefixing).
+ * @param $name
+ * The name of the column.
+ *
+ * @return
+ * TRUE if the given column exists, otherwise FALSE.
+ */
+ public function fieldExists($table, $column) {
+ $condition = $this->buildTableNameCondition($table);
+ $condition->condition('column_name', $column);
+ $condition->compile($this->connection, $this);
+ // Normally, we would heartily discourage the use of string
+ // concatenation for conditionals like this however, we
+ // couldn't use db_select() here because it would prefix
+ // information_schema.tables and the query would fail.
+ // Don't use {} around information_schema.columns table.
+ return (bool) $this->connection->query("SELECT 1 FROM information_schema.columns WHERE " . (string) $condition, $condition->arguments())->fetchField();
+ }
+
+ /**
+ * Returns a mapping of Drupal schema field names to DB-native field types.
+ *
+ * Because different field types do not map 1:1 between databases, Drupal has
+ * its own normalized field type names. This function returns a driver-specific
+ * mapping table from Drupal names to the native names for each database.
+ *
+ * @return array
+ * An array of Schema API field types to driver-specific field types.
+ */
+ abstract public function getFieldTypeMap();
+
+ /**
+ * Rename a table.
+ *
+ * @param $table
+ * The table to be renamed.
+ * @param $new_name
+ * The new name for the table.
+ *
+ * @throws DatabaseSchemaObjectDoesNotExistException
+ * If the specified table doesn't exist.
+ * @throws DatabaseSchemaObjectExistsException
+ * If a table with the specified new name already exists.
+ */
+ abstract public function renameTable($table, $new_name);
+
+ /**
+ * Drop a table.
+ *
+ * @param $table
+ * The table to be dropped.
+ *
+ * @return
+ * TRUE if the table was successfully dropped, FALSE if there was no table
+ * by that name to begin with.
+ */
+ abstract public function dropTable($table);
+
+ /**
+ * Add a new field to a table.
+ *
+ * @param $table
+ * Name of the table to be altered.
+ * @param $field
+ * Name of the field to be added.
+ * @param $spec
+ * The field specification array, as taken from a schema definition.
+ * The specification may also contain the key 'initial', the newly
+ * created field will be set to the value of the key in all rows.
+ * This is most useful for creating NOT NULL columns with no default
+ * value in existing tables.
+ * @param $keys_new
+ * Optional keys and indexes specification to be created on the
+ * table along with adding the field. The format is the same as a
+ * table specification but without the 'fields' element. If you are
+ * adding a type 'serial' field, you MUST specify at least one key
+ * or index including it in this array. See db_change_field() for more
+ * explanation why.
+ *
+ * @throws DatabaseSchemaObjectDoesNotExistException
+ * If the specified table doesn't exist.
+ * @throws DatabaseSchemaObjectExistsException
+ * If the specified table already has a field by that name.
+ */
+ abstract public function addField($table, $field, $spec, $keys_new = array());
+
+ /**
+ * Drop a field.
+ *
+ * @param $table
+ * The table to be altered.
+ * @param $field
+ * The field to be dropped.
+ *
+ * @return
+ * TRUE if the field was successfully dropped, FALSE if there was no field
+ * by that name to begin with.
+ */
+ abstract public function dropField($table, $field);
+
+ /**
+ * Set the default value for a field.
+ *
+ * @param $table
+ * The table to be altered.
+ * @param $field
+ * The field to be altered.
+ * @param $default
+ * Default value to be set. NULL for 'default NULL'.
+ *
+ * @throws DatabaseSchemaObjectDoesNotExistException
+ * If the specified table or field doesn't exist.
+ */
+ abstract public function fieldSetDefault($table, $field, $default);
+
+ /**
+ * Set a field to have no default value.
+ *
+ * @param $table
+ * The table to be altered.
+ * @param $field
+ * The field to be altered.
+ *
+ * @throws DatabaseSchemaObjectDoesNotExistException
+ * If the specified table or field doesn't exist.
+ */
+ abstract public function fieldSetNoDefault($table, $field);
+
+ /**
+ * Checks if an index exists in the given table.
+ *
+ * @param $table
+ * The name of the table in drupal (no prefixing).
+ * @param $name
+ * The name of the index in drupal (no prefixing).
+ *
+ * @return
+ * TRUE if the given index exists, otherwise FALSE.
+ */
+ abstract public function indexExists($table, $name);
+
+ /**
+ * Add a primary key.
+ *
+ * @param $table
+ * The table to be altered.
+ * @param $fields
+ * Fields for the primary key.
+ *
+ * @throws DatabaseSchemaObjectDoesNotExistException
+ * If the specified table doesn't exist.
+ * @throws DatabaseSchemaObjectExistsException
+ * If the specified table already has a primary key.
+ */
+ abstract public function addPrimaryKey($table, $fields);
+
+ /**
+ * Drop the primary key.
+ *
+ * @param $table
+ * The table to be altered.
+ *
+ * @return
+ * TRUE if the primary key was successfully dropped, FALSE if there was no
+ * primary key on this table to begin with.
+ */
+ abstract public function dropPrimaryKey($table);
+
+ /**
+ * Add a unique key.
+ *
+ * @param $table
+ * The table to be altered.
+ * @param $name
+ * The name of the key.
+ * @param $fields
+ * An array of field names.
+ *
+ * @throws DatabaseSchemaObjectDoesNotExistException
+ * If the specified table doesn't exist.
+ * @throws DatabaseSchemaObjectExistsException
+ * If the specified table already has a key by that name.
+ */
+ abstract public function addUniqueKey($table, $name, $fields);
+
+ /**
+ * Drop a unique key.
+ *
+ * @param $table
+ * The table to be altered.
+ * @param $name
+ * The name of the key.
+ *
+ * @return
+ * TRUE if the key was successfully dropped, FALSE if there was no key by
+ * that name to begin with.
+ */
+ abstract public function dropUniqueKey($table, $name);
+
+ /**
+ * Add an index.
+ *
+ * @param $table
+ * The table to be altered.
+ * @param $name
+ * The name of the index.
+ * @param $fields
+ * An array of field names.
+ *
+ * @throws DatabaseSchemaObjectDoesNotExistException
+ * If the specified table doesn't exist.
+ * @throws DatabaseSchemaObjectExistsException
+ * If the specified table already has an index by that name.
+ */
+ abstract public function addIndex($table, $name, $fields);
+
+ /**
+ * Drop an index.
+ *
+ * @param $table
+ * The table to be altered.
+ * @param $name
+ * The name of the index.
+ *
+ * @return
+ * TRUE if the index was successfully dropped, FALSE if there was no index
+ * by that name to begin with.
+ */
+ abstract public function dropIndex($table, $name);
+
+ /**
+ * Change a field definition.
+ *
+ * IMPORTANT NOTE: To maintain database portability, you have to explicitly
+ * recreate all indices and primary keys that are using the changed field.
+ *
+ * That means that you have to drop all affected keys and indexes with
+ * db_drop_{primary_key,unique_key,index}() before calling db_change_field().
+ * To recreate the keys and indices, pass the key definitions as the
+ * optional $keys_new argument directly to db_change_field().
+ *
+ * For example, suppose you have:
+ * @code
+ * $schema['foo'] = array(
+ * 'fields' => array(
+ * 'bar' => array('type' => 'int', 'not null' => TRUE)
+ * ),
+ * 'primary key' => array('bar')
+ * );
+ * @endcode
+ * and you want to change foo.bar to be type serial, leaving it as the
+ * primary key. The correct sequence is:
+ * @code
+ * db_drop_primary_key('foo');
+ * db_change_field('foo', 'bar', 'bar',
+ * array('type' => 'serial', 'not null' => TRUE),
+ * array('primary key' => array('bar')));
+ * @endcode
+ *
+ * The reasons for this are due to the different database engines:
+ *
+ * On PostgreSQL, changing a field definition involves adding a new field
+ * and dropping an old one which* causes any indices, primary keys and
+ * sequences (from serial-type fields) that use the changed field to be dropped.
+ *
+ * On MySQL, all type 'serial' fields must be part of at least one key
+ * or index as soon as they are created. You cannot use
+ * db_add_{primary_key,unique_key,index}() for this purpose because
+ * the ALTER TABLE command will fail to add the column without a key
+ * or index specification. The solution is to use the optional
+ * $keys_new argument to create the key or index at the same time as
+ * field.
+ *
+ * You could use db_add_{primary_key,unique_key,index}() in all cases
+ * unless you are converting a field to be type serial. You can use
+ * the $keys_new argument in all cases.
+ *
+ * @param $table
+ * Name of the table.
+ * @param $field
+ * Name of the field to change.
+ * @param $field_new
+ * New name for the field (set to the same as $field if you don't want to change the name).
+ * @param $spec
+ * The field specification for the new field.
+ * @param $keys_new
+ * Optional keys and indexes specification to be created on the
+ * table along with changing the field. The format is the same as a
+ * table specification but without the 'fields' element.
+ *
+ * @throws DatabaseSchemaObjectDoesNotExistException
+ * If the specified table or source field doesn't exist.
+ * @throws DatabaseSchemaObjectExistsException
+ * If the specified destination field already exists.
+ */
+ abstract public function changeField($table, $field, $field_new, $spec, $keys_new = array());
+
+ /**
+ * Create a new table from a Drupal table definition.
+ *
+ * @param $name
+ * The name of the table to create.
+ * @param $table
+ * A Schema API table definition array.
+ *
+ * @throws DatabaseSchemaObjectExistsException
+ * 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)));
+ }
+ $statements = $this->createTableSql($name, $table);
+ foreach ($statements as $statement) {
+ $this->connection->query($statement);
+ }
+ }
+
+ /**
+ * Return an array of field names from an array of key/index column specifiers.
+ *
+ * This is usually an identity function but if a key/index uses a column prefix
+ * specification, this function extracts just the name.
+ *
+ * @param $fields
+ * An array of key/index column specifiers.
+ *
+ * @return
+ * An array of field names.
+ */
+ public function fieldNames($fields) {
+ $return = array();
+ foreach ($fields as $field) {
+ if (is_array($field)) {
+ $return[] = $field[0];
+ }
+ else {
+ $return[] = $field;
+ }
+ }
+ return $return;
+ }
+
+ /**
+ * Prepare a table or column comment for database query.
+ *
+ * @param $comment
+ * The comment string to prepare.
+ * @param $length
+ * Optional upper limit on the returned string length.
+ *
+ * @return
+ * The prepared comment.
+ */
+ public function prepareComment($comment, $length = NULL) {
+ return $this->connection->quote($comment);
+ }
+}
+
+/**
+ * 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/includes/database/select.inc b/core/includes/database/select.inc
new file mode 100644
index 00000000000..75047785493
--- /dev/null
+++ b/core/includes/database/select.inc
@@ -0,0 +1,1630 @@
+<?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->condition($field, $value, $operator, $num_args);
+ return $this;
+ }
+
+ public function &havingConditions() {
+ return $this->having->conditions();
+ }
+
+ public function havingArguments() {
+ return $this->having->arguments();
+ }
+
+ 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() {
+ // Create our new query object that we will mutate into a count query.
+ $count = clone($this);
+
+ // Zero-out existing fields and expressions.
+ $fields =& $count->getFields();
+ $fields = array();
+ $expressions =& $count->getExpressions();
+ $expressions = array();
+
+ // Also remove 'all_fields' statements, which are expanded into tablename.*
+ // when the query is executed.
+ $tables = &$count->getTables();
+ foreach ($tables as $alias => &$table) {
+ unset($table['all_fields']);
+ }
+
+ // Ordering a count query is a waste of cycles, and breaks on some
+ // databases anyway.
+ $orders = &$count->getOrderBy();
+ $orders = array();
+
+ // COUNT() is an expression, so we add that back in.
+ $count->addExpression('COUNT(*)');
+
+ return $count;
+ }
+
+ 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();
+
+ if (!$count->distinct) {
+ // When not executing a distinct query, we can zero-out existing fields
+ // and expressions that are not used by a GROUP BY. Fields listed in
+ // the GROUP BY clause need to be present in the query.
+ $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/database.inc b/core/includes/database/sqlite/database.inc
new file mode 100644
index 00000000000..3e2490b00ca
--- /dev/null
+++ b/core/includes/database/sqlite/database.inc
@@ -0,0 +1,511 @@
+<?php
+
+/**
+ * @file
+ * Database interface code for SQLite embedded database engine.
+ */
+
+/**
+ * @ingroup database
+ * @{
+ */
+
+include_once DRUPAL_ROOT . '/includes/database/prefetch.inc';
+
+/**
+ * Specific SQLite implementation of DatabaseConnection.
+ */
+class DatabaseConnection_sqlite extends DatabaseConnection {
+
+ /**
+ * Whether this database connection supports savepoints.
+ *
+ * Version of sqlite lower then 3.6.8 can't use savepoints.
+ * See http://www.sqlite.org/releaselog/3_6_8.html
+ *
+ * @var boolean
+ */
+ protected $savepointSupport = FALSE;
+
+ /**
+ * Whether or not the active transaction (if any) will be rolled back.
+ *
+ * @var boolean
+ */
+ protected $willRollback;
+
+ /**
+ * All databases attached to the current database. This is used to allow
+ * prefixes to be safely handled without locking the table
+ *
+ * @var array
+ */
+ protected $attachedDatabases = array();
+
+ /**
+ * Whether or not a table has been dropped this request: the destructor will
+ * only try to get rid of unnecessary databases if there is potential of them
+ * being empty.
+ *
+ * This variable is set to public because DatabaseSchema_sqlite needs to
+ * access it. However, it should not be manually set.
+ *
+ * @var boolean
+ */
+ var $tableDropped = FALSE;
+
+ public function __construct(array $connection_options = array()) {
+ // We don't need a specific PDOStatement class here, we simulate it below.
+ $this->statementClass = NULL;
+
+ // This driver defaults to transaction support, except if explicitly passed FALSE.
+ $this->transactionSupport = !isset($connection_options['transactions']) || $connection_options['transactions'] !== FALSE;
+
+ $this->connectionOptions = $connection_options;
+
+ parent::__construct('sqlite:' . $connection_options['database'], '', '', array(
+ // Force column names to lower case.
+ PDO::ATTR_CASE => PDO::CASE_LOWER,
+ // Convert numeric values to strings when fetching.
+ PDO::ATTR_STRINGIFY_FETCHES => TRUE,
+ ));
+
+ // Attach one database for each registered prefix.
+ $prefixes = $this->prefixes;
+ foreach ($prefixes as $table => &$prefix) {
+ // Empty prefix means query the main database -- no need to attach anything.
+ if (!empty($prefix)) {
+ // Only attach the database once.
+ if (!isset($this->attachedDatabases[$prefix])) {
+ $this->attachedDatabases[$prefix] = $prefix;
+ $this->query('ATTACH DATABASE :database AS :prefix', array(':database' => $connection_options['database'] . '-' . $prefix, ':prefix' => $prefix));
+ }
+
+ // Add a ., so queries become prefix.table, which is proper syntax for
+ // querying an attached database.
+ $prefix .= '.';
+ }
+ }
+ // Regenerate the prefixes replacement table.
+ $this->setPrefix($prefixes);
+
+ // Detect support for SAVEPOINT.
+ $version = $this->query('SELECT sqlite_version()')->fetchField();
+ $this->savepointSupport = (version_compare($version, '3.6.8') >= 0);
+
+ // Create functions needed by SQLite.
+ $this->sqliteCreateFunction('if', array($this, 'sqlFunctionIf'));
+ $this->sqliteCreateFunction('greatest', array($this, 'sqlFunctionGreatest'));
+ $this->sqliteCreateFunction('pow', 'pow', 2);
+ $this->sqliteCreateFunction('length', 'strlen', 1);
+ $this->sqliteCreateFunction('md5', 'md5', 1);
+ $this->sqliteCreateFunction('concat', array($this, 'sqlFunctionConcat'));
+ $this->sqliteCreateFunction('substring', array($this, 'sqlFunctionSubstring'), 3);
+ $this->sqliteCreateFunction('substring_index', array($this, 'sqlFunctionSubstringIndex'), 3);
+ $this->sqliteCreateFunction('rand', array($this, 'sqlFunctionRand'));
+ }
+
+ /**
+ * Destructor for the SQLite connection.
+ *
+ * We prune empty databases on destruct, but only if tables have been
+ * dropped. This is especially needed when running the test suite, which
+ * creates and destroy databases several times in a row.
+ */
+ public function __destruct() {
+ if ($this->tableDropped && !empty($this->attachedDatabases)) {
+ foreach ($this->attachedDatabases as $prefix) {
+ // Check if the database is now empty, ignore the internal SQLite tables.
+ try {
+ $count = $this->query('SELECT COUNT(*) FROM ' . $prefix . '.sqlite_master WHERE type = :type AND name NOT LIKE :pattern', array(':type' => 'table', ':pattern' => 'sqlite_%'))->fetchField();
+
+ // We can prune the database file if it doesn't have any tables.
+ if ($count == 0) {
+ // Detach the database.
+ $this->query('DETACH DATABASE :schema', array(':schema' => $prefix));
+ // Destroy the database file.
+ unlink($this->connectionOptions['database'] . '-' . $prefix);
+ }
+ }
+ catch (Exception $e) {
+ // Ignore the exception and continue. There is nothing we can do here
+ // to report the error or fail safe.
+ }
+ }
+ }
+ }
+
+ /**
+ * SQLite compatibility implementation for the IF() SQL function.
+ */
+ public function sqlFunctionIf($condition, $expr1, $expr2 = NULL) {
+ return $condition ? $expr1 : $expr2;
+ }
+
+ /**
+ * SQLite compatibility implementation for the GREATEST() SQL function.
+ */
+ public function sqlFunctionGreatest() {
+ $args = func_get_args();
+ foreach ($args as $k => $v) {
+ if (!isset($v)) {
+ unset($args);
+ }
+ }
+ if (count($args)) {
+ return max($args);
+ }
+ else {
+ return NULL;
+ }
+ }
+
+ /**
+ * SQLite compatibility implementation for the CONCAT() SQL function.
+ */
+ public function sqlFunctionConcat() {
+ $args = func_get_args();
+ return implode('', $args);
+ }
+
+ /**
+ * SQLite compatibility implementation for the SUBSTRING() SQL function.
+ */
+ public function sqlFunctionSubstring($string, $from, $length) {
+ return substr($string, $from - 1, $length);
+ }
+
+ /**
+ * SQLite compatibility implementation for the SUBSTRING_INDEX() SQL function.
+ */
+ public function sqlFunctionSubstringIndex($string, $delimiter, $count) {
+ // If string is empty, simply return an empty string.
+ if (empty($string)) {
+ return '';
+ }
+ $end = 0;
+ for ($i = 0; $i < $count; $i++) {
+ $end = strpos($string, $delimiter, $end + 1);
+ if ($end === FALSE) {
+ $end = strlen($string);
+ }
+ }
+ return substr($string, 0, $end);
+ }
+
+ /**
+ * SQLite compatibility implementation for the RAND() SQL function.
+ */
+ public function sqlFunctionRand($seed = NULL) {
+ if (isset($seed)) {
+ mt_srand($seed);
+ }
+ return mt_rand() / mt_getrandmax();
+ }
+
+ /**
+ * 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
+ * using the semi-private PDOPrepare() method below.
+ */
+ public function prepare($query, $options = array()) {
+ return new DatabaseStatement_sqlite($this, $query, $options);
+ }
+
+ /**
+ * NEVER CALL THIS FUNCTION: YOU MIGHT DEADLOCK YOUR PHP PROCESS.
+ *
+ * This is a wrapper around the parent PDO::prepare method. However, as
+ * the PDO SQLite driver only closes SELECT statements when the PDOStatement
+ * destructor is called and SQLite does not allow data change (INSERT,
+ * UPDATE etc) on a table which has open SELECT statements, 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.
+ */
+ public function PDOPrepare($query, array $options = array()) {
+ return parent::prepare($query, $options);
+ }
+
+ public function queryRange($query, $from, $count, array $args = array(), array $options = array()) {
+ return $this->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options);
+ }
+
+ public function queryTemporary($query, array $args = array(), array $options = array()) {
+ // Generate a new temporary table name and protect it from prefixing.
+ // SQLite requires that temporary tables to be non-qualified.
+ $tablename = $this->generateTemporaryTableName();
+ $prefixes = $this->prefixes;
+ $prefixes[$tablename] = '';
+ $this->setPrefix($prefixes);
+
+ $this->query(preg_replace('/^SELECT/i', 'CREATE TEMPORARY TABLE ' . $tablename . ' AS SELECT', $query), $args, $options);
+ return $tablename;
+ }
+
+ public function driver() {
+ return 'sqlite';
+ }
+
+ public function databaseType() {
+ return 'sqlite';
+ }
+
+ public function mapConditionOperator($operator) {
+ // We don't want to override any of the defaults.
+ static $specials = array(
+ 'LIKE' => array('postfix' => " ESCAPE '\\'"),
+ 'NOT LIKE' => array('postfix' => " ESCAPE '\\'"),
+ );
+ return isset($specials[$operator]) ? $specials[$operator] : NULL;
+ }
+
+ public function prepareQuery($query) {
+ return $this->prepare($this->prefixTables($query));
+ }
+
+ public function nextId($existing_id = 0) {
+ $transaction = $this->startTransaction();
+ // We can safely use literal queries here instead of the slower query
+ // builder because if a given database breaks here then it can simply
+ // override nextId. However, this is unlikely as we deal with short strings
+ // and integers and no known databases require special handling for those
+ // simple cases. If another transaction wants to write the same row, it will
+ // wait until this transaction commits.
+ $stmt = $this->query('UPDATE {sequences} SET value = GREATEST(value, :existing_id) + 1', array(
+ ':existing_id' => $existing_id,
+ ));
+ if (!$stmt->rowCount()) {
+ $this->query('INSERT INTO {sequences} (value) VALUES (:existing_id + 1)', array(
+ ':existing_id' => $existing_id,
+ ));
+ }
+ // The transaction gets committed when the transaction object gets destroyed
+ // because it gets out of scope.
+ return $this->query('SELECT value FROM {sequences}')->fetchField();
+ }
+
+ public function rollback($savepoint_name = 'drupal_transaction') {
+ if ($this->savepointSupport) {
+ return parent::rollBack($savepoint_name);
+ }
+
+ if (!$this->inTransaction()) {
+ throw new DatabaseTransactionNoActiveException();
+ }
+ // A previous rollback to an earlier savepoint may mean that the savepoint
+ // in question has already been rolled back.
+ if (!in_array($savepoint_name, $this->transactionLayers)) {
+ return;
+ }
+
+ // We need to find the point we're rolling back to, all other savepoints
+ // before are no longer needed.
+ while ($savepoint = array_pop($this->transactionLayers)) {
+ if ($savepoint == $savepoint_name) {
+ // Mark whole stack of transactions as needed roll back.
+ $this->willRollback = TRUE;
+ // 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;
+ }
+ return;
+ }
+ }
+ if ($this->supportsTransactions()) {
+ PDO::rollBack();
+ }
+ }
+
+ public function pushTransaction($name) {
+ if ($this->savepointSupport) {
+ return parent::pushTransaction($name);
+ }
+ if (!$this->supportsTransactions()) {
+ return;
+ }
+ if (isset($this->transactionLayers[$name])) {
+ throw new DatabaseTransactionNameNonUniqueException($name . " is already in use.");
+ }
+ if (!$this->inTransaction()) {
+ PDO::beginTransaction();
+ }
+ $this->transactionLayers[$name] = $name;
+ }
+
+ public function popTransaction($name) {
+ if ($this->savepointSupport) {
+ return parent::popTransaction($name);
+ }
+ if (!$this->supportsTransactions()) {
+ return;
+ }
+ if (!$this->inTransaction()) {
+ throw new DatabaseTransactionNoActiveException();
+ }
+
+ // Commit everything since SAVEPOINT $name.
+ while($savepoint = array_pop($this->transactionLayers)) {
+ if ($savepoint != $name) continue;
+
+ // If there are no more layers left then we should commit or rollback.
+ if (empty($this->transactionLayers)) {
+ // If there was any rollback() we should roll back whole transaction.
+ if ($this->willRollback) {
+ $this->willRollback = FALSE;
+ PDO::rollBack();
+ }
+ elseif (!PDO::commit()) {
+ throw new DatabaseTransactionCommitFailedException();
+ }
+ }
+ else {
+ break;
+ }
+ }
+ }
+
+}
+
+/**
+ * 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/includes/database/sqlite/install.inc b/core/includes/database/sqlite/install.inc
new file mode 100644
index 00000000000..62cbac381f1
--- /dev/null
+++ b/core/includes/database/sqlite/install.inc
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * @file
+ * SQLite specific install functions
+ */
+
+class DatabaseTasks_sqlite extends DatabaseTasks {
+ protected $pdoDriver = 'sqlite';
+
+ public function name() {
+ return st('SQLite');
+ }
+
+ /**
+ * Minimum engine version.
+ *
+ * @todo: consider upping to 3.6.8 in Drupal 8 to get SAVEPOINT support.
+ */
+ public function minimumVersion() {
+ return '3.3.7';
+ }
+
+ public function getFormOptions($database) {
+ $form = parent::getFormOptions($database);
+
+ // Remove the options that only apply to client/server style databases.
+ unset($form['username'], $form['password'], $form['advanced_options']['host'], $form['advanced_options']['port']);
+
+ // Make the text more accurate for SQLite.
+ $form['database']['#title'] = st('Database file');
+ $form['database']['#description'] = st('The absolute path to the file where @drupal data will be stored. This must be writable by the web server and should exist outside of the web root.', array('@drupal' => drupal_install_profile_distribution_name()));
+ $default_database = conf_path(FALSE, TRUE) . '/files/.ht.sqlite';
+ $form['database']['#default_value'] = empty($database['database']) ? $default_database : $database['database'];
+ return $form;
+ }
+
+ public function validateDatabaseSettings($database) {
+ // Perform standard validation.
+ $errors = parent::validateDatabaseSettings($database);
+
+ // Verify the database is writable.
+ $db_directory = new SplFileInfo(dirname($database['database']));
+ if (!$db_directory->isWritable()) {
+ $errors[$database['driver'] . '][database'] = st('The directory you specified is not writable by the web server.');
+ }
+
+ return $errors;
+ }
+}
+
diff --git a/core/includes/database/sqlite/query.inc b/core/includes/database/sqlite/query.inc
new file mode 100644
index 00000000000..6b8a72f2ab4
--- /dev/null
+++ b/core/includes/database/sqlite/query.inc
@@ -0,0 +1,160 @@
+<?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/schema.inc b/core/includes/database/sqlite/schema.inc
new file mode 100644
index 00000000000..c5882f12715
--- /dev/null
+++ b/core/includes/database/sqlite/schema.inc
@@ -0,0 +1,683 @@
+<?php
+
+/**
+ * @file
+ * Database schema code for SQLite databases.
+ */
+
+
+/**
+ * @ingroup schemaapi
+ * @{
+ */
+
+class DatabaseSchema_sqlite extends DatabaseSchema {
+
+ /**
+ * Override DatabaseSchema::$defaultSchema
+ */
+ protected $defaultSchema = 'main';
+
+ public function tableExists($table) {
+ $info = $this->getPrefixInfo($table);
+
+ // Don't use {} around sqlite_master table.
+ return (bool) $this->connection->query('SELECT 1 FROM ' . $info['schema'] . '.sqlite_master WHERE type = :type AND name = :name', array(':type' => 'table', ':name' => $info['table']))->fetchField();
+ }
+
+ public function fieldExists($table, $column) {
+ $schema = $this->introspectSchema($table);
+ return !empty($schema['fields'][$column]);
+ }
+
+ /**
+ * Generate SQL to create a new table from a Drupal schema definition.
+ *
+ * @param $name
+ * The name of the table to create.
+ * @param $table
+ * A Schema API table definition array.
+ * @return
+ * An array of SQL statements to create the table.
+ */
+ public function createTableSql($name, $table) {
+ $sql = array();
+ $sql[] = "CREATE TABLE {" . $name . "} (\n" . $this->createColumsSql($name, $table) . "\n);\n";
+ return array_merge($sql, $this->createIndexSql($name, $table));
+ }
+
+ /**
+ * Build the SQL expression for indexes.
+ */
+ protected function createIndexSql($tablename, $schema) {
+ $sql = array();
+ $info = $this->getPrefixInfo($tablename);
+ if (!empty($schema['unique keys'])) {
+ foreach ($schema['unique keys'] as $key => $fields) {
+ $sql[] = 'CREATE UNIQUE INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $key . ' ON ' . $info['table'] . ' (' . $this->createKeySql($fields) . "); \n";
+ }
+ }
+ if (!empty($schema['indexes'])) {
+ foreach ($schema['indexes'] as $key => $fields) {
+ $sql[] = 'CREATE INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $key . ' ON ' . $info['table'] . ' (' . $this->createKeySql($fields) . "); \n";
+ }
+ }
+ return $sql;
+ }
+
+ /**
+ * Build the SQL expression for creating columns.
+ */
+ protected function createColumsSql($tablename, $schema) {
+ $sql_array = array();
+
+ // Add the SQL statement for each field.
+ foreach ($schema['fields'] as $name => $field) {
+ if (isset($field['type']) && $field['type'] == 'serial') {
+ if (isset($schema['primary key']) && ($key = array_search($name, $schema['primary key'])) !== FALSE) {
+ unset($schema['primary key'][$key]);
+ }
+ }
+ $sql_array[] = $this->createFieldSql($name, $this->processField($field));
+ }
+
+ // Process keys.
+ if (!empty($schema['primary key'])) {
+ $sql_array[] = " PRIMARY KEY (" . $this->createKeySql($schema['primary key']) . ")";
+ }
+
+ return implode(", \n", $sql_array);
+ }
+
+ /**
+ * Build the SQL expression for keys.
+ */
+ protected function createKeySql($fields) {
+ $return = array();
+ foreach ($fields as $field) {
+ if (is_array($field)) {
+ $return[] = $field[0];
+ }
+ else {
+ $return[] = $field;
+ }
+ }
+ return implode(', ', $return);
+ }
+
+ /**
+ * Set database-engine specific properties for a field.
+ *
+ * @param $field
+ * A field description array, as specified in the schema documentation.
+ */
+ protected function processField($field) {
+ if (!isset($field['size'])) {
+ $field['size'] = 'normal';
+ }
+
+ // Set the correct database-engine specific datatype.
+ // In case one is already provided, force it to uppercase.
+ if (isset($field['sqlite_type'])) {
+ $field['sqlite_type'] = drupal_strtoupper($field['sqlite_type']);
+ }
+ else {
+ $map = $this->getFieldTypeMap();
+ $field['sqlite_type'] = $map[$field['type'] . ':' . $field['size']];
+ }
+
+ if (isset($field['type']) && $field['type'] == 'serial') {
+ $field['auto_increment'] = TRUE;
+ }
+
+ return $field;
+ }
+
+ /**
+ * Create an SQL string for a field to be used in table creation or alteration.
+ *
+ * Before passing a field out of a schema definition into this function it has
+ * to be processed by db_processField().
+ *
+ * @param $name
+ * Name of the field.
+ * @param $spec
+ * The field specification, as per the schema data structure format.
+ */
+ protected function createFieldSql($name, $spec) {
+ if (!empty($spec['auto_increment'])) {
+ $sql = $name . " INTEGER PRIMARY KEY AUTOINCREMENT";
+ if (!empty($spec['unsigned'])) {
+ $sql .= ' CHECK (' . $name . '>= 0)';
+ }
+ }
+ else {
+ $sql = $name . ' ' . $spec['sqlite_type'];
+
+ if (in_array($spec['sqlite_type'], array('VARCHAR', 'TEXT')) && isset($spec['length'])) {
+ $sql .= '(' . $spec['length'] . ')';
+ }
+
+ if (isset($spec['not null'])) {
+ if ($spec['not null']) {
+ $sql .= ' NOT NULL';
+ }
+ else {
+ $sql .= ' NULL';
+ }
+ }
+
+ if (!empty($spec['unsigned'])) {
+ $sql .= ' CHECK (' . $name . '>= 0)';
+ }
+
+ if (isset($spec['default'])) {
+ if (is_string($spec['default'])) {
+ $spec['default'] = "'" . $spec['default'] . "'";
+ }
+ $sql .= ' DEFAULT ' . $spec['default'];
+ }
+
+ if (empty($spec['not null']) && !isset($spec['default'])) {
+ $sql .= ' DEFAULT NULL';
+ }
+ }
+ return $sql;
+ }
+
+ /**
+ * This maps a generic data type in combination with its data size
+ * to the engine-specific data type.
+ */
+ public function getFieldTypeMap() {
+ // Put :normal last so it gets preserved by array_flip. This makes
+ // it much easier for modules (such as schema.module) to map
+ // database types back into schema types.
+ // $map does not use drupal_static as its value never changes.
+ static $map = array(
+ 'varchar:normal' => 'VARCHAR',
+ 'char:normal' => 'CHAR',
+
+ 'text:tiny' => 'TEXT',
+ 'text:small' => 'TEXT',
+ 'text:medium' => 'TEXT',
+ 'text:big' => 'TEXT',
+ 'text:normal' => 'TEXT',
+
+ 'serial:tiny' => 'INTEGER',
+ 'serial:small' => 'INTEGER',
+ 'serial:medium' => 'INTEGER',
+ 'serial:big' => 'INTEGER',
+ 'serial:normal' => 'INTEGER',
+
+ 'int:tiny' => 'INTEGER',
+ 'int:small' => 'INTEGER',
+ 'int:medium' => 'INTEGER',
+ 'int:big' => 'INTEGER',
+ 'int:normal' => 'INTEGER',
+
+ 'float:tiny' => 'FLOAT',
+ 'float:small' => 'FLOAT',
+ 'float:medium' => 'FLOAT',
+ 'float:big' => 'FLOAT',
+ 'float:normal' => 'FLOAT',
+
+ 'numeric:normal' => 'NUMERIC',
+
+ 'blob:big' => 'BLOB',
+ 'blob:normal' => 'BLOB',
+ );
+ return $map;
+ }
+
+ public function renameTable($table, $new_name) {
+ if (!$this->tableExists($table)) {
+ throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot rename %table to %table_new: table %table doesn't exist.", array('%table' => $table, '%table_new' => $new_name)));
+ }
+ if ($this->tableExists($new_name)) {
+ throw new DatabaseSchemaObjectExistsException(t("Cannot rename %table to %table_new: table %table_new already exists.", array('%table' => $table, '%table_new' => $new_name)));
+ }
+
+ $schema = $this->introspectSchema($table);
+
+ // SQLite doesn't allow you to rename tables outside of the current
+ // database. So the syntax '...RENAME TO database.table' would fail.
+ // So we must determine the full table name here rather than surrounding
+ // the table with curly braces incase the db_prefix contains a reference
+ // to a database outside of our existsing database.
+ $info = $this->getPrefixInfo($new_name);
+ $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO ' . $info['table']);
+
+ // Drop the indexes, there is no RENAME INDEX command in SQLite.
+ if (!empty($schema['unique keys'])) {
+ foreach ($schema['unique keys'] as $key => $fields) {
+ $this->dropIndex($table, $key);
+ }
+ }
+ if (!empty($schema['indexes'])) {
+ foreach ($schema['indexes'] as $index => $fields) {
+ $this->dropIndex($table, $index);
+ }
+ }
+
+ // Recreate the indexes.
+ $statements = $this->createIndexSql($new_name, $schema);
+ foreach ($statements as $statement) {
+ $this->connection->query($statement);
+ }
+ }
+
+ public function dropTable($table) {
+ if (!$this->tableExists($table)) {
+ return FALSE;
+ }
+ $this->connection->tableDropped = TRUE;
+ $this->connection->query('DROP TABLE {' . $table . '}');
+ return TRUE;
+ }
+
+ public function addField($table, $field, $specification, $keys_new = array()) {
+ if (!$this->tableExists($table)) {
+ throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field %table.%field: table doesn't exist.", array('%field' => $field, '%table' => $table)));
+ }
+ if ($this->fieldExists($table, $field)) {
+ throw new DatabaseSchemaObjectExistsException(t("Cannot add field %table.%field: field already exists.", array('%field' => $field, '%table' => $table)));
+ }
+
+ // SQLite doesn't have a full-featured ALTER TABLE statement. It only
+ // supports adding new fields to a table, in some simple cases. In most
+ // cases, we have to create a new table and copy the data over.
+ if (empty($keys_new) && (empty($specification['not null']) || isset($specification['default']))) {
+ // When we don't have to create new keys and we are not creating a
+ // NOT NULL column without a default value, we can use the quicker version.
+ $query = 'ALTER TABLE {' . $table . '} ADD ' . $this->createFieldSql($field, $this->processField($specification));
+ $this->connection->query($query);
+
+ // Apply the initial value if set.
+ if (isset($specification['initial'])) {
+ $this->connection->update($table)
+ ->fields(array($field => $specification['initial']))
+ ->execute();
+ }
+ }
+ else {
+ // We cannot add the field directly. Use the slower table alteration
+ // method, starting from the old schema.
+ $old_schema = $this->introspectSchema($table);
+ $new_schema = $old_schema;
+
+ // Add the new field.
+ $new_schema['fields'][$field] = $specification;
+
+ // Build the mapping between the old fields and the new fields.
+ $mapping = array();
+ if (isset($specification['initial'])) {
+ // If we have a initial value, copy it over.
+ $mapping[$field] = array(
+ 'expression' => ':newfieldinitial',
+ 'arguments' => array(':newfieldinitial' => $specification['initial']),
+ );
+ }
+ else {
+ // Else use the default of the field.
+ $mapping[$field] = NULL;
+ }
+
+ // Add the new indexes.
+ $new_schema += $keys_new;
+
+ $this->alterTable($table, $old_schema, $new_schema, $mapping);
+ }
+ }
+
+ /**
+ * Create a table with a new schema containing the old content.
+ *
+ * As SQLite does not support ALTER TABLE (with a few exceptions) it is
+ * necessary to create a new table and copy over the old content.
+ *
+ * @param $table
+ * Name of the table to be altered.
+ * @param $old_schema
+ * The old schema array for the table.
+ * @param $new_schema
+ * The new schema array for the table.
+ * @param $mapping
+ * An optional mapping between the fields of the old specification and the
+ * fields of the new specification. An associative array, whose keys are
+ * the fields of the new table, and values can take two possible forms:
+ * - a simple string, which is interpreted as the name of a field of the
+ * old table,
+ * - an associative array with two keys 'expression' and 'arguments',
+ * that will be used as an expression field.
+ */
+ protected function alterTable($table, $old_schema, $new_schema, array $mapping = array()) {
+ $i = 0;
+ do {
+ $new_table = $table . '_' . $i++;
+ } while ($this->tableExists($new_table));
+
+ $this->createTable($new_table, $new_schema);
+
+ // Build a SQL query to migrate the data from the old table to the new.
+ $select = $this->connection->select($table);
+
+ // Complete the mapping.
+ $possible_keys = array_keys($new_schema['fields']);
+ $mapping += array_combine($possible_keys, $possible_keys);
+
+ // Now add the fields.
+ foreach ($mapping as $field_alias => $field_source) {
+ // Just ignore this field (ie. use it's default value).
+ if (!isset($field_source)) {
+ continue;
+ }
+
+ if (is_array($field_source)) {
+ $select->addExpression($field_source['expression'], $field_alias, $field_source['arguments']);
+ }
+ else {
+ $select->addField($table, $field_source, $field_alias);
+ }
+ }
+
+ // Execute the data migration query.
+ $this->connection->insert($new_table)
+ ->from($select)
+ ->execute();
+
+ $old_count = $this->connection->query('SELECT COUNT(*) FROM {' . $table . '}')->fetchField();
+ $new_count = $this->connection->query('SELECT COUNT(*) FROM {' . $new_table . '}')->fetchField();
+ if ($old_count == $new_count) {
+ $this->dropTable($table);
+ $this->renameTable($new_table, $table);
+ }
+ }
+
+ /**
+ * Find out the schema of a table.
+ *
+ * This function uses introspection methods provided by the database to
+ * create a schema array. This is useful, for example, during update when
+ * the old schema is not available.
+ *
+ * @param $table
+ * Name of the table.
+ * @return
+ * An array representing the schema, from drupal_get_schema().
+ * @see drupal_get_schema()
+ */
+ protected function introspectSchema($table) {
+ $mapped_fields = array_flip($this->getFieldTypeMap());
+ $schema = array(
+ 'fields' => array(),
+ 'primary key' => array(),
+ 'unique keys' => array(),
+ 'indexes' => array(),
+ );
+
+ $info = $this->getPrefixInfo($table);
+ $result = $this->connection->query('PRAGMA ' . $info['schema'] . '.table_info(' . $info['table'] . ')');
+ foreach ($result as $row) {
+ if (preg_match('/^([^(]+)\((.*)\)$/', $row->type, $matches)) {
+ $type = $matches[1];
+ $length = $matches[2];
+ }
+ else {
+ $type = $row->type;
+ $length = NULL;
+ }
+ if (isset($mapped_fields[$type])) {
+ list($type, $size) = explode(':', $mapped_fields[$type]);
+ $schema['fields'][$row->name] = array(
+ 'type' => $type,
+ 'size' => $size,
+ 'not null' => !empty($row->notnull),
+ 'default' => trim($row->dflt_value, "'"),
+ );
+ if ($length) {
+ $schema['fields'][$row->name]['length'] = $length;
+ }
+ if ($row->pk) {
+ $schema['primary key'][] = $row->name;
+ }
+ }
+ else {
+ new Exception("Unable to parse the column type " . $row->type);
+ }
+ }
+ $indexes = array();
+ $result = $this->connection->query('PRAGMA ' . $info['schema'] . '.index_list(' . $info['table'] . ')');
+ foreach ($result as $row) {
+ if (strpos($row->name, 'sqlite_autoindex_') !== 0) {
+ $indexes[] = array(
+ 'schema_key' => $row->unique ? 'unique keys' : 'indexes',
+ 'name' => $row->name,
+ );
+ }
+ }
+ foreach ($indexes as $index) {
+ $name = $index['name'];
+ // Get index name without prefix.
+ $index_name = substr($name, strlen($info['table']) + 1);
+ $result = $this->connection->query('PRAGMA ' . $info['schema'] . '.index_info(' . $name . ')');
+ foreach ($result as $row) {
+ $schema[$index['schema_key']][$index_name][] = $row->name;
+ }
+ }
+ return $schema;
+ }
+
+ public function dropField($table, $field) {
+ if (!$this->fieldExists($table, $field)) {
+ return FALSE;
+ }
+
+ $old_schema = $this->introspectSchema($table);
+ $new_schema = $old_schema;
+
+ unset($new_schema['fields'][$field]);
+ foreach ($new_schema['indexes'] as $index => $fields) {
+ foreach ($fields as $key => $field_name) {
+ if ($field_name == $field) {
+ unset($new_schema['indexes'][$index][$key]);
+ }
+ }
+ // If this index has no more fields then remove it.
+ if (empty($new_schema['indexes'][$index])) {
+ unset($new_schema['indexes'][$index]);
+ }
+ }
+ $this->alterTable($table, $old_schema, $new_schema);
+ return TRUE;
+ }
+
+ public function changeField($table, $field, $field_new, $spec, $keys_new = array()) {
+ if (!$this->fieldExists($table, $field)) {
+ throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot change the definition of field %table.%name: field doesn't exist.", array('%table' => $table, '%name' => $field)));
+ }
+ if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
+ throw new DatabaseSchemaObjectExistsException(t("Cannot rename field %table.%name to %name_new: target field already exists.", array('%table' => $table, '%name' => $field, '%name_new' => $field_new)));
+ }
+
+ $old_schema = $this->introspectSchema($table);
+ $new_schema = $old_schema;
+
+ // Map the old field to the new field.
+ if ($field != $field_new) {
+ $mapping[$field_new] = $field;
+ }
+ else {
+ $mapping = array();
+ }
+
+ // Remove the previous definition and swap in the new one.
+ unset($new_schema['fields'][$field]);
+ $new_schema['fields'][$field_new] = $spec;
+
+ // Map the former indexes to the new column name.
+ $new_schema['primary key'] = $this->mapKeyDefinition($new_schema['primary key'], $mapping);
+ foreach (array('unique keys', 'indexes') as $k) {
+ foreach ($new_schema[$k] as &$key_definition) {
+ $key_definition = $this->mapKeyDefinition($key_definition, $mapping);
+ }
+ }
+
+ // Add in the keys from $keys_new.
+ if (isset($keys_new['primary key'])) {
+ $new_schema['primary key'] = $keys_new['primary key'];
+ }
+ foreach (array('unique keys', 'indexes') as $k) {
+ if (!empty($keys_new[$k])) {
+ $new_schema[$k] = $keys_new[$k] + $new_schema[$k];
+ }
+ }
+
+ $this->alterTable($table, $old_schema, $new_schema, $mapping);
+ }
+
+ /**
+ * Utility method: rename columns in an index definition according to a new mapping.
+ *
+ * @param $key_definition
+ * The key definition.
+ * @param $mapping
+ * The new mapping.
+ */
+ protected function mapKeyDefinition(array $key_definition, array $mapping) {
+ foreach ($key_definition as &$field) {
+ // The key definition can be an array($field, $length).
+ if (is_array($field)) {
+ $field = &$field[0];
+ }
+ if (isset($mapping[$field])) {
+ $field = $mapping[$field];
+ }
+ }
+ return $key_definition;
+ }
+
+ public function addIndex($table, $name, $fields) {
+ if (!$this->tableExists($table)) {
+ throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add index %name to table %table: table doesn't exist.", array('%table' => $table, '%name' => $name)));
+ }
+ if ($this->indexExists($table, $name)) {
+ throw new DatabaseSchemaObjectExistsException(t("Cannot add index %name to table %table: index already exists.", array('%table' => $table, '%name' => $name)));
+ }
+
+ $schema['indexes'][$name] = $fields;
+ $statements = $this->createIndexSql($table, $schema);
+ foreach ($statements as $statement) {
+ $this->connection->query($statement);
+ }
+ }
+
+ public function indexExists($table, $name) {
+ $info = $this->getPrefixInfo($table);
+
+ return $this->connection->query('PRAGMA ' . $info['schema'] . '.index_info(' . $info['table'] . '_' . $name . ')')->fetchField() != '';
+ }
+
+ public function dropIndex($table, $name) {
+ if (!$this->indexExists($table, $name)) {
+ return FALSE;
+ }
+
+ $info = $this->getPrefixInfo($table);
+
+ $this->connection->query('DROP INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $name);
+ return TRUE;
+ }
+
+ public function addUniqueKey($table, $name, $fields) {
+ if (!$this->tableExists($table)) {
+ throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add unique key %name to table %table: table doesn't exist.", array('%table' => $table, '%name' => $name)));
+ }
+ if ($this->indexExists($table, $name)) {
+ throw new DatabaseSchemaObjectExistsException(t("Cannot add unique key %name to table %table: unique key already exists.", array('%table' => $table, '%name' => $name)));
+ }
+
+ $schema['unique keys'][$name] = $fields;
+ $statements = $this->createIndexSql($table, $schema);
+ foreach ($statements as $statement) {
+ $this->connection->query($statement);
+ }
+ }
+
+ public function dropUniqueKey($table, $name) {
+ if (!$this->indexExists($table, $name)) {
+ return FALSE;
+ }
+
+ $info = $this->getPrefixInfo($table);
+
+ $this->connection->query('DROP INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $name);
+ return TRUE;
+ }
+
+ public function addPrimaryKey($table, $fields) {
+ if (!$this->tableExists($table)) {
+ throw new DatabaseSchemaObjectDoesNotExistException(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)));
+ }
+
+ $new_schema['primary key'] = $fields;
+ $this->alterTable($table, $old_schema, $new_schema);
+ }
+
+ public function dropPrimaryKey($table) {
+ $old_schema = $this->introspectSchema($table);
+ $new_schema = $old_schema;
+
+ if (empty($new_schema['primary key'])) {
+ return FALSE;
+ }
+
+ unset($new_schema['primary key']);
+ $this->alterTable($table, $old_schema, $new_schema);
+ return TRUE;
+ }
+
+ public function fieldSetDefault($table, $field, $default) {
+ if (!$this->fieldExists($table, $field)) {
+ throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot set default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field)));
+ }
+
+ $old_schema = $this->introspectSchema($table);
+ $new_schema = $old_schema;
+
+ $new_schema['fields'][$field]['default'] = $default;
+ $this->alterTable($table, $old_schema, $new_schema);
+ }
+
+ public function fieldSetNoDefault($table, $field) {
+ if (!$this->fieldExists($table, $field)) {
+ throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot remove default value of field %table.%field: field doesn't exist.", array('%table' => $table, '%field' => $field)));
+ }
+
+ $old_schema = $this->introspectSchema($table);
+ $new_schema = $old_schema;
+
+ unset($new_schema['fields'][$field]['default']);
+ $this->alterTable($table, $old_schema, $new_schema);
+ }
+
+ public function findTables($table_expression) {
+ // Don't add the prefix, $table_expression already includes the prefix.
+ $info = $this->getPrefixInfo($table_expression, FALSE);
+
+ // Can't use query placeholders for the schema because the query would have
+ // to be :prefixsqlite_master, which does not work.
+ $result = db_query("SELECT name FROM " . $info['schema'] . ".sqlite_master WHERE type = :type AND name LIKE :table_name", array(
+ ':type' => 'table',
+ ':table_name' => $info['table'],
+ ));
+ return $result->fetchAllKeyed(0, 0);
+ }
+}
diff --git a/core/includes/database/sqlite/select.inc b/core/includes/database/sqlite/select.inc
new file mode 100644
index 00000000000..fb926ef04d3
--- /dev/null
+++ b/core/includes/database/sqlite/select.inc
@@ -0,0 +1,27 @@
+<?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".
+ */
+
+