Commit d938b0a8 authored by catch's avatar catch

Issue #3024728 by amateescu, plach: Preserve the backup tables after an entity...

Issue #3024728 by amateescu, plach: Preserve the backup tables after an entity type schema conversion process
parent 12d7ed53
......@@ -17,6 +17,7 @@
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 @@ protected function deletedFieldsRepository() {
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 @@ protected function preUpdateEntityTypeSchema(EntityTypeInterface $entity_type, E
$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 @@ protected function postUpdateEntityTypeSchema(EntityTypeInterface $entity_type,
}
// 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 @@ protected function handleEntityTypeSchemaUpdateExceptionOnDataCopy(EntityTypeInt
* 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 @@ public static function getTemporaryTableMappingPrefix(EntityTypeInterface $entit
$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);
}
/**
......
......@@ -165,6 +165,9 @@ public function testFieldableEntityTypeUpdates($initial_rev, $initial_mul, $new_
// 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 @@ protected function assertNonRevisionableAndNonTranslatable() {
$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 @@ public function testFieldableEntityTypeUpdatesErrorHandling() {
$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 @@ public function testFieldableEntityTypeUpdatesErrorHandling() {
$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 @@ public function testFieldableEntityTypeUpdatesErrorHandling() {
}
}
/**
* 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);
}
}
......@@ -754,6 +754,15 @@
*/
$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.
*
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment