diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php index e71b74d..dc5f5d4 100644 --- a/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php @@ -2,6 +2,7 @@ namespace Drupal\Core\Database\Driver\sqlite; +use Drupal\Component\Uuid\UuidInterface; use Drupal\Core\Database\Database; use Drupal\Core\Database\DatabaseNotFoundException; use Drupal\Core\Database\Connection as DatabaseConnection; @@ -138,6 +139,7 @@ public static function open(array &$connection_options = array()) { $pdo->sqliteCreateFunction('substring_index', array(__CLASS__, 'sqlFunctionSubstringIndex'), 3); $pdo->sqliteCreateFunction('rand', array(__CLASS__, 'sqlFunctionRand')); $pdo->sqliteCreateFunction('regexp', array(__CLASS__, 'sqlFunctionRegexp')); + $pdo->sqliteCreateFunction('UUID', array(__CLASS__, 'sqlFunctionUUID')); // SQLite does not support the LIKE BINARY operator, so we overload the // non-standard GLOB operator for case-sensitive matching. Another option @@ -166,7 +168,6 @@ public static function open(array &$connection_options = array()) { return $pdo; } - /** * Destructor for the SQLite connection. * @@ -330,6 +331,18 @@ public static function sqlFunctionLikeBinary($pattern, $subject) { } /** + * SQLite compatibility implementation for the UUID() SQL function. + * + * @return string + * A UUID string. + */ + public static function sqlFunctionUUID() { + /** @var UuidInterface $generator */ + $generator = \Drupal::service('uuid'); + return $generator->generate(); + } + + /** * {@inheritdoc} */ public function prepare($statement, array $driver_options = array()) { diff --git a/core/lib/Drupal/Core/Database/Schema.php b/core/lib/Drupal/Core/Database/Schema.php index 8b9eb7e..c4bba41 100644 --- a/core/lib/Drupal/Core/Database/Schema.php +++ b/core/lib/Drupal/Core/Database/Schema.php @@ -305,7 +305,7 @@ public function fieldExists($table, $column) { * 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. - * Alternatively, the 'initial_form_field' key may be used, which will + * Alternatively, the 'initial_from_field' key may be used, which will * auto-populate the new field with values from the specified field. * @param $keys_new * (optional) Keys and indexes specification to be created on the diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php index 1d69be2..444684c 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php @@ -17,7 +17,7 @@ * * @ingroup entity_api */ -abstract class ContentEntityBase extends Entity implements \IteratorAggregate, ContentEntityInterface, TranslationStatusInterface { +abstract class ContentEntityBase extends Entity implements \IteratorAggregate, ContentEntityInterface, TranslationStatusInterface, RevisionUuidInterface { /** * The plain data values of the contained fields. @@ -257,6 +257,11 @@ public function setNewRevision($value = TRUE) { // to ensure that a new revision will actually be created. $this->set($this->getEntityType()->getKey('revision'), NULL); + if ($this->getEntityType()->hasKey('revision_uuid')) { + // Also generate a new revision uuid. + $this->set($this->getEntityType()->getKey('revision_uuid'), \Drupal::service('uuid')->generate()); + } + // Make sure that the flag tracking which translations are affected by the // current revision is reset. foreach ($this->translations as $langcode => $data) { @@ -324,6 +329,13 @@ public function getRevisionId() { /** * {@inheritdoc} */ + public function getRevisionUuid() { + return $this->getEntityKey('revision_uuid'); + } + + /** + * {@inheritdoc} + */ public function isTranslatable() { // Check that the bundle is translatable, the entity has a language defined // and if we have more than one language on the site. @@ -1025,6 +1037,12 @@ public function createDuplicate() { // Check whether the entity type supports revisions and initialize it if so. if ($entity_type->isRevisionable()) { $duplicate->{$entity_type->getKey('revision')}->value = NULL; + + // Check if the entity type supports revision UUIDs and generate a new one + // if so. + if ($entity_type->hasKey('revision_uuid')) { + $duplicate->{$entity_type->getKey('revision_uuid')}->value = $this->uuidGenerator()->generate(); + } } return $duplicate; @@ -1167,6 +1185,12 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setReadOnly(TRUE) ->setSetting('unsigned', TRUE); } + if ($entity_type->hasKey('revision_uuid')) { + $fields[$entity_type->getKey('revision_uuid')] = BaseFieldDefinition::create('uuid') + ->setLabel(new TranslatableMarkup('Revision UUID')) + ->setRevisionable(TRUE) + ->setReadOnly(TRUE); + } if ($entity_type->hasKey('langcode')) { $fields[$entity_type->getKey('langcode')] = BaseFieldDefinition::create('language') ->setLabel(new TranslatableMarkup('Language')) @@ -1246,7 +1270,7 @@ public function hasTranslationChanges() { foreach ($this->getFieldDefinitions() as $field_name => $definition) { // @todo Avoid special-casing the following fields. See // https://www.drupal.org/node/2329253. - if ($field_name == 'revision_translation_affected' || $field_name == 'revision_id') { + if ($field_name == 'revision_translation_affected' || $field_name == 'revision_id' || ($this->getEntityType()->hasKey('revision_uuid') && $field_name == $this->getEntityType()->getKey('revision_uuid'))) { continue; } if (!$definition->isComputed() && (!$translated || $definition->isTranslatable())) { diff --git a/core/lib/Drupal/Core/Entity/EntityType.php b/core/lib/Drupal/Core/Entity/EntityType.php index 89b83cf..384dbb4 100644 --- a/core/lib/Drupal/Core/Entity/EntityType.php +++ b/core/lib/Drupal/Core/Entity/EntityType.php @@ -288,10 +288,12 @@ public function __construct($definition) { // Ensure defaults. $this->entity_keys += array( 'revision' => '', + 'revision_uuid' => '', 'bundle' => '', 'langcode' => '', 'default_langcode' => 'default_langcode', ); + $this->handlers += array( 'access' => 'Drupal\Core\Entity\EntityAccessControlHandler', ); diff --git a/core/lib/Drupal/Core/Entity/EntityTypeInterface.php b/core/lib/Drupal/Core/Entity/EntityTypeInterface.php index c6336c9..fc581be 100644 --- a/core/lib/Drupal/Core/Entity/EntityTypeInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityTypeInterface.php @@ -88,6 +88,9 @@ public function getOriginalClass(); * revision ID of the entity. The Field API assumes that all revision IDs * are unique across all entities of a type. If this entry is omitted * the entities of this type are not revisionable. + * - revision_uuid: (optional) The name of the property that contains the + * revision UUID of the entity. Defaults to "revision_uuid" if the + * 'revision' key is set. * - bundle: (optional) The name of the property that contains the bundle * name for the entity. The bundle name defines which set of fields are * attached to the entity (e.g. what nodes call "content type"). This diff --git a/core/lib/Drupal/Core/Entity/RevisionUuidInterface.php b/core/lib/Drupal/Core/Entity/RevisionUuidInterface.php new file mode 100644 index 0000000..ed20b42 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/RevisionUuidInterface.php @@ -0,0 +1,25 @@ +entityType->isRevisionable(); $translatable = $this->entityType->isTranslatable(); @@ -317,7 +317,10 @@ public function getTableMapping(array $storage_definitions = NULL) { // together with the entity ID and the revision ID as identifiers. $table_mapping->setFieldNames($this->baseTable, array_diff($all_fields, $revision_metadata_fields)); $revision_key_fields = array($this->idKey, $this->revisionKey); - $table_mapping->setFieldNames($this->revisionTable, array_merge($revision_key_fields, $revisionable_fields)); + if ($this->entityType->hasKey('revision_uuid')) { + $revision_key_fields[] = $this->entityType->getKey('revision_uuid'); + } + $table_mapping->setFieldNames($this->revisionTable, array_unique(array_merge($revision_key_fields, $revisionable_fields))); } elseif (!$revisionable && $translatable) { // Multilingual layouts store key field values in the base table. The @@ -347,11 +350,14 @@ public function getTableMapping(array $storage_definitions = NULL) { $table_mapping->setFieldNames($this->dataTable, $data_fields); $revision_base_fields = array_merge(array($this->idKey, $this->revisionKey, $this->langcodeKey), $revision_metadata_fields); + if ($this->entityType->hasKey('revision_uuid')) { + $revision_base_fields[] = $this->entityType->getKey('revision_uuid'); + } $table_mapping->setFieldNames($this->revisionTable, $revision_base_fields); $revision_data_key_fields = array($this->idKey, $this->revisionKey, $this->langcodeKey); $revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, array($this->langcodeKey)); - $table_mapping->setFieldNames($this->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields)); + $table_mapping->setFieldNames($this->revisionDataTable, array_unique(array_merge($revision_data_key_fields, $revision_data_fields))); } // Add dedicated tables. diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php index f43461f..6ae9600 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php @@ -176,7 +176,6 @@ public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterfac if ( $storage_definition->hasCustomStorage() != $original->hasCustomStorage() || - $storage_definition->getSchema() != $original->getSchema() || $storage_definition->isRevisionable() != $original->isRevisionable() || $table_mapping->allowsSharedTableStorage($storage_definition) != $table_mapping->allowsSharedTableStorage($original) || $table_mapping->requiresDedicatedTableStorage($storage_definition) != $table_mapping->requiresDedicatedTableStorage($original) @@ -192,7 +191,40 @@ public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterfac return FALSE; } - return $this->getSchemaFromStorageDefinition($storage_definition) != $this->loadFieldSchemaData($original); + $storage_definition_schema = $this->getSchemaFromStorageDefinition($storage_definition); + $original_schema = $this->loadFieldSchemaData($original); + + // Filter out irrelevant schema. + return $this->removeIrrelevantSchemaKeys($storage_definition_schema) != $this->removeIrrelevantSchemaKeys($original_schema); + } + + /** + * Remove irrelevant schema keys. + * + * We need to remove irrelevant schema keys before comparing two schemas to + * see if they've changed as there are certain keys that can safely be added + * at any point to existing schema. E.g. initial or initial_from_field. + * + * @param array $schema + * The schema array. + * @param array $remove_keys + * (optional) An array of keys to remove regardless of the depth. + * + * @return array + * The schema with any irrelevant keys removed. + */ + protected function removeIrrelevantSchemaKeys(&$schema, $remove_keys = ['initial', 'initial_from_field']) { + foreach ($schema as $key => &$value) { + if (is_array($value)) { + $this->removeIrrelevantSchemaKeys($value, $remove_keys); + } + else { + if (in_array($key, $remove_keys, TRUE)) { + unset($schema[$key]); + } + } + } + return $schema; } /** @@ -1153,7 +1185,9 @@ protected function createSharedTableSchema(FieldStorageDefinitionInterface $stor } if (!empty($schema[$table_name]['unique keys'])) { foreach ($schema[$table_name]['unique keys'] as $name => $specifier) { - $schema_handler->addUniqueKey($table_name, $name, $specifier); + // Check if the unique key exists because it might already have + // been created as part of the earlier entity type update event. + $this->addUniqueKey($table_name, $name, $specifier); } } } @@ -1944,7 +1978,19 @@ protected function isTableEmpty($table_name) { * Returns TRUE if there are schema changes in the column definitions. */ protected function hasColumnChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { - if ($storage_definition->getColumns() != $original->getColumns()) { + // Remove any fields that can be changed in the column array structure such + // as initial. + $new_columns = $storage_definition->getColumns(); + $original_columns = $original->getColumns(); + foreach ($new_columns as $delta => $column) { + foreach ($column as $key => $value) { + if (in_array($key, ['initial', 'initial_from_field'], TRUE)) { + unset($new_columns[$delta][$key]); + unset($original_columns[$delta][$key]); + } + } + } + if ($new_columns != $original_columns) { // Base field definitions have schema data stored in the original // definition. return TRUE; diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/UuidItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/UuidItem.php index 94aa8aa..f5ed4dc 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/UuidItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/UuidItem.php @@ -44,7 +44,10 @@ public function applyDefaultValue($notify = TRUE) { */ public static function schema(FieldStorageDefinitionInterface $field_definition) { $schema = parent::schema($field_definition); - $schema['unique keys']['value'] = array('value'); + if (!$field_definition->isRevisionable()) { + $schema['unique keys']['value'] = array('value'); + } + $schema['columns']['value']['initial_from_field'] = 'UUID()'; return $schema; } diff --git a/core/modules/block_content/src/Entity/BlockContent.php b/core/modules/block_content/src/Entity/BlockContent.php index 13af342..d1cdbbd 100644 --- a/core/modules/block_content/src/Entity/BlockContent.php +++ b/core/modules/block_content/src/Entity/BlockContent.php @@ -45,6 +45,7 @@ * entity_keys = { * "id" = "id", * "revision" = "revision_id", + * "revision_uuid" = "revision_uuid", * "bundle" = "type", * "label" = "info", * "langcode" = "langcode", diff --git a/core/modules/comment/src/CommentStorage.php b/core/modules/comment/src/CommentStorage.php index cc4fdcf..0931f41 100644 --- a/core/modules/comment/src/CommentStorage.php +++ b/core/modules/comment/src/CommentStorage.php @@ -2,6 +2,7 @@ namespace Drupal\comment; +use Drupal\Component\Uuid\UuidInterface; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Database\Connection; use Drupal\Core\Entity\EntityManagerInterface; @@ -43,9 +44,11 @@ class CommentStorage extends SqlContentEntityStorage implements CommentStorageIn * Cache backend instance to use. * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager * The language manager. + * @param \Drupal\Component\Uuid\UuidInterface $uuid_service + * The UUID service. */ - public function __construct(EntityTypeInterface $entity_info, Connection $database, EntityManagerInterface $entity_manager, AccountInterface $current_user, CacheBackendInterface $cache, LanguageManagerInterface $language_manager) { - parent::__construct($entity_info, $database, $entity_manager, $cache, $language_manager); + public function __construct(EntityTypeInterface $entity_info, Connection $database, EntityManagerInterface $entity_manager, AccountInterface $current_user, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, UuidInterface $uuid_service) { + parent::__construct($entity_info, $database, $entity_manager, $cache, $language_manager, $uuid_service); $this->currentUser = $current_user; } @@ -59,7 +62,8 @@ public static function createInstance(ContainerInterface $container, EntityTypeI $container->get('entity.manager'), $container->get('current_user'), $container->get('cache.entity'), - $container->get('language_manager') + $container->get('language_manager'), + $container->get('uuid') ); } diff --git a/core/modules/node/src/Entity/Node.php b/core/modules/node/src/Entity/Node.php index d4e4715..664a47d 100644 --- a/core/modules/node/src/Entity/Node.php +++ b/core/modules/node/src/Entity/Node.php @@ -51,6 +51,7 @@ * entity_keys = { * "id" = "nid", * "revision" = "vid", + * "revision_uuid" = "revision_uuid", * "bundle" = "type", * "label" = "title", * "langcode" = "langcode", diff --git a/core/modules/node/src/Tests/Views/FilterUidRevisionTest.php b/core/modules/node/src/Tests/Views/FilterUidRevisionTest.php index 017d5eb..083db64 100644 --- a/core/modules/node/src/Tests/Views/FilterUidRevisionTest.php +++ b/core/modules/node/src/Tests/Views/FilterUidRevisionTest.php @@ -35,7 +35,7 @@ public function testFilter() { $expected_result[] = array('nid' => $node->id()); $revision = clone $node; // Force to add a new revision. - $revision->set('vid', NULL); + $revision->setNewRevision(TRUE); $revision->set('revision_uid', $author->id()); $revision->save(); diff --git a/core/modules/system/src/Tests/Entity/EntityDefinitionTestTrait.php b/core/modules/system/src/Tests/Entity/EntityDefinitionTestTrait.php index 0123597..0dc7053 100644 --- a/core/modules/system/src/Tests/Entity/EntityDefinitionTestTrait.php +++ b/core/modules/system/src/Tests/Entity/EntityDefinitionTestTrait.php @@ -36,6 +36,7 @@ protected function updateEntityTypeToRevisionable() { $keys = $entity_type->getKeys(); $keys['revision'] = 'revision_id'; + $keys['revision_uuid'] = 'revision_uuid'; $entity_type->set('entity_keys', $keys); $this->state->set('entity_test_update.entity_type', $entity_type); diff --git a/core/modules/system/system.install b/core/modules/system/system.install index e8f45eb..79d97ce 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -1768,3 +1768,35 @@ function system_update_8201() { /** * @} End of "addtogroup updates-8.2.0". */ + +/** + * @addtogroup updates-8.3.0 + * @{ + */ + +/** + * Install new revision_uuid schema. + */ +function system_update_8300() { + foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type_id => $entity_type) { + /** @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface $manager */ + $manager = \Drupal::entityDefinitionUpdateManager(); + + // Install the new revision_uuid field. + if ($entity_type->hasKey('revision_uuid')) { + $field_manager = \Drupal::service('entity_field.manager'); + $field_storage_definitions = $field_manager->getFieldStorageDefinitions($entity_type_id); + $manager->installFieldStorageDefinition($entity_type->getKey('revision_uuid'), $entity_type_id, $entity_type->getProvider(), $field_storage_definitions[$entity_type->getKey('revision_uuid')]); + } + + // Update the existing uuid field which has a new schema. + if ($entity_type->hasKey('uuid') && $definition = $manager->getFieldStorageDefinition('uuid', $entity_type_id)) { + $manager->updateFieldStorageDefinition($definition); + } + } +} + +/** + * @} End of "addtogroup updates-8.3.0". + */ + diff --git a/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.module b/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.module index 45892a8..ecd9da6 100644 --- a/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.module +++ b/core/modules/system/tests/modules/entity_schema_test/entity_schema_test.module @@ -20,6 +20,7 @@ function entity_schema_test_entity_type_alter(array &$entity_types) { $entity_type->set('data_table', 'entity_test_field_data'); $keys = $entity_type->getKeys(); $keys['revision'] = 'revision_id'; + $keys['revision_uuid'] = 'revision_uuid'; $entity_type->set('entity_keys', $keys); } } diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php index 23cf034..8b6b7e4 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php @@ -102,6 +102,7 @@ public function testEntityTypeUpdateWithoutData() { $expected = array( 'entity_test_update' => array( t('The %entity_type entity type needs to be updated.', ['%entity_type' => $this->entityManager->getDefinition('entity_test_update')->getLabel()]), + t('The %field_name field needs to be installed.', ['%field_name' => $this->entityManager->getFieldStorageDefinitions('entity_test_update')['revision_uuid']->getLabel()]), ), ); $this->assertEqual($this->entityDefinitionUpdateManager->getChangeSummary(), $expected); //, 'EntityDefinitionUpdateManager reports the expected change summary.'); diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityFieldTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityFieldTest.php index 56af888..e51b132 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityFieldTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityFieldTest.php @@ -572,6 +572,9 @@ protected function doTestDataStructureInterfaces($entity_type) { // Field format. NULL, ); + if ($uuid = $entity->getRevisionUuid()) { + $target_strings[] = $uuid; + } asort($strings); asort($target_strings); $this->assertEqual(array_values($strings), array_values($target_strings), format_string('%entity_type: All contained strings found.', array('%entity_type' => $entity_type))); diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityUUIDTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityUUIDTest.php index ae26ec5..2541264 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityUUIDTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityUUIDTest.php @@ -97,6 +97,10 @@ protected function assertCRUD($entity_type) { $this->assertNotEqual($entity_duplicate->getRevisionId(), $entity->getRevisionId()); $this->assertNotEqual($entity_duplicate->{$property}->getValue(), $entity->{$property}->getValue()); break; + case 'revision_uuid': + $this->assertNotNull($entity->getRevisionUuid()); + $this->assertNotEqual($entity_duplicate->getRevisionUuid(), $entity->getRevisionUuid()); + break; default: $this->assertEqual($entity_duplicate->{$property}->getValue(), $entity->{$property}->getValue()); } diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityTypeTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityTypeTest.php index d633afc..7f2f67a 100644 --- a/core/tests/Drupal/Tests/Core/Entity/EntityTypeTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/EntityTypeTest.php @@ -117,9 +117,9 @@ public function providerTestSet() { */ public function providerTestGetKeys() { return array( - array(array(), array('revision' => '', 'bundle' => '', 'langcode' => '')), - array(array('id' => 'id'), array('id' => 'id', 'revision' => '', 'bundle' => '', 'langcode' => '')), - array(array('bundle' => 'bundle'), array('bundle' => 'bundle', 'revision' => '', 'langcode' => '')), + array(array(), array('revision' => '', 'revision_uuid' => '', 'bundle' => '', 'langcode' => '')), + array(array('id' => 'id'), array('id' => 'id', 'revision' => '', 'revision_uuid' => '', 'bundle' => '', 'langcode' => '')), + array(array('bundle' => 'bundle'), array('bundle' => 'bundle', 'revision' => '', 'revision_uuid' => '', 'langcode' => '')), ); }