Problem/Motivation

When uninstalling a module that adds a custom base field definition to a entity that has been translated into more than one language, the process crashes and the module is unable to be uninstalled:

Drupal\Core\Database\IntegrityConstraintViolationException: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '3-21-1-0-es'; for key 'PRIMARY': INSERT INTO {field_deleted_revision_fd812c0eda} (bundle, entity_id, revision_id, langcode, my_field_value, deleted, delta) SELECT base_table.type AS bundle, entity_table.nid AS entity_id, entity_table.vid AS revision_id, entity_table.langcode AS langcode, entity_table.my_field AS my_field_value, :deleted AS deleted, :delta AS delta
FROM
{node_field_data} entity_table
INNER JOIN {node_field_data} base_table ON entity_table.nid = base_table.nid
WHERE entity_table.my_field IS NOT NULL FOR UPDATE;

Steps to reproduce

  1. Add a second language to the site.
  2. Install the Content Translation module.
  3. Enable content translation for Basic Page.
  4. Install a module that adds a base field to the node entity type. (Sample hook below, full sample module attached.)
  5. Create a basic page.
  6. Add a translation of the page in the second language.
  7. Edit the page, check the "My field" checkbox, and save the page.
  8. Uninstall the module that added the base field definition.

Expected result: Module is uninstalled, and the my_field column is dropped from the node_field_data table.
Actual result: Error message above.

Sample module:

use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;

/**
 * Implements hook_entity_base_field_info().
 */
function mymodule_entity_base_field_info(EntityTypeInterface $entity_type) {
  if ($entity_type->id() == 'node') {
    $fields['my_field'] = BaseFieldDefinition::create('boolean')
      ->setLabel(t('My field'))
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayOptions('form', [
        'type' => 'boolean_checkbox',
        'weight' => 100,
      ]);
    return $fields;
  }
}

Proposed resolution

The error appears to be caused by a query in which the node_field_data is JOINed on itself by id, but not langcode, causing duplicate rows. I don't fully understand the purpose of the JOIN, but my workaround is to add the langcode as a JOIN condition. (Patch attached.)

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

bgreco created an issue. See original summary.

lpeabody’s picture

Status: Active » Reviewed & tested by the community

This is happening to me and it's resulting in content_moderation not being removable. After applying the patch I was able to uninstall content_moderation.

alexpott’s picture

Version: 8.6.x-dev » 8.7.x-dev
Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs tests

Thanks for filing this bug report. Bug fixing is very valuable. However in order in order to commit a bug fix we need an automated to test to prove that we've fixed the bug and ensure that we don't break it again in the future. For more information about writing tests in Drupal 8 see the following links:

  1. https://www.drupal.org/docs/8/testing
  2. https://api.drupal.org/api/drupal/core%21core.api.php/group/testing/8.7.x
lpeabody’s picture

lpeabody’s picture

Status: Needs work » Needs review
lpeabody’s picture

I don't know why these tests are passing. As far as I can tell the test runner isn't even running the test I added in the patch. Not sure what's going on, and I'm out of ideas. Test definitely fails for me locally though.

lpeabody’s picture

Status: Needs review » Needs work
lpeabody’s picture

lpeabody’s picture

Status: Needs review » Needs work
lpeabody’s picture

Status: Needs review » Needs work
lpeabody’s picture

lpeabody’s picture

Issue tags: -Needs tests
lpeabody’s picture

Bump on this? Seems like something we'd like to get in and failing/passing tests are created. If there's anything else needed to get this one committed let me know.

amateescu’s picture

Status: Needs review » Needs work

Looking at the query generated by \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::getSelectQueryForFieldStorageDeletion() from the issue summary, the problem seems to be that we're trying to populate the (deleted) revision field table with records from the data table, so that's what we need to fix :)

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.9 was released on November 6 and is the final full bugfix release for the Drupal 8.7.x series. Drupal 8.7.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.8.0 on December 4, 2019. (Drupal 8.8.0-beta1 is available for testing.)

Bug reports should be targeted against the 8.8.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.9.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

robotjox’s picture

#12 fixed the issue for me (on 8.8) - thanks!

catch’s picture

Priority: Normal » Critical
amateescu’s picture

Status: Needs work » Needs review
FileSize
11.28 KB
12.74 KB

I looked a bit into this and it turns out there are two problems there:

1) the JOIN needs a langcode condition, as correctly discovered by @bgreco in the issue summary
2) the JOIN shouldn't exist when the two table names are the same (as mentioned in #15), i.e. when the entity type is revisionable but the field storage is non-revisionable => the field is stored only in the data table so we don't need to join it to retrieve the value for the bundle field column

Added two new test cases to \Drupal\KernelTests\Core\Entity\EntityDefinitionUpdateTest::baseFieldDeleteWithExistingDataTestCases() that cover both scenarios. I also had to refactor testBaseFieldDeleteWithExistingData() a bit to allow a field to be made translatable, but in the end I think that whole test method is a bit more clear and easier to follow now.

amateescu’s picture

Title: IntegrityConstraintViolationException on module uninstall » Base field purging is not handling translatable fields correctly

