Skip to content
Snippets Groups Projects
Commit 139f9114 authored by catch's avatar catch
Browse files

Issue #838992 by alexpott, Charlie ChX Negyesi, daffie, Damien Tournoud, sun,...

Issue #838992 by alexpott, Charlie ChX Negyesi, daffie, Damien Tournoud, sun, quietone, longwave: Change the uid field from integer to serial by leveraging NO_AUTO_VALUE_ON_ZERO on MySQL
parent bd272a4c
No related branches found
No related tags found
16 merge requests!7452Issue #1797438. HTML5 validation is preventing form submit and not fully...,!1896Issue #2940605: Can only intentionally re-render an entity with references 20 times,!1101Issue #2412669 by claudiu.cristea, Julfabre, sidharrell, catch, daffie,...,!1039Issue #2556069 by claudiu.cristea, bnjmnm, lauriii, pfrenssen, Tim Bozeman,...,!10223132456: Fix issue where views instances are emptied before an ajax request is complete,!872Draft: Issue #3221319: Race condition when creating menu links and editing content deletes menu links,!594Put each entity type table into a details element on admin/config/regional/content-language,!579Issue #2230909: Simple decimals fail to pass validation,!560Move callback classRemove outside of the loop,!555Issue #3202493,!512Issue #3207771: Menu UI node type form documentation points to non-existent function,!485Sets the autocomplete attribute for username/password input field on login form.,!449Issue #2784233: Allow multiple vocabularies in the taxonomy filter,!231Issue #2671162: summary text wysiwyg patch working fine on 9.2.0-dev,!43Resolve #3173180: Add UI for 'loading' html attribute to images,!30Issue #3182188: Updates composer usage to point at ./vendor/bin/composer
...@@ -410,9 +410,15 @@ protected function getTemplate() { ...@@ -410,9 +410,15 @@ protected function getTemplate() {
use Drupal\Core\Database\Database; use Drupal\Core\Database\Database;
$connection = Database::getConnection(); $connection = Database::getConnection();
// Ensure any tables with a serial column with a value of 0 are created as
// expected.
$sql_mode = $connection->query("SELECT @@sql_mode;")->fetchField();
$connection->query("SET sql_mode = '$sql_mode,NO_AUTO_VALUE_ON_ZERO'");
{{TABLES}} {{TABLES}}
// Reset the SQL mode.
$connection->query("SET sql_mode = '$sql_mode'");
ENDOFSCRIPT; ENDOFSCRIPT;
return $script; return $script;
} }
......
...@@ -1057,7 +1057,14 @@ protected function mapToStorageRecord(ContentEntityInterface $entity, $table_nam ...@@ -1057,7 +1057,14 @@ protected function mapToStorageRecord(ContentEntityInterface $entity, $table_nam
// SQL database drivers. // SQL database drivers.
// @see https://www.drupal.org/node/2279395 // @see https://www.drupal.org/node/2279395
$value = SqlContentEntityStorageSchema::castValue($definition->getSchema()['columns'][$column_name], $value); $value = SqlContentEntityStorageSchema::castValue($definition->getSchema()['columns'][$column_name], $value);
if (!(empty($value) && $this->isColumnSerial($table_name, $schema_name))) { $empty_serial = empty($value) && $this->isColumnSerial($table_name, $schema_name);
// The user entity is a very special case where the ID field is a serial
// but we need to insert a row with an ID of 0 to represent the
// anonymous user.
// @todo https://drupal.org/i/3222123 implement a generic fix for all
// entity types.
$user_zero = $this->entityTypeId === 'user' && $value === 0;
if (!$empty_serial || $user_zero) {
$record->$schema_name = $value; $record->$schema_name = $value;
} }
} }
......
...@@ -18,21 +18,24 @@ class UserStorage extends SqlContentEntityStorage implements UserStorageInterfac ...@@ -18,21 +18,24 @@ class UserStorage extends SqlContentEntityStorage implements UserStorageInterfac
* {@inheritdoc} * {@inheritdoc}
*/ */
protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) { protected function doSaveFieldItems(ContentEntityInterface $entity, array $names = []) {
// The anonymous user account is saved with the fixed user ID of 0. // The anonymous user account is saved with the fixed user ID of 0. MySQL
// Therefore we need to check for NULL explicitly. // does not support inserting an ID of 0 into serial field unless the SQL
if ($entity->id() === NULL) { // mode is set to NO_AUTO_VALUE_ON_ZERO.
$entity->uid->value = $this->database->nextId($this->database->query('SELECT MAX([uid]) FROM {' . $this->getBaseTable() . '}')->fetchField()); // @todo https://drupal.org/i/3222123 implement a generic fix for all entity
$entity->enforceIsNew(); // types.
if ($entity->id() === 0) {
$database = \Drupal::database();
if ($database->databaseType() === 'mysql') {
$sql_mode = $database->query("SELECT @@sql_mode;")->fetchField();
$database->query("SET sql_mode = '$sql_mode,NO_AUTO_VALUE_ON_ZERO'");
}
} }
return parent::doSaveFieldItems($entity, $names); parent::doSaveFieldItems($entity, $names);
}
/** // Reset the SQL mode if we've changed it.
* {@inheritdoc} if (isset($sql_mode, $database)) {
*/ $database->query("SET sql_mode = '$sql_mode'");
protected function isColumnSerial($table_name, $schema_name) { }
// User storage does not use a serial column for the user id.
return $table_name == $this->revisionTable && $schema_name == $this->revisionKey;
} }
/** /**
......
...@@ -26,16 +26,6 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res ...@@ -26,16 +26,6 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res
return $schema; return $schema;
} }
/**
* {@inheritdoc}
*/
protected function processIdentifierSchema(&$schema, $key) {
// The "users" table does not use serial identifiers.
if ($key != $this->entityType->getKey('id')) {
parent::processIdentifierSchema($schema, $key);
}
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
......
<?php
namespace Drupal\Tests\user\Functional;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
// cSpell:ignore refobjid regclass attname attrelid attnum refobjsubid objid
// cSpell:ignore classid
/**
* Tests user_update_9301().
*
* @group user
*/
class UidUpdateToSerialTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles[] = __DIR__ . '/../../../../system/tests/fixtures/update/drupal-9.0.0.bare.standard.php.gz';
}
/**
* Tests user_update_9301().
*/
public function testDatabaseLoaded() {
$key_value_store = \Drupal::keyValue('entity.storage_schema.sql');
$id_schema = $key_value_store->get('user.field_schema_data.uid', []);
$this->assertSame('int', $id_schema['users']['fields']['uid']['type']);
$this->runUpdates();
$key_value_store = \Drupal::keyValue('entity.storage_schema.sql');
$id_schema = $key_value_store->get('user.field_schema_data.uid', []);
$this->assertSame('serial', $id_schema['users']['fields']['uid']['type']);
$connection = \Drupal::database();
if ($connection->driver() == 'pgsql') {
$seq_name = $connection->makeSequenceName('users', 'uid');
$seq_owner = $connection->query("SELECT d.refobjid::regclass as table_name, a.attname as field_name
FROM pg_depend d
JOIN pg_attribute a ON a.attrelid = d.refobjid AND a.attnum = d.refobjsubid
WHERE d.objid = :seq_name::regclass
AND d.refobjsubid > 0
AND d.classid = 'pg_class'::regclass", [':seq_name' => 'public.' . $seq_name])->fetchObject();
$this->assertEquals($connection->tablePrefix('users') . 'users', $seq_owner->table_name);
$this->assertEquals('uid', $seq_owner->field_name);
$seq_last_value = $connection->query("SELECT last_value FROM $seq_name")->fetchField();
$maximum_uid = $connection->query('SELECT MAX([uid]) FROM {users}')->fetchField();
$this->assertEquals($maximum_uid + 1, $seq_last_value);
}
}
}
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
* Install, update and uninstall functions for the user module. * Install, update and uninstall functions for the user module.
*/ */
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
/** /**
* Implements hook_schema(). * Implements hook_schema().
*/ */
...@@ -97,3 +99,38 @@ function user_install() { ...@@ -97,3 +99,38 @@ function user_install() {
function user_update_last_removed() { function user_update_last_removed() {
return 8100; return 8100;
} }
/**
* Change the users table to use an serial uid field.
*/
function user_update_9301(&$sandbox) {
if (!\Drupal::entityTypeManager()->getStorage('user') instanceof SqlContentEntityStorage) {
return t('The user entity storage is not using an SQL storage, update skipped.');
}
$connection = \Drupal::database();
$connection->schema()->dropPrimaryKey('users');
if ($connection->databaseType() === 'mysql') {
$sql_mode = $connection->query("SELECT @@sql_mode;")->fetchField();
$connection->query("SET sql_mode = '$sql_mode,NO_AUTO_VALUE_ON_ZERO'");
}
$connection->schema()->changeField('users', 'uid', 'uid', ['type' => 'serial', 'not null' => TRUE], ['primary key' => ['uid']]);
if (isset($sql_mode)) {
$connection->query("SET sql_mode = '$sql_mode'");
}
// Update the last installed schema to reflect the change of field type.
$installed_storage_schema = \Drupal::keyValue('entity.storage_schema.sql');
$field_schema_data = $installed_storage_schema->get('user.field_schema_data.uid');
$field_schema_data['users']['fields']['uid']['type'] = 'serial';
$installed_storage_schema->set('user.field_schema_data.uid', $field_schema_data);
// The new PostgreSQL sequence for the uid field needs to start with the last
// used user ID + 1 and the sequence must be owned by uid field.
// @todo https://drupal.org/i/3028706 implement a generic fix.
if ($connection->driver() == 'pgsql') {
$maximum_uid = $connection->query('SELECT MAX([uid]) FROM {users}')->fetchField();
$seq = $connection->makeSequenceName('users', 'uid');
$connection->query("ALTER SEQUENCE " . $seq . " RESTART WITH " . ($maximum_uid + 1) . " OWNED BY {users}.uid");
}
}
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
namespace Drupal\Tests; namespace Drupal\Tests;
use Drupal\Core\Database\Database;
use Drupal\Core\Url; use Drupal\Core\Url;
/** /**
...@@ -107,6 +108,15 @@ protected function runUpdates($update_url = NULL) { ...@@ -107,6 +108,15 @@ protected function runUpdates($update_url = NULL) {
$this->kernel->updateModules($module_handler_list, $module_handler_list); $this->kernel->updateModules($module_handler_list, $module_handler_list);
} }
// Close any open database connections. This allows DB drivers that store
// static information to refresh it in the update runner.
// @todo https://drupal.org/i/3222121 consider doing this in
// \Drupal\Core\DrupalKernel::initializeContainer() for container
// rebuilds.
foreach (Database::getAllConnectionInfo() as $key => $info) {
Database::closeConnection(NULL, $key);
}
// If we have successfully clicked 'Apply pending updates' then we need to // If we have successfully clicked 'Apply pending updates' then we need to
// clear the caches in the update test runner as this has occurred as part // clear the caches in the update test runner as this has occurred as part
// of the updates. // of the updates.
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment