diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php index 910e870..7d87a32 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php @@ -1669,16 +1669,7 @@ public function countFieldData($storage_definition, $as_bool = FALSE) { elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) { // Ascertain the table this field is mapped too. $field_name = $storage_definition->getName(); - try { - $table_name = $table_mapping->getFieldTableName($field_name); - } - catch (SqlContentEntityStorageException $e) { - // This may happen when changing field storage schema, since we are not - // able to use a table mapping matching the passed storage definition. - // @todo Revisit this once we are able to instantiate the table mapping - // properly. See https://www.drupal.org/node/2274017. - $table_name = $this->dataTable ?: $this->baseTable; - } + $table_name = $table_mapping->getFieldTableName($field_name); $query = $this->database->select($table_name, 't'); $or = $query->orConditionGroup(); foreach (array_keys($storage_definition->getColumns()) as $property_name) { diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php index a9530b7..f854550 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php @@ -3,7 +3,6 @@ namespace Drupal\Core\Entity\Sql; use Drupal\Core\Database\Connection; -use Drupal\Core\Database\DatabaseExceptionWrapper; use Drupal\Core\DependencyInjection\DependencySerializationTrait; use Drupal\Core\Entity\ContentEntityTypeInterface; use Drupal\Core\Entity\EntityManagerInterface; @@ -441,28 +440,9 @@ public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $ * {@inheritdoc} */ public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) { - try { - $has_data = $this->storage->countFieldData($storage_definition, TRUE); - - // @todo Add support for deleting bundle fields. Bundle field definitions - // are also using the \Drupal\Core\Field\BaseFieldDefinition class, but - // their isBaseField() method returns FALSE. - // @see https://www.drupal.org/node/2907780 - if ($storage_definition instanceof BaseFieldDefinition && !$storage_definition->isBaseField() && $has_data) { - throw new FieldStorageDefinitionUpdateForbiddenException('Unable to delete a field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data that cannot be purged.'); - } - } - catch (DatabaseExceptionWrapper $e) { - // This may happen when changing field storage schema, since we are not - // able to use a table mapping matching the passed storage definition. - // @todo Revisit this once we are able to instantiate the table mapping - // properly. See https://www.drupal.org/node/2274017. - return; - } - // If the field storage does not have any data, we can safely delete its // schema. - if (!$has_data) { + if (!$this->storage->countFieldData($storage_definition, TRUE)) { $this->performFieldSchemaOperation('delete', $storage_definition); return; } diff --git a/core/lib/Drupal/Core/Field/FieldStorageDefinitionListener.php b/core/lib/Drupal/Core/Field/FieldStorageDefinitionListener.php index 535f525..e67629b 100644 --- a/core/lib/Drupal/Core/Field/FieldStorageDefinitionListener.php +++ b/core/lib/Drupal/Core/Field/FieldStorageDefinitionListener.php @@ -153,14 +153,8 @@ public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $ // Keep the field definition in the deleted fields repository so we can use // it later during field_purge_batch(), but only if the field has data. - // @todo The check on BaseFieldDefinition is only needed because - // FieldStorageDefinitionInterface does not have a setDeleted() method. - // Figure out if we need to introduce it there or not. - // @todo Remove the isBaseField() check when we support deleting bundle - // fields. - // @see https://www.drupal.org/node/2907780 try { - if ($storage_definition instanceof BaseFieldDefinition && $storage_definition->isBaseField() && $storage instanceof FieldableEntityStorageInterface && $storage->countFieldData($storage_definition, TRUE)) { + if ($storage_definition instanceof BaseFieldDefinition && $storage instanceof FieldableEntityStorageInterface && $storage->countFieldData($storage_definition, TRUE)) { $deleted_storage_definition = clone $storage_definition; $deleted_storage_definition->setDeleted(TRUE); $this->deletedFieldsRepository->addFieldDefinition($deleted_storage_definition); diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php index 2dd2ddf..9dc8bb6 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php @@ -404,7 +404,7 @@ public function testBaseFieldDeleteWithExistingData() { $storage_definition = $this->entityManager->getLastInstalledFieldStorageDefinitions('entity_test_update')['new_base_field']; // Save an entity with the base field populated. - $entity = $this->entityManager->getStorage('entity_test_update')->create(['new_base_field' => 'foo']); + $entity = $storage->create(['new_base_field' => 'foo']); $entity->save(); // Remove the base field and apply updates. @@ -416,12 +416,12 @@ public function testBaseFieldDeleteWithExistingData() { // Check that a dedicated 'deleted' table was created for the deleted base // field. - $dedicated_table_name = $table_mapping->getDedicatedDataTableName($storage_definition, TRUE); - $this->assertTrue($schema_handler->tableExists($dedicated_table_name), 'A dedicated table was created for the deleted new_base_field.'); + $dedicated_deleted_table_name = $table_mapping->getDedicatedDataTableName($storage_definition, TRUE); + $this->assertTrue($schema_handler->tableExists($dedicated_deleted_table_name), 'A dedicated table was created for the deleted new_base_field.'); // Check that the deleted field's data is preserved in the dedicated // 'deleted' table. - $result = $this->database->select($dedicated_table_name, 't') + $result = $this->database->select($dedicated_deleted_table_name, 't') ->fields('t') ->execute() ->fetchAll(); @@ -438,42 +438,90 @@ public function testBaseFieldDeleteWithExistingData() { ]; $this->assertSame($expected, (array) $result[0]); - // Check that the field storage is marked for purging. - $deleted_storages = \Drupal::service('field.deleted_fields_repository')->getFieldStorages(); + // Check that the field storage definition is marked for purging. + $deleted_storages = \Drupal::service('field.deleted_fields_repository')->getFieldStorageDefinitions(); $this->assertArrayHasKey($storage_definition->getUniqueStorageIdentifier(), $deleted_storages, 'The base field is marked for purging.'); // Purge field data, and check that the storage definition has been // completely removed once the data is purged. field_purge_batch(10); - $deleted_storages = \Drupal::service('field.deleted_fields_repository')->getFieldStorages(); + $deleted_storages = \Drupal::service('field.deleted_fields_repository')->getFieldStorageDefinitions(); $this->assertEmpty($deleted_storages, 'The base field has been deleted.'); - $this->assertFalse($schema_handler->tableExists($dedicated_table_name), 'A dedicated table was created for the deleted new_base_field.'); + $this->assertFalse($schema_handler->tableExists($dedicated_deleted_table_name), 'A dedicated table was created for the deleted new_base_field.'); } /** * Tests deleting a bundle field when it has existing data. */ public function testBundleFieldDeleteWithExistingData() { + /** @var \Drupal\Core\Entity\Sql\SqlEntityStorageInterface $storage */ + $storage = $this->entityManager->getStorage('entity_test_update'); + $schema_handler = $this->database->schema(); + // Add the bundle field and run the update. $this->addBundleField(); $this->entityDefinitionUpdateManager->applyUpdates(); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $storage->getTableMapping(); + $storage_definition = $this->entityManager->getLastInstalledFieldStorageDefinitions('entity_test_update')['new_bundle_field']; + + // Check that the bundle field has a dedicated table. + $dedicated_table_name = $table_mapping->getDedicatedDataTableName($storage_definition); + $this->assertTrue($schema_handler->tableExists($dedicated_table_name), 'The bundle field uses a dedicated table.'); + + // Save an entity with the bundle field populated. entity_test_create_bundle('custom'); - $this->entityManager->getStorage('entity_test_update')->create(['type' => 'test_bundle', 'new_bundle_field' => 'foo'])->save(); + $entity = $storage->create(['type' => 'test_bundle', 'new_bundle_field' => 'foo']); + $entity->save(); - // Remove the bundle field and apply updates. It's expected to throw an - // exception. - // @todo Revisit that expectation once purging is implemented for - // bundle base fields: https://www.drupal.org/node/2907780. + // Remove the bundle field and apply updates. $this->removeBundleField(); - try { - $this->entityDefinitionUpdateManager->applyUpdates(); - $this->fail('FieldStorageDefinitionUpdateForbiddenException thrown when trying to apply an update that deletes a non-purgeable field with data.'); - } - catch (FieldStorageDefinitionUpdateForbiddenException $e) { - $this->pass('FieldStorageDefinitionUpdateForbiddenException thrown when trying to apply an update that deletes a non-purgeable field with data.'); - } + $this->entityDefinitionUpdateManager->applyUpdates(); + + // Check that the table of the bundle field has been renamed to use a + // 'deleted' table name. + $this->assertFalse($schema_handler->tableExists($dedicated_table_name), 'The dedicated table of the bundle field no longer exists.'); + + $dedicated_deleted_table_name = $table_mapping->getDedicatedDataTableName($storage_definition, TRUE); + $this->assertTrue($schema_handler->tableExists($dedicated_deleted_table_name), 'The dedicated table of the bundle fields has been renamed to use the "deleted" name.'); + + // Check that the deleted field's data is preserved in the dedicated + // 'deleted' table. + $result = $this->database->select($dedicated_deleted_table_name, 't') + ->fields('t') + ->execute() + ->fetchAll(); + $this->assertCount(1, $result); + + $expected = [ + 'bundle' => $entity->bundle(), + 'deleted' => '1', + 'entity_id' => $entity->id(), + 'revision_id' => $entity->id(), + 'langcode' => $entity->language()->getId(), + 'delta' => '0', + 'new_bundle_field_value' => $entity->new_bundle_field->value, + ]; + $this->assertSame($expected, (array) $result[0]); + + // Check that the field definition is marked for purging. + $deleted_fields = \Drupal::service('field.deleted_fields_repository')->getFieldDefinitions(); + $this->assertArrayHasKey($storage_definition->getUniqueIdentifier(), $deleted_fields, 'The bundle field is marked for purging.'); + + // Check that the field storage definition is marked for purging. + $deleted_storages = \Drupal::service('field.deleted_fields_repository')->getFieldStorageDefinitions(); + $this->assertArrayHasKey($storage_definition->getUniqueStorageIdentifier(), $deleted_storages, 'The bundle field storage is marked for purging.'); + + // Purge field data, and check that the storage definition has been + // completely removed once the data is purged. + field_purge_batch(10); + $deleted_fields = \Drupal::service('field.deleted_fields_repository')->getFieldDefinitions(); + $this->assertEmpty($deleted_fields, 'The bundle field has been deleted.'); + $deleted_storages = \Drupal::service('field.deleted_fields_repository')->getFieldStorageDefinitions(); + $this->assertEmpty($deleted_storages, 'The bundle field storage has been deleted.'); + $this->assertFalse($schema_handler->tableExists($dedicated_deleted_table_name), 'A dedicated table was created for the deleted new_base_field.'); } /**