Issue #3024728 by amateescu, plach: Preserve the backup tables after an entity type schema conversion process
							parent
							
								
									12d7ed5308
								
							
						
					
					
						commit
						d938b0a8ec
					
				| 
						 | 
				
			
			@ -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);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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.
 | 
			
		||||
 *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue