diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
index d696a94..6d0b93b 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
@@ -118,6 +118,13 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
protected $languageManager;
/**
+ * Whether this storage should use the temporary table mapping.
+ *
+ * @var bool
+ */
+ protected $isTemporary = FALSE;
+
+ /**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
@@ -267,6 +274,31 @@ 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. Expected to be removed
+ * by https://www.drupal.org/node/2554235.
+ */
+ public function setTableMapping(TableMappingInterface $table_mapping) {
+ $this->tableMapping = $table_mapping;
+ }
+
+ /**
+ * Changes the temporary state of the storage.
+ *
+ * @param bool $temporary
+ * Whether to use a temporary table mapping or not.
+ *
+ * @internal Only to be used internally by Entity API.
+ */
+ public function setTemporary($temporary) {
+ $this->isTemporary = $temporary;
+ }
+
+ /**
* {@inheritdoc}
*/
public function getTableMapping(array $storage_definitions = NULL) {
@@ -279,8 +311,10 @@ public function getTableMapping(array $storage_definitions = NULL) {
// @todo Clean-up this in https://www.drupal.org/node/2274017 so we can
// easily instantiate a new table mapping whenever needed.
if (!isset($this->tableMapping) || $storage_definitions) {
+ $table_mapping_class = $this->isTemporary ? TemporaryTableMapping::class : DefaultTableMapping::class;
$definitions = $storage_definitions ?: $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
- $table_mapping = new DefaultTableMapping($this->entityType, $definitions);
+ /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping|\Drupal\Core\Entity\Sql\TemporaryTableMapping $table_mapping */
+ $table_mapping = new $table_mapping_class($this->entityType, $definitions);
$shared_table_definitions = array_filter($definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
return $table_mapping->allowsSharedTableStorage($definition);
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
index 85d6fbe..2889f4b 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
@@ -229,6 +229,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();
@@ -1204,10 +1210,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/SqlContentEntityStorageSchemaConverter.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchemaConverter.php
new file mode 100644
index 0000000..0ff9db0
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchemaConverter.php
@@ -0,0 +1,453 @@
+entityTypeId = $entity_type_id;
+ $this->entityTypeManager = $entity_type_manager;
+ $this->entityDefinitionUpdateManager = $entity_definition_update_manager;
+ $this->lastInstalledSchemaRepository = $last_installed_schema_repository;
+ $this->installedStorageSchema = $installed_storage_schema;
+ $this->database = $database;
+ }
+
+ /**
+ * Converts an entity type with existing data to be revisionable.
+ *
+ * This process does the following tasks:
+ * - creates the schema from scratch with the new revisionable entity type
+ * definition (i.e. the current definition of the entity type from code)
+ * using temporary table names;
+ * - loads the initial entity data by using the last installed entity and
+ * field storage definitions;
+ * - saves the entity data to the temporary tables;
+ * - at the end of the process:
+ * - deletes the original tables and replaces them with the temporary ones
+ * that hold the new (revisionable) entity data;
+ * - updates the installed entity schema data;
+ * - updates the entity type definition in order to trigger the
+ * \Drupal\Core\Entity\EntityTypeEvents::UPDATE event;
+ * - updates the field storage definitions in order to mark the
+ * revisionable ones as such.
+ *
+ * In case of an error during the entity save process, the temporary tables
+ * are deleted and the original entity type and field storage definitions are
+ * restored.
+ *
+ * @param array $sandbox
+ * The sandbox array from a hook_update_N() implementation.
+ * @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.
+ *
+ * @throws \Exception
+ * Re-throws any exception raised during the update process.
+ */
+ public function convertToRevisionable(array &$sandbox, array $fields_to_update = []) {
+ // If 'progress' is not set, then this will be the first run of the batch.
+ if (!isset($sandbox['progress'])) {
+ // Store the original entity type and field definitions in the $sandbox
+ // array so we can use them later in the update process.
+ $this->collectOriginalDefinitions($sandbox);
+
+ // Create a temporary environment in which the new data will be stored.
+ $this->createTemporaryDefinitions($sandbox, $fields_to_update);
+
+ // Create the updated entity schema using temporary tables.
+ /** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */
+ $storage = $this->entityTypeManager->getStorage($this->entityTypeId);
+ $storage->setTemporary(TRUE);
+ $storage->setEntityType($sandbox['temporary_entity_type']);
+ $storage->onEntityTypeCreate($sandbox['temporary_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) {
+ $this->entityTypeManager->useCaches(FALSE);
+ $actual_entity_type = $this->entityTypeManager->getDefinition($this->entityTypeId);
+
+ // Rename the original tables so we can put them back in place in case
+ // anything goes wrong.
+ foreach ($sandbox['original_table_mapping']->getTableNames() as $table_name) {
+ $old_table_table = TemporaryTableMapping::getTempTableName($table_name, 'old_');
+ $this->database->schema()->renameTable($table_name, $old_table_table);
+ }
+
+ // Put the new tables in place and update the entity type and field
+ // storage definitions.
+ try {
+ $storage = $this->entityTypeManager->getStorage($this->entityTypeId);
+ $storage->setEntityType($actual_entity_type);
+ $storage->setTemporary(FALSE);
+ $actual_table_names = $storage->getTableMapping()->getTableNames();
+
+ $table_name_mapping = [];
+ foreach ($actual_table_names as $new_table_name) {
+ $temp_table_name = TemporaryTableMapping::getTempTableName($new_table_name);
+ $table_name_mapping[$temp_table_name] = $new_table_name;
+ $this->database->schema()->renameTable($temp_table_name, $new_table_name);
+ }
+
+ // Rename the tables in the cached entity schema data.
+ $entity_schema_data = $this->installedStorageSchema->get($this->entityTypeId . '.entity_schema_data', []);
+ foreach ($entity_schema_data as $temp_table_name => $schema) {
+ if (isset($table_name_mapping[$temp_table_name])) {
+ $entity_schema_data[$table_name_mapping[$temp_table_name]] = $schema;
+ unset($entity_schema_data[$temp_table_name]);
+ }
+ }
+ $this->installedStorageSchema->set($this->entityTypeId . '.entity_schema_data', $entity_schema_data);
+
+ // Rename the tables in the cached field schema data.
+ foreach ($sandbox['updated_storage_definitions'] as $storage_definition) {
+ $field_schema_data = $this->installedStorageSchema->get($this->entityTypeId . '.field_schema_data.' . $storage_definition->getName(), []);
+ foreach ($field_schema_data as $temp_table_name => $schema) {
+ if (isset($table_name_mapping[$temp_table_name])) {
+ $field_schema_data[$table_name_mapping[$temp_table_name]] = $schema;
+ unset($field_schema_data[$temp_table_name]);
+ }
+ }
+ $this->installedStorageSchema->set($this->entityTypeId . '.field_schema_data.' . $storage_definition->getName(), $field_schema_data);
+ }
+
+ // Instruct the entity schema handler that data migration has been
+ // handled already and update the entity type.
+ $actual_entity_type->requiresDataMigration = FALSE;
+ $this->entityDefinitionUpdateManager->updateEntityType($actual_entity_type);
+
+ // Update the field storage definitions.
+ $this->updateFieldStorageDefinitionsToRevisionable($actual_entity_type, $sandbox['original_storage_definitions'], $fields_to_update);
+ }
+ catch (\Exception $e) {
+ // Something went wrong, bring back the original tables.
+ foreach ($sandbox['original_table_mapping']->getTableNames() as $table_name) {
+ // We are in the 'original data recovery' phase, so we need to be sure
+ // that the initial tables can be recovered properly.
+ if ($this->database->schema()->tableExists($table_name)) {
+ $this->database->schema()->dropTable($table_name);
+ }
+
+ $old_table_table = TemporaryTableMapping::getTempTableName($table_name, 'old_');
+ $this->database->schema()->renameTable($old_table_table, $table_name);
+ }
+
+ // Re-throw the original exception.
+ throw $e;
+ }
+ }
+ }
+
+ /**
+ * Loads entities from the original storage and saves them to a temporary one.
+ *
+ * @param array &$sandbox
+ * The sandbox array from a hook_update_N() implementation.
+ *
+ * @throws \Drupal\Core\Entity\EntityStorageException
+ * Thrown in case of an error during the entity save process.
+ */
+ 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'];
+ $original_table_mapping = $sandbox['original_table_mapping'];
+ $original_entity_type = $sandbox['original_entity_type'];
+
+ $original_base_table = $original_entity_type->getBaseTable();
+
+ $revision_id_key = $temporary_entity_type->getKey('revision');
+
+ // If 'progress' is not set, then this will be the first run of the batch.
+ if (!isset($sandbox['progress'])) {
+ $sandbox['progress'] = 0;
+ $sandbox['current_id'] = 0;
+ $sandbox['max'] = $this->database->select($original_base_table)
+ ->countQuery()
+ ->execute()
+ ->fetchField();
+ }
+
+ $id = $original_entity_type->getKey('id');
+
+ // Define the step size.
+ $steps = Settings::get('entity_update_batch_size', 50);
+
+ // Get the next entity IDs to migrate.
+ $entity_ids = $this->database->select($original_base_table)
+ ->fields($original_base_table, [$id])
+ ->condition($id, $sandbox['current_id'], '>')
+ ->orderBy($id, 'ASC')
+ ->range(0, $steps)
+ ->execute()
+ ->fetchAllKeyed(0, 0);
+
+ /** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */
+ $storage = $this->entityTypeManager->getStorage($temporary_entity_type->id());
+ $storage->setEntityType($original_entity_type);
+ $storage->setTableMapping($original_table_mapping);
+
+ $entities = $storage->loadMultiple($entity_ids);
+
+ // Now inject the temporary entity type definition and table mapping in the
+ // storage and re-save the entities.
+ $storage->setEntityType($temporary_entity_type);
+ $storage->setTableMapping($temporary_table_mapping);
+
+ foreach ($entities as $entity_id => $entity) {
+ try {
+ // 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 temporary storage.
+ $storage->save($entity);
+ }
+ catch (\Exception $e) {
+ // In case of an error during the save process, we need to roll back the
+ // original entity type and field storage definitions and clean up the
+ // temporary tables.
+ $this->restoreOriginalDefinitions($sandbox);
+
+ foreach ($temporary_table_mapping->getTableNames() as $table_name) {
+ $this->database->schema()->dropTable($table_name);
+ }
+
+ // Re-throw the original exception with a helpful message.
+ throw new EntityStorageException("The entity update process failed while processing the entity {$original_entity_type->id()}:$entity_id.", $e->getCode(), $e);
+ }
+
+ $sandbox['progress']++;
+ $sandbox['current_id'] = $entity_id;
+ }
+
+ // If we're not in maintenance mode, the number of entities could change at
+ // any time so make sure that we always use the latest record count.
+ $sandbox['max'] = $this->database->select($original_base_table)
+ ->countQuery()
+ ->execute()
+ ->fetchField();
+
+ $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.
+ * @param bool $update_cached_definitions
+ * (optional) Whether to update the cached field storage definitions in the
+ * entity definition update manager. Defaults to TRUE.
+ *
+ * @return \Drupal\Core\Field\FieldStorageDefinitionInterface[]
+ * An array of updated field storage definitions.
+ */
+ protected function updateFieldStorageDefinitionsToRevisionable(ContentEntityTypeInterface $entity_type, array $storage_definitions, array $fields_to_update = [], $update_cached_definitions = TRUE) {
+ $updated_storage_definitions = array_map(function ($storage_definition) { return clone $storage_definition; }, $storage_definitions);
+
+ // 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) {
+ if (!$updated_storage_definitions[$field_name]->isRevisionable()) {
+ $updated_storage_definitions[$field_name]->setRevisionable(TRUE);
+
+ if ($update_cached_definitions) {
+ $this->entityDefinitionUpdateManager->updateFieldStorageDefinition($updated_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);
+
+ if ($update_cached_definitions) {
+ $this->entityDefinitionUpdateManager->installFieldStorageDefinition($revision_field->getName(), $entity_type->id(), $entity_type->getProvider(), $revision_field);
+ }
+
+ $updated_storage_definitions[$entity_type->getKey('revision')] = $revision_field;
+
+ return $updated_storage_definitions;
+ }
+
+ /**
+ * Collects the original definitions of an entity type and its fields.
+ *
+ * @param array &$sandbox
+ * A sandbox array from a hook_update_N() implementation.
+ */
+ protected function collectOriginalDefinitions(array &$sandbox) {
+ $original_entity_type = $this->lastInstalledSchemaRepository->getLastInstalledDefinition($this->entityTypeId);
+ $original_storage_definitions = $this->lastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions($this->entityTypeId);
+
+ /** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */
+ $storage = $this->entityTypeManager->getStorage($this->entityTypeId);
+ $storage->setEntityType($original_entity_type);
+ $original_table_mapping = $storage->getTableMapping($original_storage_definitions);
+
+ $sandbox['original_entity_type'] = $original_entity_type;
+ $sandbox['original_storage_definitions'] = $original_storage_definitions;
+ $sandbox['original_table_mapping'] = $original_table_mapping;
+
+ $sandbox['original_entity_schema_data'] = $this->installedStorageSchema->get($this->entityTypeId . '.entity_schema_data', []);
+ foreach ($original_storage_definitions as $storage_definition) {
+ $sandbox['original_field_schema_data'][$storage_definition->getName()] = $this->installedStorageSchema->get($this->entityTypeId . '.field_schema_data.' . $storage_definition->getName(), []);
+ }
+ }
+
+ /**
+ * Restores the entity type, field storage definitions and their schema data.
+ *
+ * @param array $sandbox
+ * The sandbox array from a hook_update_N() implementation.
+ */
+ protected function restoreOriginalDefinitions(array $sandbox) {
+ $original_entity_type = $sandbox['original_entity_type'];
+ $original_storage_definitions = $sandbox['original_storage_definitions'];
+ $original_entity_schema_data = $sandbox['original_entity_schema_data'];
+ $original_field_schema_data = $sandbox['original_field_schema_data'];
+
+ $this->lastInstalledSchemaRepository->setLastInstalledDefinition($original_entity_type);
+ $this->lastInstalledSchemaRepository->setLastInstalledFieldStorageDefinitions($original_entity_type->id(), $original_storage_definitions);
+
+ $this->installedStorageSchema->set($original_entity_type->id() . '.entity_schema_data', $original_entity_schema_data);
+ foreach ($original_field_schema_data as $field_name => $field_schema_data) {
+ $this->installedStorageSchema->set($original_entity_type->id() . '.field_schema_data.' . $field_name, $field_schema_data);
+ }
+ }
+
+ /**
+ * Creates temporary entity type, field storage and table mapping objects.
+ *
+ * @param array &$sandbox
+ * A sandbox array from a hook_update_N() implementation.
+ * @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.
+ */
+ protected function createTemporaryDefinitions(array &$sandbox, array $fields_to_update) {
+ // Make sure to get the latest entity type definition from code.
+ $this->entityTypeManager->useCaches(FALSE);
+ $actual_entity_type = $this->entityTypeManager->getDefinition($this->entityTypeId);
+
+ $temporary_entity_type = clone $actual_entity_type;
+ $temporary_entity_type->set('base_table', TemporaryTableMapping::getTempTableName($temporary_entity_type->getBaseTable()));
+ $temporary_entity_type->set('revision_table', TemporaryTableMapping::getTempTableName($temporary_entity_type->getRevisionTable()));
+ if ($temporary_entity_type->isTranslatable()) {
+ $temporary_entity_type->set('data_table', TemporaryTableMapping::getTempTableName($temporary_entity_type->getDataTable()));
+ $temporary_entity_type->set('revision_data_table', TemporaryTableMapping::getTempTableName($temporary_entity_type->getRevisionDataTable()));
+ }
+
+ /** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */
+ $storage = $this->entityTypeManager->getStorage($this->entityTypeId);
+ $storage->setTemporary(TRUE);
+ $storage->setEntityType($temporary_entity_type);
+
+ $updated_storage_definitions = $this->updateFieldStorageDefinitionsToRevisionable($temporary_entity_type, $sandbox['original_storage_definitions'], $fields_to_update, FALSE);
+ $temporary_table_mapping = $storage->getTableMapping($updated_storage_definitions);
+
+ $sandbox['temporary_entity_type'] = $temporary_entity_type;
+ $sandbox['temporary_table_mapping'] = $temporary_table_mapping;
+ $sandbox['updated_storage_definitions'] = $updated_storage_definitions;
+ }
+
+}
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..c53e660
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Sql/TemporaryTableMapping.php
@@ -0,0 +1,46 @@
+ 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/src/Tests/Entity/EntityDefinitionTestTrait.php b/core/modules/system/src/Tests/Entity/EntityDefinitionTestTrait.php
index 7fcabd7..3b1e7bd 100644
--- a/core/modules/system/src/Tests/Entity/EntityDefinitionTestTrait.php
+++ b/core/modules/system/src/Tests/Entity/EntityDefinitionTestTrait.php
@@ -37,6 +37,7 @@ protected function updateEntityTypeToRevisionable() {
$keys = $entity_type->getKeys();
$keys['revision'] = 'revision_id';
$entity_type->set('entity_keys', $keys);
+ $entity_type->set('revision_table', 'entity_test_update_revision');
$this->state->set('entity_test_update.entity_type', $entity_type);
}
@@ -50,6 +51,7 @@ protected function updateEntityTypeToNotRevisionable() {
$keys = $entity_type->getKeys();
unset($keys['revision']);
$entity_type->set('entity_keys', $keys);
+ $entity_type->set('revision_table', NULL);
$this->state->set('entity_test_update.entity_type', $entity_type);
}
@@ -87,6 +89,24 @@ protected function updateEntityTypeToNotTranslatable() {
}
/**
+ * Updates the 'entity_test_update' entity type to revisionable and
+ * translatable.
+ */
+ protected function updateEntityTypeToRevisionableAndTranslatable() {
+ $entity_type = clone $this->entityManager->getDefinition('entity_test_update');
+
+ $keys = $entity_type->getKeys();
+ $keys['revision'] = 'revision_id';
+ $entity_type->set('entity_keys', $keys);
+ $entity_type->set('translatable', TRUE);
+ $entity_type->set('data_table', 'entity_test_update_data');
+ $entity_type->set('revision_table', 'entity_test_update_revision');
+ $entity_type->set('revision_data_table', 'entity_test_update_revision_data');
+
+ $this->state->set('entity_test_update.entity_type', $entity_type);
+ }
+
+ /**
* Adds a new base field to the 'entity_test_update' entity type.
*
* @param string $type
@@ -203,7 +223,7 @@ protected function removeBundleField() {
*/
protected function addEntityIndex() {
$indexes = [
- 'entity_test_update__new_index' => ['name', 'user_id'],
+ 'entity_test_update__new_index' => ['name', 'test_single_property'],
];
$this->state->set('entity_test_update.additional_entity_indexes', $indexes);
}
diff --git a/core/modules/system/src/Tests/Entity/Update/SqlContentEntityStorageSchemaConverterTest.php b/core/modules/system/src/Tests/Entity/Update/SqlContentEntityStorageSchemaConverterTest.php
new file mode 100644
index 0000000..ec993da
--- /dev/null
+++ b/core/modules/system/src/Tests/Entity/Update/SqlContentEntityStorageSchemaConverterTest.php
@@ -0,0 +1,256 @@
+entityManager = \Drupal::entityManager();
+ $this->entityDefinitionUpdateManager = \Drupal::entityDefinitionUpdateManager();
+ $this->lastInstalledSchemaRepository = \Drupal::service('entity.last_installed_schema.repository');
+ $this->installedStorageSchema = \Drupal::keyValue('entity.storage_schema.sql');
+ $this->state = \Drupal::state();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setDatabaseDumpFiles() {
+ $this->databaseDumpFiles = [
+ __DIR__ . '/../../../../tests/fixtures/update/drupal-8.0.0-rc1-filled.standard.entity_test_update_mul.php.gz',
+ __DIR__ . '/../../../../tests/fixtures/update/drupal-8.entity-test-schema-converter-enabled.php',
+ ];
+ }
+
+ /**
+ * Tests the conversion of an entity type to revisionable.
+ */
+ public function testMakeRevisionable() {
+ // Check that entity type is not revisionable prior to running the update
+ // process.
+ $entity_test_update = $this->lastInstalledSchemaRepository->getLastInstalledDefinition('entity_test_update');
+ $this->assertFalse($entity_test_update->isRevisionable());
+
+ // Make the entity type revisionable and translatable and run the updates.
+ $this->updateEntityTypeToRevisionableAndTranslatable();
+
+ $this->runUpdates();
+
+ /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_test_update */
+ $entity_test_update = $this->lastInstalledSchemaRepository->getLastInstalledDefinition('entity_test_update');
+ $this->assertTrue($entity_test_update->isRevisionable());
+
+ /** @var \Drupal\Core\Entity\Sql\SqlEntityStorageInterface $storage */
+ $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update');
+ $this->assertEqual(count($storage->loadMultiple()), 102, 'All test entities were found.');
+
+ // Check that each field value was copied correctly to the revision tables.
+ for ($i = 1; $i <= 102; $i++) {
+ /** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
+ $revision = $storage->loadRevision($i);
+
+ $this->assertEqual($i, $revision->id());
+ $this->assertEqual($i, $revision->getRevisionId());
+
+ $this->assertEqual($i . ' - test single property', $revision->test_single_property->value);
+
+ $this->assertEqual($i . ' - test multiple properties - value1', $revision->test_multiple_properties->value1);
+ $this->assertEqual($i . ' - test multiple properties - value2', $revision->test_multiple_properties->value2);
+
+ $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 - value1 0', $revision->test_multiple_properties_multiple_values[0]->value1);
+ $this->assertEqual($i . ' - test multiple properties multiple values - value2 0', $revision->test_multiple_properties_multiple_values[0]->value2);
+ $this->assertEqual($i . ' - test multiple properties multiple values - value1 1', $revision->test_multiple_properties_multiple_values[1]->value1);
+ $this->assertEqual($i . ' - test multiple properties multiple values - value2 1', $revision->test_multiple_properties_multiple_values[1]->value2);
+
+ $this->assertEqual($i . ' - field test configurable field - value1 0', $revision->field_test_configurable_field[0]->value1);
+ $this->assertEqual($i . ' - field test configurable field - value2 0', $revision->field_test_configurable_field[0]->value2);
+ $this->assertEqual($i . ' - field test configurable field - value1 1', $revision->field_test_configurable_field[1]->value1);
+ $this->assertEqual($i . ' - field test configurable field - value2 1', $revision->field_test_configurable_field[1]->value2);
+
+ $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 - value1 - ro', $translation->test_multiple_properties->value1);
+ $this->assertEqual($i . ' - test multiple properties - value2 - ro', $translation->test_multiple_properties->value2);
+
+ $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 - value1 0 - ro', $translation->test_multiple_properties_multiple_values[0]->value1);
+ $this->assertEqual($i . ' - test multiple properties multiple values - value2 0 - ro', $translation->test_multiple_properties_multiple_values[0]->value2);
+ $this->assertEqual($i . ' - test multiple properties multiple values - value1 1 - ro', $translation->test_multiple_properties_multiple_values[1]->value1);
+ $this->assertEqual($i . ' - test multiple properties multiple values - value2 1 - ro', $translation->test_multiple_properties_multiple_values[1]->value2);
+
+ $this->assertEqual($i . ' - field test configurable field - value1 0 - ro', $translation->field_test_configurable_field[0]->value1);
+ $this->assertEqual($i . ' - field test configurable field - value2 0 - ro', $translation->field_test_configurable_field[0]->value2);
+ $this->assertEqual($i . ' - field test configurable field - value1 1 - ro', $translation->field_test_configurable_field[1]->value1);
+ $this->assertEqual($i . ' - field test configurable field - value2 1 - ro', $translation->field_test_configurable_field[1]->value2);
+
+ $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 ($storage->getTableMapping()->getTableNames() as $table_name) {
+ $this->assertFalse($schema->tableExists(TemporaryTableMapping::getTempTableName($table_name)));
+ }
+ }
+
+ /**
+ * Tests that a failed "make revisionable" update preserves the existing data.
+ */
+ public function testMakeRevisionableErrorHandling() {
+ $original_entity_type = $this->lastInstalledSchemaRepository->getLastInstalledDefinition('entity_test_update');
+ $original_storage_definitions = $this->lastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions('entity_test_update');
+
+ $original_entity_schema_data = $this->installedStorageSchema->get('entity_test_update.entity_schema_data', []);
+ foreach ($original_storage_definitions as $storage_definition) {
+ $original_field_schema_data[$storage_definition->getName()] = $this->installedStorageSchema->get('entity_test_update.field_schema_data.' . $storage_definition->getName(), []);
+ }
+
+ // Check that entity type is not revisionable prior to running the update
+ // process.
+ $this->assertFalse($original_entity_type->isRevisionable());
+
+ // Make the update throw an exception during the entity save process.
+ \Drupal::state()->set('entity_test_update.throw_exception', TRUE);
+
+ // Since the update process is interrupted by the exception thrown above,
+ // we can not do the full post update testing offered by UpdatePathTestBase.
+ $this->checkFailedUpdates = FALSE;
+
+ // Make the entity type revisionable and run the updates.
+ $this->updateEntityTypeToRevisionableAndTranslatable();
+
+ $this->runUpdates();
+
+ // Check that the update failed.
+ $this->assertRaw('' . t('Failed:') . '');
+
+ // Check that the last installed entity type definition is kept as
+ // non-revisionable.
+ $new_entity_type = $this->lastInstalledSchemaRepository->getLastInstalledDefinition('entity_test_update');
+ $this->assertFalse($new_entity_type->isRevisionable(), 'The entity type is kept unchanged.');
+
+ // Check that the last installed field storage definitions did not change by
+ // looking at the 'langcode' field, which is updated automatically.
+ $new_storage_definitions = $this->lastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions('entity_test_update');
+ $langcode_key = $original_entity_type->getKey('langcode');
+ $this->assertEqual($original_storage_definitions[$langcode_key]->isRevisionable(), $new_storage_definitions[$langcode_key]->isRevisionable(), "The 'langcode' field is kept unchanged.");
+
+ /** @var \Drupal\Core\Entity\Sql\SqlEntityStorageInterface $storage */
+ $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update');
+
+ // Check that installed storage schema did not change.
+ $new_entity_schema_data = $this->installedStorageSchema->get('entity_test_update.entity_schema_data', []);
+ $this->assertEqual($original_entity_schema_data, $new_entity_schema_data);
+
+ foreach ($new_storage_definitions as $storage_definition) {
+ $new_field_schema_data[$storage_definition->getName()] = $this->installedStorageSchema->get('entity_test_update.field_schema_data.' . $storage_definition->getName(), []);
+ }
+ $this->assertEqual($original_field_schema_data, $new_field_schema_data);
+
+ // Check that temporary tables have been removed.
+ $schema = \Drupal::database()->schema();
+ foreach ($storage->getTableMapping()->getTableNames() as $table_name) {
+ $this->assertFalse($schema->tableExists(TemporaryTableMapping::getTempTableName($table_name)));
+ }
+
+ // Check that the original tables still exist and their data is intact.
+ $this->assertTrue($schema->tableExists('entity_test_update'));
+ $this->assertTrue($schema->tableExists('entity_test_update_data'));
+
+ $base_table_count = \Drupal::database()->select('entity_test_update')
+ ->countQuery()
+ ->execute()
+ ->fetchField();
+ $this->assertEqual($base_table_count, 102);
+
+ $data_table_count = \Drupal::database()->select('entity_test_update_data')
+ ->countQuery()
+ ->execute()
+ ->fetchField();
+ // There are two records for each entity, one for English and one for
+ // Romanian.
+ $this->assertEqual($data_table_count, 204);
+
+ $base_table_row = \Drupal::database()->select('entity_test_update')
+ ->fields('entity_test_update')
+ ->condition('id', 1, '=')
+ ->condition('langcode', 'en', '=')
+ ->execute()
+ ->fetchAllAssoc('id');
+ $this->assertEqual('843e9ac7-3351-4cc1-a202-2dbffffae21c', $base_table_row[1]->uuid);
+
+ $data_table_table_row = \Drupal::database()->select('entity_test_update_data')
+ ->fields('entity_test_update_data')
+ ->condition('id', 1, '=')
+ ->condition('langcode', 'en', '=')
+ ->execute()
+ ->fetchAllAssoc('id');
+ $this->assertEqual('1 - test single property', $data_table_table_row[1]->test_single_property);
+ $this->assertEqual('1 - test multiple properties - value1', $data_table_table_row[1]->test_multiple_properties__value1);
+ $this->assertEqual('1 - test multiple properties - value2', $data_table_table_row[1]->test_multiple_properties__value2);
+ $this->assertEqual('1 - test entity base field info', $data_table_table_row[1]->test_entity_base_field_info);
+ }
+
+}
diff --git a/core/modules/system/tests/fixtures/update/drupal-8.0.0-rc1-filled.standard.entity_test_update.php.gz b/core/modules/system/tests/fixtures/update/drupal-8.0.0-rc1-filled.standard.entity_test_update.php.gz
new file mode 100644
index 0000000..a7ecfdc
--- /dev/null
+++ b/core/modules/system/tests/fixtures/update/drupal-8.0.0-rc1-filled.standard.entity_test_update.php.gz
@@ -0,0 +1,472 @@
+X drupal-8.0.0-rc1-filled.standard.entity_test_update.php ]6
+@'ABIN6 Llz`P$Uִ,9ܝA}tIeU/u=>mv>Wg_0/5NsLh>N}O>E_T`oO^4go4W:Eij_8|!~;>~7qk>T,@[,:Oމi64]ӌY"盢M^w0t;cA8֎%HC|Zf3]ni#|\.8yC[BY.u&B}[_jXn
s!8fl&Ұ36~ӧj,4:Ii/=KfSk2Bz-^~KJ
+iղ>fjScPN\IS>G0~9ZX?L2hGg{(E@Qq\,l]Eyiw02rIj|9}V]ȩ4JB1rL9:ITJ>}jc=@*#?yۭ6ڰy60;BD{ʄ93qvJϼ?fΞc3䢨z|mRԦR{ڀs7`<y7:sn 1BR9a\MT!c❾/.Lt2x| C6E]*X
+e9&-]:@GS|gL@%&Dd\Q H^Y:WֹX'a,+\J.WCkɭSuǯ`s^
+,[4UFzzT]4ԅ~T>)w(E(Tns(˚^TJw!+gܿZXn_ =ޞw,?x4xk@SGh6G}~yTǒ}|