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:

  1. SqlContentEntityStorageSchema::preUpdateEntityTypeSchema() calls $this->storage->setEntityType($entity_type) with the new, translatable definition. This sets $langcodeKey = 'langcode'.
  2. It then calls $this->storage->setEntityType($original) so the existing data can be loaded with the original definition. $langcodeKey is still 'langcode'.
  3. SqlFieldableEntityTypeListenerTrait::copyData() loads the existing entities through that storage. loadFromDedicatedTables() adds [base_table].[langcode] to the query and joins dedicated field tables on langcode. 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

  1. Install a revisionable, non-translatable content entity type without a langcode entity key.
  2. Create at least one entity.
  3. Change the code definition to be translatable: add the langcode key and a data_table / revision_data_table.
  4. 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);
  5. The update fails with "Unknown column '..langcode'". Even when the query happens to work (e.g. the column exists), the stale $defaultLangcodeKey can 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

Issue fork drupal-3635920

Command icon 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

anybody created an issue. See original summary.

anybody’s picture

Issue summary: View changes
anybody’s picture

Assigned: Unassigned » anybody
Status: Active » Needs review

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new2.59 KB

The 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.

anybody’s picture

Assigned: anybody » Unassigned
Status: Needs work » Needs review
anybody’s picture

Locally this works, let's see it things go green now :)

anybody’s picture

Okay 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!