=== 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-23 07:00:33 +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); } @@ -45,7 +44,6 @@ function cache_get($cid, $table = 'cache return FALSE; } else { - $cache->data = db_decode_blob($cache->data); if ($cache->serialized) { $cache->data = unserialize($cache->data); } @@ -101,16 +99,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 +174,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-23 07:00:33 +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,3541 @@ 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; + + /** + * 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. + */ + +/** + * 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); +} + +/** + * Drop a table. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be dropped. + */ +function db_drop_table(&$ret, $table) { + return Database::getActiveConnection()->schema()->dropTable($ret, $table); +} /** - * Perform an SQL query and return success or failure. + * Add a new field to 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 + * 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 update_sql($sql) { - $result = db_query($sql, true); - return array('success' => $result !== FALSE, 'query' => check_plain($sql)); +function db_add_field(&$ret, $table, $field, $spec, $keys_new = array()) { + return Database::getActiveConnection()->schema()->addField($ret, $table, $field, $spec, $keys_new); } /** - * Append a database prefix to all tables in a query. + * Drop a field. * - * 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 $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 $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 + * 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_prefix_tables($sql) { - global $db_prefix; +function db_field_set_default(&$ret, $table, $field, $default) { + return Database::getActiveConnection()->schema()->dropField($ret, $table, $field, $default); +} - 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, '}' => '')); - } +/** + * Set a field to have no default value. + * + * @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_field_set_no_default(&$ret, $table, $field) { + return Database::getActiveConnection()->schema()->fieldSetNoDefault($ret, $table, $field); } /** - * Activate a database for future queries. + * Add a primary key. * - * 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. + * @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); +} + +/** + * Drop the primary key. * - * 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. + */ +function db_drop_primary_key(&$ret, $table) { + return Database::getActiveConnection()->schema()->dropPrimaryKey($ret, $table); +} + +/** + * 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 assigned to the newly active database connection. If omitted, the - * default connection will be made active. - * - * @return the name of the previously active database or FALSE if non was found. + * The name of the key. + * @param $fields + * An array of field names. */ -function db_set_active($name = 'default') { - global $db_url, $db_type, $active_db, $db_prefix; - static $db_conns, $active_name = FALSE; - - if (empty($db_url)) { - include_once 'includes/install.inc'; - install_goto('install.php'); - } - - 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; - } - - $db_type = substr($connect_url, 0, strpos($connect_url, '://')); - $handler = "./includes/database.$db_type.inc"; +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'); +} + +/** + * @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. + * + * @{ + */ - $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.

'; +function db_fetch_object(DatabaseStatement $statement) { + return $statement->fetch(PDO::FETCH_OBJ); +} - if ($error && ini_get('display_errors')) { - $message .= '

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

'; - } +function db_fetch_array(DatabaseStatement $statement) { + return $statement->fetch(PDO::FETCH_ASSOC); +} - print theme('maintenance_page', $message); - exit; +function db_result(DatabaseStatement $statement) { + return $statement->fetchField(); } -/** - * Returns a boolean depending on the availability of the database. - */ -function db_is_active() { - global $active_db; - return !empty($active_db); +function _db_need_install() { + if (!function_exists('install_goto')) { + include_once 'includes/install.inc'; + install_goto('install.php'); + } } /** - * Helper function for db_query(). + * Backward-compatibility utility. + * + * This function should be removed after all queries have been converted + * to the new API. It is temporary only. + * + * @todo Remove this once the query conversion is complete. */ -function _db_query_callback($match, $init = FALSE) { - static $args = NULL; - if ($init) { - $args = $match; - return; - } - - switch ($match[1]) { - case '%d': // We must use type casting to int to convert FALSE/NULL/(TRUE?) - return (int) array_shift($args); // We don't need db_escape_string as numbers are db-safe - case '%s': - return db_escape_string(array_shift($args)); - case '%n': - // Numeric values have arbitrary precision, so can't be treated as float. - // is_numeric() allows hex values (0xFF), but they are not valid. - $value = trim(array_shift($args)); - return (is_numeric($value) && !stripos($value, 'x')) ? $value : '0'; - case '%%': - return '%'; - case '%f': - return (float) array_shift($args); - case '%b': // binary data - return db_encode_blob(array_shift($args)); +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); } + /** - * 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 + * Returns the last insert id. * - * @param $arguments - * An array with at least one element. - * @param $type - * The Schema API type of a field (e.g. 'int', 'text', or 'varchar'). + * @todo Remove this function when all queries have been ported to db_insert(). + * @param $table + * The name of the table you inserted into. + * @param $field + * The name of the autoincrement field. */ -function db_placeholders($arguments, $type = 'int') { - $placeholder = db_type_placeholder($type); - return implode(',', array_fill(0, count($arguments), $placeholder)); +function db_last_insert_id($table, $field) { + $sequence_name = Database::getActiveConnection()->makeSequenceName($table, $field); + return Database::getActiveConnection()->lastInsertId($sequence_name); } /** - * Indicates the place holders that should be replaced in _db_query_callback(). + * Determine the number of rows changed by the preceding query. + * + * This may not work, actually, without some tricky temp code. + * + * @todo Remove this function when all queries have been ported to db_update(). */ -define('DB_QUERY_REGEXP', '/(%d|%s|%%|%f|%b|%n)/'); +function db_affected_rows() { + $statement = Database::getActiveConnection()->lastStatement; + if (!$statement) { + return 0; + } + return $statement->rowCount(); +} /** * Helper function for db_rewrite_sql. @@ -257,6 +3584,7 @@ define('DB_QUERY_REGEXP', '/(%d|%s|%%|%f * Collects JOIN and WHERE statements via hook_db_rewrite_sql() * Decides whether to select primary_key or DISTINCT(primary_key) * + * @todo Remove this function when all code has been converted to query_alter. * @param $query * Query to be rewritten. * @param $primary_table @@ -303,6 +3631,7 @@ function _db_rewrite_sql($query = '', $p * Rewrites node, taxonomy and comment queries. Use it for listing queries. Do not * use FROM table1, table2 syntax, use JOIN instead. * + * @todo Remove this function when all code has been converted to query_alter. * @param $query * Query to be rewritten. * @param $primary_table @@ -342,7 +3671,7 @@ function db_rewrite_sql($query, $primary if ($where) { $n = strlen($matches[1]); $second_part = substr($query, $n); - $first_part = substr($matches[1], 0, $n - 5) . " $join WHERE $where AND ( "; + $first_part = substr($matches[1], 0, $n - 5) ." $join WHERE $where AND ( "; // PHP 4 does not support strrpos for strings. We emulate it. $haystack_reverse = strrev($second_part); // No need to use strrev on the needle, we supply GROUP, ORDER, LIMIT @@ -370,212 +3699,7 @@ function db_rewrite_sql($query, $primary return $query; } -/** - * Restrict a dynamic table, column or constraint name to safe characters. - * - * Only keeps alphanumeric and underscores. - */ -function db_escape_table($string) { - return preg_replace('/[^A-Za-z0-9_]+/', '', $string); -} - -/** - * @} End of "defgroup database". - */ - -/** - * @defgroup schemaapi Schema API - * @{ - * - * A Drupal schema definition is an array structure representing one or - * more tables and their related keys and indexes. A schema is defined by - * hook_schema(), which usually lives in a modulename.install file. - * - * By implementing hook_schema() and specifying the tables your module - * declares, you can easily create and drop these tables on all - * supported database engines. You don't have to deal with the - * different SQL dialects for table creation and alteration of the - * supported database engines. - * - * hook_schema() should return an array with a key for each table that - * the module defines. - * - * The following keys are defined: - * - * - 'description': A string describing this table and its purpose. - * References to other tables should be enclosed in - * curly-brackets. For example, the node_revisions table - * description field might contain "Stores per-revision title and - * body data for each {node}." - * - 'fields': An associative array ('fieldname' => specification) - * that describes the table's database columns. The specification - * is also an array. The following specification parameters are defined: - * - * - 'description': A string describing this field and its purpose. - * References to other tables should be enclosed in - * curly-brackets. For example, the node table vid field - * description might contain "Always holds the largest (most - * recent) {node_revisions}.vid value for this nid." - * - 'type': The generic datatype: 'varchar', 'int', 'serial' - * 'float', 'numeric', 'text', 'blob' or 'datetime'. Most types - * just map to the according database engine specific - * datatypes. Use 'serial' for auto incrementing fields. This - * will expand to 'int auto_increment' on mysql. - * - 'size': The data size: 'tiny', 'small', 'medium', 'normal', - * 'big'. This is a hint about the largest value the field will - * store and determines which of the database engine specific - * datatypes will be used (e.g. on MySQL, TINYINT vs. INT vs. BIGINT). - * 'normal', the default, selects the base type (e.g. on MySQL, - * INT, VARCHAR, BLOB, etc.). - * - * Not all sizes are available for all data types. See - * db_type_map() for possible combinations. - * - 'not null': If true, no NULL values will be allowed in this - * database column. Defaults to false. - * - 'default': The field's default value. The PHP type of the - * value matters: '', '0', and 0 are all different. If you - * specify '0' as the default value for a type 'int' field it - * will not work because '0' is a string containing the - * character "zero", not an integer. - * - 'length': The maximal length of a type 'varchar' or 'text' - * field. Ignored for other field types. - * - 'unsigned': A boolean indicating whether a type 'int', 'float' - * and 'numeric' only is signed or unsigned. Defaults to - * FALSE. Ignored for other field types. - * - 'precision', 'scale': For type 'numeric' fields, indicates - * the precision (total number of significant digits) and scale - * (decimal digits right of the decimal point). Both values are - * mandatory. Ignored for other field types. - * - * All parameters apart from 'type' are optional except that type - * 'numeric' columns must specify 'precision' and 'scale'. - * - * - 'primary key': An array of one or more key column specifiers (see below) - * that form the primary key. - * - 'unique key': An associative array of unique keys ('keyname' => - * specification). Each specification is an array of one or more - * key column specifiers (see below) that form a unique key on the table. - * - 'indexes': An associative array of indexes ('indexame' => - * specification). Each specification is an array of one or more - * key column specifiers (see below) that form an index on the - * table. - * - * A key column specifier is either a string naming a column or an - * array of two elements, column name and length, specifying a prefix - * of the named column. - * - * As an example, here is a SUBSET of the schema definition for - * Drupal's 'node' table. It show four fields (nid, vid, type, and - * title), the primary key on field 'nid', a unique key named 'vid' on - * field 'vid', and two indexes, one named 'nid' on field 'nid' and - * one named 'node_title_type' on the field 'title' and the first four - * bytes of the field 'type': - * - * @code - * $schema['node'] = array( - * 'fields' => array( - * 'nid' => array('type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE), - * 'vid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), - * 'type' => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => ''), - * 'title' => array('type' => 'varchar', 'length' => 128, 'not null' => TRUE, 'default' => ''), - * ), - * 'primary key' => array('nid'), - * 'unique keys' => array( - * 'vid' => array('vid') - * ), - * 'indexes' => array( - * 'nid' => array('nid'), - * 'node_title_type' => array('title', array('type', 4)), - * ), - * ); - * @endcode - * - * @see drupal_install_schema() - */ - - /** - * 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) { - $statements = db_create_table_sql($name, $table); - foreach ($statements as $statement) { - $ret[] = update_sql($statement); - } -} - -/** - * Return an array of field names from an array of key/index column specifiers. - * - * This is usually an identity function but if a key/index uses a column prefix - * specification, this function extracts just the name. - * - * @param $fields - * An array of key/index column specifiers. - * @return - * An array of field names. - */ -function db_field_names($fields) { - $ret = array(); - foreach ($fields as $field) { - if (is_array($field)) { - $ret[] = $field[0]; - } - else { - $ret[] = $field; - } - } - return $ret; -} - -/** - * 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. - * - * @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': - // Numeric values are arbitrary precision numbers. Syntacically, numerics - // should be specified directly in SQL. However, without single quotes - // the %s placeholder does not protect against non-numeric characters such - // as spaces which would expose us to SQL injection. - return '%n'; - - 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'; -} /** - * @} End of "defgroup schemaapi". + * @} End of "ingroup database-legacy". */ === removed file 'includes/database.mysql-common.inc' --- includes/database.mysql-common.inc 2008-04-14 17:48:33 +0000 +++ includes/database.mysql-common.inc 1970-01-01 00:00:00 +0000 @@ -1,533 +0,0 @@ - $field) { - $sql .= _db_create_field_sql($field_name, _db_process_field($field)) . ", \n"; - } - - // Process keys & indexes. - $keys = _db_create_keys_sql($table); - if (count($keys)) { - $sql .= implode(", \n", $keys) . ", \n"; - } - - // Remove the last comma and space. - $sql = substr($sql, 0, -3) . "\n) "; - - $sql .= $table['mysql_suffix']; - - return array($sql); -} - -function _db_create_keys_sql($spec) { - $keys = array(); - - if (!empty($spec['primary key'])) { - $keys[] = 'PRIMARY KEY (' . _db_create_key_sql($spec['primary key']) . ')'; - } - if (!empty($spec['unique keys'])) { - foreach ($spec['unique keys'] as $key => $fields) { - $keys[] = 'UNIQUE KEY ' . $key . ' (' . _db_create_key_sql($fields) . ')'; - } - } - if (!empty($spec['indexes'])) { - foreach ($spec['indexes'] as $index => $fields) { - $keys[] = 'INDEX ' . $index . ' (' . _db_create_key_sql($fields) . ')'; - } - } - - return $keys; -} - -function _db_create_key_sql($fields) { - $ret = array(); - foreach ($fields as $field) { - if (is_array($field)) { - $ret[] = $field[0] . '(' . $field[1] . ')'; - } - else { - $ret[] = $field; - } - } - return implode(', ', $ret); -} - -/** - * Set database-engine specific properties for a field. - * - * @param $field - * A field description array, as specified in the schema documentation. - */ -function _db_process_field($field) { - - if (!isset($field['size'])) { - $field['size'] = 'normal'; - } - - // Set the correct database-engine specific datatype. - if (!isset($field['mysql_type'])) { - $map = db_type_map(); - $field['mysql_type'] = $map[$field['type'] . ':' . $field['size']]; - } - - if ($field['type'] == 'serial') { - $field['auto_increment'] = TRUE; - } - - return $field; -} - -/** - * Create an SQL string for a field to be used in table creation or alteration. - * - * Before passing a field out of a schema definition into this function it has - * to be processed by _db_process_field(). - * - * @param $name - * Name of the field. - * @param $spec - * The field specification, as per the schema data structure format. - */ -function _db_create_field_sql($name, $spec) { - $sql = "`" . $name . "` " . $spec['mysql_type']; - - if (isset($spec['length'])) { - $sql .= '(' . $spec['length'] . ')'; - } - elseif (isset($spec['precision']) && isset($spec['scale'])) { - $sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')'; - } - - if (!empty($spec['unsigned'])) { - $sql .= ' unsigned'; - } - - if (!empty($spec['not null'])) { - $sql .= ' NOT NULL'; - } - - if (!empty($spec['auto_increment'])) { - $sql .= ' auto_increment'; - } - - if (isset($spec['default'])) { - if (is_string($spec['default'])) { - $spec['default'] = "'" . $spec['default'] . "'"; - } - $sql .= ' DEFAULT ' . $spec['default']; - } - - if (empty($spec['not null']) && !isset($spec['default'])) { - $sql .= ' DEFAULT NULL'; - } - - return $sql; -} - -/** - * This maps a generic data type in combination with its data size - * to the engine-specific data type. - */ -function db_type_map() { - // Put :normal last so it gets preserved by array_flip. This makes - // it much easier for modules (such as schema.module) to map - // database types back into schema types. - $map = array( - 'varchar:normal' => 'VARCHAR', - 'char:normal' => 'CHAR', - - 'text:tiny' => 'TINYTEXT', - 'text:small' => 'TINYTEXT', - 'text:medium' => 'MEDIUMTEXT', - 'text:big' => 'LONGTEXT', - 'text:normal' => 'TEXT', - - 'serial:tiny' => 'TINYINT', - 'serial:small' => 'SMALLINT', - 'serial:medium' => 'MEDIUMINT', - 'serial:big' => 'BIGINT', - 'serial:normal' => 'INT', - - 'int:tiny' => 'TINYINT', - 'int:small' => 'SMALLINT', - 'int:medium' => 'MEDIUMINT', - 'int:big' => 'BIGINT', - 'int:normal' => 'INT', - - 'float:tiny' => 'FLOAT', - 'float:small' => 'FLOAT', - 'float:medium' => 'FLOAT', - 'float:big' => 'DOUBLE', - 'float:normal' => 'FLOAT', - - 'numeric:normal' => 'DECIMAL', - - 'blob:big' => 'LONGBLOB', - 'blob:normal' => 'BLOB', - - 'datetime:normal' => 'DATETIME', - ); - return $map; -} - -/** - * 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) { - $ret[] = update_sql('ALTER TABLE {' . $table . '} RENAME TO {' . $new_name . '}'); -} - -/** - * Drop a table. - * - * @param $ret - * Array to which query results will be added. - * @param $table - * The table to be dropped. - */ -function db_drop_table(&$ret, $table) { - $ret[] = update_sql('DROP TABLE {' . $table . '}'); -} - -/** - * Add a new field to a table. - * - * @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_add_field(&$ret, $table, $field, $spec, $keys_new = array()) { - $fixnull = FALSE; - if (!empty($spec['not null']) && !isset($spec['default'])) { - $fixnull = TRUE; - $spec['not null'] = FALSE; - } - $query = 'ALTER TABLE {' . $table . '} ADD '; - $query .= _db_create_field_sql($field, _db_process_field($spec)); - if (count($keys_new)) { - $query .= ', ADD ' . implode(', ADD ', _db_create_keys_sql($keys_new)); - } - $ret[] = update_sql($query); - if (isset($spec['initial'])) { - // All this because update_sql does not support %-placeholders. - $sql = 'UPDATE {' . $table . '} SET ' . $field . ' = ' . db_type_placeholder($spec['type']); - $result = db_query($sql, $spec['initial']); - $ret[] = array('success' => $result !== FALSE, 'query' => check_plain($sql . ' (' . $spec['initial'] . ')')); - } - if ($fixnull) { - $spec['not null'] = TRUE; - db_change_field($ret, $table, $field, $field, $spec); - } -} - -/** - * Drop a field. - * - * @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) { - $ret[] = update_sql('ALTER TABLE {' . $table . '} DROP ' . $field); -} - -/** - * Set the default value for a field. - * - * @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) { - if ($default == NULL) { - $default = 'NULL'; - } - else { - $default = is_string($default) ? "'$default'" : $default; - } - - $ret[] = update_sql('ALTER TABLE {' . $table . '} ALTER COLUMN ' . $field . ' SET DEFAULT ' . $default); -} - -/** - * Set a field to have no default value. - * - * @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_field_set_no_default(&$ret, $table, $field) { - $ret[] = update_sql('ALTER TABLE {' . $table . '} ALTER COLUMN ' . $field . ' DROP DEFAULT'); -} - -/** - * 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) { - $ret[] = update_sql('ALTER TABLE {' . $table . '} ADD PRIMARY KEY (' . - _db_create_key_sql($fields) . ')'); -} - -/** - * 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) { - $ret[] = update_sql('ALTER TABLE {' . $table . '} DROP PRIMARY KEY'); -} - -/** - * 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) { - $ret[] = update_sql('ALTER TABLE {' . $table . '} ADD UNIQUE KEY ' . - $name . ' (' . _db_create_key_sql($fields) . ')'); -} - -/** - * 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) { - $ret[] = update_sql('ALTER TABLE {' . $table . '} DROP KEY ' . $name); -} - -/** - * 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) { - $query = 'ALTER TABLE {' . $table . '} ADD INDEX ' . $name . ' (' . _db_create_key_sql($fields) . ')'; - $ret[] = update_sql($query); -} - -/** - * 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) { - $ret[] = update_sql('ALTER TABLE {' . $table . '} DROP INDEX ' . $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. - */ - -function db_change_field(&$ret, $table, $field, $field_new, $spec, $keys_new = array()) { - $sql = 'ALTER TABLE {' . $table . '} CHANGE ' . $field . ' ' . - _db_create_field_sql($field_new, _db_process_field($spec)); - if (count($keys_new)) { - $sql .= ', ADD ' . implode(', ADD ', _db_create_keys_sql($keys_new)); - } - $ret[] = update_sql($sql); -} - -/** - * Returns the last insert id. - * - * @param $table - * The name of the table you inserted into. - * @param $field - * The name of the autoincrement field. - */ -function db_last_insert_id($table, $field) { - return db_result(db_query('SELECT LAST_INSERT_ID()')); -} === modified file 'includes/database.mysql.inc' --- includes/database.mysql.inc 2008-04-14 17:48:33 +0000 +++ includes/database.mysql.inc 2008-07-22 07:28:50 +0000 @@ -11,360 +11,254 @@ * @{ */ -// Include functions shared between mysql and mysqli. -require_once './includes/database.mysql-common.inc'; +class DatabaseConnection_mysql extends DatabaseConnection { -/** - * Report database status. - */ -function db_status_report($phase) { - $t = get_t(); + public static $rand = 'RAND()'; + public static $timestamp = 'Y-m-d h:i:s'; + + protected $transactionSupport; - $version = db_version(); + public function __construct(Array $connection_options = array()) { - $form['mysql'] = array( - 'title' => $t('MySQL database'), - 'value' => ($phase == 'runtime') ? l($version, 'admin/reports/status/sql') : $version, - ); + $connection_options += array( + 'transactions' => FALSE, + 'port' => 3306, + ); + $this->transactionSupport = $connection_options['transactions']; - if (version_compare($version, DRUPAL_MINIMUM_MYSQL) < 0) { - $form['mysql']['severity'] = REQUIREMENT_ERROR; - $form['mysql']['description'] = $t('Your MySQL Server is too old. Drupal requires at least MySQL %version.', array('%version' => DRUPAL_MINIMUM_MYSQL)); + $dsn = 'mysql:host=' . $connection_options['host'] . ';port=' . $connection_options['port'] . ';dbname=' . $connection_options['database']; + parent::__construct($dsn, $connection_options['username'], $connection_options['password'], array( + PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => TRUE , // So we don't have to mess around with cursors. + PDO::ATTR_EMULATE_PREPARES => TRUE,// Because MySQL's prepared statements skip the query cache, because it's dumb. + )); } - return $form; -} + public function queryRange($query, Array $args, $from, $count, Array $options) { + // Backward compatibility hack, temporary. + $query = str_replace(array('%d' , '%f' , '%b' , "'%s'"), '?', $query); -/** - * Returns the version of the database server currently in use. - * - * @return Database server version - */ -function db_version() { - list($version) = explode('-', mysql_get_server_info()); - return $version; -} + return $this->runQuery($query . ' LIMIT ' . $from . ', ' . $count, $args, $options); + } -/** - * Initialize a database connection. - */ -function db_connect($url) { - $url = parse_url($url); + public function queryTemporary($query, Array $args, $tablename) { + $query = preg_replace('/^SELECT/i', 'CREATE TEMPORARY TABLE ' . $tablename . ' Engine=HEAP SELECT', $this->prefixTables($query)); - // Check if MySQL support is present in PHP - if (!function_exists('mysql_connect')) { - _db_error_page('Unable to use the MySQL database because the MySQL extension for PHP is not installed. Check your php.ini to see how you can enable it.'); - } - - // Decode url-encoded information in the db connection string - $url['user'] = urldecode($url['user']); - // Test if database url has a password. - $url['pass'] = isset($url['pass']) ? urldecode($url['pass']) : ''; - $url['host'] = urldecode($url['host']); - $url['path'] = urldecode($url['path']); - - // Allow for non-standard MySQL port. - if (isset($url['port'])) { - $url['host'] = $url['host'] . ':' . $url['port']; - } - - // - TRUE makes mysql_connect() always open a new link, even if - // mysql_connect() was called before with the same parameters. - // This is important if you are using two databases on the same - // server. - // - 2 means CLIENT_FOUND_ROWS: return the number of found - // (matched) rows, not the number of affected rows. - $connection = @mysql_connect($url['host'], $url['user'], $url['pass'], TRUE, 2); - if (!$connection || !mysql_select_db(substr($url['path'], 1))) { - // Show error screen otherwise - _db_error_page(mysql_error()); - } - // Require ANSI mode to improve SQL portability. - mysql_query("SET SESSION sql_mode='ANSI'", $connection); - // Force UTF-8. - mysql_query('SET NAMES "utf8"', $connection); - return $connection; -} + return $this->runQuery($query, $args, $options); + } -/** - * Helper function for db_query(). - */ -function _db_query($query, $debug = 0) { - global $active_db, $queries, $user; + public function driver() { + return 'mysql'; + } - if (variable_get('dev_query', 0)) { - list($usec, $sec) = explode(' ', microtime()); - $timer = (float)$usec + (float)$sec; - // If devel.module query logging is enabled, prepend a comment with the username and calling function - // to the SQL string. This is useful when running mysql's SHOW PROCESSLIST to learn what exact - // code is issueing the slow query. - $bt = debug_backtrace(); - // t() may not be available yet so we don't wrap 'Anonymous'. - $name = $user->uid ? $user->name : variable_get('anonymous', 'Anonymous'); - // str_replace() to prevent SQL injection via username or anonymous name. - $name = str_replace(array('*', '/'), '', $name); - $query = '/* ' . $name . ' : ' . $bt[2]['function'] . ' */ ' . $query; - } - - $result = mysql_query($query, $active_db); - - if (variable_get('dev_query', 0)) { - $query = $bt[2]['function'] . "\n" . $query; - list($usec, $sec) = explode(' ', microtime()); - $stop = (float)$usec + (float)$sec; - $diff = $stop - $timer; - $queries[] = array($query, $diff); - } - - if ($debug) { - print '

query: ' . $query . '
error:' . mysql_error($active_db) . '

