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;
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.
*/
protected $installedStorageSchema;
+ /**
+ * The deleted fields repository.
+ *
+ * @var \Drupal\Core\Field\DeletedFieldsRepositoryInterface
+ */
+ protected $deletedFieldsRepository;
+
/**
* Constructs a SqlContentEntityStorageSchema.
*
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}
*/
* 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)) {
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
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.
$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);
}
}
$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.
*
* 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);
}