Commit f27fd1f5 authored by alexpott's avatar alexpott

Issue #2337927 by effulgentsia, plach, fago: Fixed...

Issue #2337927 by effulgentsia, plach, fago: Fixed SqlContentEntityStorage::onFieldStorageDefinition(Create|Update|Delete)() is broken for base fields.
parent fb4e8cdf
......@@ -189,8 +189,13 @@ protected function getChangeList() {
// Detect updated field storage definitions.
foreach (array_intersect_key($storage_definitions, $original_storage_definitions) as $field_name => $storage_definition) {
// @todo Support non-storage-schema-changing definition updates too:
// https://www.drupal.org/node/2336895.
if ($this->requiresFieldStorageSchemaChanges($storage_definition, $original_storage_definitions[$field_name])) {
// https://www.drupal.org/node/2336895. So long as we're checking
// based on schema change requirements rather than definition
// equality, skip the check if the entity type itself needs to be
// updated, since that can affect the schema of all fields, so we
// want to process that update first without reporting false
// positives here.
if (!isset($change_list[$entity_type_id]['entity_type']) && $this->requiresFieldStorageSchemaChanges($storage_definition, $original_storage_definitions[$field_name])) {
$field_changes[$field_name] = static::DEFINITION_UPDATED;
}
}
......
......@@ -1165,7 +1165,7 @@ protected function deleteLastInstalledDefinition($entity_type_id) {
* {@inheritdoc}
*/
public function getLastInstalledFieldStorageDefinitions($entity_type_id) {
return $this->installedDefinitions->get($entity_type_id . '.field_storage_definitions');
return $this->installedDefinitions->get($entity_type_id . '.field_storage_definitions', array());
}
/**
......
......@@ -67,4 +67,12 @@ public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterfac
*/
public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original);
/**
* Performs final cleanup after all data of a field has been purged.
*
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
* The field being purged.
*/
public function finalizePurge(FieldStorageDefinitionInterface $storage_definition);
}
......@@ -15,7 +15,7 @@
class DefaultTableMapping implements TableMappingInterface {
/**
* A list of field storage definitions that are available for this mapping.
* The field storage definitions of this mapping.
*
* @var \Drupal\Core\Field\FieldStorageDefinitionInterface[]
*/
......@@ -101,8 +101,17 @@ public function getAllColumns($table_name) {
$this->allColumns[$table_name] = array_merge($this->allColumns[$table_name], array_values($this->getColumnNames($field_name)));
}
// There is just one field for each dedicated storage table, thus
// $field_name can only refer to it.
if (isset($field_name) && $this->requiresDedicatedTableStorage($this->fieldStorageDefinitions[$field_name])) {
// Unlike in shared storage tables, in dedicated ones field columns are
// positioned last.
$this->allColumns[$table_name] = array_merge($this->getExtraColumns($table_name), $this->allColumns[$table_name]);
}
else {
$this->allColumns[$table_name] = array_merge($this->allColumns[$table_name], $this->getExtraColumns($table_name));
}
}
return $this->allColumns[$table_name];
}
......@@ -179,6 +188,47 @@ public function setExtraColumns($table_name, array $column_names) {
return $this;
}
/**
* Checks whether the given field can be stored in a shared table.
*
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
* The field storage definition.
*
* @return bool
* TRUE if the field can be stored in a dedicated table, FALSE otherwise.
*/
public function allowsSharedTableStorage(FieldStorageDefinitionInterface $storage_definition) {
return !$storage_definition->hasCustomStorage() && $storage_definition->isBaseField() && !$storage_definition->isMultiple();
}
/**
* Checks whether the given field has to be stored in a dedicated table.
*
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
* The field storage definition.
*
* @return bool
* TRUE if the field can be stored in a dedicated table, FALSE otherwise.
*/
public function requiresDedicatedTableStorage(FieldStorageDefinitionInterface $storage_definition) {
return !$storage_definition->hasCustomStorage() && !$this->allowsSharedTableStorage($storage_definition);
}
/**
* Returns a list of dedicated table names for this mapping.
*
* @return string[]
* An array of table names.
*/
public function getDedicatedTableNames() {
$table_mapping = $this;
$definitions = array_filter($this->fieldStorageDefinitions, function($definition) use ($table_mapping) { return $table_mapping->requiresDedicatedTableStorage($definition); });
$data_tables = array_map(function($definition) use ($table_mapping) { return $table_mapping->getDedicatedDataTableName($definition); }, $definitions);
$revision_tables = array_map(function($definition) use ($table_mapping) { return $table_mapping->getDedicatedRevisionTableName($definition); }, $definitions);
$dedicated_tables = array_merge(array_values($data_tables), array_values($revision_tables));
return $dedicated_tables;
}
/**
* {@inheritdoc}
*/
......
......@@ -17,12 +17,12 @@
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
use Drupal\Core\Entity\Query\QueryInterface;
use Drupal\Core\Entity\Schema\FieldableEntityStorageSchemaInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\field\FieldStorageConfigInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
......@@ -33,9 +33,8 @@
*
* The class uses \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema
* internally in order to automatically generate the database schema based on
* the defined base fields. Entity types can override
* SqlContentEntityStorage::getSchema() to customize the generated
* schema; e.g., to add additional indexes.
* the defined base fields. Entity types can override the schema handler to
* customize the generated schema; e.g., to add additional indexes.
*
* @ingroup entity_api
*/
......@@ -107,11 +106,11 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt
protected $entityManager;
/**
* The entity schema handler.
* The entity type's storage schema object.
*
* @var \Drupal\Core\Entity\Schema\EntityStorageSchemaInterface
*/
protected $schemaHandler;
protected $storageSchema;
/**
* Cache backend.
......@@ -157,15 +156,27 @@ public function getFieldStorageDefinitions() {
*/
public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache) {
parent::__construct($entity_type);
$this->database = $database;
$this->entityManager = $entity_manager;
$this->cacheBackend = $cache;
$this->initTableLayout();
}
/**
* Initializes table name variables.
*/
protected function initTableLayout() {
// Reset table field values to ensure changes in the entity type definition
// are correctly reflected in the table layout.
$this->tableMapping = NULL;
$this->revisionKey = NULL;
$this->revisionTable = NULL;
$this->dataTable = NULL;
$this->revisionDataTable = NULL;
// @todo Remove table names from the entity type definition in
// https://drupal.org/node/2232465
$this->baseTable = $this->entityType->getBaseTable() ?: $this->entityTypeId;
$revisionable = $this->entityType->isRevisionable();
if ($revisionable) {
$this->revisionKey = $this->entityType->getKey('revision') ?: 'revision_id';
......@@ -223,32 +234,57 @@ public function getRevisionDataTable() {
}
/**
* Gets the schema handler for this entity storage.
* Returns the entity type's storage schema object.
*
* @return \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema
* The schema handler.
* The schema object.
*/
protected function schemaHandler() {
if (!isset($this->schemaHandler)) {
$schema_handler_class = $this->entityType->getHandlerClass('storage_schema') ?: 'Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema';
$this->schemaHandler = new $schema_handler_class($this->entityManager, $this->entityType, $this, $this->database);
protected function getStorageSchema() {
if (!isset($this->storageSchema)) {
$class = $this->entityType->getHandlerClass('storage_schema') ?: 'Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema';
$this->storageSchema = new $class($this->entityManager, $this->entityType, $this, $this->database);
}
return $this->schemaHandler;
return $this->storageSchema;
}
/**
* {@inheritdoc}
* Updates the wrapped entity type definition.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The update entity type.
*
* @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
* See https://www.drupal.org/node/2274017.
*/
public function getTableMapping() {
if (!isset($this->tableMapping)) {
public function setEntityType(EntityTypeInterface $entity_type) {
if ($this->entityType->id() == $entity_type->id()) {
$this->entityType = $entity_type;
$this->initTableLayout();
}
else {
throw new EntityStorageException(String::format('Unsupported entity type @id', array('@id' => $entity_type->id())));
}
}
$definitions = array_filter($this->getFieldStorageDefinitions(), function (FieldStorageDefinitionInterface $definition) {
// @todo Remove the check for FieldDefinitionInterface::isMultiple() when
// multiple-value base fields are supported in
// https://drupal.org/node/2248977.
return !$definition->hasCustomStorage() && !$definition->isMultiple();
/**
* {@inheritdoc}
*/
public function getTableMapping(array $storage_definitions = NULL) {
$table_mapping = $this->tableMapping;
// If we are using our internal storage definitions, which is our main use
// case, we can statically cache the computed table mapping. If a new set
// of field storage definitions is passed, for instance when comparing old
// and new storage schema, we compute the table mapping without caching.
// @todo Clean-up this in https://www.drupal.org/node/2274017 so we can
// easily instantiate a new table mapping whenever needed.
if (!isset($this->tableMapping) || $storage_definitions) {
$definitions = $storage_definitions ?: $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
$table_mapping = new DefaultTableMapping($definitions);
$definitions = array_filter($definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
return $table_mapping->allowsSharedTableStorage($definition);
});
$this->tableMapping = new DefaultTableMapping($definitions);
$key_fields = array_values(array_filter(array($this->idKey, $this->revisionKey, $this->bundleKey, $this->uuidKey, $this->langcodeKey)));
$all_fields = array_keys($definitions);
......@@ -272,16 +308,16 @@ public function getTableMapping() {
$translatable = $this->entityType->isTranslatable();
if (!$revisionable && !$translatable) {
// The base layout stores all the base field values in the base table.
$this->tableMapping->setFieldNames($this->baseTable, $all_fields);
$table_mapping->setFieldNames($this->baseTable, $all_fields);
}
elseif ($revisionable && !$translatable) {
// The revisionable layout stores all the base field values in the base
// table, except for revision metadata fields. Revisionable fields
// denormalized in the base table but also stored in the revision table
// together with the entity ID and the revision ID as identifiers.
$this->tableMapping->setFieldNames($this->baseTable, array_diff($all_fields, $revision_metadata_fields));
$table_mapping->setFieldNames($this->baseTable, array_diff($all_fields, $revision_metadata_fields));
$revision_key_fields = array($this->idKey, $this->revisionKey);
$this->tableMapping->setFieldNames($this->revisionTable, array_merge($revision_key_fields, $revisionable_fields));
$table_mapping->setFieldNames($this->revisionTable, array_merge($revision_key_fields, $revisionable_fields));
}
elseif (!$revisionable && $translatable) {
// Multilingual layouts store key field values in the base table. The
......@@ -290,7 +326,7 @@ public function getTableMapping() {
// denormalized copy of the bundle field value to allow for more
// performant queries. This means that only the UUID is not stored on
// the data table.
$this->tableMapping
$table_mapping
->setFieldNames($this->baseTable, $key_fields)
->setFieldNames($this->dataTable, array_values(array_diff($all_fields, array($this->uuidKey))))
// Add the denormalized 'default_langcode' field to the mapping. Its
......@@ -306,13 +342,13 @@ public function getTableMapping() {
// holds the data field values for all non-revisionable fields. The data
// field values of revisionable fields are denormalized in the data
// table, as well.
$this->tableMapping->setFieldNames($this->baseTable, array_values(array_diff($key_fields, array($this->langcodeKey))));
$table_mapping->setFieldNames($this->baseTable, array_values(array_diff($key_fields, array($this->langcodeKey))));
// Like in the multilingual, non-revisionable case the UUID is not
// in the data table. Additionally, do not store revision metadata
// fields in the data table.
$data_fields = array_values(array_diff($all_fields, array($this->uuidKey), $revision_metadata_fields));
$this->tableMapping
$table_mapping
->setFieldNames($this->dataTable, $data_fields)
// Add the denormalized 'default_langcode' field to the mapping. Its
// value is identical to the query expression
......@@ -321,20 +357,45 @@ public function getTableMapping() {
->setExtraColumns($this->dataTable, array('default_langcode'));
$revision_base_fields = array_merge(array($this->idKey, $this->revisionKey, $this->langcodeKey), $revision_metadata_fields);
$this->tableMapping->setFieldNames($this->revisionTable, $revision_base_fields);
$table_mapping->setFieldNames($this->revisionTable, $revision_base_fields);
$revision_data_key_fields = array($this->idKey, $this->revisionKey, $this->langcodeKey);
$revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields);
$this->tableMapping
$revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, array($this->langcodeKey));
$table_mapping
->setFieldNames($this->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields))
// Add the denormalized 'default_langcode' field to the mapping. Its
// value is identical to the query expression
// "revision_table.langcode = data_table.langcode".
->setExtraColumns($this->revisionDataTable, array('default_langcode'));
}
// Add dedicated tables.
$definitions = array_filter($definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
return $table_mapping->requiresDedicatedTableStorage($definition);
});
$extra_columns = array(
'bundle',
'deleted',
'entity_id',
'revision_id',
'langcode',
'delta',
);
foreach ($definitions as $field_name => $definition) {
foreach (array($table_mapping->getDedicatedDataTableName($definition), $table_mapping->getDedicatedRevisionTableName($definition)) as $table_name) {
$table_mapping->setFieldNames($table_name, array($field_name));
$table_mapping->setExtraColumns($table_name, $extra_columns);
}
}
// Cache the computed table mapping only if we are using our internal
// storage definitions.
if (!$storage_definitions) {
$this->tableMapping = $table_mapping;
}
}
return $this->tableMapping;
return $table_mapping;
}
/**
......@@ -581,7 +642,7 @@ protected function attachPropertyData(array &$entities) {
$table_mapping = $this->getTableMapping();
$translations = array();
if ($this->revisionDataTable) {
$data_fields = array_diff_key($table_mapping->getFieldNames($this->revisionDataTable), $table_mapping->getFieldNames($this->baseTable));
$data_fields = array_diff($table_mapping->getFieldNames($this->revisionDataTable), $table_mapping->getFieldNames($this->baseTable));
}
else {
$data_fields = $table_mapping->getFieldNames($this->dataTable);
......@@ -1150,7 +1211,7 @@ protected function loadFieldItems(array $entities) {
$definitions[$bundle] = $this->entityManager->getFieldDefinitions($this->entityTypeId, $bundle);
foreach ($definitions[$bundle] as $field_name => $field_definition) {
$storage_definition = $field_definition->getFieldStorageDefinition();
if ($this->usesDedicatedTable($storage_definition)) {
if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
$storage_definitions[$field_name] = $storage_definition;
}
}
......@@ -1225,7 +1286,7 @@ protected function saveFieldItems(EntityInterface $entity, $update = TRUE) {
foreach ($this->entityManager->getFieldDefinitions($entity_type, $bundle) as $field_name => $field_definition) {
$storage_definition = $field_definition->getFieldStorageDefinition();
if (!$this->usesDedicatedTable($storage_definition)) {
if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
continue;
}
$table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
......@@ -1240,11 +1301,13 @@ protected function saveFieldItems(EntityInterface $entity, $update = TRUE) {
->condition('entity_id', $id)
->execute();
}
if ($this->entityType->isRevisionable()) {
$this->database->delete($revision_name)
->condition('entity_id', $id)
->condition('revision_id', $vid)
->execute();
}
}
// Prepare the multi-insert query.
$do_insert = FALSE;
......@@ -1253,7 +1316,9 @@ protected function saveFieldItems(EntityInterface $entity, $update = TRUE) {
$columns[] = $table_mapping->getFieldColumnName($storage_definition, $column);
}
$query = $this->database->insert($table_name)->fields($columns);
if ($this->entityType->isRevisionable()) {
$revision_query = $this->database->insert($revision_name)->fields($columns);
}
$langcodes = $field_definition->isTranslatable() ? $translation_langcodes : array($default_langcode);
foreach ($langcodes as $langcode) {
......@@ -1276,7 +1341,9 @@ protected function saveFieldItems(EntityInterface $entity, $update = TRUE) {
$record[$column_name] = !empty($attributes['serialize']) ? serialize($item->$column) : $item->$column;
}
$query->values($record);
if ($this->entityType->isRevisionable()) {
$revision_query->values($record);
}
if ($storage_definition->getCardinality() != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && ++$delta_count == $storage_definition->getCardinality()) {
break;
......@@ -1291,10 +1358,12 @@ protected function saveFieldItems(EntityInterface $entity, $update = TRUE) {
if ($entity->isDefaultRevision()) {
$query->execute();
}
if ($this->entityType->isRevisionable()) {
$revision_query->execute();
}
}
}
}
/**
* Deletes values of configurable fields for all revisions of an entity.
......@@ -1306,7 +1375,7 @@ protected function deleteFieldItems(EntityInterface $entity) {
$table_mapping = $this->getTableMapping();
foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) {
$storage_definition = $field_definition->getFieldStorageDefinition();
if (!$this->usesDedicatedTable($storage_definition)) {
if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
continue;
}
$table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
......@@ -1314,11 +1383,13 @@ protected function deleteFieldItems(EntityInterface $entity) {
$this->database->delete($table_name)
->condition('entity_id', $entity->id())
->execute();
if ($this->entityType->isRevisionable()) {
$this->database->delete($revision_name)
->condition('entity_id', $entity->id())
->execute();
}
}
}
/**
* Deletes values of configurable fields for a single revision of an entity.
......@@ -1332,7 +1403,7 @@ protected function deleteFieldItemsRevision(EntityInterface $entity) {
$table_mapping = $this->getTableMapping();
foreach ($this->entityManager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle()) as $field_definition) {
$storage_definition = $field_definition->getFieldStorageDefinition();
if (!$this->usesDedicatedTable($storage_definition)) {
if (!$table_mapping->requiresDedicatedTableStorage($storage_definition)) {
continue;
}
$revision_name = $table_mapping->getDedicatedRevisionTableName($storage_definition);
......@@ -1344,185 +1415,109 @@ protected function deleteFieldItemsRevision(EntityInterface $entity) {
}
}
/**
* Returns whether the field uses a dedicated table for storage.
*
* @param FieldStorageDefinitionInterface $definition
* The field storage definition.
*
* @return bool
* Whether the field uses a dedicated table for storage.
*/
protected function usesDedicatedTable(FieldStorageDefinitionInterface $definition) {
// Everything that is not provided by the entity type is stored in a
// dedicated table.
return $definition->getProvider() != $this->entityType->getProvider() && !$definition->hasCustomStorage();
}
/**
* {@inheritdoc}
*/
public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
return $this->schemaHandler()->requiresEntityStorageSchemaChanges($entity_type, $original);
return $this->getStorageSchema()->requiresEntityStorageSchemaChanges($entity_type, $original);
}
/**
* {@inheritdoc}
*/
public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
return $this->schemaHandler()->requiresFieldStorageSchemaChanges($storage_definition, $original);
return $this->getStorageSchema()->requiresFieldStorageSchemaChanges($storage_definition, $original);
}
/**
* {@inheritdoc}
*/
public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
return $this->schemaHandler()->requiresEntityDataMigration($entity_type, $original);
return $this->getStorageSchema()->requiresEntityDataMigration($entity_type, $original);
}
/**
* {@inheritdoc}
*/
public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
return $this->schemaHandler()->requiresFieldDataMigration($storage_definition, $original);
return $this->getStorageSchema()->requiresFieldDataMigration($storage_definition, $original);
}
/**
* {@inheritdoc}
*/
public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
$this->schemaHandler()->onEntityTypeCreate($entity_type);
$this->getStorageSchema()->onEntityTypeCreate($entity_type);
}
/**
* {@inheritdoc}
*/
public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
$this->schemaHandler()->onEntityTypeUpdate($entity_type, $original);
// Ensure we have an updated entity type definition.
$this->entityType = $entity_type;
// The table layout may have changed depending on the new entity type
// definition.
$this->initTableLayout();
// Let the schema handler adapt to possible table layout changes.
$this->getStorageSchema()->onEntityTypeUpdate($entity_type, $original);
}
/**
* {@inheritdoc}
*/
public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
$this->schemaHandler()->onEntityTypeDelete($entity_type);
$this->getStorageSchema()->onEntityTypeDelete($entity_type);
}
/**
* {@inheritdoc}
*/
public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
$schema = $this->_fieldSqlSchema($storage_definition);
foreach ($schema as $name => $table) {
$this->database->schema()->createTable($name, $table);
// If we are adding a field stored in a shared table we need to recompute
// the table mapping.
// @todo This does not belong here. Remove it once we are able to generate a
// fresh table mapping in the schema handler. See
// https://www.drupal.org/node/2274017.
if ($this->getTableMapping()->allowsSharedTableStorage($storage_definition)) {
$this->tableMapping = NULL;
}
$this->getStorageSchema()->onFieldStorageDefinitionCreate($storage_definition);
}
/**
* {@inheritdoc}
*/
public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
if (!$storage_definition->hasData()) {
// There is no data. Re-create the tables completely.
if ($this->database->supportsTransactionalDDL()) {
// If the database supports transactional DDL, we can go ahead and rely
// on it. If not, we will have to rollback manually if something fails.
$transaction = $this->database->startTransaction();
}
try {
$original_schema = $this->_fieldSqlSchema($original);
foreach ($original_schema as $name => $table) {
$this->database->schema()->dropTable($name, $table);
}
$schema = $this->_fieldSqlSchema($storage_definition);
foreach ($schema as $name => $table) {
$this->database->schema()->createTable($name, $table);
}
}
catch (\Exception $e) {
if ($this->database->supportsTransactionalDDL()) {
$transaction->rollback();