X-Git-Url: http://www.aleph1.co.uk/gitweb/?p=yaffs-website;a=blobdiff_plain;f=web%2Fcore%2Flib%2FDrupal%2FCore%2FEntity%2FSql%2FSqlContentEntityStorageSchema.php;fp=web%2Fcore%2Flib%2FDrupal%2FCore%2FEntity%2FSql%2FSqlContentEntityStorageSchema.php;h=3349ebd30da4a048e319acd393e40233c75db4e7;hp=9797e7bb731ffb691ad53101f53311dfef441f2d;hb=af6d1fb995500ae68849458ee10d66abbdcfb252;hpb=680c79a86e3ed402f263faeac92e89fb6d9edcc0 diff --git a/web/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/web/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php index 9797e7bb7..3349ebd30 100644 --- a/web/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php +++ b/web/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php @@ -3,7 +3,7 @@ namespace Drupal\Core\Entity\Sql; use Drupal\Core\Database\Connection; -use Drupal\Core\Database\DatabaseException; +use Drupal\Core\Database\DatabaseExceptionWrapper; use Drupal\Core\DependencyInjection\DependencySerializationTrait; use Drupal\Core\Entity\ContentEntityTypeInterface; use Drupal\Core\Entity\EntityManagerInterface; @@ -15,7 +15,7 @@ use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Field\FieldException; use Drupal\Core\Field\FieldStorageDefinitionInterface; -use Drupal\field\FieldStorageConfigInterface; +use Drupal\Core\Language\LanguageInterface; /** * Defines a schema handler that supports revisionable, translatable entities. @@ -86,6 +86,13 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage */ protected $installedStorageSchema; + /** + * The deleted fields repository. + * + * @var \Drupal\Core\Field\DeletedFieldsRepositoryInterface + */ + protected $deletedFieldsRepository; + /** * Constructs a SqlContentEntityStorageSchema. * @@ -122,6 +129,23 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage return $this->installedStorageSchema; } + /** + * Gets the deleted fields repository. + * + * @return \Drupal\Core\Field\DeletedFieldsRepositoryInterface + * The deleted fields repository. + * + * @todo Inject this dependency in the constructor once this class can be + * instantiated as a regular entity handler: + * https://www.drupal.org/node/2332857. + */ + protected function deletedFieldsRepository() { + if (!isset($this->deletedFieldsRepository)) { + $this->deletedFieldsRepository = \Drupal::service('entity_field.deleted_fields_repository'); + } + return $this->deletedFieldsRepository; + } + /** * {@inheritdoc} */ @@ -221,7 +245,7 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage * The schema data. */ protected function getSchemaFromStorageDefinition(FieldStorageDefinitionInterface $storage_definition) { - assert('!$storage_definition->hasCustomStorage();'); + assert(!$storage_definition->hasCustomStorage()); $table_mapping = $this->storage->getTableMapping(); $schema = []; if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { @@ -412,25 +436,17 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { // Store original definitions so that switching between shared and dedicated // field table layout works. - $this->originalDefinitions = $this->fieldStorageDefinitions; - $this->originalDefinitions[$original->getName()] = $original; $this->performFieldSchemaOperation('update', $storage_definition, $original); - $this->originalDefinitions = NULL; } /** * {@inheritdoc} */ public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) { - // Only configurable fields currently support purging, so prevent deletion - // of ones we can't purge if they have existing data. - // @todo Add purging to all fields: https://www.drupal.org/node/2282119. try { - if (!($storage_definition instanceof FieldStorageConfigInterface) && $this->storage->countFieldData($storage_definition, TRUE)) { - throw new FieldStorageDefinitionUpdateForbiddenException('Unable to delete a field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data that cannot be purged.'); - } + $has_data = $this->storage->countFieldData($storage_definition, TRUE); } - catch (DatabaseException $e) { + catch (DatabaseExceptionWrapper $e) { // This may happen when changing field storage schema, since we are not // able to use a table mapping matching the passed storage definition. // @todo Revisit this once we are able to instantiate the table mapping @@ -438,10 +454,23 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage return; } + // If the field storage does not have any data, we can safely delete its + // schema. + if (!$has_data) { + $this->performFieldSchemaOperation('delete', $storage_definition); + return; + } + + // There's nothing else we can do if the field storage has a custom storage. + if ($storage_definition->hasCustomStorage()) { + return; + } + // Retrieve a table mapping which contains the deleted field still. - $table_mapping = $this->storage->getTableMapping( - $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id()) - ); + $storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id()); + $table_mapping = $this->storage->getTableMapping($storage_definitions); + $field_table_name = $table_mapping->getFieldTableName($storage_definition->getName()); + if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { // Move the table to a unique name while the table contents are being // deleted. @@ -454,11 +483,75 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage $this->database->schema()->renameTable($revision_table, $revision_new_table); } } + else { + // Move the field data from the shared table to a dedicated one in order + // to allow it to be purged like any other field. + $shared_table_field_columns = $table_mapping->getColumnNames($storage_definition->getName()); - // @todo Remove when finalizePurge() is invoked from the outside for all - // fields: https://www.drupal.org/node/2282119. - if (!($storage_definition instanceof FieldStorageConfigInterface)) { - $this->performFieldSchemaOperation('delete', $storage_definition); + // Refresh the table mapping to use the deleted storage definition. + $deleted_storage_definition = $this->deletedFieldsRepository()->getFieldStorageDefinitions()[$storage_definition->getUniqueStorageIdentifier()]; + $original_storage_definitions = [$storage_definition->getName() => $deleted_storage_definition] + $storage_definitions; + $table_mapping = $this->storage->getTableMapping($original_storage_definitions); + + $dedicated_table_field_schema = $this->getDedicatedTableSchema($deleted_storage_definition); + $dedicated_table_field_columns = $table_mapping->getColumnNames($deleted_storage_definition->getName()); + + $dedicated_table_name = $table_mapping->getDedicatedDataTableName($deleted_storage_definition, TRUE); + $dedicated_table_name_mapping[$table_mapping->getDedicatedDataTableName($deleted_storage_definition)] = $dedicated_table_name; + if ($this->entityType->isRevisionable()) { + $dedicated_revision_table_name = $table_mapping->getDedicatedRevisionTableName($deleted_storage_definition, TRUE); + $dedicated_table_name_mapping[$table_mapping->getDedicatedRevisionTableName($deleted_storage_definition)] = $dedicated_revision_table_name; + } + + // Create the dedicated field tables using "deleted" table names. + foreach ($dedicated_table_field_schema as $name => $table) { + if (!$this->database->schema()->tableExists($dedicated_table_name_mapping[$name])) { + $this->database->schema()->createTable($dedicated_table_name_mapping[$name], $table); + } + else { + throw new EntityStorageException('The field ' . $storage_definition->getName() . ' has already been deleted and it is in the process of being purged.'); + } + } + + 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 { + // Copy the data from the base table. + $this->database->insert($dedicated_table_name) + ->from($this->getSelectQueryForFieldStorageDeletion($field_table_name, $shared_table_field_columns, $dedicated_table_field_columns)) + ->execute(); + + // Copy the data from the revision table. + if (isset($dedicated_revision_table_name)) { + if ($this->entityType->isTranslatable()) { + $revision_table = $storage_definition->isRevisionable() ? $this->storage->getRevisionDataTable() : $this->storage->getDataTable(); + } + else { + $revision_table = $storage_definition->isRevisionable() ? $this->storage->getRevisionTable() : $this->storage->getBaseTable(); + } + $this->database->insert($dedicated_revision_table_name) + ->from($this->getSelectQueryForFieldStorageDeletion($revision_table, $shared_table_field_columns, $dedicated_table_field_columns, $field_table_name)) + ->execute(); + } + } + catch (\Exception $e) { + if (isset($transaction)) { + $transaction->rollBack(); + } + else { + // Delete the dedicated tables. + foreach ($dedicated_table_field_schema as $name => $table) { + $this->database->schema()->dropTable($dedicated_table_name_mapping[$name]); + } + } + throw $e; + } + + // Delete the field from the shared tables. + $this->deleteSharedTableSchema($storage_definition); } } @@ -469,6 +562,80 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage $this->performFieldSchemaOperation('delete', $storage_definition); } + /** + * Returns a SELECT query suitable for inserting data into a dedicated table. + * + * @param string $table_name + * The entity table name to select from. + * @param array $shared_table_field_columns + * An array of field column names for a shared table schema. + * @param array $dedicated_table_field_columns + * An array of field column names for a dedicated table schema. + * @param string $base_table + * (optional) The name of the base entity table. Defaults to NULL. + * + * @return \Drupal\Core\Database\Query\SelectInterface + * A database select query. + */ + protected function getSelectQueryForFieldStorageDeletion($table_name, array $shared_table_field_columns, array $dedicated_table_field_columns, $base_table = NULL) { + // Create a SELECT query that generates a result suitable for writing into + // a dedicated field table. + $select = $this->database->select($table_name, 'entity_table'); + + // Add the bundle column. + if ($bundle = $this->entityType->getKey('bundle')) { + if ($base_table) { + $select->join($base_table, 'base_table', "entity_table.{$this->entityType->getKey('id')} = %alias.{$this->entityType->getKey('id')}"); + $select->addField('base_table', $bundle, 'bundle'); + } + else { + $select->addField('entity_table', $bundle, 'bundle'); + } + } + else { + $select->addExpression(':bundle', 'bundle', [':bundle' => $this->entityType->id()]); + } + + // Add the deleted column. + $select->addExpression(':deleted', 'deleted', [':deleted' => 1]); + + // Add the entity_id column. + $select->addField('entity_table', $this->entityType->getKey('id'), 'entity_id'); + + // Add the revision_id column. + if ($this->entityType->isRevisionable()) { + $select->addField('entity_table', $this->entityType->getKey('revision'), 'revision_id'); + } + else { + $select->addField('entity_table', $this->entityType->getKey('id'), 'revision_id'); + } + + // Add the langcode column. + if ($langcode = $this->entityType->getKey('langcode')) { + $select->addField('entity_table', $langcode, 'langcode'); + } + else { + $select->addExpression(':langcode', 'langcode', [':langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED]); + } + + // Add the delta column and set it to 0 because we are only dealing with + // single cardinality fields. + $select->addExpression(':delta', 'delta', [':delta' => 0]); + + // Add all the dynamic field columns. + $or = $select->orConditionGroup(); + foreach ($shared_table_field_columns as $field_column_name => $schema_column_name) { + $select->addField('entity_table', $schema_column_name, $dedicated_table_field_columns[$field_column_name]); + $or->isNotNull('entity_table.' . $schema_column_name); + } + $select->condition($or); + + // Lock the table rows. + $select->forUpdate(TRUE); + + return $select; + } + /** * Checks that we are dealing with the correct entity type. * @@ -1233,17 +1400,13 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorage * The storage definition of the field being deleted. */ protected function deleteDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) { - // When switching from dedicated to shared field table layout we need need - // to delete the field tables with their regular names. When this happens - // original definitions will be defined. - $deleted = !$this->originalDefinitions; $table_mapping = $this->storage->getTableMapping(); - $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $deleted); + $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $storage_definition->isDeleted()); if ($this->database->schema()->tableExists($table_name)) { $this->database->schema()->dropTable($table_name); } if ($this->entityType->isRevisionable()) { - $revision_table_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $deleted); + $revision_table_name = $table_mapping->getDedicatedRevisionTableName($storage_definition, $storage_definition->isDeleted()); if ($this->database->schema()->tableExists($revision_table_name)) { $this->database->schema()->dropTable($revision_table_name); }