Problem/Motivation

This is the final beta-blocking child of #1498720: [meta] Make the entity storage system handle changes in the entity and field schema definitions. The parent issue in general, and #2333113: Add an EntityDefinitionUpdateManager so that entity handlers can respond (e.g., by updating db schema) to code updates in a controlled way (e.g., from update.php) specifically, made it so that when you deploy new code that changes an entity type's definition or its field definitions, update.php can make entity handlers react to that. Which means, it's possible for onFieldStorageDefinitionCreate(), onFieldStorageDefinitionUpdate(), and onFieldStorageDefinitionDelete() to be called on base fields, but the SqlContentEntityStorage implementation of those methods were initially written to only handle fields whose storage is in their own dedicated table, because that's all that was supported in Drupal 7 (via Field UI).

Proposed resolution

  • Improve DefaultTableMapping to also support fields that can be stored in the entity's base tables.
  • Fix SqlContentEntityStorage to work with the improved DefaultTableMapping.
  • Move all field schema generation code from SqlContentEntityStorage to SqlContentEntityStorageSchema, where it belongs, and make it work with the improved DefaultTableMapping.

Remaining tasks

Review. Commit.

User interface changes

None.

API changes

  • Removes getReservedColumns() from BaseFieldDefinition and FieldStorageConfig. That is now the exclusive province of TableMappingInterface.
  • Adds a isBaseField() method to FieldStorageDefinitionInterface. Whether a field is required to exist for every bundle of an entity type can and does impact storage decisions, so it makes sense for FSDI to provide that info.
  • Adds allowsSharedTableStorage(), requiresDedicatedTableStorage(), and getDedicatedTableNames() methods to DefaultTableMapping. Although public, in the patch they are only called by SqlContentEntityStorage and SqlContentEntityStorageSchema. #2326719-25: Move pseudo-private table mapping functions from ContentEntityDatabaseStorage to public API of DefaultTableMapping contains a proposal for a future followup issue to expand TableMappingInterface to provide some of this information in a more generic way, but for now, these are methods specific to the DefaultTableMapping implementation.
  • Implements DefaultTableMapping::allowsSharedTableStorage() as returning true for single-valued base-fields only. Multivalued base fields get a dedicated table, which is a database schema change compared to HEAD, but as a result, now work. The only two core examples of a multi-valued base field are user.roles and taxonomy_term.parent, and because SqlContentEntityStorage hasn't supported this until now, they've had to implement their own custom storage. This patch doesn't change those two use cases, but #2248977: Complete support for multi-value base fields in ContentEntitySchemaHandler and use it for the user.roles field is an issue for doing so.
  • Adds a finalizePurge() method to FieldableEntityStorageSchemaInterface that's identical to HEAD's FieldableEntityStorageInterface::finalizePurge(). With this patch, both the storage handler and the storage schema handler need to do something when field data is done being purged.
  • Adds an optional $storage_definitions parameter to SqlEntityStorageInterface::getTableMapping() that defaults to the current ones defined for the entity type. But, during a definition update or delete, this allows a table mapping to be retrieved for the old definitions.
  • Moves all schema affecting code from SqlContentEntityStorage::onFieldStorageDefinition(Create|Update|Delete)() and SqlContentEntityStorage::finalizePurge() to the same methods on SqlContentEntityStorageSchema. In the process, makes those methods work for all fields (shared-table and dedicated-table).
  • Makes SqlContentEntityStorage::countFieldData() work for shared-table fields.
  • Moves pseudo-private SqlContentEntityStorage::_fieldSqlSchema() method to protected SqlContentEntityStorageSchema::getDedicatedTableSchema() and also adds a protected SqlContentEntityStorageSchema::getSharedTableFieldSchema() to handle shared-table fields.
  • Moves pseudo-private SqlContentEntityStorage::_fieldIndexName() method to protected SqlContentEntityStorageSchema::getFieldIndexName().
  • Requires entity-type-specific subclasses of SqlContentEntityStorageSchema (core has several, see patch) to move their field-level overrides from getEntitySchema() to getSharedTableFieldSchema(). This is because when an updated field definition gets deployed, but there are no entity type definition changes being deployed, only the latter runs.
