#195416: Table prefixes are now per database connection, update SimpleTest to deal with this. From: Damien Tournoud --- bootstrap.inc | 37 ++++++++++++ common.inc | 6 +- database/database.inc | 107 ++++++++++++++++++++++++++--------- database/mysql/database.inc | 2 + database/pgsql/database.inc | 2 + database/sqlite/database.inc | 2 + install.php | 23 +++----- simpletest/drupal_web_test_case.php | 104 ++++++++++++++++------------------ default/default.settings.php | 18 +++--- 9 files changed, 192 insertions(+), 109 deletions(-) diff --git includes/bootstrap.inc includes/bootstrap.inc index 280a56a..f814b24 100644 --- includes/bootstrap.inc +++ includes/bootstrap.inc @@ -457,7 +457,7 @@ function conf_init() { global $base_url, $base_path, $base_root; // Export the following settings.php variables to the global namespace - global $databases, $db_prefix, $cookie_domain, $conf, $installed_profile, $update_free_access; + global $databases, $cookie_domain, $conf, $installed_profile, $update_free_access; $conf = array(); if (file_exists(DRUPAL_ROOT . '/' . conf_path() . '/settings.php')) { @@ -527,6 +527,41 @@ function conf_init() { ini_set('session.cookie_domain', $cookie_domain); } session_name('SESS' . md5($session_name)); + + // Use the simpletest database prefix passed by the user agent header. + if (preg_match("/^simpletest\d+$/", $_SERVER['HTTP_USER_AGENT'])) { + // Set the test run id for use in other parts of Drupal. + drupal_test_info(array('test_run_id' => $_SERVER['HTTP_USER_AGENT'], 'in_child_site' => TRUE)); + + foreach ($databases['default'] as $target => $value) { + // Extract the current default database prefix. + if (empty($value['prefix'])) { + $current_prefix = ''; + } + else if (is_array($value['prefix'])) { + $current_prefix = $value['prefix']['default']; + } + else { + $current_prefix = $value['prefix']; + } + + // Remove the current database prefix and replace it by our own. + $databases['default'][$target]['prefix'] = array( + 'default' => $current_prefix . $_SERVER['HTTP_USER_AGENT'], + ); + } + } +} + +/** + * Retrieve (and optionally set) information about the current test being run. + */ +function drupal_test_info($value = FALSE) { + static $cache; + if ($value !== FALSE) { + $cache = $value; + } + return $cache; } /** diff --git includes/common.inc includes/common.inc index e48e6a7..bdd7b02 100644 --- includes/common.inc +++ includes/common.inc @@ -464,8 +464,6 @@ function drupal_access_denied() { * A string containing the response body that was received. */ function drupal_http_request($url, array $options = array()) { - global $db_prefix; - $result = new stdClass(); // Parse the URL and make sure we can handle the schema. @@ -549,8 +547,8 @@ function drupal_http_request($url, array $options = array()) { // user-agent is used to ensure that multiple testing sessions running at the // same time won't interfere with each other as they would if the database // prefix were stored statically in a file or database variable. - if (preg_match("/simpletest\d+/", $db_prefix, $matches)) { - $options['headers']['User-Agent'] = $matches[0]; + if ($test_info = drupal_test_info()) { + $options['headers']['User-Agent'] = $test_info['test_run_id']; } foreach ($options['headers'] as $name => $value) { diff --git includes/database/database.inc includes/database/database.inc index 94aad52..7ab1141 100644 --- includes/database/database.inc +++ includes/database/database.inc @@ -319,6 +319,16 @@ abstract class DatabaseConnection extends PDO { */ protected $schema = NULL; + /** + * The default prefix used by that database connection. + */ + protected $defaultPrefix = NULL; + + /** + * The non-default prefixes used by that database connection. + */ + protected $prefixes = array(); + function __construct($dsn, $username, $password, $driver_options = array()) { // Because the other methods don't seem to work right. $driver_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION; @@ -392,6 +402,24 @@ abstract class DatabaseConnection extends PDO { } /** + * Preprocess the prefix used by this database connection. + * + * @param $prefix + * The prefix, in any of the multiple forms documented in default.settings.php. + */ + protected function setPrefix($prefix) { + if (is_array($prefix)) { + $this->defaultPrefix = isset($prefix['default']) ? $prefix['default'] : ''; + unset($prefix['default']); + $this->prefixes = $prefix; + } + else { + $this->defaultPrefix = $prefix; + $this->prefixes = array(); + } + } + + /** * Append a database prefix to all tables in a query. * * Queries sent to Drupal should wrap all table names in curly brackets. This @@ -405,27 +433,12 @@ abstract class DatabaseConnection extends PDO { * The properly-prefixed string. */ public 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 , '}' => '')); + // Replace specific table prefixes first. + foreach ($this->prefixes as $key => $val) { + $sql = strtr($sql, array('{' . $key . '}' => $val . $key)); } + // Then replace remaining tables with the default prefix. + return strtr($sql, array('{' => $this->defaultPrefix , '}' => '')); } /** @@ -1240,6 +1253,20 @@ abstract class Database { if (empty($value['driver'])) { $databaseInfo[$index][$target] = $databaseInfo[$index][$target][mt_rand(0, count($databaseInfo[$index][$target]) - 1)]; } + + // Parse the prefix information. + if (!isset($databaseInfo[$index][$target]['prefix'])) { + // Default to an empty prefix. + $databaseInfo[$index][$target]['prefix'] = array( + 'default' => '', + ); + } + else if (!is_array($databaseInfo[$index][$target]['prefix'])) { + // Transform the flat form into an array form. + $databaseInfo[$index][$target]['prefix'] = array( + 'default' => $databaseInfo[$index][$target]['prefix'], + ); + } } } @@ -1286,7 +1313,40 @@ abstract class Database { if (!empty(self::$databaseInfo[$key])) { return self::$databaseInfo[$key]; } + } + + /** + * Rename a connection and its corresponding connection information. + * + * @param $old_key + * The old connection key. + * @param $new_key + * The new connection key. + */ + final public static function renameConnection($old_key, $new_key) { + if (empty(self::$databaseInfo)) { + self::parseConnectionInfo(); + } + if (!empty(self::$databaseInfo[$old_key]) && empty(self::$databaseInfo[$new_key])) { + self::$databaseInfo[$new_key] = self::$databaseInfo[$old_key]; + unset(self::$databaseInfo[$old_key]); + if (isset(self::$connections[$old_key])) { + self::$connections[$new_key] = self::$connections[$old_key]; + unset(self::$connections[$old_key]); + } + } + } + + /** + * Remove a connection and its corresponding connection information. + * + * @param $key + * The connection key. + */ + final public static function removeConnection($key) { + unset(self::$databaseInfo[$key]); + unset(self::$connections[$key]); } /** @@ -1299,7 +1359,6 @@ abstract class Database { * The database target to open. */ final protected static function openConnection($key, $target) { - global $db_prefix; if (empty(self::$databaseInfo)) { self::parseConnectionInfo(); @@ -1326,12 +1385,6 @@ abstract class Database { if (!empty(self::$logs[$key])) { $new_connection->setLogger(self::$logs[$key]); } - - // 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']; - } return $new_connection; } catch (Exception $e) { diff --git includes/database/mysql/database.inc includes/database/mysql/database.inc index 47573a2..43755b7 100644 --- includes/database/mysql/database.inc +++ includes/database/mysql/database.inc @@ -25,6 +25,8 @@ class DatabaseConnection_mysql extends DatabaseConnection { $connection_options['port'] = 3306; } + $this->setPrefix($connection_options['prefix'] ? $connection_options['prefix'] : ''); + $dsn = 'mysql:host=' . $connection_options['host'] . ';port=' . $connection_options['port'] . ';dbname=' . $connection_options['database']; parent::__construct($dsn, $connection_options['username'], $connection_options['password'], array( // So we don't have to mess around with cursors and unbuffered queries by default. diff --git includes/database/pgsql/database.inc includes/database/pgsql/database.inc index 292eb89..5e17314 100644 --- includes/database/pgsql/database.inc +++ includes/database/pgsql/database.inc @@ -26,6 +26,8 @@ class DatabaseConnection_pgsql extends DatabaseConnection { $connection_options['port'] = 5432; } + $this->setPrefix($connection_options['prefix'] ? $connection_options['prefix'] : ''); + $dsn = 'pgsql:host=' . $connection_options['host'] . ' dbname=' . $connection_options['database'] . ' port=' . $connection_options['port']; parent::__construct($dsn, $connection_options['username'], $connection_options['password'], array( // Convert numeric values to strings when fetching. diff --git includes/database/sqlite/database.inc includes/database/sqlite/database.inc index 7b97951..474d7e5 100644 --- includes/database/sqlite/database.inc +++ includes/database/sqlite/database.inc @@ -25,6 +25,8 @@ class DatabaseConnection_sqlite extends DatabaseConnection { // This driver defaults to transaction support, except if explicitly passed FALSE. $this->transactionSupport = !isset($connection_options['transactions']) || $connection_options['transactions'] !== FALSE; + $this->setPrefix($connection_options['prefix'] ? $connection_options['prefix'] : ''); + parent::__construct('sqlite:'. $connection_options['database'], '', '', array( // Force column names to lower case. PDO::ATTR_CASE => PDO::CASE_LOWER, diff --git install.php install.php index 6507703..1d5f587 100644 --- install.php +++ install.php @@ -177,7 +177,7 @@ function install_verify_drupal() { * Verify existing settings.php */ function install_verify_settings() { - global $db_prefix, $databases; + global $databases; // Verify existing settings (if any). if (!empty($databases)) { @@ -200,7 +200,7 @@ function install_verify_settings() { * Configure and rewrite settings.php. */ function install_change_settings($profile = 'default', $install_locale = '') { - global $databases, $db_prefix; + global $databases; $conf_path = './' . conf_path(FALSE, TRUE); $settings_file = $conf_path . '/settings.php'; @@ -307,14 +307,14 @@ function install_settings_form(&$form_state, $profile, $install_locale, $setting ); // Table prefix - $db_prefix = ($profile == 'default') ? 'drupal_' : $profile . '_'; - $form['advanced_options']['db_prefix'] = array( + $prefix = ($profile == 'default') ? 'drupal_' : $profile . '_'; + $form['advanced_options']['prefix'] = array( '#type' => 'textfield', '#title' => st('Table 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' => $db_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' => $prefix)), ); $form['save'] = array( @@ -335,7 +335,6 @@ function install_settings_form(&$form_state, $profile, $install_locale, $setting * Form API validate for install_settings form. */ function install_settings_form_validate($form, &$form_state) { - global $db_url; _install_settings_form_validate($form_state['values'], $form_state['values']['settings_file'], $form_state, $form); } @@ -345,12 +344,12 @@ function install_settings_form_validate($form, &$form_state) { function _install_settings_form_validate($database, $settings_file, &$form_state, $form = NULL) { global $databases; // Verify the table 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($database['prefix']) && is_string($database['prefix']) && !preg_match('/^[A-Za-z0-9_.]+$/', $database['prefix'])) { + form_set_error('prefix', st('The database table prefix you have entered, %prefix, is invalid. The table prefix can only contain alphanumeric characters, periods, or underscores.', array('%prefix' => $prefix)), 'error'); } if (!empty($database['port']) && !is_numeric($database['port'])) { - form_set_error('db_port', st('Database port must be a number.')); + form_set_error('port', st('Database port must be a number.')); } // Check database type @@ -384,16 +383,12 @@ function _install_settings_form_validate($database, $settings_file, &$form_state 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'))); + $database = array_intersect_key($form_state['values']['_database'], array_flip(array('driver', 'database', 'username', 'password', 'host', 'port', 'prefix'))); // Update global settings array and save $settings['databases'] = array( 'value' => array('default' => array('default' => $database)), 'required' => TRUE, ); - $settings['db_prefix'] = array( - 'value' => $form_state['values']['db_prefix'], - 'required' => TRUE, - ); drupal_rewrite_settings($settings); // Continue to install profile step diff --git modules/simpletest/drupal_web_test_case.php modules/simpletest/drupal_web_test_case.php index d935e4e..8c8467c 100644 --- modules/simpletest/drupal_web_test_case.php +++ modules/simpletest/drupal_web_test_case.php @@ -78,11 +78,11 @@ class DrupalWebTestCase { protected $additionalCurlOptions = array(); /** - * The original database prefix, before it was changed for testing purposes. + * The database prefix of this test run. * * @var string */ - protected $originalPrefix = NULL; + protected $databasePrefix = NULL; /** * The original file directory, before it was changed for testing purposes. @@ -150,8 +150,6 @@ class DrupalWebTestCase { * is the caller function itself. */ private function assert($status, $message = '', $group = 'Other', array $caller = NULL) { - global $db_prefix; - // Convert boolean status to string status. if (is_bool($status)) { $status = $status ? 'pass' : 'fail'; @@ -165,10 +163,6 @@ class DrupalWebTestCase { $caller = $this->getAssertionCall(); } - // Switch to non-testing database to store results in. - $current_db_prefix = $db_prefix; - $db_prefix = $this->originalPrefix; - // Creation assertion array that can be displayed while tests are running. $this->assertions[] = $assertion = array( 'test_id' => $this->testId, @@ -182,10 +176,10 @@ class DrupalWebTestCase { ); // Store assertion for display after the test has completed. - db_insert('simpletest')->fields($assertion)->execute(); - - // Return to testing prefix. - $db_prefix = $current_db_prefix; + Database::getConnection('default', 'simpletest_original_default') + ->insert('simpletest') + ->fields($assertion) + ->execute(); return $status == 'pass' ? TRUE : FALSE; } @@ -812,14 +806,26 @@ class DrupalWebTestCase { * List of modules to enable for the duration of the test. */ protected function setUp() { - global $db_prefix, $user; + global $user; + + // Generate a temporary prefixed database to ensure that tests have a clean starting point. + $this->databasePrefix = 'simpletest' . mt_rand(1000, 1000000); + + // Clone the current connection and replace the current prefix. + $connection_info = Database::getConnectionInfo('default'); + Database::renameConnection('default', 'simpletest_original_default'); + foreach ($connection_info as $target => $value) { + $connection_info[$target]['prefix'] = array( + 'default' => $value['prefix']['default'] . $this->databasePrefix, + ); + } + Database::addConnectionInfo('default', 'default', $connection_info['default']); // Store necessary current values before switching to prefixed database. - $this->originalPrefix = $db_prefix; $clean_url_original = variable_get('clean_url', 0); - // Generate temporary prefixed database to ensure that tests have a clean starting point. - $db_prefix = Database::getConnection()->prefixTables('{simpletest' . mt_rand(1000, 1000000) . '}'); + // Set the simpletest id for use in other parts of Drupal. + drupal_test_info(array('test_run_id' => $this->databasePrefix, 'in_child_site' => FALSE)); include_once DRUPAL_ROOT . '/includes/install.inc'; drupal_install_system(); @@ -860,7 +866,7 @@ class DrupalWebTestCase { // Use temporary files directory with the same prefix as database. $this->originalFileDirectory = file_directory_path(); - variable_set('file_directory_path', file_directory_path() . '/' . $db_prefix); + variable_set('file_directory_path', file_directory_path() . '/' . $this->databasePrefix); $directory = file_directory_path(); file_check_directory($directory, FILE_CREATE_DIRECTORY); // Create the files directory. set_time_limit($this->timeLimit); @@ -872,8 +878,10 @@ class DrupalWebTestCase { * setup a clean environment for the current test run. */ protected function preloadRegistry() { - db_query('INSERT INTO {registry} SELECT * FROM ' . $this->originalPrefix . 'registry'); - db_query('INSERT INTO {registry_file} SELECT * FROM ' . $this->originalPrefix . 'registry_file'); + $original_connection = Database::getConnection('default', 'simpletest_original_default'); + $this->pass('INSERT INTO {registry} SELECT * FROM ' . $original_connection->prefixTables('{registry}')); + db_query('INSERT INTO {registry} SELECT * FROM ' . $original_connection->prefixTables('{registry}')); + db_query('INSERT INTO {registry_file} SELECT * FROM ' . $original_connection->prefixTables('{registry_file}')); } /** @@ -899,44 +907,34 @@ class DrupalWebTestCase { * and reset the database prefix. */ protected function tearDown() { - global $db_prefix, $user; - if (preg_match('/simpletest\d+/', $db_prefix)) { - // Delete temporary files directory and reset files directory path. - file_unmanaged_delete_recursive(file_directory_path()); - variable_set('file_directory_path', $this->originalFileDirectory); - - // Remove all prefixed tables (all the tables in the schema). - $schema = drupal_get_schema(NULL, TRUE); - $ret = array(); - foreach ($schema as $name => $table) { - db_drop_table($ret, $name); - } + // Get back to the original connection. + Database::removeConnection('default'); + Database::renameConnection('simpletest_original_default', 'default'); - // Return the database prefix to the original. - $db_prefix = $this->originalPrefix; + // Return the user to the original one. + $user = $this->originalUser; + drupal_save_session(TRUE); - // Return the user to the original one. - $user = $this->originalUser; - drupal_save_session(TRUE); + // Ensure that internal logged in variable and cURL options are reset. + $this->isLoggedIn = FALSE; + $this->additionalCurlOptions = array(); - // Ensure that internal logged in variable and cURL options are reset. - $this->isLoggedIn = FALSE; - $this->additionalCurlOptions = array(); + // Reload module list and implementations to ensure that test module hooks + // aren't called after tests. + module_list(TRUE); + module_implements(MODULE_IMPLEMENTS_CLEAR_CACHE); - // Reload module list and implementations to ensure that test module hooks - // aren't called after tests. - module_list(TRUE); - module_implements(MODULE_IMPLEMENTS_CLEAR_CACHE); + // Reset the Field API. + field_cache_clear(); - // Reset the Field API. - field_cache_clear(); + // Ensure that the internal logged in variable is reset. + $this->_logged_in = FALSE; - // Rebuild caches. - $this->refreshVariables(); + // Rebuild caches. + $this->refreshVariables(); - // Close the CURL handler. - $this->curlClose(); - } + // Close the CURL handler. + $this->curlClose(); } /** @@ -947,7 +945,7 @@ class DrupalWebTestCase { * see the description of $curl_options among the properties. */ protected function curlInitialize() { - global $base_url, $db_prefix; + global $base_url; if (!isset($this->curlHandle)) { $this->curlHandle = curl_init(); $curl_options = $this->additionalCurlOptions + array( @@ -958,10 +956,8 @@ class DrupalWebTestCase { CURLOPT_SSL_VERIFYPEER => FALSE, // Required to make the tests run on https:// CURLOPT_SSL_VERIFYHOST => FALSE, // Required to make the tests run on https:// CURLOPT_HEADERFUNCTION => array(&$this, 'curlHeaderCallback'), + CURLOPT_USERAGENT => $this->databasePrefix, ); - if (preg_match('/simpletest\d+/', $db_prefix, $matches)) { - $curl_options[CURLOPT_USERAGENT] = $matches[0]; - } if (!isset($curl_options[CURLOPT_USERPWD]) && ($auth = variable_get('simpletest_httpauth_username', ''))) { if ($pass = variable_get('simpletest_httpauth_pass', '')) { $auth .= ':' . $pass; diff --git sites/default/default.settings.php sites/default/default.settings.php index f8c7ffa..5aa4411 100644 --- sites/default/default.settings.php +++ sites/default/default.settings.php @@ -105,30 +105,31 @@ * 'username' => 'username', * 'password' => 'password', * 'host' => 'localhost', + * 'prefix' => '', * ); * * 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 + * by using the 'prefix' setting. If a prefix is specified, the table * name will be prepended with its value. Be sure to use valid database * characters only, usually alphanumeric and underscore. If no prefixes * are desired, leave it as an empty string ''. * - * To have all database names prefixed, set $db_prefix as a string: + * To have all database names prefixed, set 'prefix' as a string: * - * $db_prefix = 'main_'; + * 'prefix' => 'main_', * - * To provide prefixes for specific tables, set $db_prefix as an array. + * To provide prefixes for specific tables, set 'prefix' as an array. * The array's keys are the table names and the values are the prefixes. - * The 'default' element holds the prefix for any tables not specified - * elsewhere in the array. Example: + * The 'default' element is mandatory and holds the prefix for any tables + * not specified elsewhere in the array. Example: * - * $db_prefix = array( + * 'prefix' = array( * 'default' => 'main_', * 'users' => 'shared_', * 'sessions' => 'shared_', * 'role' => 'shared_', * 'authmap' => 'shared_', - * ); + * ), * * Database configuration format: * $databases['default']['default'] = array( @@ -151,7 +152,6 @@ * ); */ $databases = array(); -$db_prefix = ''; /** * Access control for update.php script