=== modified file 'includes/bootstrap.inc' --- includes/bootstrap.inc 2008-07-17 21:10:38 +0000 +++ includes/bootstrap.inc 2008-07-22 08:01:14 +0000 @@ -323,7 +323,7 @@ function conf_init() { global $base_url, $base_path, $base_root; // Export the following settings.php variables to the global namespace - global $db_url, $db_prefix, $cookie_domain, $conf, $installed_profile, $update_free_access; + global $databases, $db_prefix, $cookie_domain, $conf, $installed_profile, $update_free_access; $conf = array(); if (file_exists('./' . conf_path() . '/settings.php')) { @@ -511,11 +511,7 @@ function variable_get($name, $default) { function variable_set($name, $value) { global $conf; - $serialized_value = serialize($value); - db_query("UPDATE {variable} SET value = '%s' WHERE name = '%s'", $serialized_value, $name); - if (!db_affected_rows()) { - @db_query("INSERT INTO {variable} (name, value) VALUES ('%s', '%s')", $name, $serialized_value); - } + db_merge('variable')->key(array('name' => $name))->fields(array('value' => serialize($value)))->execute(); cache_clear_all('variables', 'cache'); @@ -804,24 +800,33 @@ function request_uri() { function watchdog($type, $message, $variables = array(), $severity = WATCHDOG_NOTICE, $link = NULL) { global $user, $base_root; - // Prepare the fields to be logged - $log_message = array( - 'type' => $type, - 'message' => $message, - 'variables' => $variables, - 'severity' => $severity, - 'link' => $link, - 'user' => $user, - 'request_uri' => $base_root . request_uri(), - 'referer' => referer_uri(), - 'ip' => ip_address(), - 'timestamp' => time(), + static $in_error_state = FALSE; + + // It is possible that the error handling will itself trigger an error. In that case, we could + // end up in an infinite loop. To avoid that, we implement a simple static semaphore. + if (!$in_error_state) { + $in_error_state = TRUE; + + // Prepare the fields to be logged + $log_message = array( + 'type' => $type, + 'message' => $message, + 'variables' => $variables, + 'severity' => $severity, + 'link' => $link, + 'user' => $user, + 'request_uri' => $base_root . request_uri(), + 'referer' => referer_uri(), + 'ip' => ip_address(), + 'timestamp' => time(), ); - // Call the logging hooks to log/process the message - foreach (module_implements('watchdog', TRUE) as $module) { - module_invoke($module, 'watchdog', $log_message); + // Call the logging hooks to log/process the message + foreach (module_implements('watchdog', TRUE) as $module) { + module_invoke($module, 'watchdog', $log_message); + } } + $in_error_state = FALSE; } /** @@ -964,9 +969,24 @@ function drupal_bootstrap($phase) { $current_phase = $phases[$phase_index]; unset($phases[$phase_index++]); _drupal_bootstrap($current_phase); + + global $_drupal_current_bootstrap_phase; + $_drupal_current_bootstrap_phase = $current_phase; } } +/** + * Return the current bootstrap phase for this Drupal process. The + * current phase is the one most recently completed by + * drupal_bootstrap(). + * + * @see drupal_bootstrap + */ +function drupal_get_bootstrap_phase() { + global $_drupal_current_bootstrap_phase; + return $_drupal_current_bootstrap_phase; +} + function _drupal_bootstrap($phase) { global $conf; @@ -994,9 +1014,9 @@ function _drupal_bootstrap($phase) { break; case DRUPAL_BOOTSTRAP_DATABASE: - // Initialize the default database. + // Initialize the database system. Note that the connection + // won't be initialized until it is actually requested. require_once './includes/database.inc'; - db_set_active(); // Register autoload functions so that we can access classes and interfaces. spl_autoload_register('drupal_autoload_class'); spl_autoload_register('drupal_autoload_interface'); @@ -1201,6 +1221,67 @@ function ip_address($reset = false) { } /** + * @ingroup schemaapi + * @{ + */ + +/** + * Get the schema definition of a table, or the whole database schema. + * + * The returned schema will include any modifications made by any + * module that implements hook_schema_alter(). + * + * @param $table + * The name of the table. If not given, the schema of all tables is returned. + * @param $rebuild + * If true, the schema will be rebuilt instead of retrieved from the cache. + */ +function drupal_get_schema($table = NULL, $rebuild = FALSE) { + static $schema = array(); + + if (empty($schema) || $rebuild) { + // Try to load the schema from cache. + if (!$rebuild && $cached = cache_get('schema')) { + $schema = $cached->data; + } + // Otherwise, rebuild the schema cache. + else { + $schema = array(); + // Load the .install files to get hook_schema. + module_load_all_includes('install'); + + // Invoke hook_schema for all modules. + foreach (module_implements('schema') as $module) { + $current = module_invoke($module, 'schema'); + _drupal_initialize_schema($module, $current); + $schema = array_merge($schema, $current); + } + + drupal_alter('schema', $schema); + + if (drupal_get_bootstrap_phase() == DRUPAL_BOOTSTRAP_FULL) { + cache_set('schema', $schema); + } + } + } + + if (!isset($table)) { + return $schema; + } + elseif (isset($schema[$table])) { + return $schema[$table]; + } + else { + return FALSE; + } +} + +/** + * @} End of "ingroup schemaapi". + */ + + +/** * @ingroup registry * @{ */ @@ -1422,4 +1503,3 @@ function registry_get_hook_implementatio /** * @} End of "ingroup registry". */ - === modified file 'includes/cache.inc' --- includes/cache.inc 2008-07-02 20:42:25 +0000 +++ includes/cache.inc 2008-07-22 07:28:50 +0000 @@ -29,7 +29,6 @@ function cache_get($cid, $table = 'cache // If the data is permanent or we're not enforcing a minimum cache lifetime // always return the cached data. if ($cache->expire == CACHE_PERMANENT || !variable_get('cache_lifetime', 0)) { - $cache->data = db_decode_blob($cache->data); if ($cache->serialized) { $cache->data = unserialize($cache->data); } @@ -101,16 +100,22 @@ function cache_get($cid, $table = 'cache * A string containing HTTP header information for cached pages. */ function cache_set($cid, $data, $table = 'cache', $expire = CACHE_PERMANENT, $headers = NULL) { - $serialized = 0; + $fields = array( + 'serialized' => 0, + 'created' => time(), + 'expire' => $expire, + 'headers' => $headers, + ); if (!is_string($data)) { - $data = serialize($data); - $serialized = 1; + $fields['data'] = serialize($data); + $fields['serialized'] = 1; } - $created = time(); - db_query("UPDATE {" . $table . "} SET data = %b, created = %d, expire = %d, headers = '%s', serialized = %d WHERE cid = '%s'", $data, $created, $expire, $headers, $serialized, $cid); - if (!db_affected_rows()) { - @db_query("INSERT INTO {" . $table . "} (cid, data, created, expire, headers, serialized) VALUES ('%s', %b, %d, %d, '%s', %d)", $cid, $data, $created, $expire, $headers, $serialized); + else { + $fields['data'] = $data; + $fields['serialized'] = 0; } + + db_merge($table)->key(array('cid' => $cid))->fields($fields)->execute(); } /** @@ -170,14 +175,14 @@ function cache_clear_all($cid = NULL, $t else { if ($wildcard) { if ($cid == '*') { - db_query("DELETE FROM {" . $table . "}"); + db_delete($table)->execute(); } else { - db_query("DELETE FROM {" . $table . "} WHERE cid LIKE '%s%%'", $cid); + db_delete($table)->condition('cid', $cid .'%', 'LIKE')->execute(); } } else { - db_query("DELETE FROM {" . $table . "} WHERE cid = '%s'", $cid); + db_delete($table)->condition('cid', $cid)->execute(); } } } === modified file 'includes/common.inc' --- includes/common.inc 2008-07-19 10:38:13 +0000 +++ includes/common.inc 2008-07-22 07:28:50 +0000 @@ -2467,6 +2467,8 @@ function _drupal_bootstrap_full() { fix_gpc_magic(); // Load all enabled modules module_load_all(); + // Rebuild the module hook cache + module_implements('', NULL, TRUE); // Let all modules take action before menu system handles the request // We do not want this while running update.php. if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') { @@ -2643,7 +2645,6 @@ function drupal_system_listing($mask, $d return $files; } - /** * This dispatch function hands off structured Drupal arrays to type-specific * *_alter implementations. It ensures a consistent interface for all altering @@ -2692,7 +2693,6 @@ function drupal_alter($type, &$data) { } } - /** * Renders HTML given a structured array tree. * @@ -3031,54 +3031,6 @@ function drupal_common_theme() { */ /** - * Get the schema definition of a table, or the whole database schema. - * - * The returned schema will include any modifications made by any - * module that implements hook_schema_alter(). - * - * @param $table - * The name of the table. If not given, the schema of all tables is returned. - * @param $rebuild - * If true, the schema will be rebuilt instead of retrieved from the cache. - */ -function drupal_get_schema($table = NULL, $rebuild = FALSE) { - static $schema = array(); - - if (empty($schema) || $rebuild) { - // Try to load the schema from cache. - if (!$rebuild && $cached = cache_get('schema')) { - $schema = $cached->data; - } - // Otherwise, rebuild the schema cache. - else { - $schema = array(); - // Load the .install files to get hook_schema. - module_load_all_includes('install'); - - // Invoke hook_schema for all modules. - foreach (module_implements('schema') as $module) { - $current = module_invoke($module, 'schema'); - _drupal_initialize_schema($module, $current); - $schema = array_merge($schema, $current); - } - - drupal_alter('schema', $schema); - cache_set('schema', $schema); - } - } - - if (!isset($table)) { - return $schema; - } - elseif (isset($schema[$table])) { - return $schema[$table]; - } - else { - return FALSE; - } -} - -/** * Create all tables that a module defines in its hook_schema(). * * Note: This function does not pass the module's schema through @@ -3123,7 +3075,9 @@ function drupal_uninstall_schema($module $ret = array(); foreach ($schema as $table) { - db_drop_table($ret, $table['name']); + if (db_table_exists($table['name'])) { + db_drop_table($ret, $table['name']); + } } return $ret; } === modified file 'includes/database.inc' --- includes/database.inc 2008-07-19 12:31:14 +0000 +++ includes/database.inc 2008-07-22 08:01:14 +0000 @@ -3,7 +3,7 @@ /** * @file - * Wrapper for database interface code. + * Base classes for the database layer. */ /** @@ -18,13 +18,18 @@ define('DB_ERROR', 'a515ac9c2796ca0e23ad * @{ * Allow the use of different database servers using the same code base. * - * Drupal provides a slim 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, while - * letting Drupal control the pieces of queries that need to be written - * differently for different servers and provide basic security checks. + * 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. * - * Most Drupal database queries are performed by a call to db_query() or + * 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 pager_query() for * queries that return results that need to be presented on multiple pages, and * tablesort_sql() for generating appropriate queries for sortable tables. @@ -37,219 +42,3537 @@ define('DB_ERROR', 'a515ac9c2796ca0e23ad * one would instead call the Drupal functions: * @code * $result = db_query_range('SELECT n.title, n.body, n.created - * FROM {node} n WHERE n.uid = %d', $uid, 0, 10); - * while ($node = db_fetch_object($result)) { + * FROM {node} n WHERE n.uid = :uid', array(':uid' => $uid), 0, 10); + * foreach($result as $record) { * // Perform operations on $node->body, etc. here. * } * @endcode * Curly braces are used around "node" to provide table prefixing via - * db_prefix_tables(). 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 - * common pattern of iterating over the result set using db_fetch_object(). + * 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 foreach() over the result set. + * + * + * 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('my_query', '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. + */ + + +/** + * 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. + * + * @link http://us.php.net/manual/en/ref.pdo.php + */ +abstract class DatabaseConnection extends PDO { + + /** + * Reference to the last statement that was executed. + * + * We only need this for the legacy db_affected_rows() call, which will be removed. + * + * @var DatabaseStatement + * @todo Remove this variable. + */ + public $lastStatement; + + function __construct($dsn, $username, $password, $driver_options = array()) { + $driver_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION; // Because the other methods don't seem to work right. + parent::__construct($dsn, $username, $password, $driver_options); + $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('DatabaseStatement', array($this))); + } + + /** + * Return the default query options for any given query. + * + * A given query can be customized with a number of option flags in an associative array. + * + * return_affected - If true, this method will return the number of rows + * affected by the previous query. If false, this function will return + * the executed statement. It should be set to TRUE for INSERT, UPDATE, + * and DELETE queries and FALSE otherwise. In most cases, this will be set + * by the database system correctly and a module author should not set it. + * + * 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://www.php.net/PDOStatement-fetch + * + * 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. + * + * 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 supress 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, + 'already_prepared' => FALSE, + ); + } + + /** + * Append 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 if + * necessary. + * + * @param $sql + * A string containing a partial or entire SQL query. + * @return + * The properly-prefixed string. + */ + protected function prefixTables($sql) { + global $db_prefix; + + if (is_array($db_prefix)) { + if (array_key_exists('default', $db_prefix)) { + $tmp = $db_prefix; + unset($tmp['default']); + foreach ($tmp as $key => $val) { + $sql = strtr($sql, array('{' . $key . '}' => $val . $key)); + } + return strtr($sql, array('{' => $db_prefix['default'] , '}' => '')); + } + else { + foreach ($db_prefix as $key => $val) { + $sql = strtr($sql, array('{' . $key . '}' => $val . $key)); + } + return strtr($sql, array('{' => '' , '}' => '')); + } + } + else { + return strtr($sql, array('{' => $db_prefix , '}' => '')); + } + } + + /** + * Prepare a query string and return the prepared statement. This + * method statically 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 + * A PDO prepared statement ready for its execute() method. + */ + protected function prepareQuery($query) { + static $statements = array(); + $query = self::prefixTables($query); + if (empty($statements[$query])) { + $statements[$query] = parent::prepare($query); + } + return $statements[$query]; + } + + public function makeSequenceName($table, $field) { + return $this->prefixTables('{'. $table .'}_'. $field .'_seq'); + } + + /** + * 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. This method statically caches those + * prepared statements, reusing them when possible. + * + * @param $query + * The query string to execute, as a prepared statement. If + * $options['already_prepared'] is TRUE, this parameter is presumed + * to be an already-prepared DatabaseStatement object. + * @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 + * 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 id 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. + */ + protected function runQuery($query, Array $args = array(), $options = array()) { + + $options += $this->defaultOptions(); + + try { + if ($query instanceof DatabaseStatement) { + $stmt = $query; + $stmt->execute(NULL, $options); + } + else { + $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(); + case Database::RETURN_NULL: + return; + default: + throw new PDOException('Invalid return directive: ' . $options['return']); + } + } + catch (PDOException $e) { + if (!function_exists('module_implements')) { + _db_need_install(); + } + //watchdog('database', var_export($e, TRUE) . $e->getMessage(), NULL, WATCHDOG_ERROR); + if ($options['throw_exception']) { + if ($query instanceof DatabaseStatement) { + $query_string = $stmt->queryString; + } + else { + $query_string = $query; + } + throw new PDOException($query_string . " - \n" . print_r($args,1) . $e->getMessage()); + } + return NULL; + } + } + + /** + * Executes a prepared statement with bound variables against the database. + * + * + * + * @param $stmt + * The prepared statement object to execute. All parameters must + * have been bound already. + * @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 + * 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 id 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. + */ + public function runBoundQuery($stmt, Array $args = array(), $options = array()) { + try { + $stmt = $this->prepareQuery($query); + $options += $this->defaultOptions(); + + $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(); + default: + throw new PDOException('Invalid return directive: ' . $options['return']); + } + } + catch (PDOException $e) { + if (!function_exists('module_implements')) { + _db_need_install(); + } + watchdog('database', var_export($e, TRUE) . $e->getMessage(), NULL, WATCHDOG_ERROR); + if ($options['throw_exception']) { + throw new PDOException($query . " - \n" . print_r($args,1) . $e->getMessage()); + } + return NULL; + } + } + + + /** + * Execute an arbitrary query string against this database. + * + * @param $query + * A string containing an SQL query. + * @param $args + * An array of values to substitute into the query at placeholder markers. + * @param $options + * An array of options on the query. + * @return + * A database query result resource, or NULL if the query was not executed + * correctly. + */ + public function query($query, Array $args = array(), Array $options = array()) { + return $this->runQuery($query, $args, $options); + } + + /** + * Prepare and return a SELECT query object with the specified ID. + * + * @see SelectQuery + * @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 + * A new SelectQuery object. + */ + public function select($table, $alias = NULL, Array $options = array()) { + $class_type = 'SelectQuery_' . $this->driver(); + return new $class_type($table, $alias, $this, $options); + } + + /** + * Prepare and return an INSERT query object with the specified ID. + * + * @see InsertQuery + * @param $options + * An array of options on the query. + * @return + * A new InsertQuery object. + */ + public function insert($table, Array $options = array()) { + $class_type = 'InsertQuery_' . $this->driver(); + return new $class_type($this, $table, $options); + } + + /** + * Prepare and return a MERGE query object with the specified ID. + * + * @see MergeQuery + * @param $options + * An array of options on the query. + * @return + * A new MergeQuery object. + */ + public function merge($table, Array $options = array()) { + $class_type = 'MergeQuery_' . $this->driver(); + return new $class_type($this, $table, $options); + } + + /** + * Prepare and return an UPDATE query object with the specified ID. + * + * @see UpdateQuery + * @param $options + * An array of options on the query. + * @return + * A new UpdateQuery object. + */ + public function update($table, Array $options = array()) { + $class_type = 'UpdateQuery_' . $this->driver(); + return new $class_type($this, $table, $options); + } + + /** + * Prepare and return a DELETE query object with the specified ID. + * + * @see DeleteQuery + * @param $options + * An array of options on the query. + * @return + * A new DeleteQuery object. + */ + public function delete($table, Array $options = array()) { + $class_type = 'DeleteQuery_' . $this->driver(); + return new $class_type($this, $table, $options); + } + + /** + * Returns a DatabaseSchema object for manipulating the schema of this database. + * + * This method will lazy-load the appropriate schema library file. + * + * @return + * The DatabaseSchema object for this connection. + */ + public function schema() { + static $schema; + if (empty($schema)) { + require_once('./includes/schema.inc'); + require_once('./includes/schema.' . $this->driver() . '.inc'); + $class_type = 'DatabaseSchema_' . $this->driver(); + $schema = new $class_type($this); + } + return $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_]+/', '', $string); + } + + /** + * Returns a new DatabaseTransaction object on this connection. + * + * @param $required + * If executing an operation that absolutely must use transactions, specify + * TRUE for this parameter. If the connection does not support transactions, + * this method will throw an exception and the operation will not be possible. + * @see DatabaseTransaction + */ + public function startTransaction($required = FALSE) { + if ($required && !$this->supportsTransactions()) { + throw new TransactionsNotSupportedException(); + } + $class_type = 'DatabaseTransaction_' . $this->driver(); + return new $class_type($this); + } + + /** + * 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 + * A database query result resource, or NULL if the query was not executed + * correctly. + */ + abstract public function queryRange($query, Array $args, $from, $count, Array $options); + + /** + * 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 $tablename + * The name of the temporary table to select into. This name will not be + * prefixed as there is no risk of collision. + * @return + * A database query result resource, or FALSE if the query was not executed + * correctly. + */ + abstract function queryTemporary($query, Array $args, $tablename); + + /** + * 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(); + + /** + * Determine if this driver supports transactions. + */ + abstract public function supportsTransactions(); + + /** + * Returns the type of the database being accessed. + */ + abstract public function databaseType(); + + /** + * Declare the table and serial column affected by the previous + * INSERT query so that lastInsertId() can work on drivers that + * require this information. This is an internal function but is + * declared public because db_last_insert_id() needs to use it (PHP + * does not support "friend" functions like C++). + */ + public function setLastInsertInfo($table, $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. Database connections should define only + * those operators they wish to be handled differently than the default. + * + * @see DatabaseCondition::compile(). + * @param $operator + * The condition operator, such as "IN", "BETWEEN", etc. Case-sensitive. + * @return + * The extra handling directives for the specified operator, or NULL. + */ + abstract public function mapConditionOperator($operator); +} + +/** + * 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; + + /** + * Flag to indicate a query should have its arguments bound to it explicitly. + * + */ + const ARGUMENT_BOUND = 2; + + /** + * 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; + + /** + * The key of the currently active database connection. + * + * @var string + */ + static protected $activeKey = 'default'; + + /** + * Gets the active connection object for the specified target. + * + * @return + * The active connection object. + */ + final public static function getActiveConnection($target = 'default') { + return self::getConnection(self::$activeKey, $target); + } + + /** + * Gets the connection object for the specified database key and target. + * + * @return + * The corresponding connection object. + */ + final public static function getConnection($key = 'default', $target = 'default') { + if (!isset(self::$connections[$key][$target])) { + self::openConnection($key, $target); + } + + return isset(self::$connections[$key][$target]) ? self::$connections[$key][$target] : NULL; + } + + /** + * Determine 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::$connections); + } + + /** + * Set 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; + } + } + + /** + * Parse out the database connection information specified in the config + * file and specify defaults where necessary. + */ + final protected static function parseConnectionInfo() { + global $databases; + + if (empty($databases)) { + _db_need_install(); + } + $databaseInfo = $databases; + + // If no database key is specified, default to default. + if (!is_array($databaseInfo)) { + $databaseInfo = array('default' => $databaseInfo); + } + + foreach ($databaseInfo as $index => $info) { + // If no targets are specified, default to one default. + if (!is_array($databaseInfo[$index])) { + $databaseInfo[$index] = array('default' => $info); + } + + foreach ($databaseInfo[$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'])) { + $databaseInfo[$index][$target] = $databaseInfo[$index][$target][mt_rand(0, count($databaseInfo[$index][$target]) - 1)]; + } + } + } + + self::$databaseInfo = $databaseInfo; + } + + /** + * Open a connection to the server specified by the given + * key and target. + * + * @param $key + * @param $target + */ + final protected static function openConnection($key, $target) { + global $db_prefix; + + if (empty(self::$connectionInfo)) { + self::parseConnectionInfo(); + } + try { + // If the requested database does not exist then it is an unrecoverable error. + // If the requested target does not exist, however, 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 (!isset(self::$databaseInfo[$key])) { + throw new Exception('DB does not exist'); + } + if (!isset(self::$databaseInfo[$key][$target])) { + $target = 'default'; + } + + if (!$driver = self::$databaseInfo[$key][$target]['driver']) { + throw new Exception('Drupal is not set up'); + } + $driver_class = 'DatabaseConnection_' . $driver; + $driver_file = './includes/database.' . $driver . '.inc'; + require_once($driver_file); + self::$connections[$key][$target] = new $driver_class(self::$databaseInfo[$key][$target]); + + // We need to pass around the simpletest database prefix in the request + // and we put that in the user_agent header. + if (preg_match("/^simpletest\d+$/", $_SERVER['HTTP_USER_AGENT'])) { + $db_prefix = $_SERVER['HTTP_USER_AGENT']; + } + } + catch (Exception $e) { + _db_need_install(); + throw $e; + // TODO. error handling. + } + } +} + +class TransactionsNotSupportedException extends PDOException { } + +/** + * 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; + + /** + * Whether or not this connection supports transactions. + * + * This can be derived from the connection itself with a method call, + * but is cached here for performance. + * + * @var boolean + */ + protected $supportsTransactions; + + /** + * Whether or not this transaction has been rolled back. + * + * @var boolean + */ + protected $hasRolledBack = FALSE; + + /** + * Whether or not this transaction has been committed. + * + * @var boolean + */ + protected $hasCommitted = FALSE; + + /** + * Track 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 int + */ + protected static $layers = 0; + + public function __construct(DatabaseConnection $connection) { + $this->connection = $connection; + $this->supportsTransactions = $connection->supportsTransactions(); + + if (self::$layers == 0 && $this->supportsTransactions) { + $connection->beginTransaction(); + } + + ++self::$layers; + } + + /** + * Commit this transaction. + */ + public function commit() { + --self::$layers; + if (self::$layers == 0 && $this->supportsTransactions) { + $this->connection->commit(); + $this->hasCommitted = TRUE; + } + } + + /** + * Roll back this transaction. + */ + public function rollBack() { + if ($this->supportsTransactions) { + $this->connection->rollBack(); + $this->hasRolledBack = TRUE; + } + } + + /** + * Determine if this transaction has already been rolled back. + * + * @return + * TRUE if the transaction has been rolled back, FALSE otherwise. + */ + public function hasRolledBack() { + return $this->hasRolledBack; + } + + public function __destruct() { + --self::$layers; + if (self::$layers == 0 && $this->supportsTransactions && !$this->hasRolledBack && !$this->hasCommitted) { + $this->connection->commit(); + } + } + +} + +/** + * Prepared statement class. + * + * 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. + * + * @link http://us.php.net/manual/en/ref.pdo.php + */ +class DatabaseStatement extends PDOStatement { + + public $dbh; + + protected function __construct($dbh) { + $this->dbh = $dbh; + $this->setFetchMode(PDO::FETCH_OBJ); + } + + /** + * 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, $options) { + if (isset($options['fetch'])) { + if (is_string($options['fetch'])) { + $this->setFetchMode(PDO::FETCH_CLASS, $options['fetch']); + } + else { + $this->setFetchMode($options['fetch']); + } + } + $this->dbh->lastStatement = $this; + return parent::execute($args); + } + + /** + * 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. + */ + public function fetchCol($index = 0) { + return $this->fetchAll(PDO::FETCH_COLUMN, $index); + } + + /** + * Returns an entire result set as an associative array of stdClass objects, keyed by the named field. + * + * If the given key appears multiple times, later records will overwrite earlier ones. + * + * Note that this method will run the result set to the end. + * + * @param $key + * The name of the field on which to index the array. + * @return + * An associative array. + */ + public function fetchAllAssoc($key) { + $return = array(); + $this->setFetchMode(PDO::FETCH_OBJ); + foreach ($this as $record) { + $return[$record->$key] = $record; + } + return $return; + } + + /** + * 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. + */ + 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; + } + + /** + * Return a single field out of the current + * + * @param $index + * The numeric index of the field to return. Defaults to the first field. + * @return + * A single field from the next record. + */ + public function fetchField($index = 0) { + return $this->fetchColumn($index); + } + + /** + * 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. + */ + public function fetchAssoc() { + return $this->fetch(PDO::FETCH_ASSOC); + } +} + +/** + * Interface for a conditional clause in a query. + */ +interface QueryConditionInterface { + + /** + * Helper function to build 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 =. + * + * @param $field + * The name of the field to check. + * @param $operator + * The comparison operator, such as =, <, or >=. It also accepts more complex + * options such as IN, LIKE, or BETWEEN. + * @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 $num_args + * For internal use only. This argument is used to track the recursive calls when + * processing complex conditions. + * @return + * The called object. + */ + public function condition($field, $operator = NULL, $value = NULL); + + /** + * Add 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 + * The called object. + */ + public function where($snippet, $args = array()); + + /** + * 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: + * + * array( + * 'field' => $field, + * 'value' => $value, + * 'operator' => $operator, + * ); + * + * 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. + * + * @returns + * 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. + */ + public function compile(DatabaseConnection $connection); +} + + +/** + * 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. + */ + 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. + * + */ + 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); +} + +/** + * Base class for the query builders. + * + * All query builders inherit from a common base class. + */ +abstract class Query { + + /** + * The connection object on which to run this query. + * + * @var DatabaseConnection + */ + protected $connection; + + /** + * The query options to pass on to the connection object. + * + * @var array + */ + protected $queryOptions; + + public function __construct(DatabaseConnection $connection, $options) { + $this->connection = $connection; + $this->queryOptions = $options; + } + + /** + * Run the query against the database. + */ + abstract protected function execute(); + + /** + * Returns the query as a prepared statement string. + */ + abstract protected function __toString(); +} + +/** + * General class for an abstracted INSERT operation. + */ +abstract class InsertQuery extends Query { + + /** + * The table on which to insert. + * + * @var string + */ + protected $table; + + /** + * Whether or not this query is "delay-safe". Different database drivers + * may or may not implement this feature in their own ways. + * + * @var boolean + */ + protected $delay; + + /** + * 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. + * + * @var array + */ + protected $defaultFields = array(); + + /** + * A nested array of values to insert. + * + * $insertValues itself is an array of arrays. Each sub-array is an array of + * field names to values to insert. 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 efficient. The order of values in each + * sub-array must match the order of fields in $insertFields. + * + * @var string + */ + protected $insertValues = array(); + + public function __construct($connection, $table, Array $options = array()) { + $options['return'] = Database::RETURN_INSERT_ID; + $options += array('delay' => FALSE); + parent::__construct($connection, $options); + $this->table = $table; + } + + /** + * Add 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 + * 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; + } + + /** + * Add another set of values to the query to be inserted. + * + * If $values is a numeric 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 + * 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; + } + + /** + * Specify fields for which the database-defaults should be used. + * + * 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 + * The called object. + */ + public function useDefaults(Array $fields) { + $this->defaultFields = $fields; + 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. + */ + public function execute() { + + $last_insert_id = 0; + + // Confirm that the user did not try to specify an identical + // field and default field. + if (array_intersect($this->insertFields, $this->defaultFields)) { + throw new PDOException('You may not specify the same field to have a value and a schema-default value.'); + } + + // 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(); + $sql = (string)$this; + foreach ($this->insertValues as $insert_values) { + $last_insert_id = $this->connection->runQuery($sql, $insert_values, $this->queryOptions); + } + $transaction->commit(); + + // Re-initialize the values array so that we can re-use this query. + $this->insertValues = array(); + + return $last_insert_id; + } + + public function __toString() { + + // Default fields are always placed first for consistency. + $insert_fields = array_merge($this->defaultFields, $this->insertFields); + + // 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 'INSERT INTO {'. $this->table .'} ('. implode(', ', $insert_fields) .') VALUES ('. implode(', ', $placeholders) .')'; + } +} + +/** + * General class for an abstracted MERGE operation. + */ +abstract class MergeQuery 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 to update instead of the values specified in + * $insertFields; + * + * @var array + */ + protected $updateFields = array(); + + /** + * An array of key fields for this query. + * + * @var array + */ + protected $keyFields = array(); + + /** + * An array of fields to not update in case of a duplicate record. + * + * @var array + */ + protected $excludeFields = array(); + + /** + * An array of fields to update to an expression in case of a duplicate record. + * + * This variable is a nested array in the following format: + * => array( + * 'condition' => + * 'arguments' => + * ); + * + * @var array + */ + protected $expressionFields = array(); + + public function __construct($connection, $table, Array $options = array()) { + $options['return'] = Database::RETURN_AFFECTED; + parent::__construct($connection, $options); + $this->table = $table; + } + + /** + * Set the field->value pairs to be merged into the table. + * + * 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 set. + * @param $values + * An array of fields to set into the database. The values must be + * specified in the same order as the $fields array. + * @return + * The called object. + */ + public function fields(Array $fields, Array $values = array()) { + if (count($values) > 0) { + $fields = array_combine($fields, $values); + } + $this->insertFields = $fields; + + return $this; + } + + /** + * Set the key field(s) to be used to insert or update into the table. + * + * 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. + * + * These fields are the "pivot" fields of the query. Typically they + * will be the fields of the primary key. If the record does not + * yet exist, they will be inserted into the table along with the + * values set in the fields() method. If the record does exist, + * these fields will be used in the WHERE clause to select the + * record to update. + * + * @param $fields + * An array of fields to set. + * @param $values + * An array of fields to set into the database. The values must be + * specified in the same order as the $fields array. + * @return + * The called object. + */ + public function key(Array $fields, Array $values = array()) { + if ($values) { + $fields = array_combine($fields, $values); + } + $this->keyFields = $fields; + + return $this; + } + + /** + * Specify fields to update in case of a duplicate record. + * + * If a record with the values in keys() already exists, the fields and values + * specified here will be updated in that record. If this method is not called, + * it defaults to the same values as were passed to the fields() method. + * + * @param $fields + * An array of fields to set. + * @param $values + * An array of fields to set into the database. The values must be + * specified in the same order as the $fields array. + * @return + * The called object. + */ + public function update(Array $fields, Array $values = array()) { + if ($values) { + $fields = array_combine($fields, $values); + } + $this->updateFields = $fields; + + return $this; + } + + /** + * Specify fields that should not be updated in case of a duplicate record. + * + * If this method is called and a record with the values in keys() already + * exists, Drupal will instead update the record with the values passed + * in the fields() method except for the fields specified in this method. That + * is, calling this method is equivalent to calling update() with identical + * parameters as fields() minus the keys specified here. + * + * The update() method takes precedent over this method. If update() is called, + * this method has no effect. + * + * @param $exclude_fields + * An array of fields in the query that should not be updated to match those + * specified by the fields() method. + * Alternatively, the fields may be specified as a variable number of string + * parameters. + * @return + * The called object. + */ + public function updateExcept($exclude_fields) { + if (!is_array($exclude_fields)) { + $exclude_fields = func_get_args(); + } + $this->excludeFields = $exclude_fields; + + return $this; + } + + /** + * Specify fields to be updated as an expression. + * + * Expression fields are cases such as counter=counter+1. This method only + * applies if a duplicate key is detected. This method takes precedent over + * both update() and updateExcept(). + * + * @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 + * The called object. + */ + public function expression($field, $expression, Array $arguments = NULL) { + $this->expressionFields[$field] = array( + 'expression' => $expression, + 'arguments' => $arguments, + ); + + return $this; + } + + public function execute() { + + // In the degenerate case of this query type, we have to run multiple + // queries as there is no universal single-query mechanism that will work. + // Our degenerate case is not designed for performance efficiency but + // for comprehensibility. Any practical database driver will override + // this method with database-specific logic, so this function serves only + // as a fallback to aid developers of new drivers. + + //Wrap multiple queries in a transaction, if the database supports it. + $transaction = $this->connection->startTransaction(); + + // Manually check if the record already exists. + $select = $this->connection->select($this->table); + foreach ($this->keyFields as $field => $value) { + $select->condition($field, $value); + } + + $select = $select->countQuery(); + $sql = (string)$select; + $arguments = $select->getArguments(); + $num_existing = db_query($sql, $arguments)->fetchField(); + + + if ($num_existing) { + // If there is already an existing record, run an update query. + + if ($this->updateFields) { + $update_fields = $this->updateFields; + } + else { + $update_fields = $this->insertFields; + // If there are no exclude fields, this is a no-op. + foreach ($this->excludeFields as $exclude_field) { + unset($update_fields[$exclude_field]); + } + } + $update = $this->connection->update($this->table, $this->queryOptions)->fields($update_fields); + foreach ($this->keyFields as $field => $value) { + $update->condition($field, $value); + } + foreach ($this->expressionFields as $field => $expression) { + $update->expression($field, $expression['expression'], $expression['arguments']); + } + $update->execute(); + } + else { + // If there is no existing record, run an insert query. + $insert_fields = $this->insertFields + $this->keyFields; + $this->connection->insert($this->table, $this->queryOptions)->fields($insert_fields)->execute(); + } + + // Commit the transaction. + $transaction->commit(); + } + + public function __toString() { + // In the degenerate case, there is no string-able query as this operation + // is potentially two queries. + return ''; + } +} + + +/** + * General class for an abstracted DELETE operation. + * + * The conditional WHERE handling of this class is all inherited from Query. + */ +abstract 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; + + 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'); + } + + public function condition($field, $value = NULL, $operator = '=') { + if (!isset($num_args)) { + $num_args = func_num_args(); + } + $this->condition->condition($field, $value, $operator, $num_args); + return $this; + } + + public function &conditions() { + return $this->condition->conditions(); + } + + public function arguments() { + return $this->condition->arguments(); + } + + public function where($snippet, $args = array()) { + $this->condition->where($snippet, $args); + return $this; + } + + public function compile(DatabaseConnection $connection) { + return $this->condition->compile($connection); + } + + public function execute() { + $values = array(); + if (count($this->condition)) { + $this->condition->compile($this->connection); + $values = $this->condition->arguments(); + } + + return $this->connection->runQuery((string)$this, $values, $this->queryOptions); + } + + public function __toString() { + $query = 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '} '; + + if (count($this->condition)) { + $this->condition->compile($this->connection); + $query .= "\nWHERE " . $this->condition; + } + + return $query; + } +} + +/** + * General class for an abstracted UPDATE operation. + * + * The conditional WHERE handling of this class is all inherited from Query. + */ +abstract 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; + + /** + * 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; + + /** + * An array of fields to update to an expression in case of a duplicate record. + * + * This variable is a nested array in the following format: + * => array( + * 'condition' => + * 'arguments' => + * ); + * + * @var array + */ + protected $expressionFields = array(); + + + 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'); + } + + public function condition($field, $value = NULL, $operator = '=') { + if (!isset($num_args)) { + $num_args = func_num_args(); + } + $this->condition->condition($field, $value, $operator, $num_args); + return $this; + } + + public function &conditions() { + return $this->condition->conditions(); + } + + public function arguments() { + return $this->condition->arguments(); + } + + public function where($snippet, $args = array()) { + $this->condition->where($snippet, $args); + return $this; + } + + public function compile(DatabaseConnection $connection) { + return $this->condition->compile($connection); + } + + /** + * Add 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 while the values are the values to which to set them. + * @return + * The called object. + */ + public function fields(Array $fields) { + $this->fields = $fields; + return $this; + } + + /** + * Specify 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 + * The called object. + */ + public function expression($field, $expression, Array $arguments = NULL) { + $this->expressionFields[$field] = array( + 'expression' => $expression, + 'arguments' => $arguments, + ); + + return $this; + } + + 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); + $update_values = array_merge($update_values, $this->condition->arguments()); + } + + return $this->connection->runQuery((string)$this, $update_values, $this->queryOptions); + } + + public function __toString() { + // 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 = 'UPDATE {' . $this->connection->escapeTable($this->table) . '} SET ' . implode(', ', $update_fields); + + if (count($this->condition)) { + $this->condition->compile($this->connection); + // There is an implicit string cast on $this->condition. + $query .= "\nWHERE " . $this->condition; + } + + return $query; + } + +} + + +/** + * Abstract query builder for SELECT statements. + */ +abstract class SelectQuery extends Query implements QueryConditionInterface, QueryAlterableInterface { + + /** + * 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' => $name_of_table, + * 'alias' => $alias_of_the_table, + * 'condition' => $condition_clause_on_which_to_join, + * 'arguments' => $array_of_arguments_for_placeholders_in_the condition. + * ) + * + * @var array + */ + protected $tables = array(); + + /** + * The values to insert into the prepared statement of this query. + * + * @var array + */ + //protected $arguments = 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; + + 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; + } + + 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; + } + + 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 = '=') { + if (!isset($num_args)) { + $num_args = func_num_args(); + } + $this->where->condition($field, $value, $operator, $num_args); + return $this; + } + + public function &conditions() { + return $this->where->conditions(); + } + + public function arguments() { + return $this->where->arguments(); + } + + public function where($snippet, $args = array()) { + $this->where->where($snippet, $args); + return $this; + } + + public function compile(DatabaseConnection $connection) { + return $this->where->compile($connection); + } + + /* Implmeentations of QueryConditionInterface for the HAVING clause. */ + + public function havingCondition($field, $value = NULL, $operator = '=') { + if (!isset($num_args)) { + $num_args = func_num_args(); + } + $this->having->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->having->where($snippet, $args); + return $this; + } + + public function havingCompile(DatabaseConnection $connection) { + return $this->having->compile($connection); + } + + /* 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() { + return $this->fields; + } + + /** + * 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() { + return $this->expressions; + } + + /** + * 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() { + return $this->order; + } + + /** + * 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() { + return $this->tables; + } + + /** + * Compiles and returns an associative array of the arguments for this prepared statement. + * + * @return array + */ + public function getArguments() { + $this->where->compile($this->connection); + $this->having->compile($this->connection); + $args = $this->where->arguments() + $this->having->arguments(); + foreach ($this->tables as $table) { + if ($table['arguments']) { + $args += $table['arguments']; + } + } + foreach ($this->expressions as $expression) { + if ($expression['arguments']) { + $args += $expression['arguments']; + } + } + + return $args; + } + + public function execute() { + drupal_alter('query', $this); + + $this->where->compile($this->connection); + $this->having->compile($this->connection); + $args = $this->where->arguments() + $this->having->arguments(); + foreach ($this->tables as $table) { + if ($table['arguments']) { + $args += $table['arguments']; + } + } + foreach ($this->expressions as $expression) { + if ($expression['arguments']) { + $args += $expression['arguments']; + } + } + + if (!empty($this->range)) { + return $this->connection->queryRange((string)$this, $args, $this->range['start'], $this->range['length'], $this->queryOptions); + } + return $this->connection->runQuery((string)$this, $args, $this->queryOptions); + } + + /** + * Sets this query to be DISTINCT. + * + * @param $distinct + * TRUE to flag this query DISTINCT, FALSE to disable it. + * @return + * The called object. + */ + public function distinct($distinct = TRUE) { + $this->distinct = $distinct; + return $this; + } + + /** + * Adds a field to the list to be SELECTed. + * + * @param $field + * The name of the field. + * @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 $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($field, $table_alias, $alias = NULL) { + if (empty($alias)) { + $alias = $table_alias . '_' . $field; + } + + $alias_candidate = $alias; + $count = 2; + while (!empty($this->tables[$alias_candidate])) { + $alias_candidate = $alias . '_' . $count++; + } + $alias = $alias_candidate; + + $this->fields[$alias] = array( + 'field' => $field, + 'table' => $table_alias, + 'alias' => $alias, + ); + + return $alias; + } + + /** + * 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-dependant. 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 asigned + * 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()) { + static $alaises = array(); + + if (empty($alias)) { + $alias = 'expression'; + } + + if (empty($aliases[$alias])) { + $aliases[$alias] = 1; + } + + if (!empty($this->expressions[$alias])) { + $alias = $alias . '_' . $aliases[$alias]++; + } + + $this->expressions[$alias] = array( + 'expression' => $expression, + 'alias' => $alias, + 'arguments' => $arguments, + ); + + return $alias; + } + + /** + * 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. + * @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()) { + return $this->addJoin('INNER', $table, $alias, $condition, $arguments); + } + + /** + * 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. + * @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()) { + return $this->addJoin('INNER', $table, $alias, $condition, $arguments); + } + + /** + * 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. + * @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()) { + return $this->addJoin('LEFT OUTER', $table, $alias, $condition, $arguments); + } + + /** + * 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. + * @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()) { + return $this->addJoin('RIGHT OUTER', $table, $alias, $condition, $arguments); + } + + /** + * 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 $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. 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. + * @param $argments + * 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()) { + + if (empty($alias)) { + $alias = $table; + } + + $alias_candidate = $alias; + $count = 2; + while (!empty($this->tables[$alias_candidate])) { + $alias_candidate = $alias . '_' . $count++; + } + $alias = $alias_candidate; + + $this->tables[$alias] = array( + 'join type' => $type, + 'table' => $table, + 'alias' => $alias, + 'condition' => $condition, + 'arguments' => $arguments, + ); + + return $alias; + } + + /** + * 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. + * + * @param $field + * The field on which to order. + * @param $direction + * The direction to sort. Legal values are "ASC" and "DESC". + * @return + * The called object. + */ + public function orderBy($field, $direction = 'ASC') { + $this->order[$field] = $direction; + return $this; + } + + /** + * 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 $limit + * The number of records to return from the result set. + * @return + * The called object. + */ + public function range($start = NULL, $length = NULL) { + $this->range = func_num_args() ? array('start' => $start, 'length' => $length) : array(); + return $this; + } + + /** + * Groups the result set by the specified field. + * + * @param $field + * The field on which to group. This should be the field as aliased. + * @return + * The called object. + */ + public function groupBy($field) { + $this->group[] = $field; + } + + /** + * Get the equivalent COUNT query of this query as a new query object. + * + * @return + * A new SelectQuery object with no fields or expressions besides COUNT(*). + */ + public function countQuery() { + // Shallow-clone this query. We don't want to duplicate any of the + // referenced objects, so a shallow query is all we need. + $count = clone($this); + + // Zero-out existing fields and expressions. + $fields =& $count->getFields(); + $fields = array(); + $expressions =& $count->getExpressions(); + $expressions = array(); + + // 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; + } + + public function __toString() { + + // SELECT + $query = 'SELECT '; + if ($this->distinct) { + $query .= 'DISTINCT '; + } + + // FIELDS and EXPRESSIONS + $fields = array(); + foreach ($this->fields as $alias => $field) { + $fields[] = (isset($field['table']) ? $field['table'] . '.' : '') . $field['field'] . ' AS ' . $field['alias']; + } + foreach ($this->expressions as $alias => $expression) { + $fields[] = $expression['expression'] . ' AS ' . $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 '; + } + $query .= '{' . $this->connection->escapeTable($table['table']) . '} AS ' . $table['alias']; + if (!empty($table['condition'])) { + $query .= ' ON ' . $table['condition']; + } + } + + // WHERE + if (count($this->where)) { + $this->where->compile($this->connection); + // 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)) { + $this->having->compile($this->connection); + // There is an implicit string cast on $this->having. + $query .= "\nHAVING " . $this->having; + } + + // ORDER BY + if ($this->order) { + $query .= "\nORDER BY "; + foreach ($this->order as $field => $direction) { + $query .= $field . ' ' . $direction . ' '; + } + } + + // RANGE is database specific, so we can't do it here. + + return $query; + } + + public function __clone() { + // On cloning, also clone the conditional 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); + } +} + +/** + * Generic class for a series of conditions in a query. + */ + +class DatabaseCondition implements QueryConditionInterface, Countable { + + protected $conditions = array(); + protected $arguments = array(); + + protected $changed = TRUE; + + public function __construct($conjunction) { + $this->conditions['#conjunction'] = $conjunction; + } + + /** + * Return the size of this conditional. This is part of the Countable interface. + * + * 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; + } + + public function condition($field, $value = NULL, $operator = '=') { + $this->conditions[] = array( + 'field' => $field, + 'value' => $value, + 'operator' => $operator, + ); + + $this->changed = TRUE; + + return $this; + } + + public function where($snippet, $args = array()) { + $this->conditions[] = array( + 'field' => $snippet, + 'value' => $args, + 'operator' => NULL, + ); + $this->changed = TRUE; + + return $this; + } + + public function &conditions() { + return $this->conditions; + } + + public function arguments() { + // If the caller forgot to call compile() first, refuse to run. + if ($this->changed) { + return NULL; + } + return $this->arguments; + } + + public function compile(DatabaseConnection $connection) { + // This value is static, so it will increment across the entire request + // rather than just this query. That is OK, because we only need definitive + // placeholder names if we're going to use them for _alter hooks, which we + // are not. The alter hook would intervene before compilation. + static $next_placeholder = 1; + + if ($this->changed) { + + $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. + if ($condition['field'] instanceof QueryConditionInterface) { + // Compile the sub-condition recursively and add it to the list. + $condition['field']->compile($connection); + $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'], + ); + $operator = $connection->mapConditionOperator($condition['operator']); + if (!isset($operator)) { + $operator = $this->mapConditionOperator($condition['operator']); + } + $operator += $operator_defaults; + + if ($condition['value'] instanceof SelectQuery) { + $placeholders[] = (string)$condition['value']; + $arguments += $condition['value']->arguments(); + } + // 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']); + } + $placeholders = array(); + foreach ($condition['value'] as $value) { + $placeholder = ':db_condition_placeholder_' . $next_placeholder++; + $arguments[$placeholder] = $value; + $placeholders[] = $placeholder; + } + $condition_fragments[] = ' (' . $condition['field'] . ' ' . $operator['operator'] . ' ' . $operator['prefix'] . implode($operator['delimiter'], $placeholders) . $operator['postfix'] . ') '; + + } + } + } + + $this->changed = FALSE; + $this->stringVersion = implode($conjunction, $condition_fragments); + $this->arguments = $arguments; + } + } + + public function __toString() { + // If the caller forgot to call compile() first, refuse to run. + if ($this->changed) { + return NULL; + } + return $this->stringVersion; + } + + /** + * 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) { + static $specials = array( + 'BETWEEN' => array('delimiter' => ' AND '), + 'IN' => array('delimiter' => ', ', 'prefix' => ' (', 'postfix' => ')'), + 'NOT IN' => array('delimiter' => ', ', 'prefix' => ' (', 'postfix' => ')'), + 'LIKE' => array('operator' => 'LIKE'), + ); + + $return = isset($specials[$operator]) ? $specials[$operator] : array(); + $return += array('operator' => $operator); + + return $return; + } + +} + +/** + * Returns a new DatabaseCondition, set to "OR" all conditions together. + */ +function db_or() { + return new DatabaseCondition('OR'); +} + +/** + * Returns a new DatabaseCondition, set to "AND" all conditions together. + */ +function db_and() { + return new DatabaseCondition('AND'); +} + +/** + * Returns a new DatabaseCondition, set to "XOR" all conditions together. + */ +function db_xor() { + return new DatabaseCondition('XOR'); +} + +/** + * Returns a new DatabaseCondition, set to the specified conjunction. + * + * @param + * The conjunction (AND, OR, XOR, etc.) to use on conditions. + */ +function db_condition($conjunction) { + return new DatabaseCondition($conjunction); +} + +/** + * The following utility functions are simply convenience wrappers. + * They should never, ever have any database-specific code in them. + */ + +function _db_query_process_args($query, $args, $options) { + // Temporary backward-compatibliity hacks. Remove later. + if (!is_array($options)) { + $options = array(); + } + + $old_query = $query; + $query = str_replace(array('%d', '%f', '%b', "'%s'", '%s'), '?', $old_query); + if ($old_query !== $query) { + $args = array_values($args); // The old system allowed named arrays, but PDO doesn't if you use ?. + } + + // A large number of queries pass FALSE or empty-string for + // int/float fields because the previous version of db_query() + // casted them to int/float, resulting in 0. MySQL PDO happily + // accepts these values as zero but PostgreSQL PDO does not, and I + // do not feel like tracking down and fixing every such query at + // this time. + if (preg_match_all('/%([dsfb])/', $old_query, $m) > 0) { + foreach ($m[1] as $idx => $char) { + switch ($char) { + case 'd': + $args[$idx] = (int) $args[$idx]; + break; + case 'f': + $args[$idx] = (float) $args[$idx]; + break; + } + } + } + + if (empty($options['target'])) { + $options['target'] = 'default'; + } + + return array($query, $args, $options); +} + +/** + * Execute an arbitrary query string against the active database. + * + * Do not use this function for INSERT, UPDATE, or DELETE queries. Those should + * be handled via the appropriate query builder factory. Use this function for + * SELECT queries that do not require a query builder. + * + * @see DatabaseConnection::defaultOptions() + * @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 + * A prepared statement object, already executed. + */ +function db_query($query, $args = array(), $options = array()) { + if (!is_array($args)) { + $args = func_get_args(); + array_shift($args); + } + + list($query, $args, $options) = _db_query_process_args($query, $args, $options); + return Database::getActiveConnection($options['target'])->query($query, $args, $options); +} + +/** + * Execute an arbitrary query string against the active database, restricted to a specified range. + * + * @see DatabaseConnection::defaultOptions() + * @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 $from + * The first record from the result set to return. + * @param $limit + * The number of records to return from the result set. + * @param $options + * An array of options to control how the query operates. + * @return + * A prepared statement object, already executed. + */ +function db_query_range($query, $args, $from = 0, $count = 0, $options = array()) { + if (!is_array($args)) { + $args = func_get_args(); + array_shift($args); + $count = array_pop($args); + $from = array_pop($args); + } + + list($query, $args, $options) = _db_query_process_args($query, $args, $options); + return Database::getActiveConnection($options['target'])->queryRange($query, $args, $from, $count, $options); +} + +/** + * Execute a query string against the active database and save the result set to a temp table. + * + * @see DatabaseConnection::defaultOptions() + * @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 $from + * The first record from the result set to return. + * @param $limit + * The number of records to return from the result set. + * @param $options + * An array of options to control how the query operates. + */ +function db_query_temporary($query, $args, $tablename, $options = array()) { + if (!is_array($args)) { + $args = func_get_args(); + array_shift($args); + } + list($query, $args, $options) = _db_query_process_args($query, $args, $options); + return Database::getActiveConnection($options['target'])->queryTemporary($query, $args, $tablename, $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 + * 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::getActiveConnection($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 + * 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::getActiveConnection($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 + * 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::getActiveConnection($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 + * 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::getActiveConnection($options['target'])->delete($table, $options); +} + +/** + * Returns a new SelectQuery object for the active database. + * + * @param $table + * The base table for this query. + * @param $alias + * The alias for the base table of this query. + * @param $options + * An array of options to control how the query operates. + * @return + * 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::getActiveConnection($options['target'])->select($table, $alias, $options); +} + +/** + * Sets a new active database. + * + * @param $key + * The key in the $databases array to set as the default database. + * @returns + * The key of the formerly active database. + */ +function db_set_active($key = 'default') { + return Database::setActiveConnection($key); +} + +/** + * Determine 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. + */ +function db_is_active() { + return Database::isActiveConnection(); +} + +/** + * Restrict a dynamic table, column or constraint 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::getActiveConnection()->escapeTable($table); +} + +/** + * Perform an SQL query and return success or failure. + * + * @param $sql + * A string containing a complete SQL query. %-substitution + * parameters are not supported. + * @return + * An array containing the keys: + * success: a boolean indicating whether the query succeeded + * query: the SQL query executed, passed through check_plain() + */ +function update_sql($sql) { + $result = Database::getActiveConnection()->query($sql/*, array(true)*/); + return array('success' => $result !== FALSE, 'query' => check_plain($sql)); +} + +/** + * Generate placeholders for an array of query arguments of a single type. + * + * Given a Schema API field type, return correct %-placeholders to + * embed in a query + * + * @todo This may be possible to remove in favor of db_select(). + * @param $arguments + * An array with at least one element. + * @param $type + * The Schema API type of a field (e.g. 'int', 'text', or 'varchar'). + */ +function db_placeholders($arguments, $type = 'int') { + $placeholder = db_type_placeholder($type); + return implode(',', array_fill(0, count($arguments), $placeholder)); +} + +/** + * Wraps the given table.field entry with a DISTINCT(). The wrapper is added to + * the SELECT list entry of the given query and the resulting query is returned. + * This function only applies the wrapper if a DISTINCT doesn't already exist in + * the query. + * + * @todo Remove this. + * @param $table + * Table containing the field to set as DISTINCT + * @param $field + * Field to set as DISTINCT + * @param $query + * Query to apply the wrapper to + * @return + * SQL query with the DISTINCT wrapper surrounding the given table.field. + */ +function db_distinct_field($table, $field, $query) { + return Database::getActiveConnection()->distinctField($table, $field, $query); +} + +/** + * Retrieve the name of the currently active database driver, such as + * "mysql" or "pgsql". + * + * @return The name of the currently active database driver. + */ +function db_driver() { + return Database::getActiveConnection()->driver(); +} + +/** + * @} End of "defgroup database". + */ + + +/** + * @ingroup schemaapi + * @{ + */ + + +/** + * Create a new table from a Drupal table definition. + * + * @param $ret + * Array to which query results will be added. + * @param $name + * The name of the table to create. + * @param $table + * A Schema API table definition array. + */ +function db_create_table(&$ret, $name, $table) { + return Database::getActiveConnection()->schema()->createTable($ret, $name, $table); +} + +/** + * 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. + */ +function db_field_names($fields) { + return Database::getActiveConnection()->schema()->fieldNames($fields); +} + +/** + * Check if a table exists. + */ +function db_table_exists($table) { + return Database::getActiveConnection()->schema()->tableExists($table); +} + +/** + * Check if a column exists in the given table. + */ +function db_column_exists($table, $column) { + return Database::getActiveConnection()->schema()->columnExists($table, $column); +} + + +/** + * Given a Schema API field type, return the correct %-placeholder. + * + * Embed the placeholder in a query to be passed to db_query and and pass as an + * argument to db_query a value of the specified type. + * + * @todo Remove this after all queries are converted to type-agnostic form. + * @param $type + * The Schema API type of a field. + * @return + * The placeholder string to embed in a query for that type. + */ +function db_type_placeholder($type) { + switch ($type) { + case 'varchar': + case 'char': + case 'text': + case 'datetime': + return '\'%s\''; + + case 'numeric': + // For 'numeric' values, we use '%s', not '\'%s\'' as with + // string types, because numeric values should not be enclosed + // in quotes in queries (though they can be, at least on mysql + // and pgsql). Numerics should only have [0-9.+-] and + // presumably no db's "escape string" function will mess with + // those characters. + return '%s'; + + case 'serial': + case 'int': + return '%d'; + + case 'float': + return '%f'; + + case 'blob': + return '%b'; + } + + // There is no safe value to return here, so return something that + // will cause the query to fail. + return 'unsupported type ' . $type . 'for db_type_placeholder'; +} + + +function _db_create_keys_sql($spec) { + return Database::getActiveConnection()->schema()->createKeysSql($spec); +} + +/** + * This maps a generic data type in combination with its data size + * to the engine-specific data type. + */ +function db_type_map() { + return Database::getActiveConnection()->schema()->getFieldTypeMap(); +} + +/** + * Rename a table. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be renamed. + * @param $new_name + * The new name for the table. */ +function db_rename_table(&$ret, $table, $new_name) { + return Database::getActiveConnection()->schema()->renameTable($ret, $table, $new_name); +} /** - * Perform an SQL query and return success or failure. + * Drop a table. * - * @param $sql - * A string containing a complete SQL query. %-substitution - * parameters are not supported. - * @return - * An array containing the keys: - * success: a boolean indicating whether the query succeeded - * query: the SQL query executed, passed through check_plain() + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be dropped. */ -function update_sql($sql) { - $result = db_query($sql, true); - return array('success' => $result !== FALSE, 'query' => check_plain($sql)); +function db_drop_table(&$ret, $table) { + return Database::getActiveConnection()->schema()->dropTable($ret, $table); } /** - * Append a database prefix to all tables in a query. + * Add a new field to a table. * - * 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 if - * necessary. - * - * @param $sql - * A string containing a partial or entire SQL query. - * @return - * The properly-prefixed string. + * @param $ret + * Array to which query results will be added. + * @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. */ -function db_prefix_tables($sql) { - global $db_prefix; - - if (is_array($db_prefix)) { - if (array_key_exists('default', $db_prefix)) { - $tmp = $db_prefix; - unset($tmp['default']); - foreach ($tmp as $key => $val) { - $sql = strtr($sql, array('{' . $key . '}' => $val . $key)); - } - return strtr($sql, array('{' => $db_prefix['default'], '}' => '')); - } - else { - foreach ($db_prefix as $key => $val) { - $sql = strtr($sql, array('{' . $key . '}' => $val . $key)); - } - return strtr($sql, array('{' => '', '}' => '')); - } - } - else { - return strtr($sql, array('{' => $db_prefix, '}' => '')); - } +function db_add_field(&$ret, $table, $field, $spec, $keys_new = array()) { + return Database::getActiveConnection()->schema()->addField($ret, $table, $field, $spec, $keys_new); } /** - * Activate a database for future queries. + * Drop a field. * - * If it is necessary to use external databases in a project, this function can - * be used to change where database queries are sent. If the database has not - * yet been used, it is initialized using the URL specified for that name in - * Drupal's configuration file. If this name is not defined, a duplicate of the - * default connection is made instead. - * - * Be sure to change the connection back to the default when done with custom - * code. + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + * @param $field + * The field to be dropped. + */ +function db_drop_field(&$ret, $table, $field) { + return Database::getActiveConnection()->schema()->dropField($ret, $table, $field); +} + +/** + * Set the default value for a field. * - * @param $name - * The name assigned to the newly active database connection. If omitted, the - * default connection will be made active. + * @param $ret + * Array to which query results will be added. + * @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(&$ret, $table, $field, $default) { + return Database::getActiveConnection()->schema()->dropField($ret, $table, $field, $default); +} + +/** + * Set a field to have no default value. * - * @return the name of the previously active database or FALSE if non was found. + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + * @param $field + * The field to be altered. */ -function db_set_active($name = 'default') { - global $db_url, $db_type, $active_db, $db_prefix; - static $db_conns, $active_name = FALSE; +function db_field_set_no_default(&$ret, $table, $field) { + return Database::getActiveConnection()->schema()->fieldSetNoDefault($ret, $table, $field); +} - if (empty($db_url)) { - include_once 'includes/install.inc'; - install_goto('install.php'); - } +/** + * Add a primary key. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + * @param $fields + * Fields for the primary key. + */ +function db_add_primary_key(&$ret, $table, $fields) { + return Database::getActiveConnection()->schema()->addPrimaryKey($ret, $table, $field); +} - if (!isset($db_conns[$name])) { - // Initiate a new connection, using the named DB URL specified. - if (is_array($db_url)) { - $connect_url = array_key_exists($name, $db_url) ? $db_url[$name] : $db_url['default']; - } - else { - $connect_url = $db_url; - } +/** + * Drop the primary key. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + */ +function db_drop_primary_key(&$ret, $table) { + return Database::getActiveConnection()->schema()->dropPrimaryKey($ret, $table); +} - $db_type = substr($connect_url, 0, strpos($connect_url, '://')); - $handler = "./includes/database.$db_type.inc"; +/** + * Add a unique key. + * + * @param $ret + * Array to which query results will be added. + * @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(&$ret, $table, $name, $fields) { + return Database::getActiveConnection()->schema()->addUniqueKey($ret, $table, $name, $fields); +} - if (is_file($handler)) { - include_once $handler; - } - else { - _db_error_page("The database type '" . $db_type . "' is unsupported. Please use either 'mysql' or 'mysqli' for MySQL, or 'pgsql' for PostgreSQL databases."); - } +/** + * Drop a unique key. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + * @param $name + * The name of the key. + */ +function db_drop_unique_key(&$ret, $table, $name) { + return Database::getActiveConnection()->schema()->dropUniqueKey($ret, $table, $name); +} - $db_conns[$name] = db_connect($connect_url); - // We need to pass around the simpletest database prefix in the request - // and we put that in the user_agent header. - if (preg_match("/^simpletest\d+$/", $_SERVER['HTTP_USER_AGENT'])) { - $db_prefix = $_SERVER['HTTP_USER_AGENT']; - } +/** + * Add an index. + * + * @param $ret + * Array to which query results will be added. + * @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(&$ret, $table, $name, $fields) { + return Database::getActiveConnection()->schema()->addIndex($ret, $table, $name, $fields); +} - } +/** + * Drop an index. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + * @param $name + * The name of the index. + */ +function db_drop_index(&$ret, $table, $name) { + return Database::getActiveConnection()->schema()->addIndex($ret, $table, $name); +} - $previous_name = $active_name; - // Set the active connection. - $active_name = $name; - $active_db = $db_conns[$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($ret, 'foo'); + * db_change_field($ret, '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 $ret + * Array to which query results will be added. + * @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. + */ - return $previous_name; +function db_change_field(&$ret, $table, $field, $field_new, $spec, $keys_new = array()) { + return Database::getActiveConnection()->schema()->changeField($ret, $table, $field, $field_new, $spec, $keys_new); } /** - * Helper function to show fatal database errors. - * + * @} End of "ingroup schemaapi". + */ + +/** * Prints a themed maintenance page with the 'Site offline' text, * adding the provided error message in the case of 'display_errors' * set to on. Ends the page request; no return. - * - * @param $error - * The error message to be appended if 'display_errors' is on. */ function _db_error_page($error = '') { global $db_type; drupal_maintenance_theme(); drupal_set_header('HTTP/1.1 503 Service Unavailable'); drupal_set_title('Site offline'); +} - $message = '

The site is currently not available due to technical problems. Please try again later. Thank you for your understanding.

'; - $message .= '

If you are the maintainer of this site, please check your database settings in the settings.php file and ensure that your hosting provider\'s database server is running. For more help, see the handbook, or contact your hosting provider.

'; +/** + * @ingroup database-legacy + * + * These functions are no longer necessary, as the DatabaseStatement object + * offers this and much more functionality. They are kept temporarily for backward + * compatibility during conversion and should be removed as soon as possible. + * + * @{ + */ - if ($error && ini_get('display_errors')) { - $message .= '

The ' . theme('placeholder', $db_type) . ' error was: ' . theme('placeholder', $error) . '.

'; - } +function db_fetch_object(DatabaseStatement $statement) { + return $statement->fetch(PDO::FETCH_OBJ); +} - print theme('maintenance_page', $message); - exit; +function db_fetch_array(DatabaseStatement $statement) { + return $statement->fetch(PDO::FETCH_ASSOC); } -/** - * Returns a boolean depending on the availability of