CommentFileSizeAuthor
#80 interdiff.txt829 byteseffulgentsia
#80 entity-fix-sql-storage-2337927-80.patch225.16 KBeffulgentsia
#78 interdiff.txt28.22 KBeffulgentsia
#78 entity-fix-sql-storage-2337927-78.patch225.13 KBeffulgentsia
#74 interdiff.txt8 KBeffulgentsia
#74 entity-fix-sql-storage-2337927-74.patch212.67 KBeffulgentsia
#73 interdiff.txt875 byteseffulgentsia
#73 entity-fix-sql-storage-2337927-73.patch210.12 KBeffulgentsia
#71 interdiff.txt2.64 KBeffulgentsia
#71 entity-fix-sql-storage-2337927-71.patch210.1 KBeffulgentsia
#70 entity-fix-sql-storage-2337927-70.patch210.24 KBplach
#70 entity-fix-sql-storage-2337927-70.interdiff.txt709 bytesplach
#68 interdiff.txt17.22 KBeffulgentsia
#68 entity-fix-sql-storage-2337927-68.patch210.14 KBeffulgentsia
#67 d8_base_field_changes.interdiff.txt6.77 KBfago
#67 d8_base_field_changes.patch209.11 KBfago
#64 interdiff.txt2.56 KBeffulgentsia
#64 entity-fix-sql-storage-2337927-63.patch206.85 KBeffulgentsia
#63 interdiff-61-62.txt6.74 KBeffulgentsia
#62 interdiff.txt40.12 KBeffulgentsia
#62 entity-fix-sql-storage-2337927-62.patch205.96 KBeffulgentsia
#61 interdiff.txt27.71 KBeffulgentsia
#61 entity-fix-sql-storage-2337927-61.patch201.15 KBeffulgentsia
#60 interdiff.txt2.92 KBeffulgentsia
#60 entity-fix-sql-storage-2337927-60.patch190.55 KBeffulgentsia
#58 entity-fix-sql-storage-2337927-58.patch189.41 KBplach
#58 entity-fix-sql-storage-2337927-58.interdiff.txt37.99 KBplach
#53 interdiff.txt7.44 KBeffulgentsia
#53 entity-fix-sql-storage-2337927-53.patch162.14 KBeffulgentsia
#52 interdiff.txt7.05 KBeffulgentsia
#52 entity-fix-sql-storage-2337927-52.patch165.78 KBeffulgentsia
#50 interdiff.txt11.58 KBeffulgentsia
#50 entity-fix-sql-storage-2337927-50.patch163.75 KBeffulgentsia
#47 interdiff.txt19.01 KBeffulgentsia
#47 entity-fix-sql-storage-2337927-47.patch156.81 KBeffulgentsia
#40 d8_base_field_changes.interdiff.txt11 KBfago
#40 d8_base_field_changes.patch158.94 KBfago
#37 interdiff.txt5.17 KBeffulgentsia
#37 entity-fix-sql-storage-2337927-37.patch156.58 KBeffulgentsia
#35 interdiff.txt11.41 KBeffulgentsia
#35 entity-fix-sql-storage-2337927-35.patch151.99 KBeffulgentsia
#27 entity-fix-sql-storage-2337927-27.patch146.3 KBplach
#27 entity-fix-sql-storage-2337927-27.interdiff.txt2.57 KBplach
#15 entity-fix-sql-storage-2337927-12.patch145.71 KBeffulgentsia
#13 interdiff.txt879 byteseffulgentsia
#13 entity-fix-sql-storage-2337927-13.patch145.06 KBeffulgentsia
#12 interdiff.txt1.86 KBeffulgentsia
#12 entity-fix-sql-storage-2337927-12.patch145.71 KBeffulgentsia
#2 interdiff.txt4.34 KBeffulgentsia
#2 entity-fix-sql-storage-2337927-2.patch147.37 KBeffulgentsia
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

effulgentsia’s picture

Issue summary: View changes
effulgentsia’s picture

effulgentsia’s picture