'; - } - - if (!mysql_errno($active_db)) { - return $result; - } - else { - // Indicate to drupal_error_handler that this is a database error. - ${DB_ERROR} = TRUE; - trigger_error(check_plain(mysql_error($active_db) . "\nquery: " . $query), E_USER_WARNING); - return FALSE; + public function databaseType() { + return 'mysql'; } -} -/** - * Fetch one result row from the previous query as an object. - * - * @param $result - * A database query result resource, as returned from db_query(). - * @return - * An object representing the next row of the result, or FALSE. The attributes - * of this object are the table fields selected by the query. - */ -function db_fetch_object($result) { - if ($result) { - return mysql_fetch_object($result); + public function supportsTransactions() { + return $this->transactionSupport; } -} -/** - * Fetch one result row from the previous query as an array. - * - * @param $result - * A database query result resource, as returned from db_query(). - * @return - * An associative array representing the next row of the result, or FALSE. - * The keys of this object are the names of the table fields selected by the - * query, and the values are the field values for this result row. - */ -function db_fetch_array($result) { - if ($result) { - return mysql_fetch_array($result, MYSQL_ASSOC); + public function escapeTable($table) { + return preg_replace('/[^A-Za-z0-9_]+/', '', $table); } -} -/** - * Return an individual result field from the previous query. - * - * Only use this function if exactly one field is being selected; otherwise, - * use db_fetch_object() or db_fetch_array(). - * - * @param $result - * A database query result resource, as returned from db_query(). - * @return - * The resulting field or FALSE. - */ -function db_result($result) { - if ($result && mysql_num_rows($result) > 0) { - // The mysql_fetch_row function has an optional second parameter $row - // but that can't be used for compatibility with Oracle, DB2, etc. - $array = mysql_fetch_row($result); - return $array[0]; + public function mapConditionOperator($operator) { + // We don't want to override any of the defaults. + return NULL; } - return FALSE; -} -/** - * Determine whether the previous query caused an error. - */ -function db_error() { - global $active_db; - return mysql_errno($active_db); -} + /** + * @todo Remove this as soon as db_rewrite_sql() has been exterminated. + */ + public function distinctField($table, $field, $query) { + $field_to_select = 'DISTINCT(' . $table . '.' . $field . ')'; + // (?insertFields, $this->defaultFields)) { + throw new PDOException('You may not specify the same field to have a value and a schema-default value.'); + } + + $last_insert_id = 0; -/** - * Prepare user input for use in a database query, preventing SQL injection attacks. - */ -function db_escape_string($text) { - global $active_db; - return mysql_real_escape_string($text, $active_db); -} + $max_placeholder = 0; + $values = array(); + foreach ($this->insertValues as $insert_values) { + foreach ($insert_values as $value) { + $values[':db_insert_placeholder_' . $max_placeholder++] = $value; + } + } -/** - * Lock a table. - */ -function db_lock_table($table) { - db_query('LOCK TABLES {' . db_escape_table($table) . '} WRITE'); -} + $last_insert_id = $this->connection->runQuery((string)$this, $values, $this->queryOptions); -/** - * Unlock all locked tables. - */ -function db_unlock_tables() { - db_query('UNLOCK TABLES'); -} + // Re-initialize the values array so that we can re-use this query. + $this->insertValues = array(); -/** - * Check if a table exists. - */ -function db_table_exists($table) { - return (bool) db_fetch_object(db_query("SHOW TABLES LIKE '{" . db_escape_table($table) . "}'")); + return $last_insert_id; + } + + public function __toString() { + + $delay = $this->queryOptions['delay'] ? 'DELAYED' : ''; + + // Default fields are always placed first for consistency. + $insert_fields = array_merge($this->defaultFields, $this->insertFields); + + $query = "INSERT $delay INTO {" . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES '; + + $max_placeholder = 0; + $values = array(); + if (count($this->insertValues)) { + foreach ($this->insertValues as $insert_values) { + $placeholders = array(); + + // Default fields aren't really placeholders, but this is the most convenient + // way to handle them. + $placeholders = array_pad($placeholders, count($this->defaultFields), 'default'); + + $new_placeholder = $max_placeholder + count($insert_values); + for ($i = $max_placeholder; $i < $new_placeholder; ++$i) { + $placeholders[] = ':db_insert_placeholder_'. $i; + } + $max_placeholder = $new_placeholder; + $values[] = '('. implode(', ', $placeholders) .')'; + } + } + else { + // If there are no values, then this is a default-only query. We still need to handle that. + $placeholders = array_fill(0, count($this->defaultFields), 'default'); + $values[] = '(' . implode(', ', $placeholders) .')'; + } + + $query .= implode(', ', $values); + + return $query; + } } -/** - * Check if a column exists in the given table. - */ -function db_column_exists($table, $column) { - return (bool) db_fetch_object(db_query("SHOW COLUMNS FROM {" . db_escape_table($table) . "} LIKE '" . db_escape_table($column) . "'")); +class MergeQuery_mysql extends MergeQuery { + + public function execute() { + //drupal_alter('query', $this->table, $this); + + // Set defaults. + 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]); + } + } + + $insert_fields = $this->insertFields + $this->keyFields; + + $max_placeholder = 0; + $values = array(); + // We assume that the order here is the same as in __toString(). If that's + // not the case, then we have serious problems. + foreach ($insert_fields as $value) { + $values[':db_insert_placeholder_' . $max_placeholder++] = $value; + } + + // Expressions take priority over literal fields, so we process those first + // and remove any literal fields that conflict. + foreach ($this->expressionFields as $field => $data) { + if (!empty($data['arguments'])) { + $values += $data['arguments']; + } + unset($update_fields[$field]); + } + + // Because we filter $fields the same way here and in __toString(), the + // placeholders will all match up properly. + $max_placeholder = 0; + foreach ($update_fields as $field => $value) { + $values[':db_update_placeholder_' . ($max_placeholder++)] = $value; + } + + $last_insert_id = $this->connection->runQuery((string)$this, $values, $this->queryOptions); + + return $last_insert_id; + } + + + public function __toString() { + + // Set defaults. + $update_fields = array(); + 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]); + } + } + + $insert_fields = $this->insertFields + $this->keyFields; + + $query = "INSERT INTO {" . $this->table . '} (' . implode(', ', array_keys($insert_fields)) . ') VALUES '; + + $max_placeholder = 0; + $values = array(); + // We don't need the $field, but this is a convenient way to count. + foreach ($insert_fields as $field) { + $values[] = ':db_insert_placeholder_' . $max_placeholder++; + } + + $query .= '(' . implode(', ', $values) . ') ON DUPLICATE KEY UPDATE '; + + // Expressions take priority over literal fields, so we process those first + // and remove any literal fields that conflict. + $max_placeholder = 0; + $update = array(); + foreach ($this->expressionFields as $field => $data) { + $update[] = $field . '=' . $data['expression']; + unset($update_fields[$field]); + } + + foreach ($update_fields as $field => $value) { + $update[] = ($field . '=:db_update_placeholder_' . $max_placeholder++); + } + + $query .= implode(', ', $update); + + return $query; + } + } -/** - * 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. - * - * @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) { - $field_to_select = 'DISTINCT(' . $table . '.' . $field . ')'; - // (? $t('MySQL database'), - 'value' => ($phase == 'runtime') ? l($version, 'admin/reports/status/sql') : $version, - ); - - if (version_compare($version, DRUPAL_MINIMUM_MYSQL) < 0) { - $form['mysql']['severity'] = REQUIREMENT_ERROR; - $form['mysql']['description'] = $t('Your MySQL Server is too old. Drupal requires at least MySQL %version.', array('%version' => DRUPAL_MINIMUM_MYSQL)); - } - - return $form; -} - -/** - * Returns the version of the database server currently in use. - * - * @return Database server version - */ -function db_version() { - global $active_db; - list($version) = explode('-', mysqli_get_server_info($active_db)); - return $version; -} - -/** - * Initialise a database connection. - * - * Note that mysqli does not support persistent connections. - */ -function db_connect($url) { - // Check if MySQLi support is present in PHP - if (!function_exists('mysqli_init') && !extension_loaded('mysqli')) { - _db_error_page('Unable to use the MySQLi database because the MySQLi extension for PHP is not installed. Check your php.ini to see how you can enable it.'); - } - - $url = parse_url($url); - - // Decode url-encoded information in the db connection string - $url['user'] = urldecode($url['user']); - // Test if database url has a password. - $url['pass'] = isset($url['pass']) ? urldecode($url['pass']) : ''; - $url['host'] = urldecode($url['host']); - $url['path'] = urldecode($url['path']); - if (!isset($url['port'])) { - $url['port'] = NULL; - } - - $connection = mysqli_init(); - @mysqli_real_connect($connection, $url['host'], $url['user'], $url['pass'], substr($url['path'], 1), $url['port'], NULL, MYSQLI_CLIENT_FOUND_ROWS); - - if (mysqli_connect_errno() > 0) { - _db_error_page(mysqli_connect_error()); - } - - // Force UTF-8. - mysqli_query($connection, 'SET NAMES "utf8"'); - // Require ANSI mode to improve SQL portability. - mysqli_query($connection, "SET SESSION sql_mode='ANSI'"); - - return $connection; -} - -/** - * Helper function for db_query(). - */ -function _db_query($query, $debug = 0) { - global $active_db, $queries, $user; - - if (variable_get('dev_query', 0)) { - list($usec, $sec) = explode(' ', microtime()); - $timer = (float)$usec + (float)$sec; - // If devel.module query logging is enabled, prepend a comment with the username and calling function - // to the SQL string. This is useful when running mysql's SHOW PROCESSLIST to learn what exact - // code is issueing the slow query. - $bt = debug_backtrace(); - // t() may not be available yet so we don't wrap 'Anonymous' - $name = $user->uid ? $user->name : variable_get('anonymous', 'Anonymous'); - // str_replace() to prevent SQL injection via username or anonymous name. - $name = str_replace(array('*', '/'), '', $name); - $query = '/* ' . $name . ' : ' . $bt[2]['function'] . ' */ ' . $query; - } - - $result = mysqli_query($active_db, $query); - - if (variable_get('dev_query', 0)) { - $query = $bt[2]['function'] . "\n" . $query; - list($usec, $sec) = explode(' ', microtime()); - $stop = (float)$usec + (float)$sec; - $diff = $stop - $timer; - $queries[] = array($query, $diff); - } - - if ($debug) { - print '

query: ' . $query . '
error:' . mysqli_error($active_db) . '

'; - } - - if (!mysqli_errno($active_db)) { - return $result; - } - else { - // Indicate to drupal_error_handler that this is a database error. - ${DB_ERROR} = TRUE; - trigger_error(check_plain(mysqli_error($active_db) . "\nquery: " . $query), E_USER_WARNING); - return FALSE; - } -} - -/** - * Fetch one result row from the previous query as an object. - * - * @param $result - * A database query result resource, as returned from db_query(). - * @return - * An object representing the next row of the result, or FALSE. The attributes - * of this object are the table fields selected by the query. - */ -function db_fetch_object($result) { - if ($result) { - $object = mysqli_fetch_object($result); - return isset($object) ? $object : FALSE; - } -} - -/** - * Fetch one result row from the previous query as an array. - * - * @param $result - * A database query result resource, as returned from db_query(). - * @return - * An associative array representing the next row of the result, or FALSE. - * The keys of this object are the names of the table fields selected by the - * query, and the values are the field values for this result row. - */ -function db_fetch_array($result) { - if ($result) { - $array = mysqli_fetch_array($result, MYSQLI_ASSOC); - return isset($array) ? $array : FALSE; - } -} - -/** - * Return an individual result field from the previous query. - * - * Only use this function if exactly one field is being selected; otherwise, - * use db_fetch_object() or db_fetch_array(). - * - * @param $result - * A database query result resource, as returned from db_query(). - * @return - * The resulting field or FALSE. - */ -function db_result($result) { - if ($result && mysqli_num_rows($result) > 0) { - // The mysqli_fetch_row function has an optional second parameter $row - // but that can't be used for compatibility with Oracle, DB2, etc. - $array = mysqli_fetch_row($result); - return $array[0]; - } - return FALSE; -} - -/** - * Determine whether the previous query caused an error. - */ -function db_error() { - global $active_db; - return mysqli_errno($active_db); -} - -/** - * Determine the number of rows changed by the preceding query. - */ -function db_affected_rows() { - global $active_db; /* mysqli connection resource */ - return mysqli_affected_rows($active_db); -} - -/** - * Runs a limited-range query in the active database. - * - * Use this as a substitute for db_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 ... - * A variable number of arguments which are substituted into the query - * using printf() syntax. The query arguments can be enclosed in one - * array instead. - * Valid %-modifiers are: %s, %d, %f, %b (binary data, do not enclose - * in '') and %%. - * - * NOTE: using this syntax will cast NULL and FALSE values to decimal 0, - * and TRUE values to decimal 1. - * - * @param $from - * The first result row to return. - * @param $count - * The maximum number of result rows to return. - * @return - * A database query result resource, or FALSE if the query was not executed - * correctly. - */ -function db_query_range($query) { - $args = func_get_args(); - $count = array_pop($args); - $from = array_pop($args); - array_shift($args); - - $query = db_prefix_tables($query); - if (isset($args[0]) and is_array($args[0])) { // 'All arguments in one array' syntax - $args = $args[0]; - } - _db_query_callback($args, TRUE); - $query = preg_replace_callback(DB_QUERY_REGEXP, '_db_query_callback', $query); - $query .= ' LIMIT ' . (int)$from . ', ' . (int)$count; - return _db_query($query); -} - -/** - * Runs a SELECT query and stores its results in a temporary table. - * - * Use this as a substitute for db_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. db_affected_rows() does - * not give consistent result across different database types in this case. - * - * @param $query - * A string containing a normal SELECT SQL query. - * @param ... - * A variable number of arguments which are substituted into the query - * using printf() syntax. The query arguments can be enclosed in one - * array instead. - * Valid %-modifiers are: %s, %d, %f, %b (binary data, do not enclose - * in '') and %%. - * - * NOTE: using this syntax will cast NULL and FALSE values to decimal 0, - * and TRUE values to decimal 1. - * - * @param $table - * 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. - */ -function db_query_temporary($query) { - $args = func_get_args(); - $tablename = array_pop($args); - array_shift($args); - - $query = preg_replace('/^SELECT/i', 'CREATE TEMPORARY TABLE ' . $tablename . ' Engine=HEAP SELECT', db_prefix_tables($query)); - if (isset($args[0]) and is_array($args[0])) { // 'All arguments in one array' syntax - $args = $args[0]; - } - _db_query_callback($args, TRUE); - $query = preg_replace_callback(DB_QUERY_REGEXP, '_db_query_callback', $query); - return _db_query($query); -} - -/** - * Returns a properly formatted Binary Large Object value. - * - * @param $data - * Data to encode. - * @return - * Encoded data. - */ -function db_encode_blob($data) { - global $active_db; - return "'" . mysqli_real_escape_string($active_db, $data) . "'"; -} - -/** - * Returns text from a Binary Large OBject value. - * - * @param $data - * Data to decode. - * @return - * Decoded data. - */ -function db_decode_blob($data) { - return $data; -} - -/** - * Prepare user input for use in a database query, preventing SQL injection attacks. - */ -function db_escape_string($text) { - global $active_db; - return mysqli_real_escape_string($active_db, $text); -} - -/** - * Lock a table. - */ -function db_lock_table($table) { - db_query('LOCK TABLES {' . db_escape_table($table) . '} WRITE'); -} - -/** - * Unlock all locked tables. - */ -function db_unlock_tables() { - db_query('UNLOCK TABLES'); -} - -/** - * Check if a table exists. - */ -function db_table_exists($table) { - return (bool) db_fetch_object(db_query("SHOW TABLES LIKE '{" . db_escape_table($table) . "}'")); -} - -/** - * Check if a column exists in the given table. - */ -function db_column_exists($table, $column) { - return (bool) db_fetch_object(db_query("SHOW COLUMNS FROM {" . db_escape_table($table) . "} LIKE '" . db_escape_table($column) . "'")); -} - -/** - * 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. - * - * @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) { - $field_to_select = 'DISTINCT(' . $table . '.' . $field . ')'; - // (? $t('PostgreSQL database'), - 'value' => $version, - ); + public function __construct(Array $connection_options = array()) { - if (version_compare($version, DRUPAL_MINIMUM_PGSQL) < 0) { - $form['pgsql']['severity'] = REQUIREMENT_ERROR; - $form['pgsql']['description'] = $t('Your PostgreSQL Server is too old. Drupal requires at least PostgreSQL %version.', array('%version' => DRUPAL_MINIMUM_PGSQL)); - } + $connection_options += array( + 'transactions' => TRUE, + ); + $this->transactionSupport = $connection_options['transactions']; - return $form; -} - -/** - * Returns the version of the database server currently in use. - * - * @return Database server version - */ -function db_version() { - return db_result(db_query("SHOW SERVER_VERSION")); -} + $dsn = 'pgsql:host=' . $connection_options['host'] . ' dbname=' . $connection_options['database']; + if (!empty($connection_options['port'])) { + $dsn .= ' port=' . $connection_options['port']; + } -/** - * Initialize a database connection. - */ -function db_connect($url) { - // Check if PostgreSQL support is present in PHP - if (!function_exists('pg_connect')) { - _db_error_page('Unable to use the PostgreSQL database because the PostgreSQL extension for PHP is not installed. Check your php.ini to see how you can enable it.'); + parent::__construct($dsn, $connection_options['username'], $connection_options['password'], array(PDO::ATTR_STRINGIFY_FETCHES => TRUE)); } - $url = parse_url($url); - $conn_string = ''; + protected function runQuery($query, Array $args = array(), $options = array()) { - // Decode url-encoded information in the db connection string - if (isset($url['user'])) { - $conn_string .= ' user=' . urldecode($url['user']); - } - if (isset($url['pass'])) { - $conn_string .= ' password=' . urldecode($url['pass']); - } - if (isset($url['host'])) { - $conn_string .= ' host=' . urldecode($url['host']); - } - if (isset($url['path'])) { - $conn_string .= ' dbname=' . substr(urldecode($url['path']), 1); - } - if (isset($url['port'])) { - $conn_string .= ' port=' . urldecode($url['port']); + $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($options['sequence_name']); + 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; + } } + + public function queryRange($query, Array $args, $from, $count, Array $options) { + // Backward compatibility hack, temporary. + $query = str_replace(array('%d' , '%f' , '%b' , "'%s'"), '?', $query); - // pg_last_error() does not return a useful error message for database - // connection errors. We must turn on error tracking to get at a good error - // message, which will be stored in $php_errormsg. - $track_errors_previous = ini_get('track_errors'); - ini_set('track_errors', 1); - - $connection = @pg_connect($conn_string); - if (!$connection) { - require_once './includes/unicode.inc'; - _db_error_page(decode_entities($php_errormsg)); + return $this->runQuery($query . ' LIMIT ' . $count . ' OFFSET ' . $from, $args, $options); } - // Restore error tracking setting - ini_set('track_errors', $track_errors_previous); - - return $connection; -} - -/** - * Runs a basic query in the active database. - * - * 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 ... - * A variable number of arguments which are substituted into the query - * using printf() syntax. Instead of a variable number of query arguments, - * you may also pass a single array containing the query arguments. - * - * Valid %-modifiers are: %s, %d, %f, %b (binary data, do not enclose - * in '') and %%. - * - * NOTE: using this syntax will cast NULL and FALSE values to decimal 0, - * and TRUE values to decimal 1. - * - * @return - * A database query result resource, or FALSE if the query was not - * executed correctly. - */ -function db_query($query) { - $args = func_get_args(); - array_shift($args); - $query = db_prefix_tables($query); - if (isset($args[0]) and is_array($args[0])) { // 'All arguments in one array' syntax - $args = $args[0]; - } - _db_query_callback($args, TRUE); - $query = preg_replace_callback(DB_QUERY_REGEXP, '_db_query_callback', $query); - return _db_query($query); -} - -/** - * Helper function for db_query(). - */ -function _db_query($query, $debug = 0) { - global $active_db, $last_result, $queries; + public function queryTemporary($query, Array $args, $tablename) { + $query = preg_replace('/^SELECT/i', 'CREATE TEMPORARY TABLE ' . $tablename . ' Engine=HEAP SELECT', $this->prefixTables($query)); - if (variable_get('dev_query', 0)) { - list($usec, $sec) = explode(' ', microtime()); - $timer = (float)$usec + (float)$sec; + return $this->runQuery($query, $args, $options); } - $last_result = pg_query($active_db, $query); - - if (variable_get('dev_query', 0)) { - $bt = debug_backtrace(); - $query = $bt[2]['function'] . "\n" . $query; - list($usec, $sec) = explode(' ', microtime()); - $stop = (float)$usec + (float)$sec; - $diff = $stop - $timer; - $queries[] = array($query, $diff); + public function driver() { + return 'pgsql'; } - if ($debug) { - print '

query: ' . $query . '
error:' . pg_last_error($active_db) . '

'; + public function databaseType() { + return 'pgsql'; } - if ($last_result !== FALSE) { - return $last_result; + public function supportsTransactions() { + return $this->transactionSupport; } - else { - // Indicate to drupal_error_handler that this is a database error. - ${DB_ERROR} = TRUE; - trigger_error(check_plain(pg_last_error($active_db) . "\nquery: " . $query), E_USER_WARNING); - return FALSE; - } -} -/** - * Fetch one result row from the previous query as an object. - * - * @param $result - * A database query result resource, as returned from db_query(). - * @return - * An object representing the next row of the result, or FALSE. The attributes - * of this object are the table fields selected by the query. - */ -function db_fetch_object($result) { - if ($result) { - return pg_fetch_object($result); + public function escapeTable($table) { + return preg_replace('/[^A-Za-z0-9_]+/', '', $table); } -} -/** - * Fetch one result row from the previous query as an array. - * - * @param $result - * A database query result resource, as returned from db_query(). - * @return - * An associative array representing the next row of the result, or FALSE. - * The keys of this object are the names of the table fields selected by the - * query, and the values are the field values for this result row. - */ -function db_fetch_array($result) { - if ($result) { - return pg_fetch_assoc($result); - } -} + public function mapConditionOperator($operator) { + static $specials = array( + // In PostgreSQL, 'LIKE' is case-sensitive. For case-insensitive LIKE + // statements, we need to use ILIKE instead. + 'LIKE' => array('operator' => 'ILIKE'), + ); -/** - * Return an individual result field from the previous query. - * - * Only use this function if exactly one field is being selected; otherwise, - * use db_fetch_object() or db_fetch_array(). - * - * @param $result - * A database query result resource, as returned from db_query(). - * @return - * The resulting field or FALSE. - */ -function db_result($result) { - if ($result && pg_num_rows($result) > 0) { - $array = pg_fetch_row($result); - return $array[0]; + return isset($specials[$operator]) ? $specials[$operator] : NULL; } - return FALSE; -} - -/** - * Determine whether the previous query caused an error. - */ -function db_error() { - global $active_db; - return pg_last_error($active_db); -} - -/** - * Returns the last insert id. This function is thread safe. - * - * @param $table - * The name of the table you inserted into. - * @param $field - * The name of the autoincrement field. - */ -function db_last_insert_id($table, $field) { - return db_result(db_query("SELECT CURRVAL('{" . db_escape_table($table) . "}_" . db_escape_table($field) . "_seq')")); -} - -/** - * Determine the number of rows changed by the preceding query. - */ -function db_affected_rows() { - global $last_result; - return empty($last_result) ? 0 : pg_affected_rows($last_result); -} -/** - * Runs a limited-range query in the active database. - * - * Use this as a substitute for db_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 ... - * A variable number of arguments which are substituted into the query - * using printf() syntax. Instead of a variable number of query arguments, - * you may also pass a single array containing the query arguments. - * Valid %-modifiers are: %s, %d, %f, %b (binary data, do not enclose - * in '') and %%. - * - * NOTE: using this syntax will cast NULL and FALSE values to decimal 0, - * and TRUE values to decimal 1. - * - * @param $from - * The first result row to return. - * @param $count - * The maximum number of result rows to return. - * @return - * A database query result resource, or FALSE if the query was not executed - * correctly. - */ -function db_query_range($query) { - $args = func_get_args(); - $count = array_pop($args); - $from = array_pop($args); - array_shift($args); - - $query = db_prefix_tables($query); - if (isset($args[0]) and is_array($args[0])) { // 'All arguments in one array' syntax - $args = $args[0]; - } - _db_query_callback($args, TRUE); - $query = preg_replace_callback(DB_QUERY_REGEXP, '_db_query_callback', $query); - $query .= ' LIMIT ' . (int)$count . ' OFFSET ' . (int)$from; - return _db_query($query); -} - -/** - * Runs a SELECT query and stores its results in a temporary table. - * - * Use this as a substitute for db_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. db_affected_rows() does - * not give consistent result across different database types in this case. - * - * @param $query - * A string containing a normal SELECT SQL query. - * @param ... - * A variable number of arguments which are substituted into the query - * using printf() syntax. The query arguments can be enclosed in one - * array instead. - * Valid %-modifiers are: %s, %d, %f, %b (binary data, do not enclose - * in '') and %%. - * - * NOTE: using this syntax will cast NULL and FALSE values to decimal 0, - * and TRUE values to decimal 1. - * - * @param $table - * 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. - */ -function db_query_temporary($query) { - $args = func_get_args(); - $tablename = array_pop($args); - array_shift($args); - - $query = preg_replace('/^SELECT/i', 'CREATE TEMPORARY TABLE ' . $tablename . ' AS SELECT', db_prefix_tables($query)); - if (isset($args[0]) and is_array($args[0])) { // 'All arguments in one array' syntax - $args = $args[0]; - } - _db_query_callback($args, TRUE); - $query = preg_replace_callback(DB_QUERY_REGEXP, '_db_query_callback', $query); - return _db_query($query); -} - -/** - * Returns a properly formatted Binary Large OBject value. - * In case of PostgreSQL encodes data for insert into bytea field. - * - * @param $data - * Data to encode. - * @return - * Encoded data. - */ -function db_encode_blob($data) { - return "'" . pg_escape_bytea($data) . "'"; -} - -/** - * Returns text from a Binary Large OBject value. - * In case of PostgreSQL decodes data after select from bytea field. - * - * @param $data - * Data to decode. - * @return - * Decoded data. - */ -function db_decode_blob($data) { - return pg_unescape_bytea($data); -} - -/** - * Prepare user input for use in a database query, preventing SQL injection attacks. - * Note: This function requires PostgreSQL 7.2 or later. - */ -function db_escape_string($text) { - return pg_escape_string($text); -} - -/** - * Lock a table. - * This function automatically starts a transaction. - */ -function db_lock_table($table) { - db_query('BEGIN; LOCK TABLE {' . db_escape_table($table) . '} IN EXCLUSIVE MODE'); -} - -/** - * Unlock all locked tables. - * This function automatically commits a transaction. - */ -function db_unlock_tables() { - db_query('COMMIT'); -} - -/** - * Check if a table exists. - */ -function db_table_exists($table) { - return (bool) db_result(db_query("SELECT COUNT(*) FROM pg_class WHERE relname = '{" . db_escape_table($table) . "}'")); -} - -/** - * Check if a column exists in the given table. - */ -function db_column_exists($table, $column) { - return (bool) db_result(db_query("SELECT COUNT(pg_attribute.attname) FROM pg_class, pg_attribute WHERE pg_attribute.attrelid = pg_class.oid AND pg_class.relname = '{" . db_escape_table($table) . "}' AND attname = '" . db_escape_table($column) . "'")); -} - -/** - * Verify if the database is set up correctly. - */ -function db_check_setup() { - $t = get_t(); + /* + public function setLastInsertInfo($table, $field) { + if (isset($table) && isset($field)) { + $this->lastInsertInfo = $this->prefixTables('{'. $table .'}_'. $field .'_seq'); + } + else { + $this->lastInsertInfo = NULL; + } + } +*/ + /* + public function lastInsertId() { + return isset($this->lastInsertInfo) ? parent::lastInsertId($this->lastInsertInfo) : NULL; + } +*/ - $encoding = db_result(db_query('SHOW server_encoding')); - if (!in_array(strtolower($encoding), array('unicode', 'utf8'))) { - drupal_set_message($t('Your PostgreSQL database is set up with the wrong character encoding (%encoding). It is possible it will not work as expected. It is advised to recreate it with UTF-8/Unicode encoding. More information can be found in the PostgreSQL documentation.', array('%encoding' => $encoding, '@url' => 'http://www.postgresql.org/docs/7.4/interactive/multibyte.html')), 'status'); + /** + * @todo Remove this as soon as db_rewrite_sql() has been exterminated. + */ + public function distinctField($table, $field, $query) { + $field_to_select = 'DISTINCT(' . $table . '.' . $field . ')'; + // (? 'varchar', - 'char:normal' => 'character', - - 'text:tiny' => 'text', - 'text:small' => 'text', - 'text:medium' => 'text', - 'text:big' => 'text', - 'text:normal' => 'text', - - 'int:tiny' => 'smallint', - 'int:small' => 'smallint', - 'int:medium' => 'int', - 'int:big' => 'bigint', - 'int:normal' => 'int', - - 'float:tiny' => 'real', - 'float:small' => 'real', - 'float:medium' => 'real', - 'float:big' => 'double precision', - 'float:normal' => 'real', - - 'numeric:normal' => 'numeric', - - 'blob:big' => 'bytea', - 'blob:normal' => 'bytea', - - 'datetime:normal' => 'timestamp', - - 'serial:tiny' => 'serial', - 'serial:small' => 'serial', - 'serial:medium' => 'serial', - 'serial:big' => 'bigserial', - 'serial:normal' => 'serial', - ); - return $map; -} +class DatabaseTransaction_pgsql extends DatabaseTransaction { } -/** - * Generate SQL to create a new table from a Drupal schema definition. - * - * @param $name - * The name of the table to create. - * @param $table - * A Schema API table definition array. - * @return - * An array of SQL statements to create the table. - */ -function db_create_table_sql($name, $table) { - $sql_fields = array(); - foreach ($table['fields'] as $field_name => $field) { - $sql_fields[] = _db_create_field_sql($field_name, _db_process_field($field)); - } +class SelectQuery_pgsql extends SelectQuery { } - $sql_keys = array(); - if (isset($table['primary key']) && is_array($table['primary key'])) { - $sql_keys[] = 'PRIMARY KEY (' . implode(', ', $table['primary key']) . ')'; - } - if (isset($table['unique keys']) && is_array($table['unique keys'])) { - foreach ($table['unique keys'] as $key_name => $key) { - $sql_keys[] = 'CONSTRAINT {' . $name . '}_' . $key_name . '_key UNIQUE (' . implode(', ', $key) . ')'; - } - } +class InsertQuery_pgsql extends InsertQuery { - $sql = "CREATE TABLE {" . $name . "} (\n\t"; - $sql .= implode(",\n\t", $sql_fields); - if (count($sql_keys) > 0) { - $sql .= ",\n\t"; + public function __construct($connection, $table, Array $options = array()) { + parent::__construct($connection, $table, $options); + $this->queryOptions['return'] = Database::RETURN_NULL; } - $sql .= implode(",\n\t", $sql_keys); - $sql .= "\n)"; - $statements[] = $sql; - - if (isset($table['indexes']) && is_array($table['indexes'])) { - foreach ($table['indexes'] as $key_name => $key) { - $statements[] = _db_create_index_sql($name, $key_name, $key); + + public function execute() { + + // 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.'); } - } - return $statements; -} + $schema = drupal_get_schema($this->table); -function _db_create_index_sql($table, $name, $fields) { - $query = 'CREATE INDEX {' . $table . '}_' . $name . '_idx ON {' . $table . '} ('; - $query .= _db_create_key_sql($fields) . ')'; - return $query; -} + $stmt = $this->connection->prepareQuery((string)$this); -function _db_create_key_sql($fields) { - $ret = array(); - foreach ($fields as $field) { - if (is_array($field)) { - $ret[] = 'substr(' . $field[0] . ', 1, ' . $field[1] . ')'; + $max_placeholder = 0; + $blobs = array(); + $blob_cnt = 0; + foreach ($this->insertValues as &$insert_values) { + foreach ($this->insertFields as $idx => $field) { + switch ($schema['fields'][$field]['type']) { + case 'blob': + $blobs[$blob_cnt] = fopen('php://memory', 'a'); + fwrite($blobs[$blob_cnt], $insert_values[$idx]); + rewind($blobs[$blob_cnt]); + + $stmt->bindParam(':db_insert_placeholder_' . $max_placeholder++, $blobs[$blob_cnt], PDO::PARAM_LOB); + + ++$blob_cnt; + + break; + default: + $stmt->bindParam(':db_insert_placeholder_'. $max_placeholder++, $insert_values[$idx]); + break; + } + } } - else { - $ret[] = $field; - } - } - return implode(', ', $ret); -} -function _db_create_keys(&$ret, $table, $new_keys) { - if (isset($new_keys['primary key'])) { - db_add_primary_key($ret, $table, $new_keys['primary key']); - } - if (isset($new_keys['unique keys'])) { - foreach ($new_keys['unique keys'] as $name => $fields) { - db_add_unique_key($ret, $table, $name, $fields); + // PostgreSQL requires the table name to be specified explicitly + // when requesting the last insert ID, so we pass that in via + // the options array. + $options = $this->queryOptions; + + if ($schema['fields'][$schema['primary key'][0]]['type'] == 'serial') { + $options['sequence_name'] = $this->connection->makeSequenceName($this->table, $schema['primary key'][0]); + $options['return'] = Database::RETURN_INSERT_ID; } - } - if (isset($new_keys['indexes'])) { - foreach ($new_keys['indexes'] as $name => $fields) { - db_add_index($ret, $table, $name, $fields); + $last_insert_id = $this->connection->runQuery($stmt, array(), $options); + + // Re-initialize the values array so that we can re-use this query. + $this->insertValues = array(); + + return $last_insert_id; + } + + public function __toString() { + + // Default fields are always placed first for consistency. + $insert_fields = array_merge($this->defaultFields, $this->insertFields); + + $query = "INSERT INTO {" . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES '; + + $max_placeholder = 0; + $values = array(); + if (count($this->insertValues)) { + foreach ($this->insertValues as $insert_values) { + $placeholders = array(); + + // Default fields aren't really placeholders, but this is the most convenient + // way to handle them. + $placeholders = array_pad($placeholders, count($this->defaultFields), 'default'); + + $new_placeholder = $max_placeholder + count($insert_values); + for ($i = $max_placeholder; $i < $new_placeholder; ++$i) { + $placeholders[] = ':db_insert_placeholder_' . $i; + } + $max_placeholder = $new_placeholder; + $values[] = '(' . implode(', ', $placeholders) . ')'; + } } - } -} - -/** - * Set database-engine specific properties for a field. - * - * @param $field - * A field description array, as specified in the schema documentation. - */ -function _db_process_field($field) { - if (!isset($field['size'])) { - $field['size'] = 'normal'; - } - // Set the correct database-engine specific datatype. - if (!isset($field['pgsql_type'])) { - $map = db_type_map(); - $field['pgsql_type'] = $map[$field['type'] . ':' . $field['size']]; - } - if ($field['type'] == 'serial') { - unset($field['not null']); - } - return $field; -} - -/** - * Create an SQL string for a field to be used in table creation or alteration. - * - * Before passing a field out of a schema definition into this function it has - * to be processed by _db_process_field(). - * - * @param $name - * Name of the field. - * @param $spec - * The field specification, as per the schema data structure format. - */ -function _db_create_field_sql($name, $spec) { - $sql = $name . ' ' . $spec['pgsql_type']; - - if ($spec['type'] == 'serial') { - unset($spec['not null']); - } - - // pgsql does not have unsigned types but supports constraints to - // restricted a signed field to be non-negative (e.g. CHECK (VALUE - // >= 0)). system.module defines {,small,big}int_unsigned as the - // corresponding integer type with this constraint but does not do - // so for serial or numeric types. It probably would have been - // cleaner to unify unsigned handling but, for now, we use the - // *int_unsigned types for int and otherwise apply a column - // constraint explicitly. - if (!empty($spec['unsigned'])) { - switch ($spec['type']) { - case 'int': - $sql .= '_unsigned'; - break; - case 'serial': - case 'float': - $sql .= " CHECK ($name >= 0)"; - break; - case 'numeric': - // handled below - break; - default: - // unsigned is not supported on other column types - break; + else { + // If there are no values, then this is a default-only query. We still need to handle that. + $placeholders = array_fill(0, count($this->defaultFields), 'default'); + $values[] = '(' . implode(', ', $placeholders) .')'; } - } - - if (!empty($spec['length'])) { - $sql .= '(' . $spec['length'] . ')'; - } - elseif (isset($spec['precision']) && isset($spec['scale'])) { - $sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')'; - } - // For numeric columns this has to come after (precision,scale). - if ($spec['type'] == 'numeric' && !empty($spec['unsigned'])) { - $sql .= " CHECK ($name >= 0)"; - } + $query .= implode(', ', $values); - if (isset($spec['not null']) && $spec['not null']) { - $sql .= ' NOT NULL'; + return $query; } - if (isset($spec['default'])) { - $default = is_string($spec['default']) ? "'" . $spec['default'] . "'" : $spec['default']; - $sql .= " default $default"; - } - - return $sql; } -/** - * 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) { - $ret[] = update_sql('ALTER TABLE {' . $table . '} RENAME TO {' . $new_name . '}'); -} +class UpdateQuery_pgsql extends UpdateQuery { + public function execute() { + $max_placeholder = 0; + $blobs = array(); + $blob_count = 0; + + $schema = drupal_get_schema($this->table); + + // Because we filter $fields the same way here and in __toString(), the + // placeholders will all match up properly. + $stmt = $this->connection->prepareQuery((string)$this); + + // Expressions take priority over literal fields, so we process those first + // and remove any literal fields that conflict. + $fields = $this->fields; + $expression_fields = array(); + foreach ($this->expressionFields as $field => $data) { + if (!empty($data['arguments'])) { + foreach ($data['arguments'] as $placeholder => $argument) { + // We assume that an expression will never happen on a BLOB field, + // which is a fairly safe assumption to make since in most cases + // it would be an invalid query anyway. + $stmt->bindParam($placeholder, $argument); + } + } + unset($fields[$field]); + } + + foreach ($fields as $field => &$value) { + $placeholder = ':db_update_placeholder_' . ($max_placeholder++); + + switch ($schema['fields'][$field]['type']) { + case 'blob': + $blobs[$blob_count] = fopen('php://memory', 'a'); + fwrite($blobs[$blob_count], $value); + rewind($blobs[$blob_count]); + $stmt->bindParam($placeholder, $blobs[$blob_count], PDO::PARAM_LOB); + ++$blob_count; + break; + default: + $stmt->bindParam($placeholder, $value); + break; + } + } -/** - * Drop a table. - * - * @param $ret - * Array to which query results will be added. - * @param $table - * The table to be dropped. - */ -function db_drop_table(&$ret, $table) { - $ret[] = update_sql('DROP TABLE {' . $table . '}'); -} + if (count($this->condition)) { + $this->condition->compile($this->connection); -/** - * Add a new field to a table. - * - * @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_add_field(&$ret, $table, $field, $spec, $new_keys = array()) { - $fixnull = FALSE; - if (!empty($spec['not null']) && !isset($spec['default'])) { - $fixnull = TRUE; - $spec['not null'] = FALSE; - } - $query = 'ALTER TABLE {' . $table . '} ADD COLUMN '; - $query .= _db_create_field_sql($field, _db_process_field($spec)); - $ret[] = update_sql($query); - if (isset($spec['initial'])) { - // All this because update_sql does not support %-placeholders. - $sql = 'UPDATE {' . $table . '} SET ' . $field . ' = ' . db_type_placeholder($spec['type']); - $result = db_query($sql, $spec['initial']); - $ret[] = array('success' => $result !== FALSE, 'query' => check_plain($sql . ' (' . $spec['initial'] . ')')); - } - if ($fixnull) { - $ret[] = update_sql("ALTER TABLE {" . $table . "} ALTER $field SET NOT NULL"); - } - if (isset($new_keys)) { - _db_create_keys($ret, $table, $new_keys); - } -} - -/** - * Drop a field. - * - * @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) { - $ret[] = update_sql('ALTER TABLE {' . $table . '} DROP COLUMN ' . $field); -} + $arguments = $this->condition->arguments(); + foreach ($arguments as $placeholder => &$value) { + $stmt->bindParam($placeholder, $value); + } + } -/** - * Set the default value for a field. - * - * @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) { - if ($default == NULL) { - $default = 'NULL'; - } - else { - $default = is_string($default) ? "'$default'" : $default; + $options = $this->queryOptions; + $options['already_prepared'] = TRUE; + $this->connection->runQuery($stmt, $options); + + //$stmt->execute(NULL, $this->queryOptions); + return $stmt->rowCount(); } - - $ret[] = update_sql('ALTER TABLE {' . $table . '} ALTER COLUMN ' . $field . ' SET DEFAULT ' . $default); } -/** - * Set a field to have no default value. - * - * @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_field_set_no_default(&$ret, $table, $field) { - $ret[] = update_sql('ALTER TABLE {' . $table . '} ALTER COLUMN ' . $field . ' DROP DEFAULT'); -} +class MergeQuery_pgsql extends MergeQuery { } -/** - * 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) { - $ret[] = update_sql('ALTER TABLE {' . $table . '} ADD PRIMARY KEY (' . - implode(',', $fields) . ')'); -} +class DeleteQuery_pgsql extends DeleteQuery { } -/** - * 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) { - $ret[] = update_sql('ALTER TABLE {' . $table . '} DROP CONSTRAINT {' . $table . '}_pkey'); -} - -/** - * 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) { - $name = '{' . $table . '}_' . $name . '_key'; - $ret[] = update_sql('ALTER TABLE {' . $table . '} ADD CONSTRAINT ' . - $name . ' UNIQUE (' . implode(',', $fields) . ')'); -} - -/** - * 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) { - $name = '{' . $table . '}_' . $name . '_key'; - $ret[] = update_sql('ALTER TABLE {' . $table . '} DROP CONSTRAINT ' . $name); -} - -/** - * 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) { - $ret[] = update_sql(_db_create_index_sql($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) { - $name = '{' . $table . '}_' . $name . '_idx'; - $ret[] = update_sql('DROP INDEX ' . $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 $new_keys 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 - * $new_keys 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 $new_keys 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 $new_keys - * Optional keys and indexes specification to be created on the - * table along with changing the field. The format is the same as a - * table specification but without the 'fields' element. - */ -function db_change_field(&$ret, $table, $field, $field_new, $spec, $new_keys = array()) { - $ret[] = update_sql("ALTER TABLE {" . $table . "} RENAME $field TO " . $field . "_old"); - $not_null = isset($spec['not null']) ? $spec['not null'] : FALSE; - unset($spec['not null']); - - db_add_field($ret, $table, "$field_new", $spec); - - $ret[] = update_sql("UPDATE {" . $table . "} SET $field_new = " . $field . "_old"); - - if ($not_null) { - $ret[] = update_sql("ALTER TABLE {" . $table . "} ALTER $field_new SET NOT NULL"); - } - - db_drop_field($ret, $table, $field . '_old'); - - if (isset($new_keys)) { - _db_create_keys($ret, $table, $new_keys); +if (class_exists('DatabaseInstaller')) { + class DatabaseInstaller_pgsql extends DatabaseInstaller { + protected $pdoDriver = 'pgsql'; + public function name() { + return 'PostgreSQL'; + } } } /** - * @} End of "ingroup schemaapi". + * @} End of "ingroup database". */ - === modified file 'includes/install.inc' --- includes/install.inc 2008-05-26 17:12:54 +0000 +++ includes/install.inc 2008-07-22 07:28:50 +0000 @@ -204,19 +204,112 @@ function drupal_detect_baseurl($file = ' function drupal_detect_database_types() { $databases = array(); - foreach (array('mysql', 'mysqli', 'pgsql') as $type) { - if (file_exists('./includes/install.' . $type . '.inc')) { - include_once './includes/install.' . $type . '.inc'; - $function = $type . '_is_available'; - if ($function()) { - $databases[$type] = $type; - } + foreach (glob('./includes/database.*.inc') as $driver_file) { + list(, $driver) = explode('.', basename($driver_file)); + $drivers[] = $driver; + include_once $driver_file; + $install_file = './includes/install.' . $driver . '.inc'; + if (file_exists($install_file)) { + include_once $install_file; + } + $class = 'DatabaseInstaller_' . $driver; + $installer = new $class(); + if ($installer->installable()) { + $databases[$driver] = $installer->name(); } } return $databases; } +abstract class DatabaseInstaller { + protected $success = array(); + protected $tests = array( + 'testCreate' => array( + 'query' => 'CREATE TABLE drupal_install_test (id int NULL)', + 'success' => 'CREATE', + 'message' => 'Failed to create a test table on your %name database server with the command %query. %name reports the following message: %error.
  • Are you sure the configured username has the necessary %name permissions to create tables in the database?
For more help, see the Installation and upgrading handbook. If you are unsure what these terms mean you should probably contact your hosting provider.', + 'fatal' => TRUE, + ), + 'testInsert' => array( + 'query' => 'INSERT INTO drupal_install_test (id) VALUES (1)', + 'success' => 'INSERT', + 'message' => 'Failed to insert a value into a test table on your %name database server. We tried inserting a value with the command %query and %name reported the following error: %error.', + ), + 'testUpdate' => array( + 'query' => 'UPDATE drupal_install_test SET id = 2', + 'success' => 'UPDATE', + 'message' => 'Failed to update a value in a test table on your %name database server. We tried updating a value with the command %query and %name reported the following error: %error.', + ), + 'testDelete' => array( + 'query' => 'DELETE FROM drupal_install_test', + 'success' => 'DELETE', + 'message' => 'Failed to delete a value from a test table on your %name database server. We tried deleting a value with the command %query and %name reported the following error: %error.', + ), + 'testDrop' => array( + 'query' => 'DROP TABLE drupal_install_test', + 'success' => 'DELETE', + 'message' => 'Failed to drop a test table from your %name database server. We tried dropping a table with the command %query and %name reported the following error %error.', + ), + ); + public $error = FALSE; + + protected function hasPdoDriver() { + return in_array($this->pdoDriver, PDO::getAvailableDrivers()); + } + + public function installable() { + return $this->hasPdoDriver(); + } + + abstract public function name(); + + public function test() { + $return = $this->testConnect(); + if ($return === FALSE) { + return FALSE; + } + foreach ($this->tests as $test) { + $return = $this->runTestQuery($test['query'], $test['success'], $test['message'], !empty($tests['fatal'])); + if ($return === FALSE) { + return FALSE; + } + } + return $this->success; + } + + /** + * Check if we can connect to the database. + * + * @return + * FALSE on failure. + */ + protected function testConnect() { + try { + db_set_active(); + $this->success[] = 'CONNECT'; + } + catch (Exception $e) { + drupal_set_message(st('Failed to connect to your %name database server. %name reports the following message: %error.
  • Are you sure you have the correct username and password?
  • Are you sure that you have typed the correct database hostname?
  • Are you sure that the database server is running?
For more help, see the Installation and upgrading handbook. If you are unsure what these terms mean you should probably contact your hosting provider.', array('%error' => $e->getMessage(), 'name' => $this->name())), 'error'); + return FALSE; + } + } + + protected function runTestQuery($query, $success, $message, $fatal = FALSE) { + try { + db_query($query); + $this->success[] = $success; + } + catch (Exception $e) { + drupal_set_message(st($message, array('%query' => $query, '%error' => $e->getMessage(), '%name' => $this->name())), 'error'); + $this->error = TRUE; + if ($fatal) { + return FALSE; + } + } + } +} + /** * Read settings.php into a buffer line by line, changing values specified in * $settings array, then over-writing the old settings.php file. @@ -265,7 +358,7 @@ function drupal_rewrite_settings($settin // Write new value to settings.php in the following format: // $'setting' = 'value'; // 'comment' $setting = $settings[$variable[1]]; - $buffer .= '$' . $variable[1] . " = '" . $setting['value'] . "';" . (!empty($setting['comment']) ? ' // ' . $setting['comment'] . "\n" : "\n"); + $buffer .= '$' . $variable[1] . " = " . var_export($setting['value'], TRUE) . ";" . (!empty($setting['comment']) ? ' // ' . $setting['comment'] . "\n" : "\n"); unset($settings[$variable[1]]); } else { @@ -281,7 +374,7 @@ function drupal_rewrite_settings($settin // Add required settings that were missing from settings.php. foreach ($settings as $setting => $data) { if ($data['required']) { - $buffer .= "\$$setting = '" . $data['value'] . "';\n"; + $buffer .= "\$$setting = " . var_export($data['value'], TRUE) . ";\n"; } } === removed file 'includes/install.mysqli.inc' --- includes/install.mysqli.inc 2008-01-23 09:59:28 +0000 +++ includes/install.mysqli.inc 1970-01-01 00:00:00 +0000 @@ -1,112 +0,0 @@ -= 2000 || mysqli_connect_errno() == 1045) { - drupal_set_message(st('Failed to connect to your MySQL database server. MySQL reports the following message: %error.
  • Are you sure you have the correct username and password?
  • Are you sure that you have typed the correct database hostname?
  • Are you sure that the database server is running?
For more help, see the Installation and upgrading handbook. If you are unsure what these terms mean you should probably contact your hosting provider.', array('%error' => mysqli_connect_error())), 'error'); - return FALSE; - } - - // Test selecting the database. - if (mysqli_connect_errno() > 0) { - drupal_set_message(st('Failed to select your database on your MySQL database server, which means the connection username and password are valid, but there is a problem accessing your data. MySQL reports the following message: %error.
  • Are you sure you have the correct database name?
  • Are you sure the database exists?
  • Are you sure the username has permission to access the database?
For more help, see the Installation and upgrading handbook. If you are unsure what these terms mean you should probably contact your hosting provider.', array('%error' => mysqli_connect_error())), 'error'); - return FALSE; - } - - $success = array('CONNECT'); - - // Test CREATE. - $query = 'CREATE TABLE drupal_install_test (id int NULL)'; - $result = mysqli_query($connection, $query); - if ($error = mysqli_error($connection)) { - drupal_set_message(st('Failed to create a test table on your MySQL database server with the command %query. MySQL reports the following message: %error.
  • Are you sure the configured username has the necessary MySQL permissions to create tables in the database?
For more help, see the Installation and upgrading handbook. If you are unsure what these terms mean you should probably contact your hosting provider.', array('%query' => $query, '%error' => $error)), 'error'); - return FALSE; - } - $err = FALSE; - $success[] = 'SELECT'; - $success[] = 'CREATE'; - - // Test INSERT. - $query = 'INSERT INTO drupal_install_test (id) VALUES (1)'; - $result = mysqli_query($connection, $query); - if ($error = mysqli_error($connection)) { - drupal_set_message(st('Failed to insert a value into a test table on your MySQL database server. We tried inserting a value with the command %query and MySQL reported the following error: %error.', array('%query' => $query, '%error' => $error)), 'error'); - $err = TRUE; - } - else { - $success[] = 'INSERT'; - } - - // Test UPDATE. - $query = 'UPDATE drupal_install_test SET id = 2'; - $result = mysqli_query($connection, $query); - if ($error = mysqli_error($connection)) { - drupal_set_message(st('Failed to update a value in a test table on your MySQL database server. We tried updating a value with the command %query and MySQL reported the following error: %error.', array('%query' => $query, '%error' => $error)), 'error'); - $err = TRUE; - } - else { - $success[] = 'UPDATE'; - } - - // Test DELETE. - $query = 'DELETE FROM drupal_install_test'; - $result = mysqli_query($connection, $query); - if ($error = mysqli_error($connection)) { - drupal_set_message(st('Failed to delete a value from a test table on your MySQL database server. We tried deleting a value with the command %query and MySQL reported the following error: %error.', array('%query' => $query, '%error' => $error)), 'error'); - $err = TRUE; - } - else { - $success[] = 'DELETE'; - } - - // Test DROP. - $query = 'DROP TABLE drupal_install_test'; - $result = mysqli_query($connection, $query); - if ($error = mysqli_error($connection)) { - drupal_set_message(st('Failed to drop a test table from your MySQL database server. We tried dropping a table with the command %query and MySQL reported the following error %error.', array('%query' => $query, '%error' => $error)), 'error'); - $err = TRUE; - } - else { - $success[] = 'DROP'; - } - - if ($err) { - return FALSE; - } - - mysqli_close($connection); - return TRUE; -} === modified file 'includes/locale.inc' --- includes/locale.inc 2008-07-16 21:59:24 +0000 +++ includes/locale.inc 2008-07-22 07:28:50 +0000 @@ -1923,34 +1923,34 @@ function _locale_translate_seek() { // Compute LIKE section switch ($query['translation']) { case 'translated': - $where = "WHERE (t.translation LIKE '%%%s%%')"; + $where = "WHERE (t.translation LIKE ?)"; $orderby = "ORDER BY t.translation"; - $arguments[] = $query['string']; + $arguments[] = '%'. $query['string'] .'%'; break; case 'untranslated': - $where = "WHERE (s.source LIKE '%%%s%%' AND t.translation IS NULL)"; + $where = "WHERE (s.source LIKE ? AND t.translation IS NULL)"; $orderby = "ORDER BY s.source"; - $arguments[] = $query['string']; + $arguments[] = '%'. $query['string'] .'%'; break; case 'all' : default: - $where = "WHERE (s.source LIKE '%%%s%%' OR t.translation LIKE '%%%s%%')"; + $where = "WHERE (s.source LIKE ? OR t.translation LIKE ?)"; $orderby = ''; - $arguments[] = $query['string']; - $arguments[] = $query['string']; + $arguments[] = '%'. $query['string'] .'%'; + $arguments[] = '%'. $query['string'] .'%'; break; } $grouplimit = ''; if (!empty($query['group']) && $query['group'] != 'all') { - $grouplimit = " AND s.textgroup = '%s'"; + $grouplimit = " AND s.textgroup = ?"; $arguments[] = $query['group']; } switch ($query['language']) { // Force search in source strings case "en": - $sql = $join . " WHERE s.source LIKE '%%%s%%' $grouplimit ORDER BY s.source"; - $arguments = array($query['string']); // $where is not used, discard its arguments + $sql = $join . " WHERE s.source LIKE ? $grouplimit ORDER BY s.source"; + $arguments = array('%' . $query['string'] . '%'); // $where is not used, discard its arguments if (!empty($grouplimit)) { $arguments[] = $query['group']; } @@ -1961,7 +1961,7 @@ function _locale_translate_seek() { break; // Some different language default: - $sql = "$join AND t.language = '%s' $where $grouplimit $orderby"; + $sql = "$join AND t.language = ? $where $grouplimit $orderby"; array_unshift($arguments, $query['language']); // Don't show translation flags for other languages, we can't see them with this search. $limit_language = $query['language']; @@ -2071,7 +2071,12 @@ function _locale_rebuild_js($langcode = // Construct the array for JavaScript translations. // We sort on plural so that we have all plural forms before singular forms. - $result = db_query("SELECT s.lid, s.source, t.plid, t.plural, t.translation FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = '%s' WHERE s.location LIKE '%%.js%%' AND s.textgroup = 'default' ORDER BY t.plural DESC", $language->language); + $result = db_query("SELECT s.lid, s.source, t.plid, t.plural, t.translation + FROM {locales_source} s + LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language + WHERE s.location LIKE '%.js%' + AND s.textgroup = 'default' + ORDER BY t.plural DESC", array(':language' => $language->language)); $translations = $plurals = array(); while ($data = db_fetch_object($result)) { === modified file 'includes/menu.inc' --- includes/menu.inc 2008-07-10 10:58:01 +0000 +++ includes/menu.inc 2008-07-22 07:28:50 +0000 @@ -1810,7 +1810,7 @@ function _menu_navigation_links_rebuild( db_query("UPDATE {menu_links} SET router_path = '%s', updated = %d WHERE mlid = %d", $router_path, $updated, $item['mlid']); } } - // Find any item whose router path does not exist any more. + // Find any items where their router path does not exist any more. $result = db_query("SELECT * FROM {menu_links} WHERE router_path NOT IN ($placeholders) AND external = 0 AND updated = 0 AND customized = 0 ORDER BY depth DESC", $paths); // Remove all such items. Starting from those with the greatest depth will // minimize the amount of re-parenting done by menu_link_delete(). @@ -1888,7 +1888,7 @@ function menu_link_save(&$item) { // This is the easiest way to handle the unique internal path '', // since a path marked as external does not need to match a router path. - $item['_external'] = menu_path_is_external($item['link_path']) || $item['link_path'] == ''; + $item['_external'] = (menu_path_is_external($item['link_path']) || $item['link_path'] == '') ? 1 : 0; // Load defaults. $item += array( 'menu_name' => 'navigation', @@ -2212,10 +2212,22 @@ function _menu_update_parental_status($i // If plid == 0, there is nothing to update. if ($item['plid']) { // We may want to exclude the passed link as a possible child. - $where = $exclude ? " AND mlid != %d" : ''; + //$where = $exclude ? " AND mlid != %d" : ''; // Check if at least one visible child exists in the table. - $parent_has_children = (bool)db_result(db_query_range("SELECT mlid FROM {menu_links} WHERE menu_name = '%s' AND plid = %d AND hidden = 0" . $where, $item['menu_name'], $item['plid'], $item['mlid'], 0, 1)); + $query = db_select('menu_links', 'm'); + $query->addField('mlid', 'm'); + $query->condition('menu_name', $item['menu_name']); + $query->condition('plid', $item['plid']); + $query->range(0, 1); + + if ($exclude) { + $query->condition('mlid', $item['mlid'], '!='); + } + + $parent_has_children = ((bool) $query->execute()->fetchField()) ? 1 : 0; + //$parent_has_children = (bool)db_result(db_query_range("SELECT mlid FROM {menu_links} WHERE menu_name = '%s' AND plid = %d AND hidden = 0" . $where, $item['menu_name'], $item['plid'], $item['mlid'], 0, 1)); db_query("UPDATE {menu_links} SET has_children = %d WHERE mlid = %d", $parent_has_children, $item['plid']); + } } === modified file 'includes/module.inc' --- includes/module.inc 2008-05-13 17:38:42 +0000 +++ includes/module.inc 2008-07-22 07:28:50 +0000 @@ -45,7 +45,7 @@ function module_iterate($function, $argu * modules. */ function module_list($refresh = FALSE, $bootstrap = TRUE, $sort = FALSE, $fixed_list = NULL) { - static $list, $sorted_list; + static $list = array(), $sorted_list; if ($refresh || $fixed_list) { unset($sorted_list); === added file 'includes/schema.inc' --- includes/schema.inc 1970-01-01 00:00:00 +0000 +++ includes/schema.inc 2008-07-22 07:28:50 +0000 @@ -0,0 +1,413 @@ + specification) + * that describes the table's database columns. The specification + * is also an array. The following specification parameters are defined: + * + * - 'description': A string describing this field and its purpose. + * References to other tables should be enclosed in + * curly-brackets. For example, the node table vid field + * description might contain "Always holds the largest (most + * recent) {node_revisions}.vid value for this nid." + * - 'type': The generic datatype: 'varchar', 'int', 'serial' + * 'float', 'numeric', 'text', 'blob' or 'datetime'. Most types + * just map to the according database engine specific + * datatypes. Use 'serial' for auto incrementing fields. This + * will expand to 'int auto_increment' on mysql. + * - 'size': The data size: 'tiny', 'small', 'medium', 'normal', + * 'big'. This is a hint about the largest value the field will + * store and determines which of the database engine specific + * datatypes will be used (e.g. on MySQL, TINYINT vs. INT vs. BIGINT). + * 'normal', the default, selects the base type (e.g. on MySQL, + * INT, VARCHAR, BLOB, etc.). + * + * Not all sizes are available for all data types. See + * db_type_map() for possible combinations. + * - 'not null': If true, no NULL values will be allowed in this + * database column. Defaults to false. + * - 'default': The field's default value. The PHP type of the + * value matters: '', '0', and 0 are all different. If you + * specify '0' as the default value for a type 'int' field it + * will not work because '0' is a string containing the + * character "zero", not an integer. + * - 'length': The maximal length of a type 'varchar' or 'text' + * field. Ignored for other field types. + * - 'unsigned': A boolean indicating whether a type 'int', 'float' + * and 'numeric' only is signed or unsigned. Defaults to + * FALSE. Ignored for other field types. + * - 'precision', 'scale': For type 'numeric' fields, indicates + * the precision (total number of significant digits) and scale + * (decimal digits right of the decimal point). Both values are + * mandatory. Ignored for other field types. + * + * All parameters apart from 'type' are optional except that type + * 'numeric' columns must specify 'precision' and 'scale'. + * + * - 'primary key': An array of one or more key column specifiers (see below) + * that form the primary key. + * - 'unique key': An associative array of unique keys ('keyname' => + * specification). Each specification is an array of one or more + * key column specifiers (see below) that form a unique key on the table. + * - 'indexes': An associative array of indexes ('indexame' => + * specification). Each specification is an array of one or more + * key column specifiers (see below) that form an index on the + * table. + * + * A key column specifier is either a string naming a column or an + * array of two elements, column name and length, specifying a prefix + * of the named column. + * + * As an example, here is a SUBSET of the schema definition for + * Drupal's 'node' table. It show four fields (nid, vid, type, and + * title), the primary key on field 'nid', a unique key named 'vid' on + * field 'vid', and two indexes, one named 'nid' on field 'nid' and + * one named 'node_title_type' on the field 'title' and the first four + * bytes of the field 'type': + * + * @code + * $schema['node'] = array( + * 'fields' => array( + * 'nid' => array('type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE), + * 'vid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), + * 'type' => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => ''), + * 'title' => array('type' => 'varchar', 'length' => 128, 'not null' => TRUE, 'default' => ''), + * ), + * 'primary key' => array('nid'), + * 'unique keys' => array( + * 'vid' => array('vid') + * ), + * 'indexes' => array( + * 'nid' => array('nid'), + * 'node_title_type' => array('title', array('type', 4)), + * ), + * ); + * @endcode + * + * @see drupal_install_schema() + */ + +abstract class DatabaseSchema { + + protected $connection; + + public function __construct($connection) { + $this->connection = $connection; + } + + /** + * Check if a table exists. + */ + abstract public function tableExists($table); + + /** + * Check if a column exists in the given table. + */ + abstract public function columnExists($table, $column); + + /** + * This maps a generic data type in combination with its data size + * to the engine-specific data type. + */ + abstract public function 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. + */ + abstract public function renameTable(&$ret, $table, $new_name); + + /** + * Drop a table. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be dropped. + */ + abstract public function dropTable(&$ret, $table); + + /** + * Add a new field to a table. + * + * @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. + */ + abstract public function addField(&$ret, $table, $field, $spec, $keys_new = array()); + + /** + * Drop a field. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + * @param $field + * The field to be dropped. + */ + abstract public function dropField(&$ret, $table, $field); + + /** + * Set the default value for a field. + * + * @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'. + */ + abstract public function fieldSetDefault(&$ret, $table, $field, $default); + + /** + * Set a field to have no default value. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + * @param $field + * The field to be altered. + */ + abstract public function fieldSetNoDefault(&$ret, $table, $field); + + /** + * 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. + */ + abstract public function addPrimaryKey(&$ret, $table, $fields); + + /** + * Drop the primary key. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + */ + abstract public function dropPrimaryKey(&$ret, $table); + + /** + * 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. + */ + abstract public function addUniqueKey(&$ret, $table, $name, $fields); + + /** + * 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. + */ + abstract public function dropUniqueKey(&$ret, $table, $name); + + /** + * 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. + */ + abstract public function 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. + */ + abstract public function dropIndex(&$ret, $table, $name); + + + /** + * Change a field definition. + * + * IMPORTANT NOTE: To maintain database portability, you have to explicitly + * recreate all indices and primary keys that are using the changed field. + * + * That means that you have to drop all affected keys and indexes with + * db_drop_{primary_key,unique_key,index}() before calling db_change_field(). + * To recreate the keys and indices, pass the key definitions as the + * optional $keys_new argument directly to db_change_field(). + * + * For example, suppose you have: + * @code + * $schema['foo'] = array( + * 'fields' => array( + * 'bar' => array('type' => 'int', 'not null' => TRUE) + * ), + * 'primary key' => array('bar') + * ); + * @endcode + * and you want to change foo.bar to be type serial, leaving it as the + * primary key. The correct sequence is: + * @code + * db_drop_primary_key($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. + */ + abstract public function changeField(&$ret, $table, $field, $field_new, $spec, $keys_new = array()); + + /** + * 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. + */ + public function createTable(&$ret, $name, $table) { + $statements = $this->createTableSql($name, $table); + foreach ($statements as $statement) { + $ret[] = update_sql($statement); + } + } + + /** + * Return an array of field names from an array of key/index column specifiers. + * + * This is usually an identity function but if a key/index uses a column prefix + * specification, this function extracts just the name. + * + * @param $fields + * An array of key/index column specifiers. + * @return + * An array of field names. + */ + public function fieldNames($fields) { + $ret = array(); + foreach ($fields as $field) { + if (is_array($field)) { + $ret[] = $field[0]; + } + else { + $ret[] = $field; + } + } + return $ret; + } + +} + +/** + * @} End of "defgroup schemaapi". + */ + === added file 'includes/schema.mysql.inc' --- includes/schema.mysql.inc 1970-01-01 00:00:00 +0000 +++ includes/schema.mysql.inc 2008-07-22 07:28:50 +0000 @@ -0,0 +1,313 @@ +connection->runQuery("SHOW TABLES LIKE '{" . $table . "}'", array(), array())->fetchField(); + } + + public function columnExists($table, $column) { + return (bool) $this->connection->runQuery("SHOW COLUMNS FROM {" . $this->escapeTable($table) . "} LIKE '" . $this->escapeTable($column) . "'", array(), array())->fetchField(); + } + + + /** + * Generate SQL to create a new table from a Drupal schema definition. + * + * @param $name + * The name of the table to create. + * @param $table + * A Schema API table definition array. + * @return + * An array of SQL statements to create the table. + */ + protected function createTableSql($name, $table) { + if (empty($table['mysql_suffix'])) { + $table['mysql_suffix'] = "/*!40100 DEFAULT CHARACTER SET UTF8 */"; + } + + $sql = "CREATE TABLE {" . $name . "} (\n"; + + // Add the SQL statement for each field. + foreach ($table['fields'] as $field_name => $field) { + $sql .= $this->createFieldSql($field_name, $this->processField($field)) . ", \n"; + } + + // Process keys & indexes. + $keys = $this->createKeysSql($table); + if (count($keys)) { + $sql .= implode(", \n", $keys) . ", \n"; + } + + // Remove the last comma and space. + $sql = substr($sql, 0, -3) . "\n) "; + + $sql .= $table['mysql_suffix']; + + return array($sql); + } + + /** + * Create an SQL string for a field to be used in table creation or alteration. + * + * Before passing a field out of a schema definition into this function it has + * to be processed by _db_process_field(). + * + * @param $name + * Name of the field. + * @param $spec + * The field specification, as per the schema data structure format. + */ + protected function createFieldSql($name, $spec) { + $sql = "`" . $name . "` " . $spec['mysql_type']; + + if (isset($spec['length'])) { + $sql .= '(' . $spec['length'] . ')'; + } + elseif (isset($spec['precision']) && isset($spec['scale'])) { + $sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')'; + } + + if (!empty($spec['unsigned'])) { + $sql .= ' unsigned'; + } + + if (!empty($spec['not null'])) { + $sql .= ' NOT NULL'; + } + + if (!empty($spec['auto_increment'])) { + $sql .= ' auto_increment'; + } + + if (isset($spec['default'])) { + if (is_string($spec['default'])) { + $spec['default'] = "'" . $spec['default'] . "'"; + } + $sql .= ' DEFAULT ' . $spec['default']; + } + + if (empty($spec['not null']) && !isset($spec['default'])) { + $sql .= ' DEFAULT NULL'; + } + + return $sql; + } + + /** + * Set database-engine specific properties for a field. + * + * @param $field + * A field description array, as specified in the schema documentation. + */ + protected function processField($field) { + + if (!isset($field['size'])) { + $field['size'] = 'normal'; + } + + // Set the correct database-engine specific datatype. + if (!isset($field['mysql_type'])) { + $map = db_type_map(); + $field['mysql_type'] = $map[$field['type'] . ':' . $field['size']]; + } + + if ($field['type'] == 'serial') { + $field['auto_increment'] = TRUE; + } + + return $field; + } + + public function getFieldTypeMap() { + // Put :normal last so it gets preserved by array_flip. This makes + // it much easier for modules (such as schema.module) to map + // database types back into schema types. + static $map = array( + 'varchar:normal' => 'VARCHAR', + 'char:normal' => 'CHAR', + + 'text:tiny' => 'TINYTEXT', + 'text:small' => 'TINYTEXT', + 'text:medium' => 'MEDIUMTEXT', + 'text:big' => 'LONGTEXT', + 'text:normal' => 'TEXT', + + 'serial:tiny' => 'TINYINT', + 'serial:small' => 'SMALLINT', + 'serial:medium' => 'MEDIUMINT', + 'serial:big' => 'BIGINT', + 'serial:normal' => 'INT', + + 'int:tiny' => 'TINYINT', + 'int:small' => 'SMALLINT', + 'int:medium' => 'MEDIUMINT', + 'int:big' => 'BIGINT', + 'int:normal' => 'INT', + + 'float:tiny' => 'FLOAT', + 'float:small' => 'FLOAT', + 'float:medium' => 'FLOAT', + 'float:big' => 'DOUBLE', + 'float:normal' => 'FLOAT', + + 'numeric:normal' => 'DECIMAL', + + 'blob:big' => 'LONGBLOB', + 'blob:normal' => 'BLOB', + + 'datetime:normal' => 'DATETIME', + ); + return $map; + } + + + + + protected function createKeysSql($spec) { + $keys = array(); + + if (!empty($spec['primary key'])) { + $keys[] = 'PRIMARY KEY (' . $this->createKeysSqlHelper($spec['primary key']) . ')'; + } + if (!empty($spec['unique keys'])) { + foreach ($spec['unique keys'] as $key => $fields) { + $keys[] = 'UNIQUE KEY ' . $key .' ('. $this->createKeysSqlHelper($fields) . ')'; + } + } + if (!empty($spec['indexes'])) { + foreach ($spec['indexes'] as $index => $fields) { + $keys[] = 'INDEX ' . $index . ' (' . $this->createKeysSqlHelper($fields) . ')'; + } + } + + return $keys; + } + + protected function createKeySql($fields) { + $ret = array(); + foreach ($fields as $field) { + if (is_array($field)) { + $ret[] = $field[0] . '(' . $field[1] . ')'; + } + else { + $ret[] = $field; + } + } + return implode(', ', $ret); + } + + protected function createKeysSqlHelper($fields) { + $ret = array(); + foreach ($fields as $field) { + if (is_array($field)) { + $ret[] = $field[0] . '(' . $field[1] . ')'; + } + else { + $ret[] = $field; + } + } + return implode(', ', $ret); + } + + public function renameTable(&$ret, $table, $new_name) { + $ret[] = update_sql('ALTER TABLE {' . $table . '} RENAME TO {' . $new_name . '}'); + } + + public function dropTable(&$ret, $table) { + $ret[] = update_sql('DROP TABLE {' . $table . '}'); + } + + public function addField(&$ret, $table, $field, $spec, $keys_new = array()) { + $fixnull = FALSE; + if (!empty($spec['not null']) && !isset($spec['default'])) { + $fixnull = TRUE; + $spec['not null'] = FALSE; + } + $query = 'ALTER TABLE {' . $table . '} ADD '; + $query .= $this->createFieldSql($field, $this->processField($spec)); + if (count($keys_new)) { + $query .= ', ADD ' . implode(', ADD ', $this->createKeysSql($keys_new)); + } + $ret[] = update_sql($query); + if (isset($spec['initial'])) { + // All this because update_sql does not support %-placeholders. + $sql = 'UPDATE {' . $table . '} SET ' . $field . ' = ' . db_type_placeholder($spec['type']); + $result = db_query($sql, $spec['initial']); + $ret[] = array('success' => $result !== FALSE, 'query' => check_plain($sql . ' (' . $spec['initial'] . ')')); + } + if ($fixnull) { + $spec['not null'] = TRUE; + $this->changeField($ret, $table, $field, $field, $spec); + } + } + + public function dropField(&$ret, $table, $field) { + $ret[] = update_sql('ALTER TABLE {' . $table . '} DROP ' . $field); + } + + public function fieldSetDefault(&$ret, $table, $field, $default) { + if ($default == NULL) { + $default = 'NULL'; + } + else { + $default = is_string($default) ? "'$default'" : $default; + } + + $ret[] = update_sql('ALTER TABLE {' . $table . '} ALTER COLUMN ' . $field . ' SET DEFAULT ' . $default); + } + + public function fieldSetNoDefault(&$ret, $table, $field) { + $ret[] = update_sql('ALTER TABLE {' . $table . '} ALTER COLUMN ' . $field . ' DROP DEFAULT'); + } + + public function addPrimaryKey(&$ret, $table, $fields) { + $ret[] = update_sql('ALTER TABLE {' . $table . '} ADD PRIMARY KEY (' . $this->createKeySql($fields) . ')'); + } + + public function dropPrimaryKey(&$ret, $table) { + $ret[] = update_sql('ALTER TABLE {' . $table . '} DROP PRIMARY KEY'); + } + + public function addUniqueKey(&$ret, $table, $name, $fields) { + $ret[] = update_sql('ALTER TABLE {' . $table . '} ADD UNIQUE KEY ' . $name . ' (' . $this->createKeySql($fields) . ')'); + } + + public function dropUniqueKey(&$ret, $table, $name) { + $ret[] = update_sql('ALTER TABLE {' . $table . '} DROP KEY ' . $name); + } + + public function addIndex(&$ret, $table, $name, $fields) { + $query = 'ALTER TABLE {' . $table . '} ADD INDEX ' . $name . ' (' . $this->createKeySql($fields) . ')'; + $ret[] = update_sql($query); + } + + public function dropIndex(&$ret, $table, $name) { + $ret[] = update_sql('ALTER TABLE {' . $table . '} DROP INDEX ' . $name); + } + + public function changeField(&$ret, $table, $field, $field_new, $spec, $keys_new = array()) { + $sql = 'ALTER TABLE {' . $table . '} CHANGE ' . $field . ' ' . $this->createFieldSql($field_new, $this->processField($spec)); + if (count($keys_new)) { + $sql .= ', ADD ' . implode(', ADD ', $this->createKeysSql($keys_new)); + } + $ret[] = update_sql($sql); + } + +} + +/** + * @} End of "ingroup schemaapi". + */ === added file 'includes/schema.pgsql.inc' --- includes/schema.pgsql.inc 1970-01-01 00:00:00 +0000 +++ includes/schema.pgsql.inc 2008-07-22 07:28:50 +0000 @@ -0,0 +1,509 @@ + $field) { + $sql_fields[] = $this->createFieldSql($field_name, $this->processField($field)); + } + + $sql_keys = array(); + if (isset($table['primary key']) && is_array($table['primary key'])) { + $sql_keys[] = 'PRIMARY KEY (' . implode(', ', $table['primary key']) . ')'; + } + if (isset($table['unique keys']) && is_array($table['unique keys'])) { + foreach ($table['unique keys'] as $key_name => $key) { + $sql_keys[] = 'CONSTRAINT {' . $name . '}_' . $key_name . '_key UNIQUE (' . implode(', ', $key) . ')'; + } + } + + $sql = "CREATE TABLE {" . $name . "} (\n\t"; + $sql .= implode(",\n\t", $sql_fields); + if (count($sql_keys) > 0) { + $sql .= ",\n\t"; + } + $sql .= implode(",\n\t", $sql_keys); + $sql .= "\n)"; + $statements[] = $sql; + + if (isset($table['indexes']) && is_array($table['indexes'])) { + foreach ($table['indexes'] as $key_name => $key) { + $statements[] = $this->_createIndexSql($name, $key_name, $key); + } + } + + return $statements; + } + + /** + * Create an SQL string for a field to be used in table creation or + * alteration. + * + * Before passing a field out of a schema definition into this + * function it has to be processed by _db_process_field(). + * + * @param $name + * Name of the field. + * @param $spec + * The field specification, as per the schema data structure format. + */ + protected function createFieldSql($name, $spec) { + $sql = $name . ' ' . $spec['pgsql_type']; + + if ($spec['type'] == 'serial') { + unset($spec['not null']); + } + if (!empty($spec['unsigned'])) { + if ($spec['type'] == 'serial') { + $sql .= " CHECK ($name >= 0)"; + } + else { + $sql .= '_unsigned'; + } + } + + if (!empty($spec['length'])) { + $sql .= '(' . $spec['length'] . ')'; + } + elseif (isset($spec['precision']) && isset($spec['scale'])) { + $sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')'; + } + + if (isset($spec['not null']) && $spec['not null']) { + $sql .= ' NOT NULL'; + } + if (isset($spec['default'])) { + $default = is_string($spec['default']) ? "'" . $spec['default'] . "'" : $spec['default']; + $sql .= " default $default"; + } + + return $sql; + } + + /** + * Set database-engine specific properties for a field. + * + * @param $field + * A field description array, as specified in the schema documentation. + */ + protected function processField($field) { + if (!isset($field['size'])) { + $field['size'] = 'normal'; + } + // Set the correct database-engine specific datatype. + if (!isset($field['pgsql_type'])) { + $map = $this->getFieldTypeMap(); + $field['pgsql_type'] = $map[$field['type'] . ':' . $field['size']]; + } + if ($field['type'] == 'serial') { + unset($field['not null']); + } + return $field; + } + + + /** + * This maps a generic data type in combination with its data size + * to the engine-specific data type. + */ + function getFieldTypeMap() { + // Put :normal last so it gets preserved by array_flip. This makes + // it much easier for modules (such as schema.module) to map + // database types back into schema types. + $map = array( + 'varchar:normal' => 'varchar', + 'char:normal' => 'character', + + 'text:tiny' => 'text', + 'text:small' => 'text', + 'text:medium' => 'text', + 'text:big' => 'text', + 'text:normal' => 'text', + + 'int:tiny' => 'smallint', + 'int:small' => 'smallint', + 'int:medium' => 'int', + 'int:big' => 'bigint', + 'int:normal' => 'int', + + 'float:tiny' => 'real', + 'float:small' => 'real', + 'float:medium' => 'real', + 'float:big' => 'double precision', + 'float:normal' => 'real', + + 'numeric:normal' => 'numeric', + + 'blob:big' => 'bytea', + 'blob:normal' => 'bytea', + + 'datetime:normal' => 'timestamp', + + 'serial:tiny' => 'serial', + 'serial:small' => 'serial', + 'serial:medium' => 'serial', + 'serial:big' => 'bigserial', + 'serial:normal' => 'serial', + ); + return $map; + } + + protected function _createKeySql($fields) { + $ret = array(); + foreach ($fields as $field) { + if (is_array($field)) { + $ret[] = 'substr(' . $field[0] . ', 1, ' . $field[1] . ')'; + } + else { + $ret[] = $field; + } + } + return implode(', ', $ret); + } + + /** + * 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 renameTable(&$ret, $table, $new_name) { + $ret[] = update_sql('ALTER TABLE {' . $table . '} RENAME TO {' . $new_name . '}'); + } + + /** + * Drop a table. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be dropped. + */ + public function dropTable(&$ret, $table) { + $ret[] = update_sql('DROP TABLE {' . $table . '}'); + } + + /** + * Add a new field to a table. + * + * @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. + */ + public function addField(&$ret, $table, $field, $spec, $new_keys = array()) { + $fixnull = FALSE; + if (!empty($spec['not null']) && !isset($spec['default'])) { + $fixnull = TRUE; + $spec['not null'] = FALSE; + } + $query = 'ALTER TABLE {' . $table . '} ADD COLUMN '; + $query .= $this->_createFieldSql($field, $this->_processField($spec)); + $ret[] = update_sql($query); + if (isset($spec['initial'])) { + // All this because update_sql does not support %-placeholders. + $sql = 'UPDATE {' . $table . '} SET ' . $field . ' = ' . db_type_placeholder($spec['type']); + $result = db_query($sql, $spec['initial']); + $ret[] = array('success' => $result !== FALSE, 'query' => check_plain($sql . ' (' . $spec['initial'] . ')')); + } + if ($fixnull) { + $ret[] = update_sql("ALTER TABLE {" . $table . "} ALTER $field SET NOT NULL"); + } + if (isset($new_keys)) { + $this->_createKeys($ret, $table, $new_keys); + } + } + + /** + * Drop a field. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + * @param $field + * The field to be dropped. + */ + public function dropField(&$ret, $table, $field) { + $ret[] = update_sql('ALTER TABLE {' . $table . '} DROP COLUMN ' . $field); + } + + /** + * Set the default value for a field. + * + * @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'. + */ + public function fieldSetDefault(&$ret, $table, $field, $default) { + if ($default == NULL) { + $default = 'NULL'; + } + else { + $default = is_string($default) ? "'$default'" : $default; + } + + $ret[] = update_sql('ALTER TABLE {' . $table . '} ALTER COLUMN ' . $field . ' SET DEFAULT ' . $default); + } + + /** + * Set a field to have no default value. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + * @param $field + * The field to be altered. + */ + public function fieldSetNoDefault(&$ret, $table, $field) { + $ret[] = update_sql('ALTER TABLE {' . $table . '} ALTER COLUMN ' . $field . ' DROP DEFAULT'); + } + + /** + * 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. + */ + public function addPrimaryKey(&$ret, $table, $fields) { + $ret[] = update_sql('ALTER TABLE {' . $table . '} ADD PRIMARY KEY (' . implode(',', $fields) . ')'); + } + + /** + * Drop the primary key. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + */ + public function dropPrimaryKey(&$ret, $table) { + $ret[] = update_sql('ALTER TABLE {' . $table . '} DROP CONSTRAINT {' . $table . '}_pkey'); + } + + /** + * 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 addUniqueKey(&$ret, $table, $name, $fields) { + $name = '{' . $table . '}_' . $name . '_key'; + $ret[] = update_sql('ALTER TABLE {' . $table . '} ADD CONSTRAINT ' . $name . ' UNIQUE (' . implode(',', $fields) . ')'); + } + + /** + * 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. + */ + public function dropUniqueKey(&$ret, $table, $name) { + $name = '{' . $table . '}_' . $name . '_key'; + $ret[] = update_sql('ALTER TABLE {' . $table . '} DROP CONSTRAINT ' . $name); + } + + /** + * 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. + */ + public function addIndex(&$ret, $table, $name, $fields) { + $ret[] = update_sql($this->_createIndexSql($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. + */ + public function dropIndex(&$ret, $table, $name) { + $name = '{' . $table . '}_' . $name . '_idx'; + $ret[] = update_sql('DROP INDEX ' . $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 $new_keys 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 + * $new_keys 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 $new_keys 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 $new_keys + * 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. + */ + public function changeField(&$ret, $table, $field, $field_new, $spec, $new_keys = array()) { + $ret[] = update_sql("ALTER TABLE {" . $table . "} RENAME $field TO " . $field . "_old"); + $not_null = isset($spec['not null']) ? $spec['not null'] : FALSE; + unset($spec['not null']); + + $this->addField($ret, $table, "$field_new", $spec); + + $ret[] = update_sql("UPDATE {" . $table . "} SET $field_new = " . $field . "_old"); + + if ($not_null) { + $ret[] = update_sql("ALTER TABLE {" . $table . "} ALTER $field_new SET NOT NULL"); + } + + $this->dropField($ret, $table, $field . '_old'); + + if (isset($new_keys)) { + $this->_createKeys($ret, $table, $new_keys); + } + } + + protected function _createIndexSql($table, $name, $fields) { + $query = 'CREATE INDEX {' . $table . '}_' . $name . '_idx ON {' . $table . '} ('; + $query .= $this->_createKeySql($fields) . ')'; + return $query; + } + + protected function _createKeys(&$ret, $table, $new_keys) { + if (isset($new_keys['primary key'])) { + $this->addPrimaryKey($ret, $table, $new_keys['primary key']); + } + if (isset($new_keys['unique keys'])) { + foreach ($new_keys['unique keys'] as $name => $fields) { + $this->addUniqueKey($ret, $table, $name, $fields); + } + } + if (isset($new_keys['indexes'])) { + foreach ($new_keys['indexes'] as $name => $fields) { + $this->addIndex($ret, $table, $name, $fields); + } + } + } +} === modified file 'includes/session.inc' --- includes/session.inc 2008-07-11 10:14:27 +0000 +++ includes/session.inc 2008-07-22 07:28:50 +0000 @@ -65,18 +65,27 @@ function sess_write($key, $value) { return TRUE; } - db_query("UPDATE {sessions} SET uid = %d, cache = %d, hostname = '%s', session = '%s', timestamp = %d WHERE sid = '%s'", $user->uid, isset($user->cache) ? $user->cache : '', ip_address(), $value, time(), $key); - if (db_affected_rows()) { - // Last access time is updated no more frequently than once every 180 seconds. - // This reduces contention in the users table. - if ($user->uid && time() - $user->access > variable_get('session_write_interval', 180)) { - db_query("UPDATE {users} SET access = %d WHERE uid = %d", time(), $user->uid); + $result = db_result(db_query("SELECT COUNT(*) FROM {sessions} WHERE sid = '%s'", $key)); + + if (!$result) { + // Only save session data when when the browser sends a cookie. This keeps + // crawlers out of session table. This reduces memory and server load, + // and gives more useful statistics. We can't eliminate anonymous session + // table rows without breaking "Who's Online" block. + if ($user->uid || $value || count($_COOKIE)) { + db_query("INSERT INTO {sessions} (sid, uid, cache, hostname, session, timestamp) VALUES ('%s', %d, %d, '%s', '%s', %d)", $key, $user->uid, isset($user->cache) ? $user->cache : 0, ip_address(), $value, time()); } } else { - // If this query fails, another parallel request probably got here first. - // In that case, any session data generated in this request is discarded. - @db_query("INSERT INTO {sessions} (sid, uid, cache, hostname, session, timestamp) VALUES ('%s', %d, %d, '%s', '%s', %d)", $key, $user->uid, isset($user->cache) ? $user->cache : '', ip_address(), $value, time()); + db_query("UPDATE {sessions} SET uid = %d, cache = %d, hostname = '%s', session = '%s', timestamp = %d WHERE sid = '%s'", $user->uid, isset($user->cache) ? $user->cache : 0, ip_address(), $value, time(), $key); + + if (db_affected_rows()) { + // Last access time is updated no more frequently than once every 180 seconds. + // This reduces contention in the users table. + if ($user->uid && time() - $user->access > variable_get('session_write_interval', 180)) { + db_query("UPDATE {users} SET access = %d WHERE uid = %d", time(), $user->uid); + } + } } return TRUE; === removed file 'includes/tests/database.test' --- includes/tests/database.test 2008-07-19 12:31:14 +0000 +++ includes/tests/database.test 1970-01-01 00:00:00 +0000 @@ -1,182 +0,0 @@ - t('Database placeholders'), - 'description' => t('Make sure that invalid values do not get passed through the %n, %d, or %f placeholders.'), - 'group' => t('System') - ); - } - - function testPlaceholders() { - // First test the numeric type - $valid = array( - '0' => 0, - '1' => 1, - '543.21' => 543.21, - '123.456' => 123.46, - '+0.1e3' => 0.1e3, - ); - $not_valid = array( - '1x' => 0, - '4.4 OR 1=1' => 0, - '9 9' => 0, - '0xff' => 0, - 'XXX' => 0, - '0Xaa' => 0, - 'e' => 0, - '--1' => 0, - 'DROP TABLE' => 0, - '44-66' => 0, - '' => 0, - '.' => 0, - '%88' => 0, - ); - - $schema = array( - 'fields' => array( - 'n' => array( - 'type' => 'numeric', - 'precision' => 5, - 'scale' => 2, - 'not null' => TRUE, - ), - ) - ); - - $ret = array(); - db_create_table($ret, 'test_numeric', $schema); - $insert_query = 'INSERT INTO {test_numeric} (n) VALUES (' . db_type_placeholder('numeric') . ')'; - foreach ($valid as $insert => $select) { - db_query('DELETE FROM {test_numeric}'); - db_query($insert_query, $insert); - $count = db_result(db_query('SELECT COUNT(*) FROM {test_numeric}')); - $this->assertEqual(1, $count, "[numeric] One row ($count) after inserting $insert"); - $test = db_result(db_query('SELECT n FROM {test_numeric}')); - $this->assertEqual($select, $test, "[numeric] Got $select ($test) after inserting valid value $insert"); - } - foreach ($not_valid as $insert => $select) { - db_query('DELETE FROM {test_numeric}'); - db_query($insert_query, $insert); - $count = db_result(db_query('SELECT COUNT(*) FROM {test_numeric}')); - $this->assertEqual(1, $count, "[numeric] One row ($count) after inserting $insert"); - $test = db_result(db_query('SELECT n FROM {test_numeric}')); - $this->assertEqual(0, $test, "[numeric] Got $select ($test) after inserting invalid value $insert"); - } - - // Test ints - $valid = array( - '0' => 0, - '1' => 1, - '543.21' => 543, - '123.456' => 123, - '22' => 22, - ); - $not_valid = array( - '+0.1e3' => 0, - '0xff' => 0, - '0Xaa' => 0, - '1x' => 1, - '4.4 OR 1=1' => 4, - '9 9' => 9, - 'XXX' => 0, - 'e' => 0, - '--1' => 0, - 'DROP TABLE' => 0, - '44-66' => 44, - '' => 0, - '.' => 0, - '%88' => 0, - ); - - $schema = array( - 'fields' => array( - 'n' => array( - 'type' => 'int', - 'not null' => TRUE, - ), - ) - ); - - $ret = array(); - db_create_table($ret, 'test_int', $schema); - $insert_query = 'INSERT INTO {test_int} (n) VALUES (' . db_type_placeholder('int') . ')'; - foreach ($valid as $insert => $select) { - db_query('DELETE FROM {test_int}'); - db_query($insert_query, $insert); - $count = db_result(db_query('SELECT COUNT(*) FROM {test_int}')); - $this->assertEqual(1, $count, "[int] One row ($count) after inserting $insert"); - $test = db_result(db_query('SELECT n FROM {test_int}')); - $this->assertEqual($select, $test, "[int] Got $select ($test) after inserting valid value $insert"); - } - foreach ($not_valid as $insert => $select) { - db_query('DELETE FROM {test_int}'); - db_query($insert_query, $insert); - $count = db_result(db_query('SELECT COUNT(*) FROM {test_int}')); - $this->assertEqual(1, $count, "[int] One row ($count) after inserting $insert"); - $test = db_result(db_query('SELECT n FROM {test_int}')); - $this->assertEqual($select, $test, "[int] Got $select ($test) after inserting invalid value $insert"); - } - - // Test floats - $valid = array( - '0' => 0, - '1' => 1, - '543.21' => 543.21, - '123.456' => 123.456, - '22' => 22, - '+0.1e3' => 100, - ); - $not_valid = array( - '0xff' => 0, - '0Xaa' => 0, - '1x' => 1, - '4.4 OR 1=1' => 4.4, - '9 9' => 9, - 'XXX' => 0, - 'e' => 0, - '--1' => 0, - 'DROP TABLE' => 0, - '44-66' => 44, - '' => 0, - '.' => 0, - '%88' => 0, - ); - - $schema = array( - 'fields' => array( - 'n' => array( - 'type' => 'float', - 'not null' => TRUE, - ), - ) - ); - - $ret = array(); - db_create_table($ret, 'test_float', $schema); - $insert_query = 'INSERT INTO {test_float} (n) VALUES (' . db_type_placeholder('float') . ')'; - foreach ($valid as $insert => $select) { - db_query('DELETE FROM {test_float}'); - db_query($insert_query, $insert); - $count = db_result(db_query('SELECT COUNT(*) FROM {test_float}')); - $this->assertEqual(1, $count, "[float] One row ($count) after inserting $insert"); - $test = db_result(db_query('SELECT n FROM {test_float}')); - $this->assertEqual($select, $test, "[float] Got $select ($test) after inserting valid value $insert"); - } - foreach ($not_valid as $insert => $select) { - db_query('DELETE FROM {test_float}'); - db_query($insert_query, $insert); - $count = db_result(db_query('SELECT COUNT(*) FROM {test_float}')); - $this->assertEqual(1, $count, "[float] One row ($count) after inserting $insert"); - $test = db_result(db_query('SELECT n FROM {test_float}')); - $this->assertEqual($select, $test, "[float] Got $select ($test) after inserting invalid value $insert"); - } - - } -} === modified file 'install.php' --- install.php 2008-07-18 07:24:29 +0000 +++ install.php 2008-07-22 07:28:59 +0000 @@ -66,7 +66,7 @@ function install_main() { // Establish a connection to the database. require_once './includes/database.inc'; - db_set_active(); + //db_set_active(); // Check if Drupal is installed. $task = install_verify_drupal(); @@ -154,9 +154,12 @@ function install_main() { */ function install_verify_drupal() { // Read the variable manually using the @ so we don't trigger an error if it fails. - $result = @db_query("SELECT value FROM {variable} WHERE name = '%s'", 'install_task'); - if ($result) { - return unserialize(db_result($result)); + try { + if ($result = db_query("SELECT value FROM {variable} WHERE name = '%s'", 'install_task')) { + return unserialize(db_result($result)); + } + } + catch (Exception $e) { } } @@ -164,23 +167,18 @@ function install_verify_drupal() { * Verify existing settings.php */ function install_verify_settings() { - global $db_prefix, $db_type, $db_url; + global $db_prefix, $databases; // Verify existing settings (if any). - if (!empty($db_url)) { + if (!empty($databases)) { // We need this because we want to run form_get_errors. include_once './includes/form.inc'; - $url = parse_url(is_array($db_url) ? $db_url['default'] : $db_url); - $db_user = urldecode($url['user']); - $db_pass = isset($url['pass']) ? urldecode($url['pass']) : NULL; - $db_host = urldecode($url['host']); - $db_port = isset($url['port']) ? urldecode($url['port']) : ''; - $db_path = ltrim(urldecode($url['path']), '/'); + $database = $databases['default']['default']; $settings_file = './' . conf_path(FALSE, TRUE) . '/settings.php'; $form_state = array(); - _install_settings_form_validate($db_prefix, $db_type, $db_user, $db_pass, $db_host, $db_port, $db_path, $settings_file, $form_state); + _install_settings_form_validate($database, $settings_file, $form_state); if (!form_get_errors()) { return TRUE; } @@ -192,22 +190,23 @@ function install_verify_settings() { * Configure and rewrite settings.php. */ function install_change_settings($profile = 'default', $install_locale = '') { - global $db_url, $db_type, $db_prefix; + global $databases, $db_prefix; - $url = parse_url(is_array($db_url) ? $db_url['default'] : $db_url); - $db_user = isset($url['user']) ? urldecode($url['user']) : ''; - $db_pass = isset($url['pass']) ? urldecode($url['pass']) : ''; - $db_host = isset($url['host']) ? urldecode($url['host']) : ''; - $db_port = isset($url['port']) ? urldecode($url['port']) : ''; - $db_path = ltrim(urldecode($url['path']), '/'); $conf_path = './' . conf_path(FALSE, TRUE); $settings_file = $conf_path . '/settings.php'; - + $database = $databases['default']['default']; // We always need this because we want to run form_get_errors. include_once './includes/form.inc'; install_task_list('database'); - $output = drupal_get_form('install_settings_form', $profile, $install_locale, $settings_file, $db_url, $db_type, $db_prefix, $db_user, $db_pass, $db_host, $db_port, $db_path); +/* + if (isset($database['username'])) { + // Do not install over a configured settings.php. + install_already_done_error(); + } +*/ + + $output = drupal_get_form('install_settings_form', $profile, $install_locale, $settings_file, $database); drupal_set_title(st('Database configuration')); print theme('install_page', $output); exit; @@ -217,19 +216,11 @@ function install_change_settings($profil /** * Form API array definition for install_settings. */ -function install_settings_form(&$form_state, $profile, $install_locale, $settings_file, $db_url, $db_type, $db_prefix, $db_user, $db_pass, $db_host, $db_port, $db_path) { - if (empty($db_host)) { - $db_host = 'localhost'; - } - $db_types = drupal_detect_database_types(); - - // If both 'mysql' and 'mysqli' are available, we disable 'mysql': - if (isset($db_types['mysqli'])) { - unset($db_types['mysql']); - } +function install_settings_form(&$form_state, $profile, $install_locale, $settings_file, $database) { + $drivers = drupal_detect_database_types(); - if (count($db_types) == 0) { - $form['no_db_types'] = array( + if (!$drivers) { + $form['no_drivers'] = array( '#markup' => st('Your web server does not appear to support any common database types. Check with your hosting provider to see if they offer any databases that Drupal supports.', array('@drupal-databases' => 'http://drupal.org/node/270#database')), ); } @@ -240,54 +231,51 @@ function install_settings_form(&$form_st '#description' => '

' . st('To set up your @drupal database, enter the following information.', array('@drupal' => drupal_install_profile_name())) . '

', ); - if (count($db_types) > 1) { - $form['basic_options']['db_type'] = array( + if (count($drivers) == 1) { + $form['basic_options']['driver'] = array( + '#type' => 'hidden', + '#value' => current(array_keys($drivers)), + ); + $database_description = st('The name of the %driver database your @drupal data will be stored in. It must exist on your server before @drupal can be installed.', array('%driver' => current($drivers), '@drupal' => drupal_install_profile_name())); + } + else { + $form['basic_options']['driver'] = array( '#type' => 'radios', - '#title' => st('Database type'), + '#title' => st('Database driver'), '#required' => TRUE, - '#options' => $db_types, - '#default_value' => ($db_type ? $db_type : current($db_types)), + '#options' => $drivers, + '#default_value' => !empty($database['driver']) ? $database['driver'] : current($drivers), '#description' => st('The type of database your @drupal data will be stored in.', array('@drupal' => drupal_install_profile_name())), ); - $db_path_description = st('The name of the database your @drupal data will be stored in. It must exist on your server before @drupal can be installed.', array('@drupal' => drupal_install_profile_name())); - } - else { - if (count($db_types) == 1) { - $db_types = array_values($db_types); - $form['basic_options']['db_type'] = array( - '#type' => 'hidden', - '#value' => $db_types[0], - ); - $db_path_description = st('The name of the %db_type database your @drupal data will be stored in. It must exist on your server before @drupal can be installed.', array('%db_type' => $db_types[0], '@drupal' => drupal_install_profile_name())); - } + $database_description = st('The name of the database your @drupal data will be stored in. It must exist on your server before @drupal can be installed.', array('@drupal' => drupal_install_profile_name())); } // Database name - $form['basic_options']['db_path'] = array( + $form['basic_options']['database'] = array( '#type' => 'textfield', '#title' => st('Database name'), - '#default_value' => $db_path, + '#default_value' => empty($database['database']) ? '' : $database['database'], '#size' => 45, '#maxlength' => 45, '#required' => TRUE, - '#description' => $db_path_description + '#description' => $database_description, ); // Database username - $form['basic_options']['db_user'] = array( + $form['basic_options']['username'] = array( '#type' => 'textfield', '#title' => st('Database username'), - '#default_value' => $db_user, + '#default_value' => empty($database['username']) ? '' : $database['username'], '#size' => 45, '#maxlength' => 45, '#required' => TRUE, ); // Database username - $form['basic_options']['db_pass'] = array( + $form['basic_options']['password'] = array( '#type' => 'password', '#title' => st('Database password'), - '#default_value' => $db_pass, + '#default_value' => empty($database['password']) ? '' : $database['password'], '#size' => 45, '#maxlength' => 45, ); @@ -301,10 +289,10 @@ function install_settings_form(&$form_st ); // Database host - $form['advanced_options']['db_host'] = array( + $form['advanced_options']['host'] = array( '#type' => 'textfield', '#title' => st('Database host'), - '#default_value' => $db_host, + '#default_value' => empty($database['host']) ? 'localhost' : $database['host'], '#size' => 45, '#maxlength' => 45, '#required' => TRUE, @@ -312,24 +300,24 @@ function install_settings_form(&$form_st ); // Database port - $form['advanced_options']['db_port'] = array( + $form['advanced_options']['port'] = array( '#type' => 'textfield', '#title' => st('Database port'), - '#default_value' => $db_port, + '#default_value' => empty($database['port']) ? '' : $database['port'], '#size' => 45, '#maxlength' => 45, '#description' => st('If your database server is listening to a non-standard port, enter its number.'), ); // Table prefix - $prefix = ($profile == 'default') ? 'drupal_' : $profile . '_'; + $db_prefix = ($profile == 'default') ? 'drupal_' : $profile . '_'; $form['advanced_options']['db_prefix'] = array( '#type' => 'textfield', '#title' => st('Table prefix'), - '#default_value' => $db_prefix, + '#default_value' => '', '#size' => 45, '#maxlength' => 45, - '#description' => st('If more than one application will be sharing this database, enter a table prefix such as %prefix for your @drupal site here.', array('@drupal' => drupal_install_profile_name(), '%prefix' => $prefix)), + '#description' => st('If more than one application will be sharing this database, enter a table prefix such as %prefix for your @drupal site here.', array('@drupal' => drupal_install_profile_name(), '%prefix' => $db_prefix)), ); $form['save'] = array( @@ -339,7 +327,7 @@ function install_settings_form(&$form_st $form['errors'] = array(); $form['settings_file'] = array('#type' => 'value', '#value' => $settings_file); - $form['_db_url'] = array('#type' => 'value'); + $form['_database'] = array('#type' => 'value'); $form['#action'] = "install.php?profile=$profile" . ($install_locale ? "&locale=$install_locale" : ''); $form['#redirect'] = FALSE; } @@ -351,48 +339,43 @@ function install_settings_form(&$form_st */ function install_settings_form_validate($form, &$form_state) { global $db_url; - _install_settings_form_validate($form_state['values']['db_prefix'], $form_state['values']['db_type'], $form_state['values']['db_user'], $form_state['values']['db_pass'], $form_state['values']['db_host'], $form_state['values']['db_port'], $form_state['values']['db_path'], $form_state['values']['settings_file'], $form_state, $form); + _install_settings_form_validate($form_state['values'], $form_state['values']['settings_file'], $form_state, $form); } /** * Helper function for install_settings_validate. */ -function _install_settings_form_validate($db_prefix, $db_type, $db_user, $db_pass, $db_host, $db_port, $db_path, $settings_file, &$form_state, $form = NULL) { - global $db_url; - +function _install_settings_form_validate($database, $settings_file, &$form_state, $form = NULL) { + global $databases; // Verify the table prefix - if (!empty($db_prefix) && is_string($db_prefix) && !preg_match('/^[A-Za-z0-9_.]+$/', $db_prefix)) { + if (!empty($database['prefix']) && is_string($database['prefix']) && !preg_match('/^[A-Za-z0-9_.]+$/', $database['dprefix'])) { form_set_error('db_prefix', st('The database table prefix you have entered, %db_prefix, is invalid. The table prefix can only contain alphanumeric characters, periods, or underscores.', array('%db_prefix' => $db_prefix)), 'error'); } - if (!empty($db_port) && !is_numeric($db_port)) { + if (!empty($database['port']) && !is_numeric($database['port'])) { form_set_error('db_port', st('Database port must be a number.')); } // Check database type - if (!isset($form)) { - $_db_url = is_array($db_url) ? $db_url['default'] : $db_url; - $db_type = substr($_db_url, 0, strpos($_db_url, '://')); - } - $databases = drupal_detect_database_types(); - if (!in_array($db_type, $databases)) { - form_set_error('db_type', st("In your %settings_file file you have configured @drupal to use a %db_type server, however your PHP installation currently does not support this database type.", array('%settings_file' => $settings_file, '@drupal' => drupal_install_profile_name(), '%db_type' => $db_type))); + $database_types = drupal_detect_database_types(); + $driver = $database['driver']; + if (!isset($database_types[$driver])) { + form_set_error('driver', st("In your %settings_file file you have configured @drupal to use a %driver server, however your PHP installation currently does not support this database type.", array('%settings_file' => $settings_file, '@drupal' => drupal_install_profile_name(), '%driver' => $database['driver']))); } else { - // Verify - $db_url = $db_type . '://' . urlencode($db_user) . ($db_pass ? ':' . urlencode($db_pass) : '') . '@' . ($db_host ? urlencode($db_host) : 'localhost') . ($db_port ? ":$db_port" : '') . '/' . urlencode($db_path); if (isset($form)) { - form_set_value($form['_db_url'], $db_url, $form_state); + form_set_value($form['_database'], $database, $form_state); } - $success = array(); - - $function = 'drupal_test_' . $db_type; - if (!$function($db_url, $success)) { - if (isset($success['CONNECT'])) { - form_set_error('db_type', st('In order for Drupal to work, and to continue with the installation process, you must resolve all permission issues reported above. We were able to verify that we have permission for the following commands: %commands. For more help with configuring your database server, see the Installation and upgrading handbook. If you are unsure what any of this means you should probably contact your hosting provider.', array('%commands' => implode($success, ', ')))); + $class = "DatabaseInstaller_$driver"; + $test = new $class; + $databases = array('default' => array('default' => $database)); + $return = $test->test(); + if (!$return || $test->error) { + if (!empty($test->success)) { + form_set_error('db_type', st('In order for Drupal to work, and to continue with the installation process, you must resolve all permission issues reported above. We were able to verify that we have permission for the following commands: %commands. For more help with configuring your database server, see the Installation and upgrading handbook. If you are unsure what any of this means you should probably contact your hosting provider.', array('%commands' => implode($test->success, ', ')))); } else { - form_set_error('db_type', ''); + form_set_error('driver', ''); } } } @@ -404,9 +387,10 @@ function _install_settings_form_validate function install_settings_form_submit($form, &$form_state) { global $profile, $install_locale; + $database = array_intersect_key($form_state['values']['_database'], array_flip(array('driver', 'database', 'username', 'password', 'host', 'port'))); // Update global settings array and save - $settings['db_url'] = array( - 'value' => $form_state['values']['_db_url'], + $settings['databases'] = array( + 'value' => array('default' => array('default' => $database)), 'required' => TRUE, ); $settings['db_prefix'] = array( @@ -751,6 +735,17 @@ if (Drupal.jsEnabled) { }', 'inline'); // Build menu to allow clean URL check. menu_rebuild(); + + // Cache a fully-built schema. This is necessary for any + // invocation of index.php because: (1) setting cache table + // entries requires schema information, (2) that occurs during + // bootstrap before any module are loaded, so (3) if there is no + // cached schema, drupal_get_schema() will try to generate one + // but with no loaded modules will return nothing. + // + // This logically could be done during task 'done' but the clean + // URL check requires it now. + drupal_get_schema(NULL, TRUE); } else { @@ -826,6 +821,9 @@ if (Drupal.jsEnabled) { _drupal_flush_css_js(); variable_set('install_profile', $profile); + + // Cache a fully-built schema. + drupal_get_schema(NULL, TRUE); } // Set task for user, and remember the task in the database. @@ -1185,3 +1183,4 @@ function install_configure_form_submit($ // Start the installer. install_main(); + === modified file 'modules/comment/comment.module' --- modules/comment/comment.module 2008-07-16 21:59:24 +0000 +++ modules/comment/comment.module 2008-07-23 07:00:33 +0000 @@ -693,7 +693,7 @@ function comment_save($edit) { // Strip the "/" from the end of the parent thread. $parent->thread = (string) rtrim((string) $parent->thread, '/'); // Get the max value in *this* thread. - $max = db_result(db_query("SELECT MAX(thread) FROM {comments} WHERE thread LIKE '%s.%%' AND nid = %d", $parent->thread, $edit['nid'])); + $max = db_query("SELECT MAX(thread) FROM {comments} WHERE thread LIKE :thread AND nid = :nid", array(':thread' => $parent->thread .'%', ':nid' => $edit['nid']))->fetchField(); if ($max == '') { // First child of this parent. === modified file 'modules/dblog/dblog.install' --- modules/dblog/dblog.install 2008-03-15 12:31:28 +0000 +++ modules/dblog/dblog.install 2008-07-22 07:28:55 +0000 @@ -65,7 +65,7 @@ function dblog_schema() { 'link' => array( 'type' => 'varchar', 'length' => 255, - 'not null' => TRUE, + 'not null' => FALSE, 'default' => '', 'description' => t('Link to view the result of the event.'), ), @@ -77,7 +77,7 @@ function dblog_schema() { 'referer' => array( 'type' => 'varchar', 'length' => 128, - 'not null' => TRUE, + 'not null' => FALSE, 'default' => '', 'description' => t('URL of referring page.'), ), @@ -103,3 +103,13 @@ function dblog_schema() { return $schema; } + +/** + * Allow NULL values for links. + */ +function dblog_update_7001() { + $ret = array(); + db_change_field($ret, 'watchdog', 'link', 'link', array('type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'default' => '')); + db_change_field($ret, 'watchdog', 'referer', 'referer', array('type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'default' => '')); + return $ret; +} === modified file 'modules/dblog/dblog.module' --- modules/dblog/dblog.module 2008-05-06 12:18:44 +0000 +++ modules/dblog/dblog.module 2008-07-22 07:28:55 +0000 @@ -135,7 +135,8 @@ function dblog_watchdog($log = array()) $log['request_uri'], $log['referer'], $log['ip'], - $log['timestamp']); + $log['timestamp'] + ); if ($current_db) { db_set_active($current_db); === modified file 'modules/dblog/dblog.test' --- modules/dblog/dblog.test 2008-05-30 07:30:49 +0000 +++ modules/dblog/dblog.test 2008-07-23 07:00:33 +0000 @@ -210,11 +210,15 @@ class DBLogTestCase extends DrupalWebTes // Count rows that have uids for the user. $count = db_result(db_query('SELECT COUNT(wid) FROM {watchdog} WHERE uid = %d', $user->uid)); $this->assertTrue($count == 0, t('DBLog contains @count records for @name', array('@count' => $count, '@name' => $user->name))); + // Fetch row ids in watchdog that previously related to the deleted user. - $result = db_query('SELECT wid FROM {watchdog} WHERE uid = 0 AND wid IN (%s)', implode(', ', $ids)); + $select = db_select('watchdog'); + $select->addField('wid', 'watchdog'); + $select->condition('uid', 0)->condition('wid', $ids, 'IN'); + $result = $select->execute(); unset($ids); - while ($row = db_fetch_array($result)) { - $ids[] = $row['wid']; + foreach ($result as $row) { + $ids[] = $row->wid; } $count_after = (isset($ids)) ? count($ids) : 0; $this->assertTrue($count_after == $count_before, t('DBLog contains @count records for @name that now have uid = 0', array('@count' => $count_before, '@name' => $user->name))); === modified file 'modules/filter/filter.module' --- modules/filter/filter.module 2008-07-19 20:06:25 +0000 +++ modules/filter/filter.module 2008-07-22 07:28:54 +0000 @@ -292,24 +292,24 @@ function filter_formats($index = NULL) { if (!isset($formats)) { $formats = array(); - $query = 'SELECT * FROM {filter_formats}'; + $query = db_select('filter_formats', 'f'); + $query->addField('format', 'f', 'format'); + $query->addField('name', 'f', 'name'); + $query->addField('roles', 'f', 'roles'); + $query->addField('cache', 'f', 'cache'); + $query->addField('weight', 'f', 'weight'); + $query->orderBy('weight'); // Build query for selecting the format(s) based on the user's roles. - $args = array(); if (!$all) { - $where = array(); + $or = db_or()->condition('format', variable_get('filter_default_format', 1)); foreach ($user->roles as $rid => $role) { - $where[] = "roles LIKE '%%,%d,%%'"; - $args[] = $rid; + $or->condition('roles', '%'. (int)$rid .'%', 'LIKE'); } - $query .= ' WHERE ' . implode(' OR ', $where) . ' OR format = %d'; - $args[] = variable_get('filter_default_format', 1); + $query->condition($or); } - $result = db_query($query . ' ORDER by weight', $args); - while ($format = db_fetch_object($result)) { - $formats[$format->format] = $format; - } + $formats = $query->execute()->fetchAllAssoc('format'); } if (isset($index)) { return isset($formats[$index]) ? $formats[$index] : FALSE; === modified file 'modules/menu/menu.module' --- modules/menu/menu.module 2008-06-25 09:12:24 +0000 +++ modules/menu/menu.module 2008-07-22 07:28:52 +0000 @@ -302,7 +302,7 @@ function menu_nodeapi(&$node, $op) { break; case 'delete': // Delete all menu module links that point to this node. - $result = db_query("SELECT mlid FROM {menu_links} WHERE link_path = 'node/%d' AND module = 'menu'", $node->nid); + $result = db_query("SELECT mlid FROM {menu_links} WHERE link_path = :path AND module = 'menu'", array(':path' => 'node/'. $node->nid)); while ($m = db_fetch_array($result)) { menu_link_delete($m['mlid']); } @@ -314,10 +314,15 @@ function menu_nodeapi(&$node, $op) { $item = array(); if (isset($node->nid)) { // Give priority to the default menu - $mlid = db_result(db_query_range("SELECT mlid FROM {menu_links} WHERE link_path = 'node/%d' AND menu_name = '%s' AND module = 'menu' ORDER BY mlid ASC", $node->nid, $menu_name, 0, 1)); + $mlid = db_result(db_query_range("SELECT mlid FROM {menu_links} WHERE link_path = :path AND menu_name = :menu_name AND module = 'menu' ORDER BY mlid ASC", array( + ':path' => 'node/'. $node->nid, + ':menu_name' => $menu_name, + ), 0, 1)); // Check all menus if a link does not exist in the default menu. if (!$mlid) { - $mlid = db_result(db_query_range("SELECT mlid FROM {menu_links} WHERE link_path = 'node/%d' AND module = 'menu' ORDER BY mlid ASC", $node->nid, 0, 1)); + $mlid = db_result(db_query_range("SELECT mlid FROM {menu_links} WHERE link_path = :path AND module = 'menu' ORDER BY mlid ASC", array( + ':path' => 'node/'. $node->nid, + ), 0, 1)); } if ($mlid) { $item = menu_link_load($mlid); @@ -428,15 +433,13 @@ function menu_node_form_submit($form, &$ */ function menu_get_menus($all = TRUE) { $system_menus = menu_list_system_menus(); - $sql = 'SELECT * FROM {menu_custom}'; + $query = db_select('menu_custom'); + $query->addField('menu_name', 'menu_custom', 'menu_name'); + $query->addField('title', 'menu_custom', 'title'); if (!$all) { - $sql .= ' WHERE menu_name NOT IN (' . implode(',', array_fill(0, count($system_menus), "'%s'")) . ')'; + $query->condition('menu_name', $system_menus, 'NOT IN'); } - $sql .= ' ORDER BY title'; - $result = db_query($sql, $system_menus); - $rows = array(); - while ($r = db_fetch_array($result)) { - $rows[$r['menu_name']] = $r['title']; - } - return $rows; + $query->orderBy('title'); + + return $query->execute()->fetchAllKeyed(); } === modified file 'modules/node/node.module' --- modules/node/node.module 2008-07-16 21:59:24 +0000 +++ modules/node/node.module 2008-07-22 07:28:54 +0000 @@ -511,12 +511,18 @@ function node_type_save($info) { if (!isset($info->help)) { $info->help = ''; } - if (!isset($info->min_word_count)) { + if (empty($info->min_word_count)) { $info->min_word_count = 0; } if (!isset($info->body_label)) { $info->body_label = ''; } + if (empty($info->custom)) { + $info->custom = 0; + } + if (empty($info->locked)) { + $info->locked = 0; + } if ($is_existing) { db_query("UPDATE {node_type} SET type = '%s', name = '%s', module = '%s', has_title = %d, title_label = '%s', has_body = %d, body_label = '%s', description = '%s', help = '%s', min_word_count = %d, custom = %d, modified = %d, locked = %d WHERE type = '%s'", $info->type, $info->name, $info->module, $info->has_title, $info->title_label, $info->has_body, $info->body_label, $info->description, $info->help, $info->min_word_count, $info->custom, $info->modified, $info->locked, $existing_type); @@ -746,7 +752,7 @@ function node_load($param = array(), $re elseif (is_array($param)) { // Turn the conditions into a query. foreach ($param as $key => $value) { - $cond[] = 'n.' . db_escape_string($key) . " = '%s'"; + $cond[] = 'n.' . preg_replace('/[^A-Za-z0-9_]+/', '', $key) . " = '%s'"; $arguments[] = $value; } $cond = implode(' AND ', $cond); @@ -2233,6 +2239,37 @@ function node_db_rewrite_sql($query, $pr } } + +/** + * Implementation of hook_query_alter(). + * @todo This doesn't quite work yet. + */ +function DISABLED_node_query_alter(Query $query) { + if ($query->hasTag('node_access')) { + if (! user_access('administer nodes')) { + $query->distinct(); + $access_alias = $query->join('node_access', 'na', 'na.nid = n.nid'); + dsm('hello'); + _node_query_alter_where($query, 'view', $access_alias); + } + } +} + +function _node_query_alter_where($query, $op = 'view', $node_access_alias = 'na', $account = NULL) { + $or = db_or(); + foreach (node_access_grants($op, $account) as $realm => $gids) { + foreach ($gids as $gid) { + $or->condition("{$node_access_alias}.gid = :gid AND {$node_access_alias}.realm = :realm", array(':gid' => $gid, ':realm' => $realm)); + } + } + + if (count($or->conditions())) { + $query->condition($or); + } + + $query->condition("$node_access_alias.grant_$op", '>=', 1); +} + /** * This function will call module invoke to get a list of grants and then * write them to the database. It is called at node save, and should be @@ -2287,11 +2324,11 @@ function node_access_acquire_grants($nod */ function node_access_write_grants($node, $grants, $realm = NULL, $delete = TRUE) { if ($delete) { - $query = 'DELETE FROM {node_access} WHERE nid = %d'; + $query = db_delete('node_access')->condition('nid', $node->nid); if ($realm) { - $query .= " AND realm in ('%s', 'all')"; + $query->condition('realm', array($realm, 'all'), 'IN'); } - db_query($query, $node->nid, $realm); + $query->execute(); } // Only perform work when node_access modules are active. === modified file 'modules/path/path.admin.inc' --- modules/path/path.admin.inc 2008-04-14 17:48:33 +0000 +++ modules/path/path.admin.inc 2008-07-22 07:28:57 +0000 @@ -19,12 +19,14 @@ function path_admin_overview($keys = NUL $multilanguage = (module_exists('locale') || $count); if ($keys) { - // Replace wildcards with MySQL/PostgreSQL wildcards. + // Replace wildcards with PDO wildcards. $keys = preg_replace('!\*+!', '%', $keys); - $sql = "SELECT * FROM {url_alias} WHERE dst LIKE '%%%s%%'"; + $sql = "SELECT * FROM {url_alias} WHERE dst LIKE :keys"; + $args = array(':keys' => '%'. $keys .'%'); } else { $sql = 'SELECT * FROM {url_alias}'; + $args = array(); } $header = array( array('data' => t('Alias'), 'field' => 'dst', 'sort' => 'asc'), @@ -36,7 +38,7 @@ function path_admin_overview($keys = NUL $header[2] = array('data' => t('Language'), 'field' => 'language'); } $sql .= tablesort_sql($header); - $result = pager_query($sql, 50, 0 , NULL, $keys); + $result = pager_query($sql, 50, 0 , NULL, $args); $rows = array(); $destination = drupal_get_destination(); === modified file 'modules/profile/profile.admin.inc' --- modules/profile/profile.admin.inc 2008-07-16 21:59:24 +0000 +++ modules/profile/profile.admin.inc 2008-07-22 07:28:57 +0000 @@ -396,7 +396,7 @@ function profile_field_delete_submit($fo */ function profile_admin_settings_autocomplete($string) { $matches = array(); - $result = db_query_range("SELECT category FROM {profile_fields} WHERE LOWER(category) LIKE LOWER('%s%%')", $string, 0, 10); + $result = db_query_range("SELECT category FROM {profile_fields} WHERE LOWER(category) LIKE LOWER(:category)", array(':category' => $string .'%'), 0, 10); while ($data = db_fetch_object($result)) { $matches[$data->category] = check_plain($data->category); } === modified file 'modules/profile/profile.pages.inc' --- modules/profile/profile.pages.inc 2007-12-08 14:06:20 +0000 +++ modules/profile/profile.pages.inc 2008-07-22 07:28:57 +0000 @@ -110,7 +110,10 @@ function profile_browse() { function profile_autocomplete($field, $string) { $matches = array(); if (db_result(db_query("SELECT COUNT(*) FROM {profile_fields} WHERE fid = %d AND autocomplete = 1", $field))) { - $result = db_query_range("SELECT value FROM {profile_values} WHERE fid = %d AND LOWER(value) LIKE LOWER('%s%%') GROUP BY value ORDER BY value ASC", $field, $string, 0, 10); + $result = db_query_range("SELECT value FROM {profile_values} WHERE fid = :fid AND LOWER(value) LIKE LOWER(:value) GROUP BY value ORDER BY value ASC", array( + ':fid' => $field, + ':value' => $string .'%', + ), 0, 10); while ($data = db_fetch_object($result)) { $matches[$data->value] = check_plain($data->value); } === modified file 'modules/search/search.module' --- modules/search/search.module 2008-07-10 02:13:02 +0000 +++ modules/search/search.module 2008-07-22 07:28:53 +0000 @@ -284,10 +284,7 @@ function search_update_totals() { $total = db_result(db_query("SELECT SUM(score) FROM {search_index} WHERE word = '%s'", $word)); // Apply Zipf's law to equalize the probability distribution $total = log10(1 + 1/(max(1, $total))); - db_query("UPDATE {search_total} SET count = %f WHERE word = '%s'", $total, $word); - if (!db_affected_rows()) { - db_query("INSERT INTO {search_total} (word, count) VALUES ('%s', %f)", $word, $total); - } + db_merge('search_total')->key(array('word' => $word))->fields(array('count' => $total))->execute(); } // Find words that were deleted from search_index, but are still in // search_total. We use a LEFT JOIN between the two tables and keep only the @@ -575,10 +572,12 @@ function search_index($sid, $type, $text foreach ($results[0] as $word => $score) { // The database will collate similar words (accented and non-accented forms, etc.), // and the score is additive, so first add and then insert. - db_query("UPDATE {search_index} SET score = score + %d WHERE word = '%s' AND sid = '%d' AND type = '%s'", $score, $word, $sid, $type); - if (!db_affected_rows()) { - db_query("INSERT INTO {search_index} (word, sid, type, score) VALUES ('%s', %d, '%s', %f)", $word, $sid, $type, $score); - } + db_merge('search_index')->key(array( + 'word' => $word, + 'sid' => $sid, + 'type' => $type, + ))->fields(array('score' => $score))->expression('score', 'score + :score', array(':score' => $score)) + ->execute(); search_dirty($word); } unset($results[0]); @@ -789,7 +788,7 @@ function search_parse_query($text) { $any |= $num_new_scores; if ($q) { $queryor[] = $q; - $arguments[] = $or; + $arguments[] = "% $or %"; } } if (count($queryor)) { @@ -804,7 +803,7 @@ function search_parse_query($text) { list($q, $num_new_scores, $num_valid_words) = _search_parse_query($key, $arguments2); if ($q) { $query[] = $q; - $arguments[] = $key; + $arguments[] = "% $key %"; if (!$num_valid_words) { $simple = FALSE; } @@ -821,7 +820,7 @@ function search_parse_query($text) { list($q) = _search_parse_query($key, $arguments2, TRUE); if ($q) { $query[] = $q; - $arguments[] = $key; + $arguments[] = "% $key %"; $simple = FALSE; } } @@ -855,7 +854,7 @@ function _search_parse_query(&$word, &$s } } // Return matching snippet and number of added words - return array("d.data " . ($not ? 'NOT ' : '') . "LIKE '%% %s %%'", $num_new_scores, $num_valid_words); + return array("d.data " . ($not ? 'NOT ' : '') . "LIKE '%s'", $num_new_scores, $num_valid_words); } /** === modified file 'modules/simpletest/drupal_web_test_case.php' --- modules/simpletest/drupal_web_test_case.php 2008-07-18 07:30:34 +0000 +++ modules/simpletest/drupal_web_test_case.php 2008-07-22 07:28:56 +0000 @@ -630,6 +630,7 @@ class DrupalWebTestCase { // Generate temporary prefixed database to ensure that tests have a clean starting point. $db_prefix = 'simpletest' . mt_rand(1000, 1000000); + include_once './includes/install.inc'; drupal_install_system(); @@ -638,6 +639,12 @@ class DrupalWebTestCase { $modules = array_unique(array_merge(drupal_verify_profile('default', 'en'), $args)); drupal_install_modules($modules); + // Because the schema is static cached, we need to flush + // it between each run. If we don't, then it will contain + // stale data for the previous run's database prefix and all + // calls to it will fail. + drupal_get_schema(NULL, TRUE); + // Run default profile tasks. $task = 'profile'; default_profile_tasks($task, ''); === modified file 'modules/simpletest/simpletest.module' --- modules/simpletest/simpletest.module 2008-07-16 21:59:24 +0000 +++ modules/simpletest/simpletest.module 2008-07-23 07:00:33 +0000 @@ -303,8 +303,7 @@ function simpletest_test_form_submit($fo function simpletest_run_tests($test_list, $reporter = 'drupal', $batch_mode = FALSE) { global $db_prefix, $db_prefix_original; cache_clear_all(); - db_query('INSERT INTO {simpletest_test_id} VALUES (default)'); - $test_id = db_last_insert_id('simpletest_test_id', 'test_id'); + $test_id = db_insert('simpletest_test_id')->useDefaults(array('message_id'))->execute(); if ($batch_mode) { $batch = array( @@ -495,11 +494,15 @@ function simpletest_clean_database() { * @return mixed Array of matching tables or count of tables. */ function simpletest_get_like_tables($base_table = 'simpletest', $count = FALSE) { - global $db_url, $db_prefix; - $url = parse_url($db_url); - $database = substr($url['path'], 1); + global $db_url, $db_prefix, $database; + $database_name = $database['default']['default']['database']; + /*$url = parse_url($db_url); + $database = substr($url['path'], 1);*/ $select = $count ? 'COUNT(table_name)' : 'table_name'; - $result = db_query("SELECT $select FROM information_schema.tables WHERE table_schema = '$database' AND table_name LIKE '$db_prefix$base_table%'"); + $result = db_query("SELECT $select FROM information_schema.tables WHERE table_schema = :database AND table_name LIKE :table_name", array( + ':database' => $database_name, + ':table_name' => $db_prefix . $base_table . '%', + )); $schema = drupal_get_schema_unprocessed('simpletest'); if ($count) { === modified file 'modules/statistics/statistics.admin.inc' --- modules/statistics/statistics.admin.inc 2008-05-07 19:17:50 +0000 +++ modules/statistics/statistics.admin.inc 2008-07-22 07:28:51 +0000 @@ -107,8 +107,8 @@ function statistics_top_visitors() { * Menu callback; presents the "referrer" page. */ function statistics_top_referrers() { - $query = "SELECT url, COUNT(url) AS hits, MAX(timestamp) AS last FROM {accesslog} WHERE url NOT LIKE '%%%s%%' AND url <> '' GROUP BY url"; - $query_cnt = "SELECT COUNT(DISTINCT(url)) FROM {accesslog} WHERE url <> '' AND url NOT LIKE '%%%s%%'"; + $query = "SELECT url, COUNT(url) AS hits, MAX(timestamp) AS last FROM {accesslog} WHERE url NOT LIKE :host AND url <> '' GROUP BY url"; + $query_cnt = "SELECT COUNT(DISTINCT(url)) FROM {accesslog} WHERE url <> '' AND url NOT LIKE :host"; drupal_set_title(t('Top referrers in the past %interval', array('%interval' => format_interval(variable_get('statistics_flush_accesslog_timer', 259200))))); $header = array( @@ -118,7 +118,7 @@ function statistics_top_referrers() { ); $query .= tablesort_sql($header); - $result = pager_query($query, 30, 0, $query_cnt, $_SERVER['HTTP_HOST']); + $result = pager_query($query, 30, 0, $query_cnt, array(':host' => '%'. $_SERVER['HTTP_HOST'] .'%')); $rows = array(); while ($referrer = db_fetch_object($result)) { === modified file 'modules/statistics/statistics.module' --- modules/statistics/statistics.module 2008-06-12 18:46:51 +0000 +++ modules/statistics/statistics.module 2008-07-22 07:28:51 +0000 @@ -51,17 +51,31 @@ function statistics_exit() { // We are counting content views. if ((arg(0) == 'node') && is_numeric(arg(1)) && arg(2) == '') { // A node has been viewed, so update the node's counters. - db_query('UPDATE {node_counter} SET daycount = daycount + 1, totalcount = totalcount + 1, timestamp = %d WHERE nid = %d', time(), arg(1)); - // If we affected 0 rows, this is the first time viewing the node. - if (!db_affected_rows()) { - // We must create a new row to store counters for the new node. - db_query('INSERT INTO {node_counter} (nid, daycount, totalcount, timestamp) VALUES (%d, 1, 1, %d)', arg(1), time()); - } + $fields = array( + 'daycount' => 1, + 'totalcount' => 1, + 'nid' => arg(1), + 'timestamp' => time(), + ); + db_merge('node_counter') + ->fields($fields) + ->expression('daycount', 'daycount + 1') + ->expression('totalcount', 'totalcount + 1') + ->execute(); } } if (variable_get('statistics_enable_access_log', 0)) { // Log this page access. - db_query("INSERT INTO {accesslog} (title, path, url, hostname, uid, sid, timer, timestamp) values('%s', '%s', '%s', '%s', %d, '%s', %d, %d)", strip_tags(drupal_get_title()), $_GET['q'], referer_uri(), ip_address(), $user->uid, session_id(), timer_read('page'), time()); + db_insert('accesslog')->fields(array( + 'title' => strip_tags(drupal_get_title()), + 'path' => $_GET['q'], + 'url' => referer_uri(), + 'hostname' => ip_address(), + 'uid' => $user->uid, + 'sid' => session_id(), + 'timer' => timer_read('page'), + 'timestamp' => time(), + ))->execute(); } } === modified file 'modules/system/system.admin.inc' --- modules/system/system.admin.inc 2008-07-19 19:35:49 +0000 +++ modules/system/system.admin.inc 2008-07-22 07:28:51 +0000 @@ -608,7 +608,7 @@ function system_modules($form_state = ar drupal_rebuild_theme_registry(); node_types_rebuild(); menu_rebuild(); - cache_clear_all('schema', 'cache'); + drupal_get_schema(NULL, TRUE); // Get current list of modules. $files = module_rebuild_cache(); === modified file 'modules/system/system.install' --- modules/system/system.install 2008-07-01 20:36:39 +0000 +++ modules/system/system.install 2008-07-22 07:28:51 +0000 @@ -303,11 +303,17 @@ function system_requirements($phase) { * Implementation of hook_install(). */ function system_install() { - if ($GLOBALS['db_type'] == 'pgsql') { + if (db_driver() == 'pgsql') { // Create unsigned types. - db_query("CREATE DOMAIN int_unsigned integer CHECK (VALUE >= 0)"); - db_query("CREATE DOMAIN smallint_unsigned smallint CHECK (VALUE >= 0)"); - db_query("CREATE DOMAIN bigint_unsigned bigint CHECK (VALUE >= 0)"); + if (!db_result(db_query("SELECT COUNT(*) FROM pg_constraint WHERE conname = 'int_unsigned_check'"))) { + db_query("CREATE DOMAIN int_unsigned integer CHECK (VALUE >= 0)"); + } + if (!db_result(db_query("SELECT COUNT(*) FROM pg_constraint WHERE conname = 'smallint_unsigned_check'"))) { + db_query("CREATE DOMAIN smallint_unsigned smallint CHECK (VALUE >= 0)"); + } + if (!db_result(db_query("SELECT COUNT(*) FROM pg_constraint WHERE conname = 'bigint_unsigned_check'"))) { + db_query("CREATE DOMAIN bigint_unsigned bigint CHECK (VALUE >= 0)"); + } // Create functions. db_query('CREATE OR REPLACE FUNCTION "greatest"(numeric, numeric) RETURNS numeric AS === modified file 'modules/system/system.module' --- modules/system/system.module 2008-07-18 07:06:24 +0000 +++ modules/system/system.module 2008-07-22 07:28:51 +0000 @@ -1119,7 +1119,7 @@ function system_region_list($theme_key) static $list = array(); if (!array_key_exists($theme_key, $list)) { - $info = unserialize(db_result(db_query("SELECT info FROM {system} WHERE type = 'theme' AND name = '%s'", $theme_key))); + $info = unserialize(db_result(db_query("SELECT info FROM {system} WHERE type = :type AND name = :name", array(':type' => 'theme', ':name' => $theme_key)))); $list[$theme_key] = array_map('t', $info['regions']); } === modified file 'modules/taxonomy/taxonomy.pages.inc' --- modules/taxonomy/taxonomy.pages.inc 2008-04-14 17:48:33 +0000 +++ modules/taxonomy/taxonomy.pages.inc 2008-07-22 07:28:53 +0000 @@ -119,7 +119,10 @@ function taxonomy_autocomplete($vid, $st $last_string = trim(array_pop($array)); $matches = array(); if ($last_string != '') { - $result = db_query_range(db_rewrite_sql("SELECT t.tid, t.name FROM {term_data} t WHERE t.vid = %d AND LOWER(t.name) LIKE LOWER('%%%s%%')", 't', 'tid'), $vid, $last_string, 0, 10); + $result = db_query_range(db_rewrite_sql("SELECT t.tid, t.name FROM {term_data} t WHERE t.vid = :vid AND LOWER(t.name) LIKE LOWER(:last_string)", 't', 'tid'), array( + ':vid' => $vid, + ':last_string' => '%'. $last_string .'%', + ), 0, 10); $prefix = count($array) ? implode(', ', $array) . ', ' : ''; === modified file 'modules/trigger/trigger.module' --- modules/trigger/trigger.module 2008-05-06 12:18:44 +0000 +++ modules/trigger/trigger.module 2008-07-22 07:28:57 +0000 @@ -295,7 +295,8 @@ function trigger_comment($a1, $op) { actions_do($aid, $objects[$action_info['type']], $context); } else { - actions_do($aid, (object) $a1, $context); + $a1 = (object) $a1; + actions_do($aid, $a1, $context); } } } @@ -387,8 +388,9 @@ function trigger_taxonomy($op, $type, $a 'hook' => 'taxonomy', 'op' => $op ); + $_array = (object) $array; foreach ($aids as $aid => $action_info) { - actions_do($aid, (object) $array, $context); + actions_do($aid, $_array, $context); } } === modified file 'modules/user/user.module' --- modules/user/user.module 2008-07-16 21:59:24 +0000 +++ modules/user/user.module 2008-07-22 07:28:58 +0000 @@ -1192,10 +1192,12 @@ function user_set_authmaps($account, $au foreach ($authmaps as $key => $value) { $module = explode('_', $key, 2); if ($value) { - db_query("UPDATE {authmap} SET authname = '%s' WHERE uid = %d AND module = '%s'", $value, $account->uid, $module[1]); - if (!db_affected_rows()) { - db_query("INSERT INTO {authmap} (authname, uid, module) VALUES ('%s', %d, '%s')", $value, $account->uid, $module[1]); - } + db_insert('authmap')->key(array( + 'uid' => $account->uid, + 'module' => $module[1], + ))->fields(array( + 'authname' => $value, + ))->execute(); } else { db_query("DELETE FROM {authmap} WHERE uid = %d AND module = '%s'", $account->uid, $module[1]); === modified file 'modules/user/user.pages.inc' --- modules/user/user.pages.inc 2008-07-16 21:59:24 +0000 +++ modules/user/user.pages.inc 2008-07-22 07:28:58 +0000 @@ -12,7 +12,7 @@ function user_autocomplete($string = '') { $matches = array(); if ($string) { - $result = db_query_range("SELECT name FROM {users} WHERE LOWER(name) LIKE LOWER('%s%%')", $string, 0, 10); + $result = db_query_range("SELECT name FROM {users} WHERE LOWER(name) LIKE LOWER(:name)", array(':name' => $string .'%'), 0, 10); while ($user = db_fetch_object($result)) { $matches[$user->name] = check_plain($user->name); } === modified file 'profiles/default/default.profile' --- profiles/default/default.profile 2008-06-24 21:26:48 +0000 +++ profiles/default/default.profile 2008-07-22 07:28:40 +0000 @@ -136,10 +136,21 @@ function default_profile_tasks(&$task, $ // Create a default vocabulary named "Tags", enabled for the 'article' content type. $description = st('Use tags to group articles on similar topics into categories.'); $help = st('Enter a comma-separated list of words.'); - db_query("INSERT INTO {vocabulary} VALUES (NULL, 'Tags', '%s', '%s', 0, 0, 0, 0, 1, 'taxonomy', 0)", $description, $help); - $vid = db_last_insert_id('vocabulary', 'vid'); - db_query("INSERT INTO {vocabulary_node_types} VALUES (%d, 'article')", $vid); - + + $vid = db_insert('vocabulary')->fields(array( + 'name' => 'Tags', + 'description' => $description, + 'help' => $help, + 'relations' => 0, + 'hierarchy' => 0, + 'multiple' => 0, + 'required' => 0, + 'tags' => 1, + 'module' => 'taxonomy', + 'weight' => 0, + ))->execute(); + db_insert('vocabulary_node_types')->fields(array('vid' => $vid, 'type' => 'article'))->execute(); + // Update the menu router information. menu_rebuild(); } === added directory 'sites/all/modules' === added directory 'sites/all/modules/dbtest' === added file 'sites/all/modules/dbtest/dbtest.info' --- sites/all/modules/dbtest/dbtest.info 1970-01-01 00:00:00 +0000 +++ sites/all/modules/dbtest/dbtest.info 2008-07-22 07:28:45 +0000 @@ -0,0 +1,6 @@ +name = dbtest +description = unit tests for the database system. +core = 7.x +dependencies[] = simpletest +dependencies[] = devel +files[] = dbtest.module === added file 'sites/all/modules/dbtest/dbtest.install' --- sites/all/modules/dbtest/dbtest.install 1970-01-01 00:00:00 +0000 +++ sites/all/modules/dbtest/dbtest.install 2008-07-22 07:28:45 +0000 @@ -0,0 +1,75 @@ + array( + 'id' => array('type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE), + 'name' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''), + 'age' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), + 'job' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => 'Undefined'), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'name' => array('name') + ), + 'indexes' => array( + 'ages' => array('age'), + ), + ); + + // This is an alternate version of the same table that is structured the same + // but has a non-serial Primary Key. + $schema['test_people'] = array( + 'fields' => array( + 'name' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''), + 'age' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), + 'job' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''), + ), + 'primary key' => array('job'), + 'indexes' => array( + 'ages' => array('age'), + ), + ); + + $schema['test_one_blob'] = array( + 'fields' => array( + 'id' => array('type' => 'serial', 'not null' => TRUE), + 'blob1' => array('type' => 'blob'), + ), + 'primary key' => array('id'), + ); + + $schema['test_two_blobs'] = array( + 'fields' => array( + 'id' => array('type' => 'serial', 'not null' => TRUE), + 'blob1' => array('type' => 'blob'), + 'blob2' => array('type' => 'blob'), + ), + 'primary key' => array('id'), + ); + + $schema['test_task'] = array( + 'fields' => array( + 'tid' => array('type' => 'serial', 'not null' => TRUE), + 'pid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), + 'task' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''), + 'priority' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), + ), + 'primary key' => array('tid'), + + ); + + return $schema; +} + +function dbtest_install() { + drupal_install_schema('dbtest'); +} + +function dbtest_uninstall() { + drupal_uninstall_schema('dbtest'); +} \ No newline at end of file === added file 'sites/all/modules/dbtest/dbtest.module' --- sites/all/modules/dbtest/dbtest.module 1970-01-01 00:00:00 +0000 +++ sites/all/modules/dbtest/dbtest.module 2008-07-22 07:28:45 +0000 @@ -0,0 +1,40 @@ +hasTag('db_test_alter_add_range')) { + $query->range(0, 2); + } + + if ($query->hasTag('db_test_alter_remove_range')) { + $query->range(); + } + + if ($query->hasTag('db_test_alter_add_join')) { + $people_alias = $query->join('test', 'people', "test_task.pid=people.id"); + $name_field = $query->addField('name', 'people', 'name'); + $query->condition($people_alias . '.id', 2); + } + + if ($query->hasTag('db_test_alter_change_conditional')) { + $conditions =& $query->conditions(); + $conditions[0]['value'] = 2; + } + + if ($query->hasTag('db_test_alter_change_fields')) { + $fields =& $query->getFields(); + unset($fields['age']); + } + + if ($query->hasTag('db_test_alter_change_expressions')) { + $expressions =& $query->getExpressions(); + $expressions['double_age']['expression'] = 'age*3'; + } + + + +} + === added file 'sites/all/modules/dbtest/dbtest.test' --- sites/all/modules/dbtest/dbtest.test 1970-01-01 00:00:00 +0000 +++ sites/all/modules/dbtest/dbtest.test 2008-07-22 07:28:45 +0000 @@ -0,0 +1,1375 @@ + $data) { + if (db_table_exists($name)) { + db_drop_table($ret, $name); + } + db_create_table($ret, $name, $data); + } + + foreach ($schema as $name => $data) { + $this->assertTrue(db_table_exists($name), "Table {$name} created successfully"); + } + + $this->addSampleData(); + } + catch (Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } + + function tearDown() { + parent::tearDown(); + + $schema['test'] = drupal_get_schema('test'); + $schema['test_people'] = drupal_get_schema('test_people'); + $schema['test_one_blob'] = drupal_get_schema('test_one_blob'); + $schema['test_two_blobs'] = drupal_get_schema('test_two_blobs'); + $schema['test_task'] = drupal_get_schema('test_task'); + + $ret = array(); + + foreach ($schema as $name => $data) { + if (db_table_exists($name)) { + db_drop_table($ret, $name); + } + } + } + + function addSampleData() { + // These are added using db_query(), since we're not trying to test the INSERT operations here, just populate. + + db_query("INSERT INTO {test} (name, age, job) VALUES ('John', 25, 'Singer')"); + $john = db_last_insert_id('test', 'id'); + db_query("INSERT INTO {test} (name, age, job) VALUES ('George', 27, 'Singer')"); + $george = db_last_insert_id('test', 'id'); + db_query("INSERT INTO {test} (name, age, job) VALUES ('Ringo', 28, 'Drummer')"); + $ringo = db_last_insert_id('test', 'id'); + db_query("INSERT INTO {test} (name, age, job) VALUES ('Paul', 26, 'Songwriter')"); + $paul = db_last_insert_id('test', 'id'); + + db_query("INSERT INTO {test_people} (name, age, job) VALUES ('Meredith', 30, 'Speaker')"); + + db_query("INSERT INTO {test_task} (pid, task, priority) VALUES (%d, '%s', %d)", array($john, 'eat', 3)); + db_query("INSERT INTO {test_task} (pid, task, priority) VALUES (%d, '%s', %d)", array($john, 'sleep', 4)); + db_query("INSERT INTO {test_task} (pid, task, priority) VALUES (%d, '%s', %d)", array($john, 'code', 1)); + db_query("INSERT INTO {test_task} (pid, task, priority) VALUES (%d, '%s', %d)", array($george, 'sing', 2)); + db_query("INSERT INTO {test_task} (pid, task, priority) VALUES (%d, '%s', %d)", array($george, 'sleep', 2)); + db_query("INSERT INTO {test_task} (pid, task, priority) VALUES (%d, '%s', %d)", array($paul, 'found new band', 1)); + db_query("INSERT INTO {test_task} (pid, task, priority) VALUES (%d, '%s', %d)", array($paul, 'perform at superbowl', 3)); + + } + +} + +class DatabaseFetchTestCase extends DatabaseTestCase { + + function setUp() { + parent::setUp(); + } + + function getInfo() { + return array( + 'name' => t('Fetch tests'), + 'description' => t('Test the Database system\'s various fetch capabilities.'), + 'group' => t('Database'), + ); + } + + /** + * Confirm that we can fetch a record properly, and that the default + * fetch mode is object. + */ + function testQueryFetchDefault() { + + $records = array(); + $result = db_query("SELECT name FROM {test} WHERE age = :age", array(':age' => 25)); + $this->assertTrue($result instanceof DatabaseStatement, 'Result set is a Drupal statement object.'); + foreach ($result as $record) { + $records[] = $record; + $this->assertTrue(is_object($record), 'Record is an object'); + $this->assertIdentical($record->name, 'John', '25 year old is John'); + } + + $this->assertIdentical(count($records), 1, 'There is only one record'); + } + + /** + * Confirm that we can fetch a record to an object explicitly. + */ + function testQueryFetchObject() { + + $records = array(); + $result = db_query("SELECT name FROM {test} WHERE age = :age", array(':age' => 25), array('fetch' => PDO::FETCH_OBJ)); + foreach ($result as $record) { + $records[] = $record; + $this->assertTrue(is_object($record), 'Record is an object'); + $this->assertIdentical($record->name, 'John', '25 year old is John'); + } + + $this->assertIdentical(count($records), 1, 'There is only one record'); + } + + /** + * Confirm that we can fetch a record to an array associative explicitly. + */ + function testQueryFetchArray() { + + $records = array(); + $result = db_query("SELECT name FROM {test} WHERE age = :age", array(':age' => 25), array('fetch' => PDO::FETCH_ASSOC)); + foreach ($result as $record) { + $records[] = $record; + if ($this->assertTrue(is_array($record), 'Record is an array')) { + $this->assertIdentical($record['name'], 'John', 'Record can be accessed associatively.'); + } + } + + $this->assertIdentical(count($records), 1, 'There is only one record'); + } + + /** + * Confirm that we can fetch a record into a new instance of a custom class. + * + * @see FakeRecord + */ + function testQueryFetchClass() { + + $records = array(); + $result = db_query("SELECT name FROM {test} WHERE age = :age", array(':age' => 25), array('fetch' => 'FakeRecord')); + foreach ($result as $record) { + $records[] = $record; + if ($this->assertTrue($record instanceof FakeRecord, 'Record is an object of class FakeRecord')) { + $this->assertIdentical($record->name, 'John', '25 year old is John'); + } + } + + $this->assertIdentical(count($records), 1, 'There is only one record'); + } + + /** + * Confirm that we can fetch a record into an indexed array explicitly. + */ + function testQueryFetchNum() { + + $records = array(); + $result = db_query("SELECT name FROM {test} WHERE age = :age", array(':age' => 25), array('fetch' => PDO::FETCH_NUM)); + foreach ($result as $record) { + $records[] = $record; + if ($this->assertTrue(is_array($record), 'Record is an array')) { + $this->assertIdentical($record[0], 'John', 'Record can be accessed numerically.'); + } + } + + $this->assertIdentical(count($records), 1, 'There is only one record'); + } + + /** + * Confirm that we can fetch a record into a doubly-keyed array explicitly. + */ + function testQueryFetchBoth() { + + $records = array(); + $result = db_query("SELECT name FROM {test} WHERE age = :age", array(':age' => 25), array('fetch' => PDO::FETCH_BOTH)); + foreach ($result as $record) { + $records[] = $record; + if ($this->assertTrue(is_array($record), 'Record is an array')) { + $this->assertIdentical($record[0], 'John', 'Record can be accessed numerically.'); + $this->assertIdentical($record['name'], 'John', 'Record can be accessed associatively.'); + } + } + + $this->assertIdentical(count($records), 1, 'There is only one record'); + } + + /** + * Confirm that we can fetch an entire column of a result set at once. + */ + function testQueryFetchCol() { + + $records = array(); + $result = db_query("SELECT name FROM {test} WHERE age > :age", array(':age' => 25)); + $column = $result->fetchCol(); + $this->assertIdentical(count($column), 3, 'fetchCol() returns the right number of records'); + + + $result = db_query("SELECT name FROM {test} WHERE age > :age", array(':age' => 25)); + $i = 0; + foreach ($result as $record) { + $this->assertIdentical($record->name, $column[$i++], 'Column matches direct accesss'); + } + + } + +} + +class DatabaseInsertTestCase extends DatabaseTestCase { + + function setUp() { + parent::setUp(); + } + + function getInfo() { + return array( + 'name' => t('Insert tests'), + 'description' => t('Test the Insert query builder.'), + 'group' => t('Database'), + ); + } + + function testSimpleInsert() { + try { + $num_records_before = db_query("SELECT COUNT(*) FROM {test}")->fetchField(); + + $query = db_insert('test'); + $query->fields(array( + 'name' => 'Yoko', + 'age' => '29', + )); + $query->execute(); + + $num_records_after = db_query("SELECT COUNT(*) FROM {test}")->fetchField(); + $this->assertIdentical($num_records_before + 1, (int)$num_records_after, 'Record inserts correctly'); + + $saved_age = db_query("SELECT age FROM {test} WHERE name=:name", array(':name' => 'Yoko'))->fetchField(); + $this->assertIdentical($saved_age, '29', 'Can retrieve after inserting'); + } + catch (Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } + + function testMultiInsert() { + try { + $num_records_before = (int) db_query("SELECT COUNT(*) FROM {test}")->fetchField(); + + $query = db_insert('test'); + $query->fields(array( + 'name' => 'Larry', + 'age' => '30', + )); + + // We should be able to specify values in any order if named. + $query->values(array( + 'age' => '31', + 'name' => 'Curly', + )); + + // We should be able to say "use the field order". + $query->values(array('Moe', '32')); + $query->execute(); + + $num_records_after = (int) db_query("SELECT COUNT(*) FROM {test}")->fetchField(); + $this->assertIdentical($num_records_before + 3, $num_records_after, 'Record inserts correctly'); + $saved_age = db_query("SELECT age FROM {test} WHERE name=:name", array(':name' => 'Larry'))->fetchField(); + $this->assertIdentical($saved_age, '30', 'Can retrieve by name'); + $saved_age = db_query("SELECT age FROM {test} WHERE name=:name", array(':name' => 'Curly'))->fetchField(); + $this->assertIdentical($saved_age, '31', 'Can retrieve by name'); + $saved_age = db_query("SELECT age FROM {test} WHERE name=:name", array(':name' => 'Moe'))->fetchField(); + $this->assertIdentical($saved_age, '32', 'Can retrieve by name'); + } + catch (Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + + } + + function testRepeatedInsert() { + try { + $num_records_before = db_query("SELECT COUNT(*) FROM {test}")->fetchField(); + + $query = db_insert('test'); + + $query->fields(array( + 'name' => 'Larry', + 'age' => '30', + )); + $query->execute(); // This should run the insert, but leave the fields intact. + + // We should be able to specify values in any order if named. + $query->values(array( + 'age' => '31', + 'name' => 'Curly', + )); + $query->execute(); + + // We should be able to say "use the field order". + $query->values(array('Moe', '32')); + $query->execute(); + + $num_records_after = db_query("SELECT COUNT(*) FROM {test}")->fetchField(); + $this->assertIdentical((int) $num_records_before + 3, (int) $num_records_after, 'Record inserts correctly'); + $saved_age = db_query("SELECT age FROM {test} WHERE name=:name", array(':name' => 'Larry'))->fetchField(); + $this->assertIdentical($saved_age, '30', 'Can retrieve by name'); + $saved_age = db_query("SELECT age FROM {test} WHERE name=:name", array(':name' => 'Curly'))->fetchField(); + $this->assertIdentical($saved_age, '31', 'Can retrieve by name'); + $saved_age = db_query("SELECT age FROM {test} WHERE name=:name", array(':name' => 'Moe'))->fetchField(); + $this->assertIdentical($saved_age, '32', 'Can retrieve by name'); + } + catch (Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } + + function testInsertFieldOnlyDefinintion() { + // This is useful for importers, when we want to create a query and define + // its fields once, then loop over a multi-insert execution. + + try { + db_insert('test') + ->fields(array('name', 'age')) + ->values(array('Larry', '30')) + ->values(array('Curly', '31')) + ->values(array('Moe', '32')) + ->execute(); + } + // Barry, what's going on here? :-) --LG + catch (Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + $saved_age = db_query("SELECT age FROM {test} WHERE name=:name", array(':name' => 'Larry'))->fetchField(); + $this->assertIdentical($saved_age, '30', 'Can retrieve after inserting'); + } + + function testInsertLastInsertID() { + try { + $id = db_insert('test')->fields(array( + 'name' => 'Larry', + 'age' => '30', + ))->execute(); + + $this->assertIdentical($id, '5', 'Auto-increment ID returned successfully.'); + } + catch (Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } + +} + +class DatabaseInsertLOBTestCase extends DatabaseTestCase { + + function setUp() { + parent::setUp(); + } + + function getInfo() { + return array( + 'name' => t('Insert tests, LOB fields'), + 'description' => t('Test the Insert query builder with LOB fields.'), + 'group' => t('Database'), + ); + } + + function testInsertOneBlob() { + $data = "This is\000a test."; + $this->assertTrue(strlen($data) === 15, "Test data contains a NULL (".strlen($data).")"); + $id = db_insert('test_one_blob')->fields(array('blob1' => $data))->execute(); + $res = db_query('SELECT * FROM {test_one_blob} WHERE id=%d', $id); + $r = db_fetch_array($res); + $this->assertTrue($r['blob1'] === $data, "Can insert a blob: id $id, ".serialize($r)); + } + + function testInsertMultipleBlob() { + + $id = db_insert('test_two_blobs')->fields(array('blob1' => 'This is', 'blob2' => 'a test'))->execute(); + $res = db_query('SELECT * FROM {test_two_blobs} WHERE id=%d', $id); + $r = db_fetch_array($res); + $this->assertTrue($r['blob1'] === 'This is' && $r['blob2'] === 'a test', + 'Can insert multiple blobs per row'); + } +} + +class DatabaseInsertDefaultsTestCase extends DatabaseTestCase { + + function setUp() { + parent::setUp(); + } + + function getInfo() { + return array( + 'name' => t('Insert tests, Default fields'), + 'description' => t('Test the Insert query builder with default values.'), + 'group' => t('Database'), + ); + } + + function testDefaultInsert() { + try { + $query = db_insert('test')->useDefaults(array('job')); + $id = $query->execute(); + + $schema = drupal_get_schema('test'); + + $job = db_query("SELECT job FROM {test} WHERE id=:id", array(':id' => $id))->fetchField(); + $this->assertEqual($job, $schema['fields']['job']['default'], 'Default field value is set.'); + } + catch (Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } + + function testDefaultInsertWithFields() { + try { + $query = db_insert('test')->fields(array('name' => 'Bob'))->useDefaults(array('job')); + $id = $query->execute(); + + $schema = drupal_get_schema('test'); + + $job = db_query("SELECT job FROM {test} WHERE id=:id", array(':id' => $id))->fetchField(); + $this->assertEqual($job, $schema['fields']['job']['default'], 'Default field value is set.'); + } + catch (Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } +} + +class DatabaseUpdateTestCase extends DatabaseTestCase { + + function setUp() { + parent::setUp(); + } + + function getInfo() { + return array( + 'name' => t('Update tests'), + 'description' => t('Test the Update query builder.'), + 'group' => t('Database'), + ); + } + + /** + * Confirm that we can update a single record successfully. + */ + function testSimpleUpdate() { + + $num_updated = db_update('test')->fields(array('name' => 'Tiffany'))->condition('id', 1)->execute(); + $this->assertIdentical($num_updated, 1, 'Updated 1 record'); + + $saved_name = db_query("SELECT name FROM {test} WHERE id=:id", array(':id' => 1))->fetchField(); + $this->assertIdentical($saved_name, 'Tiffany', 'Updated name successfully'); + } + + /** + * Confirm that we can update a multiple records successfully. + */ + function testMultiUpdate() { + $num_updated = db_update('test')->fields(array('job' => 'Musician'))->condition('job', 'Singer')->execute(); + $this->assertIdentical($num_updated, 2, 'Updated 2 records'); + + $num_matches = db_query("SELECT count(*) FROM {test} WHERE job = :job", array(':job' => 'Musician'))->fetchField(); + $this->assertIdentical($num_matches, '2', 'Updated fields successfully'); + } + + /** + * Confirm that we can update a multiple records with a non-equality condition. + */ + function testMultiGTUpdate() { + $num_updated = db_update('test')->fields(array('job' => 'Musician'))->condition('age', 26, '>')->execute(); + $this->assertIdentical($num_updated, 2, 'Updated 2 records'); + + $num_matches = db_query("SELECT count(*) FROM {test} WHERE job = :job", array(':job' => 'Musician'))->fetchField(); + $this->assertIdentical($num_matches, '2', 'Updated fields successfully'); + } + + /** + * Confirm that we can update a multiple records with a where call. + */ + function testWhereUpdate() { + $num_updated = db_update('test')->fields(array('job' => 'Musician'))->where('age > :age', array(':age' => 26))->execute(); + $this->assertIdentical($num_updated, 2, 'Updated 2 records'); + + $num_matches = db_query("SELECT count(*) FROM {test} WHERE job = :job", array(':job' => 'Musician'))->fetchField(); + $this->assertIdentical($num_matches, '2', 'Updated fields successfully'); + } + + /** + * Confirm that we can stack condition and where calls. + */ + function testWhereAndConditionUpdate() { + $update = db_update('test')->fields(array('job' => 'Musician'))->where('age > :age', array(':age' => 26))->condition('name', 'Ringo'); + $num_updated = $update->execute(); + $this->assertIdentical($num_updated, 1, 'Updated 1 record'); + + $num_matches = db_query("SELECT count(*) FROM {test} WHERE job = :job", array(':job' => 'Musician'))->fetchField(); + $this->assertIdentical($num_matches, '1', 'Updated fields successfully'); + } + +} + +class DatabaseUpdateComplexTestCase extends DatabaseTestCase { + + function setUp() { + parent::setUp(); + } + + function getInfo() { + return array( + 'name' => t('Update tests, Complex'), + 'description' => t('Test the Update query builder, complex queries.'), + 'group' => t('Database'), + ); + } + + function testOrConditionUpdate() { + $update = db_update('test')->fields(array('job' => 'Musician')) + ->condition(db_or()->condition('name', 'John')->condition('name', 'Paul')); + $num_updated = $update->execute(); + $this->assertIdentical($num_updated, 2, 'Updated 2 records'); + + $num_matches = db_query("SELECT count(*) FROM {test} WHERE job = :job", array(':job' => 'Musician'))->fetchField(); + $this->assertIdentical($num_matches, '2', 'Updated fields successfully'); + } + + function testInConditionUpdate() { + $num_updated = db_update('test')->fields(array('job' => 'Musician')) + ->condition('name', array('John', 'Paul'), 'IN') + ->execute(); + $this->assertIdentical($num_updated, 2, 'Updated 2 records'); + + $num_matches = db_query("SELECT count(*) FROM {test} WHERE job = :job", array(':job' => 'Musician'))->fetchField(); + $this->assertIdentical($num_matches, '2', 'Updated fields successfully'); + } + + function testNotInConditionUpdate() { + $num_updated = db_update('test')->fields(array('job' => 'Musician')) + ->condition('name', array('John', 'Paul', 'George'), 'NOT IN') + ->execute(); + $this->assertIdentical($num_updated, 1, 'Updated 1 record'); + + $num_matches = db_query("SELECT count(*) FROM {test} WHERE job = :job", array(':job' => 'Musician'))->fetchField(); + $this->assertIdentical($num_matches, '1', 'Updated fields successfully'); + } + + function testBetweenConditionUpdate() { + try{ + $num_updated = db_update('test')->fields(array('job' => 'Musician')) + ->condition('age', array(25, 26), 'BETWEEN') + ->execute(); + $this->assertIdentical($num_updated, 2, 'Updated 2 records'); + + $num_matches = db_query("SELECT count(*) FROM {test} WHERE job = :job", array(':job' => 'Musician'))->fetchField(); + $this->assertIdentical($num_matches, '2', 'Updated fields successfully'); + } + catch (Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } + + function testLikeConditionUpdate() { + $num_updated = db_update('test')->fields(array('job' => 'Musician')) + ->condition('name', '%ge%', 'LIKE') + ->execute(); + $this->assertIdentical($num_updated, 1, 'Updated 1 record'); + + $num_matches = db_query("SELECT count(*) FROM {test} WHERE job = :job", array(':job' => 'Musician'))->fetchField(); + $this->assertIdentical($num_matches, '1', 'Updated fields successfully'); + } + + function testUpdateExpression() { + try { + $before_age = db_query("SELECT age FROM {test} WHERE name='Ringo'")->fetchField(); + $GLOBALS['larry_test'] = 1; + $num_updated = db_update('test') + ->condition('name', 'Ringo') + ->fields(array('job' => 'Musician')) + ->expression('age', 'age+:age',array(':age' => 4)) + ->execute(); + $this->assertIdentical($num_updated, 1, 'Updated 1 record'); + + $num_matches = db_query("SELECT count(*) FROM {test} WHERE job = :job", array(':job' => 'Musician'))->fetchField(); + $this->assertIdentical($num_matches, '1', 'Updated fields successfully'); + + $person = db_query("SELECT * FROM {test} WHERE name=:name", array(':name' => 'Ringo'))->fetch(); + $this->assertEqual($person->name, 'Ringo', 'Name set correctly'); + $this->assertEqual($person->age, $before_age + 4, 'Age set correctly'); + $this->assertEqual($person->job, 'Musician', 'Job set correctly'); + $GLOBALS['larry_test'] = 0; + } + catch (Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } +} + +class DatabaseUpdateLOBTestCase extends DatabaseTestCase { + + function setUp() { + parent::setUp(); + } + + function getInfo() { + return array( + 'name' => t('Update tests, LOB'), + 'description' => t('Test the Update query builder with LOB fields.'), + 'group' => t('Database'), + ); + } + + /** + * Confirm that we can update a blob column. + */ + function testUpdateOneBlob() { + $data = "This is\000a test."; + $this->assertTrue(strlen($data) === 15, "Test data contains a NULL (".strlen($data).")"); + $id = db_insert('test_one_blob')->fields(array('blob1' => $data))->execute(); + + $data .= $data; + db_update('test_one_blob')->condition('id', $id)->fields(array('blob1' => $data))->execute(); + + $res = db_query('SELECT * FROM {test_one_blob} WHERE id=%d', $id); + $r = db_fetch_array($res); + $this->assertTrue($r['blob1'] === $data, "Can update a blob: id $id, ".serialize($r)); + } + + /** + * Confirm that we can update two blob columns in the same + * table. + */ + function testUpdateMultipleBlob() { + $id = db_insert('test_two_blobs')->fields(array('blob1' => 'This is', 'blob2' => 'a test'))->execute(); + + db_update('test_two_blobs')->condition('id', $id)->fields(array('blob1' => 'and so', 'blob2' => 'is this'))->execute(); + + $res = db_query('SELECT * FROM {test_two_blobs} WHERE id=%d', $id); + $r = db_fetch_array($res); + $this->assertTrue($r['blob1'] === 'and so' && $r['blob2'] === 'is this', + 'Can update multiple blobs per row'); + } +} + +/** + * Delete tests. + * + * The DELETE tests are not as extensive, as all of the interesting code for + * DELETE queries is in the conditional which is identical to the UPDATE and + * SELECT conditional handling. + * + */ +class DatabaseDeleteTestCase extends DatabaseTestCase { + + function setUp() { + parent::setUp(); + } + + function getInfo() { + return array( + 'name' => t('Delete tests'), + 'description' => t('Test the Delete query builder.'), + 'group' => t('Database'), + ); + } + + /** + * Confirm that we can delete a single record successfully. + */ + function testSimpleDelete() { + + $num_records_before = db_query("SELECT COUNT(*) FROM {test}")->fetchField(); + + $num_deleted = db_delete('test')->condition('id', 1)->execute(); + $this->assertIdentical($num_deleted, 1, 'Deleted 1 record'); + + $num_records_after = db_query("SELECT COUNT(*) FROM {test}")->fetchField(); + $this->assertEqual($num_records_before, $num_records_after + $num_deleted, 'Deletion adds up'); + } +} + +class DatabaseMergeTestCase extends DatabaseTestCase { + + function setUp() { + parent::setUp(); + } + + function getInfo() { + return array( + 'name' => t('Merge tests'), + 'description' => t('Test the Merge query builder.'), + 'group' => t('Database'), + ); + } + + /** + * Confirm that we can merge-insert a record successfully. + */ + function testMergeInsert() { + try{ + $num_records_before = db_query("SELECT COUNT(*) FROM {test_people}")->fetchField(); + + db_merge('test_people')->key(array('job' => 'Presenter'))->fields(array('age' => 31, 'name' => 'Tiffany'))->execute(); + + $num_records_after = db_query("SELECT COUNT(*) FROM {test_people}")->fetchField(); + $this->assertEqual($num_records_before + 1, $num_records_after, 'Merge inserted properly'); + + $person = db_query("SELECT * FROM {test_people} WHERE job=:job", array(':job' => 'Presenter'))->fetch(); + $this->assertEqual($person->name, 'Tiffany', 'Name set correctly'); + $this->assertEqual($person->age, 31, 'Age set correctly'); + $this->assertEqual($person->job, 'Presenter', 'Job set correctly'); + } + catch(Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } + + /** + * Confirm that we can merge-update a record successfully. + */ + function testMergeUpdate() { + $num_records_before = db_query("SELECT COUNT(*) FROM {test_people}")->fetchField(); + + db_merge('test_people')->key(array('job' => 'Speaker'))->fields(array('age' => 31, 'name' => 'Tiffany'))->execute(); + + $num_records_after = db_query("SELECT COUNT(*) FROM {test_people}")->fetchField(); + $this->assertEqual($num_records_before, $num_records_after, 'Merge updated properly'); + + $person = db_query("SELECT * FROM {test_people} WHERE job=:job", array(':job' => 'Speaker'))->fetch(); + $this->assertEqual($person->name, 'Tiffany', 'Name set correctly'); + $this->assertEqual($person->age, 31, 'Age set correctly'); + $this->assertEqual($person->job, 'Speaker', 'Job set correctly'); + } + + /** + * Confirm that we can merge-update a record successfully, with exclusion. + */ + function testMergeUpdateExcept() { + $num_records_before = db_query("SELECT COUNT(*) FROM {test_people}")->fetchField(); + + db_merge('test_people')->key(array('job' => 'Speaker'))->fields(array('age' => 31, 'name' => 'Tiffany'))->updateExcept('age')->execute(); + + $num_records_after = db_query("SELECT COUNT(*) FROM {test_people}")->fetchField(); + $this->assertEqual($num_records_before, $num_records_after, 'Merge updated properly'); + + $person = db_query("SELECT * FROM {test_people} WHERE job=:job", array(':job' => 'Speaker'))->fetch(); + $this->assertEqual($person->name, 'Tiffany', 'Name set correctly'); + $this->assertEqual($person->age, 30, 'Age skipped correctly'); + $this->assertEqual($person->job, 'Speaker', 'Job set correctly'); + } + + /** + * Confirm that we can merge-update a record successfully, with alternate replacement. + */ + function testMergeUpdateExplicit() { + $num_records_before = db_query("SELECT COUNT(*) FROM {test_people}")->fetchField(); + + db_merge('test_people')->key(array('job' => 'Speaker'))->fields(array('age' => 31, 'name' => 'Tiffany'))->update(array('name' => 'Joe'))->execute(); + + $num_records_after = db_query("SELECT COUNT(*) FROM {test_people}")->fetchField(); + $this->assertEqual($num_records_before, $num_records_after, 'Merge updated properly'); + + $person = db_query("SELECT * FROM {test_people} WHERE job=:job", array(':job' => 'Speaker'))->fetch(); + $this->assertEqual($person->name, 'Joe', 'Name set correctly'); + $this->assertEqual($person->age, 30, 'Age skipped correctly'); + $this->assertEqual($person->job, 'Speaker', 'Job set correctly'); + } + + /** + * Confirm that we can merge-update a record successfully, with expressions. + */ + function testMergeUpdateExpression() { + $num_records_before = db_query("SELECT COUNT(*) FROM {test_people}")->fetchField(); + + $age_before = db_query("SELECT age FROM {test_people} WHERE job='Speaker'")->fetchField(); + + // This is a very contrived example, as I have no idea why you'd want to change + // age this way, but that's beside the point. + // Note that we are also double-setting age here, once as a literal and + // once as an expression. This test will only pass if the expression wins, + // which is what is supposed to happen. + db_merge('test_people')->key(array('job' => 'Speaker'))->fields(array('age' => 31, 'name' => 'Tiffany'))->expression('age', 'age+:age', array(':age' => 4))->execute(); + + $num_records_after = db_query("SELECT COUNT(*) FROM {test_people}")->fetchField(); + $this->assertEqual($num_records_before, $num_records_after, 'Merge updated properly'); + + $person = db_query("SELECT * FROM {test_people} WHERE job=:job", array(':job' => 'Speaker'))->fetch(); + $this->assertEqual($person->name, 'Tiffany', 'Name set correctly'); + $this->assertEqual($person->age, $age_before + 4, 'Age updated correctly'); + $this->assertEqual($person->job, 'Speaker', 'Job set correctly'); + } +} + +class DatabaseSelectTestCase extends DatabaseTestCase { + + function setUp() { + parent::setUp(); + } + + function getInfo() { + return array( + 'name' => t('Select tests'), + 'description' => t('Test the Select query builder.'), + 'group' => t('Database'), + ); + } + + function testSimpleSelect() { + try { + $query = db_select('test'); + $name_field = $query->addField('name', 'test'); + $age_field = $query->addField('age', 'test', 'age'); + $result = $query->execute(); + + $num_records = 0; + foreach ($result as $record) { + $num_records++; + } + + $this->assertEqual($num_records, 4, "Returned the correct number of rows"); + } + catch(Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } + + function testSimpleSelectConditional() { + + $query = db_select('test'); + $name_field = $query->addField('name', 'test'); + $age_field = $query->addField('age', 'test', 'age'); + $query->condition('age', 27); + $result = $query->execute(); + + // Check that the aliases are being created the way we want. + $this->assertEqual($name_field, 'test_name', "Name field alias is correct."); + $this->assertEqual($age_field, 'age', "Age field alias is correct."); + + // Ensure that we got the right record. + $record = $result->fetch(); + $this->assertEqual($record->$name_field, 'George', "Fetched name is correct."); + $this->assertEqual($record->$age_field, 27, "Fetched age is correct."); + } + + function testSimpleSelectExpression() { + + $query = db_select('test'); + $name_field = $query->addField('name', 'test'); + $age_field = $query->addExpression("age*2", 'double_age'); + $query->condition('age', 27); + $result = $query->execute(); + + // Check that the aliases are being created the way we want. + $this->assertEqual($name_field, 'test_name', "Name field alias is correct."); + $this->assertEqual($age_field, 'double_age', "Age field alias is correct."); + + // Ensure that we got the right record. + $record = $result->fetch(); + $this->assertEqual($record->$name_field, 'George', "Fetched name is correct."); + $this->assertEqual($record->$age_field, 27*2, "Fetched age expression is correct."); + } + + function testSimpleSelectOrdered() { + try { + $query = db_select('test'); + $name_field = $query->addField('name', 'test'); + $age_field = $query->addField('age', 'test', 'age'); + $query->orderBy($age_field); + $result = $query->execute(); + + $num_records = 0; + $last_age = 0; + foreach ($result as $record) { + $num_records++; + $this->assertTrue($record->age >= $last_age, "Results returned in correct order."); + $last_age = $record->age; + } + + $this->assertEqual($num_records, 4, "Returned the correct number of rows."); + } + catch(Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } + + function testSimpleSelectOrderedDesc() { + try { + $query = db_select('test'); + $name_field = $query->addField('name', 'test'); + $age_field = $query->addField('age', 'test', 'age'); + $query->orderBy($age_field, 'DESC'); + $result = $query->execute(); + + $num_records = 0; + $last_age = 100000000; + foreach ($result as $record) { + $num_records++; + $this->assertTrue($record->age <= $last_age, "Results returned in correct order."); + $last_age = $record->age; + } + + $this->assertEqual($num_records, 4, "Returned the correct number of rows."); + } + catch(Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } +} + +class DatabaseSelectComplexTestCase extends DatabaseTestCase { + + function setUp() { + parent::setUp(); + } + + function getInfo() { + return array( + 'name' => t('Select tests, Complex'), + 'description' => t('Test the Select query builder with more complex queries.'), + 'group' => t('Database'), + ); + } + + function testDefaultJoin() { + try { + $query = db_select('test_task', 't'); + $people_alias = $query->join('test', 'p', 't.pid=p.id'); + $name_field = $query->addField('name', $people_alias, 'name'); + $task_field = $query->addField('task', 't', 'task'); + $priority_field = $query->addField('priority', 't', 'priority'); + + $query->orderBy($priority_field); + $result = $query->execute(); + + $num_records = 0; + $last_priority = 0; + foreach ($result as $record) { + $num_records++; + $this->assertTrue($record->$priority_field >= $last_priority, "Results returned in correct order."); + $this->assertNotEqual($record->$name_field, 'Ringo', 'Taskless person not selected.'); + $last_priority = $record->$priority_field; + } + + $this->assertEqual($num_records, 7, "Returned the correct number of rows."); + } + catch(Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } + + function testLeftOuterJoin() { + try { + $query = db_select('test', 'p'); + $people_alias = $query->leftJoin('test_task', 't', 't.pid=p.id'); + $name_field = $query->addField('name', 'p', 'name'); + $task_field = $query->addField('task', $people_alias, 'task'); + $priority_field = $query->addField('priority', $people_alias, 'priority'); + + $query->orderBy($name_field); + $result = $query->execute(); + + $num_records = 0; + $last_name = 0; + + foreach ($result as $record) { + $num_records++; + $this->assertTrue(strcmp($record->$name_field, $last_name) >= 0, "Results returned in correct order."); + $last_priority = $record->$name_field; + } + + $this->assertEqual($num_records, 8, "Returned the correct number of rows."); + } + catch(Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } + + function testGroupBy() { + try { + $query = db_select('test_task', 't'); + $count_field = $query->addExpression('COUNT(task)', 'num'); + $task_field = $query->addField('task', 't'); + $query->orderBy($count_field); + $query->groupBy($task_field); + $result = $query->execute(); + + $num_records = 0; + $last_count = 0; + $records = array(); + foreach ($result as $record) { + $num_records++; + $this->assertTrue($record->$count_field >= $last_count, "Results returned in correct order."); + $last_count = $record->$count_field; + $records[$record->$task_field] = $record->$count_field; + } + + $correct_results = array( + 'eat' => 1, + 'sleep' => 2, + 'code' => 1, + 'found new band' => 1, + 'perform at superbowl' => 1, + ); + + foreach ($correct_results as $task => $count) { + $this->assertEqual($records[$task], $count, "Correct number of '$task' records found."); + } + + $this->assertEqual($num_records, 6, "Returned the correct number of total rows."); + } + catch(Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } + + function testGroupByAndHaving() { + try { + $query = db_select('test_task', 't'); + $count_field = $query->addExpression('COUNT(task)', 'num'); + $task_field = $query->addField('task', 't'); + $query->orderBy($count_field); + $query->groupBy($task_field); + $query->havingCondition('COUNT(task)', 2, '>='); + $result = $query->execute(); + + $num_records = 0; + $last_count = 0; + $records = array(); + foreach ($result as $record) { + $num_records++; + $this->assertTrue($record->$count_field >= 2, "Record has the minimum count."); + $this->assertTrue($record->$count_field >= $last_count, "Results returned in correct order."); + $last_count = $record->$count_field; + $records[$record->$task_field] = $record->$count_field; + } + + $correct_results = array( + 'sleep' => 2, + ); + + foreach ($correct_results as $task => $count) { + $this->assertEqual($records[$task], $count, "Correct number of '$task' records found."); + } + + $this->assertEqual($num_records, 1, "Returned the correct number of total rows."); + } + catch(Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } + + function testRange() { + try { + $query = db_select('test'); + $name_field = $query->addField('name', 'test'); + $age_field = $query->addField('age', 'test', 'age'); + $query->range(0, 2); + $result = $query->execute(); + + $num_records = 0; + foreach ($result as $record) { + $num_records++; + } + + $this->assertEqual($num_records, 2, "Returned the correct number of rows"); + } + catch(Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } + + function testDistinct() { + try { + $query = db_select('test_task'); + $task_field = $query->addField('task', 'test_task'); + $query->distinct(); + $result = $query->execute(); + + $num_records = 0; + foreach ($result as $record) { + $num_records++; + } + + $this->assertEqual($num_records, 6, "Returned the correct number of rows"); + } + catch(Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } + + function testCountQuery() { + try { + $query = db_select('test'); + $name_field = $query->addField('name', 'test'); + $age_field = $query->addField('age', 'test', 'age'); + $query->orderBy('name'); + + $count = $query->countQuery()->execute()->fetchField(); + + $this->assertEqual($count, 4, "Counted the correct number of records."); + + // Now make sure we didn't break the original query! We should still have + // all of the fields we asked for. + $record = $query->execute()->fetch(); + $this->assertEqual($record->$name_field, 'George', "Correct data retrieved"); + $this->assertEqual($record->$age_field, 27, "Correct data retrieved"); + } + catch(Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } +} + +/** + * Select tagging tests. + */ +class DatabaseTaggingTestCase extends DatabaseTestCase { + + function setUp() { + parent::setUp(); + } + + function getInfo() { + return array( + 'name' => t('Query tagging tests'), + 'description' => t('Test the tagging capabilities of the Select builder.'), + 'group' => t('Database'), + ); + } + + function testHasTag() { + try { + $query = db_select('test'); + $query->addField('name', 'test'); + $query->addField('age', 'test', 'age'); + + $query->addTag('test'); + + $this->assertTrue($query->hasTag('test'), "hasTag() returned true."); + $this->assertFalse($query->hasTag('other'), "hasTag() returned false."); + } + catch(Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } + + function testHasAllTags() { + try { + $query = db_select('test'); + $query->addField('name', 'test'); + $query->addField('age', 'test', 'age'); + + $query->addTag('test'); + $query->addTag('other'); + + $this->assertTrue($query->hasAllTags('test', 'other'), "hasAllTags() returned true."); + $this->assertFalse($query->hasAllTags('test', 'stuff'), "hasAllTags() returned false."); + } + catch(Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } + + function testHasAnyTag() { + try { + $query = db_select('test'); + $query->addField('name', 'test'); + $query->addField('age', 'test', 'age'); + + $query->addTag('test'); + + $this->assertTrue($query->hasAnyTag('test', 'other'), "hasAnyTag() returned true."); + $this->assertFalse($query->hasAnyTag('other', 'stuff'), "hasAnyTag() returned false."); + } + catch(Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } + + function testMetaData() { + try { + $query = db_select('test'); + $query->addField('name', 'test'); + $query->addField('age', 'test', 'age'); + + $data = array( + 'a' => 'A', + 'b' => 'B', + ); + + $query->addMetaData('test', $data); + + $return = $query->getMetaData('test'); + $this->assertEqual($data, $return, "Corect metadata returned."); + + $return = $query->getMetaData('nothere'); + $this->assertNull($return, "Non-existant key returned NULL"); + } + catch(Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } +} + +/** + * Select alter tests. + */ +class DatabaseAlterTestCase extends DatabaseTestCase { + + function setUp() { + parent::setUp(); + } + + function getInfo() { + return array( + 'name' => t('Query altering tests'), + 'description' => t('Test the hook_query_alter capabilities of the Select builder.'), + 'group' => t('Database'), + ); + } + + + function testSimpleAlter() { + try { + $query = db_select('test'); + $query->addField('name', 'test'); + $query->addField('age', 'test', 'age'); + $query->addTag('db_test_alter_add_range'); + + $result = $query->execute(); + + $num_records = 0; + foreach ($result as $record) { + $num_records++; + } + + $this->assertEqual($num_records, 2, "Returned the correct number of rows"); + + } + catch(Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } + + function testAlterWithJoin() { + try { + $query = db_select('test_task'); + $tid_field = $query->addField('tid', 'test_task'); + $task_field = $query->addField('task', 'test_task'); + $query->orderBy($task_field); + $query->addTag('db_test_alter_add_join'); + + $result = $query->execute(); + + $records = $result->fetchAll(); + + $this->assertEqual(count($records), 2, "Returned the correct number of rows"); + + $this->assertEqual($records[0]->name, 'George', "Correct data retrieved"); + $this->assertEqual($records[0]->$tid_field, 4, "Correct data retrieved"); + $this->assertEqual($records[0]->$task_field, 'sing', "Correct data retrieved"); + $this->assertEqual($records[1]->name, 'George', "Correct data retrieved"); + $this->assertEqual($records[1]->$tid_field, 5, "Correct data retrieved"); + $this->assertEqual($records[1]->$task_field, 'sleep', "Correct data retrieved"); + } + catch(Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } + + function testAlterChangeConditional() { + try { + $query = db_select('test_task'); + $tid_field = $query->addField('tid', 'test_task'); + $pid_field = $query->addField('pid', 'test_task'); + $task_field = $query->addField('task', 'test_task'); + $people_alias = $query->join('test', 'people', "test_task.pid=people.id"); + $name_field = $query->addField('name', 'people', 'name'); + $query->condition('test_task.tid', '1'); + $query->orderBy($tid_field); + $query->addTag('db_test_alter_change_conditional'); + + $result = $query->execute(); + + $records = $result->fetchAll(); + + $this->assertEqual(count($records), 1, "Returned the correct number of rows"); + + $this->assertEqual($records[0]->$name_field, 'John', "Correct data retrieved"); + $this->assertEqual($records[0]->$tid_field, 2, "Correct data retrieved"); + $this->assertEqual($records[0]->$pid_field, 1, "Correct data retrieved"); + $this->assertEqual($records[0]->$task_field, 'sleep', "Correct data retrieved"); + } + catch(Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } + + function testAlterChangeFields() { + try { + $query = db_select('test'); + $name_field = $query->addField('name', 'test'); + $age_field = $query->addField('age', 'test', 'age'); + $query->orderBy('name'); + $query->addTag('db_test_alter_change_fields'); + + $record = $query->execute()->fetch(); + $this->assertEqual($record->$name_field, 'George', "Correct data retrieved"); + $this->assertNull($record->$age_field, 'Age field not found, as intended.'); + } + catch(Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } + + function testAlterExpression() { + try { + $query = db_select('test'); + $name_field = $query->addField('name', 'test'); + $age_field = $query->addExpression("age*2", 'double_age'); + $query->condition('age', 27); + $query->addTag('db_test_alter_change_expressions'); + $result = $query->execute(); + + // Ensure that we got the right record. + $record = $result->fetch(); + + $this->assertEqual($record->$name_field, 'George', "Fetched name is correct."); + $this->assertEqual($record->$age_field, 27*3, "Fetched age expression is correct."); + } + catch(Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } + + function testAlterRemoveRange() { + try { + $query = db_select('test'); + $query->addField('name', 'test'); + $query->addField('age', 'test', 'age'); + $query->range(0, 2); + $query->addTag('db_test_alter_remove_range'); + + $num_records = count($query->execute()->fetchAll()); + + $this->assertEqual($num_records, 4, "Returned the correct number of rows"); + + } + catch(Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } +} + === modified file 'sites/default/default.settings.php' (properties changed) --- sites/default/default.settings.php 2008-07-19 12:50:56 +0000 +++ sites/default/default.settings.php 2008-07-22 07:28:43 +0000 @@ -46,20 +46,76 @@ /** * Database settings: * - * Note that the $db_url variable gets parsed using PHP's built-in - * URL parser (i.e. using the "parse_url()" function) so make sure - * not to confuse the parser. If your username, password - * or database name contain characters used to delineate - * $db_url parts, you can escape them via URI hex encodings: - * - * : = %3a / = %2f @ = %40 - * + = %2b ( = %28 ) = %29 - * ? = %3f = = %3d & = %26 - * - * To specify multiple connections to be used in your site (i.e. for - * complex custom modules) you can also specify an associative array - * of $db_url variables with the 'default' element used until otherwise - * requested. + * The $databases array specifies the database connection or + * connections that Drupal may use. Drupal is able to connect + * to multiple databases, including multiple types of databases, + * during the same request. + * + * Each database connection is specified as an array of settings, + * similar to the following: + * + * array( + * 'driver' => 'mysql', + * 'database' => 'databasename', + * 'username' => 'username', + * 'password' => 'password', + * 'host' => 'localhost', + * ); + * + * The "driver" property indicates what Drupal database driver the + * connection should use. This is usually the same as the name of the + * database type, such as mysql or sqlite, but not always. The other + * properties will vary depending on the driver. For SQLite, you must + * specify a database. For most other drivers, you must specify a username, + * password, host, and database name. + * + * Some database engines support transactions. In order to enable + * transaction support for a given database, set the 'transaction' key + * to TRUE. To disable it, set it to FALSE. Note that the default value + * varies by driver. For MySQL, the default is FALSE since MyISAM tables + * do not support transactions. + * + * For each database, you may optionally specify multiple "target" databases. + * A target database allows Drupal to try to send certain queries to a + * different database if it can but fall back to the default connection if not. + * That is useful for master/slave replication, as Drupal may try to connect + * to a slave server when appropriate and if one is not available will simply + * fall back to the single master server. + * + * The general format for the $databases array is as follows: + * + * $databases['default']['default'] = $info_array; + * $databases['default']['slave'][] = $info_array; + * $databases['default']['slave'][] = $info_array; + * $databases['extra'] = $info_array; + * + * In the above example, $info_array is an array of settings described above. + * The first line sets a "default" database that has one master database + * (the second level default). The second and third lines create an array + * of potential slave databases. Drupal will select one at random for a given + * request as needed. The fourth line creates a new database with a name of + * "extra". Since no target is specified, it is assumed to be "default", that + * is, only one server. + * + * For a single database configuration, the following is sufficient: + * + * $databases = array( + * 'driver' => 'mysql', + * 'database' => 'databasename', + * 'username' => 'username', + * 'password' => 'password', + * 'host' => 'localhost', + * ); + * + * That is equivalent to: + * + * $databases['default']['default'] = array( + * 'driver' => 'mysql', + * 'database' => 'databasename', + * 'username' => 'username', + * 'password' => 'password', + * 'host' => 'localhost', + * ); * * You can optionally set prefixes for some or all database table names * by using the $db_prefix setting. If a prefix is specified, the table @@ -85,12 +141,27 @@ * 'sequences' => 'shared_', * ); * - * Database URL format: - * $db_url = 'mysql://username:password@localhost/databasename'; - * $db_url = 'mysqli://username:password@localhost/databasename'; - * $db_url = 'pgsql://username:password@localhost/databasename'; + * Database configuration format: + * $databases = array( + * 'driver' => 'mysql', + * 'database' => 'databasename', + * 'username' => 'username', + * 'password' => 'password', + * 'host' => 'localhost', + * ); + * $databases = array( + * 'driver' => 'pgsql', + * 'database' => 'databasename', + * 'username' => 'username', + * 'password' => 'password', + * 'host' => 'localhost', + * ); + * $databases = array( + * 'driver' => 'sqlite', + * 'database' => 'databasefilename', + * ); */ -$db_url = ''; +$databases = array(); $db_prefix = ''; /**