Commit 76fdebab authored by plach's avatar plach

Issue #2960136 by amateescu, tstoeckler, vijaycs85: Add the ability to use a...

Issue #2960136 by amateescu, tstoeckler, vijaycs85: Add the ability to use a prefix for all the tables in a table mapping
parent 543b3f4e
......@@ -3,6 +3,7 @@
namespace Drupal\Core\Entity\Sql;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
/**
......@@ -24,6 +25,13 @@ class DefaultTableMapping implements TableMappingInterface {
*/
protected $fieldStorageDefinitions = [];
/**
* The prefix to be used by all the tables of this mapping.
*
* @var string
*/
protected $prefix;
/**
* The base table of the entity.
*
......@@ -112,22 +120,26 @@ class DefaultTableMapping implements TableMappingInterface {
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions
* A list of field storage definitions that should be available for the
* field columns of this table mapping.
* @param string $prefix
* (optional) A prefix to be used by all the tables of this mapping.
* Defaults to an empty string.
*/
public function __construct(ContentEntityTypeInterface $entity_type, array $storage_definitions) {
public function __construct(ContentEntityTypeInterface $entity_type, array $storage_definitions, $prefix = '') {
$this->entityType = $entity_type;
$this->fieldStorageDefinitions = $storage_definitions;
$this->prefix = $prefix;
// @todo Remove table names from the entity type definition in
// https://www.drupal.org/node/2232465.
$this->baseTable = $entity_type->getBaseTable() ?: $entity_type->id();
$this->baseTable = $this->prefix . $entity_type->getBaseTable() ?: $entity_type->id();
if ($entity_type->isRevisionable()) {
$this->revisionTable = $entity_type->getRevisionTable() ?: $entity_type->id() . '_revision';
$this->revisionTable = $this->prefix . $entity_type->getRevisionTable() ?: $entity_type->id() . '_revision';
}
if ($entity_type->isTranslatable()) {
$this->dataTable = $entity_type->getDataTable() ?: $entity_type->id() . '_field_data';
$this->dataTable = $this->prefix . $entity_type->getDataTable() ?: $entity_type->id() . '_field_data';
}
if ($entity_type->isRevisionable() && $entity_type->isTranslatable()) {
$this->revisionDataTable = $entity_type->getRevisionDataTable() ?: $entity_type->id() . '_field_revision';
$this->revisionDataTable = $this->prefix . $entity_type->getRevisionDataTable() ?: $entity_type->id() . '_field_revision';
}
}
......@@ -139,13 +151,16 @@ public function __construct(ContentEntityTypeInterface $entity_type, array $stor
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions
* A list of field storage definitions that should be available for the
* field columns of this table mapping.
* @param string $prefix
* (optional) A prefix to be used by all the tables of this mapping.
* Defaults to an empty string.
*
* @return static
*
* @internal
*/
public static function create(ContentEntityTypeInterface $entity_type, array $storage_definitions) {
$table_mapping = new static($entity_type, $storage_definitions);
public static function create(ContentEntityTypeInterface $entity_type, array $storage_definitions, $prefix = '') {
$table_mapping = new static($entity_type, $storage_definitions, $prefix);
$revisionable = $entity_type->isRevisionable();
$translatable = $entity_type->isTranslatable();
......@@ -590,18 +605,30 @@ public function getDedicatedRevisionTableName(FieldStorageDefinitionInterface $s
* The final table name.
*/
protected function generateFieldTableName(FieldStorageDefinitionInterface $storage_definition, $revision) {
// The maximum length of an entity type ID is 32 characters.
$entity_type_id = substr($storage_definition->getTargetEntityTypeId(), 0, EntityTypeInterface::ID_MAX_LENGTH);
$separator = $revision ? '_revision__' : '__';
$table_name = $storage_definition->getTargetEntityTypeId() . $separator . $storage_definition->getName();
$table_name = $this->prefix . $entity_type_id . $separator . $storage_definition->getName();
// Limit the string to 48 characters, keeping a 16 characters margin for db
// prefixes.
if (strlen($table_name) > 48) {
// Use a shorter separator, a truncated entity_type, and a hash of the
// field storage unique identifier.
// Use a shorter separator and a hash of the field storage unique
// identifier.
$separator = $revision ? '_r__' : '__';
// Truncate to the same length for the current and revision tables.
$entity_type = substr($storage_definition->getTargetEntityTypeId(), 0, 34);
$field_hash = substr(hash('sha256', $storage_definition->getUniqueStorageIdentifier()), 0, 10);
$table_name = $entity_type . $separator . $field_hash;
$table_name = $this->prefix . $entity_type_id . $separator . $field_hash;
// If the resulting table name is still longer than 48 characters, use the
// following pattern:
// - prefix: max 34 chars;
// - separator: max 4 chars;
// - field_hash: max 10 chars.
if (strlen($table_name) > 48) {
$prefix = substr($this->prefix, 0, 34);
$table_name = $prefix . $separator . $field_hash;
}
}
return $table_name;
}
......
......@@ -336,15 +336,18 @@ public function getTableMapping(array $storage_definitions = NULL) {
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions
* An array of field storage definitions to be used to compute the table
* mapping.
* @param string $prefix
* (optional) A prefix to be used by all the tables of this mapping.
* Defaults to an empty string.
*
* @return \Drupal\Core\Entity\Sql\TableMappingInterface
* A table mapping object for the entity's tables.
*
* @internal
*/
public function getCustomTableMapping(ContentEntityTypeInterface $entity_type, array $storage_definitions) {
$table_mapping_class = $this->temporary ? TemporaryTableMapping::class : DefaultTableMapping::class;
return $table_mapping_class::create($entity_type, $storage_definitions);
public function getCustomTableMapping(ContentEntityTypeInterface $entity_type, array $storage_definitions, $prefix = '') {
$prefix = $prefix ?: ($this->temporary ? 'tmp_' : '');
return DefaultTableMapping::create($entity_type, $storage_definitions, $prefix);
}
/**
......
......@@ -149,23 +149,19 @@ public function convertToRevisionable(array &$sandbox, array $fields_to_update =
// Rename the original tables so we can put them back in place in case
// anything goes wrong.
foreach ($sandbox['original_table_mapping']->getTableNames() as $table_name) {
$old_table_name = TemporaryTableMapping::getTempTableName($table_name, 'old_');
$this->database->schema()->renameTable($table_name, $old_table_name);
$backup_table_names = array_combine($sandbox['original_table_mapping']->getTableNames(), $sandbox['backup_table_mapping']->getTableNames());
foreach ($backup_table_names as $original_table_name => $backup_table_name) {
$this->database->schema()->renameTable($original_table_name, $backup_table_name);
}
// Put the new tables in place and update the entity type and field
// storage definitions.
try {
$storage = $this->entityTypeManager->getStorage($this->entityTypeId);
$storage->setEntityType($actual_entity_type);
$storage->setTemporary(FALSE);
$actual_table_names = $storage->getTableMapping()->getTableNames();
$table_name_mapping = [];
foreach ($actual_table_names as $new_table_name) {
$temp_table_name = TemporaryTableMapping::getTempTableName($new_table_name);
$table_name_mapping[$temp_table_name] = $new_table_name;
$storage = $this->entityTypeManager->createHandlerInstance($actual_entity_type->getStorageClass(), $actual_entity_type);
$current_table_mapping = $storage->getCustomTableMapping($actual_entity_type, $sandbox['updated_storage_definitions']);
$table_name_mapping = array_combine($sandbox['temporary_table_mapping']->getTableNames(), $current_table_mapping->getTableNames());
foreach ($table_name_mapping as $temp_table_name => $new_table_name) {
$this->database->schema()->renameTable($temp_table_name, $new_table_name);
}
......@@ -201,15 +197,14 @@ public function convertToRevisionable(array &$sandbox, array $fields_to_update =
}
catch (\Exception $e) {
// Something went wrong, bring back the original tables.
foreach ($sandbox['original_table_mapping']->getTableNames() as $table_name) {
foreach ($backup_table_names as $original_table_name => $backup_table_name) {
// We are in the 'original data recovery' phase, so we need to be sure
// that the initial tables can be properly restored.
if ($this->database->schema()->tableExists($table_name)) {
$this->database->schema()->dropTable($table_name);
if ($this->database->schema()->tableExists($original_table_name)) {
$this->database->schema()->dropTable($original_table_name);
}
$old_table_name = TemporaryTableMapping::getTempTableName($table_name, 'old_');
$this->database->schema()->renameTable($old_table_name, $table_name);
$this->database->schema()->renameTable($backup_table_name, $original_table_name);
}
// Re-throw the original exception.
......@@ -219,9 +214,8 @@ public function convertToRevisionable(array &$sandbox, array $fields_to_update =
// At this point the update process either finished successfully or any
// error has been handled already, so we can drop the backup entity
// tables.
foreach ($sandbox['original_table_mapping']->getTableNames() as $table_name) {
$old_table_name = TemporaryTableMapping::getTempTableName($table_name, 'old_');
$this->database->schema()->dropTable($old_table_name);
foreach ($backup_table_names as $original_table_name => $backup_table_name) {
$this->database->schema()->dropTable($backup_table_name);
}
}
}
......@@ -236,7 +230,7 @@ public function convertToRevisionable(array &$sandbox, array $fields_to_update =
* Thrown in case of an error during the entity save process.
*/
protected function copyData(array &$sandbox) {
/** @var \Drupal\Core\Entity\Sql\TemporaryTableMapping $temporary_table_mapping */
/** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $temporary_table_mapping */
$temporary_table_mapping = $sandbox['temporary_table_mapping'];
$temporary_entity_type = $sandbox['temporary_entity_type'];
$original_table_mapping = $sandbox['original_table_mapping'];
......@@ -437,12 +431,13 @@ protected function collectOriginalDefinitions(array &$sandbox) {
/** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */
$storage = $this->entityTypeManager->getStorage($this->entityTypeId);
$storage->setEntityType($original_entity_type);
$original_table_mapping = $storage->getTableMapping($original_storage_definitions);
$original_table_mapping = $storage->getCustomTableMapping($original_entity_type, $original_storage_definitions);
$backup_table_mapping = $storage->getCustomTableMapping($original_entity_type, $original_storage_definitions, 'old_');
$sandbox['original_entity_type'] = $original_entity_type;
$sandbox['original_storage_definitions'] = $original_storage_definitions;
$sandbox['original_table_mapping'] = $original_table_mapping;
$sandbox['backup_table_mapping'] = $backup_table_mapping;
$sandbox['original_entity_schema_data'] = $this->installedStorageSchema->get($this->entityTypeId . '.entity_schema_data', []);
foreach ($original_storage_definitions as $storage_definition) {
......@@ -487,20 +482,11 @@ protected function createTemporaryDefinitions(array &$sandbox, array $fields_to_
$actual_entity_type = $this->entityTypeManager->getDefinition($this->entityTypeId);
$temporary_entity_type = clone $actual_entity_type;
$temporary_entity_type->set('base_table', TemporaryTableMapping::getTempTableName($temporary_entity_type->getBaseTable()));
$temporary_entity_type->set('revision_table', TemporaryTableMapping::getTempTableName($temporary_entity_type->getRevisionTable()));
if ($temporary_entity_type->isTranslatable()) {
$temporary_entity_type->set('data_table', TemporaryTableMapping::getTempTableName($temporary_entity_type->getDataTable()));
$temporary_entity_type->set('revision_data_table', TemporaryTableMapping::getTempTableName($temporary_entity_type->getRevisionDataTable()));
}
$updated_storage_definitions = $this->updateFieldStorageDefinitionsToRevisionable($temporary_entity_type, $sandbox['original_storage_definitions'], $fields_to_update, FALSE);
/** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */
$storage = $this->entityTypeManager->getStorage($this->entityTypeId);
$storage->setTemporary(TRUE);
$storage->setEntityType($temporary_entity_type);
$updated_storage_definitions = $this->updateFieldStorageDefinitionsToRevisionable($temporary_entity_type, $sandbox['original_storage_definitions'], $fields_to_update, FALSE);
$temporary_table_mapping = $storage->getTableMapping($updated_storage_definitions);
$temporary_table_mapping = $storage->getCustomTableMapping($temporary_entity_type, $updated_storage_definitions, 'tmp_');
$sandbox['temporary_entity_type'] = $temporary_entity_type;
$sandbox['temporary_table_mapping'] = $temporary_table_mapping;
......
......@@ -4,8 +4,13 @@
use Drupal\Core\Field\FieldStorageDefinitionInterface;
@trigger_error(__NAMESPACE__ . '\TemporaryTableMapping is deprecated in Drupal 8.6.x and will be removed before Drupal 9.0.0. Use the default table mapping with a prefix instead.', E_USER_DEPRECATED);
/**
* Defines a temporary table mapping class.
*
* @deprecated in Drupal 8.6.x and will be removed before Drupal 9.0.0. Use the
* default table mapping with a prefix instead.
*/
class TemporaryTableMapping extends DefaultTableMapping {
......
......@@ -18,7 +18,7 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res
$schema = parent::getEntitySchema($entity_type, $reset);
if ($entity_type->id() == 'entity_test_update') {
$schema[$entity_type->getBaseTable()]['indexes'] += \Drupal::state()->get('entity_test_update.additional_entity_indexes', []);
$schema[$this->storage->getTableMapping()->getBaseTable()]['indexes'] += \Drupal::state()->get('entity_test_update.additional_entity_indexes', []);
}
return $schema;
}
......
......@@ -2,7 +2,6 @@
namespace Drupal\Tests\system\Functional\Entity\Update;
use Drupal\Core\Entity\Sql\TemporaryTableMapping;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
use Drupal\Tests\system\Functional\Entity\Traits\EntityDefinitionTestTrait;
......@@ -67,10 +66,11 @@ protected function setUp() {
public function testMakeRevisionable() {
// Check that entity type is not revisionable prior to running the update
// process.
$entity_test_update = $this->lastInstalledSchemaRepository->getLastInstalledDefinition('entity_test_update');
$this->assertFalse($entity_test_update->isRevisionable());
$original_entity_type_definition = $this->lastInstalledSchemaRepository->getLastInstalledDefinition('entity_test_update');
$original_field_storage_definitions = $this->lastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions('entity_test_update');
$this->assertFalse($original_entity_type_definition->isRevisionable());
$translatable = $entity_test_update->isTranslatable();
$translatable = $original_entity_type_definition->isTranslatable();
// Make the entity type revisionable and run the updates.
if ($translatable) {
......@@ -156,14 +156,17 @@ public function testMakeRevisionable() {
// Check that temporary tables have been removed at the end of the process.
$schema = \Drupal::database()->schema();
foreach ($storage->getTableMapping()->getTableNames() as $table_name) {
$this->assertFalse($schema->tableExists(TemporaryTableMapping::getTempTableName($table_name)));
$temporary_table_names = $storage->getCustomTableMapping($entity_test_update, $field_storage_definitions, 'tmp_')->getTableNames();
$current_table_names = $storage->getCustomTableMapping($entity_test_update, $field_storage_definitions)->getTableNames();
foreach (array_combine($temporary_table_names, $current_table_names) as $temp_table_name => $table_name) {
$this->assertTrue($schema->tableExists($table_name));
$this->assertFalse($schema->tableExists($temp_table_name));
}
// Check that backup tables have been removed at the end of the process.
$schema = \Drupal::database()->schema();
foreach ($storage->getTableMapping()->getTableNames() as $table_name) {
$this->assertFalse($schema->tableExists(TemporaryTableMapping::getTempTableName($table_name, 'old_')));
$table_mapping = $storage->getCustomTableMapping($original_entity_type_definition, $original_field_storage_definitions, 'old_');
foreach ($table_mapping->getTableNames() as $table_name) {
$this->assertFalse($schema->tableExists($table_name));
}
}
......
......@@ -2,8 +2,6 @@
namespace Drupal\Tests\system\Functional\Entity\Update;
use Drupal\Core\Entity\Sql\TemporaryTableMapping;
/**
* Tests converting a translatable entity type with data to revisionable.
*
......@@ -79,8 +77,11 @@ public function testMakeRevisionableErrorHandling() {
// Check that temporary tables have been removed.
$schema = \Drupal::database()->schema();
foreach ($storage->getTableMapping()->getTableNames() as $table_name) {
$this->assertFalse($schema->tableExists(TemporaryTableMapping::getTempTableName($table_name)));
$temporary_table_names = $storage->getCustomTableMapping($new_entity_type, $new_storage_definitions, 'tmp_')->getTableNames();
$current_table_names = $storage->getCustomTableMapping($new_entity_type, $new_storage_definitions)->getTableNames();
foreach (array_combine($temporary_table_names, $current_table_names) as $temp_table_name => $table_name) {
$this->assertTrue($schema->tableExists($table_name));
$this->assertFalse($schema->tableExists($temp_table_name));
}
// Check that the original tables still exist and their data is intact.
......
......@@ -517,9 +517,9 @@ public function testTableNames() {
'field_name' => $field_name,
'type' => 'test_field',
]);
$expected = 'long_entity_type_abcdefghijklmnopq__' . substr(hash('sha256', $field_storage->uuid()), 0, 10);
$expected = 'long_entity_type_abcdefghijklmno__' . substr(hash('sha256', $field_storage->uuid()), 0, 10);
$this->assertEqual($this->tableMapping->getDedicatedDataTableName($field_storage), $expected);
$expected = 'long_entity_type_abcdefghijklmnopq_r__' . substr(hash('sha256', $field_storage->uuid()), 0, 10);
$expected = 'long_entity_type_abcdefghijklmno_r__' . substr(hash('sha256', $field_storage->uuid()), 0, 10);
$this->assertEqual($this->tableMapping->getDedicatedRevisionTableName($field_storage), $expected);
// Long entity type and field name.
......@@ -530,9 +530,9 @@ public function testTableNames() {
'field_name' => $field_name,
'type' => 'test_field',
]);
$expected = 'long_entity_type_abcdefghijklmnopq__' . substr(hash('sha256', $field_storage->uuid()), 0, 10);
$expected = 'long_entity_type_abcdefghijklmno__' . substr(hash('sha256', $field_storage->uuid()), 0, 10);
$this->assertEqual($this->tableMapping->getDedicatedDataTableName($field_storage), $expected);
$expected = 'long_entity_type_abcdefghijklmnopq_r__' . substr(hash('sha256', $field_storage->uuid()), 0, 10);
$expected = 'long_entity_type_abcdefghijklmno_r__' . substr(hash('sha256', $field_storage->uuid()), 0, 10);
$this->assertEqual($this->tableMapping->getDedicatedRevisionTableName($field_storage), $expected);
// Try creating a second field and check there are no clashes.
$field_storage2 = FieldStorageConfig::create([
......
......@@ -4,6 +4,7 @@
use Drupal\Core\Entity\Sql\DefaultTableMapping;
use Drupal\Core\Entity\Sql\SqlContentEntityStorageException;
use Drupal\Core\Entity\Sql\TemporaryTableMapping;
use Drupal\Tests\UnitTestCase;
/**
......@@ -444,6 +445,142 @@ public function testGetFieldTableNameInvalid() {
$table_mapping->getFieldTableName('invalid_field_name');
}
/**
* @covers ::getDedicatedDataTableName
* @covers ::getDedicatedRevisionTableName
*
* @dataProvider providerTestGetDedicatedTableName
*/
public function testGetDedicatedTableName($info, $expected_data_table, $expected_revision_table) {
$entity_type_id = $info['entity_type_id'];
$field_name = $info['field_name'];
$definition = $this->setUpDefinition($field_name, []);
$definition->expects($this->any())
->method('getTargetEntityTypeId')
->will($this->returnValue($entity_type_id));
$definition->expects($this->any())
->method('getUniqueStorageIdentifier')
->will($this->returnValue($entity_type_id . '-' . $field_name));
$this->entityType
->expects($this->any())
->method('getBaseTable')
->willReturn($info['entity_type_id']);
$this->entityType
->expects($this->any())
->method('isTranslatable')
->willReturn(FALSE);
$this->entityType
->expects($this->any())
->method('isRevisionable')
->willReturn(FALSE);
$table_mapping = new DefaultTableMapping($this->entityType, [], $info['prefix']);
$this->assertSame($expected_data_table, $table_mapping->getDedicatedDataTableName($definition));
$this->assertSame($expected_revision_table, $table_mapping->getDedicatedRevisionTableName($definition));
}
/**
* Provides test data for testGetDedicatedTableName().
*
* @return array[]
* A nested array where each inner array has the following values: an array
* consisting of the entity type ID, field name and a table prefix, followed
* by the expected data table name and the revision table name.
*/
public function providerTestGetDedicatedTableName() {
$data = [];
$data['short entity type; short field name; no prefix'] = [
[
'entity_type_id' => 'short_entity_type',
'field_name' => 'short_field_name',
'prefix' => '',
],
'short_entity_type__short_field_name',
'short_entity_type_revision__short_field_name',
];
$data['short entity type; long field name; no prefix'] = [
[
'entity_type_id' => 'short_entity_type',
'field_name' => 'long_field_name_abcdefghijklmnopqrstuvwxyz',
'prefix' => '',
],
'short_entity_type__28a01c7777',
'short_entity_type_r__28a01c7777',
];
$data['long entity type; short field name; no prefix'] = [
[
'entity_type_id' => 'long_entity_type_abcdefghijklmnopqrstuvwxyz',
'field_name' => 'short_field_name',
'prefix' => '',
],
'long_entity_type_abcdefghijklmno__a526e4e042',
'long_entity_type_abcdefghijklmno_r__a526e4e042',
];
$data['long entity type; long field name; no prefix'] = [
[
'entity_type_id' => 'long_entity_type_abcdefghijklmnopqrstuvwxyz',
'field_name' => 'long_field_name_abcdefghijklmnopqrstuvwxyz',
'prefix' => '',
],
'long_entity_type_abcdefghijklmno__7705d52d75',
'long_entity_type_abcdefghijklmno_r__7705d52d75',
];
$data['short entity type; short field name; with prefix'] = [
[
'entity_type_id' => 'short_entity_type',
'field_name' => 'short_field_name',
'prefix' => 'prefix_',
],
'prefix_short_entity_type__short_field_name',
'prefix_short_entity_type_r__a133cc765a',
];
$data['short entity type; long field name; with prefix'] = [
[
'entity_type_id' => 'short_entity_type',
'field_name' => 'long_field_name_abcdefghijklmnopqrstuvwxyz',
'prefix' => 'prefix_',
],
'prefix_short_entity_type__28a01c7777',
'prefix_short_entity_type_r__28a01c7777',
];
$data['long entity type; short field name; with prefix'] = [
[
'entity_type_id' => 'long_entity_type_abcdefghijklmnopqrstuvwxyz',
'field_name' => 'short_field_name',
'prefix' => 'prefix_',
],
'prefix___a526e4e042',
'prefix__r__a526e4e042',
];
$data['long entity type; long field name; with prefix'] = [
[
'entity_type_id' => 'long_entity_type_abcdefghijklmnopqrstuvwxyz',
'field_name' => 'long_field_name_abcdefghijklmnopqrstuvwxyz',
'prefix' => 'prefix_',
],
'prefix___7705d52d75',
'prefix__r__7705d52d75',
];
return $data;
}
/**
* @coversDefaultClass \Drupal\Core\Entity\Sql\TemporaryTableMapping
*
* @expectedDeprecation Drupal\Core\Entity\Sql\TemporaryTableMapping is deprecated in Drupal 8.6.x and will be removed before Drupal 9.0.0. Use the default table mapping with a prefix instead.
* @group legacy
*/
public function testTemporaryTableMapping() {
$table_mapping = new TemporaryTableMapping($this->entityType, [], '');
$this->assertTrue($table_mapping instanceof DefaultTableMapping);
}
/**
* Sets up a field storage definition for the test.
*
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment