diff options
Diffstat (limited to 'core/includes/database')
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". + */ + + |