diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php index d696a94..a5dc02c 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 $temporary = 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->temporary = $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->temporary ? 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..ab77047 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 ($entity_type->get('requires_data_migration') === FALSE) { + 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..5347573 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchemaConverter.php @@ -0,0 +1,461 @@ +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_name = TemporaryTableMapping::getTempTableName($table_name, 'old_'); + $this->database->schema()->renameTable($table_name, $old_table_name); + } + + // 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->set('requires_data_migration', 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 properly restored. + if ($this->database->schema()->tableExists($table_name)) { + $this->database->schema()->dropTable($table_name); + } + + $old_table_name = TemporaryTableMapping::getTempTableName($table_name, 'old_'); + $this->database->schema()->renameTable($old_table_name, $table_name); + } + + // Re-throw the original exception. + throw $e; + } + + // At this point the update process either finished successfully or any + // error has been handled already, so we can drop the initial entity + // tables. + foreach ($sandbox['original_table_mapping']->getTableNames() as $table_name) { + $old_table_name = TemporaryTableMapping::getTempTableName($table_name, 'old_'); + $this->database->schema()->dropTable($old_table_name); + } + } + } + + /** + * 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. + $step_size = 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, $step_size) + ->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/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.entity-test-schema-converter-enabled.php b/core/modules/system/tests/fixtures/update/drupal-8.entity-test-schema-converter-enabled.php new file mode 100644 index 0000000..3a5bc18 --- /dev/null +++ b/core/modules/system/tests/fixtures/update/drupal-8.entity-test-schema-converter-enabled.php @@ -0,0 +1,36 @@ +merge('key_value') + ->fields([ + 'value' => 'i:8000;', + 'name' => 'entity_test_schema_converter', + 'collection' => 'system.schema', + ]) + ->condition('collection', 'system.schema') + ->condition('name', 'entity_test_schema_converter') + ->execute(); + +// Update core.extension. +$extensions = $connection->select('config') + ->fields('config', ['data']) + ->condition('collection', '') + ->condition('name', 'core.extension') + ->execute() + ->fetchField(); +$extensions = unserialize($extensions); +$extensions['module']['entity_test_schema_converter'] = 8000; +$connection->update('config') + ->fields([ + 'data' => serialize($extensions), + 'collection' => '', + 'name' => 'core.extension', + ]) + ->condition('collection', '') + ->condition('name', 'core.extension') + ->execute(); diff --git a/core/modules/system/tests/modules/entity_test_schema_converter/entity_test_schema_converter.info.yml b/core/modules/system/tests/modules/entity_test_schema_converter/entity_test_schema_converter.info.yml new file mode 100644 index 0000000..b77990a --- /dev/null +++ b/core/modules/system/tests/modules/entity_test_schema_converter/entity_test_schema_converter.info.yml @@ -0,0 +1,8 @@ +name: 'Entity Schema Converter Test' +type: module +description: 'Provides testing for the entity schema converter.' +package: Testing +version: VERSION +core: 8.x +dependencies: + - entity_test_update \ No newline at end of file diff --git a/core/modules/system/tests/modules/entity_test_schema_converter/entity_test_schema_converter.post_update.php b/core/modules/system/tests/modules/entity_test_schema_converter/entity_test_schema_converter.post_update.php new file mode 100644 index 0000000..d6fd0ca --- /dev/null +++ b/core/modules/system/tests/modules/entity_test_schema_converter/entity_test_schema_converter.post_update.php @@ -0,0 +1,39 @@ +convertToRevisionable( + $sandbox, + [ + 'test_single_property', + 'test_multiple_properties', + 'test_single_property_multiple_values', + 'test_multiple_properties_multiple_values', + 'test_entity_base_field_info', + ]); +} + +/** + * @} End of "addtogroup updates-8.4.x". + */