Commit ba95bbb2 authored by effulgentsia's avatar effulgentsia

Issue #2542748 by plach, effulgentsia, jhedstrom, Gábor Hojtsy, alexpott,...

Issue #2542748 by plach, effulgentsia, jhedstrom, Gábor Hojtsy, alexpott, mpdonadio, catch, dawehner: Automatic entity updates can fail when there is existing content, leaving the site's schema in an unpredictable state
parent 3e82ee5d
...@@ -1599,6 +1599,10 @@ function install_bootstrap_full() { ...@@ -1599,6 +1599,10 @@ function install_bootstrap_full() {
* The batch definition. * The batch definition.
*/ */
function install_profile_modules(&$install_state) { 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(); $modules = \Drupal::state()->get('install_profile_modules') ?: array();
$files = system_rebuild_module_data(); $files = system_rebuild_module_data();
\Drupal::state()->delete('install_profile_modules'); \Drupal::state()->delete('install_profile_modules');
...@@ -1638,6 +1642,18 @@ function install_profile_modules(&$install_state) { ...@@ -1638,6 +1642,18 @@ function install_profile_modules(&$install_state) {
return $batch; 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. * Installs themes.
* *
...@@ -1665,12 +1681,6 @@ function install_profile_themes(&$install_state) { ...@@ -1665,12 +1681,6 @@ function install_profile_themes(&$install_state) {
* An array of information about the current installation state. * An array of information about the current installation state.
*/ */
function install_install_profile(&$install_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); \Drupal::service('module_installer')->install(array(drupal_get_profile()), FALSE);
// Install all available optional config. During installation the module order // Install all available optional config. During installation the module order
// is determined by dependencies. If there are no dependencies between modules // is determined by dependencies. If there are no dependencies between modules
......
...@@ -218,26 +218,6 @@ function update_do_one($module, $number, $dependency_map, &$context) { ...@@ -218,26 +218,6 @@ function update_do_one($module, $number, $dependency_map, &$context) {
$context['message'] = 'Updating ' . Html::escape($module) . ' module'; $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. * Returns a list of all the pending database updates.
* *
......
...@@ -687,4 +687,14 @@ public static function destination() { ...@@ -687,4 +687,14 @@ public static function destination() {
return static::getContainer()->get('redirect.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');
}
} }
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface; use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
use Drupal\Core\Entity\Schema\EntityStorageSchemaInterface; use Drupal\Core\Entity\Schema\EntityStorageSchemaInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\StringTranslation\StringTranslationTrait;
...@@ -95,13 +96,13 @@ public function getChangeSummary() { ...@@ -95,13 +96,13 @@ public function getChangeSummary() {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function applyUpdates() { public function applyUpdates() {
$change_list = $this->getChangeList(); $complete_change_list = $this->getChangeList();
if ($change_list) { if ($complete_change_list) {
// self::getChangeList() only disables the cache and does not invalidate. // self::getChangeList() only disables the cache and does not invalidate.
// In case there are changes, explicitly invalidate caches. // In case there are changes, explicitly invalidate caches.
$this->entityManager->clearCachedDefinitions(); $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 // Process entity type definition changes before storage definitions ones
// this is necessary when you change an entity type from non-revisionable // this is necessary when you change an entity type from non-revisionable
// to revisionable and at the same time add revisionable fields to the // to revisionable and at the same time add revisionable fields to the
...@@ -127,42 +128,76 @@ public function applyUpdates() { ...@@ -127,42 +128,76 @@ public function applyUpdates() {
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function applyEntityUpdate($op, $entity_type_id, $reset_cached_definitions = TRUE) { public function getEntityType($entity_type_id) {
$change_list = $this->getChangeList(); $entity_type = $this->entityManager->getLastInstalledDefinition($entity_type_id);
if (!isset($change_list[$entity_type_id]) || $change_list[$entity_type_id]['entity_type'] !== $op) { return $entity_type ? clone $entity_type : NULL;
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;
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function applyFieldUpdate($op, $entity_type_id, $field_name, $reset_cached_definitions = TRUE) { public function installEntityType(EntityTypeInterface $entity_type) {
$change_list = $this->getChangeList(); $this->entityManager->clearCachedDefinitions();
if (!isset($change_list[$entity_type_id]['field_storage_definitions']) || $change_list[$entity_type_id]['field_storage_definitions'][$field_name] !== $op) { $this->entityManager->onEntityTypeCreate($entity_type);
return FALSE; }
}
if ($reset_cached_definitions) { /**
// self::getChangeList() only disables the cache and does not invalidate. * {@inheritdoc}
// In case there are changes, explicitly invalidate caches. */
$this->entityManager->clearCachedDefinitions(); 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); * {@inheritdoc}
$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; 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);
} }
/** /**
......
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
namespace Drupal\Core\Entity; namespace Drupal\Core\Entity;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
/** /**
* Defines an interface for managing entity definition updates. * Defines an interface for managing entity definition updates.
* *
...@@ -25,12 +27,22 @@ ...@@ -25,12 +27,22 @@
* report the differences or when to apply each update. This interface is for * report the differences or when to apply each update. This interface is for
* managing that. * 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::getDefinition()
* @see \Drupal\Core\Entity\EntityManagerInterface::getLastInstalledDefinition() * @see \Drupal\Core\Entity\EntityManagerInterface::getLastInstalledDefinition()
* @see \Drupal\Core\Entity\EntityManagerInterface::getFieldStorageDefinitions() * @see \Drupal\Core\Entity\EntityManagerInterface::getFieldStorageDefinitions()
* @see \Drupal\Core\Entity\EntityManagerInterface::getLastInstalledFieldStorageDefinitions() * @see \Drupal\Core\Entity\EntityManagerInterface::getLastInstalledFieldStorageDefinitions()
* @see \Drupal\Core\Entity\EntityTypeListenerInterface * @see hook_update_N()
* @see \Drupal\Core\Field\FieldStorageDefinitionListenerInterface
*/ */
interface EntityDefinitionUpdateManagerInterface { interface EntityDefinitionUpdateManagerInterface {
...@@ -75,6 +87,9 @@ public function getChangeSummary(); ...@@ -75,6 +87,9 @@ public function getChangeSummary();
/** /**
* Applies all the detected valid changes. * 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 * @throws \Drupal\Core\Entity\EntityStorageException
* This exception is thrown if a change cannot be applied without * This exception is thrown if a change cannot be applied without
* unacceptable data loss. In such a case, the site administrator needs to * unacceptable data loss. In such a case, the site administrator needs to
...@@ -84,67 +99,92 @@ public function getChangeSummary(); ...@@ -84,67 +99,92 @@ public function getChangeSummary();
public function applyUpdates(); 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 * When needing to apply updates to existing entity type definitions, this
* entity definition updates as part of the update function. This is only * method should always be used to retrieve a definition ready to be
* necessary if the hook_update_N() implementation relies on the entity * manipulated.
* definition update. All remaining entity definition updates will be run
* automatically after the hook_update_N() implementations.
* *
* @param string $op
* The operation to perform, either static::DEFINITION_CREATED or
* static::DEFINITION_UPDATED.
* @param string $entity_type_id * @param string $entity_type_id
* The entity type to update. * The entity type identifier.
* @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.
* *
* @return bool * @return \Drupal\Core\Entity\EntityTypeInterface
* TRUE if the entity update is processed, FALSE if not. * The entity type definition.
*/
public function getEntityType($entity_type_id);
/**
* Installs a new entity type definition.
* *
* @throws \Drupal\Core\Entity\EntityStorageException * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* This exception is thrown if a change cannot be applied without * The entity type definition.
* unacceptable data loss. In such a case, the site administrator needs to */
* apply some other process, such as a custom update function or a public function installEntityType(EntityTypeInterface $entity_type);
* migration via the Migrate module.
/**
* 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 * When needing to apply updates to existing field storage definitions, this
* storage definition updates as part of the update function. This is only * method should always be used to retrieve a storage definition ready to be
* necessary if the hook_update_N() implementation relies on the field storage * manipulated.
* definition update. All remaining field storage definition updates will be
* run automatically after the hook_update_N() implementations.
* *
* @param string $op * @param string $name
* The operation to perform, possible values are static::DEFINITION_CREATED, * The field name.
* static::DEFINITION_UPDATED or static::DEFINITION_DELETED.
* @param string $entity_type_id * @param string $entity_type_id
* The entity type to update. * The entity type identifier.
* @param string $field_name *
* The field name to update. * @return \Drupal\Core\Field\FieldStorageDefinitionInterface
* @param bool $reset_cached_definitions * The field storage definition.
* (optional). Determines whether to clear the Entity Manager's cached *
* definitions before applying the update. Defaults to TRUE. Can be used * @todo Make this return a mutable storage definition interface when we have
* to prevent unnecessary cache invalidation when a hook_update_N() makes * one. See https://www.drupal.org/node/2346329.
* multiple calls to this method. */
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 * @param string $name
* This exception is thrown if a change cannot be applied without * The field storage definition name.
* unacceptable data loss. In such a case, the site administrator needs to * @param string $entity_type_id
* apply some other process, such as a custom update function or a * The target entity type identifier.
* migration via the Migrate module. * @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);
} }
...@@ -189,9 +189,10 @@ public function getFieldTableName($field_name) { ...@@ -189,9 +189,10 @@ public function getFieldTableName($field_name) {
public function getColumnNames($field_name) { public function getColumnNames($field_name) {
if (!isset($this->columnMapping[$field_name])) { if (!isset($this->columnMapping[$field_name])) {
$this->columnMapping[$field_name] = array(); $this->columnMapping[$field_name] = array();
$storage_definition = $this->fieldStorageDefinitions[$field_name]; if (isset($this->fieldStorageDefinitions[$field_name])) {
foreach (array_keys($this->fieldStorageDefinitions[$field_name]->getColumns()) as $property_name) { foreach (array_keys($this->fieldStorageDefinitions[$field_name]->getColumns()) as $property_name) {
$this->columnMapping[$field_name][$property_name] = $this->getFieldColumnName($storage_definition, $property_name); $this->columnMapping[$field_name][$property_name] = $this->getFieldColumnName($this->fieldStorageDefinitions[$field_name], $property_name);
}
} }
} }
return $this->columnMapping[$field_name]; return $this->columnMapping[$field_name];
......
...@@ -10,6 +10,8 @@ ...@@ -10,6 +10,8 @@
use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Database\Connection; use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Database; use Drupal\Core\Database\Database;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Database\SchemaException;
use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityStorageBase; use Drupal\Core\Entity\ContentEntityStorageBase;
use Drupal\Core\Entity\EntityBundleListenerInterface; use Drupal\Core\Entity\EntityBundleListenerInterface;
...@@ -1351,7 +1353,9 @@ public function requiresFieldDataMigration(FieldStorageDefinitionInterface $stor ...@@ -1351,7 +1353,9 @@ public function requiresFieldDataMigration(FieldStorageDefinitionInterface $stor
* {@inheritdoc} * {@inheritdoc}
*/ */
public function onEntityTypeCreate(EntityTypeInterface $entity_type) { 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 ...@@ -1364,14 +1368,18 @@ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeI
// definition. // definition.
$this->initTableLayout(); $this->initTableLayout();
// Let the schema handler adapt to possible table layout changes. // 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} * {@inheritdoc}
*/ */
public function onEntityTypeDelete(EntityTypeInterface $entity_type) { 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 $ ...@@ -1386,14 +1394,18 @@ public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $
if ($this->getTableMapping()->allowsSharedTableStorage($storage_definition)) { if ($this->getTableMapping()->allowsSharedTableStorage($storage_definition)) {
$this->tableMapping = NULL; $this->tableMapping = NULL;
} }
$this->getStorageSchema()->onFieldStorageDefinitionCreate($storage_definition); $this->wrapSchemaException(function () use ($storage_definition) {
$this->getStorageSchema()->onFieldStorageDefinitionCreate($storage_definition);
});
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { 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 $ ...@@ -1421,7 +1433,31 @@ public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $
} }
// Update the field schema. // 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);
}
} }
/** /**
......
...@@ -1391,6 +1391,20 @@ protected function updateSharedTableSchema(FieldStorageDefinitionInterface $stor ...@@ -1391,6 +1391,20 @@ protected function updateSharedTableSchema(FieldStorageDefinitionInterface $stor
if ($field_name == $updated_field_name) { if ($field_name == $updated_field_name) {
$schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names); $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. // Drop original indexes and unique keys.
if (!empty($original_schema[$table_name]['indexes'])) { if (!empty($original_schema[$table_name]['indexes'])) {
foreach ($original_schema[$table_name]['indexes'] as $name => $specifier) { foreach ($original_schema[$table_name]['indexes'] as $name => $specifier) {
...@@ -1422,6 +1436,26 @@ protected function updateSharedTableSchema(FieldStorageDefinitionInterface $stor ...@@ -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. * Gets the schema for a single field definition.
* *
...@@ -1803,10 +1837,15 @@ protected function hasColumnChanges(FieldStorageDefinitionInterface $storage_def ...@@ -1803,10 +1837,15 @@ protected function hasColumnChanges(FieldStorageDefinitionInterface $storage_def
} }
if (!$storage_definition->hasCustomStorage()) { if (!$storage_definition->hasCustomStorage()) {
$schema = $this->getSchemaFromStorageDefinition($storage_definition); $keys = array_flip($this->getColumnSchemaRelevantKeys());
foreach ($this->loadFieldSchemaData($original) as $table => $spec) { $definition_schema = $this->getSchemaFromStorageDefinition($storage_definition);
if ($spec['fields'] != $schema[$table]['fields']) { foreach ($this->loadFieldSchemaData($original) as $table => $table_schema) {
return TRUE; 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 ...@@ -1814,4 +1853,18 @@ protected function hasColumnChanges(FieldStorageDefinitionInterface $storage_def
return FALSE; return FALSE;
} }