$uid)); * foreach ($result as $record) { * // Perform operations on $record->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 * * @see 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/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 Statement class, unless the driver opted out. if (!empty($this->statementClass)) { $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array($this->statementClass, array($this))); } } /** * Destroys this Connection object. * * PHP does not destruct an object if it is still referenced in other * variables. In case of PDO database connection objects, PHP only closes the * connection when the PDO object is destructed, so any references to this * object may cause the number of maximum allowed connections to be exceeded. */ public function destroy() { // Destroy all references to this connection by setting them to NULL. // The Statement class attribute only accepts a new value that presents a // proper callable, so we reset it to PDOStatement. $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('PDOStatement', array())); $this->schema = NULL; } /** * Returns the default query options for any given query. * * 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. * * @return DatabaseTransaction * A DatabaseTransaction object. * * @see DatabaseTransaction */ public function startTransaction($name = '') { $class = $this->getDriverClass('DatabaseTransaction'); return new $class($this, $name); } /** * Rolls back the transaction entirely or to a named savepoint. * * This method throws an exception if no transaction is active. * * @param $savepoint_name * The name of the savepoint. The default, 'drupal_transaction', will roll * the entire transaction back. * * @throws DatabaseTransactionNoActiveException * * @see DatabaseTransaction::rollback() */ public function rollback($savepoint_name = 'drupal_transaction') { if (!$this->supportsTransactions()) { return; } if (!$this->inTransaction()) { throw new DatabaseTransactionNoActiveException(); } // A previous rollback to an earlier savepoint may mean that the savepoint // in question has already been accidentally committed. if (!isset($this->transactionLayers[$savepoint_name])) { throw new DatabaseTransactionNoActiveException(); } // We need to find the point we're rolling back to, all other savepoints // before are no longer needed. If we rolled back other active savepoints, // we need to throw an exception. $rolled_back_other_active_savepoints = FALSE; while ($savepoint = array_pop($this->transactionLayers)) { if ($savepoint == $savepoint_name) { // If it is the last the transaction in the stack, then it is not a // savepoint, it is the transaction itself so we will need to roll back // the transaction rather than a savepoint. if (empty($this->transactionLayers)) { break; } $this->query('ROLLBACK TO SAVEPOINT ' . $savepoint); $this->popCommittableTransactions(); if ($rolled_back_other_active_savepoints) { throw new DatabaseTransactionOutOfOrderException(); } return; } else { $rolled_back_other_active_savepoints = TRUE; } } parent::rollBack(); if ($rolled_back_other_active_savepoints) { throw new DatabaseTransactionOutOfOrderException(); } } /** * Increases the depth of transaction nesting. * * If no transaction is already active, we begin a new transaction. * * @throws DatabaseTransactionNameNonUniqueException * * @see DatabaseTransaction */ public function pushTransaction($name) { if (!$this->supportsTransactions()) { return; } if (isset($this->transactionLayers[$name])) { throw new DatabaseTransactionNameNonUniqueException($name . " is already in use."); } // If we're already in a transaction then we want to create a savepoint // rather than try to create another transaction. if ($this->inTransaction()) { $this->query('SAVEPOINT ' . $name); } else { parent::beginTransaction(); } $this->transactionLayers[$name] = $name; } /** * Decreases the depth of transaction nesting. * * If we pop off the last transaction layer, then we either commit or roll * back the transaction as necessary. If no transaction is active, we return * because the transaction may have manually been rolled back. * * @param $name * The name of the savepoint * * @throws DatabaseTransactionNoActiveException * @throws DatabaseTransactionCommitFailedException * * @see DatabaseTransaction */ public function popTransaction($name) { if (!$this->supportsTransactions()) { return; } // The transaction has already been committed earlier. There is nothing we // need to do. If this transaction was part of an earlier out-of-order // rollback, an exception would already have been thrown by // Database::rollback(). if (!isset($this->transactionLayers[$name])) { return; } // Mark this layer as committable. $this->transactionLayers[$name] = FALSE; $this->popCommittableTransactions(); } /** * Internal function: commit all the transaction layers that can commit. */ protected function popCommittableTransactions() { // Commit all the committable layers. foreach (array_reverse($this->transactionLayers) as $name => $active) { // Stop once we found an active transaction. if ($active) { break; } // If there are no more layers left then we should commit. unset($this->transactionLayers[$name]); if (empty($this->transactionLayers)) { if (!parent::commit()) { throw new DatabaseTransactionCommitFailedException(); } } else { $this->query('RELEASE SAVEPOINT ' . $name); } } } /** * Runs a limited-range query on this database object. * * Use this as a substitute for ->query() when a subset of the query is to be * returned. User-supplied arguments to the query should be passed in as * separate parameters so that they can be properly escaped to avoid SQL * injection attacks. * * @param $query * A string containing an SQL query. * @param $args * An array of values to substitute into the query at placeholder markers. * @param $from * The first result row to return. * @param $count * The maximum number of result rows to return. * @param $options * An array of options on the query. * * @return DatabaseStatementInterface * A database query result resource, or NULL if the query was not executed * correctly. */ abstract public function queryRange($query, $from, $count, array $args = array(), array $options = array()); /** * Generates a temporary table name. * * @return * A table name. */ protected function generateTemporaryTableName() { return "db_temporary_" . $this->temporaryNameIndex++; } /** * Runs a SELECT query and stores its results in a temporary table. * * Use this as a substitute for ->query() when the results need to stored * in a temporary table. Temporary tables exist for the duration of the page * request. User-supplied arguments to the query should be passed in as * separate parameters so that they can be properly escaped to avoid SQL * injection attacks. * * Note that if you need to know how many results were returned, you should do * a SELECT COUNT(*) on the temporary table afterwards. * * @param $query * A string containing a normal SELECT SQL query. * @param $args * An array of values to substitute into the query at placeholder markers. * @param $options * An associative array of options to control how the query is run. See * the documentation for DatabaseConnection::defaultOptions() for details. * * @return * The name of the temporary table. */ abstract function queryTemporary($query, array $args = array(), array $options = array()); /** * Returns the type of database driver. * * This is not necessarily the same as the type of the database itself. For * instance, there could be two MySQL drivers, mysql and mysql_mock. This * function would return different values for each, but both would return * "mysql" for databaseType(). */ abstract public function driver(); /** * Returns the version of the database server. */ public function version() { return $this->getAttribute(PDO::ATTR_SERVER_VERSION); } /** * Determines if this driver supports transactions. * * @return * TRUE if this connection supports transactions, FALSE otherwise. */ public function supportsTransactions() { return $this->transactionSupport; } /** * Determines if this driver supports transactional DDL. * * DDL queries are those that change the schema, such as ALTER queries. * * @return * TRUE if this connection supports transactions for DDL queries, FALSE * otherwise. */ public function supportsTransactionalDDL() { return $this->transactionalDDLSupport; } /** * Returns the name of the PDO driver for this connection. */ abstract public function databaseType(); /** * Gets any special processing requirements for the condition operator. * * Some condition types require special processing, such as IN, because * the value data they pass in is not a simple value. This is a simple * overridable lookup function. Database connections should define only * those operators they wish to be handled differently than the default. * * @param $operator * The condition operator, such as "IN", "BETWEEN", etc. Case-sensitive. * * @return * The extra handling directives for the specified operator, or NULL. * * @see DatabaseCondition::compile() */ abstract public function mapConditionOperator($operator); /** * Throws an exception to deny direct access to transaction commits. * * We do not want to allow users to commit transactions at any time, only * by destroying the transaction object or allowing it to go out of scope. * A direct commit bypasses all of the safety checks we've built on top of * PDO's transaction routines. * * @throws DatabaseTransactionExplicitCommitNotAllowedException * * @see DatabaseTransaction */ public function commit() { throw new DatabaseTransactionExplicitCommitNotAllowedException(); } /** * Retrieves an unique id from a given sequence. * * Use this function if for some reason you can't use a serial field. For * example, MySQL has no ways of reading of the current value of a sequence * and PostgreSQL can not advance the sequence to be larger than a given * value. Or sometimes you just need a unique integer. * * @param $existing_id * After a database import, it might be that the sequences table is behind, * so by passing in the maximum existing id, it can be assured that we * never issue the same id. * * @return * An integer number larger than any number returned by earlier calls and * also larger than the $existing_id if one was passed in. */ abstract public function nextId($existing_id = 0); } /** * Primary front-controller for the database system. * * This class is uninstantiatable and un-extendable. It acts to encapsulate * all control and shepherding of database connections into a single location * without the use of globals. */ abstract class Database { /** * Flag to indicate a query call should simply return NULL. * * This is used for queries that have no reasonable return value anyway, such * as INSERT statements to a table without a serial primary key. */ const RETURN_NULL = 0; /** * Flag to indicate a query call should return the prepared statement. */ const RETURN_STATEMENT = 1; /** * Flag to indicate a query call should return the number of affected rows. */ const RETURN_AFFECTED = 2; /** * Flag to indicate a query call should return the "last insert id". */ const RETURN_INSERT_ID = 3; /** * An nested array of all active connections. It is keyed by database name * and target. * * @var array */ static protected $connections = array(); /** * A processed copy of the database connection information from settings.php. * * @var array */ static protected $databaseInfo = NULL; /** * A list of key/target credentials to simply ignore. * * @var array */ static protected $ignoreTargets = array(); /** * The key of the currently active database connection. * * @var string */ static protected $activeKey = 'default'; /** * An array of active query log objects. * * Every connection has one and only one logger object for all targets and * logging keys. * * array( * '$db_key' => DatabaseLog object. * ); * * @var array */ static protected $logs = array(); /** * Starts logging a given logging key on the specified connection. * * @param $logging_key * The logging key to log. * @param $key * The database connection key for which we want to log. * * @return DatabaseLog * The query log object. Note that the log object does support richer * methods than the few exposed through the Database class, so in some * cases it may be desirable to access it directly. * * @see DatabaseLog */ final public static function startLog($logging_key, $key = 'default') { if (empty(self::$logs[$key])) { self::$logs[$key] = new DatabaseLog($key); // Every target already active for this connection key needs to have the // logging object associated with it. if (!empty(self::$connections[$key])) { foreach (self::$connections[$key] as $connection) { $connection->setLogger(self::$logs[$key]); } } } self::$logs[$key]->start($logging_key); return self::$logs[$key]; } /** * Retrieves the queries logged on for given logging key. * * This method also ends logging for the specified key. To get the query log * to date without ending the logger request the logging object by starting * it again (which does nothing to an open log key) and call methods on it as * desired. * * @param $logging_key * The logging key to log. * @param $key * The database connection key for which we want to log. * * @return array * The query log for the specified logging key and connection. * * @see DatabaseLog */ final public static function getLog($logging_key, $key = 'default') { if (empty(self::$logs[$key])) { return NULL; } $queries = self::$logs[$key]->get($logging_key); self::$logs[$key]->end($logging_key); return $queries; } /** * Gets the connection object for the specified database key and target. * * @param $target * The database target name. * @param $key * The database connection key. Defaults to NULL which means the active key. * * @return DatabaseConnection * The corresponding connection object. */ final public static function getConnection($target = 'default', $key = NULL) { if (!isset($key)) { // By default, we want the active connection, set in setActiveConnection. $key = self::$activeKey; } // If the requested target does not exist, or if it is ignored, we fall back // to the default target. The target is typically either "default" or // "slave", indicating to use a slave SQL server if one is available. If // it's not available, then the default/master server is the correct server // to use. if (!empty(self::$ignoreTargets[$key][$target]) || !isset(self::$databaseInfo[$key][$target])) { $target = 'default'; } if (!isset(self::$connections[$key][$target])) { // If necessary, a new connection is opened. self::$connections[$key][$target] = self::openConnection($key, $target); } return self::$connections[$key][$target]; } /** * Determines if there is an active connection. * * Note that this method will return FALSE if no connection has been * established yet, even if one could be. * * @return * TRUE if there is at least one database connection established, FALSE * otherwise. */ final public static function isActiveConnection() { return !empty(self::$activeKey) && !empty(self::$connections) && !empty(self::$connections[self::$activeKey]); } /** * Sets the active connection to the specified key. * * @return * The previous database connection key. */ final public static function setActiveConnection($key = 'default') { if (empty(self::$databaseInfo)) { self::parseConnectionInfo(); } if (!empty(self::$databaseInfo[$key])) { $old_key = self::$activeKey; self::$activeKey = $key; return $old_key; } } /** * Process the configuration file for database information. */ final public static function parseConnectionInfo() { global $databases; $database_info = is_array($databases) ? $databases : array(); foreach ($database_info as $index => $info) { foreach ($database_info[$index] as $target => $value) { // If there is no "driver" property, then we assume it's an array of // possible connections for this target. Pick one at random. That allows // us to have, for example, multiple slave servers. if (empty($value['driver'])) { $database_info[$index][$target] = $database_info[$index][$target][mt_rand(0, count($database_info[$index][$target]) - 1)]; } // Parse the prefix information. if (!isset($database_info[$index][$target]['prefix'])) { // Default to an empty prefix. $database_info[$index][$target]['prefix'] = array( 'default' => '', ); } elseif (!is_array($database_info[$index][$target]['prefix'])) { // Transform the flat form into an array form. $database_info[$index][$target]['prefix'] = array( 'default' => $database_info[$index][$target]['prefix'], ); } } } if (!is_array(self::$databaseInfo)) { self::$databaseInfo = $database_info; } // Merge the new $database_info into the existing. // array_merge_recursive() cannot be used, as it would make multiple // database, user, and password keys in the same database array. else { foreach ($database_info as $database_key => $database_values) { foreach ($database_values as $target => $target_values) { self::$databaseInfo[$database_key][$target] = $target_values; } } } } /** * Adds database connection information for a given key/target. * * This method allows the addition of new connection credentials at runtime. * Under normal circumstances the preferred way to specify database * credentials is via settings.php. However, this method allows them to be * added at arbitrary times, such as during unit tests, when connecting to * admin-defined third party databases, etc. * * If the given key/target pair already exists, this method will be ignored. * * @param $key * The database key. * @param $target * The database target name. * @param $info * The database connection information, as it would be defined in * settings.php. Note that the structure of this array will depend on the * database driver it is connecting to. */ public static function addConnectionInfo($key, $target, $info) { if (empty(self::$databaseInfo[$key][$target])) { self::$databaseInfo[$key][$target] = $info; } } /** * Gets information on the specified database connection. * * @param $connection * The connection key for which we want information. */ final public static function getConnectionInfo($key = 'default') { if (empty(self::$databaseInfo)) { self::parseConnectionInfo(); } if (!empty(self::$databaseInfo[$key])) { return self::$databaseInfo[$key]; } } /** * Rename a connection and its corresponding connection information. * * @param $old_key * The old connection key. * @param $new_key * The new connection key. * @return * TRUE in case of success, FALSE otherwise. */ final public static function renameConnection($old_key, $new_key) { if (empty(self::$databaseInfo)) { self::parseConnectionInfo(); } if (!empty(self::$databaseInfo[$old_key]) && empty(self::$databaseInfo[$new_key])) { // Migrate the database connection information. self::$databaseInfo[$new_key] = self::$databaseInfo[$old_key]; unset(self::$databaseInfo[$old_key]); // Migrate over the DatabaseConnection object if it exists. if (isset(self::$connections[$old_key])) { self::$connections[$new_key] = self::$connections[$old_key]; unset(self::$connections[$old_key]); } return TRUE; } else { return FALSE; } } /** * Remove a connection and its corresponding connection information. * * @param $key * The connection key. * @return * TRUE in case of success, FALSE otherwise. */ final public static function removeConnection($key) { if (isset(self::$databaseInfo[$key])) { self::closeConnection(NULL, $key); unset(self::$databaseInfo[$key]); return TRUE; } else { return FALSE; } } /** * Opens a connection to the server specified by the given key and target. * * @param $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 a connection, it needs to be set to NULL and removed from the // static variable. In all cases, closeConnection() might be called for a // connection that was not opened yet, in which case the key is not defined // yet and we just ensure that the connection key is undefined. if (isset($target)) { if (isset(self::$connections[$key][$target])) { self::$connections[$key][$target]->destroy(); self::$connections[$key][$target] = NULL; } unset(self::$connections[$key][$target]); } else { if (isset(self::$connections[$key])) { foreach (self::$connections[$key] as $target => $connection) { self::$connections[$key][$target]->destroy(); self::$connections[$key][$target] = NULL; } } unset(self::$connections[$key]); } } /** * Instructs the system to temporarily ignore a given key/target. * * At times we need to temporarily disable 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/pdo.constants.php for the definition of the * constants used. * * @param $mode * One of the PDO::FETCH_* constants. * @param $a1 * An option depending of the fetch mode specified by $mode: * - for PDO::FETCH_COLUMN, the index of the column to fetch * - for PDO::FETCH_CLASS, the name of the class to create * - for PDO::FETCH_INTO, the object to add the data to * @param $a2 * If $mode is PDO::FETCH_CLASS, the optional arguments to pass to the * constructor. */ // public function setFetchMode($mode, $a1 = NULL, $a2 = array()); /** * Fetches the next row from a result set. * * See http://php.net/manual/pdo.constants.php for the definition of the * constants used. * * @param $mode * One of the PDO::FETCH_* constants. * Default to what was specified by setFetchMode(). * @param $cursor_orientation * Not implemented in all database drivers, don't use. * @param $cursor_offset * Not implemented in all database drivers, don't use. * * @return * A result, formatted according to $mode. */ // public function fetch($mode = NULL, $cursor_orientation = NULL, $cursor_offset = NULL); /** * Returns a single field from the next record of a result set. * * @param $index * The numeric index of the field to return. Defaults to the first field. * * @return * A single field from the next record, or FALSE if there is no next record. */ public function fetchField($index = 0); /** * Fetches the next row and returns it as an object. * * The object will be of the class specified by 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 SELECT query string and saves the result set to a temporary table. * * The execution of the query string happens against the active database. * * @param $query * The prepared SELECT statement query to run. Although it will accept both * named and unnamed placeholders, named placeholders are strongly preferred * as they are more self-documenting. * @param $args * An array of values to substitute into the query. If the query uses named * placeholders, this is an associative array in any order. If the query uses * 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". */ /** * @addtogroup 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 current name of 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 "addtogroup 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; } }