From 6bf02028a8a55182fe4bb4a564d6834aff88c347 Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Fri, 7 Aug 2015 12:13:59 +0100
Subject: [PATCH] Issue #2544954 by stefan.r, jhedstrom:
 SqlContentEntityStorageSchema does not detect column-level schema changes in
 non-base-fields

---
 .../Sql/SqlContentEntityStorageSchema.php     |  72 ++++++++++--
 ...qlContentEntityStorageSchemaColumnTest.php | 104 ++++++++++++++++++
 2 files changed, 164 insertions(+), 12 deletions(-)
 create mode 100644 core/modules/field/src/Tests/Entity/Update/SqlContentEntityStorageSchemaColumnTest.php

diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
index 463b9eb8bb3b..b92636c6357c 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
@@ -186,27 +186,44 @@ public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterfac
       return TRUE;
     }
 
+    if ($storage_definition->hasCustomStorage()) {
+      // The field has custom storage, so we don't know if a schema change is
+      // needed or not, but since per the initial checks earlier in this
+      // function, nothing about the definition changed that we manage, we
+      // return FALSE.
+      return FALSE;
+    }
+
+    return $this->getSchemaFromStorageDefinition($storage_definition) != $this->loadFieldSchemaData($original);
+  }
+
+  /**
+   * Gets the schema data for the given field storage definition.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   The field storage definition. The field that must not have custom
+   *   storage, i.e. the storage must take care of storing the field.
+   *
+   * @return array
+   *   The schema data.
+   */
+  protected function getSchemaFromStorageDefinition(FieldStorageDefinitionInterface $storage_definition) {
+    assert('!$storage_definition->hasCustomStorage();');
+    $table_mapping = $this->storage->getTableMapping();
+    $schema = [];
     if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
-      return $this->getDedicatedTableSchema($storage_definition) != $this->loadFieldSchemaData($original);
+      $schema = $this->getDedicatedTableSchema($storage_definition);
     }
     elseif ($table_mapping->allowsSharedTableStorage($storage_definition)) {
       $field_name = $storage_definition->getName();
-      $schema = array();
       foreach (array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames()) as $table_name) {
         if (in_array($field_name, $table_mapping->getFieldNames($table_name))) {
           $column_names = $table_mapping->getColumnNames($storage_definition->getName());
           $schema[$table_name] = $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
         }
       }
-      return $schema != $this->loadFieldSchemaData($original);
-    }
-    else {
-      // The field has custom storage, so we don't know if a schema change is
-      // needed or not, but since per the initial checks earlier in this
-      // function, nothing about the definition changed that we manage, we
-      // return FALSE.
-      return FALSE;
     }
+    return $schema;
   }
 
   /**
@@ -1267,7 +1284,7 @@ protected function updateDedicatedTableSchema(FieldStorageDefinitionInterface $s
       }
     }
     else {
-      if ($storage_definition->getColumns() != $original->getColumns()) {
+      if ($this->hasColumnChanges($storage_definition, $original)) {
         throw new FieldStorageDefinitionUpdateForbiddenException('The SQL storage cannot change the schema for an existing field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data.');
       }
       // There is data, so there are no column changes. Drop all the prior
@@ -1356,7 +1373,7 @@ protected function updateSharedTableSchema(FieldStorageDefinitionInterface $stor
       }
     }
     else {
-      if ($storage_definition->getColumns() != $original->getColumns()) {
+      if ($this->hasColumnChanges($storage_definition, $original)) {
         throw new FieldStorageDefinitionUpdateForbiddenException('The SQL storage cannot change the schema for an existing field (' . $storage_definition->getName() . ' in ' . $storage_definition->getTargetEntityTypeId() . ' entity) with data.');
       }
 
@@ -1746,6 +1763,7 @@ protected function getEntityIndexName(ContentEntityTypeInterface $entity_type, $
   protected function getFieldIndexName(FieldStorageDefinitionInterface $storage_definition, $index) {
     return $storage_definition->getName() . '_' . $index;
   }
+
   /**
    * Checks whether a database table is non-existent or empty.
    *
@@ -1766,4 +1784,34 @@ protected function isTableEmpty($table_name) {
         ->fetchField();
   }
 
+  /**
+   * Compares schemas to check for changes in the column definitions.
+   *
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition
+   *   Current field storage definition.
+   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original
+   *   The original field storage definition.
+   *
+   * @return bool
+   *   Returns TRUE if there are schema changes in the column definitions.
+   */
+  protected function hasColumnChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
+    if ($storage_definition->getColumns() != $original->getColumns()) {
+      // Base field definitions have schema data stored in the original
+      // definition.
+      return TRUE;
+    }
+
+    if (!$storage_definition->hasCustomStorage()) {
+      $schema = $this->getSchemaFromStorageDefinition($storage_definition);
+      foreach ($this->loadFieldSchemaData($original) as $table => $spec) {
+        if ($spec['fields'] != $schema[$table]['fields']) {
+          return TRUE;
+        }
+      }
+    }
+
+    return FALSE;
+  }
+
 }
diff --git a/core/modules/field/src/Tests/Entity/Update/SqlContentEntityStorageSchemaColumnTest.php b/core/modules/field/src/Tests/Entity/Update/SqlContentEntityStorageSchemaColumnTest.php
new file mode 100644
index 000000000000..379382c040fc
--- /dev/null
+++ b/core/modules/field/src/Tests/Entity/Update/SqlContentEntityStorageSchemaColumnTest.php
@@ -0,0 +1,104 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\field\Tests\Entity\Update\SqlContentEntityStorageSchemaColumnTest.
+ */
+
+namespace Drupal\field\Tests\Entity\Update;
+
+use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
+use Drupal\entity_test\Entity\EntityTestRev;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\simpletest\KernelTestBase;
+
+/**
+ * Tests that schema changes in fields with data are detected during updates.
+ *
+ * @group Entity
+ */
+class SqlContentEntityStorageSchemaColumnTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['entity_test', 'field', 'text', 'user'];
+
+  /**
+   * The created entity.
+   *
+   * @var \Drupal\Core\Entity\Entity
+   */
+  protected $entity;
+
+  /**
+   * The field.
+   *
+   * @var \Drupal\field\FieldConfigInterface
+   */
+  protected $field;
+
+  /**
+   * The field storage.
+   *
+   * @var \Drupal\field\FieldStorageConfigInterface
+   */
+  protected $fieldStorage;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $this->installEntitySchema('entity_test_rev');
+    $this->installEntitySchema('user');
+
+    $field_name = 'test';
+    $this->fieldStorage = FieldStorageConfig::create([
+      'field_name' => $field_name,
+      'entity_type' => 'entity_test_rev',
+      'type' => 'string',
+      'cardinality' => 1,
+    ]);
+    $this->fieldStorage->save();
+
+    $this->field = FieldConfig::create([
+      'field_name' => $field_name,
+      'entity_type' => 'entity_test_rev',
+      'bundle' => 'entity_test_rev',
+      'required' => TRUE,
+    ]);
+    $this->field->save();
+
+    // Create an entity with field data.
+    $this->entity = EntityTestRev::create([
+      'user_id' => mt_rand(1, 10),
+      'name' => $this->randomMachineName(),
+      $field_name => $this->randomString(),
+    ]);
+    $this->entity->save();
+  }
+
+  /**
+   * Tests that column-level schema changes are detected for fields with data.
+   */
+  public function testColumnUpdate() {
+    // Change the field type in the stored schema.
+    $schema = \Drupal::keyValue('entity.storage_schema.sql')->get('entity_test_rev.field_schema_data.test');
+    $schema['entity_test_rev__test']['fields']['test_value']['type'] = 'varchar_ascii';
+    \Drupal::keyValue('entity.storage_schema.sql')->set('entity_test_rev.field_schema_data.test', $schema);
+
+    // Now attempt to run automatic updates. An exception should be thrown
+    // since there is data in the table.
+    try {
+      \Drupal::service('entity.definition_update_manager')->applyUpdates();
+      $this->fail('Failed to detect a schema change in a field with data.');
+    }
+    catch (FieldStorageDefinitionUpdateForbiddenException $e) {
+      $this->pass('Detected a schema change in a field with data.');
+    }
+  }
+
+}
-- 
GitLab