Problem/Motivation

The following test failure is occurring on postgres. This is a newly added test, so it's likely a driver issue rather than a regression.

https://www.drupal.org/pift-ci-job/25521

fail: [Other] Line 43 of core/modules/system/src/Tests/Entity/Update/SqlContentEntityStorageSchemaIndexTest.php:
Index node__id__default_langcode__langcode properly created on the node_field_data table.

Tagging beta target because it would be good to confirm it really is a postgres driver issue and not a hidden upgrade path bug.

Proposed resolution

Drop and re-create composite indexes whenever a field involving them is dropped or created.

Remaining tasks

  • Validate the proposed solution
  • Code it
  • Reviews

User interface changes

None

API changes

None

Data model changes

None

CommentFileSizeAuthor
#30 entity-schema_composite_indexes-2561129-30.patch18.22 KBplach
#30 entity-schema_composite_indexes-2561129-30.interdiff.txt717 bytesplach
#19 entity-schema_composite_indexes-2561129-18.test.patch1.44 KBplach
#19 entity-schema_composite_indexes-2561129-18.interdiff.txt5.23 KBplach
#19 entity-schema_composite_indexes-2561129-18.patch14.78 KBplach
#15 entity-schema_composite_indexes-2561129-15.patch13.25 KBplach
#15 entity-schema_composite_indexes-2561129-15.interdiff.txt1.81 KBplach
#12 entity-schema_composite_indexes-2561129-11.patch12.98 KBplach
#22 entity-schema_composite_indexes-2561129-22.interdiff.txt3.32 KBplach
#22 entity-schema_composite_indexes-2561129-22.test.patch4.06 KBplach
#22 entity-schema_composite_indexes-2561129-22.patch17.84 KBplach
#25 entity-schema_composite_indexes-2561129-25.patch18.03 KBplach
#25 entity-schema_composite_indexes-2561129-25.interdiff.txt6.87 KBplach
#25 entity-schema_composite_indexes-2561129-24.interdiff.txt1.43 KBplach
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

catch created an issue. See original summary.

catch’s picture

Issue tags: +D8 upgrade path
alexpott’s picture

What's awesome is that Drupal\system\Tests\Entity\Update\SqlContentEntityStorageSchemaIndexFilledTest passes... so this is a difference between what occurs when the db is empty and when it is has data.

catch’s picture

Issue tags: +beta target
alexpott’s picture

In node_update_8002()...

  // Regenerate entity type indexes, this should drop "node__default_langcode".
  $manager->updateEntityType($manager->getEntityType('node'));
  // Regenerate "langcode" indexes, this should drop "node_field__langcode".
  $manager->updateFieldStorageDefinition($manager->getFieldStorageDefinition('langcode', 'node'));

So the index is created correctly during the first update but then deleted in the next one... weird and scary this is different between SQL engines.

effulgentsia’s picture

Those are different indexes. We're dropping 2 old indexes and creating one new one with more fields. The error message in the issue summary indicates that it is the index creation that is broken (in the presence of existing data). I don't have experience with Postgres to know if that's a common problem.

effulgentsia’s picture

Oh, unless you're saying that the new index, node__id__default_langcode__langcode, is being attempted to get created twice: once for each line of code in #5, and MySQL is more forgiving of that than Postgres? I haven't debugged to see if any of this true, just speculating based on what's written in #5.

alexpott’s picture

So the problem occurs because when $manager->updateFieldStorageDefinition($manager->getFieldStorageDefinition('langcode', 'node')); fires on an empty db it ends up dropping and recreating the langcode column. In postgres land this causes the index added on the entity level to be dropped because the column is part of the index. In mysql land this doesn't happen.

alexpott’s picture

I think what we need to do is to have something that checks that all the entity level indexes exist if EM::updateFieldStorageDefinition() is called. Discussed with @plach and @jhedstrom on IRC.

plach’s picture

Assigned: Unassigned » plach

@effulgentsia:

@alexpott found out what's going, I'm working on a fix. See the following chat:

