New sequences API. From: damz --- actions.inc | 2 - database/database.inc | 56 ++++++++++++++++++++++++- database/mysql/database.inc | 20 +++++++++ simpletest/tests/database_test.test | 27 ++++++++++++ system/system.install | 80 +++++++++++++++++------------------ system/system.module | 22 +++++----- system/system.queue.inc | 4 -- user/user.install | 3 + user/user.module | 30 +++++++------ 9 files changed, 172 insertions(+), 72 deletions(-) diff --git includes/actions.inc includes/actions.inc index 080f932..25496d4 100644 --- includes/actions.inc +++ includes/actions.inc @@ -324,7 +324,7 @@ function actions_save($function, $type, $params, $desc, $aid = NULL) { // aid is the callback for singleton actions so we need to keep a separate // table for numeric aids. if (!$aid) { - $aid = db_insert('actions_aid')->useDefaults(array('aid'))->execute(); + $aid = db_next_id('actions'); } db_merge('actions') diff --git includes/database/database.inc includes/database/database.inc index 1dcd693..b600283 100644 --- includes/database/database.inc +++ includes/database/database.inc @@ -1046,6 +1046,38 @@ abstract class DatabaseConnection extends PDO { public function commit() { throw new ExplicitTransactionsNotSupportedException(); } + + /** + * Retrieves an unique id from a given sequence. + * + * Use this function if for some reason you can't use a serial field. + * + * @param $name + * The name of the sequence. + * @param $existing_id + * After a database import, it might be that the sequences table is behind, + * so by passing in a minimum id, it can be assured that we never issue the + * same id. + * @return + * An integer number larger than any number returned by earlier calls + * to this function with the same sequence argument. + */ + public function nextId($name, $existing_id) { + $transaction = $this->startTransaction(); + $current_value = $this + ->query('SELECT value FROM {sequences} WHERE name = :name', array(':name' => $name)) + ->fetchField(); + $new_value = max($current_value, $existing_id) + 1; + // As we start the sequence with 1, if $current_value is 0 then the sequence + // does not exist. + if ($current_value) { + $this->query('UPDATE {sequences} SET value = :value WHERE name = :name', array(':name' => $name, ':value' => $new_value)); + } + else { + $this->query('INSERT INTO {sequences} (name, value) VALUES (:name, :value)', array(':name' => $name, ':value' => $new_value)); + } + return $new_value; + } } /** @@ -1348,7 +1380,7 @@ abstract class Database { // We need to pass around the simpletest database prefix in the request // and we put that in the user_agent header. - if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^simpletest\d+$/", $_SERVER['HTTP_USER_AGENT'])) { + if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^simpletest\d+$/", $_SERVER['HTTP_USER_AGENT'])) { $db_prefix .= $_SERVER['HTTP_USER_AGENT']; } return $new_connection; @@ -2041,6 +2073,26 @@ function db_driver() { } /** + * Retrieves an unique id from a given sequence. + * + * Use this function if for some reason you can't use a serial field, + * normally a serial field with db_last_insert_id is preferred. + * + * @param $name + * The name of the sequence. + * @param $existing_id + * After a database import, it might be that the sequences table is behind, + * so by passing in a minimum id, it can be assured that we never issue the + * same id. + * @return + * An integer number larger than any number returned before for this + * sequence. + */ +function db_next_id($name, $existing_id = 0) { + return Database::getConnection()->nextId($name, $existing_id); +} + +/** * @} End of "defgroup database". */ @@ -2438,7 +2490,7 @@ function _db_error_page($error = '') { function db_ignore_slave() { $connection_info = Database::getConnectionInfo(); // Only set ignore_slave_server if there are slave servers - // being used, which is assumed if there are more than one. + // being used, which is assumed if there are more than one. if (count($connection_info) > 1) { // Five minutes is long enough to allow the slave to break and resume // interrupted replication without causing problems on the Drupal site diff --git includes/database/mysql/database.inc includes/database/mysql/database.inc index b91d1d4..2cf03e8 100644 --- includes/database/mysql/database.inc +++ includes/database/mysql/database.inc @@ -77,6 +77,26 @@ class DatabaseConnection_mysql extends DatabaseConnection { // (?query('INSERT INTO {sequences} (name, value) VALUES (:name, :value)', array(':name' => $name, ':value' => $existing_id)); + } + catch (PDOException $e) { + // The most likely case for an exception is that the sequence already + // exists and that's exactly what we wanted to assure. + } + // LAST_INSERT_ID(x) stores x to be retrieved with LAST_INSERT_ID(). + // This way there is no window for race condition between the UPDATE and + // the SELECT. + $this->query('UPDATE {sequences} SET value = LAST_INSERT_ID(IF(:existing_id > value, :existing_id_1, value) + 1) WHERE name = :name', array( + ':existing_id' => $existing_id, + ':existing_id_1' => $existing_id, + ':name' => $name, + )); + return $this->query('SELECT LAST_INSERT_ID()')->fetchField(); + } } diff --git modules/simpletest/tests/database_test.test modules/simpletest/tests/database_test.test index b6ef342..382b071 100644 --- modules/simpletest/tests/database_test.test +++ modules/simpletest/tests/database_test.test @@ -2746,3 +2746,30 @@ class DatabaseTransactionTestCase extends DatabaseTestCase { } } } + +/** + * Check the sequences API. + */ +class DatabaseNextIdCase extends DrupalWebTestCase { + function getInfo() { + return array( + 'name' => t('Sequences API'), + 'description' => t('Test the secondary sequences API.'), + 'group' => t('Database'), + ); + } + + /** + * Test that the sequences API work. + */ + function testDbNextId() { + $first = db_next_id('test'); + $second = db_next_id('test'); + // We can test for exact increase in here because we know there is no + // other process operating on these tables -- normally we could only + // expect $second > $first. + $this->assertEqual($first + 1, $second, t('The second call from a sequence provides a number increased by one.')); + $result = db_next_id('test', 1000); + $this->assertEqual($result, 1001, t('Sequence provides a larger number than the existing ID.')); + } +} diff --git modules/system/system.install modules/system/system.install index e0385e3..f5fbdb1 100644 --- modules/system/system.install +++ modules/system/system.install @@ -355,21 +355,7 @@ function system_install() { // Load system theme data appropriately. system_get_theme_data(); - // Inserting uid 0 here confuses MySQL -- the next user might be created as - // uid 2 which is not what we want. So we insert the first user here, the - // anonymous user. uid is 1 here for now, but very soon it will be changed - // to 0. db_query("INSERT INTO {users} (name, mail) VALUES('%s', '%s')", '', ''); - // We need some placeholders here as name and mail are uniques and data is - // presumed to be a serialized array. Install will change uid 1 immediately - // anyways. So we insert the superuser here, the uid is 2 here for now, but - // very soon it will be changed to 1. - db_query("INSERT INTO {users} (name, mail, created, status, data) VALUES('%s', '%s', %d, %d, '%s')", 'placeholder-for-uid-1', 'placeholder-for-uid-1', REQUEST_TIME, 1, serialize(array())); - // This sets the above two users uid 0 (anonymous). We avoid an explicit 0 - // otherwise MySQL might insert the next auto_increment value. - db_query("UPDATE {users} SET uid = uid - uid WHERE name = '%s'", ''); - // This sets uid 1 (superuser). We skip uid 2 but that's not a big problem. - db_query("UPDATE {users} SET uid = 1 WHERE name = '%s'", 'placeholder-for-uid-1'); // Built-in roles. $rid_anonymous = db_insert('role') @@ -382,8 +368,8 @@ function system_install() { ->values(array('name' => 'authenticated user')) ->execute(); - // Sanity check to ensure the anonymous and authenticated role IDs are the - // same as the drupal defined constants. In certain situations, this will + // Sanity check to ensure the anonymous and authenticated role IDs are the + // same as the drupal defined constants. In certain situations, this will // not be true. if ($rid_anonymous != DRUPAL_ANONYMOUS_RID) { db_update('role') @@ -585,19 +571,6 @@ function system_schema() { 'primary key' => array('aid'), ); - $schema['actions_aid'] = array( - 'description' => 'Stores action IDs for non-default actions.', - 'fields' => array( - 'aid' => array( - 'description' => 'Primary Key: Unique actions ID.', - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - ), - ), - 'primary key' => array('aid'), - ); - $schema['batch'] = array( 'description' => 'Stores details about batches (processes that run in multiple HTTP requests).', 'fields' => array( @@ -1207,18 +1180,6 @@ function system_schema() { ), ); - $schema['queue_consumer_id'] = array( - 'description' => 'Stores queue consumer IDs, used to auto-increment the consumer ID so that a unique consumer ID is used.', - 'fields' => array( - 'consumer_id' => array( - 'type' => 'serial', - 'not null' => TRUE, - 'description' => 'Primary Key: Unique consumer ID used to make sure only one consumer gets one item.', - ), - ), - 'primary key' => array('consumer_id'), - ); - $schema['registry'] = array( 'description' => "Each record is a function, class, or interface name and the file it is in.", 'fields' => array( @@ -1294,6 +1255,26 @@ function system_schema() { 'primary key' => array('filename'), ); + $schema['sequences'] = array( + 'description' => 'Stores IDs.', + 'fields' => array( + 'value' => array( + 'description' => 'The .', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'name' => array( + 'description' => "Primary key: A session ID. The value is generated by PHP's Session API.", + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + 'default' => '', + ), + ), + 'primary key' => array('name'), + ); + $schema['sessions'] = array( 'description' => "Drupal's session handlers read and write into the sessions table. Each record represents a user session, either anonymous or authenticated.", 'fields' => array( @@ -2248,6 +2229,23 @@ function system_update_7029() { } /** + * Reuse the actions_aid table as sequences. + */ +function system_update_7030() { + $ret = array(); + db_drop_primary_key($ret, 'actions_aid'); + db_change_field($ret, 'actions_aid', 'aid', 'sid', array('type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE), array('primary key' => array('sid'))); + db_rename_table($ret, 'actions_aid', 'sequences'); + $max_sid = db_query('SELECT MAX(sid) FROM {sequences}')->fetchField(); + db_delete('sequences')->condition('sid', $max_sid, '<'); + $max_uid = db_query('SELECT MAX(uid) FROM {users}')->fetchField(); + if ($max_uid > $max_sid) { + db_update('sequences')->fields(array('sid' => $max_uid))->execute(); + } + return $ret; +} + +/** * @} End of "defgroup updates-6.x-to-7.x" * The next series of updates should start at 8000. */ diff --git modules/system/system.module modules/system/system.module index fa0f5c7..cb6721f 100644 --- modules/system/system.module +++ modules/system/system.module @@ -1200,7 +1200,7 @@ function system_filetransfer_backends() { 'weight' => 0, ); } - + if (ini_get('allow_url_fopen')) { $backends['ftp_wrapper'] = array( 'title' => t('FTP using file streams'), @@ -1209,7 +1209,7 @@ function system_filetransfer_backends() { 'weight' => 10, ); } - + // SSH2 lib connection is only available if the proper PHP extension is // installed. if (function_exists('ssh2_connect')) { @@ -1235,7 +1235,7 @@ function system_filetransfer_backends() { function system_get_filetransfer_settings_form($filetransfer_backend_name, $defaults) { $available_backends = module_invoke_all('filetransfer_backends'); $form = call_user_func($available_backends[$filetransfer_backend_name]['settings_form']); - + foreach ($form as $name => &$element) { if (isset($defaults[$name])) { $element['#default_value'] = $defaults[$name]; @@ -1267,30 +1267,30 @@ function system_filetransfer_backend_form_ssh() { */ function _system_filetransfer_backend_form_common() { $form = array(); - + $form['hostname'] = array ( '#type' => 'textfield', '#title' => t('Host'), '#default_value' => 'localhost', ); - + $form['port'] = array ( '#type' => 'textfield', '#title' => t('Port'), '#default_value' => NULL, ); - + $form['username'] = array ( '#type' => 'textfield', '#title' => t('Username'), ); - + $form['password'] = array ( '#type' => 'password', '#title' => t('Password'), '#description' => t('This is not saved in the database and is only used to test the connection'), ); - + return $form; } @@ -3025,17 +3025,17 @@ function system_retrieve_file($url, $destination = NULL, $overwrite = TRUE) { } $parsed_url = parse_url($url); $local = is_dir(file_directory_path() . '/' . $destination) ? $destination . '/' . basename($parsed_url['path']) : $destination; - + if (!$overwrite && file_exists($local)) { drupal_set_message(t('@remote could not be saved. @local already exists', array('@remote' => $url, '@local' => $local)), 'error'); return FALSE; } - + $result = drupal_http_request($url); if ($result->code != 200 || !file_save_data($result->data, $local)) { drupal_set_message(t('@remote could not be saved.', array('@remote' => $url)), 'error'); return FALSE; } - + return $local; } diff --git modules/system/system.queue.inc modules/system/system.queue.inc index 36bcd7f..c6eff35 100644 --- modules/system/system.queue.inc +++ modules/system/system.queue.inc @@ -195,9 +195,7 @@ class SystemQueue implements DrupalQueueInterface { public function claimItem($lease_time = 30) { if (!isset($this->consumerId)) { - $this->consumerId = db_insert('queue_consumer_id') - ->useDefaults(array('consumer_id')) - ->execute(); + $this->consumerId = db_next_id('queue_consumer_id'); } // Claim an item by updating its consumer_id and expire fields. If claim // is not successful another thread may have claimed the item in the diff --git modules/user/user.install modules/user/user.install index 7a63b2f..1abde8c 100644 --- modules/user/user.install +++ modules/user/user.install @@ -102,10 +102,11 @@ function user_schema() { 'description' => 'Stores user data.', 'fields' => array( 'uid' => array( - 'type' => 'serial', + 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'description' => 'Primary Key: Unique user ID.', + 'default' => 0, ), 'name' => array( 'type' => 'varchar', diff --git modules/user/user.module modules/user/user.module index 3749e1f..5b300c9 100644 --- modules/user/user.module +++ modules/user/user.module @@ -325,11 +325,11 @@ function user_load_by_name($name) { * * @param $account * (optional) The user object to modify or add. If you want to modify - * an existing user account, you will need to ensure that (a) $account - * is an object, and (b) you have set $account->uid to the numeric + * an existing user account, you will need to ensure that (a) $account + * is an object, and (b) you have set $account->uid to the numeric * user ID of the user account you wish to modify. If you * want to create a new user account, you can set $account->is_new to - * TRUE or omit the $account->uid field. + * TRUE or omit the $account->uid field. * @param $edit * An array of fields and values to save. For example array('name' * => 'My name'). Keys that do not belong to columns in the user-related @@ -376,6 +376,11 @@ function user_save($account, $edit = array(), $category = 'account') { if (!isset($account->is_new)) { $account->is_new = empty($account->uid); } + + if ($account->is_new && empty($account->uid)) { + $account->uid = db_next_id('users', db_query('SELECT MAX(uid) FROM {users}')->fetchField()); + } + if (is_object($account) && !$account->is_new) { user_module_invoke('update', $edit, $account, $category); $data = unserialize(db_query('SELECT data FROM {users} WHERE uid = :uid', array(':uid' => $account->uid))->fetchField()); @@ -398,7 +403,6 @@ function user_save($account, $edit = array(), $category = 'account') { } } - // Process picture uploads. if (!empty($edit['picture']->fid)) { $picture = $edit['picture']; @@ -1594,7 +1598,7 @@ function user_login(&$form_state) { * authentication fails. Distributed authentication modules are welcome * to use hook_form_alter() to change this series in order to * authenticate against their user database instead of the local users - * table. If a distributed authentication module is successful, it + * table. If a distributed authentication module is successful, it * should set $form_state['uid'] to a user ID. * * We use three validators instead of one since external authentication @@ -1622,7 +1626,7 @@ function user_login_name_validate($form, &$form_state) { /** * A validate handler on the login form. Check supplied username/password - * against local users table. If successful, $form_state['uid'] + * against local users table. If successful, $form_state['uid'] * is set to the matching user ID. */ function user_login_authenticate_validate($form, &$form_state) { @@ -1656,10 +1660,10 @@ function user_authenticate(&$form_state) { // Allow alternate password hashing schemes. require_once DRUPAL_ROOT . '/' . variable_get('password_inc', 'includes/password.inc'); if (user_check_password($password, $account)) { - + // Successful authentication. Set a flag for user_login_final_validate(). $form_state['uid'] = $account->uid; - + // Update user to new password scheme if needed. if (user_needs_new_hash($account)) { $new_hash = user_hash_password($password); @@ -1701,15 +1705,15 @@ function user_login_finalize(&$edit = array()) { } /** - * Submit handler for the login form. Load $user object and perform standard login - * tasks. The user is then redirected to the My Account page. Setting the + * Submit handler for the login form. Load $user object and perform standard login + * tasks. The user is then redirected to the My Account page. Setting the * destination in the query string overrides the redirect. */ function user_login_submit($form, &$form_state) { global $user; $user = user_load($form_state['uid']); user_login_finalize(); - + $form_state['redirect'] = 'user/' . $user->uid; } @@ -1737,7 +1741,7 @@ function user_external_login_register($name, $module) { } user_set_authmaps($account, array("authname_$module" => $name)); } - + // Log user in. $form_state['uid'] = $account->uid; user_login_submit(array(), $form_state); @@ -2803,7 +2807,7 @@ function user_register_submit($form, &$form_state) { drupal_set_message(t('

Your password is %pass. You may change your password below.

', array('%pass' => $pass))); } - $form_state['values'] += $merge_data; + $form_state['values'] += $merge_data; user_authenticate(array_merge($form_state)); $form_state['redirect'] = 'user/1/edit';