=== modified file 'includes/actions.inc' --- includes/actions.inc 2009-06-18 10:37:37 +0000 +++ includes/actions.inc 2009-07-14 15:46:25 +0000 @@ -324,7 +324,7 @@ function actions_save($function, $type, // 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') === modified file 'includes/database/database.inc' --- includes/database/database.inc 2009-07-14 10:22:15 +0000 +++ includes/database/database.inc 2009-07-14 15:46:25 +0000 @@ -1046,6 +1046,38 @@ abstract class DatabaseConnection extend 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; @@ -2058,6 +2090,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". */ @@ -2455,7 +2507,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 === modified file 'includes/database/mysql/database.inc' --- includes/database/mysql/database.inc 2009-03-25 18:43:01 +0000 +++ includes/database/mysql/database.inc 2009-07-14 16:57:53 +0000 @@ -77,6 +77,26 @@ class DatabaseConnection_mysql extends D // (?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(); + } } === modified file 'modules/simpletest/tests/database_test.test' --- modules/simpletest/tests/database_test.test 2009-07-13 21:51:08 +0000 +++ modules/simpletest/tests/database_test.test 2009-07-14 15:46:25 +0000 @@ -2746,3 +2746,30 @@ class DatabaseTransactionTestCase extend } } } + +/** + * 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.')); + } +} === modified file 'modules/system/system.install' --- modules/system/system.install 2009-07-10 04:28:15 +0000 +++ modules/system/system.install 2009-07-16 06:57:47 +0000 @@ -355,21 +355,10 @@ 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')", '', ''); + db_query("INSERT INTO {users} (uid, name, mail) VALUES (0, '%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'); + // presumed to be a serialized array. + db_query("INSERT INTO {users} (uid, name, mail, created, status, data) VALUES (1, '%s', '%s', %d, %d, '%s')", 'placeholder-for-uid-1', 'placeholder-for-uid-1', REQUEST_TIME, 1, serialize(array())); // Built-in roles. $rid_anonymous = db_insert('role') @@ -382,8 +371,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 +574,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 +1183,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( @@ -1288,6 +1252,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( @@ -2241,6 +2225,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. */ === modified file 'modules/system/system.queue.inc' --- modules/system/system.queue.inc 2009-05-24 17:39:30 +0000 +++ modules/system/system.queue.inc 2009-07-14 15:46:25 +0000 @@ -195,9 +195,7 @@ class SystemQueue implements DrupalQueue 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 === modified file 'modules/user/user.install' --- modules/user/user.install 2009-06-01 22:07:08 +0000 +++ modules/user/user.install 2009-07-14 15:46:25 +0000 @@ -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', === modified file 'modules/user/user.module' --- modules/user/user.module 2009-07-12 08:36:34 +0000 +++ modules/user/user.module 2009-07-16 16:54:13 +0000 @@ -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,7 @@ function user_save($account, $edit = arr if (!isset($account->is_new)) { $account->is_new = empty($account->uid); } + 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 +399,6 @@ function user_save($account, $edit = arr } } - // Process picture uploads. if (!empty($edit['picture']->fid)) { $picture = $edit['picture']; @@ -416,6 +416,7 @@ function user_save($account, $edit = arr $edit['picture'] = empty($edit['picture']->fid) ? 0 : $edit['picture']->fid; $edit['data'] = $data; + // Do not allow 'uid' to be changed. $edit['uid'] = $account->uid; // Save changes to the user table. $success = drupal_write_record('users', $edit, 'uid'); @@ -486,6 +487,11 @@ function user_save($account, $edit = arr user_module_invoke('after_update', $edit, $user, $category); } else { + // Allow 'uid' to be set by the caller. There is no danger of writing an + // existing user as drupal_write_record will do an INSERT. + if (empty($edit['uid'])) { + $edit['uid'] = db_next_id('users', db_query('SELECT MAX(uid) FROM {users}')->fetchField()); + } // Allow 'created' to be set by the caller. if (!isset($edit['created'])) { $edit['created'] = REQUEST_TIME; @@ -1594,7 +1600,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 +1628,7 @@ function user_login_name_validate($form, /** * 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 +1662,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 +1707,15 @@ function user_login_finalize(&$edit = ar } /** - * 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 +1743,7 @@ function user_external_login_register($n } user_set_authmaps($account, array("authname_$module" => $name)); } - + // Log user in. $form_state['uid'] = $account->uid; user_login_submit(array(), $form_state); @@ -2803,7 +2809,7 @@ function user_register_submit($form, &$f 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';