Commit 3e7619bd authored by catch's avatar catch
Browse files

Issue #3056539 by plach, DamienMcKenna, catch, hchonov, amateescu,...

Issue #3056539 by plach, DamienMcKenna, catch, hchonov, amateescu, tim.plunkett: Updating an entity type from non-revisionable to revisionable fails if it has non-revisionable fields stored in dedicated tables
parent f8d4b113
......@@ -386,7 +386,8 @@ public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
$schema_handler = $this->database->schema();
// Delete entity and field tables.
foreach ($this->getTableMapping($entity_type)->getTableNames() as $table_name) {
$table_names = $this->getTableNames($entity_type, $this->fieldStorageDefinitions, $this->getTableMapping($entity_type));
foreach ($table_names as $table_name) {
if ($schema_handler->tableExists($table_name)) {
$schema_handler->dropTable($table_name);
}
......@@ -461,7 +462,10 @@ protected function preUpdateEntityTypeSchema(EntityTypeInterface $entity_type, E
// Create temporary tables based on the new entity type and field storage
// definitions.
$temporary_table_names = array_combine($sandbox['new_table_mapping']->getTableNames(), $sandbox['temporary_table_mapping']->getTableNames());
$temporary_table_names = array_combine(
$this->getTableNames($entity_type, $field_storage_definitions, $sandbox['new_table_mapping']),
$this->getTableNames($entity_type, $field_storage_definitions, $sandbox['temporary_table_mapping'])
);
$this->entityType = $entity_type;
$this->fieldStorageDefinitions = $field_storage_definitions;
......@@ -501,14 +505,15 @@ protected function preUpdateEntityTypeSchema(EntityTypeInterface $entity_type, E
$this->storage->setEntityType($original);
$this->storage->setFieldStorageDefinitions($original_field_storage_definitions);
$this->storage->setTableMapping($sandbox['original_table_mapping']);
// Store the temporary table name mappings for later reuse.
$sandbox['temporary_table_names'] = $temporary_table_names;
}
/**
* {@inheritdoc}
*/
protected function postUpdateEntityTypeSchema(EntityTypeInterface $entity_type, EntityTypeInterface $original, array $field_storage_definitions, array $original_field_storage_definitions, array &$sandbox = NULL) {
/** @var \Drupal\Core\Entity\Sql\TableMappingInterface $temporary_table_mapping */
$temporary_table_mapping = $sandbox['temporary_table_mapping'];
/** @var \Drupal\Core\Entity\Sql\TableMappingInterface $original_table_mapping */
$original_table_mapping = $sandbox['original_table_mapping'];
/** @var \Drupal\Core\Entity\Sql\TableMappingInterface $new_table_mapping */
......@@ -518,7 +523,10 @@ protected function postUpdateEntityTypeSchema(EntityTypeInterface $entity_type,
// Rename the original tables so we can put them back in place in case
// anything goes wrong.
$backup_table_names = array_combine($original_table_mapping->getTableNames(), $backup_table_mapping->getTableNames());
$backup_table_names = array_combine(
$this->getTableNames($original, $original_field_storage_definitions, $original_table_mapping),
$this->getTableNames($original, $original_field_storage_definitions, $backup_table_mapping)
);
$renamed_tables = [];
try {
foreach ($backup_table_names as $original_table_name => $backup_table_name) {
......@@ -538,8 +546,7 @@ protected function postUpdateEntityTypeSchema(EntityTypeInterface $entity_type,
// Put the new tables in place and update the entity type and field storage
// definitions.
try {
$table_name_mapping = array_combine($temporary_table_mapping->getTableNames(), $new_table_mapping->getTableNames());
foreach ($table_name_mapping as $temp_table_name => $current_table_name) {
foreach ($sandbox['temporary_table_names'] as $current_table_name => $temp_table_name) {
$this->database->schema()->renameTable($temp_table_name, $current_table_name);
}
......@@ -604,15 +611,50 @@ protected function postUpdateEntityTypeSchema(EntityTypeInterface $entity_type,
}
}
/**
* Gets a list of table names for this entity type, field storage and mapping.
*
* The default table mapping does not return dedicated revision table names
* for non-revisionable fields attached to revisionable entity types. Since
* both the storage and the storage handlers expect them to be existing, the
* missing table names need to be manually restored.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* An entity type definition.
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_storage_definitions
* An array of field storage definitions.
* @param \Drupal\Core\Entity\Sql\TableMappingInterface $table_mapping
* A table mapping.
*
* @return string[]
* An array of field table names.
*
* @todo Remove this once the behavior of the default table mapping, the
* storage handler, and the storage schema handler are reconciled in
* https://www.drupal.org/node/3113639.
*/
private function getTableNames(EntityTypeInterface $entity_type, array $field_storage_definitions, TableMappingInterface $table_mapping) {
$table_names = $table_mapping->getTableNames();
if ($table_mapping instanceof DefaultTableMapping && $entity_type->isRevisionable()) {
foreach ($field_storage_definitions as $storage_definition) {
if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
$dedicated_revision_table_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
if (!$storage_definition->isRevisionable() && !in_array($dedicated_revision_table_name, $table_names)) {
$table_names[] = $dedicated_revision_table_name;
}
}
}
}
return $table_names;
}
/**
* {@inheritdoc}
*/
protected function handleEntityTypeSchemaUpdateExceptionOnDataCopy(EntityTypeInterface $entity_type, EntityTypeInterface $original, array &$sandbox) {
// In case of an error during the save process, we need to clean up the
// temporary tables.
/** @var \Drupal\Core\Entity\Sql\TableMappingInterface $temporary_table_mapping */
$temporary_table_mapping = $sandbox['temporary_table_mapping'];
foreach ($temporary_table_mapping->getTableNames() as $table_name) {
foreach ($sandbox['temporary_table_names'] as $table_name) {
$this->database->schema()->dropTable($table_name);
}
}
......
......@@ -1828,6 +1828,11 @@ function hook_entity_form_display_alter(\Drupal\Core\Entity\Display\EntityFormDi
/**
* Provides custom base field definitions for a content entity type.
*
* Field (storage) definitions returned by this hook must run through the
* regular field storage life-cycle operations: they need to be properly
* installed, updated, and uninstalled. This would typically be done through the
* Entity Update API provided by the entity definition update manager.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
*
......@@ -1839,6 +1844,8 @@ function hook_entity_form_display_alter(\Drupal\Core\Entity\Display\EntityFormDi
* @see hook_entity_bundle_field_info_alter()
* @see \Drupal\Core\Field\FieldDefinitionInterface
* @see \Drupal\Core\Entity\EntityFieldManagerInterface::getFieldDefinitions()
* @see \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
* @see https://www.drupal.org/node/3034742
*/
function hook_entity_base_field_info(\Drupal\Core\Entity\EntityTypeInterface $entity_type) {
if ($entity_type->id() == 'node') {
......@@ -1943,6 +1950,11 @@ function hook_entity_bundle_field_info_alter(&$fields, \Drupal\Core\Entity\Entit
/**
* Provides field storage definitions for a content entity type.
*
* Field storage definitions returned by this hook must run through the regular
* field storage life-cycle operations: they need to be properly installed,
* updated, and uninstalled. This would typically be done through the Entity
* Update API provided by the entity definition update manager.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
*
......@@ -1952,6 +1964,8 @@ function hook_entity_bundle_field_info_alter(&$fields, \Drupal\Core\Entity\Entit
* @see hook_entity_field_storage_info_alter()
* @see \Drupal\Core\Field\FieldStorageDefinitionInterface
* @see \Drupal\Core\Entity\EntityFieldManagerInterface::getFieldStorageDefinitions()
* @see \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
* @see https://www.drupal.org/node/3034742
*/
function hook_entity_field_storage_info(\Drupal\Core\Entity\EntityTypeInterface $entity_type) {
if (\Drupal::entityTypeManager()->getStorage($entity_type->id()) instanceof DynamicallyFieldableEntityStorageInterface) {
......
......@@ -318,12 +318,18 @@ protected function removeBaseFieldIndex() {
*
* @param string $type
* (optional) The field type for the new field. Defaults to 'string'.
* @param bool $revisionable
* (optional) Whether the field should be revisionable. Defaults to FALSE.
* @param bool $translatable
* (optional) Whether the field should be translatable. Defaults to FALSE.
*/
protected function addBundleField($type = 'string') {
protected function addBundleField($type = 'string', $revisionable = FALSE, $translatable = FALSE) {
$definitions['new_bundle_field'] = FieldStorageDefinition::create($type)
->setName('new_bundle_field')
->setLabel(t('A new bundle field'))
->setTargetEntityTypeId('entity_test_update');
->setTargetEntityTypeId('entity_test_update')
->setRevisionable($revisionable)
->setTranslatable($translatable);
$this->state->set('entity_test_update.additional_field_storage_definitions', $definitions);
$this->state->set('entity_test_update.additional_bundle_field_definitions.test_bundle', $definitions);
}
......
......@@ -94,6 +94,10 @@ protected function setUp() {
$this->entityFieldManager = $this->container->get('entity_field.manager');
$this->database = $this->container->get('database');
// Add a non-revisionable bundle field to test revision field table
// handling.
$this->addBundleField('string', FALSE, TRUE);
// The 'changed' field type has a special behavior because it updates itself
// automatically if any of the other field values of an entity have been
// updated, so add it to the entity type that is being tested in order to
......@@ -294,7 +298,9 @@ protected function insertData($revisionable, $translatable) {
for ($i = $next_id; $i <= $next_id + 2; $i++) {
$entity = $storage->create([
'id' => $i,
'type' => 'test_bundle',
'name' => 'test entity - ' . $i . ' - en',
'new_bundle_field' => 'bundle field - ' . $i . ' - en',
'test_multiple_properties' => [
'value1' => 'shared table - ' . $i . ' - value 1 - en',
'value2' => 'shared table - ' . $i . ' - value 2 - en',
......@@ -315,6 +321,7 @@ protected function insertData($revisionable, $translatable) {
if ($translatable) {
$translation = $entity->addTranslation('ro', [
'name' => 'test entity - ' . $i . ' - ro',
'new_bundle_field' => 'bundle field - ' . $i . ' - ro',
'test_multiple_properties' => [
'value1' => 'shared table - ' . $i . ' - value 1 - ro',
'value2' => 'shared table - ' . $i . ' - value 2 - ro',
......@@ -338,6 +345,7 @@ protected function insertData($revisionable, $translatable) {
// Create a new pending revision.
$revision_2 = $storage->createRevision($entity, FALSE);
$revision_2->name = 'test entity - ' . $i . ' - en - rev2';
$revision_2->new_bundle_field = 'bundle field - ' . $i . ' - en - rev2';
$revision_2->test_multiple_properties->value1 = 'shared table - ' . $i . ' - value 1 - en - rev2';
$revision_2->test_multiple_properties->value2 = 'shared table - ' . $i . ' - value 2 - en - rev2';
$revision_2->test_multiple_properties_multiple_values[0]->value1 = 'dedicated table - ' . $i . ' - delta 0 - value 1 - en - rev2';
......@@ -349,6 +357,7 @@ protected function insertData($revisionable, $translatable) {
if ($translatable) {
$revision_2_translation = $storage->createRevision($entity->getTranslation('ro'), FALSE);
$revision_2_translation->name = 'test entity - ' . $i . ' - ro - rev2';
$revision_2->new_bundle_field = 'bundle field - ' . $i . ' - ro - rev2';
$revision_2->test_multiple_properties->value1 = 'shared table - ' . $i . ' - value 1 - ro - rev2';
$revision_2->test_multiple_properties->value2 = 'shared table - ' . $i . ' - value 2 - ro - rev2';
$revision_2_translation->test_multiple_properties_multiple_values[0]->value1 = 'dedicated table - ' . $i . ' - delta 0 - value 1 - ro - rev2';
......@@ -375,6 +384,7 @@ protected function assertEntityData($revisionable, $translatable) {
foreach ($entities as $entity_id => $entity) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$this->assertEquals("test entity - {$entity->id()} - en", $entity->label());
$this->assertEquals("bundle field - {$entity->id()} - en", $entity->new_bundle_field->value);
$this->assertEquals("shared table - {$entity->id()} - value 1 - en", $entity->test_multiple_properties->value1);
$this->assertEquals("shared table - {$entity->id()} - value 2 - en", $entity->test_multiple_properties->value2);
$this->assertEquals("dedicated table - {$entity->id()} - delta 0 - value 1 - en", $entity->test_multiple_properties_multiple_values[0]->value1);
......@@ -385,6 +395,7 @@ protected function assertEntityData($revisionable, $translatable) {
if ($translatable) {
$translation = $entity->getTranslation('ro');
$this->assertEquals("test entity - {$translation->id()} - ro", $translation->label());
$this->assertEquals("bundle field - {$entity->id()} - ro", $translation->new_bundle_field->value);
$this->assertEquals("shared table - {$translation->id()} - value 1 - ro", $translation->test_multiple_properties->value1);
$this->assertEquals("shared table - {$translation->id()} - value 2 - ro", $translation->test_multiple_properties->value2);
$this->assertEquals("dedicated table - {$translation->id()} - delta 0 - value 1 - ro", $translation->test_multiple_properties_multiple_values[0]->value1);
......@@ -403,6 +414,7 @@ protected function assertEntityData($revisionable, $translatable) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $revision */
$revision_label = $revision->isDefaultRevision() ? NULL : ' - rev2';
$this->assertEquals("test entity - {$revision->id()} - en{$revision_label}", $revision->label());
$this->assertEquals("bundle field - {$revision->id()} - en{$revision_label}", $revision->new_bundle_field->value);
$this->assertEquals("shared table - {$revision->id()} - value 1 - en{$revision_label}", $revision->test_multiple_properties->value1);
$this->assertEquals("shared table - {$revision->id()} - value 2 - en{$revision_label}", $revision->test_multiple_properties->value2);
$this->assertEquals("dedicated table - {$revision->id()} - delta 0 - value 1 - en{$revision_label}", $revision->test_multiple_properties_multiple_values[0]->value1);
......@@ -413,6 +425,7 @@ protected function assertEntityData($revisionable, $translatable) {
if ($translatable) {
$translation = $revision->getTranslation('ro');
$this->assertEquals("test entity - {$translation->id()} - ro{$revision_label}", $translation->label());
$this->assertEquals("bundle field - {$entity->id()} - ro{$revision_label}", $translation->new_bundle_field->value);
$this->assertEquals("shared table - {$revision->id()} - value 1 - ro{$revision_label}", $translation->test_multiple_properties->value1);
$this->assertEquals("shared table - {$revision->id()} - value 2 - ro{$revision_label}", $translation->test_multiple_properties->value2);
$this->assertEquals("dedicated table - {$translation->id()} - delta 0 - value 1 - ro{$revision_label}", $translation->test_multiple_properties_multiple_values[0]->value1);
......@@ -457,6 +470,8 @@ protected function assertEntityTypeSchema($revisionable, $translatable, $new_bas
else {
$this->assertNonRevisionableAndNonTranslatable();
}
$this->assertBundleFieldSchema($revisionable);
}
/**
......@@ -589,6 +604,26 @@ protected function assertNonRevisionableAndNonTranslatable() {
$this->assertFalse($database_schema->tableExists($entity_type->getRevisionDataTable()));
}
/**
* Asserts that the bundle field schema is correct.
*
* @param bool $revisionable
* Whether the entity type is revisionable or not.
*/
protected function assertBundleFieldSchema($revisionable) {
$entity_type_id = 'entity_test_update';
$field_storage_definition = $this->entityFieldManager->getFieldStorageDefinitions($entity_type_id)['new_bundle_field'];
$database_schema = $this->database->schema();
/** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
$table_mapping = $this->entityTypeManager
->getStorage($entity_type_id)
->getTableMapping();
$this->assertTrue($database_schema->tableExists($table_mapping->getDedicatedDataTableName($field_storage_definition)));
if ($revisionable) {
$this->assertTrue($database_schema->tableExists($table_mapping->getDedicatedRevisionTableName($field_storage_definition)));
}
}
/**
* Asserts that the backup tables have been kept after a successful update.
*/
......@@ -617,7 +652,7 @@ public function testFieldableEntityTypeUpdatesErrorHandling() {
$this->insertData(FALSE, TRUE);
$tables = $schema->findTables('old_%');
$this->assertCount(3, $tables);
$this->assertCount(4, $tables);
foreach ($tables as $table) {
$schema->dropTable($table);
}
......@@ -783,7 +818,7 @@ public function testFieldableEntityTypeUpdatesRemoveBackupTables() {
// Check that backup tables are kept by default.
$tables = $schema->findTables('old_%');
$this->assertCount(3, $tables);
$this->assertCount(4, $tables);
foreach ($tables as $table) {
$schema->dropTable($table);
}
......
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