As part of #2721313: Upgrade path between revisionable / non-revisionable entities I am looking for a scalable and reusable way to update entity definitions, and in this case make them revisionable, this includes adding a "revision_id" field.

Take the example:

      $revision_id = BaseFieldDefinition::create('integer')
        ->setLabel(new TranslatableMarkup('Revision ID'))
        ->setReadOnly(TRUE)
        ->setSetting('unsigned', TRUE);
      \Drupal::entityDefinitionUpdateManager()
        ->installFieldStorageDefinition($entity_type->getKey('revision'), $entity_type->id(), $entity_type->id(), $revision_id);

This eventually ends up in \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::getSharedTableFieldSchema via \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::getEntitySchema. Here all entity keys are set as NOT NULL apart from in the base table where the revision entity key is no set to NOT NULL.

Therefore when adding new fields, which are also entity keys, when there is already content in the table, we get the issue that the fields can't be empty.

One possible fix could be:

diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php b/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php
index 107495b..6caf20b 100644
--- a/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php
+++ b/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php
@@ -413,7 +413,10 @@ public function addField($table, $field, $spec, $keys_new = array()) {
     }
     if ($fixnull) {
       $spec['not null'] = TRUE;
-      $this->changeField($table, $field, $field, $spec);
+      $count = $this->connection->query('SELECT COUNT(*) AS count FROM {' . $table . '}')->fetchAssoc();
+      if ($count['count'] == 0) {
+        $this->changeField($table, $field, $field, $spec);
+      };
     }
   }
 

But as @dawehner suggests, this may be better further up the stack, maybe in \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::getSharedTableFieldSchema, or could we pass a callable into installFieldStorageDefinition to populate the fields with a default value before adding the NOT NULL? This seems like a good option, but the callable would have to be passed along for quite a while.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

timmillwood created an issue. See original summary.

timmillwood’s picture

timmillwood’s picture

timmillwood’s picture

timmillwood’s picture

Status: Active » Needs review
dawehner’s picture

Thank you @timmillwood for creating the issue.

Status: Needs review » Needs work

The last submitted patch, 4: adding_not_null_fields-2746279-4.patch, failed testing.

timmillwood’s picture

ok, looks like I am hitting too early, before the table has even been created.

Back to the drawing board.

timmillwood’s picture

Wondering if we could pass a callable as part of the FieldStorageDefinition to get the initial values for fields. To \Drupal\Core\Field\FieldStorageDefinitionInterface we can add setInitialValueCallable to set the callable, getInitialValueCallable to get the callable (not sure we need this really), getInitialValues to run the callable and get the values.

The callable can be passed into \Drupal\Core\Entity\EntityDefinitionUpdateManager::installFieldStorageDefinition and setInitialValueCallable can be used in there.

timmillwood’s picture

It looks as though there is already a way to pass in an initial value http://cgit.drupalcode.org/drupal/tree/core/lib/Drupal/Core/Database/Dri.... This comes from the field schema, so we'd need to add this in somewhere along the line, the only issue is each row ends up with the same value, this would cause issues when fields need to be "not null" and unique.

timmillwood’s picture

This patch adds a test which exposes this issue.

Status: Needs review » Needs work

The last submitted patch, 11: adding_not_null_fields-2746279-11.patch, failed testing.

timmillwood’s picture

Not yet a working patch, but should give an idea of what I'm trying to do.

Status: Needs review » Needs work

The last submitted patch, 13: adding_not_null_fields-2746279-13.patch, failed testing.

daffie’s picture

A quick review:

  1. +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php
    @@ -407,9 +407,7 @@ public function addField($table, $field, $spec, $keys_new = array()) {
    -      $this->connection->update($table)
    -        ->fields(array($field => $spec['initial']))
    -        ->execute();
    +      $this->connection->query("UPDATE {{$table}} SET `{$field}` = {$spec['initial']};");
    

    I really do not like this change. It feels completely wrong to me.

  2. +++ b/core/modules/system/tests/modules/entity_test_not_null/entity_test_not_null.module
    @@ -0,0 +1,38 @@
    +    $fields['not_null'] = BaseFieldDefinition::create('integer')
    +      ->setLabel(new TranslatableMarkup('Not null'))
    +      ->setRequired(TRUE)
    +      ->setRevisionable(TRUE);
    
    +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityNewNotNullFieldTest.php
    @@ -0,0 +1,76 @@
    +    $field = BaseFieldDefinition::create('integer')
    +      ->setLabel(new TranslatableMarkup('Not null'))
    +      ->setRequired(TRUE)
    +      ->setRevisionable(TRUE);
    +    \Drupal::entityDefinitionUpdateManager()
    +      ->installFieldStorageDefinition('not_null', 'entity_test_rev', 'entity_test_rev', $field);
    

    Is this not doing it twice? Or am I missing something.

  3. +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityNewNotNullFieldTest.php
    @@ -0,0 +1,76 @@
    +        $this->assertEquals('YES', $column->Null, "Base table not_null column has NULL indicates.");
    ...
    +        $this->assertEquals('YES', $column->Null, "Revision table not_null column has NULL indicates.");
    

    Should the test not be equal to 'NO'.

timmillwood’s picture

Status: Needs work » Closed (outdated)