From ba95bbb26a2ef6f2ec186e4066353a507093dd9e Mon Sep 17 00:00:00 2001 From: effulgentsia Date: Wed, 26 Aug 2015 22:28:17 -0700 Subject: [PATCH] =?UTF-8?q?Issue=20#2542748=20by=20plach,=20effulgentsia,?= =?UTF-8?q?=20jhedstrom,=20G=C3=A1bor=20Hojtsy,=20alexpott,=20mpdonadio,?= =?UTF-8?q?=20catch,=20dawehner:=20Automatic=20entity=20updates=20can=20fa?= =?UTF-8?q?il=20when=20there=20is=20existing=20content,=20leaving=20the=20?= =?UTF-8?q?site's=20schema=20in=20an=20unpredictable=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/includes/install.core.inc | 22 +- core/includes/update.inc | 20 -- core/lib/Drupal.php | 10 + .../Entity/EntityDefinitionUpdateManager.php | 95 +++++--- ...EntityDefinitionUpdateManagerInterface.php | 140 +++++++----- .../Core/Entity/Sql/DefaultTableMapping.php | 7 +- .../Entity/Sql/SqlContentEntityStorage.php | 48 ++++- .../Sql/SqlContentEntityStorageSchema.php | 61 +++++- .../Drupal/Core/Extension/ModuleInstaller.php | 50 ++++- core/lib/Drupal/Core/Extension/module.api.php | 21 +- .../block_content/block_content.install | 30 +++ .../src/ContentTranslationUpdatesManager.php | 2 +- core/modules/node/node.install | 74 ++++++- .../src/Controller/DbUpdateController.php | 41 +--- .../Entity/EntityDefinitionTestTrait.php | 11 + .../Entity/EntityDefinitionUpdateTest.php | 137 ++++++++++-- ...SqlContentEntityStorageSchemaIndexTest.php | 38 +--- .../UpdateApiEntityDefinitionUpdateTest.php | 204 ++++++++++++++++++ .../src/Tests/Update/DbUpdatesTrait.php | 57 +++++ .../src/Tests/Update/UpdatePathTestBase.php | 3 + core/modules/system/system.install | 29 ++- .../modules/entity_test/entity_test.install | 5 + .../modules/entity_test/entity_test.module | 16 +- .../update/entity_definition_updates_8001.inc | 43 ++++ .../update/entity_definition_updates_8002.inc | 41 ++++ .../entity_test/update/status_report_8001.inc | 13 ++ .../entity_test/update/status_report_8002.inc | 15 ++ .../minimal/src/Tests/MinimalTest.php | 3 + .../standard/src/Tests/StandardTest.php | 3 + 29 files changed, 1013 insertions(+), 226 deletions(-) create mode 100644 core/modules/block_content/block_content.install create mode 100644 core/modules/system/src/Tests/Entity/Update/UpdateApiEntityDefinitionUpdateTest.php create mode 100644 core/modules/system/src/Tests/Update/DbUpdatesTrait.php create mode 100644 core/modules/system/tests/modules/entity_test/update/entity_definition_updates_8001.inc create mode 100644 core/modules/system/tests/modules/entity_test/update/entity_definition_updates_8002.inc create mode 100644 core/modules/system/tests/modules/entity_test/update/status_report_8001.inc create mode 100644 core/modules/system/tests/modules/entity_test/update/status_report_8002.inc diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 3b4c4abe75..7bf9eabf30 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -1599,6 +1599,10 @@ function install_bootstrap_full() { * The batch definition. */ function install_profile_modules(&$install_state) { + // We need to manually trigger the installation of core-provided entity types, + // as those will not be handled by the module installer. + install_core_entity_type_definitions(); + $modules = \Drupal::state()->get('install_profile_modules') ?: array(); $files = system_rebuild_module_data(); \Drupal::state()->delete('install_profile_modules'); @@ -1638,6 +1642,18 @@ function install_profile_modules(&$install_state) { return $batch; } +/** + * Installs entity type definitions provided by core. + */ +function install_core_entity_type_definitions() { + $update_manager = \Drupal::entityDefinitionUpdateManager(); + foreach (\Drupal::entityManager()->getDefinitions() as $entity_type) { + if ($entity_type->getProvider() == 'core') { + $update_manager->installEntityType($entity_type); + } + } +} + /** * Installs themes. * @@ -1665,12 +1681,6 @@ function install_profile_themes(&$install_state) { * An array of information about the current installation state. */ function install_install_profile(&$install_state) { - // Now that all modules are installed, make sure the entity storage and other - // handlers are up to date with the current entity and field definitions. For - // example, Path module adds a base field to nodes and taxonomy terms after - // those modules are already installed. - \Drupal::service('entity.definition_update_manager')->applyUpdates(); - \Drupal::service('module_installer')->install(array(drupal_get_profile()), FALSE); // Install all available optional config. During installation the module order // is determined by dependencies. If there are no dependencies between modules diff --git a/core/includes/update.inc b/core/includes/update.inc index 15f27a8f1f..70edeafbcd 100644 --- a/core/includes/update.inc +++ b/core/includes/update.inc @@ -218,26 +218,6 @@ function update_do_one($module, $number, $dependency_map, &$context) { $context['message'] = 'Updating ' . Html::escape($module) . ' module'; } -/** - * Performs entity definition updates, which can trigger schema updates. - * - * @param $context - * The batch context array. - */ -function update_entity_definitions(&$context) { - try { - \Drupal::service('entity.definition_update_manager')->applyUpdates(); - } - catch (EntityStorageException $e) { - watchdog_exception('update', $e); - $variables = Error::decodeException($e); - unset($variables['backtrace']); - $ret['#abort'] = array('success' => FALSE, 'query' => t('%type: @message in %function (line %line of %file).', $variables)); - $context['results']['core']['update_entity_definitions'] = $ret; - $context['results']['#abort'][] = 'update_entity_definitions'; - } -} - /** * Returns a list of all the pending database updates. * diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php index 7621a4831f..da3e02a1f3 100644 --- a/core/lib/Drupal.php +++ b/core/lib/Drupal.php @@ -687,4 +687,14 @@ public static function destination() { return static::getContainer()->get('redirect.destination'); } + /** + * Returns the entity definition update manager. + * + * @return \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface + * The entity definition update manager. + */ + public static function entityDefinitionUpdateManager() { + return static::getContainer()->get('entity.definition_update_manager'); + } + } diff --git a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php index 902aab9199..c943b95f92 100644 --- a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php +++ b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php @@ -9,6 +9,7 @@ use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface; use Drupal\Core\Entity\Schema\EntityStorageSchemaInterface; +use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; @@ -95,13 +96,13 @@ public function getChangeSummary() { * {@inheritdoc} */ public function applyUpdates() { - $change_list = $this->getChangeList(); - if ($change_list) { + $complete_change_list = $this->getChangeList(); + if ($complete_change_list) { // self::getChangeList() only disables the cache and does not invalidate. // In case there are changes, explicitly invalidate caches. $this->entityManager->clearCachedDefinitions(); } - foreach ($change_list as $entity_type_id => $change_list) { + foreach ($complete_change_list as $entity_type_id => $change_list) { // Process entity type definition changes before storage definitions ones // this is necessary when you change an entity type from non-revisionable // to revisionable and at the same time add revisionable fields to the @@ -127,42 +128,76 @@ public function applyUpdates() { /** * {@inheritdoc} */ - public function applyEntityUpdate($op, $entity_type_id, $reset_cached_definitions = TRUE) { - $change_list = $this->getChangeList(); - if (!isset($change_list[$entity_type_id]) || $change_list[$entity_type_id]['entity_type'] !== $op) { - return FALSE; - } - if ($reset_cached_definitions) { - // self::getChangeList() only disables the cache and does not invalidate. - // In case there are changes, explicitly invalidate caches. - $this->entityManager->clearCachedDefinitions(); - } - $this->doEntityUpdate($op, $entity_type_id); - return TRUE; + public function getEntityType($entity_type_id) { + $entity_type = $this->entityManager->getLastInstalledDefinition($entity_type_id); + return $entity_type ? clone $entity_type : NULL; } /** * {@inheritdoc} */ - public function applyFieldUpdate($op, $entity_type_id, $field_name, $reset_cached_definitions = TRUE) { - $change_list = $this->getChangeList(); - if (!isset($change_list[$entity_type_id]['field_storage_definitions']) || $change_list[$entity_type_id]['field_storage_definitions'][$field_name] !== $op) { - return FALSE; - } + public function installEntityType(EntityTypeInterface $entity_type) { + $this->entityManager->clearCachedDefinitions(); + $this->entityManager->onEntityTypeCreate($entity_type); + } - if ($reset_cached_definitions) { - // self::getChangeList() only disables the cache and does not invalidate. - // In case there are changes, explicitly invalidate caches. - $this->entityManager->clearCachedDefinitions(); + /** + * {@inheritdoc} + */ + public function updateEntityType(EntityTypeInterface $entity_type) { + $original = $this->getEntityType($entity_type->id()); + $this->entityManager->clearCachedDefinitions(); + $this->entityManager->onEntityTypeUpdate($entity_type, $original); + } + + /** + * {@inheritdoc} + */ + public function uninstallEntityType(EntityTypeInterface $entity_type) { + $this->entityManager->clearCachedDefinitions(); + $this->entityManager->onEntityTypeDelete($entity_type); + } + + /** + * {@inheritdoc} + */ + public function installFieldStorageDefinition($name, $entity_type_id, $provider, FieldStorageDefinitionInterface $storage_definition) { + // @todo Pass a mutable field definition interface when we have one. See + // https://www.drupal.org/node/2346329. + if ($storage_definition instanceof BaseFieldDefinition) { + $storage_definition + ->setName($name) + ->setTargetEntityTypeId($entity_type_id) + ->setProvider($provider) + ->setTargetBundle(NULL); } + $this->entityManager->clearCachedDefinitions(); + $this->entityManager->onFieldStorageDefinitionCreate($storage_definition); + } + + /** + * {@inheritdoc} + */ + public function getFieldStorageDefinition($name, $entity_type_id) { + $storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id); + return isset($storage_definitions[$name]) ? clone $storage_definitions[$name] : NULL; + } - $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id); - $original_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id); - $storage_definition = isset($storage_definitions[$field_name]) ? $storage_definitions[$field_name] : NULL; - $original_storage_definition = isset($original_storage_definitions[$field_name]) ? $original_storage_definitions[$field_name] : NULL; + /** + * {@inheritdoc} + */ + public function updateFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition) { + $original = $this->getFieldStorageDefinition($storage_definition->getName(), $storage_definition->getTargetEntityTypeId()); + $this->entityManager->clearCachedDefinitions(); + $this->entityManager->onFieldStorageDefinitionUpdate($storage_definition, $original); + } - $this->doFieldUpdate($op, $storage_definition, $original_storage_definition); - return TRUE; + /** + * {@inheritdoc} + */ + public function uninstallFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition) { + $this->entityManager->clearCachedDefinitions(); + $this->entityManager->onFieldStorageDefinitionDelete($storage_definition); } /** diff --git a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php index 5946eeed92..815060854f 100644 --- a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php @@ -7,6 +7,8 @@ namespace Drupal\Core\Entity; +use Drupal\Core\Field\FieldStorageDefinitionInterface; + /** * Defines an interface for managing entity definition updates. * @@ -25,12 +27,22 @@ * report the differences or when to apply each update. This interface is for * managing that. * + * This interface also provides methods to retrieve instances of the definitions + * to be updated ready to be manipulated. In fact when definitions change in + * code the system needs to be notified about that and the definitions stored in + * state need to be reconciled with the ones living in code. This typically + * happens in Update API functions, which need to take the system from a known + * state to another known state. Relying on the definitions living in code might + * prevent this, as the system might transition directly to the last available + * state, and thus skipping the intermediate steps. Manipulating the definitions + * in state allows to avoid this and ensures that the various steps of the + * update process are predictable and repeatable. + * * @see \Drupal\Core\Entity\EntityManagerInterface::getDefinition() * @see \Drupal\Core\Entity\EntityManagerInterface::getLastInstalledDefinition() * @see \Drupal\Core\Entity\EntityManagerInterface::getFieldStorageDefinitions() * @see \Drupal\Core\Entity\EntityManagerInterface::getLastInstalledFieldStorageDefinitions() - * @see \Drupal\Core\Entity\EntityTypeListenerInterface - * @see \Drupal\Core\Field\FieldStorageDefinitionListenerInterface + * @see hook_update_N() */ interface EntityDefinitionUpdateManagerInterface { @@ -75,6 +87,9 @@ public function getChangeSummary(); /** * Applies all the detected valid changes. * + * Use this with care, as it will apply updates for any module, which will + * lead to unpredictable results. + * * @throws \Drupal\Core\Entity\EntityStorageException * This exception is thrown if a change cannot be applied without * unacceptable data loss. In such a case, the site administrator needs to @@ -84,67 +99,92 @@ public function getChangeSummary(); public function applyUpdates(); /** - * Performs a single entity definition update. + * Returns an entity type definition ready to be manipulated. * - * This method should be used from hook_update_N() functions to process - * entity definition updates as part of the update function. This is only - * necessary if the hook_update_N() implementation relies on the entity - * definition update. All remaining entity definition updates will be run - * automatically after the hook_update_N() implementations. + * When needing to apply updates to existing entity type definitions, this + * method should always be used to retrieve a definition ready to be + * manipulated. * - * @param string $op - * The operation to perform, either static::DEFINITION_CREATED or - * static::DEFINITION_UPDATED. * @param string $entity_type_id - * The entity type to update. - * @param bool $reset_cached_definitions - * (optional). Determines whether to clear the Entity Manager's cached - * definitions before applying the update. Defaults to TRUE. Can be used - * to prevent unnecessary cache invalidation when a hook_update_N() makes - * multiple calls to this method. + * The entity type identifier. * - * @return bool - * TRUE if the entity update is processed, FALSE if not. + * @return \Drupal\Core\Entity\EntityTypeInterface + * The entity type definition. + */ + public function getEntityType($entity_type_id); + + /** + * Installs a new entity type definition. * - * @throws \Drupal\Core\Entity\EntityStorageException - * This exception is thrown if a change cannot be applied without - * unacceptable data loss. In such a case, the site administrator needs to - * apply some other process, such as a custom update function or a - * migration via the Migrate module. + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type definition. + */ + public function installEntityType(EntityTypeInterface $entity_type); + + /** + * Applies any change performed to the passed entity type definition. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type definition. + */ + public function updateEntityType(EntityTypeInterface $entity_type); + + /** + * Uninstalls an entity type definition. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type definition. */ - public function applyEntityUpdate($op, $entity_type_id, $reset_cached_definitions = TRUE); + public function uninstallEntityType(EntityTypeInterface $entity_type); /** - * Performs a single field storage definition update. + * Returns a field storage definition ready to be manipulated. * - * This method should be used from hook_update_N() functions to process field - * storage definition updates as part of the update function. This is only - * necessary if the hook_update_N() implementation relies on the field storage - * definition update. All remaining field storage definition updates will be - * run automatically after the hook_update_N() implementations. + * When needing to apply updates to existing field storage definitions, this + * method should always be used to retrieve a storage definition ready to be + * manipulated. * - * @param string $op - * The operation to perform, possible values are static::DEFINITION_CREATED, - * static::DEFINITION_UPDATED or static::DEFINITION_DELETED. + * @param string $name + * The field name. * @param string $entity_type_id - * The entity type to update. - * @param string $field_name - * The field name to update. - * @param bool $reset_cached_definitions - * (optional). Determines whether to clear the Entity Manager's cached - * definitions before applying the update. Defaults to TRUE. Can be used - * to prevent unnecessary cache invalidation when a hook_update_N() makes - * multiple calls to this method. + * The entity type identifier. + * + * @return \Drupal\Core\Field\FieldStorageDefinitionInterface + * The field storage definition. + * + * @todo Make this return a mutable storage definition interface when we have + * one. See https://www.drupal.org/node/2346329. + */ + public function getFieldStorageDefinition($name, $entity_type_id); - * @return bool - * TRUE if the entity update is processed, FALSE if not. + /** + * Installs a new field storage definition. * - * @throws \Drupal\Core\Entity\EntityStorageException - * This exception is thrown if a change cannot be applied without - * unacceptable data loss. In such a case, the site administrator needs to - * apply some other process, such as a custom update function or a - * migration via the Migrate module. + * @param string $name + * The field storage definition name. + * @param string $entity_type_id + * The target entity type identifier. + * @param string $provider + * The name of the definition provider. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. + */ + public function installFieldStorageDefinition($name, $entity_type_id, $provider, FieldStorageDefinitionInterface $storage_definition); + + /** + * Applies any change performed to the passed field storage definition. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. + */ + public function updateFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition); + + /** + * Uninstalls a field storage definition. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. */ - public function applyFieldUpdate($op, $entity_type_id, $field_name, $reset_cached_definitions = TRUE); + public function uninstallFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition); } diff --git a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php index b701279118..472ed20994 100644 --- a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php +++ b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php @@ -189,9 +189,10 @@ public function getFieldTableName($field_name) { public function getColumnNames($field_name) { if (!isset($this->columnMapping[$field_name])) { $this->columnMapping[$field_name] = array(); - $storage_definition = $this->fieldStorageDefinitions[$field_name]; - foreach (array_keys($this->fieldStorageDefinitions[$field_name]->getColumns()) as $property_name) { - $this->columnMapping[$field_name][$property_name] = $this->getFieldColumnName($storage_definition, $property_name); + if (isset($this->fieldStorageDefinitions[$field_name])) { + foreach (array_keys($this->fieldStorageDefinitions[$field_name]->getColumns()) as $property_name) { + $this->columnMapping[$field_name][$property_name] = $this->getFieldColumnName($this->fieldStorageDefinitions[$field_name], $property_name); + } } } return $this->columnMapping[$field_name]; diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php index 689655af29..259d240d1b 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php @@ -10,6 +10,8 @@ use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Database\Connection; use Drupal\Core\Database\Database; +use Drupal\Core\Database\DatabaseExceptionWrapper; +use Drupal\Core\Database\SchemaException; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\ContentEntityStorageBase; use Drupal\Core\Entity\EntityBundleListenerInterface; @@ -1351,7 +1353,9 @@ public function requiresFieldDataMigration(FieldStorageDefinitionInterface $stor * {@inheritdoc} */ public function onEntityTypeCreate(EntityTypeInterface $entity_type) { - $this->getStorageSchema()->onEntityTypeCreate($entity_type); + $this->wrapSchemaException(function () use ($entity_type) { + $this->getStorageSchema()->onEntityTypeCreate($entity_type); + }); } /** @@ -1364,14 +1368,18 @@ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeI // definition. $this->initTableLayout(); // Let the schema handler adapt to possible table layout changes. - $this->getStorageSchema()->onEntityTypeUpdate($entity_type, $original); + $this->wrapSchemaException(function () use ($entity_type, $original) { + $this->getStorageSchema()->onEntityTypeUpdate($entity_type, $original); + }); } /** * {@inheritdoc} */ public function onEntityTypeDelete(EntityTypeInterface $entity_type) { - $this->getStorageSchema()->onEntityTypeDelete($entity_type); + $this->wrapSchemaException(function () use ($entity_type) { + $this->getStorageSchema()->onEntityTypeDelete($entity_type); + }); } /** @@ -1386,14 +1394,18 @@ public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $ if ($this->getTableMapping()->allowsSharedTableStorage($storage_definition)) { $this->tableMapping = NULL; } - $this->getStorageSchema()->onFieldStorageDefinitionCreate($storage_definition); + $this->wrapSchemaException(function () use ($storage_definition) { + $this->getStorageSchema()->onFieldStorageDefinitionCreate($storage_definition); + }); } /** * {@inheritdoc} */ public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { - $this->getStorageSchema()->onFieldStorageDefinitionUpdate($storage_definition, $original); + $this->wrapSchemaException(function () use ($storage_definition, $original) { + $this->getStorageSchema()->onFieldStorageDefinitionUpdate($storage_definition, $original); + }); } /** @@ -1421,7 +1433,31 @@ public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $ } // Update the field schema. - $this->getStorageSchema()->onFieldStorageDefinitionDelete($storage_definition); + $this->wrapSchemaException(function () use ($storage_definition) { + $this->getStorageSchema()->onFieldStorageDefinitionDelete($storage_definition); + }); + } + + /** + * Wraps a database schema exception into an entity storage exception. + * + * @param callable $callback + * The callback to be executed. + * + * @throws \Drupal\Core\Entity\EntityStorageException + * When a database schema exception is thrown. + */ + protected function wrapSchemaException(callable $callback) { + $message = 'Exception thrown while performing a schema update.'; + try { + $callback(); + } + catch (SchemaException $e) { + throw new EntityStorageException($message, 0, $e); + } + catch (DatabaseExceptionWrapper $e) { + throw new EntityStorageException($message, 0, $e); + } } /** diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php index b92636c635..1eaa0d1940 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php @@ -1391,6 +1391,20 @@ protected function updateSharedTableSchema(FieldStorageDefinitionInterface $stor if ($field_name == $updated_field_name) { $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names); + // Handle NOT NULL constraints. + foreach ($schema[$table_name]['fields'] as $column_name => $specifier) { + $not_null = !empty($specifier['not null']); + $original_not_null = !empty($original_schema[$table_name]['fields'][$column_name]['not null']); + if ($not_null !== $original_not_null) { + if ($not_null && $this->hasNullFieldPropertyData($table_name, $column_name)) { + throw new EntityStorageException("The $column_name column cannot have NOT NULL constraints as it holds NULL values."); + } + $column_schema = $original_schema[$table_name]['fields'][$column_name]; + $column_schema['not null'] = $not_null; + $schema_handler->changeField($table_name, $field_name, $field_name, $column_schema); + } + } + // Drop original indexes and unique keys. if (!empty($original_schema[$table_name]['indexes'])) { foreach ($original_schema[$table_name]['indexes'] as $name => $specifier) { @@ -1422,6 +1436,26 @@ protected function updateSharedTableSchema(FieldStorageDefinitionInterface $stor } } + /** + * Checks whether a field property has NULL values. + * + * @param string $table_name + * The name of the table to inspect. + * @param string $column_name + * The name of the column holding the field property data. + * + * @return bool + * TRUE if NULL data is found, FALSE otherwise. + */ + protected function hasNullFieldPropertyData($table_name, $column_name) { + $query = $this->database->select($table_name, 't') + ->fields('t', [$column_name]) + ->range(0, 1); + $query->isNull('t.' . $column_name); + $result = $query->execute()->fetchAssoc(); + return (bool) $result; + } + /** * Gets the schema for a single field definition. * @@ -1803,10 +1837,15 @@ protected function hasColumnChanges(FieldStorageDefinitionInterface $storage_def } if (!$storage_definition->hasCustomStorage()) { - $schema = $this->getSchemaFromStorageDefinition($storage_definition); - foreach ($this->loadFieldSchemaData($original) as $table => $spec) { - if ($spec['fields'] != $schema[$table]['fields']) { - return TRUE; + $keys = array_flip($this->getColumnSchemaRelevantKeys()); + $definition_schema = $this->getSchemaFromStorageDefinition($storage_definition); + foreach ($this->loadFieldSchemaData($original) as $table => $table_schema) { + foreach ($table_schema['fields'] as $name => $spec) { + $definition_spec = array_intersect_key($definition_schema[$table]['fields'][$name], $keys); + $stored_spec = array_intersect_key($spec, $keys); + if ($definition_spec != $stored_spec) { + return TRUE; + } } } } @@ -1814,4 +1853,18 @@ protected function hasColumnChanges(FieldStorageDefinitionInterface $storage_def return FALSE; } + /** + * Returns a list of column schema keys affecting data storage. + * + * When comparing schema definitions, only changes in certain properties + * actually affect how data is stored and thus, if applied, may imply data + * manipulation. + * + * @return string[] + * An array of key names. + */ + protected function getColumnSchemaRelevantKeys() { + return ['type', 'size', 'length', 'unsigned']; + } + } diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php index 41e698c1d0..a8124d5242 100644 --- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php +++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php @@ -10,9 +10,9 @@ use Drupal\Component\Serialization\Yaml; use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheBackendInterface; -use Drupal\Core\Config\PreExistingConfigException; -use Drupal\Core\Config\StorageInterface; use Drupal\Core\DrupalKernelInterface; +use Drupal\Core\Entity\EntityStorageException; +use Drupal\Core\Entity\FieldableEntityInterface; /** * Default implementation of the module installer. @@ -212,14 +212,34 @@ public function install(array $module_list, $enable_dependencies = TRUE) { $version = max(max($versions), $version); } - // Notify interested components that this module's entity types are new. - // For example, a SQL-based storage handler can use this as an - // opportunity to create the necessary database tables. + // Notify interested components that this module's entity types and + // field storage definitions are new. For example, a SQL-based storage + // handler can use this as an opportunity to create the necessary + // database tables. // @todo Clean this up in https://www.drupal.org/node/2350111. $entity_manager = \Drupal::entityManager(); + $update_manager = \Drupal::entityDefinitionUpdateManager(); foreach ($entity_manager->getDefinitions() as $entity_type) { if ($entity_type->getProvider() == $module) { - $entity_manager->onEntityTypeCreate($entity_type); + $update_manager->installEntityType($entity_type); + } + elseif ($entity_type->isSubclassOf(FieldableEntityInterface::CLASS)) { + // The module being installed may be adding new fields to existing + // entity types. Field definitions for any entity type defined by + // the module are handled in the if branch. + foreach ($entity_manager->getFieldStorageDefinitions($entity_type->id()) as $storage_definition) { + if ($storage_definition->getProvider() == $module) { + // If the module being installed is also defining a storage key + // for the entity type, the entity schema may not exist yet. It + // will be created later in that case. + try { + $update_manager->installFieldStorageDefinition($storage_definition->getName(), $entity_type->id(), $module, $storage_definition); + } + catch (EntityStorageException $e) { + watchdog_exception('system', $e, 'An error occurred while notifying the creation of the @name field storage definition: "!message" in %function (line %line of %file).', ['@name' => $storage_definition->getName()]); + } + } + } } } @@ -362,9 +382,25 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) { // deleted. For example, a SQL-based storage handler can use this as an // opportunity to drop the corresponding database tables. // @todo Clean this up in https://www.drupal.org/node/2350111. + $update_manager = \Drupal::entityDefinitionUpdateManager(); foreach ($entity_manager->getDefinitions() as $entity_type) { if ($entity_type->getProvider() == $module) { - $entity_manager->onEntityTypeDelete($entity_type); + $update_manager->uninstallEntityType($entity_type); + } + elseif ($entity_type->isSubclassOf(FieldableEntityInterface::CLASS)) { + // The module being installed may be adding new fields to existing + // entity types. Field definitions for any entity type defined by + // the module are handled in the if branch. + $entity_type_id = $entity_type->id(); + /** @var \Drupal\Core\Entity\FieldableEntityStorageInterface $storage */ + $storage = $entity_manager->getStorage($entity_type_id); + foreach ($entity_manager->getFieldStorageDefinitions($entity_type_id) as $storage_definition) { + // @todo We need to trigger field purging here. + // See https://www.drupal.org/node/2282119. + if ($storage_definition->getProvider() == $module && !$storage->countFieldData($storage_definition, TRUE)) { + $update_manager->uninstallFieldStorageDefinition($storage_definition); + } + } } } diff --git a/core/lib/Drupal/Core/Extension/module.api.php b/core/lib/Drupal/Core/Extension/module.api.php index e30fd870c4..d7d5332d9f 100644 --- a/core/lib/Drupal/Core/Extension/module.api.php +++ b/core/lib/Drupal/Core/Extension/module.api.php @@ -6,6 +6,7 @@ */ use Drupal\Core\Database\Database; +use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Url; use Drupal\Core\Utility\UpdateException; @@ -35,8 +36,8 @@ * - Database schema changes: adding, changing, or removing a database table or * field; moving stored data to different fields or tables; changing the * format of stored data. - * - Content entity or field changes: these updates are normally handled - * automatically by the entity system, but should at least be tested. + * - Content entity or field changes: adding, changing, or removing a field + * definition, entity definition, or any of their properties. * * @section sec_how How to write update code * Update code for a module is put into an implementation of hook_update_N(), @@ -531,6 +532,16 @@ function hook_install_tasks_alter(&$tasks, $install_state) { * ignored, and make sure that the configuration data you are saving matches * the configuration schema at the time when you write the update function * (later updates may change it again to match new schema changes). + * - Never assume your field or entity type definitions are the same when the + * update will run as they are when you wrote the update function. Always + * retrieve the correct version via + * \Drupal::entityDefinitionUpdateManager()::getEntityType() or + * \Drupal::entityDefinitionUpdateManager()::getFieldStorageDefinition(). When + * adding a new definition always replicate it in the update function body as + * you would do with a schema definition. + * - Never call \Drupal::entityDefinitionUpdateManager()::applyUpdates() in an + * update function, as it will apply updates for any module not only yours, + * which will lead to unpredictable results. * - Be careful about API functions and especially CRUD operations that you use * in your update function. If they invoke hooks or use services, they may * not behave as expected, and it may actually not be appropriate to use the @@ -548,6 +559,9 @@ function hook_install_tasks_alter(&$tasks, $install_state) { * long as you make sure that your update data matches the schema, and you * use the $has_trusted_data argument in the save operation. * - Marking a container for rebuild. + * - Using the API provided by \Drupal::entityDefinitionUpdateManager() to + * update the entity schema based on changes in entity type or field + * definitions provided by your module. * * See https://www.drupal.org/node/2535316 for more on writing update functions. * @@ -585,6 +599,9 @@ function hook_install_tasks_alter(&$tasks, $install_state) { * @see schemaapi * @see hook_update_last_removed() * @see update_get_update_list() + * @see \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface + * @see node_update_8001 + * @see system_update_8004 * @see https://www.drupal.org/node/2535316 */ function hook_update_N(&$sandbox) { diff --git a/core/modules/block_content/block_content.install b/core/modules/block_content/block_content.install new file mode 100644 index 0000000000..14bb2247da --- /dev/null +++ b/core/modules/block_content/block_content.install @@ -0,0 +1,30 @@ +updateFieldStorageDefinition() + // with the new definition. + $storage_definition = BaseFieldDefinition::create('boolean') + ->setLabel(t('Revision translation affected')) + ->setDescription(t('Indicates if the last edit of a translation belongs to current revision.')) + ->setReadOnly(TRUE) + ->setRevisionable(TRUE) + ->setTranslatable(TRUE); + + \Drupal::entityDefinitionUpdateManager() + ->installFieldStorageDefinition('revision_translation_affected', 'block_content', 'block_content', $storage_definition); +} diff --git a/core/modules/content_translation/src/ContentTranslationUpdatesManager.php b/core/modules/content_translation/src/ContentTranslationUpdatesManager.php index a786a0ec46..2e4f059627 100644 --- a/core/modules/content_translation/src/ContentTranslationUpdatesManager.php +++ b/core/modules/content_translation/src/ContentTranslationUpdatesManager.php @@ -62,7 +62,7 @@ public function updateDefinitions(array $entity_types) { foreach (array_diff_key($storage_definitions, $installed_storage_definitions) as $storage_definition) { /** @var $storage_definition \Drupal\Core\Field\FieldStorageDefinitionInterface */ if ($storage_definition->getProvider() == 'content_translation') { - $this->entityManager->onFieldStorageDefinitionCreate($storage_definition); + $this->updateManager->installFieldStorageDefinition($storage_definition->getName(), $entity_type_id, 'content_translation', $storage_definition); } } } diff --git a/core/modules/node/node.install b/core/modules/node/node.install index a7cb55e0c0..1050d5a18f 100644 --- a/core/modules/node/node.install +++ b/core/modules/node/node.install @@ -5,9 +5,7 @@ * Install, update and uninstall functions for the node module. */ -use Drupal\Component\Utility\SafeMarkup; -use Drupal\Component\Uuid\Uuid; -use Drupal\Core\Url; +use Drupal\Core\Field\BaseFieldDefinition; use Drupal\user\RoleInterface; /** @@ -153,3 +151,73 @@ function node_uninstall() { // Delete remaining general module variables. \Drupal::state()->delete('node.node_access_needs_rebuild'); } + +/** + * Add 'revision_translation_affected' field to 'node' entities. + */ +function node_update_8001() { + // Install the definition that this field had in + // \Drupal\node\Entity\Node::baseFieldDefinitions() + // at the time that this update function was written. If/when code is + // deployed that changes that definition, the corresponding module must + // implement an update function that invokes + // \Drupal::entityDefinitionUpdateManager()->updateFieldStorageDefinition() + // with the new definition. + $storage_definition = BaseFieldDefinition::create('boolean') + ->setLabel(t('Revision translation affected')) + ->setDescription(t('Indicates if the last edit of a translation belongs to current revision.')) + ->setReadOnly(TRUE) + ->setRevisionable(TRUE) + ->setTranslatable(TRUE); + + \Drupal::entityDefinitionUpdateManager() + ->installFieldStorageDefinition('revision_translation_affected', 'node', 'node', $storage_definition); +} + +/** + * Remove obsolete indexes from the node schema. + */ +function node_update_8002() { + // The "node__default_langcode" and "node_field__langcode" indexes were + // removed from \Drupal\node\NodeStorageSchema in + // https://www.drupal.org/node/2261669, but this update function wasn't + // added until https://www.drupal.org/node/2542748. Regenerate the related + // schemas to ensure they match the currently expected status. + $manager = \Drupal::entityDefinitionUpdateManager(); + // Regenerate entity type indexes, this should drop "node__default_langcode". + $manager->updateEntityType($manager->getEntityType('node')); + // Regenerate "langcode" indexes, this should drop "node_field__langcode". + $manager->updateFieldStorageDefinition($manager->getFieldStorageDefinition('langcode', 'node')); +} + +/** + * Promote 'status' and 'uid' fields to entity keys. + */ +function node_update_8003() { + // The 'status' and 'uid' fields were added to the 'entity_keys' annotation + // of \Drupal\node\Entity\Node in https://www.drupal.org/node/2498919, but + // this update function wasn't added until + // https://www.drupal.org/node/2542748. In between, sites could have + // performed interim updates, which would have included automated entity + // schema updates prior to that being removed (see that issue for details). + // Therefore, we check for whether the keys have already been installed. + $manager = \Drupal::entityDefinitionUpdateManager(); + $entity_type = $manager->getEntityType('node'); + $entity_keys = $entity_type->getKeys(); + $entity_keys['status'] = 'status'; + $entity_keys['uid'] = 'uid'; + $entity_type->set('entity_keys', $entity_keys); + $manager->updateEntityType($entity_type); + + // @todo The above should be enough, since that is the only definition that + // changed. But \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema varies + // field schema by whether a field is an entity key, so invoke + // onFieldStorageDefinitionUpdate() with an unmodified + // $field_storage_definition to trigger the necessary changes. + // SqlContentEntityStorageSchema::onEntityTypeUpdate() should be fixed to + // automatically handle this. + // See https://www.drupal.org/node/2554245. + foreach (array('status', 'uid') as $field_name) { + $manager->updateFieldStorageDefinition($manager->getFieldStorageDefinition($field_name, 'node')); + } +} diff --git a/core/modules/system/src/Controller/DbUpdateController.php b/core/modules/system/src/Controller/DbUpdateController.php index aa46654847..28ab72b021 100644 --- a/core/modules/system/src/Controller/DbUpdateController.php +++ b/core/modules/system/src/Controller/DbUpdateController.php @@ -9,7 +9,6 @@ use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Controller\ControllerBase; -use Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface; use Drupal\Core\Render\BareHtmlPageRendererInterface; @@ -61,13 +60,6 @@ class DbUpdateController extends ControllerBase { */ protected $account; - /** - * The entity definition update manager. - * - * @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface - */ - protected $entityDefinitionUpdateManager; - /** * The bare HTML page renderer. * @@ -97,19 +89,16 @@ class DbUpdateController extends ControllerBase { * The module handler. * @param \Drupal\Core\Session\AccountInterface $account * The current user. - * @param \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface $entity_definition_update_manager - * The entity definition update manager. * @param \Drupal\Core\Render\BareHtmlPageRendererInterface $bare_html_page_renderer * The bare HTML page renderer. */ - public function __construct($root, KeyValueExpirableFactoryInterface $key_value_expirable_factory, CacheBackendInterface $cache, StateInterface $state, ModuleHandlerInterface $module_handler, AccountInterface $account, EntityDefinitionUpdateManagerInterface $entity_definition_update_manager, BareHtmlPageRendererInterface $bare_html_page_renderer) { + public function __construct($root, KeyValueExpirableFactoryInterface $key_value_expirable_factory, CacheBackendInterface $cache, StateInterface $state, ModuleHandlerInterface $module_handler, AccountInterface $account, BareHtmlPageRendererInterface $bare_html_page_renderer) { $this->root = $root; $this->keyValueExpirableFactory = $key_value_expirable_factory; $this->cache = $cache; $this->state = $state; $this->moduleHandler = $module_handler; $this->account = $account; - $this->entityDefinitionUpdateManager = $entity_definition_update_manager; $this->bareHtmlPageRenderer = $bare_html_page_renderer; } @@ -124,7 +113,6 @@ public static function create(ContainerInterface $container) { $container->get('state'), $container->get('module_handler'), $container->get('current_user'), - $container->get('entity.definition_update_manager'), $container->get('bare_html_page_renderer') ); } @@ -325,23 +313,6 @@ protected function selection(Request $request) { drupal_set_message($this->t('Some of the pending updates cannot be applied because their dependencies were not met.'), 'warning'); } - // If there are entity definition updates, display their summary. - if ($this->entityDefinitionUpdateManager->needsUpdates()) { - $entity_build = array(); - $summary = $this->entityDefinitionUpdateManager->getChangeSummary(); - foreach ($summary as $entity_type_id => $items) { - $entity_update_key = 'entity_type_updates_' . $entity_type_id; - $entity_build[$entity_update_key] = array( - '#theme' => 'item_list', - '#items' => $items, - '#title' => $entity_type_id . ' entity type', - ); - $count++; - } - // Display these above the module updates, since they will be run first. - $build['start'] = $entity_build + $build['start']; - } - if (empty($count)) { drupal_set_message($this->t('No pending updates.')); unset($build); @@ -600,16 +571,6 @@ protected function triggerBatch(Request $request) { } } - // Lastly, perform entity definition updates, which will update storage - // schema if needed. If module update functions need to work with specific - // entity schema they should call the entity update service for the specific - // update themselves. - // @see \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface::applyEntityUpdate() - // @see \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface::applyFieldUpdate() - if ($this->entityDefinitionUpdateManager->needsUpdates()) { - $operations[] = array('update_entity_definitions', array()); - } - $batch['operations'] = $operations; $batch += array( 'title' => $this->t('Updating'), diff --git a/core/modules/system/src/Tests/Entity/EntityDefinitionTestTrait.php b/core/modules/system/src/Tests/Entity/EntityDefinitionTestTrait.php index e5c8f4b310..a9f2b730e7 100644 --- a/core/modules/system/src/Tests/Entity/EntityDefinitionTestTrait.php +++ b/core/modules/system/src/Tests/Entity/EntityDefinitionTestTrait.php @@ -125,6 +125,17 @@ protected function modifyBaseField() { $this->addBaseField('text'); } + /** + * Promotes a field to an entity key. + */ + protected function makeBaseFieldEntityKey() { + $entity_type = clone $this->entityManager->getDefinition('entity_test_update'); + $entity_keys = $entity_type->getKeys(); + $entity_keys['new_base_field'] = 'new_base_field'; + $entity_type->set('entity_keys', $entity_keys); + $this->state->set('entity_test_update.entity_type', $entity_type); + } + /** * Removes the new base field from the 'entity_test_update' entity type. */ diff --git a/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php b/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php index 8ae7fa5e4e..a1980854f3 100644 --- a/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php +++ b/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php @@ -7,17 +7,17 @@ namespace Drupal\system\Tests\Entity; +use Drupal\Component\Plugin\Exception\PluginNotFoundException; use Drupal\Core\Database\Database; use Drupal\Core\Database\DatabaseExceptionWrapper; use Drupal\Core\Database\IntegrityConstraintViolationException; -use Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface; +use Drupal\Core\Entity\ContentEntityType; use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Entity\EntityTypeEvents; use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Field\FieldStorageDefinitionEvents; use Drupal\Core\Language\LanguageInterface; -use Drupal\entity_test\FieldStorageDefinition; /** * Tests EntityDefinitionUpdateManager functionality. @@ -610,21 +610,86 @@ public function testEntityTypeSchemaUpdateAndRevisionableBaseFieldCreateWithoutD * Tests ::applyEntityUpdate() and ::applyFieldUpdate(). */ public function testSingleActionCalls() { - // Ensure that the methods return FALSE when called with bogus information. - $this->assertFalse($this->entityDefinitionUpdateManager->applyEntityUpdate(EntityDefinitionUpdateManagerInterface::DEFINITION_CREATED, 'foo'), 'Calling applyEntityUpdate() with a non-existent entity returns FALSE.'); - $this->assertFalse($this->entityDefinitionUpdateManager->applyFieldUpdate(EntityDefinitionUpdateManagerInterface::DEFINITION_CREATED, 'foo', 'bar'), 'Calling applyFieldUpdate() with a non-existent entity returns FALSE.'); - $this->assertFalse($this->entityDefinitionUpdateManager->applyFieldUpdate(EntityDefinitionUpdateManagerInterface::DEFINITION_CREATED, 'entity_test_update', 'bar'), 'Calling applyFieldUpdate() with a non-existent field returns FALSE.'); - $this->assertFalse($this->entityDefinitionUpdateManager->applyEntityUpdate(EntityDefinitionUpdateManagerInterface::DEFINITION_CREATED, 'entity_test_update'), 'Calling applyEntityUpdate() with an $op that is not applicable to the entity type returns FALSE.'); - $this->assertFalse($this->entityDefinitionUpdateManager->applyFieldUpdate(EntityDefinitionUpdateManagerInterface::DEFINITION_DELETED, 'entity_test_update', 'new_base_field'), 'Calling applyFieldUpdate() with an $op that is not applicable to the field returns FALSE.'); + $db_schema = $this->database->schema(); + + // Ensure that a non-existing entity type cannot be installed. + $message = 'A non-existing entity type cannot be installed'; + try { + $this->entityDefinitionUpdateManager->installEntityType(new ContentEntityType(['id' => 'foo'])); + $this->fail($message); + } + catch (PluginNotFoundException $e) { + $this->pass($message); + } + + // Ensure that a field cannot be installed on non-existing entity type. + $message = 'A field cannot be installed on a non-existing entity type'; + try { + $storage_definition = BaseFieldDefinition::create('string') + ->setLabel(t('A new revisionable base field')) + ->setRevisionable(TRUE); + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('bar', 'foo', 'entity_test', $storage_definition); + $this->fail($message); + } + catch (PluginNotFoundException $e) { + $this->pass($message); + } + + // Ensure that a non-existing field cannot be installed. + $storage_definition = BaseFieldDefinition::create('string') + ->setLabel(t('A new revisionable base field')) + ->setRevisionable(TRUE); + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('bar', 'entity_test_update', 'entity_test', $storage_definition); + $this->assertFalse($db_schema->fieldExists('entity_test_update', 'bar'), "A non-existing field cannot be installed."); + + // Ensure that installing an existing entity type is a no-op. + $entity_type = $this->entityDefinitionUpdateManager->getEntityType('entity_test_update'); + $this->entityDefinitionUpdateManager->installEntityType($entity_type); + $this->assertTrue($db_schema->tableExists('entity_test_update'), 'Installing an existing entity type is a no-op'); // Create a new base field. $this->addRevisionableBaseField(); - $this->assertTrue($this->entityDefinitionUpdateManager->applyFieldUpdate(EntityDefinitionUpdateManagerInterface::DEFINITION_CREATED, 'entity_test_update', 'new_base_field'), 'Calling applyFieldUpdate() correctly returns TRUE.'); - $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table."); + $storage_definition = BaseFieldDefinition::create('string') + ->setLabel(t('A new revisionable base field')) + ->setRevisionable(TRUE); + $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' does not exist before applying the update."); + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition); + $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table."); + + // Ensure that installing an existing entity type is a no-op. + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition); + $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), 'Installing an existing entity type is a no-op'); + + // Update an existing field schema. + $this->modifyBaseField(); + $storage_definition = BaseFieldDefinition::create('text') + ->setName('new_base_field') + ->setTargetEntityTypeId('entity_test_update') + ->setLabel(t('A new revisionable base field')) + ->setRevisionable(TRUE); + $this->entityDefinitionUpdateManager->updateFieldStorageDefinition($storage_definition); + $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "Previous schema for 'new_base_field' no longer exists."); + $this->assertTrue( + $db_schema->fieldExists('entity_test_update', 'new_base_field__value') && $db_schema->fieldExists('entity_test_update', 'new_base_field__format'), + "New schema for 'new_base_field' has been created." + ); + + // Drop an existing field schema. + $this->entityDefinitionUpdateManager->uninstallFieldStorageDefinition($storage_definition); + $this->assertFalse( + $db_schema->fieldExists('entity_test_update', 'new_base_field__value') || $db_schema->fieldExists('entity_test_update', 'new_base_field__format'), + "The schema for 'new_base_field' has been dropped." + ); + // Make the entity type revisionable. $this->updateEntityTypeToRevisionable(); - $this->assertTrue($this->entityDefinitionUpdateManager->applyEntityUpdate(EntityDefinitionUpdateManagerInterface::DEFINITION_UPDATED, 'entity_test_update'), 'Calling applyEntityUpdate() correctly returns TRUE.'); - $this->assertTrue($this->database->schema()->tableExists('entity_test_update_revision'), "The 'entity_test_update_revision' table has been created."); + $this->assertFalse($db_schema->tableExists('entity_test_update_revision'), "The 'entity_test_update_revision' does not exist before applying the update."); + $entity_type = $this->entityDefinitionUpdateManager->getEntityType('entity_test_update'); + $keys = $entity_type->getKeys(); + $keys['revision'] = 'revision_id'; + $entity_type->set('entity_keys', $keys); + $this->entityDefinitionUpdateManager->updateEntityType($entity_type); + $this->assertTrue($db_schema->tableExists('entity_test_update_revision'), "The 'entity_test_update_revision' table has been created."); } /** @@ -671,4 +736,52 @@ public function testCreateIndexUsingEntityStorageSchemaWithData() { } } + /** + * Tests updating a base field when it has existing data. + */ + public function testBaseFieldEntityKeyUpdateWithExistingData() { + // Add the base field and run the update. + $this->addBaseField(); + $this->entityDefinitionUpdateManager->applyUpdates(); + + // Save an entity with the base field populated. + $this->entityManager->getStorage('entity_test_update')->create(['new_base_field' => $this->randomString()])->save(); + + // Save an entity with the base field not populated. + /** @var \Drupal\entity_test\Entity\EntityTestUpdate $entity */ + $entity = $this->entityManager->getStorage('entity_test_update')->create(); + $entity->save(); + + // Promote the base field to an entity key. This will trigger the addition + // of a NOT NULL constraint. + $this->makeBaseFieldEntityKey(); + + // Try to apply the update and verify they fail since we have a NULL value. + $message = 'An error occurs when trying to enabling NOT NULL constraints with NULL data.'; + try { + $this->entityDefinitionUpdateManager->applyUpdates(); + $this->fail($message); + } + catch (EntityStorageException $e) { + $this->pass($message); + } + + // Check that the update is correctly applied when no NULL data is left. + $entity->set('new_base_field', $this->randomString()); + $entity->save(); + $this->entityDefinitionUpdateManager->applyUpdates(); + $this->pass('The update is correctly performed when no NULL data exists.'); + + // Check that the update actually applied a NOT NULL constraint. + $entity->set('new_base_field', NULL); + $message = 'The NOT NULL constraint was correctly applied.'; + try { + $entity->save(); + $this->fail($message); + } + catch (EntityStorageException $e) { + $this->pass($message); + } + } + } diff --git a/core/modules/system/src/Tests/Entity/Update/SqlContentEntityStorageSchemaIndexTest.php b/core/modules/system/src/Tests/Entity/Update/SqlContentEntityStorageSchemaIndexTest.php index 0e613f1bfb..3481b552cc 100644 --- a/core/modules/system/src/Tests/Entity/Update/SqlContentEntityStorageSchemaIndexTest.php +++ b/core/modules/system/src/Tests/Entity/Update/SqlContentEntityStorageSchemaIndexTest.php @@ -16,11 +16,6 @@ */ class SqlContentEntityStorageSchemaIndexTest extends UpdatePathTestBase { - /** - * {@inheritdoc} - */ - protected static $modules = ['update_order_test']; - /** * {@inheritdoc} */ @@ -35,9 +30,6 @@ public function setUp() { * Tests entity and field schema database updates and execution order. */ public function testIndex() { - // Enable the hook implementations in the update_order_test module. - \Drupal::state()->set('update_order_test', TRUE); - // The initial Drupal 8 database dump before any updates does not include // the entity ID in the entity field data table indices that were added in // https://www.drupal.org/node/2261669. @@ -45,38 +37,12 @@ public function testIndex() { $this->assertFalse(db_index_exists('node_field_data', 'node__id__default_langcode__langcode'), 'Index node__id__default_langcode__langcode does not exist prior to running updates.'); $this->assertFalse(db_index_exists('users_field_data', 'user__id__default_langcode__langcode'), 'Index users__id__default_langcode__langcode does not exist prior to running updates.'); - // Running database updates should automatically update the entity schemata - // to add the indices from https://www.drupal.org/node/2261669. + // Running database updates should update the entity schemata to add the + // indices from https://www.drupal.org/node/2261669. $this->runUpdates(); $this->assertFalse(db_index_exists('node_field_data', 'node__default_langcode'), 'Index node__default_langcode properly removed.'); $this->assertTrue(db_index_exists('node_field_data', 'node__id__default_langcode__langcode'), 'Index node__id__default_langcode__langcode properly created on the node_field_data table.'); $this->assertTrue(db_index_exists('users_field_data', 'user__id__default_langcode__langcode'), 'Index users__id__default_langcode__langcode properly created on the user_field_data table.'); - - // Ensure that hook_update_N() implementations were in the expected order - // relative to the entity and field updates. The expected order is: - // 1. Initial Drupal 8.0.0-beta12 installation with no indices. - // 2. update_order_test_update_8001() is invoked. - // 3. update_order_test_update_8002() is invoked. - // 4. update_order_test_update_8002() explicitly applies the updates for - // the update_order_test_field storage. See update_order_test.module. - // 5. update_order_test_update_8002() explicitly applies the updates for - // the node entity type indices listed above. - // 6. The remaining entity schema updates are applied automatically after - // all update hook implementations have run, which applies the user - // index update. - $this->assertTrue(\Drupal::state()->get('update_order_test_update_8001', FALSE), 'Index node__default_langcode still existed during update_order_test_update_8001(), indicating that it ran before the entity type updates.'); - - // Node updates were run during update_order_test_update_8002(). - $this->assertFalse(\Drupal::state()->get('update_order_test_update_8002_node__default_langcode', TRUE), 'The node__default_langcode index was removed during update_order_test_update_8002().'); - $this->assertTrue(\Drupal::state()->get('update_order_test_update_8002_node__id__default_langcode__langcode', FALSE), 'The node__id__default_langcode__langcode index was created during update_order_test_update_8002().'); - - // Ensure that the base field created by update_order_test_update_8002() is - // created when we expect. - $this->assertFalse(\Drupal::state()->get('update_order_test_update_8002_update_order_test_before', TRUE), 'The update_order_test field was not been created on Node before update_order_test_update_8002().'); - $this->assertTrue(\Drupal::state()->get('update_order_test_update_8002_update_order_test_after', FALSE), 'The update_order_test field was created on Node by update_order_test_update_8002().'); - - // User update were not run during update_order_test_update_8002(). - $this->assertFalse(\Drupal::state()->get('update_order_test_update_8002_user__id__default_langcode__langcode', TRUE)); } } diff --git a/core/modules/system/src/Tests/Entity/Update/UpdateApiEntityDefinitionUpdateTest.php b/core/modules/system/src/Tests/Entity/Update/UpdateApiEntityDefinitionUpdateTest.php new file mode 100644 index 0000000000..25f97a993c --- /dev/null +++ b/core/modules/system/src/Tests/Entity/Update/UpdateApiEntityDefinitionUpdateTest.php @@ -0,0 +1,204 @@ +entityManager = $this->container->get('entity.manager'); + $this->updatesManager = $this->container->get('entity.definition_update_manager'); + + $admin = $this->drupalCreateUser([], FALSE, TRUE); + $this->drupalLogin($admin); + } + + /** + * Tests that individual updates applied sequentially work as expected. + */ + public function testSingleUpdates() { + // Create a test entity. + $user_ids = [mt_rand(), mt_rand()]; + $entity = EntityTest::create(['name' => $this->randomString(), 'user_id' => $user_ids]); + $entity->save(); + + // Check that only a single value is stored for 'user_id'. + $entity = $this->reloadEntity($entity); + $this->assertEqual(count($entity->user_id), 1); + $this->assertEqual($entity->user_id->target_id, $user_ids[0]); + + // Make 'user_id' multiple by running updates. + $this->enableUpdates('entity_test', 'entity_definition_updates', 8001); + $this->runUpdates(); + + // Check that data was correctly migrated. + $entity = $this->reloadEntity($entity); + $this->assertEqual(count($entity->user_id), 1); + $this->assertEqual($entity->user_id->target_id, $user_ids[0]); + + // Store multiple data and check it is correctly stored. + $entity->user_id = $user_ids; + $entity->save(); + $entity = $this->reloadEntity($entity); + $this->assertEqual(count($entity->user_id), 2); + $this->assertEqual($entity->user_id[0]->target_id, $user_ids[0]); + $this->assertEqual($entity->user_id[1]->target_id, $user_ids[1]); + + // Make 'user_id' single again by running updates. + $this->enableUpdates('entity_test', 'entity_definition_updates', 8002); + $this->runUpdates(); + + // Check that data was correctly migrated/dropped. + $entity = $this->reloadEntity($entity); + $this->assertEqual(count($entity->user_id), 1); + $this->assertEqual($entity->user_id->target_id, $user_ids[0]); + } + + /** + * Tests that multiple updates applied in bulk work as expected. + */ + public function testMultipleUpdates() { + // Create a test entity. + $user_ids = [mt_rand(), mt_rand()]; + $entity = EntityTest::create(['name' => $this->randomString(), 'user_id' => $user_ids]); + $entity->save(); + + // Check that only a single value is stored for 'user_id'. + $entity = $this->reloadEntity($entity); + $this->assertEqual(count($entity->user_id), 1); + $this->assertEqual($entity->user_id->target_id, $user_ids[0]); + + // Make 'user_id' multiple and then single again by running updates. + $this->enableUpdates('entity_test', 'entity_definition_updates', 8002); + $this->runUpdates(); + + // Check that data was correctly migrated back and forth. + $entity = $this->reloadEntity($entity); + $this->assertEqual(count($entity->user_id), 1); + $this->assertEqual($entity->user_id->target_id, $user_ids[0]); + + // Check that only a single value is stored for 'user_id' again. + $entity->user_id = $user_ids; + $entity->save(); + $entity = $this->reloadEntity($entity); + $this->assertEqual(count($entity->user_id), 1); + $this->assertEqual($entity->user_id[0]->target_id, $user_ids[0]); + } + + /** + * Tests that entity updates are correctly reported in the status report page. + */ + function testStatusReport() { + // Create a test entity. + $entity = EntityTest::create(['name' => $this->randomString(), 'user_id' => mt_rand()]); + $entity->save(); + + // Check that the status report initially displays no error. + $this->drupalGet('admin/reports/status'); + $this->assertNoRaw('Out of date'); + $this->assertNoRaw('Mismatch detected'); + + // Enable an entity update and check that we have a dedicated status report + // item. + $this->container->get('state')->set('entity_test.remove_name_field', TRUE); + $this->drupalGet('admin/reports/status'); + $this->assertNoRaw('Out of date'); + $this->assertRaw('Mismatch detected'); + + // Enable a db update and check that now the entity update status report + // item is no longer displayed. We assume an update function will fix the + // mismatch. + $this->enableUpdates('entity_test', 'status_report', 8001); + $this->drupalGet('admin/reports/status'); + $this->assertRaw('Out of date'); + $this->assertNoRaw('Mismatch detected'); + + // Run db updates and check that entity updates were not applied. + $this->runUpdates(); + $this->drupalGet('admin/reports/status'); + $this->assertNoRaw('Out of date'); + $this->assertRaw('Mismatch detected'); + + // Check that en exception would be triggered when trying to apply them with + // existing data. + $message = 'Entity updates cannot run if entity data exists.'; + try { + $this->updatesManager->applyUpdates(); + $this->fail($message); + } + catch (FieldStorageDefinitionUpdateForbiddenException $e) { + $this->pass($message); + } + + // Check the status report is the same after trying to apply updates. + $this->drupalGet('admin/reports/status'); + $this->assertNoRaw('Out of date'); + $this->assertRaw('Mismatch detected'); + + // Delete entity data, enable a new update, run updates again and check that + // entity updates were not applied even when no data exists. + $entity->delete(); + $this->enableUpdates('entity_test', 'status_report', 8002); + $this->runUpdates(); + $this->drupalGet('admin/reports/status'); + $this->assertNoRaw('Out of date'); + $this->assertRaw('Mismatch detected'); + } + + /** + * Reloads the specified entity. + * + * @param \Drupal\entity_test\Entity\EntityTest $entity + * An entity object. + * + * @return \Drupal\entity_test\Entity\EntityTest + * The reloaded entity object. + */ + protected function reloadEntity(EntityTest $entity) { + $this->entityManager->useCaches(FALSE); + $this->entityManager->getStorage('entity_test')->resetCache([$entity->id()]); + return EntityTest::load($entity->id()); + } + +} diff --git a/core/modules/system/src/Tests/Update/DbUpdatesTrait.php b/core/modules/system/src/Tests/Update/DbUpdatesTrait.php new file mode 100644 index 0000000000..e9f009358b --- /dev/null +++ b/core/modules/system/src/Tests/Update/DbUpdatesTrait.php @@ -0,0 +1,57 @@ +container->get('state')->set($module . '.db_updates.' . $group, $index); + } + + /** + * Runs DB updates. + */ + protected function runUpdates() { + $this->drupalGet(Url::fromRoute('system.db_update')); + $this->clickLink($this->t('Continue')); + $this->clickLink($this->t('Apply pending updates')); + } + + /** + * Conditionally load Update API functions for the specified group. + * + * @param string $module + * The name of the module defining the update functions. + * @param string $group + * A name identifying the group of update functions to enable. + */ + public static function includeUpdates($module, $group) { + if ($index = \Drupal::state()->get($module . '.db_updates.' . $group)) { + module_load_include('inc', $module, 'update/' . $group . '_' . $index); + } + } + +} diff --git a/core/modules/system/src/Tests/Update/UpdatePathTestBase.php b/core/modules/system/src/Tests/Update/UpdatePathTestBase.php index 6c9b23bf2b..d3d103da23 100644 --- a/core/modules/system/src/Tests/Update/UpdatePathTestBase.php +++ b/core/modules/system/src/Tests/Update/UpdatePathTestBase.php @@ -248,6 +248,9 @@ protected function runUpdates() { $config = $this->config($name); $this->assertConfigSchema($typed_config, $name, $config->get()); } + + // Ensure that the update hooks updated all entity schema. + $this->assertFalse(\Drupal::service('entity.definition_update_manager')->needsUpdates(), 'After all updates ran, entity schema is up to date.'); } /** diff --git a/core/modules/system/system.install b/core/modules/system/system.install index f7c1b39bcc..ce06b7a5be 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -594,10 +594,16 @@ function system_requirements($phase) { } } } - if (!isset($requirements['update']['severity']) && \Drupal::service('entity.definition_update_manager')->needsUpdates()) { - $requirements['update']['severity'] = REQUIREMENT_ERROR; - $requirements['update']['value'] = t('Out of date'); - $requirements['update']['description'] = t('Some modules have database schema updates to install. You should run the database update script immediately.', array('@update' => \Drupal::url('system.db_update'))); + + // Verify that no entity updates are pending after running every DB update. + if (!isset($requirements['update']['severity']) && \Drupal::entityDefinitionUpdateManager()->needsUpdates()) { + $requirements['entity_update'] = array( + 'title' => t('Entity/field definitions'), + 'value' => t('Mismatch detected'), + 'severity' => REQUIREMENT_ERROR, + // @todo Provide details: https://www.drupal.org/node/2554911 + 'description' => t('Mismatched entity and/or field definitions.'), + ); } } @@ -1247,3 +1253,18 @@ function system_update_8003() { ] ); } + +/** + * Add a (id, default_langcode, langcode) composite index to entities. + */ +function system_update_8004() { + // \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema was changed in + // https://www.drupal.org/node/2261669 to include a (id, default_langcode, + // langcode) compound index, but this update function wasn't added until + // https://www.drupal.org/node/2542748. Regenerate the related schemas to + // ensure they match the currently expected status. + $manager = \Drupal::entityDefinitionUpdateManager(); + foreach (array_keys(\Drupal::entityManager()->getDefinitions()) as $entity_type_id) { + $manager->updateEntityType($manager->getEntityType($entity_type_id)); + } +} diff --git a/core/modules/system/tests/modules/entity_test/entity_test.install b/core/modules/system/tests/modules/entity_test/entity_test.install index 1a6d9a2171..a9ff42331a 100644 --- a/core/modules/system/tests/modules/entity_test/entity_test.install +++ b/core/modules/system/tests/modules/entity_test/entity_test.install @@ -5,6 +5,8 @@ * Install, update and uninstall functions for the entity_test module. */ +use Drupal\system\Tests\Update\DbUpdatesTrait; + /** * Implements hook_install(). */ @@ -49,3 +51,6 @@ function entity_test_schema() { ); return $schema; } + +DbUpdatesTrait::includeUpdates('entity_test', 'entity_definition_updates'); +DbUpdatesTrait::includeUpdates('entity_test', 'status_report'); diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module index 8bddd3246e..eea78e82a4 100644 --- a/core/modules/system/tests/modules/entity_test/entity_test.module +++ b/core/modules/system/tests/modules/entity_test/entity_test.module @@ -12,6 +12,7 @@ use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; +use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Entity\Entity\EntityFormDisplay; @@ -119,14 +120,25 @@ function entity_test_entity_base_field_info(EntityTypeInterface $entity_type) { * Implements hook_entity_base_field_info_alter(). */ function entity_test_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type) { - if ($entity_type->id() == 'entity_test_mulrev' && ($names = \Drupal::state()->get('entity_test.field_definitions.translatable'))) { + $state = \Drupal::state(); + if ($entity_type->id() == 'entity_test_mulrev' && ($names = $state->get('entity_test.field_definitions.translatable'))) { foreach ($names as $name => $value) { $fields[$name]->setTranslatable($value); } } - if ($entity_type->id() == 'node' && Drupal::state()->get('entity_test.node_remove_status_field')) { + if ($entity_type->id() == 'node' && $state->get('entity_test.node_remove_status_field')) { unset($fields['status']); } + if ($entity_type->id() == 'entity_test' && $state->get('entity_test.remove_name_field')) { + unset($fields['name']); + } + // In 8001 we are assuming that a new definition with multiple cardinality has + // been deployed. + // @todo Remove this if we end up using state definitions at runtime. See + // https://www.drupal.org/node/2554235. + if ($entity_type->id() == 'entity_test' && $state->get('entity_test.db_updates.entity_definition_updates') == 8001) { + $fields['user_id']->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); + } } /** diff --git a/core/modules/system/tests/modules/entity_test/update/entity_definition_updates_8001.inc b/core/modules/system/tests/modules/entity_test/update/entity_definition_updates_8001.inc new file mode 100644 index 0000000000..8c5478194b --- /dev/null +++ b/core/modules/system/tests/modules/entity_test/update/entity_definition_updates_8001.inc @@ -0,0 +1,43 @@ +select('entity_test', 'et') + ->fields('et', ['id', 'user_id']) + ->execute() + ->fetchAllKeyed(); + + // Remove data from the storage. + $database->update('entity_test') + ->fields(['user_id' => NULL]) + ->execute(); + + // Update definitions and schema. + $manager = \Drupal::entityDefinitionUpdateManager(); + $storage_definition = $manager->getFieldStorageDefinition('user_id', 'entity_test'); + $storage_definition->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); + $manager->updateFieldStorageDefinition($storage_definition); + + // Restore entity data in the new schema. + $insert_query = $database->insert('entity_test__user_id') + ->fields(['bundle', 'deleted', 'entity_id', 'revision_id', 'langcode', 'delta', 'user_id_target_id']); + foreach ($user_ids as $id => $user_id) { + $insert_query->values(['entity_test', 0, $id, $id, 'en', 0, $user_id]); + } + $insert_query->execute(); +} diff --git a/core/modules/system/tests/modules/entity_test/update/entity_definition_updates_8002.inc b/core/modules/system/tests/modules/entity_test/update/entity_definition_updates_8002.inc new file mode 100644 index 0000000000..e304ebdbf5 --- /dev/null +++ b/core/modules/system/tests/modules/entity_test/update/entity_definition_updates_8002.inc @@ -0,0 +1,41 @@ +select('entity_test__user_id', 'et') + ->fields('et', ['entity_id', 'user_id_target_id']); + $query->condition('et.delta', 0); + $user_ids = $query->execute()->fetchAllKeyed(); + + // Remove data from the storage. + $database->truncate('entity_test__user_id')->execute(); + + // Update definitions and schema. + $manager = \Drupal::entityDefinitionUpdateManager(); + $storage_definition = $manager->getFieldStorageDefinition('user_id', 'entity_test'); + $storage_definition->setCardinality(1); + $manager->updateFieldStorageDefinition($storage_definition); + + // Restore entity data in the new schema. + foreach ($user_ids as $id => $user_id) { + $database->update('entity_test') + ->fields(['user_id' => $user_id]) + ->condition('id', $id) + ->execute(); + } +} diff --git a/core/modules/system/tests/modules/entity_test/update/status_report_8001.inc b/core/modules/system/tests/modules/entity_test/update/status_report_8001.inc new file mode 100644 index 0000000000..91eb72313b --- /dev/null +++ b/core/modules/system/tests/modules/entity_test/update/status_report_8001.inc @@ -0,0 +1,13 @@ +drupalLogin($this->rootUser); $this->drupalGet('update.php/selection'); $this->assertText('No pending updates.'); + + // Ensure that there are no pending entity updates after installation. + $this->assertFalse($this->container->get('entity.definition_update_manager')->needsUpdates(), 'After installation, entity schema is up to date.'); } } diff --git a/core/profiles/standard/src/Tests/StandardTest.php b/core/profiles/standard/src/Tests/StandardTest.php index ee867d125b..e46686de0b 100644 --- a/core/profiles/standard/src/Tests/StandardTest.php +++ b/core/profiles/standard/src/Tests/StandardTest.php @@ -157,6 +157,9 @@ function testStandard() { $this->drupalLogin($this->rootUser); $this->drupalGet('update.php/selection'); $this->assertText('No pending updates.'); + + // Ensure that there are no pending entity updates after installation. + $this->assertFalse($this->container->get('entity.definition_update_manager')->needsUpdates(), 'After installation, entity schema is up to date.'); } } -- GitLab