diff --git a/core/lib/Drupal/Core/Entity/Sql/RevisionableSchemaConverter.php b/core/lib/Drupal/Core/Entity/Sql/RevisionableSchemaConverter.php new file mode 100644 index 0000000..7fda795 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Sql/RevisionableSchemaConverter.php @@ -0,0 +1,338 @@ +entityTypeManager = $entity_type_manager; + $this->entityDefinitionUpdateManager = $entity_definition_update_manager; + $this->lastInstalledSchemaRepository = $last_installed_schema_repository; + $this->database = $database; + } + + /** + * Converts an entity type with existing data to be revisionable. + * + * The process works like this: + * - rename the existing entity tables to temporary names; + * - create the schema from scratch with the new revisionable entity type + * definition (i.e. the current definition of the entity type from code); + * - load the old data by using the original last install entity definition + * and a temporary table mapping class, which knows how to access the + * temporary tables created in the first step; + * - save the data using the current revisionable entity type definition + * - at the end of the process, remove the temporary tables. + * + * @param array $sandbox + * The sandbox array from a hook_update_N() implementation. + * @param string $entity_type_id + * The entity type ID. + * @param string[] $fields_to_update + * (optional) An array of field names that should be converted to be + * revisionable. Note that the 'langcode' field, if present, is updated + * automatically. Defaults to an empty array. + */ + public function convertToRevisionable(array &$sandbox, $entity_type_id, array $fields_to_update = []) { + // If 'progress' is not set, then this will be the first run of the batch. + if (!isset($sandbox['progress'])) { + $original_entity_type = $this->lastInstalledSchemaRepository->getLastInstalledDefinition($entity_type_id); + $original_storage_definitions = $this->lastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions($entity_type_id); + $sandbox['original_entity_type'] = $original_entity_type; + $sandbox['original_storage_definitions'] = $original_storage_definitions; + + /** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */ + $storage = $this->entityTypeManager->getStorage($entity_type_id); + $storage->setEntityType($original_entity_type); + + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $storage->getTableMapping($original_storage_definitions); + + // Rename the existing entity tables to temporary table names. + $original_table_names = $table_mapping->getTableNames(); + foreach ($original_table_names as $table_name) { + $temp_table_name = TemporaryTableMapping::getTempTableName($table_name); + $this->database->schema()->renameTable($table_name, $temp_table_name); + } + + // Create a temporary table mapping object that will be used to retrieve + // data from the original entity tables. + $temporary_entity_type = clone $original_entity_type; + $temporary_entity_type->set('base_table', TemporaryTableMapping::getTempTableName($original_entity_type->getBaseTable())); + if ($temporary_entity_type->isTranslatable()) { + $temporary_entity_type->set('data_table', TemporaryTableMapping::getTempTableName($original_entity_type->getDataTable())); + } + $sandbox['temporary_entity_type'] = $temporary_entity_type; + $sandbox['temporary_table_mapping'] = $this->getTemporaryTableMapping($temporary_entity_type, $original_storage_definitions); + + // Updating the entity type definition is safe to do at this stage because + // its tables have been already moved to temporary locations, so it is + // considered as not having any data. + $this->entityTypeManager->useCaches(FALSE); + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + + // Instruct the entity schema handler that data migration is being handled + // independently. + $entity_type->requiresDataMigration = FALSE; + + $this->entityDefinitionUpdateManager->updateEntityType($entity_type); + $sandbox['entity_type'] = $entity_type; + } + + // Copy over the existing data to the new temporary tables. + $this->copyData($sandbox); + + // If the data copying has finished successfully, we can drop the temporary + // tables and call the appropriate update mechanisms. + if ($sandbox['#finished'] === 1) { + // Update the field storage definitions. + $this->updateFieldStorageDefinitions($sandbox['entity_type'], $sandbox['original_storage_definitions'], $fields_to_update); + + foreach ($sandbox['temporary_table_mapping']->getTableNames() as $temporary_table) { + $this->database->schema()->dropTable($temporary_table); + } + } + } + + /** + * Loads entities from the temporary storage and re-saves them to the new one. + * + * @param array $sandbox + * The sandbox array from a hook_update_N() implementation. + */ + protected function copyData(array &$sandbox) { + /** @var \Drupal\Core\Entity\Sql\TemporaryTableMapping $temporary_table_mapping */ + $temporary_table_mapping = $sandbox['temporary_table_mapping']; + $temporary_entity_type = $sandbox['temporary_entity_type']; + $entity_type = $sandbox['entity_type']; + + // If 'progress' is not set, then this will be the first run of the batch. + $temporary_base_table = $temporary_entity_type->getBaseTable(); + if (!isset($sandbox['progress'])) { + $sandbox['progress'] = 0; + $sandbox['current_id'] = 0; + $sandbox['max'] = $this->database->select($temporary_base_table) + ->countQuery() + ->execute() + ->fetchField(); + } + + $id = $temporary_entity_type->getKey('id'); + + // Get the next 10 entity IDs to migrate. + $entity_ids = $this->database->select($temporary_base_table) + ->fields($temporary_base_table, [$id]) + ->condition($id, $sandbox['current_id'], '>') + ->range(0, 10) + ->execute() + ->fetchAllKeyed(0, 0); + + /** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */ + $storage = $this->entityTypeManager->getStorage($temporary_entity_type->id()); + $storage->setEntityType($temporary_entity_type); + $storage->setTableMapping($temporary_table_mapping); + + $temporary_entities = $storage->loadMultiple($entity_ids); + + // Now inject the updated entity type definition in the storage and re-save + // the entities. This will also reset the internal table mapping. + $storage->setEntityType($entity_type); + + foreach ($temporary_entities as $entity_id => $entity) { + $revision_id_key = $entity_type->getKey('revision'); + + // Manually set the original entity since it cannot be read from the + // temporary storage anymore. + $entity->original = $entity; + + // Set the revision ID to be same as the entity ID. + $entity->set($revision_id_key, $entity_id); + + // Treat the entity as new in order to make the storage do an INSERT + // rather than an UPDATE. + $entity->enforceIsNew(TRUE); + + // Finally, save the entity in the new revisionable storage. + $storage->save($entity); + + $sandbox['progress']++; + $sandbox['current_id'] = $entity_id; + } + $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']); + } + + /** + * Updates field definitions to be revisionable. + * + * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type + * A content entity type definition. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions + * An array of field storage definitions. + * @param array $fields_to_update + * (optional) An array of field names for which to enable revision support. + * Defaults to an empty array. + * + * @return \Drupal\Core\Field\FieldStorageDefinitionInterface[] + * An array of updated field storage definitions. + */ + protected function updateFieldStorageDefinitions(ContentEntityTypeInterface $entity_type, array $storage_definitions, array $fields_to_update = []) { + // Update the 'langcode' field manually, as it is configured in the base + // content entity field definitions. + if ($entity_type->hasKey('langcode')) { + $fields_to_update = array_merge([$entity_type->getKey('langcode')], $fields_to_update); + } + + foreach ($fields_to_update as $field_name) { + // Configurable fields are always revisionable, so we only need to care + // about base fields. + if ($storage_definitions[$field_name]->isBaseField()) { + $storage_definitions[$field_name]->setRevisionable(TRUE); + $this->entityDefinitionUpdateManager->updateFieldStorageDefinition($storage_definitions[$field_name]); + } + } + + // Add the revision ID field. + $revision_field = BaseFieldDefinition::create('integer') + ->setName($entity_type->getKey('revision')) + ->setTargetEntityTypeId($entity_type->id()) + ->setTargetBundle(NULL) + ->setLabel(new TranslatableMarkup('Revision ID')) + ->setReadOnly(TRUE) + ->setSetting('unsigned', TRUE); + $this->entityDefinitionUpdateManager->installFieldStorageDefinition($revision_field->getName(), $entity_type->id(), $entity_type->getProvider(), $revision_field); + + $storage_definitions[$entity_type->getKey('revision')] = $revision_field; + + return $storage_definitions; + } + + /** + * Gets a temporary table mapping for the entity's SQL tables. + * + * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type + * A content entity type definition. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions + * An array of field storage definitions to be used to compute the table + * mapping. + * + * @return \Drupal\Core\Entity\Sql\TemporaryTableMapping + * A table mapping object for the entity's tables. + * + * @todo Remove this when https://www.drupal.org/node/2274017 is fixed. + */ + protected function getTemporaryTableMapping(ContentEntityTypeInterface $entity_type, array $storage_definitions) { + $table_mapping = new TemporaryTableMapping($entity_type, $storage_definitions); + + $translatable = $entity_type->isTranslatable(); + $base_table = $entity_type->getBaseTable(); + if ($translatable) { + $data_table = $entity_type->getDataTable(); + } + + $id_key = $entity_type->getKey('id'); + $revision_key = $entity_type->getKey('revision'); + $bundle_key = $entity_type->getKey('bundle'); + $uuid_key = $entity_type->getKey('uuid'); + $langcode_key = $entity_type->getKey('langcode'); + + $shared_table_definitions = array_filter($storage_definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) { + return $table_mapping->allowsSharedTableStorage($definition); + }); + + $key_fields = array_values(array_filter(array($id_key, $revision_key, $bundle_key, $uuid_key, $langcode_key))); + $all_fields = array_keys($shared_table_definitions); + // Make sure the key fields come first in the list of fields. + $all_fields = array_merge($key_fields, array_diff($all_fields, $key_fields)); + + if (!$translatable) { + // The base layout stores all the base field values in the base table. + $table_mapping->setFieldNames($base_table, $all_fields); + } + else { + // Multilingual layouts store key field values in the base table. The + // other base field values are stored in the data table, no matter + // whether they are translatable or not. The data table holds also a + // denormalized copy of the bundle field value to allow for more + // performant queries. This means that only the UUID is not stored on + // the data table. + $table_mapping + ->setFieldNames($base_table, $key_fields) + ->setFieldNames($data_table, array_values(array_diff($all_fields, array($uuid_key)))); + } + + // Add dedicated tables. + $dedicated_table_definitions = array_filter($storage_definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) { + return $table_mapping->requiresDedicatedTableStorage($definition); + }); + $extra_columns = array( + 'bundle', + 'deleted', + 'entity_id', + 'revision_id', + 'langcode', + 'delta', + ); + foreach ($dedicated_table_definitions as $field_name => $definition) { + $tables = [$table_mapping->getDedicatedDataTableName($definition)]; + foreach ($tables as $table_name) { + $table_mapping->setFieldNames($table_name, array($field_name)); + $table_mapping->setExtraColumns($table_name, $extra_columns); + } + } + + return $table_mapping; + } + +} diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php index 8031830..8c08b09 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php @@ -267,6 +267,18 @@ public function setEntityType(EntityTypeInterface $entity_type) { } /** + * Sets the wrapped table mapping definition. + * + * @param \Drupal\Core\Entity\Sql\TableMappingInterface $table_mapping + * The table mapping. + * + * @internal Only to be used internally by Entity API. + */ + public function setTableMapping(TableMappingInterface $table_mapping) { + $this->tableMapping = $table_mapping; + } + + /** * {@inheritdoc} */ public function getTableMapping(array $storage_definitions = NULL) { diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php index f43461f..9f4901b 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php @@ -228,6 +228,12 @@ protected function getSchemaFromStorageDefinition(FieldStorageDefinitionInterfac * {@inheritdoc} */ public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) { + // Check if the entity type specifies that data migration is being handled + // elsewhere. + if (isset($entity_type->requiresDataMigration) && !$entity_type->requiresDataMigration) { + return FALSE; + } + // If the original storage has existing entities, or it is impossible to // determine if that is the case, require entity data to be migrated. $original_storage_class = $original->getStorageClass(); @@ -1138,6 +1144,12 @@ protected function createSharedTableSchema(FieldStorageDefinitionInterface $stor $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names); if (!$only_save) { foreach ($schema[$table_name]['fields'] as $name => $specifier) { + // Use the value of the entity ID as the initial value for the + // revision ID field. + if ($created_field_name == $this->entityType->getKey('revision')) { + $id_column_name = $table_mapping->getColumnNames($this->entityType->getKey('id')); + $specifier['initial_from_field'] = reset($id_column_name); + } // Check if the field exists because it might already have been // created as part of the earlier entity type update event. if (!$schema_handler->fieldExists($table_name, $name)) { @@ -1185,10 +1197,14 @@ protected function deleteDedicatedTableSchema(FieldStorageDefinitionInterface $s $deleted = !$this->originalDefinitions; $table_mapping = $this->storage->getTableMapping(); $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $deleted); - $this->database->schema()->dropTable($table_name); + if ($this->database->schema()->tableExists($table_name)) { + $this->database->schema()->dropTable($table_name); + } if ($this->entityType->isRevisionable()) { - $revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $deleted); - $this->database->schema()->dropTable($revision_name); + $revision_table_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $deleted); + if ($this->database->schema()->tableExists($revision_table_name)) { + $this->database->schema()->dropTable($revision_table_name); + } } $this->deleteFieldSchemaData($storage_definition); } diff --git a/core/lib/Drupal/Core/Entity/Sql/TemporaryTableMapping.php b/core/lib/Drupal/Core/Entity/Sql/TemporaryTableMapping.php new file mode 100644 index 0000000..b0f01e0 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Sql/TemporaryTableMapping.php @@ -0,0 +1,45 @@ + 48) { + $short_table_name = substr($table_name, 0, 34); + $table_hash = substr(hash('sha256', $table_name), 0, 10); + + $tmp_table_name = $prefix . $short_table_name . $table_hash; + } + return $tmp_table_name; + } + +} diff --git a/core/modules/system/tests/fixtures/update/drupal-8.entity-test-to-rev-conversion-enabled.php b/core/modules/system/tests/fixtures/update/drupal-8.entity-test-to-rev-conversion-enabled.php new file mode 100644 index 0000000..fd77660 --- /dev/null +++ b/core/modules/system/tests/fixtures/update/drupal-8.entity-test-to-rev-conversion-enabled.php @@ -0,0 +1,30 @@ +select('key_value') + ->fields('key_value', ['value']) + ->condition('collection', 'post_update') + ->condition('name', 'existing_updates') + ->execute() + ->fetchField(); +$existing_updates = unserialize($existing_updates); +if ($key = array_search('entity_test_revisionable_schema_converter_post_update_make_revisionable', $existing_updates)) { + unset($existing_updates[$key]); +} +$connection->update('key_value') + ->fields([ + 'value' => serialize($existing_updates), + ]) + ->condition('collection', 'post_update') + ->condition('name', 'existing_updates') + ->execute(); diff --git a/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml b/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml index 464a007..88c831b 100644 --- a/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml +++ b/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml @@ -14,6 +14,16 @@ field.storage_settings.shape: type: string label: 'Foreign key name' +field.value.shape: + type: mapping + label: 'Default value' + mapping: + shape: + type: sequence + sequence: + type: string + label: 'Shape' + entity_test.entity_test_bundle.*: type: config_entity label: 'Entity test bundle' diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/ShapeItem.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/ShapeItem.php index 0d2ebb9..acaf919 100644 --- a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/ShapeItem.php +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/ShapeItem.php @@ -59,11 +59,11 @@ public static function schema(FieldStorageDefinitionInterface $field_definition) 'columns' => array( 'shape' => array( 'type' => 'varchar', - 'length' => 32, + 'length' => 64, ), 'color' => array( 'type' => 'varchar', - 'length' => 32, + 'length' => 64, ), ), ) + $foreign_keys; diff --git a/core/modules/system/tests/modules/entity_test_revisionable_schema_converter/config/install/field.field.entity_test_to_rev_conversion.entity_test_to_rev_conversion.field_test_configurable_field.yml b/core/modules/system/tests/modules/entity_test_revisionable_schema_converter/config/install/field.field.entity_test_to_rev_conversion.entity_test_to_rev_conversion.field_test_configurable_field.yml new file mode 100644 index 0000000..e7bbeea --- /dev/null +++ b/core/modules/system/tests/modules/entity_test_revisionable_schema_converter/config/install/field.field.entity_test_to_rev_conversion.entity_test_to_rev_conversion.field_test_configurable_field.yml @@ -0,0 +1,22 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.entity_test_to_rev_conversion.field_test_configurable_field + module: + - entity_test + - entity_test_revisionable_schema_converter +id: entity_test_to_rev_conversion.entity_test_to_rev_conversion.field_test_configurable_field +field_name: field_test_configurable_field +entity_type: entity_test_to_rev_conversion +bundle: entity_test_to_rev_conversion +label: 'Test configurable field' +description: '' +required: false +translatable: true +default_value: + - + shape: { } +default_value_callback: '' +settings: { } +field_type: shape diff --git a/core/modules/system/tests/modules/entity_test_revisionable_schema_converter/config/install/field.storage.entity_test_to_rev_conversion.field_test_configurable_field.yml b/core/modules/system/tests/modules/entity_test_revisionable_schema_converter/config/install/field.storage.entity_test_to_rev_conversion.field_test_configurable_field.yml new file mode 100644 index 0000000..3af4f30 --- /dev/null +++ b/core/modules/system/tests/modules/entity_test_revisionable_schema_converter/config/install/field.storage.entity_test_to_rev_conversion.field_test_configurable_field.yml @@ -0,0 +1,19 @@ +langcode: en +status: true +dependencies: + module: + - entity_test + - entity_test_revisionable_schema_converter +id: entity_test_to_rev_conversion.field_test_configurable_field +field_name: field_test_configurable_field +entity_type: entity_test_to_rev_conversion +type: shape +settings: + foreign_key_name: shape +module: entity_test +locked: false +cardinality: -1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/core/modules/system/tests/modules/entity_test_revisionable_schema_converter/entity_test_revisionable_schema_converter.info.yml b/core/modules/system/tests/modules/entity_test_revisionable_schema_converter/entity_test_revisionable_schema_converter.info.yml new file mode 100644 index 0000000..1e9cdd7 --- /dev/null +++ b/core/modules/system/tests/modules/entity_test_revisionable_schema_converter/entity_test_revisionable_schema_converter.info.yml @@ -0,0 +1,8 @@ +name: 'Entity Revisionable Schema Converter Test' +type: module +description: 'Provides an entity type for testing the revisionable schema converter.' +package: Testing +version: VERSION +core: 8.x +dependencies: + - entity_test diff --git a/core/modules/system/tests/modules/entity_test_revisionable_schema_converter/entity_test_revisionable_schema_converter.module b/core/modules/system/tests/modules/entity_test_revisionable_schema_converter/entity_test_revisionable_schema_converter.module new file mode 100644 index 0000000..9e50cc8 --- /dev/null +++ b/core/modules/system/tests/modules/entity_test_revisionable_schema_converter/entity_test_revisionable_schema_converter.module @@ -0,0 +1,27 @@ +id() == 'entity_test_to_rev_conversion') { + $fields = array(); + $fields['test_entity_base_field_info'] = BaseFieldDefinition::create('string') + ->setLabel(new TranslatableMarkup('Field added by hook_entity_base_field_info()')) + ->setTranslatable(TRUE) + ->setRevisionable(TRUE); + + return $fields; + } +} diff --git a/core/modules/system/tests/modules/entity_test_revisionable_schema_converter/entity_test_revisionable_schema_converter.post_update.php b/core/modules/system/tests/modules/entity_test_revisionable_schema_converter/entity_test_revisionable_schema_converter.post_update.php new file mode 100644 index 0000000..e541c11 --- /dev/null +++ b/core/modules/system/tests/modules/entity_test_revisionable_schema_converter/entity_test_revisionable_schema_converter.post_update.php @@ -0,0 +1,38 @@ +convertToRevisionable( + $sandbox, + 'entity_test_to_rev_conversion', + [ + 'test_single_property', + 'test_multiple_properties', + 'test_single_property_multiple_values', + 'test_multiple_properties_multiple_values', + 'test_entity_base_field_info', + ]); +} + +/** + * @} End of "addtogroup updates-8.3.x". + */ diff --git a/core/modules/system/tests/modules/entity_test_revisionable_schema_converter/src/Entity/EntityTestToRevConversion.php b/core/modules/system/tests/modules/entity_test_revisionable_schema_converter/src/Entity/EntityTestToRevConversion.php new file mode 100644 index 0000000..8d54231 --- /dev/null +++ b/core/modules/system/tests/modules/entity_test_revisionable_schema_converter/src/Entity/EntityTestToRevConversion.php @@ -0,0 +1,86 @@ +setLabel(new TranslatableMarkup('Field with a single property')) + ->setTranslatable(TRUE) + ->setRevisionable(TRUE); + + $fields['test_multiple_properties'] = BaseFieldDefinition::create('shape') + ->setLabel(new TranslatableMarkup('Field with a multiple property')) + ->setTranslatable(TRUE) + ->setRevisionable(TRUE); + + $fields['test_single_property_multiple_values'] = BaseFieldDefinition::create('string') + ->setLabel(new TranslatableMarkup('Field with a single property and multiple values')) + ->setCardinality(2) + ->setTranslatable(TRUE) + ->setRevisionable(TRUE); + + $fields['test_multiple_properties_multiple_values'] = BaseFieldDefinition::create('shape') + ->setLabel(new TranslatableMarkup('Field with a multiple properties and multiple values')) + ->setCardinality(2) + ->setTranslatable(TRUE) + ->setRevisionable(TRUE); + + return $fields; + } + +} diff --git a/core/tests/Drupal/Tests/Core/Entity/RevisionableSchemaConverterTest.php b/core/tests/Drupal/Tests/Core/Entity/RevisionableSchemaConverterTest.php new file mode 100644 index 0000000..55acd15 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Entity/RevisionableSchemaConverterTest.php @@ -0,0 +1,204 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../../../modules/system/tests/fixtures/update/drupal-8.2.0-filled.standard.entity_test_to_rev_conversion.php.gz', + __DIR__ . '/../../../../../modules/system/tests/fixtures/update/drupal-8.entity-test-to-rev-conversion-enabled.php', + ]; + } + + /** + * Tests the entity type is revisionable and shortcut entities are accessible. + */ + public function testMakeRevisionable() { + /** @var \Drupal\Core\Entity\Sql\SqlEntityStorageInterface $storage */ + $storage = $this->container->get('entity_type.manager')->getStorage('entity_test_to_rev_conversion'); + + // Keep a list of all entity table names so we can check that the temporary + // ones are being removed at the end. + $table_names = $storage->getTableMapping()->getTableNames(); + + // Check that entity type is not revisionable prior to running the update + // process. + $entity_test_to_rev_conversion = $this->container->get('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_to_rev_conversion'); + $this->assertFalse($entity_test_to_rev_conversion->isRevisionable()); + + $this->runUpdates(); + + /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_test_to_rev_conversion */ + $entity_test_to_rev_conversion = $this->container->get('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_to_rev_conversion'); + $this->assertTrue($entity_test_to_rev_conversion->isRevisionable()); + + $this->assertEqual(count($storage->loadMultiple()), 101, 'All test entities were found.'); + + // Check that each field value was copied correctly to the revision tables. + for ($i = 1; $i <= 101; $i++) { + $revision = $storage->loadRevision($i); + + $this->assertEqual($i . ' - test single property', $revision->test_single_property->value); + + $this->assertEqual($i . ' - test multiple properties - shape', $revision->test_multiple_properties->shape); + $this->assertEqual($i . ' - test multiple properties - color', $revision->test_multiple_properties->color); + + $this->assertEqual($i . ' - test single property multiple values 0', $revision->test_single_property_multiple_values->value); + $this->assertEqual($i . ' - test single property multiple values 1', $revision->test_single_property_multiple_values[1]->value); + + $this->assertEqual($i . ' - test multiple properties multiple values - shape 0', $revision->test_multiple_properties_multiple_values[0]->shape); + $this->assertEqual($i . ' - test multiple properties multiple values - color 0', $revision->test_multiple_properties_multiple_values[0]->color); + $this->assertEqual($i . ' - test multiple properties multiple values - shape 1', $revision->test_multiple_properties_multiple_values[1]->shape); + $this->assertEqual($i . ' - test multiple properties multiple values - color 1', $revision->test_multiple_properties_multiple_values[1]->color); + + $this->assertEqual($i . ' - field test configurable field - shape 0', $revision->field_test_configurable_field[0]->shape); + $this->assertEqual($i . ' - field test configurable field - color 0', $revision->field_test_configurable_field[0]->color); + $this->assertEqual($i . ' - field test configurable field - shape 1', $revision->field_test_configurable_field[1]->shape); + $this->assertEqual($i . ' - field test configurable field - color 1', $revision->field_test_configurable_field[1]->color); + + $this->assertEqual($i . ' - test entity base field info', $revision->test_entity_base_field_info->value); + + // Do the same checks for translated field values. + $translation = $revision->getTranslation('ro'); + + $this->assertEqual($i . ' - test single property - ro', $translation->test_single_property->value); + + $this->assertEqual($i . ' - test multiple properties - shape - ro', $translation->test_multiple_properties->shape); + $this->assertEqual($i . ' - test multiple properties - color - ro', $translation->test_multiple_properties->color); + + $this->assertEqual($i . ' - test single property multiple values 0 - ro', $translation->test_single_property_multiple_values[0]->value); + $this->assertEqual($i . ' - test single property multiple values 1 - ro', $translation->test_single_property_multiple_values[1]->value); + + $this->assertEqual($i . ' - test multiple properties multiple values - shape 0 - ro', $translation->test_multiple_properties_multiple_values[0]->shape); + $this->assertEqual($i . ' - test multiple properties multiple values - color 0 - ro', $translation->test_multiple_properties_multiple_values[0]->color); + $this->assertEqual($i . ' - test multiple properties multiple values - shape 1 - ro', $translation->test_multiple_properties_multiple_values[1]->shape); + $this->assertEqual($i . ' - test multiple properties multiple values - color 1 - ro', $translation->test_multiple_properties_multiple_values[1]->color); + + $this->assertEqual($i . ' - field test configurable field - shape 0 - ro', $translation->field_test_configurable_field[0]->shape); + $this->assertEqual($i . ' - field test configurable field - color 0 - ro', $translation->field_test_configurable_field[0]->color); + $this->assertEqual($i . ' - field test configurable field - shape 1 - ro', $translation->field_test_configurable_field[1]->shape); + $this->assertEqual($i . ' - field test configurable field - color 1 - ro', $translation->field_test_configurable_field[1]->color); + + $this->assertEqual($i . ' - test entity base field info - ro', $translation->test_entity_base_field_info->value); + } + + // Check that temporary tables have been removed at the end of the process. + $schema = \Drupal::database()->schema(); + foreach ($table_names as $table_name) { + $this->assertFalse($schema->tableExists(TemporaryTableMapping::getTempTableName($table_name))); + } + } + + /** + * Creates entities for the database dump used in this update test. + * + * This method is only useful for generating test content for + * drupal-8.2.0-filled.standard.entity_test_to_rev_conversion.php.gz and it is + * NOT used during the test execution. + * + * That database dump also has the following characteristics: + * - Standard profile; + * - The 'entity_test_revisionable_schema_converter' module is enabled with + * the following changes: + * - The 'entity_test_to_rev_conversion' entity type does NOT define + * revision and revision data tables, or a 'revision' entity key; + * - All the fields for the 'entity_test_to_rev_conversion' entity type are + * NOT marked as revisionable. + * - Language and Content Translation modules enabled; + * - The Romanian language added; + * - All fields from the 'entity_test_to_rev_conversion' entity type are + * marked as translatable through Content Translation settings. + */ + public static function createTestEntities() { + // Create 101 items so the batch upgrade runs at least three times. + for ($i = 1; $i <= 101; $i++) { + $entity = EntityTestToRevConversion::create([ + 'name' => $i, + 'test_single_property' => $i . ' - test single property', + 'test_multiple_properties' => [[ + 'shape' => $i . ' - test multiple properties - shape', + 'color' => $i . ' - test multiple properties - color', + ]], + 'test_single_property_multiple_values' => [ + ['value' => $i . ' - test single property multiple values 0'], + ['value' => $i . ' - test single property multiple values 1'] + ], + 'test_multiple_properties_multiple_values' => [ + [ + 'shape' => $i . ' - test multiple properties multiple values - shape 0', + 'color' => $i . ' - test multiple properties multiple values - color 0', + ], + [ + 'shape' => $i . ' - test multiple properties multiple values - shape 1', + 'color' => $i . ' - test multiple properties multiple values - color 1', + ] + ], + 'field_test_configurable_field' => [ + [ + 'shape' => $i . ' - field test configurable field - shape 0', + 'color' => $i . ' - field test configurable field - color 0', + ], + [ + 'shape' => $i . ' - field test configurable field - shape 1', + 'color' => $i . ' - field test configurable field - color 1', + ] + ], + 'test_entity_base_field_info' => $i . ' - test entity base field info', + ]); + $entity->addTranslation('ro', [ + 'name' => $i . ' - ro', + 'test_single_property' => $i . ' - test single property - ro', + 'test_multiple_properties' => [[ + 'shape' => $i . ' - test multiple properties - shape - ro', + 'color' => $i . ' - test multiple properties - color - ro', + ]], + 'test_single_property_multiple_values' => [ + ['value' => $i . ' - test single property multiple values 0 - ro'], + ['value' => $i . ' - test single property multiple values 1 - ro'] + ], + 'test_multiple_properties_multiple_values' => [ + [ + 'shape' => $i . ' - test multiple properties multiple values - shape 0 - ro', + 'color' => $i . ' - test multiple properties multiple values - color 0 - ro', + ], + [ + 'shape' => $i . ' - test multiple properties multiple values - shape 1 - ro', + 'color' => $i . ' - test multiple properties multiple values - color 1 - ro', + ] + ], + 'field_test_configurable_field' => [ + [ + 'shape' => $i . ' - field test configurable field - shape 0 - ro', + 'color' => $i . ' - field test configurable field - color 0 - ro', + ], + [ + 'shape' => $i . ' - field test configurable field - shape 1 - ro', + 'color' => $i . ' - field test configurable field - color 1 - ro', + ] + ], + 'test_entity_base_field_info' => $i . ' - test entity base field info - ro', + ]); + + $entity->save(); + } + } + +}