Problem/Motivation
Converting a content entity type that has data from non-translatable to translatable with EntityDefinitionUpdateManager::updateFieldableEntityType() fails if the original entity type has no langcode entity key. The data copy step fails with:
Drupal\Core\Entity\EntityStorageException: The entity update process failed while processing the entity type ..., ID: 1, revision ID: 1. Caused by: SQLSTATE[42S22]: Column not found: 1054 Unknown column '..._revision.langcode' in 'SELECT'
The cause is SqlContentEntityStorage::initTableLayout(). It resets the table mapping, the revision key and all table names before it re-reads them from the entity type, but it does not reset $langcodeKey and $defaultLangcodeKey. It only sets those two properties when the entity type is translatable:
protected function initTableLayout() {
$this->tableMapping = NULL;
$this->revisionKey = NULL;
$this->revisionTable = NULL;
$this->dataTable = NULL;
$this->revisionDataTable = NULL;
// $this->langcodeKey and $this->defaultLangcodeKey are not reset.
...
if ($translatable) {
$this->dataTable = $table_mapping->getDataTable();
$this->langcodeKey = $this->entityType->getKey('langcode');
$this->defaultLangcodeKey = $this->entityType->getKey('default_langcode');
}
...
}So when setEntityType() switches a storage from a translatable definition to a non-translatable one, the old language keys remain. This happens during the conversion:
SqlContentEntityStorageSchema::preUpdateEntityTypeSchema()calls$this->storage->setEntityType($entity_type)with the new, translatable definition. This sets$langcodeKey = 'langcode'.- It then calls
$this->storage->setEntityType($original)so the existing data can be loaded with the original definition.$langcodeKeyis still'langcode'. SqlFieldableEntityTypeListenerTrait::copyData()loads the existing entities through that storage.loadFromDedicatedTables()adds[base_table].[langcode]to the query and joins dedicated field tables onlangcode. The original tables have no such column.
The existing test coverage (FieldableEntityDefinitionUpdateTest) misses this because entity_test_update always has a langcode entity key, so the column exists even in its non-translatable state.
The stale state only exists in the storage instance that ran preUpdateEntityTypeSchema(), i.e. in the first batch call. On update.php, later batch requests create a fresh storage and are not affected. Still, the first call always fails, so the conversion can't be done.
A contrib module ran into this: #3635868: Mismatched entity and/or field definitions in the Advertisement module. The fix there has to work around it: before converting, it installs langcode as a regular field with an initial value.
Verified on Drupal 11.4.11.
Steps to reproduce
- Install a revisionable, non-translatable content entity type without a
langcodeentity key. - Create at least one entity.
- Change the code definition to be translatable: add the
langcodekey and adata_table/revision_data_table. - Run the conversion, e.g. in a
hook_update_N():
$entity_type = \Drupal::entityTypeManager()->getDefinition('my_entity'); $field_storage_definitions = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions('my_entity'); \Drupal::entityDefinitionUpdateManager()->updateFieldableEntityType($entity_type, $field_storage_definitions, $sandbox); - The update fails with "Unknown column '..langcode'". Even when the query happens to work (e.g. the column exists), the stale
$defaultLangcodeKeycan cause values to be mapped to the wrong translation key.
Proposed resolution
Reset both language keys in SqlContentEntityStorage::initTableLayout(), like the other layout properties:
$this->tableMapping = NULL;
$this->revisionKey = NULL;
$this->revisionTable = NULL;
$this->dataTable = NULL;
$this->revisionDataTable = NULL;
$this->langcodeKey = FALSE;
$this->defaultLangcodeKey = FALSE;Add test coverage for converting an entity type with data that has no langcode key to translatable. This could be a variant of FieldableEntityDefinitionUpdateTest, or an entity_test_update state that removes the langcode key from the initial definition. The test should include a single-cardinality configurable field, so the dedicated-table join is exercised as well.
Remaining tasks
- Write a failing test.
- Apply the fix.
- Review.
User interface changes
None.
API changes
None. setEntityType() now resets the language keys correctly when it switches to a non-translatable definition.
Data model changes
None.
Release notes snippet
Not needed.
AI disclaimer
Drafted and found using AI
| Comment | File | Size | Author |
|---|
Issue fork drupal-3635920
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
anybodyComment #3
anybodyComment #5
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #6
anybodyComment #7
anybodyLocally this works, let's see it things go green now :)
Comment #8
anybodyOkay ready for review. This is going a bit deeper, but I hope core maintainers see the issue, if I'm not mistaken.
Please review carefully, I'm not totally sure my assumptions are correct, as I might miss something from the big picture!