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
- Add a second language to the site.
- Install the Content Translation module.
- Enable content translation for Basic Page.
- Install a module that adds a base field to the node entity type. (Sample hook below, full sample module attached.)
- Create a basic page.
- Add a translation of the page in the second language.
- Edit the page, check the "My field" checkbox, and save the page.
- 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.)
Comment | File | Size | Author |
---|---|---|---|
#29 | 3039991-29-8.9.x.patch | 13.04 KB | amateescu |
#29 | 3039991-29-9.1.x.patch | 13.05 KB | amateescu |
#24 | interdiff-24.txt | 5.31 KB | amateescu |
#24 | 3039991-24.patch | 12.89 KB | amateescu |
#19 | 3039991-19.patch | 12.74 KB | amateescu |
Comments
Comment #2
lpeabody CreditAttribution: lpeabody commentedThis 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.
Comment #3
alexpottThanks 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:
Comment #4
lpeabody CreditAttribution: lpeabody commentedAdds patch with failing test.
Comment #5
lpeabody CreditAttribution: lpeabody commentedComment #6
lpeabody CreditAttribution: lpeabody commentedI 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.
Comment #7
lpeabody CreditAttribution: lpeabody commentedComment #8
lpeabody CreditAttribution: lpeabody commentedComment #9
lpeabody CreditAttribution: lpeabody commentedComment #10
lpeabody CreditAttribution: lpeabody commentedComment #12
lpeabody CreditAttribution: lpeabody commentedComment #13
lpeabody CreditAttribution: lpeabody commentedComment #14
lpeabody CreditAttribution: lpeabody commentedBump 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.
Comment #15
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedLooking 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 :)Comment #17
robotjox CreditAttribution: robotjox commented#12 fixed the issue for me (on 8.8) - thanks!
Comment #18
catchBumping to critical and marking #3043650: Exception when uninstalling CT when there are existing translations as duplicate.
Comment #19
amateescu CreditAttribution: amateescu as a volunteer commentedI 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 refactortestBaseFieldDeleteWithExistingData()
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.Comment #20
amateescu CreditAttribution: amateescu as a volunteer commentedComment #21
amateescu CreditAttribution: amateescu as a volunteer commentedComment #23
plachLooks good to me!
We should use
$this->entityType->isTranslatable()
here: entity types can be language-aware (and thus have alangcode
key) without necessarily being translatable.It seems that
$create_entity_translation
and$base_field_translatable
have always an equal value. Do we really need both?Can we move this into the
if ($create_entity_translation) {
block?Is this useful to check that the revision table is actually joined when it needs to?
It seems these cases are the same, aside from the entity type/schema. Maybe we should clarify it in the test case names?
Comment #24
amateescu CreditAttribution: amateescu as a volunteer commentedThanks 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 :)
Comment #26
nightlife2008 CreditAttribution: nightlife2008 commentedHey 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!
Comment #27
lpeabody CreditAttribution: lpeabody commentedPatch in #24 addressed the error I had with uninstalling modules with base field definitions in multilingual sites.
Comment #28
alexpottWe need a patch for 9.1.x before we can backport this to the other branches. Other than that this fix looks good.
Comment #29
amateescu CreditAttribution: amateescu as a volunteer commentedRerolled 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.
Comment #30
alexpottCommitted and pushed d97a2dc030 to 9.1.x and 31f278d2fb to 9.0.x. Thanks!
Committed 7727f4f and pushed to 8.9.x. Thanks!