The last submitted patch, 19: 3039991-19-test-only.patch, failed testing. View results

plach’s picture

Status: Needs review » Needs work

Looks good to me!

  1. +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
    @@ -840,8 +840,18 @@ protected function getSelectQueryForFieldStorageDeletion($table_name, array $sha
    +        if ($langcode = $this->entityType->getKey('langcode')) {
    

    We should use $this->entityType->isTranslatable() here: entity types can be language-aware (and thus have a langcode key) without necessarily being translatable.

  2. +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php
    @@ -504,18 +505,22 @@ public function testBundleFieldCreateDeleteWithExistingEntities() {
    -  public function testBaseFieldDeleteWithExistingData($entity_type_id, $create_entity_revision, $base_field_revisionable) {
    +  public function testBaseFieldDeleteWithExistingData($entity_type_id, $create_entity_revision, $base_field_revisionable, $create_entity_translation, $base_field_translatable) {
    

    It seems that $create_entity_translation and $base_field_translatable have always an equal value. Do we really need both?

  3. +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php
    @@ -504,18 +505,22 @@ public function testBundleFieldCreateDeleteWithExistingEntities() {
    +    // Enable an additional language.
    +    ConfigurableLanguage::createFromLangcode('ro')->save();
    

    Can we move this into the if ($create_entity_translation) { block?

  4. +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php
    @@ -526,10 +531,22 @@ public function testBaseFieldDeleteWithExistingData($entity_type_id, $create_ent
    +      $entity->isDefaultRevision(FALSE);
    

    Is this useful to check that the revision table is actually joined when it needs to?

  5. +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php
    @@ -626,45 +659,75 @@ public function testBaseFieldDeleteWithExistingData($entity_type_id, $create_ent
    -      'Revisionable entity type, non revisionable base field' => [
    +      'Revisionable, non-translatable entity type, non revisionable base field' => [
             'entity_test_mulrev',
             TRUE,
             FALSE,
    +        FALSE,
    +        FALSE,
           ],
    -      'Revisionable entity type, revisionable base field' => [
    +      'Revisionable, non-translatable entity type, revisionable base field' => [
             'entity_test_mulrev',
             TRUE,
             TRUE,
    +        FALSE,
    +        FALSE,
           ],
           'Non-translatable revisionable entity type, revisionable base field' => [
             'entity_test_rev',
             TRUE,
             TRUE,
    +        FALSE,
    +        FALSE,
           ],
           'Non-translatable revisionable entity type, non-revisionable base field' => [
             'entity_test_rev',
             TRUE,
             FALSE,
    +        FALSE,
    +        FALSE,
    +      ],
    

    It seems these cases are the same, aside from the entity type/schema. Maybe we should clarify it in the test case names?

amateescu’s picture

Thanks for the review :)

Re #23:

1. Done!
2. We don't really need both, I wanted to be consistent with the other two arguments, but we can always add another one if we find the need for it, so removed the second one.
3. We can't :( This needs to be done before the first entity save, which happens just above the if ($create_entity_translation) { block.
4. Nope.. I did that so I can keep the order of the assertions consistent, i.e. to always have the values for the first revision in the data tables. If we would create a new default revision, the assertions below in that method would have to check the values for either the first or second revision, depending whether the test is creating a new revision or not.
5. Good point, a couple of test case names were even wrong, so I updated all of them... I hope they're clear enough now :)

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.7 was released on June 3, 2020 and is the final full bugfix release for the Drupal 8.8.x series. Drupal 8.8.x will not receive any further development aside from security fixes. Sites should prepare to update to Drupal 8.9.0 or Drupal 9.0.0 for ongoing support.

Bug reports should be targeted against the 8.9.x-dev branch from now on, and new development or disruptive changes should be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

nightlife2008’s picture

Hey everyone,

I just confirmed this patch works on an 8.9.x install, where I had to uninstall "search_api_exclude" module, which defines a basefield.

Well done!

lpeabody’s picture

Status: Needs review » Reviewed & tested by the community

Patch in #24 addressed the error I had with uninstalling modules with base field definitions in multilingual sites.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs reroll

We need a patch for 9.1.x before we can backport this to the other branches. Other than that this fix looks good.

amateescu’s picture

Status: Needs work » Reviewed & tested by the community
Issue tags: -Needs reroll
FileSize
13.05 KB
13.04 KB

Rerolled the patch for 9.1.x. #24 still applies on 8.9.x, but I'm re-uploading it anyway to have them both in the same comment.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed and pushed d97a2dc030 to 9.1.x and 31f278d2fb to 9.0.x. Thanks!
Committed 7727f4f and pushed to 8.9.x. Thanks!

  • alexpott committed d97a2dc on 9.1.x
    Issue #3039991 by amateescu, lpeabody, bgreco, plach: Base field purging...

  • alexpott committed 31f278d on 9.0.x
    Issue #3039991 by amateescu, lpeabody, bgreco, plach: Base field purging...

  • alexpott committed 7727f4f on 8.9.x
    Issue #3039991 by amateescu, lpeabody, bgreco, plach: Base field purging...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.