+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
@@ -745,19 +953,565 @@ protected function processIdentifierSchema(&$schema, $key) {
+    // @todo Revisit this code: the revision id should match the entity id type
+    //   if revisions are not supported.

We need to resolve this @todo or link it to an issue.

plach’s picture

I think a separate issue would be better, given the patch size.

effulgentsia’s picture

+1 to #4.

+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
@@ -76,14 +101,25 @@ public function __construct(EntityManagerInterface $entity_manager, ContentEntit
-      $entity_type->getKeys() != $original->getKeys() ||
...
+      // Detect changes in key or index definitions.
+      $this->getEntitySchemaData($entity_type, $this->getEntitySchema($entity_type, TRUE)) != $this->loadEntitySchemaData($original);

Should we remove the getKeys() comparison (as #2 is doing) or not (as #2298525-99: Test issue for Make ContentEntityDatabaseStorage handle changes in the entity schema definition)? Given the other comparisons, including the added getEntitySchemaData(), I don't know if there are any keys that could be different that we're concerned with.

effulgentsia’s picture

+++ b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php
@@ -991,11 +998,30 @@ public function testFieldSqlSchemaForEntityWithStringIdentifier() {
     // Make sure that the entity_id schema field if of type varchar.
-    $this->assertEquals($schema['test_entity__test_field']['fields']['entity_id']['type'], 'varchar');
-    $this->assertEquals($schema['test_entity__test_field']['fields']['revision_id']['type'], 'varchar');
+    // $schema['test_entity__test_field']['fields']['entity_id']['type']
+    $this->assertEquals('varchar', 'varchar');
+    // $schema['test_entity__test_field']['fields']['revision_id']['type']
+    $this->assertEquals('varchar', 'varchar');

Useless assertions. What should they be changed to?

plach’s picture

Status: Needs review » Active

We have to remember to block entity schema changes before the last iteration as some storage classes do not support them yet, AAMOF they hard-code quite some assumptions about table layouts.

plach’s picture

Status: Active » Needs review
plach’s picture

+1 on #5

Useless assertions. What should they be changed to?

Mmh, it seems I forgot to complete that refactoring: we just need to convert those assertions to expectations for the ::createTable() method of the db schema service (see ::testOnEntityTypeCreate()). I can fix those tomorrow, bed time now :)

effulgentsia’s picture

+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
@@ -76,14 +101,25 @@ public function __construct(EntityManagerInterface $entity_manager, ContentEntit
+  protected function state() {
+    if (!isset($this->state)) {
+      $this->state = \Drupal::state();
+    }
+    return $this->state;
+  }
...
+    return $this->state()->get('entity.schema.handler.' . $entity_type->id() . '.schema_data') ?: array();

Per #2333113-105: Add an EntityDefinitionUpdateManager so that entity handlers can respond (e.g., by updating db schema) to code updates in a controlled way (e.g., from update.php), we should change this from state to its own KV collection and remove that key prefix. We should also constructor inject it.

Status: Needs review » Needs work

The last submitted patch, 2: entity-fix-sql-storage-2337927-2.patch, failed testing.

effulgentsia’s picture

Status: Needs work » Needs review
FileSize
145.71 KB
1.86 KB
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
@@ -184,9 +243,40 @@ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeI
-    // Otherwise, throw an exception.
     else {
-      throw new EntityStorageException(String::format('The SQL storage cannot change the schema for an existing entity type with data.'));
+      $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);
+          }
+        }
+      }
+
+      // Create new indexes and unique keys.
+      $entity_schema = $this->getEntitySchema($entity_type, TRUE);
+      foreach ($this->getEntitySchemaData($entity_type, $entity_schema) as $table_name => $schema) {
+        if (!empty($schema['indexes'])) {
+          foreach ($schema['indexes'] as $name => $specifier) {
+            $schema_handler->addIndex($table_name, $name, $specifier);
+          }
+        }
+        if (!empty($schema['unique keys'])) {
+          foreach ($schema['unique keys'] as $name => $specifier) {
+            $schema_handler->addUniqueKey($table_name, $name, $specifier);
+          }
+        }
+      }
+
+      // Store the updated entity schema.
+      $this->saveEntitySchemaData($entity_type, $entity_schema);

This looks like potentially good code, in terms of allowing some class of schema changes on a non-empty table, but I can't tell under what circumstances it's meant to run. Seems like there needs to be some kind of check for whether indeed only index changes are involved. Without that, we get the failure in #2. However, I wonder if this is an enhancement that can be a post-beta followup. So, this patch reverts it.

effulgentsia’s picture

+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
@@ -172,6 +223,14 @@ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeI
         $this->onEntityTypeDelete($original);
         $this->onEntityTypeCreate($entity_type);
+
+        // Update dedicated table revision schema.
+        if ($original->isRevisionable() && !$entity_type->isRevisionable()) {
+          $this->dropDedicatedTableRevisionSchema();
+        }
+        elseif (!$original->isRevisionable() && $entity_type->isRevisionable()) {
+          $this->createDedicatedTableRevisionSchema($entity_type);
+        }

Why is this needed? Shouldn't the above delete/create themselves already handle dropping and creating revision tables? Reverting this to see if tests still pass.

Status: Needs review » Needs work

The last submitted patch, 13: entity-fix-sql-storage-2337927-13.patch, failed testing.

effulgentsia’s picture

Status: Needs work » Needs review
FileSize
145.71 KB

I see. Ok. Reuploading #12.

effulgentsia’s picture

Issue summary: View changes
effulgentsia’s picture

Issue tags: +beta blocker

This is the last beta blocking child of the parent issue, so moving the beta blocker tag to it.

effulgentsia’s picture

Issue summary: View changes
effulgentsia’s picture

Issue summary: View changes
Berdir’s picture

Implements DefaultTableMapping::allowsSharedTableStorage() as returning true for single-valued base-fields only. Multivalued base fields get a dedicated table, which is a database schema change compared to HEAD, but as a result, now work. There are no core examples of a multi-valued base field (except ones with custom storage, which are by definition not managed by the storage schema handler), but there might be in contrib.

Core actually has at least two use cases for this, we just couldn't use it yet. That is a) user.roles and b) taxonomy parents (which are right now lazy-loaded and lazy-updated, but with all the other writes that are now going on and the entity cache, I think we can drop that stuff and save a lot of custom code.

I've started working on this a while ago in #2248977: Complete support for multi-value base fields in ContentEntitySchemaHandler and use it for the user.roles field, will pick it up when this is in.

Will try to review this today.

alexpott’s picture

Status: Needs review » Needs work
  1. +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
    @@ -237,18 +247,38 @@ protected function schemaHandler() {
       /**
    +   * Updates the wrapped entity type definition.
    +   *
    +   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
    +   *   The update entity type.
    +   *
    +   * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
    +   *   See https://www.drupal.org/node/2274017.
    +   */
    +  public function setEntityType(EntityTypeInterface $entity_type) {
    

    Adding deprecated functions to get the beta in kind of sucks.

  2. +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
    @@ -237,18 +247,38 @@ protected function schemaHandler() {
    +  public function getTableMapping(array $storage_definitions = NULL) {
    +    $table_mapping = $this->tableMapping;
    
    @@ -321,20 +351,43 @@ public function getTableMapping() {
    +      if (!$storage_definitions) {
    +        $this->tableMapping = $table_mapping;
    +      }
    ...
    -    return $this->tableMapping;
    +    return $table_mapping;
    

    The $this->tableMapping and $table_mapping logic is really difficult to follow. Basically it seems as though we statically cache when we are not using custom storage definitions. A comment would help somewhere. We have one usage of this functionality - in \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::deleteSharedTableSchema() - i wonder if this would be simpler with an additional function for this and then refactor the common code into a protected function.

  3. +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php
    @@ -260,30 +378,54 @@ protected function checkEntityType(EntityTypeInterface $entity_type) {
    +            throw new FieldException(String::format('Fieled storage definition for "@field_name" could not be found.', array('@field_name' => $field_name)));
    

    Fieled -> Field

plach’s picture

Given 7, 12 is fine by me. I wrote most of the patch code so I am obviously generally +1 on it :)

plach’s picture

Adding deprecated functions to get the beta in kind of sucks.

I agree that's not nice, but I think solving it would require to fix #2274017: Make SqlContentEntityStorage table mapping agnostic , which still needs some thoughts/exploration and that can be addressed mostly as internal refactoring, thus not requiring to be addressed before beta. Since that method is not part of any public API, although being public, I think the problems implied by going this way are more theoretical than practical. @eff suggested a similar approach to address #2326719-25: Move pseudo-private table mapping functions from ContentEntityDatabaseStorage to public API of DefaultTableMapping, that is making some methods public but not part of an interface, which works fine. Addressing this the same way makes sense IMHO.

plach’s picture

Assigned: Unassigned » plach

Working on #21.2 and #21.3, while we wait for Alex's reply.

fago’s picture

Assigned: plach » Unassigned

I reviewed the patch and I think it's ok to proceed as is. The most unfortunate part about it is defnitely the already mentioned setEntityType() part and table mapping instantiation, but cleaning this up is no simple task and would postpone this issue unnecessarily on a non-API breaking clean-up. I do think it's better done in a separate issue where we can focus on it anyway.

@plach: setEntityType() seems to be already a public non-interface method. We discussed moving more logic in the table mapping classes, do we have a separate issue for that or is that supposed to be part of #2274017: Make SqlContentEntityStorage table mapping agnostic ?

  1. +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
    @@ -1412,96 +1466,22 @@ public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
    +    $this->schemaHandler()->onFieldStorageDefinitionCreate($storage_definition);
    

    Deprecated naming, it's not called schema handler anymore. It's just internal though, so no show-stopper.

  2. +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
    @@ -1671,23 +1658,49 @@ public function finalizePurge(FieldStorageDefinitionInterface $storage_definitio
       public function countFieldData($storage_definition, $as_bool = FALSE) {
    ...
    +    if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) {
    +      $is_deleted = $this->storageDefinitionIsDeleted($storage_definition);
    

    I think it would make sense to add an method to the table mapping that can give you the right table for a given fields given some parameters (old-revisions vs new, default language or not).

  3. +++ b/core/modules/system/src/Tests/Entity/FieldSqlStorageTest.php
    @@ -460,19 +459,9 @@ function testFieldSqlStorageForeignKeys() {
    -
    -    // Verify the SQL schema.
    -    $schemas = SqlContentEntityStorage::_fieldSqlSchema($field_storage);
    -    $schema = $schemas[$this->table_mapping->getDedicatedDataTableName($field_storage)];
    -    $this->assertEqual(count($schema['foreign keys']), 1, 'There is 1 foreign key in the schema');
    -    $foreign_key = reset($schema['foreign keys']);
    -    $foreign_key_column = $this->table_mapping->getFieldColumnName($field_storage, $foreign_key_name);
    -    $this->assertEqual($foreign_key['table'], $foreign_key_name, 'Foreign key table name preserved in the schema');
    -    $this->assertEqual($foreign_key['columns'][$foreign_key_column], 'id', 'Foreign key column name preserved in the schema');
    

    Looks like this got deleted without replacement?

    Update: Covered by testDedicatedTableSchema() of SqlContentEntityStorageSchemaTest now, so that's fine.

fago’s picture

Assigned: Unassigned » plach

cross-post

plach’s picture

Status: Needs work » Needs review
FileSize
2.57 KB
146.3 KB

This should fix #21, I'll answer to @fago's review in a separate post as soon as I get a chance. I just boarded a plane :)

plach’s picture

Assigned: plach » Unassigned

[double post]

plach’s picture

Deprecated naming, it's not called schema handler anymore. It's just internal though, so no show-stopper.

+1 on renaming that in a separate issue, it's a totally internal change.

I think it would make sense to add an method to the table mapping that can give you the right table for a given fields given some parameters (old-revisions vs new, default language or not).

@efff opened an issue for all of this, see https://www.drupal.org/comment/9095763#comment-9095763

yched’s picture

$this->schemaHandler : is that such a big deal to rename here ? Seems a bit artificial to push this to a followup, and those kind of internal cleanups tend to get easily forgotten. But no showstopper, agreed.

plach’s picture

We discussed moving more logic in the table mapping classes, do we have a separate issue for that or is that supposed to be part of #2274017: Make SqlContentEntityStorage table mapping agnostic ?

What we have for now is only that: I think it migth make sense to address moving instantiation logic in the table mapping and refactor the storage to use it together, as I imagine the required changes to be related. Otherwsie we need a separate issue and we need to update the patch @todo references.

effulgentsia’s picture

What should we rename schemaHandler() to? In docs, we refer to it as the "storage schema handler". Once #2332857: Decide how secondary entity handlers (like storage_schema) should be instantiated gets fixed, you'll be able to get it via $entity_manager->getHandler('storage_schema'). It's an object that implements the interface EntityStorageSchemaInterface (just as most other interfaces for objects you can get via $entity_manager->getHandler($handler_type) don't have the *Handler suffix. Given all that, since the current schemaHandler() method is in the storage class, and therefore, a storage prefix would be unnecessary, should we just name the method schema()? That would be consistent with Drupal\Core\Database\Connection::schema().

fago’s picture

ad #32: I'd still think that it should be getStorageSchema() and $storageSchema property as this matches the interface. I'd keep the storage prefix also as the dependencies of the storage are not all storage related.

fago’s picture

Minor:

+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
@@ -271,6 +271,13 @@ public function setEntityType(EntityTypeInterface $entity_type) {
+    // of field storage definitions is passed,for instance when comparing old

Missing space before "for".

effulgentsia’s picture

Fixes #33 and #34.

fago’s picture

Status: Needs review » Needs work

I gave this some manual testing with the following results:
1. Changed a base field without a storage change - all fine, no updates detected.
2. Added a new base field to user entity, all created fine.
3. Tried deleting an existing base field. This generally seems to think it deals with dedicated fields and fails:

Drupal\Core\Database\DatabaseExceptionWrapper: SQLSTATE[42S02]: Base table or view not found: 1146 Table 'd8.comment__homepage' doesn't exist: UPDATE {comment__homepage} SET deleted=:db_update_placeholder_0; Array ( [:db_update_placeholder_0] => 1 ) in Drupal\Core\Entity\Sql\SqlContentEntityStorage->onFieldStorageDefinitionDelete() (line 1508 of /var/www/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php).

4. Tried updating field settings influencing storage, i.e. update comment id to be not unsigned. FieldStorageDefinitionUpdateForbiddenException has been correctly thrown.

5. Tried changing the schema provided by a field type. This seems to try to change all fields but fails to detect the data migration. I guess that is as the schema for the before and after comparison is calculated both with the updated way. However, that's something what is officially not supported in d7 either so I think it's fine to not have that case covered for now. That's something we should probably fix for release though -> I'd suggest creating a follow-up, critical issue for this. Thoughts?

Thus, setting needs work for 3.

effulgentsia’s picture

Status: Needs work » Needs review
FileSize
156.58 KB
5.17 KB

Work in progress for #36.3.

fago’s picture

+++ b/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php
@@ -113,4 +113,57 @@ public function testUpdateWithData() {
+    // and a custom code-defined bundle field. Services need to be reinstantiated

comment exceeds 80chars.

Status: Needs review » Needs work

The last submitted patch, 37: entity-fix-sql-storage-2337927-37.patch, failed testing.

fago’s picture

Status: Needs work » Needs review
FileSize
158.94 KB
11 KB

Thanks eff, I took a look at fixing the fail / issue. Problem is that currently the table mapping creation relies on the entity manager to determine whether something is a base field, but when the field has been deleted this information is not available any more as we do only keep field storage definitions in state.
However, we should be able to determine the storage by looking at the field storage definitions only - as that's their purpose. Thus, passing in base field definitions into the DefaultTableMapping is problematic in general. As we need to know whether a field is a base field in there I suggest adding FSDI::isBaseField(). Imo that makes sense to have as it's required to determine the storage.

Attached patch contains those test fixes and just adds in the existing but not previously unused logic to drop the columns of a deleted shared field. This seems to work fine - but I did not check whether influences other tests, so we'll see whether it is green.
However, it drops the columns only where there is no data, because else we need to do field purging in order to call delete() on each of the purged field items. That's not implemented yet for base fields and a bigger topic left for #2282119: Make the Entity Field API handle field purging. So in the case of having data we can throw an update-not-possible exception for now. However, I do think that's something we should fix for release, thus bump #2282119: Make the Entity Field API handle field purging to critical.

fago’s picture

I've repeated my test for #36.3 and it works as it should now. However, imo it still needs some more test coverage to make sure this won't break people's data after beta:
- EntityDefinitionUpdateTest misses an testFieldCreateDeleteWithData() what makes sure we throw the respective exception when we try to delete a base field with data.

Given that improved coverage and green tests, I think this is ready to go!

Status: Needs review » Needs work

The last submitted patch, 40: d8_base_field_changes.patch, failed testing.

effulgentsia’s picture

Assigned: Unassigned » effulgentsia

Thanks fago. I'm looking into those failures.

Berdir’s picture

So, I'm trying to test this using my file_entity use case that adds a bundle field to the file entity. And unfortunately, I'm still stuck on my 'initial' problem. My attempt in the previous issue with putting it in a setting of the field definition wasn't accepted, which is OK with me :)

However, I've now tried the only other thing I can think of (short of providing a custom field type plugin for this) and that is to override the storage_schema handler and add the initial key there. That doesn't seem to work. Apparently, onFieldStorageDefinitionCreate() doesn't go through getEntitySchema(), so it doesn't pick up whatever changes are made there... That seems problematic? It would mean that any addition there on base fields will be skipped when you call onFieldStorageDefinitionCreate()/Update(), like those not null calls or the uid type override in UserStorageSchema.

Is that a known issue? Should we somehow route the schema definition for base fields through getEntitySchema()? If you think that we can solve this as a follow-up without outside API changes then I'm OK with that :) Just wanted to make sure we're aware of this limitation...

Berdir’s picture

If you want to try it out yourself, the code is in https://github.com/md-systems/file_entity/tree/schema_handling, run FileEntityFileTypeClassificationTest, which is testing exactly this scenario.

See https://github.com/md-systems/file_entity/compare/schema_handling for the relevant changes compared the current manual creation of that field.

Berdir’s picture

Implements DefaultTableMapping::allowsSharedTableStorage() as returning true for single-valued base-fields only. Multivalued base fields get a dedicated table, which is a database schema change compared to HEAD, but as a result, now work. There are no core examples of a multi-valued base field (except ones with custom storage, which are by definition not managed by the storage schema handler), but there might be in contrib.

One more thing on this :)

I started re-rolling #2248977: Complete support for multi-value base fields in ContentEntitySchemaHandler and use it for the user.roles field on top of this, to test it.

This, as a generic statement, is not (yet) true. Yes, it has logic to build the schema for those tables, but it doesn't actually create the necessary tables yet when a module is installed.

I added the following to onEntityTypeCreate() to allow that in my issue (no patch posted yet, still working on it):

    // Explicitly create the tables for base fields with dedicated tables.
    $table_mapping = $this->storage->getTableMapping();
    foreach ($this->entityManager->getFieldStorageDefinitions($this->entityType->id()) as $field_storage_definition) {
      if ($field_storage_definition->isBaseField() && $table_mapping->requiresDedicatedTableStorage($field_storage_definition)) {
        $this->onFieldStorageDefinitionCreate($field_storage_definition);
      }
    }

Feedback is very welcome if there is a better way to do it :) Again, mostly wanted to make sure that the current state is clear and what works and what doesn't.

effulgentsia’s picture

This fixes the failures from #40 and incorporates #46 (slightly different code, but basically the same approach). I still need to add the test for #41 and respond to #44. @plach: do you have thoughts on #44?

effulgentsia’s picture

Status: Needs work » Needs review

Sigh. Forgot to set status for bot.

Status: Needs review » Needs work

The last submitted patch, 47: entity-fix-sql-storage-2337927-47.patch, failed testing.

effulgentsia’s picture

Assigned: effulgentsia » Unassigned
Status: Needs work » Needs review
FileSize
163.75 KB
11.58 KB

Added the test coverage requested in #41.

Status: Needs review » Needs work

The last submitted patch, 50: entity-fix-sql-storage-2337927-50.patch, failed testing.

effulgentsia’s picture

Status: Needs work » Needs review
FileSize
165.78 KB
7.05 KB
+++ b/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php
@@ -113,4 +114,154 @@ public function testUpdateWithData() {
+    // Uninstall and apply updates. It's expected to work.
+    $this->uninstallModule('entity_schema_test');
+    $this->entityDefinitionUpdateManager->applyUpdates();
+    $this->pass('No exceptions thrown when applying an update that deletes a dedicated-table field with data.');

This is wrong. Until we solve #2282119: Make the Entity Field API handle field purging, purging doesn't work for any nonconfigurable field, regardless of table layout. This patch changes the assertion to expect an exception and fixes the code accordingly.

effulgentsia’s picture

I made two final changes before calling it a night:

  1. Per #10, I wanted to change state() to a separate KV collection, but I realized there's nothing in this patch that needs that persistence. It was there for the code that got removed in #12, and can be brought back in a separate issue specifically for that enhancement if/when we want that.
  2. I cleaned up requiresFieldStorageSchemaChanges() and requiresFieldDataMigration().

I don't have the steam left to think about #44. I'm hoping you Europeans can finish whatever is left here and get it to RTBC while I sleep :)

effulgentsia’s picture

Issue summary: View changes
fago’s picture

Created #2339855: Handle changes to the schema provided by a field type for #36.5

That seems problematic? It would mean that any addition there on base fields will be skipped when you call onFieldStorageDefinitionCreate()/Update(), like those not null calls or the uid type override in UserStorageSchema.

Agreed. That means we always have to go through entity schema to derive the schema of a certain field, i.e. we have to make sure we can invoke getEntitySchema() with old field storage definitions also. However, if we add the possiblity to add indexes in a storage acgnostic way I'm not sure the getEntitySchema() override would be still needed? So maybe that would be alternative, cleaner way to address the problem - but that's clearly follow-up.

Imo, we have to fix bailing out to getEntitySchema() for getting the schema of a single field storage definition, i.e. add a possibility to pass in the field storage definitions for which to generate the schema similar getTableMapping() on the storage. I think we need plach's input on this one though :/

But moreover, the changes of #53 make the system to fail to detect changes to this customizations, or to detect changes like #2339855: Handle changes to the schema provided by a field type, thus I'm not sure this is a good idea?

effulgentsia’s picture

But moreover, the changes of #53 make the system to fail to detect changes to this customizations, or to detect changes like #2339855: Handle changes to the schema provided by a field type, thus I'm not sure this is a good idea?

I don't think the changes in #53 did that. I think that's a problem both in HEAD and with earlier versions of this patch, since getEntitySchemaData() strips out all field schema info anyway. I opened #2340061: Overrides to field schema in *StorageSchema::getEntitySchema() aren't applied when base fields are created/changed later to deal with that problem. I don't think either that or #2339855: Handle changes to the schema provided by a field type is this issue's scope though: this issue is about reacting to changes in field storage definitions. If there are code changes other than definition changes, that's a separate scope, so let's deal with it in the follow ups.

plach’s picture

This should fix #44 by moving field schema alteration to ::getSharedTableFieldSchema() implementations. As discussed in Skype with @berdir, @eff anf @fago, this restores entity schema index/key difference tracking, which allows to detect changes also in field index/key definitions, as alterations are not prefixed with 'field_' and thus are stored together with entity index/keys.

I opted for allowing ::getSharedTableFieldSchema() to return a different definition depending on the shared table, since there are edge cases where the same field can have slightly different definitions depending on the table. For instance {node}.vid is nullable while {node_revision}.vid is not.

Status: Needs review » Needs work

The last submitted patch, 58: entity-fix-sql-storage-2337927-58.patch, failed testing.

effulgentsia’s picture

Status: Needs work » Needs review
FileSize
190.55 KB
2.92 KB
effulgentsia’s picture

Refactored EntityDefinitionUpdateTest to make it easier to add new test methods to it to cover the fixes in #58. It now doesn't rely on installing a module to make a definition change, since that's not the primary purpose of EntityDefinitionUpdateManager. The purpose of EntityDefinitionUpdateManager is to allow for definition changes via updated code, so the test now simulates that via state().

effulgentsia’s picture

As discussed in Skype with @berdir, @eff anf @fago, this restores entity schema index/key difference tracking

That part looks good. Here's an added test.

effulgentsia’s picture

FileSize
6.74 KB

#62's patch is correct, but the interdiff is wrong. Here's a better one.

effulgentsia’s picture

, which allows to detect changes also in field index/key definitions, as alterations are not prefixed with 'field_' and thus are stored together with entity index/keys.

That part is problematic. Attached is a failing test change.

Status: Needs review » Needs work

The last submitted patch, 64: entity-fix-sql-storage-2337927-63.patch, failed testing.

fago’s picture

thanks d.o. for eating my comment :(

Changes look good in general, although I must say that the DX of getSharedTableFieldSchema() is worse than the single getEntitySchema() before. Having everything in a single array makes it nicer, but moreover we have to make clear when to use which method now. I do think this is solvable with documentation for now, plus we should be able to do away with the per-field optimizations rather easily by adding storage-acnostix indexes support.

+++ b/core/modules/user/src/UserStorageSchema.php
@@ -21,20 +22,6 @@ class UserStorageSchema extends SqlContentEntityStorageSchema {
-    // The "users" table does not use serial identifiers.
-    $schema['users']['fields']['uid']['type'] = 'int';

That valuable comment got lost.

It's great to have the improved test coverage now, as this covers now all important cases. I did test my manually tests once again and came to the same result: everything's good except the problematic case of adding a field having optimizations. In my test, it detected an entity type change + the field change, thus I'd assume the problem is that the entity type change already re-initialized the schema, so the field change afterwards fails.

fago’s picture

Status: Needs work » Needs review
FileSize
209.11 KB
6.77 KB

ok, I fixed the tests, re-added above comment and added some docs on when to override which method. Given that, this now takes care of the remaining problems as well and should be ready to go imo.

effulgentsia’s picture

#67 looks good to me.

I must say that the DX of getSharedTableFieldSchema() is worse than the single getEntitySchema() before.

This patch tries to improve it. I think it's about on-par now.

Status: Needs review » Needs work

The last submitted patch, 68: entity-fix-sql-storage-2337927-68.patch, failed testing.

plach’s picture

Status: Needs work » Needs review
FileSize
709 bytes
210.24 KB

This should fix installation.

effulgentsia’s picture

Resolved the only @todo in the patch without an issue link. And changed the only one that was linking to the meta to link to its dedicated followup.

I think the only thing that's left here is fixing #6/#9.

Status: Needs review » Needs work

The last submitted patch, 71: entity-fix-sql-storage-2337927-71.patch, failed testing.

effulgentsia’s picture

Status: Needs work » Needs review
FileSize
210.12 KB
875 bytes

Fixed the failure in #71.

effulgentsia’s picture

This fixes #6 by moving the test from SqlContentEntityStorageTest to SqlContentEntityStorageSchemaTest and copying the implementation from testDedicatedTableSchema() to testDedicatedTableSchemaForEntityWithStringIdentifier(), and just changing the mock and expectation from integer to string.

I think all feedback on this issue has now been addressed.

plach’s picture

RTBC +1 if this is green :)

kylebrowning’s picture

omg. +1 if RTBC

jibran’s picture

Status: Needs review » Reviewed & tested by the community

Unable to find any nitpicks so RTBC as per #75.

effulgentsia’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
225.13 KB
28.22 KB

Yay!!! Thanks for the RTBC!

Good news: I added some more test coverage: to cover field schema updates, not just creation and deletion. For the update test, I used updating the field type from string to text, which also has the effect of testing a multicolumn type, and updating from single column to multicolumn. And I also added a test for the update to involve a change to a customization in getSharedTableFieldSchema(), which ends up being a similar problem space as #2339855: Handle changes to the schema provided by a field type (in fact, the solution solves that issue).

Bad news: Those tests required code fixes to make work. So, with much sadness, I need to un-RTBC this and await a re-RTBC from someone.

Good news: I'm about to close #2339855: Handle changes to the schema provided by a field type as a duplicate. Yay for one less followup.

Status: Needs review » Needs work

The last submitted patch, 78: entity-fix-sql-storage-2337927-78.patch, failed testing.

effulgentsia’s picture

Status: Needs work » Needs review
FileSize
225.16 KB
829 bytes
effulgentsia’s picture

Issue summary: View changes
swentel’s picture

Status: Needs review » Reviewed & tested by the community
  1. +++ b/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php
    @@ -105,10 +105,11 @@
    +   * Tests creating, updating, and deleting a base field when there are no existing entities.
    

    nitpick - 80 chars, but shouldn't hold up the patch.

  2. +++ b/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php
    @@ -135,18 +173,17 @@
    +   * Tests creating, updating, and deleting a bundle field when there are no existing entities.
    

    same here

Marking RTBC so core committers can have a first look up at it.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

I gave the patch thorough review in #21 and the discussion since has addressed further issues with interdiffs all looking good. Let's do this :)

Committed f27fd1f and pushed to 8.0.x. Thanks!

diff --git a/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php b/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php
index 3755d0f..752a1f3 100644
--- a/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php
+++ b/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php
@@ -105,7 +105,7 @@ public function testEntityTypeUpdateWithData() {
   }

   /**
-   * Tests creating, updating, and deleting a base field when there are no existing entities.
+   * Tests creating, updating, and deleting a base field if no entities exist.
    */
   public function testBaseFieldCreateUpdateDeleteWithoutData() {
     // Add a base field, ensure the update manager reports it, and the update
@@ -179,7 +179,7 @@ public function testBaseFieldCreateUpdateDeleteWithoutData() {
   }

   /**
-   * Tests creating, updating, and deleting a bundle field when there are no existing entities.
+   * Tests creating, updating, and deleting a bundle field if no entities exist.
    */
   public function testBundleFieldCreateUpdateDeleteWithoutData() {
     // Add a bundle field, ensure the update manager reports it, and the update
@@ -224,7 +224,7 @@ public function testBundleFieldCreateUpdateDeleteWithoutData() {
   }

   /**
-   * Tests creating and deleting a base field when there are existing entities.
+   * Tests creating and deleting a base field if entities exist.
    *
    * This tests deletion when there are existing entities, but not existing data
    * for the field being deleted.
@@ -255,7 +255,7 @@ public function testBaseFieldCreateDeleteWithExistingEntities() {
   }

   /**
-   * Tests creating and deleting a bundle field when there are existing entities.
+   * Tests creating and deleting a bundle field if entities exist.
    *
    * This tests deletion when there are existing entities, but not existing data
    * for the field being deleted.

  • alexpott committed f27fd1f on
    Issue #2337927 by effulgentsia, plach, fago: Fixed...
webchick’s picture

YEEEEAAAAAHHHHHH!!

yannisc’s picture

Incredible!

Congratulations and many thanks to all core devs for working on this and other issues making Drupal 8 a step closer for all the rest of us!!!

plach’s picture

WHO-HOO!

yched’s picture

Yay ! Congrats, folks ! Sorry I couldn't help much :-/

int’s picture

Congrats.. There are 0 beta blockers now. Let's wait and see :-D

xjm’s picture

Hooray!

For everyone who noticed that this was the last known beta blocker and is wondering when the beta will come out, see: Today there are zero Drupal 8 beta blockers! Here's what's next.

Status: Fixed » Closed (fixed)

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

ekes’s picture

It seems that in the course of this the code for supporting 'unique keys' got lost. It's still documented, there is code to remove them, but not create them even if they are in the schema array.

I've opened a new issue at #2742103: SqlContentEntityStorageSchema::getDedicatedTableSchema() ignores 'unique keys' from the schema definition