alexpott - plach: it's a real head scratcher
plach - alexpott: good to know :)
plach - at least I was able to replicate the failure
alexpott - plach: it's really great that version with the filled database works :)
plach - alexpott: we have two different code paths, so it seems what's not working is dropping the entire schema and re-creating it, right?
alexpott - plach: that works
alexpott - plach: what is going wrong is  $manager->updateFieldStorageDefinition($manager->getFieldStorageDefinition('langcode', 'node'));
alexpott - plach: the schema is dropped and recreated in $manager->updateEntityType($manager->getEntityType('node')); as we expect
plach - ok - alexpott - plach: but then the next line of code messes it up
alexpott - plach: in node_update_8002
plach - alexpott: are you sure just the first line would be enough?
alexpott - plach: no idea... I just know that it is the second line that is breaking the test
alexpott - plach: and I know that the test's expectations are correct :)
alexpott - plach: my hunch would be that the second line is needed when we have data
alexpott - plach: but that is just a gut feeling
plach - I see
plach - alexpott: yep, commenting that line fixes the test
plach - and I bet breaks the filled test

alexpott - plach: still very weird that mysql is different
plach - ... and, yes, breaks the filled test
plach - alexpott: maybe dropping and re-creating the index is not exactly the same on PG
plach - but yes it's weird
plach - I'll manually test the upgrade
alexpott - plach: cause we could just wrap with a db_index_exists() check
plach - alexpott: we don't know whether the old spec if up-to-date
plach - * is
alexpott - plach: well $manager->updateEntityType($manager->getEntityType('node')); updates the installed definitions
plach - alexpott: ok, in this case, but in general?
plach has the feeling he already had this discussion
plach - jhedstrom: ;)
alexpott - plach: I know why...
alexpott - plach: postgres will delete the index when the language column is deleted and added again
alexpott - plach: i bet mysql doesn't
jhedstrom - alexpott: plach so can we wrap our drop index calls in an exists, the same way our add index calls are now wrapped?
alexpott - jhedstrom: I think so
jibran is now known as jibran|afk
alexpott - jhedstrom: although I think this hints at larger problems to come
alexpott - jhedstrom: this is going to be hard to spot
alexpott - plach, jhedstrom: yep this is exactly what is going on
plach - alexpott: so basically we should ensure the index is re-created rather than not dropping it
plach - jhedstrom: ^
alexpott - plach: yep
plach - otherwise hoe can we be sure it's the most current spec?
plach - when non preceded by a full entity schema update
plach - I mean
alexpott - plach: yep
plach - alexpott++
plach - great detective work
catch’s picture

Issue summary: View changes
plach’s picture

Title: Index creation on {node_field_data} fails during upgrade path on postgres » Composite indexes are not correctly deleted/re-created when updating a field storage definition
Component: postgresql db driver » entity system
Assigned: plach » Unassigned
Issue summary: View changes
Status: Active » Needs review
Issue tags: +entity storage, +D8 Accelerate
FileSize
12.98 KB

Let's see whether this works.

This probably needs some additional test coverage, for the PG-specific changes.

catch’s picture

+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
@@ -1884,13 +1953,30 @@ protected function getColumnSchemaRelevantKeys() {
    */
-  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 $fields, array $spec) {
+    $schema_handler = $this->database->schema();
+    $schema_handler->dropIndex($table, $name);
+    $schema_handler->addIndex($table, $name, $fields, $spec);
+  }
+

Lovely lovely lovely.

This was only recently added in #2555665: When index is added for content_translation_uid, the corresponding stored schema definition is not updated, I thought the postgres fail looked familiar.

Gábor Hojtsy’s picture

Issue tags: +D8MI, +language-content, +sprint
alexpott’s picture

Issue tags: +PostgreSQL

It'd be nice if one of the Postgres peeps had a look at this one.

catch’s picture

This is a bug on MySQL as well, just our test coverage doesn't catch it:
From http://dev.mysql.com/doc/refman/5.1/en/alter-table.html

If columns are dropped from a table, the columns are also removed from any index of which they are a part. If all columns that make up an index are dropped, the index is dropped as well. If you use CHANGE or MODIFY to shorten a column for which an index exists on the column, and the resulting column length is less than the index length, MySQL shortens the index automatically.

So you have column1, column2, column3 in an index.

We drop column2, MySQL silently changes the index definition to be column1, column3.

We add back column2, MySQL does not silently change the index definition to be column1, column2, column3.

Our schema tests only check if the index exists, not if the columns are correct, so MySQL tests pass.

The last submitted patch, 12: entity-schema_composite_indexes-2561129-11.patch, failed testing.

plach’s picture

Fixed the last MySQL failure and improved test coverage. The test-only patch should fail also on MySQL.

I'm working on some test coverage for the PG-specific stuff.

Btw, I tested all the remaining failures on PG locally (VM with Ubuntu 15.04, PHP 5.6.4, and PG 9.4.4) and all tests are green...

