From a17303166447173af60d8d4b3fbfee37f098a692 Mon Sep 17 00:00:00 2001 From: Alex Pott <alex.a.pott@googlemail.com> Date: Thu, 3 Sep 2015 12:59:34 +0100 Subject: [PATCH] Issue #2561129 by plach, alexpott, catch: Composite indexes are not correctly deleted/re-created when updating a field storage definition --- .../Core/Database/Driver/pgsql/Schema.php | 23 ++- .../Sql/SqlContentEntityStorageSchema.php | 181 +++++++++++++----- .../Entity/EntityDefinitionUpdateTest.php | 12 ++ .../Driver/pgsql/PostgresqlSchemaTest.php | 81 ++++++++ .../Sql/SqlContentEntityStorageSchemaTest.php | 4 +- 5 files changed, 251 insertions(+), 50 deletions(-) create mode 100644 core/tests/Drupal/Tests/Core/Database/Driver/pgsql/PostgresqlSchemaTest.php diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php index 06b754035647..7e1e0b77ac94 100644 --- a/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php @@ -588,13 +588,28 @@ public function indexExists($table, $name) { /** * Helper function: check if a constraint (PK, FK, UK) exists. * - * @param $table + * @param string $table * The name of the table. - * @param $name - * The name of the constraint (typically 'pkey' or '[constraint]_key'). + * @param string $name + * The name of the constraint (typically 'pkey' or '[constraint]__key'). + * + * @return bool + * TRUE if the constraint exists, FALSE otherwise. */ public function constraintExists($table, $name) { - $constraint_name = $this->ensureIdentifiersLength($table, $name); + // ::ensureIdentifiersLength() expects three parameters, although not + // explicitly stated in its signature, thus we split our constraint name in + // a proper name and a suffix. + if ($name == 'pkey') { + $suffix = $name; + $name = ''; + } + else { + $pos = strrpos($name, '__'); + $suffix = substr($name, $pos + 2); + $name = substr($name, 0, $pos); + } + $constraint_name = $this->ensureIdentifiersLength($table, $name, $suffix); // Remove leading and trailing quotes because the index name is in a WHERE // clause and not used as an identifier. $constraint_name = str_replace('"', '', $constraint_name); diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php index 9adffbd8182a..729de1a96933 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php @@ -328,42 +328,12 @@ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeI } } else { - $schema_handler = $this->database->schema(); - // Drop original indexes and unique keys. - foreach ($this->loadEntitySchemaData($entity_type) as $table_name => $schema) { - if (!empty($schema['indexes'])) { - foreach ($schema['indexes'] as $name => $specifier) { - $schema_handler->dropIndex($table_name, $name); - } - } - if (!empty($schema['unique keys'])) { - foreach ($schema['unique keys'] as $name => $specifier) { - $schema_handler->dropUniqueKey($table_name, $name); - } - } - } + $this->deleteEntitySchemaIndexes($this->loadEntitySchemaData($entity_type)); // Create new indexes and unique keys. $entity_schema = $this->getEntitySchema($entity_type, TRUE); - foreach ($this->getEntitySchemaData($entity_type, $entity_schema) as $table_name => $schema) { - // Add fields schema because database driver may depend on this data to - // perform index normalization. - $schema['fields'] = $entity_schema[$table_name]['fields']; - - if (!empty($schema['indexes'])) { - foreach ($schema['indexes'] as $name => $specifier) { - // Check if the index exists because it might already have been - // created as part of the earlier entity type update event. - $this->addIndexIfNotExists($table_name, $name, $specifier, $schema); - } - } - if (!empty($schema['unique keys'])) { - foreach ($schema['unique keys'] as $name => $specifier) { - $schema_handler->addUniqueKey($table_name, $name, $specifier); - } - } - } + $this->createEntitySchemaIndexes($entity_schema); // Store the updated entity schema. $this->saveEntitySchemaData($entity_type, $entity_schema); @@ -1161,7 +1131,7 @@ protected function createSharedTableSchema(FieldStorageDefinitionInterface $stor foreach ($schema[$table_name]['indexes'] as $name => $specifier) { // Check if the index exists because it might already have been // created as part of the earlier entity type update event. - $this->addIndexIfNotExists($table_name, $name, $specifier, $schema[$table_name]); + $this->addIndex($table_name, $name, $specifier, $schema[$table_name]); } } if (!empty($schema[$table_name]['unique keys'])) { @@ -1175,7 +1145,14 @@ protected function createSharedTableSchema(FieldStorageDefinitionInterface $stor } } } + $this->saveFieldSchemaData($storage_definition, $schema); + + if (!$only_save) { + // Make sure any entity index involving this field is re-created if + // needed. + $this->createEntitySchemaIndexes($this->getEntitySchema($this->entityType), $storage_definition); + } } /** @@ -1206,6 +1183,9 @@ protected function deleteDedicatedTableSchema(FieldStorageDefinitionInterface $s * The storage definition of the field being deleted. */ protected function deleteSharedTableSchema(FieldStorageDefinitionInterface $storage_definition) { + // Make sure any entity index involving this field is dropped. + $this->deleteEntitySchemaIndexes($this->loadEntitySchemaData($this->entityType), $storage_definition); + $deleted_field_name = $storage_definition->getName(); $table_mapping = $this->storage->getTableMapping( $this->entityManager->getLastInstalledFieldStorageDefinitions($this->entityType->id()) @@ -1329,8 +1309,8 @@ protected function updateDedicatedTableSchema(FieldStorageDefinitionInterface $s } // Check if the index exists because it might already have been // created as part of the earlier entity type update event. - $this->addIndexIfNotExists($table, $real_name, $real_columns, $actual_schema[$table]); - $this->addIndexIfNotExists($revision_table, $real_name, $real_columns, $actual_schema[$revision_table]); + $this->addIndex($table, $real_name, $real_columns, $actual_schema[$table]); + $this->addIndex($revision_table, $real_name, $real_columns, $actual_schema[$revision_table]); } } $this->saveFieldSchemaData($storage_definition, $this->getDedicatedTableSchema($storage_definition)); @@ -1423,7 +1403,7 @@ protected function updateSharedTableSchema(FieldStorageDefinitionInterface $stor foreach ($schema[$table_name]['indexes'] as $name => $specifier) { // Check if the index exists because it might already have been // created as part of the earlier entity type update event. - $this->addIndexIfNotExists($table_name, $name, $specifier, $schema[$table_name]); + $this->addIndex($table_name, $name, $specifier, $schema[$table_name]); } } @@ -1441,6 +1421,100 @@ protected function updateSharedTableSchema(FieldStorageDefinitionInterface $stor } } + /** + * Creates the specified entity schema indexes and keys. + * + * @param array $entity_schema + * The entity schema definition. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface|NULL $storage_definition + * (optional) If a field storage definition is specified, only indexes and + * keys involving its columns will be processed. Otherwise all defined + * entity indexes and keys will be processed. + */ + protected function createEntitySchemaIndexes(array $entity_schema, FieldStorageDefinitionInterface $storage_definition = NULL) { + $schema_handler = $this->database->schema(); + + if ($storage_definition) { + $table_mapping = $this->storage->getTableMapping(); + $column_names = $table_mapping->getColumnNames($storage_definition->getName()); + } + + $index_keys = [ + 'indexes' => 'addIndex', + 'unique keys' => 'addUniqueKey', + ]; + + foreach ($this->getEntitySchemaData($this->entityType, $entity_schema) as $table_name => $schema) { + // Add fields schema because database driver may depend on this data to + // perform index normalization. + $schema['fields'] = $entity_schema[$table_name]['fields']; + + foreach ($index_keys as $key => $add_method) { + if (!empty($schema[$key])) { + foreach ($schema[$key] as $name => $specifier) { + // If a set of field columns were specified we process only indexes + // involving them. Only indexes for which all columns exist are + // actually created. + $create = FALSE; + $specifier_columns = array_map(function($item) { return is_string($item) ? $item : reset($item); }, $specifier); + if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) { + $create = TRUE; + foreach ($specifier_columns as $specifier_column_name) { + // This may happen when adding more than one field in the same + // update run. Eventually when all field columns have been + // created the index will be created too. + if (!$schema_handler->fieldExists($table_name, $specifier_column_name)) { + $create = FALSE; + break; + } + } + } + if ($create) { + $this->{$add_method}($table_name, $name, $specifier, $schema); + } + } + } + } + } + } + + /** + * Deletes the specified entity schema indexes and keys. + * + * @param array $entity_schema_data + * The entity schema data definition. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface|NULL $storage_definition + * (optional) If a field storage definition is specified, only indexes and + * keys involving its columns will be processed. Otherwise all defined + * entity indexes and keys will be processed. + */ + protected function deleteEntitySchemaIndexes(array $entity_schema_data, FieldStorageDefinitionInterface $storage_definition = NULL) { + $schema_handler = $this->database->schema(); + + if ($storage_definition) { + $table_mapping = $this->storage->getTableMapping(); + $column_names = $table_mapping->getColumnNames($storage_definition->getName()); + } + + $index_keys = [ + 'indexes' => 'dropIndex', + 'unique keys' => 'dropUniqueKey', + ]; + + foreach ($entity_schema_data as $table_name => $schema) { + foreach ($index_keys as $key => $drop_method) { + if (!empty($schema[$key])) { + foreach ($schema[$key] as $name => $specifier) { + $specifier_columns = array_map(function($item) { return is_string($item) ? $item : reset($item); }, $specifier); + if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) { + $schema_handler->{$drop_method}($table_name, $name); + } + } + } + } + } + } + /** * Checks whether a field property has NULL values. * @@ -1873,24 +1947,41 @@ protected function getColumnSchemaRelevantKeys() { } /** - * Create an index if it doesn't already exist. + * Creates an index, dropping it if already existing. * * @param string $table * The table name. * @param string $name * The index name. - * @param array $fields + * @param array $specifier * The fields to index. - * @param array $spec + * @param array $schema * The table specification. * - * For the full parameter descriptions see - * \Drupal\Core\Database\Schema::addIndex(). + * @see \Drupal\Core\Database\Schema::addIndex() */ - protected function addIndexIfNotExists($table, $name, array $fields, array $spec) { - if (!$this->database->schema()->indexExists($table, $name)) { - $this->database->schema()->addIndex($table, $name, $fields, $spec); - } + protected function addIndex($table, $name, array $specifier, array $schema) { + $schema_handler = $this->database->schema(); + $schema_handler->dropIndex($table, $name); + $schema_handler->addIndex($table, $name, $specifier, $schema); + } + + /** + * Creates a unique key, dropping it if already existing. + * + * @param string $table + * The table name. + * @param string $name + * The index name. + * @param array $specifier + * The unique fields. + * + * @see \Drupal\Core\Database\Schema::addUniqueKey() + */ + protected function addUniqueKey($table, $name, array $specifier) { + $schema_handler = $this->database->schema(); + $schema_handler->dropUniqueKey($table, $name); + $schema_handler->addUniqueKey($table, $name, $specifier); } } diff --git a/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php b/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php index a1980854f33f..0492b2f9be9d 100644 --- a/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php +++ b/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php @@ -509,6 +509,18 @@ public function testEntityIndexCreateDeleteWithoutData() { // Run the update and ensure the index is deleted. $this->entityDefinitionUpdateManager->applyUpdates(); $this->assertFalse($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index deleted.'); + + // Test that composite indexes are handled correctly when dropping and + // re-creating one of their columns. + $this->addEntityIndex(); + $this->entityDefinitionUpdateManager->applyUpdates(); + $storage_definition = $this->entityDefinitionUpdateManager->getFieldStorageDefinition('name', 'entity_test_update'); + $this->entityDefinitionUpdateManager->updateFieldStorageDefinition($storage_definition); + $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created.'); + $this->entityDefinitionUpdateManager->uninstallFieldStorageDefinition($storage_definition); + $this->assertFalse($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index deleted.'); + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('name', 'entity_test_update', 'entity_test', $storage_definition); + $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created again.'); } /** diff --git a/core/tests/Drupal/Tests/Core/Database/Driver/pgsql/PostgresqlSchemaTest.php b/core/tests/Drupal/Tests/Core/Database/Driver/pgsql/PostgresqlSchemaTest.php new file mode 100644 index 000000000000..2d896466344b --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Database/Driver/pgsql/PostgresqlSchemaTest.php @@ -0,0 +1,81 @@ +<?php +/** + * @file + * Contains \Drupal\Tests\Core\Database\Driver\pgsql\PostgresqlSchemaTest. + */ + +namespace Drupal\Tests\Core\Database\Driver\pgsql; + +use Drupal\Core\Database\Driver\pgsql\Schema; +use Drupal\Tests\UnitTestCase; + +/** + * @coversDefaultClass \Drupal\Core\Database\Driver\pgsql\Schema + * @group Database + */ +class PostgresqlSchemaTest extends UnitTestCase { + + /** + * The PostgreSql DB connection. + * + * @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\Database\Driver\pgsql\Connection + */ + protected $connection; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->connection = $this->getMockBuilder('\Drupal\Core\Database\Driver\pgsql\Connection') + ->disableOriginalConstructor() + ->getMock(); + } + + /** + * Tests whether the actual constraint name is correctly computed. + * + * @param string $table_name + * The table name the constrained column belongs to. + * @param string $name + * The constraint name. + * @param string $expected + * The expected computed constraint name. + * + * @covers ::constraintExists + * @dataProvider providerComputedConstraintName + */ + public function testComputedConstraintName($table_name, $name, $expected) { + $max_identifier_length = 63; + $schema = new Schema($this->connection); + + $statement = $this->getMock('\Drupal\Core\Database\StatementInterface'); + $statement->expects($this->any()) + ->method('fetchField') + ->willReturn($max_identifier_length); + + $this->connection->expects($this->any()) + ->method('query') + ->willReturn($statement); + + $this->connection->expects($this->at(2)) + ->method('query') + ->with("SELECT 1 FROM pg_constraint WHERE conname = '$expected'") + ->willReturn($this->getMock('\Drupal\Core\Database\StatementInterface')); + + $schema->constraintExists($table_name, $name); + } + + /** + * Data provider for ::testComputedConstraintName(). + */ + public function providerComputedConstraintName() { + return [ + ['user_field_data', 'pkey', 'user_field_data____pkey'], + ['user_field_data', 'name__key', 'user_field_data__name__key'], + ['user_field_data', 'a_veeeery_veery_very_super_long_field_name__key', 'drupal_BGGYAXgbqlAF1rMOyFTdZGj9zIMXZtSvEjMAKZ9wGIk_key'], + ]; + } + +} diff --git a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageSchemaTest.php b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageSchemaTest.php index 83e9e50169a6..84ec3f22fb18 100644 --- a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageSchemaTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageSchemaTest.php @@ -1395,7 +1395,6 @@ public function setUpStorageDefinition($field_name, array $schema) { * ::onEntityTypeUpdate */ public function testonEntityTypeUpdateWithNewIndex() { - $entity_type_id = 'entity_test'; $this->entityType = $original_entity_type = new ContentEntityType(array( 'id' => 'entity_test', 'entity_keys' => array('id' => 'id'), @@ -1465,6 +1464,9 @@ public function testonEntityTypeUpdateWithNewIndex() { ->method('dropIndex') ->with('entity_test', 'entity_test__removed_field'); + $this->dbSchemaHandler->expects($this->atLeastOnce()) + ->method('fieldExists') + ->willReturn(TRUE); $this->dbSchemaHandler->expects($this->atLeastOnce()) ->method('addIndex') ->with('entity_test', 'entity_test__b588603cb9', [['long_index_name', 10]], $this->callback(function($actual_value) use ($expected) { -- GitLab