diff --git a/core/lib/Drupal/Core/Entity/Sql/RevisionableSchemaConverter.php b/core/lib/Drupal/Core/Entity/Sql/RevisionableSchemaConverter.php index ee7c77a..a1e32a4 100644 --- a/core/lib/Drupal/Core/Entity/Sql/RevisionableSchemaConverter.php +++ b/core/lib/Drupal/Core/Entity/Sql/RevisionableSchemaConverter.php @@ -6,9 +6,11 @@ use Drupal\Core\Entity\ContentEntityTypeInterface; use Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface; use Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface; +use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\Core\KeyValueStore\KeyValueStoreInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; /** @@ -24,6 +26,13 @@ class RevisionableSchemaConverter { protected $entityTypeManager; /** + * The entity definition update manager service. + * + * @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface + */ + protected $entityDefinitionUpdateManager; + + /** * The last installed schema repository service. * * @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface @@ -31,11 +40,11 @@ class RevisionableSchemaConverter { protected $lastInstalledSchemaRepository; /** - * The entity definition update manager service. + * The key-value collection for tracking installed storage schema. * - * @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface + * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface */ - protected $entityDefinitionUpdateManager; + protected $installedStorageSchema; /** * The database connection. @@ -56,25 +65,36 @@ class RevisionableSchemaConverter { * @param \Drupal\Core\Database\Connection $database * Database connection. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityDefinitionUpdateManagerInterface $entity_definition_update_manager, EntityLastInstalledSchemaRepositoryInterface $last_installed_schema_repository, Connection $database) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityDefinitionUpdateManagerInterface $entity_definition_update_manager, EntityLastInstalledSchemaRepositoryInterface $last_installed_schema_repository, KeyValueStoreInterface $installed_storage_schema, Connection $database) { $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. * - * 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 installed 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. + * 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. @@ -90,45 +110,46 @@ public function convertToRevisionable(array &$sandbox, $entity_type_id, array $f 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); + $original_table_mapping = $storage->getTableMapping($original_storage_definitions); - /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ - $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; - // 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); + $sandbox['original_entity_schema_data'] = $this->installedStorageSchema->get($entity_type_id . '.entity_schema_data', []); + foreach ($original_storage_definitions as $storage_definition) { + $sandbox['original_field_schema_data'][$storage_definition->getName()] = $this->installedStorageSchema->get($entity_type_id . '.field_schema_data.' . $storage_definition->getName(), []); } - // 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())); + $this->entityTypeManager->useCaches(FALSE); + $actual_entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + + $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($original_entity_type->getDataTable())); + $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())); } - $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); + // Get a temporary table mapping object and store it in $sandbox so it can + // be used later in ::copyData(). + $storage->setTemporary(TRUE); + $storage->setEntityType($temporary_entity_type); - // Instruct the entity schema handler that data migration is being handled - // independently. - $entity_type->requiresDataMigration = FALSE; + $updated_storage_definitions = $this->updateFieldStorageDefinitions($temporary_entity_type, $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; - $this->entityDefinitionUpdateManager->updateEntityType($entity_type); - $sandbox['entity_type'] = $entity_type; + // Create the updated entity schema using temporary tables. + $storage->onEntityTypeCreate($temporary_entity_type); } // Copy over the existing data to the new temporary tables. @@ -137,43 +158,92 @@ public function convertToRevisionable(array &$sandbox, $entity_type_id, array $f // 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); + $this->entityTypeManager->useCaches(FALSE); + $actual_entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + + // Drop the old tables and put the temporary ones in place. + foreach ($sandbox['original_table_mapping']->getTableNames() as $table_name) { + $this->database->schema()->dropTable($table_name); + } - foreach ($sandbox['temporary_table_mapping']->getTableNames() as $temporary_table) { - $this->database->schema()->dropTable($temporary_table); + $storage = $this->entityTypeManager->getStorage($entity_type_id); + $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($entity_type_id . '.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($entity_type_id . '.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($entity_type_id . '.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($entity_type_id . '.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->updateFieldStorageDefinitions($actual_entity_type, $sandbox['original_storage_definitions'], $fields_to_update); } } /** - * Loads entities from the temporary storage and re-saves them to the new one. + * Loads entities from the original storage and saves them to the 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']; - $entity_type = $sandbox['entity_type']; + $temporary_table_mapping = $sandbox['temporary_table_mapping']; + $original_entity_type = $sandbox['original_entity_type']; + $original_table_mapping = $sandbox['original_table_mapping']; + + $original_base_table = $original_entity_type->getBaseTable(); // 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) + $sandbox['max'] = $this->database->select($original_base_table) ->countQuery() ->execute() ->fetchField(); } - $id = $temporary_entity_type->getKey('id'); + $id = $original_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]) + $entity_ids = $this->database->select($original_base_table) + ->fields($original_base_table, [$id]) ->condition($id, $sandbox['current_id'], '>') ->orderBy($id, 'ASC') ->range(0, 100) @@ -182,21 +252,18 @@ protected function copyData(array &$sandbox) { /** @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); + $storage->setEntityType($original_entity_type); + $storage->setTableMapping($original_table_mapping); - // 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); + $entities = $storage->loadMultiple($entity_ids); - foreach ($temporary_entities as $entity_id => $entity) { - $revision_id_key = $entity_type->getKey('revision'); + // 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); - // Manually set the original entity since it cannot be read from the - // temporary storage anymore. - $entity->original = $entity; + foreach ($entities as $entity_id => $entity) { + $revision_id_key = $temporary_entity_type->getKey('revision'); // Set the revision ID to be same as the entity ID. $entity->set($revision_id_key, $entity_id); @@ -205,8 +272,23 @@ protected function copyData(array &$sandbox) { // rather than an UPDATE. $entity->enforceIsNew(TRUE); - // Finally, save the entity in the new revisionable storage. - $storage->save($entity); + try { + // 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; @@ -224,11 +306,16 @@ protected function copyData(array &$sandbox) { * @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 updateFieldStorageDefinitions(ContentEntityTypeInterface $entity_type, array $storage_definitions, array $fields_to_update = []) { + protected function updateFieldStorageDefinitions(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')) { @@ -238,9 +325,12 @@ protected function updateFieldStorageDefinitions(ContentEntityTypeInterface $ent 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]); + if ($updated_storage_definitions[$field_name]->isBaseField()) { + $updated_storage_definitions[$field_name]->setRevisionable(TRUE); + + if ($update_cached_definitions) { + $this->entityDefinitionUpdateManager->updateFieldStorageDefinition($updated_storage_definitions[$field_name]); + } } } @@ -252,88 +342,35 @@ protected function updateFieldStorageDefinitions(ContentEntityTypeInterface $ent ->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; + 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 $storage_definitions; + return $updated_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. + * Restores the entity type, field storage definitions and their schema data. * - * @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. + * @param array $sandbox + * The sandbox array from a hook_update_N() implementation. */ - 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)))); + 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); } - - // 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 8c08b09..4e86435 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) { @@ -272,13 +279,26 @@ public function setEntityType(EntityTypeInterface $entity_type) { * @param \Drupal\Core\Entity\Sql\TableMappingInterface $table_mapping * The table mapping. * - * @internal Only to be used internally by Entity API. + * @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) { @@ -291,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 ? DefaultTableMapping::class : TemporaryTableMapping::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 f15ff33..b2407a1 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php @@ -1163,12 +1163,6 @@ 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)) { 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 620f20e..d34d949 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,16 +14,6 @@ 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 acaf919..0d2ebb9 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' => 64, + 'length' => 32, ), 'color' => array( 'type' => 'varchar', - 'length' => 64, + 'length' => 32, ), ), ) + $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 index e7bbeea..5b82415 100644 --- 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 @@ -4,7 +4,6 @@ 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 @@ -16,7 +15,7 @@ required: false translatable: true default_value: - - shape: { } + value1: { } default_value_callback: '' settings: { } -field_type: shape +field_type: multi_value_test 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 index 3af4f30..195e121 100644 --- 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 @@ -2,15 +2,13 @@ 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 +type: multi_value_test +settings: { } +module: entity_test_revisionable_schema_converter locked: false cardinality: -1 translatable: true diff --git a/core/modules/system/tests/modules/entity_test_revisionable_schema_converter/config/schema/entity_test_revisionable_schema_converter.schema.yml b/core/modules/system/tests/modules/entity_test_revisionable_schema_converter/config/schema/entity_test_revisionable_schema_converter.schema.yml new file mode 100644 index 0000000..5c60457 --- /dev/null +++ b/core/modules/system/tests/modules/entity_test_revisionable_schema_converter/config/schema/entity_test_revisionable_schema_converter.schema.yml @@ -0,0 +1,9 @@ +field.value.multi_value_test: + type: mapping + label: 'Default value' + mapping: + value1: + type: sequence + sequence: + type: string + label: 'First value' 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 index 1e9cdd7..d3d6349 100644 --- 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 @@ -4,5 +4,3 @@ description: 'Provides an entity type for testing the revisionable schema conver 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 index 9e50cc8..d844be9 100644 --- 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 @@ -5,6 +5,7 @@ * Provides an entity type for testing the revisionable schema converter. */ +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\StringTranslation\TranslatableMarkup; @@ -25,3 +26,22 @@ function entity_test_revisionable_schema_converter_entity_base_field_info(\Drupa return $fields; } } + +/** + * Implements hook_entity_type_alter(). + */ +function entity_test_revisionable_schema_converter_entity_type_alter(array &$entity_types) { + // Allow entity_test_to_rev_conversion tests to override the entity type + // definition. + $entity_types['entity_test_to_rev_conversion'] = \Drupal::state()->get('entity_test_to_rev_conversion.entity_type', $entity_types['entity_test_to_rev_conversion']); +} + +/** + * Implements hook_entity_presave(). + */ +function entity_test_revisionable_schema_converter_entity_presave(EntityInterface $entity) { + // Simulate an error during the save process of a test entity. + if ($entity->getEntityTypeId() === 'entity_test_to_rev_conversion' && \Drupal::state()->get('entity_test_to_rev_conversion.throw_exception', FALSE)) { + throw new \Exception('Peekaboo!'); + } +} 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 index e541c11..9335410 100644 --- 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 @@ -18,6 +18,7 @@ function entity_test_revisionable_schema_converter_post_update_make_revisionable \Drupal::entityTypeManager(), \Drupal::entityDefinitionUpdateManager(), \Drupal::service('entity.last_installed_schema.repository'), + \Drupal::keyValue('entity.storage_schema.sql'), \Drupal::database() ); 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 index 8d54231..329417c 100644 --- 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 @@ -2,10 +2,11 @@ namespace Drupal\entity_test_revisionable_schema_converter\Entity; +use Drupal\Core\Entity\ContentEntityBase; +use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\StringTranslation\TranslatableMarkup; -use Drupal\entity_test\Entity\EntityTest; /** * Defines the test entity_test_to_rev_conversion class. @@ -14,43 +15,33 @@ * id = "entity_test_to_rev_conversion", * label = @Translation("Test entity for revisionable schema conversion"), * handlers = { - * "view_builder" = "Drupal\entity_test\EntityTestViewBuilder", - * "access" = "Drupal\entity_test\EntityTestAccessControlHandler", - * "form" = { - * "default" = "Drupal\entity_test\EntityTestForm", - * "delete" = "Drupal\entity_test\EntityTestDeleteForm" - * }, * "translation" = "Drupal\content_translation\ContentTranslationHandler", * "views_data" = "Drupal\views\EntityViewsData", - * "route_provider" = { - * "html" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider", - * }, * }, * base_table = "entity_test_to_rev_conversion", * data_table = "entity_test_to_rev_conversion_field_data", - * revision_table = "entity_test_to_rev_conversion_revision", - * revision_data_table = "entity_test_to_rev_conversion_field_revision", - * admin_permission = "administer entity_test content", * translatable = TRUE, * entity_keys = { * "id" = "id", - * "revision" = "revision_id", * "uuid" = "uuid", * "bundle" = "type", * "label" = "name", * "langcode" = "langcode", * }, - * links = { - * "add-page" = "/entity_test_to_rev_conversion/add", - * "add-form" = "/entity_test_to_rev_conversion/add/{type}", - * "canonical" = "/entity_test_to_rev_conversion/manage/{entity_test_to_rev_conversion}", - * "edit-form" = "/entity_test_to_rev_conversion/manage/{entity_test_to_rev_conversion}/edit", - * "delete-form" = "/entity_test/delete/entity_test_to_rev_conversion/{entity_test_mul}", - * }, - * field_ui_base_route = "entity.entity_test_to_rev_conversion.admin_form", + * content_translation_ui_skip = TRUE, * ) */ -class EntityTestToRevConversion extends EntityTest { +class EntityTestToRevConversion extends ContentEntityBase { + + /** + * {@inheritdoc} + */ + public static function preCreate(EntityStorageInterface $storage, array &$values) { + parent::preCreate($storage, $values); + if (empty($values['type'])) { + $values['type'] = $storage->getEntityTypeId(); + } + } /** * {@inheritdoc} @@ -63,8 +54,8 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setTranslatable(TRUE) ->setRevisionable(TRUE); - $fields['test_multiple_properties'] = BaseFieldDefinition::create('shape') - ->setLabel(new TranslatableMarkup('Field with a multiple property')) + $fields['test_multiple_properties'] = BaseFieldDefinition::create('multi_value_test') + ->setLabel(new TranslatableMarkup('Field with multiple properties')) ->setTranslatable(TRUE) ->setRevisionable(TRUE); @@ -74,8 +65,8 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setTranslatable(TRUE) ->setRevisionable(TRUE); - $fields['test_multiple_properties_multiple_values'] = BaseFieldDefinition::create('shape') - ->setLabel(new TranslatableMarkup('Field with a multiple properties and multiple values')) + $fields['test_multiple_properties_multiple_values'] = BaseFieldDefinition::create('multi_value_test') + ->setLabel(new TranslatableMarkup('Field with multiple properties and multiple values')) ->setCardinality(2) ->setTranslatable(TRUE) ->setRevisionable(TRUE); diff --git a/core/modules/system/tests/modules/entity_test_revisionable_schema_converter/src/Plugin/Field/FieldType/MultiValueTestItem.php b/core/modules/system/tests/modules/entity_test_revisionable_schema_converter/src/Plugin/Field/FieldType/MultiValueTestItem.php new file mode 100644 index 0000000..fc75af3 --- /dev/null +++ b/core/modules/system/tests/modules/entity_test_revisionable_schema_converter/src/Plugin/Field/FieldType/MultiValueTestItem.php @@ -0,0 +1,66 @@ +setLabel(t('First value')); + + $properties['value2'] = DataDefinition::create('string') + ->setLabel(t('Second value')); + + return $properties; + } + + /** + * {@inheritdoc} + */ + public static function schema(FieldStorageDefinitionInterface $field_definition) { + return [ + 'columns' => [ + 'value1' => [ + 'type' => 'varchar', + 'length' => 64, + ], + 'value2' => [ + 'type' => 'varchar', + 'length' => 64, + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + $item = $this->getValue(); + return empty($item['value1']) && empty($item['value2']); + } + + /** + * {@inheritdoc} + */ + public static function mainPropertyName() { + return 'value1'; + } + +} diff --git a/core/tests/Drupal/Tests/Core/Entity/RevisionableSchemaConverterTest.php b/core/tests/Drupal/Tests/Core/Entity/RevisionableSchemaConverterTest.php index 9d32fc2..994a1b8 100644 --- a/core/tests/Drupal/Tests/Core/Entity/RevisionableSchemaConverterTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/RevisionableSchemaConverterTest.php @@ -14,6 +14,20 @@ class RevisionableSchemaConverterTest extends UpdatePathTestBase { /** + * The last installed schema repository service. + * + * @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface + */ + protected $lastInstalledSchemaRepository; + + /** + * The key-value collection for tracking installed storage schema. + * + * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface + */ + protected $installedStorageSchema; + + /** * {@inheritdoc} */ protected static $modules = ['entity_test_revisionable_schema_converter']; @@ -21,6 +35,16 @@ class RevisionableSchemaConverterTest extends UpdatePathTestBase { /** * {@inheritdoc} */ + protected function setUp() { + parent::setUp(); + + $this->lastInstalledSchemaRepository = \Drupal::service('entity.last_installed_schema.repository'); + $this->installedStorageSchema = \Drupal::keyValue('entity.storage_schema.sql'); + } + + /** + * {@inheritdoc} + */ protected function setDatabaseDumpFiles() { $this->databaseDumpFiles = [ __DIR__ . '/../../../../../modules/system/tests/fixtures/update/drupal-8.2.0-filled.standard.entity_test_to_rev_conversion.php.gz', @@ -29,27 +53,25 @@ protected function setDatabaseDumpFiles() { } /** - * Tests the entity type is revisionable and shortcut entities are accessible. + * Tests the conversion of an entity type to revisionable. */ 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'); + $entity_test_to_rev_conversion = $this->lastInstalledSchemaRepository->getLastInstalledDefinition('entity_test_to_rev_conversion'); $this->assertFalse($entity_test_to_rev_conversion->isRevisionable()); + // Make the entity type revisionable and run the updates. + $this->updateEntityTypeToRevisionable(); + $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'); + $entity_test_to_rev_conversion = $this->lastInstalledSchemaRepository->getLastInstalledDefinition('entity_test_to_rev_conversion'); $this->assertTrue($entity_test_to_rev_conversion->isRevisionable()); + /** @var \Drupal\Core\Entity\Sql\SqlEntityStorageInterface $storage */ + $storage = \Drupal::entityTypeManager()->getStorage('entity_test_to_rev_conversion'); $this->assertEqual(count($storage->loadMultiple()), 101, 'All test entities were found.'); // Check that each field value was copied correctly to the revision tables. @@ -58,21 +80,21 @@ public function testMakeRevisionable() { $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 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 - 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 . ' - 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 - 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 . ' - 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); @@ -81,33 +103,131 @@ public function testMakeRevisionable() { $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 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 - 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 . ' - 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 - 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 . ' - 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 ($table_names as $table_name) { + 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_to_rev_conversion'); + $original_storage_definitions = $this->lastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions('entity_test_to_rev_conversion'); + + $original_entity_schema_data = $this->installedStorageSchema->get('entity_test_to_rev_conversion.entity_schema_data', []); + foreach ($original_storage_definitions as $storage_definition) { + $original_field_schema_data[$storage_definition->getName()] = $this->installedStorageSchema->get('entity_test_to_rev_conversion.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_to_rev_conversion.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->updateEntityTypeToRevisionable(); + + $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_to_rev_conversion'); + $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_to_rev_conversion'); + $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_to_rev_conversion'); + + // Check that installed storage schema did not change. + $new_entity_schema_data = $this->installedStorageSchema->get('entity_test_to_rev_conversion.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_to_rev_conversion.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_to_rev_conversion')); + $this->assertTrue($schema->tableExists('entity_test_to_rev_conversion_field_data')); + + $base_table_count = \Drupal::database()->select('entity_test_to_rev_conversion') + ->countQuery() + ->execute() + ->fetchField(); + $this->assertEqual($base_table_count, 101); + + $data_table_count = \Drupal::database()->select('entity_test_to_rev_conversion_field_data') + ->countQuery() + ->execute() + ->fetchField(); + // There are two records for each entity, one for English and one for + // Romanian. + $this->assertEqual($data_table_count, 202); + + $base_table_row = \Drupal::database()->select('entity_test_to_rev_conversion') + ->fields('entity_test_to_rev_conversion') + ->condition('id', 1, '=') + ->condition('langcode', 'en', '=') + ->execute() + ->fetchAllAssoc('id'); + $this->assertEqual('190ba232-35d9-4a3c-b909-e052ab5b8e93', $base_table_row[1]->uuid); + + $data_table_table_row = \Drupal::database()->select('entity_test_to_rev_conversion_field_data') + ->fields('entity_test_to_rev_conversion_field_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); + } + + /** * Creates entities for the database dump used in this update test. * * This method is only useful for generating test content for @@ -116,12 +236,7 @@ public function testMakeRevisionable() { * * 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. + * - The 'entity_test_revisionable_schema_converter' module is enabled; * - Language and Content Translation modules enabled; * - The Romanian language added; * - All fields from the 'entity_test_to_rev_conversion' entity type are @@ -134,8 +249,8 @@ public static function createTestEntities() { 'name' => $i, 'test_single_property' => $i . ' - test single property', 'test_multiple_properties' => [[ - 'shape' => $i . ' - test multiple properties - shape', - 'color' => $i . ' - test multiple properties - color', + 'value1' => $i . ' - test multiple properties - value1', + 'value2' => $i . ' - test multiple properties - value2', ]], 'test_single_property_multiple_values' => [ ['value' => $i . ' - test single property multiple values 0'], @@ -143,22 +258,22 @@ public static function createTestEntities() { ], 'test_multiple_properties_multiple_values' => [ [ - 'shape' => $i . ' - test multiple properties multiple values - shape 0', - 'color' => $i . ' - test multiple properties multiple values - color 0', + 'value1' => $i . ' - test multiple properties multiple values - value1 0', + 'value2' => $i . ' - test multiple properties multiple values - value2 0', ], [ - 'shape' => $i . ' - test multiple properties multiple values - shape 1', - 'color' => $i . ' - test multiple properties multiple values - color 1', + 'value1' => $i . ' - test multiple properties multiple values - value1 1', + 'value2' => $i . ' - test multiple properties multiple values - value2 1', ] ], 'field_test_configurable_field' => [ [ - 'shape' => $i . ' - field test configurable field - shape 0', - 'color' => $i . ' - field test configurable field - color 0', + 'value1' => $i . ' - field test configurable field - value1 0', + 'value2' => $i . ' - field test configurable field - value2 0', ], [ - 'shape' => $i . ' - field test configurable field - shape 1', - 'color' => $i . ' - field test configurable field - color 1', + 'value1' => $i . ' - field test configurable field - value1 1', + 'value2' => $i . ' - field test configurable field - value2 1', ] ], 'test_entity_base_field_info' => $i . ' - test entity base field info', @@ -167,8 +282,8 @@ public static function createTestEntities() { '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', + 'value1' => $i . ' - test multiple properties - value1 - ro', + 'value2' => $i . ' - test multiple properties - value2 - ro', ]], 'test_single_property_multiple_values' => [ ['value' => $i . ' - test single property multiple values 0 - ro'], @@ -176,22 +291,22 @@ public static function createTestEntities() { ], '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', + 'value1' => $i . ' - test multiple properties multiple values - value1 0 - ro', + 'value2' => $i . ' - test multiple properties multiple values - value2 0 - ro', ], [ - 'shape' => $i . ' - test multiple properties multiple values - shape 1 - ro', - 'color' => $i . ' - test multiple properties multiple values - color 1 - ro', + 'value1' => $i . ' - test multiple properties multiple values - value1 1 - ro', + 'value2' => $i . ' - test multiple properties multiple values - value2 1 - ro', ] ], 'field_test_configurable_field' => [ [ - 'shape' => $i . ' - field test configurable field - shape 0 - ro', - 'color' => $i . ' - field test configurable field - color 0 - ro', + 'value1' => $i . ' - field test configurable field - value1 0 - ro', + 'value2' => $i . ' - field test configurable field - value2 0 - ro', ], [ - 'shape' => $i . ' - field test configurable field - shape 1 - ro', - 'color' => $i . ' - field test configurable field - color 1 - ro', + 'value1' => $i . ' - field test configurable field - value1 1 - ro', + 'value2' => $i . ' - field test configurable field - value2 1 - ro', ] ], 'test_entity_base_field_info' => $i . ' - test entity base field info - ro', @@ -201,4 +316,20 @@ public static function createTestEntities() { } } + /** + * Updates the 'entity_test_to_rev_conversion' entity type to revisionable. + */ + protected function updateEntityTypeToRevisionable() { + $entity_type = clone \Drupal::entityTypeManager()->getDefinition('entity_test_to_rev_conversion'); + + $keys = $entity_type->getKeys(); + $keys['revision'] = 'revision_id'; + $entity_type->set('entity_keys', $keys); + + $entity_type->set('revision_table', 'entity_test_to_rev_conversion_revision'); + $entity_type->set('revision_data_table', 'entity_test_to_rev_conversion_field_revision'); + + \Drupal::state()->set('entity_test_to_rev_conversion.entity_type', $entity_type); + } + }