The last submitted patch, 15: entity-schema_composite_indexes-2561129-15.patch, failed testing.

alexpott’s picture

The last submitted patch, 19: entity-schema_composite_indexes-2561129-18.test.patch, failed testing.

catch’s picture

Overall this looks great, and the optimization to ensure that only dropped/added columns get their indexes rebuilt is necessary - otherwise this could an extremely long time on large tables.

Mostly nitpicks and a couple of questions:

  1. +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
    @@ -1442,6 +1422,97 @@ protected function updateSharedTableSchema(FieldStorageDefinitionInterface $stor
    +    $schema_keys = [
    

    Could this be $index_keys? That would mean one less out of $entity_schema, $schema, $schema_handler and $schema_keys if so.

  2. +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
    @@ -1442,6 +1422,97 @@ protected function updateSharedTableSchema(FieldStorageDefinitionInterface $stor
    +      foreach ($schema_keys as $key => $add_method) {
    

    Or since $schema_keys is only used here, so could maybe directly put the array into the foreach?

  3. +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
    @@ -1442,6 +1422,97 @@ protected function updateSharedTableSchema(FieldStorageDefinitionInterface $stor
    +                if (!$schema_handler->fieldExists($table_name, $specifier_column_name)) {
    

    When can this happen? Shouldn't that throw an exception or similar if it does? Or a comment would be handy if we're silently ignoring that case for a good reason.

  4. +++ b/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php
    @@ -509,6 +509,16 @@ public function testEntityIndexCreateDeleteWithoutData() {
    +    // 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 creates, then drops, but doesn't create again - should it?

plach’s picture

Attaching also a previous interdiff I was about to post, when I noticed the last review. This fixes also a few notices introduced by the latest patch.

@catch:

3: This was happening before introducing the optimization that skips dropping/creating indexes when creating the entity schema. Theoretically I guess it could happen also if multiple fields are added and a composite index is added at the same time. I added a comment to explain this.

4: Well, creation is handled in the previous ::applyUpdates() call, but one more cannot hurt.

plach’s picture

Issue summary: View changes

The last submitted patch, 22: entity-schema_composite_indexes-2561129-22.test.patch, failed testing.

The last submitted patch, 22: entity-schema_composite_indexes-2561129-22.patch, failed testing.

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php
    @@ -588,13 +588,25 @@ public function indexExists($table, $name) {
    +    if ($name == 'pkey') {
    +      $suffix = $name;
    +      $name = '';
    +    }
    +    else {
    +      $pos = strrpos($name, '__');
    +      $suffix = substr($name, $pos + 2);
    +      $name = substr($name, 0, $pos);
    +    }
    

    Needs a comment, why we are doing this here.

  2. +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
    @@ -328,42 +328,12 @@ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeI
    +      $this->deleteEntitySchemaIndexes($this->loadEntitySchemaData($entity_type));
    ...
    +      $this->createEntitySchemaIndexes($entity_schema);
    

    It is great to move things into separate methods for readability!

plach’s picture

alexpott’s picture

Assigning credit to catch (as a reviewer) and myself (as cause of the problem finder)

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

This has been my concerns

Gábor Hojtsy’s picture

Hm, should we believe the postgres fail? (I heard those bots are not predictable(?))

plach’s picture

@Gábor Hojtsy:

The PG fail was expected, it should be fixed by #2561229: Upgrade content tests fails on postgres for translated comment now. Re-testing.

Btw, #2561121: Installer fails on postgresql due to uncaught exception fixed most of the PG bots randomosity, although I saw one random failure in https://www.drupal.org/pift-ci-job/26901. @alexpott++

alexpott’s picture

@plach there is a feeling that the first test on postgres might fail because of a DrupalCI timing issue - but it does not happen very often.. I has not occurred on the latest run. So https://www.drupal.org/pift-ci-job/27075 should be the first ever green postgres run.

Gábor Hojtsy’s picture

Yay, for first green run :)

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed a173031 and pushed to 8.0.x. Thanks!

  • alexpott committed a173031 on 8.0.x
    Issue #2561129 by plach, alexpott, catch: Composite indexes are not...
Gábor Hojtsy’s picture

Issue tags: -sprint

Yay, thanks all!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

amateescu’s picture

The primary key of the entity tables was forgotten in this patch, here's a followup for that: #2841291: Fix NOT NULL handling in the entity storage and 'primary key' changes when updating the storage definition of an identifier field