diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php index 1b3f60ff4a7..b2d1d291671 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php @@ -17,6 +17,7 @@ use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Field\FieldException; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Site\Settings; /** * Defines a schema handler that supports revisionable, translatable entities. @@ -95,6 +96,13 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage */ protected $installedStorageSchema; + /** + * The key-value collection for tracking entity update backup repository. + * + * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface + */ + protected $updateBackupRepository; + /** * The deleted fields repository. * @@ -163,6 +171,23 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage return $this->deletedFieldsRepository; } + /** + * Gets the key/value collection for tracking the entity update backups. + * + * @return \Drupal\Core\KeyValueStore\KeyValueStoreInterface + * A key/value collection. + * + * @todo Inject this dependency in the constructor once this class can be + * instantiated as a regular entity handler. + * @see https://www.drupal.org/node/2332857 + */ + protected function updateBackupRepository() { + if (!isset($this->updateBackupRepository)) { + $this->updateBackupRepository = \Drupal::keyValue('entity.update_backup'); + } + return $this->updateBackupRepository; + } + /** * Refreshes the table mapping with updated definitions. * @@ -432,7 +457,11 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage $sandbox['temporary_table_mapping'] = $this->storage->getCustomTableMapping($entity_type, $field_storage_definitions, $temporary_prefix); $sandbox['new_table_mapping'] = $this->storage->getCustomTableMapping($entity_type, $field_storage_definitions); $sandbox['original_table_mapping'] = $this->storage->getCustomTableMapping($original, $original_field_storage_definitions); - $sandbox['backup_table_mapping'] = $this->storage->getCustomTableMapping($original, $original_field_storage_definitions, 'old_'); + + $backup_prefix = static::getTemporaryTableMappingPrefix($original, $original_field_storage_definitions, 'old_'); + $sandbox['backup_table_mapping'] = $this->storage->getCustomTableMapping($original, $original_field_storage_definitions, $backup_prefix); + $sandbox['backup_prefix_key'] = substr($backup_prefix, 4); + $sandbox['backup_request_time'] = \Drupal::time()->getRequestTime(); // Create temporary tables based on the new entity type and field storage // definitions. @@ -554,11 +583,22 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage } // At this point the update process either finished successfully or any - // error has been thrown already, so we can drop the backup entity tables. - // @todo Decide whether we should keep these tables around. - // @see https://www.drupal.org/project/drupal/issues/3024728 - foreach ($backup_table_names as $original_table_name => $backup_table_name) { - $this->database->schema()->dropTable($backup_table_name); + // error has been thrown already. We can either keep the backup tables in + // place or drop them. + if (Settings::get('entity_update_backup', TRUE)) { + $backup_key = $sandbox['backup_prefix_key']; + $backup = [ + 'entity_type' => $original, + 'field_storage_definitions' => $original_field_storage_definitions, + 'table_mapping' => $backup_table_mapping, + 'request_time' => $sandbox['backup_request_time'], + ]; + $this->updateBackupRepository()->set("{$original->id()}.$backup_key", $backup); + } + else { + foreach ($backup_table_names as $original_table_name => $backup_table_name) { + $this->database->schema()->dropTable($backup_table_name); + } } } @@ -582,13 +622,15 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage * An entity type definition. * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_storage_definitions * An array of field storage definitions. + * @param string $first_prefix_part + * (optional) The first part of the prefix. Defaults to 'tmp_'. * * @return string * A temporary table mapping prefix. * * @internal */ - public static function getTemporaryTableMappingPrefix(EntityTypeInterface $entity_type, array $field_storage_definitions) { + public static function getTemporaryTableMappingPrefix(EntityTypeInterface $entity_type, array $field_storage_definitions, $first_prefix_part = 'tmp_') { // Construct a unique prefix based on the contents of the entity type and // field storage definitions. $prefix_parts[] = spl_object_hash($entity_type); @@ -598,7 +640,7 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage $prefix_parts[] = \Drupal::time()->getRequestTime(); $hash = hash('sha256', implode('', $prefix_parts)); - return 'tmp_' . substr($hash, 0, 6); + return $first_prefix_part . substr($hash, 0, 6); } /** diff --git a/core/tests/Drupal/KernelTests/Core/Entity/FieldableEntityDefinitionUpdateTest.php b/core/tests/Drupal/KernelTests/Core/Entity/FieldableEntityDefinitionUpdateTest.php index d3beb19f73a..d9a50feb65b 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/FieldableEntityDefinitionUpdateTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/FieldableEntityDefinitionUpdateTest.php @@ -165,6 +165,9 @@ class FieldableEntityDefinitionUpdateTest extends EntityKernelTestBase { // Check that we can still save new entities after the schema has been // updated. $this->insertData($new_rev, $new_mul); + + // Check that the backup tables have been kept in place. + $this->assertBackupTables(); } /** @@ -569,10 +572,25 @@ class FieldableEntityDefinitionUpdateTest extends EntityKernelTestBase { $this->assertFalse($database_schema->tableExists($entity_type->getRevisionDataTable())); } + /** + * Asserts that the backup tables have been kept after a successful update. + */ + protected function assertBackupTables() { + $backups = \Drupal::keyValue('entity.update_backup')->getAll(); + $backup = reset($backups); + + $schema = $this->database->schema(); + foreach ($backup['table_mapping']->getTableNames() as $table_name) { + $this->assertTrue($schema->tableExists($table_name)); + } + } + /** * Tests that a failed entity schema update preserves the existing data. */ public function testFieldableEntityTypeUpdatesErrorHandling() { + $schema = $this->database->schema(); + // First, convert the entity type to be translatable for better coverage and // insert some initial data. $entity_type = $this->getUpdatedEntityTypeDefinition(FALSE, TRUE); @@ -581,6 +599,12 @@ class FieldableEntityDefinitionUpdateTest extends EntityKernelTestBase { $this->assertEntityTypeSchema(FALSE, TRUE); $this->insertData(FALSE, TRUE); + $tables = $schema->findTables('old_%'); + $this->assertCount(3, $tables); + foreach ($tables as $table) { + $schema->dropTable($table); + } + $original_entity_type = $this->lastInstalledSchemaRepository->getLastInstalledDefinition('entity_test_update'); $original_storage_definitions = $this->lastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions('entity_test_update'); @@ -640,14 +664,19 @@ class FieldableEntityDefinitionUpdateTest extends EntityKernelTestBase { $this->assertEquals($original_field_schema_data, $new_field_schema_data); // Check that temporary tables have been removed. - $schema = $this->database->schema(); - $temporary_table_names = $storage->getCustomTableMapping($new_entity_type, $new_storage_definitions, 'tmp_')->getTableNames(); - $current_table_names = $storage->getCustomTableMapping($new_entity_type, $new_storage_definitions)->getTableNames(); - foreach (array_combine($temporary_table_names, $current_table_names) as $temp_table_name => $table_name) { + $tables = $schema->findTables('tmp_%'); + $this->assertCount(0, $tables); + + $current_table_names = $storage->getCustomTableMapping($original_entity_type, $original_storage_definitions)->getTableNames(); + foreach ($current_table_names as $table_name) { $this->assertTrue($schema->tableExists($table_name)); - $this->assertFalse($schema->tableExists($temp_table_name)); } + // Check that backup tables do not exist anymore, since they were + // restored/renamed. + $tables = $schema->findTables('old_%'); + $this->assertCount(0, $tables); + // 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')); @@ -723,4 +752,37 @@ class FieldableEntityDefinitionUpdateTest extends EntityKernelTestBase { } } + /** + * Tests the removal of the backup tables after a successful update. + */ + public function testFieldableEntityTypeUpdatesRemoveBackupTables() { + $schema = $this->database->schema(); + + // Convert the entity type to be revisionable. + $entity_type = $this->getUpdatedEntityTypeDefinition(TRUE, FALSE); + $field_storage_definitions = $this->getUpdatedFieldStorageDefinitions(TRUE, FALSE); + $this->entityDefinitionUpdateManager->updateFieldableEntityType($entity_type, $field_storage_definitions); + + // Check that backup tables are kept by default. + $tables = $schema->findTables('old_%'); + $this->assertCount(3, $tables); + foreach ($tables as $table) { + $schema->dropTable($table); + } + + // Make the entity update process drop the backup tables after a successful + // update. + $settings = Settings::getAll(); + $settings['entity_update_backup'] = FALSE; + new Settings($settings); + + $entity_type = $this->getUpdatedEntityTypeDefinition(TRUE, TRUE); + $field_storage_definitions = $this->getUpdatedFieldStorageDefinitions(TRUE, TRUE); + $this->entityDefinitionUpdateManager->updateFieldableEntityType($entity_type, $field_storage_definitions); + + // Check that backup tables have been dropped. + $tables = $schema->findTables('old_%'); + $this->assertCount(0, $tables); + } + } diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index ae64d2ffe89..5922cb1ea26 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -754,6 +754,15 @@ $settings['file_scan_ignore_directories'] = [ */ $settings['entity_update_batch_size'] = 50; +/** + * Entity update backup. + * + * This is used to inform the entity storage handler that the backup tables as + * well as the original entity type and field storage definitions should be + * retained after a successful entity update process. + */ +$settings['entity_update_backup'] = TRUE; + /** * Load local development override configuration, if available. *