Problem/Motivation

Nodes - as the canonical example of revisionable entities - store the revision author, the revision timestamp and a log message for each revision. The Node entity declares these fields in its baseFieldDefinitions().

Custom blocks only provide a log, but no revision author or timestamp.

We should make this consistent and make it easy for contrib entity types to provide the same behavior.

#2666808: Add a revisionable entity base class and log interface introduced three revision metadata fields, which are named "revision_created, revision_user, revision_log_message". However the core still assumes at some places that they are called "revision_timestamp, revision_uid, revision_log". That is the case where the node entity and the block entity use the previous nomenclature. But unfortunately this is not the only place.. There is one another really important place and it is in SqlContentEntityStorage::getTableMapping where only the old nomenclature is used to put these revision metadata fields in the "entity_type_id_revision" table. #2666808: Add a revisionable entity base class and log interface did not extend the list with the new revision metadata fields and what now happens is that if you use the new RevisionLogEntityTrait your revision metadata fields will not be put - as they should - in the "entity_type_id_revision" table, but they will be put in the field table.

Proposed resolution

Provide a revision_metadata_keys key in the entity annotation which provides a mapping of common identifiers like "log", "uid", "created" to the actual field names. Because there are places where we need the list of revision metadata fields explicitly, for example in SqlContentEntityStorage::getTableMapping() or in ContentEntityBase::hasTranslationChanges() with #2615016: ContentEntityBase::hasTranslationChanges should exclude the "changed" fields from the comparison, this is not part of the entity_keys key, even though it is similar in nature. This also allows us to ignore (at least for the scope of this issue) the fact that entity key fields get a NOT NULL constraint in the SQL schema. Note that calling EntityType::getKeys() will also not return the revision metadata keys.

In order to provide backwards-compatibility, in the case that revision_metadata_keys is not set, we will check the field definitions if fields with the appropriate names (revision_log, revision_log_message) exist and use those as as values. This behavior can be disabled by a boolean parameter which is utilized in RevisionLogEntityTrait because due to the above checking of the field definitions, checking the revision metadata keys while building the field definitions would lead to infinite recursion.

Because the hardcoding of incorrect revision metadata field names in SqlContentEntityStorage::getTableMapping() entity types using RevisionLogTrait have the database columns for the revision metadata fields in the wrong table. (They are in the revision data table, but they should be in the revision table.) To correct this, an update function is provided that moves the field values to the correct table and fixed any views that reference the old table names in their views field configuration.

Remaining tasks

Follow-up issue for D9 to remove the hard coded revision metadata field list from \Drupal\Core\Entity\RevisionLogEntityTrait::getRevisionMetadataKey and RevisionLogEntityTrait should create the fields only if they are mentioned in the entity annotation.

Follow-up issue for D9 to remove the backwards compatibility parameter from \Drupal\Core\Entity\ContentEntityTypeInterface::getRevisionMetadataKeys and make the annotation for revision metadata keys obligatory for revisionable entities.

User interface changes

API changes

New functions :
-EntityTypeInterface::getRevisionMetadataKeys
-EntityTypeInterface::getRevisionMetadataKey
-EntityTypeInterface::hasRevisionMetadataKey

Change record

https://www.drupal.org/node/2831499

CommentFileSizeAuthor
#267 2248983-267-followup.patch738 byteshchonov
#260 2248983-followup.patch856 bytesamateescu
#257 interdiff.txt1.21 KBamateescu
#257 2248983-257.patch216.71 KBamateescu
#255 interdiff.txt1.14 KBamateescu
#255 2248983-255.patch215.5 KBamateescu
#253 interdiff.txt1.6 KBamateescu
#253 2248983-253.patch215.47 KBamateescu
#250 interdiff.txt639 bytesamateescu
#250 2248983-250.patch215.33 KBamateescu
#247 2248983-247.patch215.33 KBamateescu
#240 2248983-240.patch214.51 KBtimmillwood
#232 interdiff.txt1.33 KBamateescu
#227 interdiff.txt1.59 KBamateescu
#227 2248983-227.patch214.6 KBamateescu
#206 interdiff.txt322.53 KBamateescu
#206 2248983-206.patch214.56 KBamateescu
#204 interdiff.txt804 bytesamateescu
#204 2248983-204.patch240.97 KBamateescu
#200 2248983-200.patch240.84 KBtimmillwood
#200 interdiff.txt3.07 KBtimmillwood
#198 2248983-198.patch239.87 KBamateescu
#195 interdiff-192-195.txt2.26 KBhchonov
#195 2248983-195.patch239.36 KBhchonov
#192 2248983-192.patch239.39 KBhchonov
#190 interdiff-186-190.txt11.02 KBhchonov
#190 2248983-190.patch239.32 KBhchonov
#186 interdiff-181-186.txt6.83 KBhchonov
#186 2248983-186.patch239.23 KBhchonov
#181 2248983-181.patch238.4 KBhchonov
#179 interdiff-177-179.txt4.36 KBhchonov
#179 2248983-179.patch238.49 KBhchonov
#177 interdiff-176-177.txt5.93 KBhchonov
#177 2248983-177.patch237.67 KBhchonov
#176 interdiff-173-176.txt2.76 KBhchonov
#176 2248983-176.patch236.81 KBhchonov
#173 interdiff-167-173.txt798 byteshchonov
#173 2248983-173.patch236.75 KBhchonov
#171 interdiff-170-171.txt457 byteshchonov
#171 2248983-171-post-update-with-batch.patch237.28 KBhchonov
#170 interdiff-167-170.txt1.44 KBhchonov
#170 2248983-170-post-update-with-batch.patch237.35 KBhchonov
#167 interdiff-166-167.txt424 byteshchonov
#167 2248983-167.patch236.69 KBhchonov
#166 interdiff-158-166.txt686 byteshchonov
#166 2248983-166-trust-data.patch236.71 KBhchonov
#158 interdiff-143-158.txt9.77 KBhchonov
#158 2248983-158.patch237 KBhchonov
#156 interdiff-143-155.txt513 byteshchonov
#156 2248983-155.patch236.58 KBhchonov
#154 interdiff-143-154.txt513 byteshchonov
#154 2248983-154.patch513 byteshchonov
#151 2248983-151.patch234.59 KBhchonov
#150 2248983-150-testing-update-views-in-hook-update.patch234.59 KBhchonov
#149 2248983-149-experimental.patch1.62 KBhchonov
#143 interdiff.txt9.4 KBamateescu
#143 2248983-143.patch237.06 KBamateescu
#136 2248983-136.patch236.05 KBtimmillwood
#112 interdiff.txt4.63 KBamateescu
#112 2248983-112.patch236.56 KBamateescu
#107 interdiff.txt5.79 KBamateescu
#107 2248983-107.patch234.82 KBamateescu
#105 interdiff.txt5.83 KBamateescu
#105 2248983-105.patch231.63 KBamateescu
#102 interdiff.txt12.92 KBamateescu
#102 2248983-102.patch231.37 KBamateescu
#99 interdiff.txt5.39 KBamateescu
#99 2248983-99.patch235.56 KBamateescu
#92 interdiff.txt3.24 KBtimmillwood
#92 2248983-92.patch231.94 KBtimmillwood
#81 interdiff-80-81.txt1.67 KBhchonov
#81 2248983-81.patch231.24 KBhchonov
#80 interdiff-77-80.txt3.95 KBhchonov
#80 2248983-80.patch230.58 KBhchonov
#77 interdiff-75-77.txt5.66 KBhchonov
#77 2248983-77.patch228.81 KBhchonov
#75 interdiff-73-75.txt215.88 KBhchonov
#75 2248983-75.patch228.53 KBhchonov
#73 interdiff-69-73.txt7.26 KBhchonov
#73 2248983-73.patch19.05 KBhchonov
#69 interdiff-68-69.txt1.79 KBhchonov
#69 2248983-69.patch19.07 KBhchonov
#68 interdiff-67-68.txt761 byteshchonov
#68 2248983-68.patch19.15 KBhchonov
#67 interdiff-64-67.txt5.24 KBhchonov
#67 2248983-67.patch19.18 KBhchonov
#64 interdiff-61-64.txt731 byteshchonov
#64 2248983-64.patch18.51 KBhchonov
#61 interdiff-46-61.txt17.25 KBhchonov
#61 2248983-61.patch18.51 KBhchonov
#46 interdiff-43-46.txt2.2 KBhchonov
#46 2248983-46.patch21.68 KBhchonov
#43 interdiff-42-43.txt1.46 KBhchonov
#43 2248983-43.patch19.46 KBhchonov
#42 interdiff-40-42.txt4.02 KBhchonov
#42 2248983-42.patch19.48 KBhchonov
#40 interdiff-34-40.txt3.28 KBhchonov
#40 2248983-40.patch19.98 KBhchonov
#34 decide_what_to_do_with-2248983-34.patch17.43 KBparanojik
#32 decide_what_to_do_with-2248983-32.patch18.84 KBparanojik
#32 interdiff-27-32.txt5.51 KBparanojik
#27 interdiff-17-27.txt5.13 KBparanojik
#27 decide_what_to_do_with-2248983-27.patch11.8 KBparanojik
#22 interdiff-18-22.txt5.65 KBparanojik
#22 decide_what_to_do_with-2248983-22.patch11.39 KBparanojik
#18 interdiff.txt11.82 KBparanojik
#18 decide_what_to_do_with-2248983-18.patch11.08 KBparanojik
#17 decide_what_to_do_with-2248983-17.patch12.39 KBparanojik
#11 2248983-11.patch1.89 KBamateescu
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

larowlan’s picture

Makes sense

andypost’s picture

Assigned: Unassigned » plach

Suppose this could be closed now.
@plach please confirm

andypost’s picture

Just need to remove @todo ContentEntityDatabaseStorage.php:273

tstoeckler’s picture

@andypost: Can you elaborate why this can be closed? I think the points in the issue summary are still valid?

plach’s picture

Assigned: plach » Unassigned

Yep, I am not sure why this should be closed. What changed since we opened it?

amateescu’s picture

Status: Active » Closed (duplicate)
amateescu’s picture

Status: Closed (duplicate) » Active

Actually, in that issue we just added the generic RevisionLogInterface. Maybe here we could change Node and Block Content to implement it?

Wim Leers’s picture

dawehner’s picture

Actually, in that issue we just added the generic RevisionLogInterface. Maybe here we could change Node and Block Content to implement it?

+1 for it, but NOT change the actual used base fields.

amateescu’s picture

Version: 8.0.x-dev » 8.2.x-dev

but NOT change the actual used base fields.

I'm not sure what that means...

Also, I assume we should only do this in 8.2.x.

amateescu’s picture

Status: Active » Needs review
FileSize
1.89 KB

For starters, we should get rid of that @todo.

And now I think I understand what @dawehner wanted to say: we shouldn't change the name of the revision metadata fields for nodes and custom blocks?

Status: Needs review » Needs work

The last submitted patch, 11: 2248983-11.patch, failed testing.

Berdir’s picture

Yes. Implement the interface but not the trait. Which is exactly why your code can't work (the trait is just a default implementation, we shouldn't really on that as an API).

To make that code more generic, we'd need entity keys or a similar approach (which have a problem that they then enforce all "keyed" fields to be required.

timmillwood’s picture

In #2716081: BlockContent should have revision_user and revision_created fields and implement RevisionLogInterface I added revision_created and revision_user to BlockContent, this does not use RevisionLogEntityTrait, it's a simple copy & paste of the two missing fields, and implementing RevisionLogInterface.

In #2705389: Selected content entities should extend EditorialContentEntityBase or RevisionableContentEntityBase I am working to make (possibly) all content entities (apart from Node and BlockContent) extend RevisionableContentEntityBase this will force them to use RevisionLogEntityTrait and get the three new fields.

The concept of the patch in #11 looks good to me, although it needs an upgrade path and tests.

hchonov’s picture

I wasn't aware of this issue and created #2732699: The SqlContentEntityStorage should be aware of the new revision metadata fields from RevisionLogEntityTrait, which however describes a problem for which we now need an update path:

#2666808: Add a revisionable entity base class and log interface introduced three revision metadata fields, which are named "revision_created, revision_user, revision_log_message". However the core still assumes at some places that they are called "revision_timestamp, revision_uid, revision_log". That is the case where the node entity and the block entity use the previous nomenclature. But unfortunately this is not the only place.. There is one another really important place and it is in SqlContentEntityStorage::getTableMapping where the only the old nomenclature is used to put these revision metadata fields in the "entity_type_id_revision" table. #2666808: Add a revisionable entity base class and log interface did not extend the list with the new revision metadata fields and what now happens is that if you use the new RevisionLogEntityTrait your revision metadata fields will not be put like they should in the "entity_type_id_revision" table, but they will be put in the field table.

hchonov’s picture

Issue summary: View changes
paranojik’s picture

Status: Needs work » Needs review
FileSize
12.39 KB

I tried to implement the approach mentioned in #13. Not sure if correctly, but the idea is to allow for a smooth transition when implementing RevisionableContentEntityBase.

paranojik’s picture

Moved revision fields to entity_keys as discussed on IRC with @hchonov...

The last submitted patch, 17: decide_what_to_do_with-2248983-17.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 18: decide_what_to_do_with-2248983-18.patch, failed testing.

hchonov’s picture

+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
@@ -181,6 +211,10 @@ protected function initTableLayout() {
+      $this->revisionLogField = $this->entityType->getKey('revision_log_message') ?: 'revision_log';
+      $this->revisionUidField = $this->entityType->getKey('revision_user') ?: 'revision_uid';
+      $this->revisionTimestampField = $this->entityType->getKey('revision_created') ?: 'revision_timestamp';

If we are not going to create the fields if the entity keys are not set then we do not need any fallback here or?

paranojik’s picture

Status: Needs work » Needs review
FileSize
11.39 KB
5.65 KB

Trying to address failing tests.

@hchonov: could be. Will try that...

Status: Needs review » Needs work

The last submitted patch, 22: decide_what_to_do_with-2248983-22.patch, failed testing.

hchonov’s picture

Issue tags: +DevDaysMilan
timmillwood’s picture

We could do with this for #2725463: WI: Add StatusItem field type to store archived state, the current patch there just adds the new metadata base field to the list in SqlContentEntityStorage::getTableMapping.

paranojik’s picture

IMHO the approach from #17 would be a much cleaner solution and would also satisfy the case from #2725463: WI: Add StatusItem field type to store archived state.

paranojik’s picture

Status: Needs work » Needs review
Issue tags: -DevDaysMilan
FileSize
11.8 KB
5.13 KB

Went ahead and cleaned up #17 a little bit. Still need to write the update path...

timmillwood’s picture

What's the difference between revision_fields and entity_keys?

paranojik’s picture

revision_fields are revision metadata fields that are stored in the "revisions" table and are not required (which is a restriction on entity_keys).

timmillwood’s picture

ok, sounds fair!

Status: Needs review » Needs work

The last submitted patch, 27: decide_what_to_do_with-2248983-27.patch, failed testing.

paranojik’s picture

Status: Needs review » Needs work

The last submitted patch, 32: decide_what_to_do_with-2248983-32.patch, failed testing.

paranojik’s picture

Status: Needs review » Needs work

The last submitted patch, 34: decide_what_to_do_with-2248983-34.patch, failed testing.

timmillwood’s picture

Many of the test failures are UpdatePathTestBase tests, and failing due to:

<li class="failure"><strong>Failed:</strong> <em class="placeholder">Drupal\Core\Database\DatabaseExceptionWrapper</em>: SQLSTATE[42S22]: Column not found: 1054 Unknown column &#039;revision.revision_log&#039; in &#039;field list&#039;: SELECT revision.revision_id AS revision_id, revision.langcode AS langcode, revision.revision_log AS revision_log, base.id AS id, base.type AS type, base.uuid AS uuid, CASE base.revision_id WHEN revision.revision_id THEN 1 ELSE 0 END AS isDefaultRevision
FROM 
{block_content} base
INNER JOIN {block_content_revision} revision ON revision.revision_id = base.revision_id; Array
(
)
 in <em class="placeholder">Drupal\Core\Entity\Sql\SqlContentEntityStorage-&gt;getFromStorage()</em> (line <em class="placeholder">425</em> of <em class="placeholder">/home/timmillwood/Code/drupal1/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php</em>).</li>

I'm struggling to track down why this is failing because after a site install block_content_revision.revision_log exists before and after this patch.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

hchonov’s picture

That is because the update path is needed. Now revision metadata fields will not be in the data and field_revision tables, but in the revision table for the revision metadata. So we need an update function, which is taking care of updating the field storage definitions, removing the revision metadata fields from the data and field_revision tables and putting them into the revision metadata table.

However there is one problem - we should still assume default revision metadata field names, as when the update function runs, the developer might still haven't updated the entity annotation with the revision metadata fields.

P.S. we have to this only for revision metadata fields using the new name nomenclature in RevisionLogEntityTrait::revisionLogBaseFieldDefinitions, because those fields are now in wrong tables.

hchonov’s picture

Unfortunately the entity definition update manager will not be able to update the definition itself, as the old code says the field is in table A and the new code says the field is in table B... So a manual update is needed.

hchonov’s picture

Status: Needs review » Needs work

The last submitted patch, 40: 2248983-40.patch, failed testing.

hchonov’s picture

Issue summary: View changes
Status: Needs work » Needs review
FileSize
19.48 KB
4.02 KB

I've added backward comparability in case the revision fields are not contained in the entity annotation. I am afraid, but we are going to need this hard coded until D9, where we could remove it and throw an exception if a revisionable entity type does not define the revision fields, but this will be a follow-up issue.

hchonov’s picture

Clearing some nit picks to match the interface definition.

The last submitted patch, 42: 2248983-42.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 43: 2248983-43.patch, failed testing.

hchonov’s picture

Status: Needs work » Needs review
FileSize
21.68 KB
2.2 KB

I've fixed the failures in SqlContentEntityStorageTest.

timmillwood’s picture

+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
@@ -294,17 +294,25 @@ public function getTableMapping(array $storage_definitions = NULL) {
+        $known_revision_fields = [
+          'revision_timestamp',
+          'revision_created',
+          'revision_uid',
+          'revision_user',
+          'revision_log',
+          'revision_log_message'
+        ];

Storing these in an array seems a bit odd, can't we fetch them from somewhere?

hchonov’s picture

Unfortunately not, they are hard coded over the entities. The list has been hard coded previously and it should remain so until D9 where we can introduce an exception if the entity is revisionable, but the revision fields are not defined in the entity annotation.

hchonov’s picture

Title: Decide what to do with revision metadata base field definitions » Define the revision metadata base fields in the entity annotation in order for the storage to create them into the revision table

Should this issue remain a task? It is also needed for fixing an existing bug.

hchonov’s picture

Title: Define the revision metadata base fields in the entity annotation in order for the storage to create them into the revision table » Define the revision metadata base fields in the entity annotation in order for the storage to create them only in the revision table
dawehner’s picture

I'm kinda confused why we not provide setters as every other field.

hchonov’s picture

@dawehner I am not really sure what you mean exactly? This here are getters for properties from the entity annotation. A setter is not needed here. They are exactly like the EntityTypeInterface::getKey function to retrieve the field name of an entity key.

tstoeckler’s picture

Status: Needs review » Needs work

I had a larger review written, but somehow lost that...

I see that this was tried above, but I actually think the revision fields should move back to entity_keys. That is exactly what that is being used for. I realize there might be some fallout with the schema generation or other places but I think we should rather fix or workaround that instead of introducing a whole new key for something very similar.

hchonov’s picture

@tstoeckler this is fine for me, however I am not sure about something - do we want the revision metadata fields on the first level in the entity keys or on a second level wrapped behind the revision key? I mean

entity_keys[revision_log] = rev_log_message

versus

entity_keys[revision_metadata] = [revision_log => rev_log_message]

The second way gives us the possibility to retrieve all the revision metadata fields at once without having the storage knowing that there is revision log, revision user, revision timestamp ...

paranojik’s picture

@tstoeckler I've already tried that, but as @berdir said in #13 all keyed fields are automatically required which does not really work for revision metadata fields. Working around that will be another hack... why would you want to do that?

hchonov’s picture

Hm, I've just added the following to the node entity keys
"revision_log" = "revision_log"
cleared the cache and saved a node entity without any revision log message and it landed in the database as NULL and not an empty string, so it means it was not flagged as required.

So I am not aware of any enforcement that all the entity keys have to be required.

hchonov’s picture

I am sorry, my fault, after I've updated the field definitions the field is really required...

tstoeckler’s picture

We discussed this in person with @Berdir, @alexpott, @hchonov and I. Will update the issue summary with the details. #53 is moot, but "needs work" still applies.

tstoeckler’s picture

Issue summary: View changes

OK, updated the issue summary.

tstoeckler’s picture

Issue tags: +Dublin2016
hchonov’s picture

hchonov’s picture

Issue summary: View changes

Status: Needs review » Needs work

The last submitted patch, 61: 2248983-61.patch, failed testing.

hchonov’s picture

Status: Needs work » Needs review
FileSize
18.51 KB
731 bytes

Oups.. I've forgot to pass the entity type to the call of Entity::baseFieldDefinitions.

Status: Needs review » Needs work

The last submitted patch, 64: 2248983-64.patch, failed testing.

tstoeckler’s picture

Edit: x-post with #61

I really like the approach, now that we've talked about it. I think it makes a lot of sense. Here's a review, most of it is nitpicks though. The upgrade path will be problematic, however, see below.

   /**
+   * An array of entity metadata fields.
+   *
+   * @var array
+   */
+  protected $revision_metadata_keys = [];

1. entity metadata fields -> entity metadata keys (I know the terminology is very unintuitive and confusing, but I think at the very least it's good to be consistent)

2. I realize that EntityType::$entity_keys is not being a good example here, but I think we could make the documentation a bit more helpful. Maybe add a second sentence like "The keys of the array are the 'revision_log', 'revision_uid', and 'revision_created' and the values are the field names of the respective fields for this entity type." It would just be nice to have docs on what's actually in this.

Edit: I know see that you added some great docs on EntityType::getRevisionMetadataKeys(), so that's fine I guess.

+    // Add a backward compatibility in case the revision metadata keys are not
+    // defined in the entity annotation.

I woud say "Add a backwards compatibility" -> "Provide backwards compatibility"

if (empty($this->revision_metadata_keys)) {

Minor, but the empty() is not needed

+      $class = $this->getClass();
+      $base_fields = $class::baseFieldDefinitions();

This should be \Drupal::service('entity_field.manager')->getBaseFieldDefinitions($this->id()). (I don't think we can inject anything properly here.)

+    return isset($keys[$key]) ? $keys[$key] : FALSE;
...
+    return !empty($keys[$key]);

I think we should be consistent with using empty() and isset() (I would personally prefer isset()).

+   *   - revision_log_message: The name of the property that contains description

property -> field (also elsewhere)

+      $revision_metadata_fields = $revisionable ? array_values($this->entityType->getRevisionMetadataKeys()) : NULL;

Minor, but it seems strange to use NULL for an array structure, what about an empty array?

+ *   revision_metadata_keys = {
+ *     "revision_user" = "revision_user",
+ *     "revision_created" = "revision_created",
+ *     "revision_log_message" = "revision_log"
+ *   },

I'm not sure about this myself, but I think the revision_ prefix inside of the keys is a bit repetitive, what do you think about removing this? (I.e. just have the inner keys be user, created, ...?)

+      $class = $entity_type->getClass();
+      $base_fields = $class::baseFieldDefinitions($entity_type);

In theory this should be using the EntityFieldManager. I'm not sure whether that's a good idea in an update hook, but it also seems wrong to ignore fields declared in a hook here. Hmm...

Awesome that you provided an upgrade path. Unfortunately:
- We need to run the actual upgrade in a batch.
- This will need upgrade path tests.

hchonov’s picture

Status: Needs work » Needs review
FileSize
19.18 KB
5.24 KB

@tstoeckler I've addressed everything beside the names of the revision metadata keys. Somehow I prefer having the revision_ prefix explicitly there.

hchonov’s picture

Forgot to finish the batch properly...

hchonov’s picture

I wasn't initializing properly the revision metadata keys.

The last submitted patch, 67: 2248983-67.patch, failed testing.

The last submitted patch, 68: 2248983-68.patch, failed testing.

Berdir’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/EntityType.php
    @@ -161,6 +161,13 @@ class EntityType implements EntityTypeInterface {
       /**
    +   * An array of entity revision metadata keys.
    +   *
    +   * @var array
    +   */
    +  protected $revision_metadata_keys = [];
    +
    +  /**
    

    I think all of this needs to be in ContentEntityType, we only have fields there, so at leat the BC logic woudln't work for other entity types.

  2. +++ b/core/lib/Drupal/Core/Entity/EntityType.php
    @@ -384,6 +391,43 @@ public function hasKey($key) {
        */
    +  public function getRevisionMetadataKeys() {
    +    // Provide backwards compatibility in case the revision metadata keys are
    +    // not defined in the entity annotation.
    +    if (!$this->revision_metadata_keys) {
    +      $base_fields = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions($this->id());
    +      if ((isset($base_fields['revision_uid']) && $revision_user = 'revision_uid') || (isset($base_fields['revision_user']) && $revision_user = 'revision_user')) {
    +        $this->revision_metadata_keys['revision_user'] = $revision_user;
    +      }
    +      if ((isset($base_fields['revision_timestamp']) && $revision_timestamp = 'revision_timestamp') || (isset($base_fields['revision_created'])) && $revision_timestamp = 'revision_created') {
    

    I think the property and method belongs in ContentEntityType. only those have revisions and those fields.

  3. +++ b/core/lib/Drupal/Core/Entity/EntityType.php
    @@ -384,6 +391,43 @@ public function hasKey($key) {
    +   */
    +  public function getRevisionMetadataKey($key) {
    +    $keys = $this->getRevisionMetadataKeys();
    +    return isset($keys[$key]) ? $keys[$key] : FALSE;
    

    false or null?

hchonov’s picture

FileSize
19.05 KB
7.26 KB

@berdir, yes of course you are right the functions and the property have to be in the ContentEntityType.
About the return value of getRevisionMetadataKey, we made it consistent with getKey, which also returns FALSE.

hchonov’s picture

Status: Needs review » Needs work

So we've just talked with tstoeckler and we need an update path test and additionally he mentioned that we have to update views as well, which are already configured for the database scheme we are changing, which dawehner confirmed.

hchonov’s picture

Status: Needs work » Needs review
FileSize
228.53 KB
215.88 KB

So here we go, I've added an update for views using revision metadata fields and an update path test.

Additionally we've decided that we have to extend the ContentEntityTypeInterface, but this is an API break, so now I am introducing a new interface ContentEntityTypeRevisionMetadataInterface, which ContentEntityType is implementing.
In D9 we could merge ContentEntityTypeRevisionMetadataInterface into ContentEntityTypeInterface.

As we require a database dump since the new revision metadata fields have been introduced, I've created a bare dump for Drupal 8.2.1 with the standard profile and additionally the entity_test module enabled.

Status: Needs review » Needs work

The last submitted patch, 75: 2248983-75.patch, failed testing.

hchonov’s picture

Status: Needs work » Needs review
FileSize
228.81 KB
5.66 KB

It should be better now.

The last submitted patch, 75: 2248983-75.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 77: 2248983-77.patch, failed testing.

hchonov’s picture

Status: Needs work » Needs review
FileSize
230.58 KB
3.95 KB

Changes since #77 :

1. The new interface ContentEntityTypeRevisionMetadataInterface now extends the ContentEntityTypeInterface and the class ContentEntityType implements the ContentEntityTypeRevisionMetadataInterface instead of the ContentEntityTypeInterface.

2. SqlContentEntityStorage::getTableMapping checks if the entity type implements the ContentEntityTypeRevisionMetadataInterface and if so retrieves the revision metadata fields through the method ContentEntityTypeRevisionMetadataInterface::getRevisionMetadataKeys, otherwise if the entity type does not implement the ContentEntityTypeRevisionMetadataInterface we still have to leave the hard coded list of revision metadata fields.

hchonov’s picture

I've added backward compatibility also to the update hook if the entity type does not implement the new ContentEntityTypeRevisionMetadataInterface, which I've forgot to do in the previous patch.

timmillwood’s picture

why does this issue not update RevisionLogEntityTrait with the revision_metadata_keys?

I want to try and get this issue pushed over the line so we can switch Node and BlockContent to extend RevisionableContentEntityBase.

hchonov’s picture

@timmillwood: because of the current implementation of ContentEntityType::getRevisionMetadataKeys, which as a backward compatibility will retrieve the base field definitions and so the function could not be used in RevisionLogEntityTrait::revisionLogBaseFieldDefinitions until D9 where we could make the revision metadata fields in the annotation obligatory.

timmillwood’s picture

@hchonov - Could we not get a revision_metadata_key, and if it's not set use a hardcoded value? This would at least allow us to use the trait in node and block_content which we currently can't do.

hchonov’s picture

@timmillwood - no, because the function calls the RevisionLogEntityTrait::revisionLogBaseFieldDefinitions, it is just not possible from within RevisionLogEntityTrait::revisionLogBaseFieldDefinitions to retrieve the revision metadata keys.

amateescu’s picture

@hchonov, it's definitely possible, RevisionLogEntityTrait::revisionLogBaseFieldDefinitions() receives the calling $entity_type definition as an argument ;)

hchonov’s picture

No, it is not possible to call ContentEntityType::getRevisionMetadataKyes from within RevisionLogEntityTrait::revisionLogBaseFieldDefinitions, because currently ContentEntityType::getRevisionMetadataKyes will call itself RevisionLogEntityTrait::revisionLogBaseFieldDefinitions because of backward compatibility.

timmillwood’s picture

Then I think the code needs refactoring.

timmillwood’s picture

+++ b/core/lib/Drupal/Core/Entity/ContentEntityType.php
@@ -41,4 +48,41 @@ protected function checkStorageClass($class) {
+  public function getRevisionMetadataKeys() {
+    // Provide backwards compatibility in case the revision metadata keys are
+    // not defined in the entity annotation.
+    if (!$this->revision_metadata_keys) {
+      $base_fields = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions($this->id());
+      if ((isset($base_fields['revision_uid']) && $revision_user = 'revision_uid') || (isset($base_fields['revision_user']) && $revision_user = 'revision_user')) {
+        $this->revision_metadata_keys['revision_user'] = $revision_user;
+      }
+      if ((isset($base_fields['revision_timestamp']) && $revision_timestamp = 'revision_timestamp') || (isset($base_fields['revision_created'])) && $revision_timestamp = 'revision_created') {
+        $this->revision_metadata_keys['revision_created'] = $revision_timestamp;
+      }
+      if ((isset($base_fields['revision_log']) && $revision_log = 'revision_log') || (isset($base_fields['revision_log_message']) && $revision_log = 'revision_log_message')) {
+        $this->revision_metadata_keys['revision_log_message'] = $revision_log;
+      }
+    }
+    return $this->revision_metadata_keys;
+  }

I don't think we should provide this backwards compatibility magic. For example this causes hasRevisionMetadataKey($key) to return true, when there isn't actually a key set.

hchonov’s picture

We sure need the bc otherwise the people update and they get exceptions, which we could not afford.

And I do not get it why would hasRevisionMetadataKey return true when the key does not exist? You are welcome to provide a test as a prove.

This BC has been discussed and approved between me, tstoeckler and berdir at the DrupalCon Dublin 2016.

timmillwood’s picture

@hchonov - Well the key kinda does exist, because getRevisionMetadataKeys() dynamically generates it, but it doesn't exist in the annotation. I think it would be better in the trait to do $revision_created_key = $this->getRevisionMetadataKey('revision_created') ?: 'revision_created'; and similar for each of the base fields.

This would work for BC if there are any entity types in contrib already using the trait, but not setting revision_metadata_keys. It will also work for node and block_content which have different keys than in the trait, as we can specifically define the keys in the entity type then use the trait.

I'll try to roll a patch today.

timmillwood’s picture

The attached patch now makes use of the revision_metadata_keys in RevisionLogEntityTrait.

hchonov’s picture

Status: Needs review » Needs work

Your change works for the storage only because of the BC added there in case the entity type does not implement the new ContentEntityTypeRevisionMedataInterface:

+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
@@ -294,17 +295,26 @@ public function getTableMapping(array $storage_definitions = NULL) {
+        // For backward comparability reasons we need to mention explicitly the
+        // revision metadata fields in case the entity type does not implement
+        // the ContentEntityTypeRevisionMetadataInterface.
+        $revision_metadata_fields = array_intersect([
+          'revision_timestamp',
+          'revision_uid',
+          'revision_log',
+          'revision_created',
+          'revision_user',
+          'revision_log_message',
+        ], $all_fields);

However now you have achieved that hasRevisionMetadataKey will return FALSE if the revision metadata key is not provided through the entity annotation but is coming from the trait, because now we do not check the base fields by removing the BC:

+++ b/core/lib/Drupal/Core/Entity/ContentEntityType.php
@@ -52,20 +52,6 @@ protected function checkStorageClass($class) {
-    // Provide backwards compatibility in case the revision metadata keys are
-    // not defined in the entity annotation.
-    if (!$this->revision_metadata_keys) {
-      $base_fields = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions($this->id());
-      if ((isset($base_fields['revision_uid']) && $revision_user = 'revision_uid') || (isset($base_fields['revision_user']) && $revision_user = 'revision_user')) {
-        $this->revision_metadata_keys['revision_user'] = $revision_user;
-      }
-      if ((isset($base_fields['revision_timestamp']) && $revision_timestamp = 'revision_timestamp') || (isset($base_fields['revision_created'])) && $revision_timestamp = 'revision_created') {
-        $this->revision_metadata_keys['revision_created'] = $revision_timestamp;
-      }
-      if ((isset($base_fields['revision_log']) && $revision_log = 'revision_log') || (isset($base_fields['revision_log_message']) && $revision_log = 'revision_log_message')) {
-        $this->revision_metadata_keys['revision_log_message'] = $revision_log;
-      }
-    }
timmillwood’s picture

Status: Needs work » Needs review

I would hope hasRevisionMetadataKey() returns FALSE if there is not revision_metadata_key set.

We have updated Node and BlockContent, so the only BC we need to cover is contrib entity types who are defining a 'revision_timestamp', 'revision_uid', 'revision_log', 'revision_created', 'revision_user', or 'revision_log_message' field. As you say, we are doing this in SqlContentEntityStorage.

The biggest benefit of the patch in #92 is that we can now use RevisionLogEntityTrait for Node and BlockContent, which also means they can extend RevisionableContentEntityBase.

Also, when we introduce EntityPublishedInterface there is talk about creating a new base class which uses EntityPublishedTrait and extends RevisionableContentEntityBase. Again we wouldn't be able to use this for Node or BlockContent.

Ultimately I find it crazy that we have a revisionable base class which neither of the core revisionable entity types can use. This is the issue to unblock that!

hchonov’s picture

But the revision metadata key is there, it is not just set by the developers in the annotation and if we do not cover this case until the annotation for the revision metadata keys is made obligatory it makes the whole idea of revision metadata keys currently unnecessary, as would not be able to do stuff with these keys if we cannot retrieve them.

timmillwood’s picture

The ultimate issue is, we can't use it with Node and BlockContent.

hchonov’s picture

The problem this issue is solving is that the revision metadata fields are not in the correct tables. For Node and BlockContent we have to find another way.

timmillwood’s picture

but if this issue gets in there will never be a good way to fix Node and BlockContent. So it makes sense just to fix this.

amateescu’s picture

The problem this issue is solving is that the revision metadata fields are not in the correct tables. For Node and BlockContent we have to find another way.

The original scope of this issue also included updating Node and BlockContent to use the trait (see #7 and #9), so I really think that we should find a good way for them to be able to use RevisionLogTrait in this issue.

But the revision metadata key is there, it is not just set by the developers in the annotation and if we do not cover this case until the annotation for the revision metadata keys is made obligatory it makes the whole idea of revision metadata keys currently unnecessary, as would not be able to do stuff with these keys if we cannot retrieve them.

If that's the only problem with the patch in #92, then I propose to make the revision metadata keys required in 8.3.x if your entity type implements the newly added \Drupal\Core\Entity\ContentEntityTypeRevisionMetadataInterface.

This way, when people upgrade to 8.3.0 they will get an update error message with a clear explanation of what they need to do in order to complete the update successfully.

Another good thing about this approach is that we would no longer need the BC code from \Drupal\Core\Entity\Sql\SqlContentEntityStorage::getTableMapping() as we will be able to rely on the existence of the revision metadata keys.

What do you think, does this solve your concerns?

Status: Needs review » Needs work

The last submitted patch, 99: 2248983-99.patch, failed testing.

Berdir’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/ContentEntityType.php
    @@ -41,4 +48,41 @@ protected function checkStorageClass($class) {
    +   */
    +  public function getRevisionMetadataKeys() {
    +    // Provide backwards compatibility in case the revision metadata keys are
    +    // not defined in the entity annotation.
    +    if (!$this->revision_metadata_keys) {
    +      $base_fields = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions($this->id());
    +      if ((isset($base_fields['revision_uid']) && $revision_user = 'revision_uid') || (isset($base_fields['revision_user']) && $revision_user = 'revision_user')) {
    

    As mentioned in IRC, I think having a BC for existing entity types that use the trait would be good to have.

    That said, we IMHO do not need BC for a method that didn't exist before. We just need a BC for existing code that used to hardcode this information continuous to work.

  2. +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
    @@ -294,17 +295,26 @@ public function getTableMapping(array $storage_definitions = NULL) {
           $revisionable = $this->entityType->isRevisionable();
    +      if ($this->entityType instanceof ContentEntityTypeRevisionMetadataInterface) {
    +        $revision_metadata_fields = $revisionable ? array_values($this->entityType->getRevisionMetadataKeys()) : [];
    +      }
    +      else {
    +        // For backward comparability reasons we need to mention explicitly the
    +        // revision metadata fields in case the entity type does not implement
    +        // the ContentEntityTypeRevisionMetadataInterface.
    

    Here we actually have a BC already. I'm not sure if need the new optional interface here, Our current rules afaik allow adding new methods if we can provide a default implementation in a base class that we can expect everyone to use (the chance of someone providing a custom entity type class is pretty much 0.. not sure if it is even possible), which is the case here.

    So this would already be enough BC.

    But I do agree that the if we switch the trait to the new method, then we should have a BC there.

    And there are multiple variants for that:

    a) Inline it: $entity_type->getRevisionMetadataKey('revision_user') ? 'revision_user'
    b) Add a separate method to get it without BC, deprecate it, clarifying that in 9.x, the other one will return it.
    c) Add a separate method, only do the BC there, deprecate as well.
    d) Add an argument to skip BC.

    IMHO, a) would be enough, unless we expect that this will be a common case that developers will need an API for.. which I doubt. If you have an entity, you can and should use the methods provided by \Drupal\Core\Entity\RevisionLogInterface, you only need the metadata for an entity type when you have no entity object or when writing/changing those values.. which is not something that a lot of contrib/custom code will need to do in a generic way.

amateescu’s picture

Status: Needs work » Needs review
FileSize
231.37 KB
12.92 KB

Here's a patch that adds a "bc" parameter to getRevisionMetadataKeys() and removes the new interface. This is option d) from #101.

The interdiff is to #81.

Berdir’s picture

+++ b/core/lib/Drupal/Core/Entity/ContentEntityTypeInterface.php
@@ -6,4 +6,48 @@
+   * @param bool $include_backwards_compatibility_field_names
+   *   (optional) Whether to provide the revision keys on a best-effort basis by
+   *   looking at the base fields defined by the entity type. Defaults to FALSE.

not sure if it should be TRUE or FALSE by default.

Since we have it, it probably should be TRUE by default. It's actually not accessible for (get|has)RevisionMetadataKey(), so if you want BC, you'd have to use that always.

We should probably also explicit document that the argument will be removed in 9.x and it will then behave as if FALSE was passed in?

hchonov’s picture

I agree with berdir and I also think the parameter should be TRUE by default and its removal in 9.x has to be documented as well.

amateescu’s picture

My initial thoughts when writing the patch in #102 were that the new API should be like we want it to behave in 9.0.x, but after sleeping on it I realized that the default value of the bc parameter doesn't matter since in 9.0.x the method will only look at what it's in the annotation.

Changed it to TRUE if that's what needed to get this RTBC :)

Berdir’s picture

+++ b/core/lib/Drupal/Core/Entity/RevisionLogEntityTrait.php
@@ -26,20 +26,21 @@
-    $revision_created_key = $entity_type->getRevisionMetadataKey('revision_created') ?: 'revision_created';
+    $revision_metadata_keys = $entity_type->getRevisionMetadataKeys(FALSE);
+    $revision_created_key = isset($revision_metadata_keys['revision_created']) ? $revision_metadata_keys['revision_created'] : 'revision_created';
     $fields[$revision_created_key] = BaseFieldDefinition::create('created')

Nitpick: You could simply add defaults to the $revision_metadata_keys with += ['revision_created' => 'revision_created', ... then you can just use the key and don't need the conditions.

amateescu’s picture

That's true, and we also need to generalize this for the rest of the methods in the trait.

timmillwood’s picture

Status: Needs review » Reviewed & tested by the community

We've come to a good conclusion here.

As we start to make more things revisionable, and potentially get more revisionable metadata fields this patch makes perfect sense.

@Berdir, @tstoeckler, etc any final thoughts? We don't want to rush this in, but after 2.5 years and over 100 patches it looks good.

Berdir’s picture

Works for me.

catch’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/modules/system/system.install
@@ -1768,3 +1768,127 @@ function system_update_8201() {
+            foreach (\Drupal::configFactory()->listAll('views.view.') as $view_name) {
+              $view_config = \Drupal::configFactory()->get($view_name);
+              $displays = $view_config->get('display');
+              $changed = FALSE;
+              foreach ($displays as $display => &$display_data) {
+                if (isset($display_data['display_options']['fields'])) {
+                  foreach ($display_data['display_options']['fields'] as $property_name => &$property_data) {
+                    if (isset($property_data['entity_type']) && ($property_data['entity_type'] == $entity_type_id) && isset($property_data['field']) && ($property_data['field'] == $revision_metadata_field_name) && isset($property_data['table']) && ($property_data['table'] != $revision_table)) {
+                      $property_data['table'] = $revision_table;
+                      $changed = TRUE;
+                    }
+                  }
+                }
+              }
+              unset($display_data, $property_data);
+              if ($changed) {
+                $view_config = \Drupal::configFactory()->getEditable($view_name);
+                $view_config->set('display', $displays)
+                  ->save(TRUE);
+              }
+            }
+          }
+        }
+        $revisions = NULL;

The views should be done in a post update hook separately, and in views.install?

Also doing the config changes here won't catch default views - shouldn't this be done in preSave() or a config listener so it covers both? Then the post update just has to load and save with no manipulation of the view.

Didn't review the rest of the patch, just this has come up with nearly every config update so far so figured check it early.

amateescu’s picture

I started to write a config listener for this but I found out that ConfigEvents::SAVE is fired after the configuration object is saved, so we can not change anything from the config object unless we save it again.

I think we need a pre-save config event in order to do this properly..

amateescu’s picture

Status: Needs work » Needs review
FileSize
236.56 KB
4.63 KB

Fixed #110 by moving the table fix code to \Drupal\views\Entity\View::preSave().

Status: Needs review » Needs work

The last submitted patch, 112: 2248983-112.patch, failed testing.

hchonov’s picture

If we do it like this then I would check that the post update function is not yet executed and then have the logic in View::preSave otherwise just skip it.

The last submitted patch, 112: 2248983-112.patch, failed testing.

hchonov’s picture

And also in which Drupal version should we mark this code for removal? If this gets into Drupal 8.3.x could we expect that everybody updates from 8.2.x to 8.3.x or we have to support the case that people might update from 8.2.x to 8.3+.x? This would mean we have to carry the code in View::preSave until Drupal 9?

The last submitted patch, 112: 2248983-112.patch, failed testing.

The last submitted patch, 112: 2248983-112.patch, failed testing.

amateescu’s picture

Status: Needs work » Needs review

If we do it like this then I would check that the post update function is not yet executed and then have the logic in View::preSave otherwise just skip it.

I don't think we ever check in runtime code if an update function was executed or not, we always assume they are executed as soon as the code base is updated.

And also in which Drupal version should we mark this code for removal? If this gets into Drupal 8.3.x could we expect that everybody updates from 8.2.x to 8.3.x or we have to support the case that people might update from 8.2.x to 8.3+.x? This would mean we have to carry the code in View::preSave until Drupal 9?

We have to support upgrading to any future 8.x.y version until 9.x, so yes, we have to keep this code until then.

Status: Needs review » Needs work

The last submitted patch, 112: 2248983-112.patch, failed testing.

hchonov’s picture

I don't think we ever check in runtime code if an update function was executed or not, we always assume they are executed as soon as the code base is updated.

So after the update is executed this additional und unnecessary code will always run each time a view entity is saved?

timmillwood’s picture

Status: Needs work » Needs review

I can't seem to replicate any of the test fails in #112.

Status: Needs review » Needs work

The last submitted patch, 112: 2248983-112.patch, failed testing.

timmillwood’s picture

Status: Needs work » Needs review

So it passes with php7.

amateescu’s picture

So after the update is executed this additional und unnecessary code will always run each time a view entity is saved?

Yes. Consider this scenario:

1. you have a custom module written that provides a default view config entity, that's currently uninstalled so the view is not in "active" configuration
2. you update core and the post update function runs, all your active views are changed accordingly
3. ... time passes ...
4. you enable your custom module, and the default view is installed. however, the view will be broken because it will look for the metadata fields in the wrong table, since it wasn't updated in step 2.

So the only way for us to be sure that you're not dealing with a broken view is if we execute that BC code on every view presave :)

Status: Needs review » Needs work

The last submitted patch, 112: 2248983-112.patch, failed testing.

amateescu’s picture

Status: Needs work » Needs review

All the random test fails are because of #2828559: UpdatePathTestBase tests randomly failing, but I'm going to keep on re-testing until we get a green patch for PHP 5.5 as well :/

Status: Needs review » Needs work

The last submitted patch, 112: 2248983-112.patch, failed testing.

The last submitted patch, 112: 2248983-112.patch, failed testing.

catch’s picture

#125 is exactly why the logic needs to be in preSave() and not the update, and why it has to exist until 9.x

hchonov’s picture

Could we get someone to review the updating of views as I am not an expert there and I've wrote the update code just by looking at couple of views configs but probably I've overlooked something?

The last submitted patch, 112: 2248983-112.patch, failed testing.

The last submitted patch, 107: 2248983-107.patch, failed testing.

The last submitted patch, 112: 2248983-112.patch, failed testing.

The last submitted patch, 112: 2248983-112.patch, failed testing.

timmillwood’s picture

Re-roll of #112 due to system.install not applying.

Status: Needs review » Needs work

The last submitted patch, 136: 2248983-136.patch, failed testing.

The last submitted patch, 136: 2248983-136.patch, failed testing.

The last submitted patch, 136: 2248983-136.patch, failed testing.

timmillwood’s picture

Status: Needs work » Needs review

In #1812202: Add UUID support for entity revisions we are looking to add a new revision metadata field. Therefore it'd be great to get this issue in first. It's still weirdly failing.

tstoeckler’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/system/src/Tests/Entity/Update/MoveRevisionMetadataFieldsUpdateTest.php
    @@ -0,0 +1,93 @@
    +use Drupal\Core\Entity\ContentEntityInterface;
    +use Drupal\Core\Entity\ContentEntityStorageInterface;
    +use Drupal\Core\Entity\ContentEntityTypeInterface;
    +use Drupal\Core\Entity\RevisionLogInterface;
    ...
    +      /** @var ContentEntityStorageInterface $storage */
    ...
    +      /** @var ContentEntityTypeInterface $entity_type */
    ...
    +      /** @var ContentEntityInterface | RevisionLogInterface $entity_rev_first */
    ...
    +      /** @var ContentEntityInterface | RevisionLogInterface $entity_rev_second */
    

    We usually do fully-qualified class references in inline variable typehints (i.e. with the full namespace). That way they will also be marked as unused in PhpStorm, which is good, as they are actually unused and phpcs will complain with the current patch.

  2. +++ b/core/modules/system/src/Tests/Entity/Update/MoveRevisionMetadataFieldsUpdateTest.php
    @@ -0,0 +1,93 @@
    +        if ($entity_type->isTranslatable()) {
    +          $this->assertFalse($database_schema->fieldExists($data_table, $revision_metadata_field_name));
    +          $this->assertFalse($database_schema->fieldExists($revision_data_table, $revision_metadata_field_name));
    +        }
    +        else {
    +          $this->assertFalse($database_schema->fieldExists($base_table, $revision_metadata_field_name));
    +        }
    

    Minor, but I guess we could do the assertion on the base table always, even for translatable entity types. I think that would make the code just a tad clearer.

  3. +++ b/core/modules/system/src/Tests/Entity/Update/MoveRevisionMetadataFieldsUpdateTest.php
    @@ -0,0 +1,93 @@
    +      // Test that the views using revision metadata fields are updated
    +      // properly.
    ...
    +            $this->assertEqual($property_data['table'], $revision_table);
    

    I think it would be awesome to actually execute the view and check that we have some results. The current assertions are good, though, let's leave those in place as well.

  4. +++ b/core/modules/system/system.install
    @@ -1794,3 +1794,105 @@ function system_update_8203() {
    +/**
    + * Move revision metadata fields to the revision table.
    + *
    + * Due to the fields from RevisionLogEntityTrait not being explicitly mentioned
    + * in the storage they might have been installed wrongly in the base table for
    + * revisionable untranslatable entities and in the data and revision data
    + * tables for revisionable and translatable entities.
    + */
    +function system_update_8300(&$sandbox) {
    

    First of all: Thank you for adding such detailed documentation. Especially in these kind of tricky issues, that is so, so helpful and not many people take the time to write this up so nicely. Yay!

    However: This comment is actually parsed for displaying a description when running update.php and such multi-line comments look pretty strange in that case. Therefore, it's better to move the second part to an inline comment at the top of the function.

  5. +++ b/core/modules/system/system.install
    @@ -1794,3 +1794,105 @@ function system_update_8203() {
    +  static $revisions = NULL;
    ...
    +              // Collect the revision ids only once for an entity type.
    +              $revisions = isset($revisions) ? $revisions : array_keys(\Drupal::entityQuery($entity_type_id)
    +                ->allRevisions()
    +                ->execute());
    ...
    +        $revisions = NULL;
    

    I think it would be better to avoid a static here. Can we not just fetch the revisions outside of the foreach?

  6. +++ b/core/modules/system/system.install
    @@ -1794,3 +1794,105 @@ function system_update_8203() {
    +    if (is_subclass_of($entity_type->getClass(), \Drupal\Core\Entity\FieldableEntityInterface::class) && $entity_type->isRevisionable()) {
    

    Let's add a use statement at the top and then just use the classname here.

  7. +++ b/core/modules/system/system.install
    @@ -1794,3 +1794,105 @@ function system_update_8203() {
    +        $base_table = $entity_type->getBaseTable() ?: $entity_type_id;
    +        $data_table = $entity_type->getDataTable() ?: $entity_type_id . '_field_data';
    +        $revision_data_table = $entity_type->getRevisionDataTable() ?: $entity_type_id . '_field_revision';
    +        $revision_table = $entity_type->getRevisionTable() ?: $entity_type_id . '_revision';
    

    As this table fallback is always cause of much fun and/or headache, can we at least add a @see to \Drupal\Core\Entity\Sql\SqlContentEntityStorage::initTableLayout()?

  8. +++ b/core/modules/system/system.install
    @@ -1794,3 +1794,105 @@ function system_update_8203() {
    +          // revision data table install its definition again with the updated
    

    Again, thank you for this comment block, that is incredibly helpful.

    But you know me, I also need to complain ;-): Missing comma after "table". I'm usually not that picky ;-), but here it's actually a bit hard to read currently.

  9. +++ b/core/modules/system/system.install
    @@ -1794,3 +1794,105 @@ function system_update_8203() {
    +          // revision table, afterwards copy over the field values and remove
    

    Can we have a period (.) and a new sentence after "table" here? My brain currently does not have enough RAM to parse the entire sentence.

  10. +++ b/core/modules/system/system.install
    @@ -1794,3 +1794,105 @@ function system_update_8203() {
    +            if ($entity_type->isTranslatable()) {
    

    I don't see the case where non-translatable entity types are being handled. What am I missing?

    Edit: Now I understand that in the non-translatable case actually no data needs to be migrated. That's awesome, but this could actually use a comment, because it's not clear in my opinion when just looking at this function.

  11. +++ b/core/modules/system/system.install
    @@ -1794,3 +1794,105 @@ function system_update_8203() {
    +              if (!isset($sandbox['progress'])) {
    ...
    +                $sandbox['max'] = count($revisions);
    ...
    +              unset($sandbox['progress'], $sandbox['#finished']);
    

    So we're actually performing a whole batch per entity-type and then resetting the batch after each entity type. I've never seen that before and it does make me slightly uneasy. Won't the progress bar in the UI go back to being empty after having been complete when switching from one entity type to another?

    I think it would be better to instead have the following structure in $sandbox:

    $sandbox['current_entity_type'] = ...;
    $sandbox['current][$entity_type_id] = ...;
    

    Then we could avoid the unsetting the sandbox information.

  12. +++ b/core/modules/system/system.install
    @@ -1794,3 +1794,105 @@ function system_update_8203() {
    +                $fetch_from_table = $entity_type->isTranslatable() ? $revision_data_table : $base_table;
    

    So above we actually checked whether the fields somehow ended up the data table but it seems this is not handled here. So I guess we can drop that check from above, but not sure...

    Also (minor): I think $fetch_from_table is a somewhat misleading variable name, it sounds like a boolean. Why not just $table ?

  13. +++ b/core/modules/system/system.install
    @@ -1794,3 +1794,105 @@ function system_update_8203() {
    +                $database_schema->dropField($data_table, $revision_metadata_field_name);
    +                $database_schema->dropField($revision_data_table, $revision_metadata_field_name);
    

    Ahh, so I guess my earlier comment about the $fetch_from_comment line was invalid. I guess there is data in both the data table and the revision data table, but it is sufficient to copy the data from the revision data table as that is a superset of the data in the data table? If so, would be nice to add a comment to the $fetch_from_table line above.

  14. +++ b/core/modules/views/src/Entity/View.php
    @@ -290,10 +291,32 @@ public function calculateDependencies() {
    +            if (is_subclass_of($entity_type->getClass(), FieldableEntityInterface::class) && $entity_type->isRevisionable()) {
    

    Great update code for the views. Very defensive and safe which is exactly how it should be.

    One question: This does not actually affect views on non-translatable (but revisionable) entity types, correct? Because in that case it seems the views field already (correctly) specifies the revision table.

    You don't necessarily need to update this, I'm just asking to make sure I understand this correctly. (If you do add a check for translatability, please add a comment to this effect.)

The last submitted patch, 136: 2248983-136.patch, failed testing.

amateescu’s picture

Status: Needs work » Needs review
FileSize
237.06 KB
9.4 KB

Great review! :)

1: Fixed.
2: Personally, I think it's correct to test both cases because it properly tests the logic in system_update_8300() which drops the fields from different tables if the entity types is translatable or not.
3: Not done yet, coming in a folloup patch.
4: 5: 6: 7: 8: 9: 10: Fixed.
11: I'm not sure that nesting stuff under $sandox['progress'] will work, wouldn't that disable the progress bar completely?
12: 13: Yeah, we don't need to check which table to get the data from. We already check above that we're dealing with a translatable entity type, so the data is always in the revision data table. Fixed by removing the $fetch_from_table assignment.
14: That's right, we don't need to do anything for non-translatable entity types. Added a check and a comment :)

tstoeckler’s picture

Thanks for the update, looks great!

Re 11: Yes, messing with $sandbox['progress'] is not good, but AFAIK

$sandbox['current']

is not part of the API and we can totally nest that. Also, if other people are fine with the current batch implementation, we can leave it, it just makes me personally a bit uneasy, but Batch API is weird and confusing either way, so.... :-)

+++ b/core/modules/system/src/Tests/Entity/Update/MoveRevisionMetadataFieldsUpdateTest.php
@@ -0,0 +1,93 @@
+namespace Drupal\system\Tests\Entity\Update;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\ContentEntityStorageInterface;
+use Drupal\Core\Entity\ContentEntityTypeInterface;
+use Drupal\Core\Entity\RevisionLogInterface;

There should be a newline after the namespace. Also the use statements should now be unused.

I guess can be fixed on commit.

I want to read through the issue again, though, before marking RTBC, so not doing that just yet. Of course feel free to beat me to it.

Status: Needs review » Needs work

The last submitted patch, 143: 2248983-143.patch, failed testing.

amateescu’s picture

Status: Needs work » Needs review
FileSize
30.9 KB

I have a feeling that all those fails are not really random, but they are caused by the patch somehow. So let's try to see how a patch without any test fares. This is simply #143 without the added test coverage.

Status: Needs review » Needs work

The last submitted patch, 146: 2248983-146-without-tests.patch, failed testing.

tstoeckler’s picture

Issue summary: View changes
+++ b/core/lib/Drupal/Core/Entity/RevisionLogEntityTrait.php
@@ -101,15 +101,38 @@ public function setRevisionUserId($user_id) {
+    $revision_metadata_keys = $entity_type->getRevisionMetadataKeys(FALSE) + [
+      'revision_created' => 'revision_created',
+      'revision_user' => 'revision_user',
+      'revision_log_message' => 'revision_log_message',
+    ];

I think this deserves a comment:

We need to prevent ContentEntityType::getRevisionMetadataKey() from providing fallbacks as that requires fetching the entity type's field definition leading to infinite recursion.

This is quite an intricate detail so marking needs work for that. Also updating the issue summary to make note of the boolean flag.

Also just a reminder that this still needs a change record.

hchonov’s picture

Status: Needs work » Needs review
FileSize
1.62 KB

I have the feeling that probably the problem is that we update the views in View::preSave.. I know it is not correct doing this in hook_update_n, but I just wanna see what happens. So please ignore this patch.

hchonov’s picture

hchonov’s picture

Hmm this is strange, somehow the testbot does not like the name of the patch or why is it skipping it? Last try with a simpler name.

The last submitted patch, 143: 2248983-143.patch, failed testing.

amateescu’s picture

I also tried out some things in #2126447-92: Patch testing (and #91) and View::preSave() is the problem indeed. However, I am away until Tuesday and I won't be able to dig deeper until then.

hchonov’s picture

Hmm would it change anything if we trust the data when saving the views? Probably we have to do this no matter what, right?
Making only this change against #143.

Status: Needs review » Needs work

The last submitted patch, 154: 2248983-154.patch, failed testing.

hchonov’s picture

Status: Needs work » Needs review
FileSize
236.58 KB
513 bytes

Oups got the patch wrong..

Status: Needs review » Needs work

The last submitted patch, 156: 2248983-155.patch, failed testing.

hchonov’s picture

Status: Needs work » Needs review
FileSize
237 KB
9.77 KB

Small changes against #143 and also found a bug in View::preSave where we haven't checked correctly if the field name is part of the revision metadata fields, which means that the testing for the correctly updated views wasn't working as the test passed with PHP 7. I've changed that and now instead of loading the config I am directly loading the view entity and through it its displays for checking.

I've addressed #144 about the new line after the namespace and #148 about the comment.

Let's see what happens now.

tstoeckler’s picture

+++ b/core/modules/views/views.post_update.php
@@ -197,6 +197,7 @@
+      $view->trustData();

This seems strange. Can you explain why this is necessary?

Status: Needs review » Needs work

The last submitted patch, 158: 2248983-158.patch, failed testing.

hchonov’s picture

I am not sure that we really need this, I was just thinking that because we are re-saving views and change them with a trusted data we probably have to do this.

Berdir’s picture

->trustData() shouldn't be required in post update functions but it also shouldn't hurt if you know that your data structure has the correct types (e.g, integers are actually integers and not strings and so on)

It is important in update functions because your change there might not actually match the schema of the current schema definition, so you explicitly want to avoid that it is being checked. but since a post update should only make content and not structural changes, it *should* be optional ;)

tstoeckler’s picture

OK, then let's remove it, especially because you added it to a pre-existing post update function.

timmillwood’s picture

What's the difference between #143 and #151? Can we get an interdiff?

hchonov’s picture

@tstoeckler: Oh yes.. I wanted to add it to the new post update function also to test if the update failure here has something to do with the trusted data or not...

@timmillwood: I was testing with updating view configs directly in hook_update_n instead of triggering resave in hook_post_update and having the update logic in View::preSave. As @amateescu has found independently this is where the patch fails.

hchonov’s picture

Status: Needs work » Needs review
FileSize
236.71 KB
686 bytes

So I just wanna make sure the problem is not caused by not trusting the data.

hchonov’s picture

And a patch without trusting the data on saving the view entities.

Status: Needs review » Needs work

The last submitted patch, 167: 2248983-167.patch, failed testing.

The last submitted patch, 166: 2248983-166-trust-data.patch, failed testing.

hchonov’s picture

Status: Needs work » Needs review
FileSize
237.35 KB
1.44 KB

I have the feeling it fails because of some timeouts with PHP 5.5. I've added a batch to the post update function.

hchonov’s picture

Oups I've forgot to remove a line of the copy paste for the sandbox :).

The last submitted patch, 170: 2248983-170-post-update-with-batch.patch, failed testing.

hchonov’s picture

So the problem are not timeouts and the batch is not needed but I think I've found out the problem with PHP 5.5...

The last submitted patch, 171: 2248983-171-post-update-with-batch.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 173: 2248983-173.patch, failed testing.

hchonov’s picture

Fixed the failing test where views of revisionable non-translatable entities weren't updated.

P.S. and also instead of using foreach by reference in View::preSave I try now directly modifying the values.

hchonov’s picture

So the problem with PHP 5.5 was the foreach by reference and altering the values ... Oh..

So after we have a passing patch now I've decided to move the logic from View::preSave into a private function which I've marked as deprecated, this should make it easy to remove and not forget in Drupal 9.

tstoeckler’s picture

The patch looks good to me. The only thing that sticks out to me is unsetting of the batch progress stuff, I would like someone else to confirm that that is OK before marking RTBC. Also this needs a change record anyway before RTBC and it's tagged "Needs documentation", not exactly sure why.

hchonov’s picture

I've reimplemented the batch which should now be progressing pro entity type instead pro revision of an entity type. I hope this looks better now?

Status: Needs review » Needs work

The last submitted patch, 179: 2248983-179.patch, failed testing.

hchonov’s picture

Status: Needs work » Needs review
FileSize
238.4 KB

Re-roll only as hook_update_n has been added to system.install.

hchonov’s picture

Issue summary: View changes
Issue tags: -Needs change record

Created a change record - https://www.drupal.org/node/2831499.

Feel free to publish it as soon as this gets committed.

hchonov’s picture

Issue summary: View changes
hchonov’s picture

Issue summary: View changes

Status: Needs review » Needs work

The last submitted patch, 181: 2248983-181.patch, failed testing.

hchonov’s picture

Status: Needs work » Needs review
FileSize
239.23 KB
6.83 KB

I had couple of errors in the batch processing.. I hope that now it is better.

Status: Needs review » Needs work

The last submitted patch, 186: 2248983-186.patch, failed testing.

The last submitted patch, 186: 2248983-186.patch, failed testing.

tstoeckler’s picture

  1. +++ b/core/modules/system/system.install
    @@ -1813,8 +1814,22 @@
    +    $sandbox['entity_type_ids'] = array_keys(\Drupal::entityTypeManager()->getDefinitions());
    +    $sandbox['max'] = count($sandbox['entity_type_ids']);
    ...
    +    if (is_subclass_of($entity_type->getClass(), FieldableEntityInterface::class) && ($entity_type instanceof ContentEntityTypeInterface) && $entity_type->isRevisionable()) {
    

    Maybe we could pre-filter the definitions so we don't "process" a bunch of unaffected entity types.

  2. +++ b/core/modules/system/system.install
    @@ -1847,18 +1862,17 @@
                 // No data needs to be migrated if the entity type is not
                 // translatable.
                 if ($entity_type->isTranslatable()) {
    
    @@ -1869,13 +1883,13 @@
    -            $sandbox['#finished'] = $entity_type->isTranslatable() ? $sandbox['current'] == $sandbox['max'] : TRUE;
    +            $sandbox[$entity_type_id]['#finished'] = $entity_type->isTranslatable() ? $sandbox[$entity_type_id]['current'] == $sandbox[$entity_type_id]['max'] : TRUE;
    

    As this is now inside the ->isTranslatable() branch the check is no longer necessary here.

    Also $sandbox['#finished'] has special meaning in Batch API, while $sandbox[$entity_type_id]['#finished'] does not - so we could just as well use $sandbox[$entity_type_id]['finished']. I can see how we want to make it consistent, though, so I'm with the current way, just wanted to point it out.

  3. +++ b/core/modules/system/system.install
    @@ -1847,18 +1862,17 @@
                     // This must be the first run. Initialize the sandbox.
    

    Can we change this to "This mist be the first run for this entity type."?

  1. +++ b/core/modules/system/system.install
    @@ -1853,35 +1853,40 @@
    +            $revisions = array_keys(\Drupal::entityQuery($entity_type_id)
    +              ->allRevisions()
    +              ->execute());
    +            $sandbox[$entity_type_id]['revisions'] = $revisions;
    
    @@ -1892,30 +1897,43 @@
    +                $steps_executed++;
    ...
    +          $sandbox[$entity_type_id]['current'] += $steps_executed;
    

    Hmm... I guess since this is really just the integer this may be fine, but if you have a huge number of revisions (think Drupal.org) this will still eat up a lot of memory. What is sometimes done to prevent this is to add a deterministic sort to the query (i.e. revision ID) and then just count the number of processed revisions and then use that as an offset in the query.

    Maybe others have more experience with this on big sites.

    Since you're already recording the number of revisions processed (vs. the actual revision ID(s)) this may actually be easy to change?

  2. +++ b/core/modules/system/system.install
    @@ -1853,35 +1853,40 @@
    +            $sandbox[$entity_type_id]['updates_pro_iteration'] = 50;
    

    This should be "updates_*per*_iteration" in English ;-)

  3. +++ b/core/modules/system/system.install
    @@ -1853,35 +1853,40 @@
    +              $entity_definition_update_manager->installFieldStorageDefinition($revision_metadata_field_name, $entity_type_id, $entity_type->getProvider(), $definition);
    

    Also should have noticed this earlier, but won't this mean that the field storage definition will be installed multiple times? Won't that be problematic?

    Can't this be moved in front of the ->isTranslatable() check - as it's also in the else branch?

hchonov’s picture

Status: Needs work » Needs review
FileSize
239.32 KB
11.02 KB

I've addressed #189.

Status: Needs review » Needs work

The last submitted patch, 190: 2248983-190.patch, failed testing.

hchonov’s picture

Status: Needs work » Needs review
FileSize
239.39 KB

Re-roll only.

tstoeckler’s picture

Yay, this looks great! Only some minor nitpicks left.

This was marked "Needs documentation" in #30 not sure why. This now has a change record, thank you, that looks good as well! Maybe @timmillwood can clarify what documentation specifically needs to be updated?

  1. +++ b/core/modules/system/system.install
    @@ -1794,3 +1797,141 @@ function system_update_8203() {
    +    $base_fields = $base_fields = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions($entity_type_id);
    

    Double assignment of $base_fields

  2. +++ b/core/modules/system/system.install
    @@ -1794,3 +1797,141 @@ function system_update_8203() {
    +          // This mist be the first run for this entity type. Initialize the
    

    "mist" -> "must" (Sorry, I realize you copied this from my comment)

  3. +++ b/core/modules/system/system.install
    @@ -1794,3 +1797,141 @@ function system_update_8203() {
    +      } else {
    ...
    +    } else {
    

    The else should be on a newline.

timmillwood’s picture

Issue tags: -Needs documentation

Not sure why I tagged it as needs documentation. I think the change record is sufficient.

hchonov’s picture

Addressing #193.

tstoeckler’s picture

Status: Needs review » Reviewed & tested by the community

Awesome, thank you so much for your persistence!

timmillwood’s picture

Issue tags: +Workflow Initiative

Tagging this for tracking.

(+1 to RTBC)

amateescu’s picture

catch’s picture

Category: Task » Bug report
Priority: Normal » Major
Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/lib/Drupal/Core/Entity/ContentEntityType.php
    @@ -41,4 +48,41 @@ protected function checkStorageClass($class) {
    +      $base_fields = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions($this->id());
    +      if ((isset($base_fields['revision_uid']) && $revision_user = 'revision_uid') || (isset($base_fields['revision_user']) && $revision_user = 'revision_user')) {
    +        $this->revision_metadata_keys['revision_user'] = $revision_user;
    +      }
    +      if ((isset($base_fields['revision_timestamp']) && $revision_timestamp = 'revision_timestamp') || (isset($base_fields['revision_created'])) && $revision_timestamp = 'revision_created') {
    +        $this->revision_metadata_keys['revision_created'] = $revision_timestamp;
    +      }
    +      if ((isset($base_fields['revision_log']) && $revision_log = 'revision_log') || (isset($base_fields['revision_log_message']) && $revision_log = 'revision_log_message')) {
    +        $this->revision_metadata_keys['revision_log_message'] = $revision_log;
    +      }
    

    In each of these three branches should we do a @trigger_error('...', E_USER_DEPRECATED)?

  2. +++ b/core/modules/system/src/Tests/Entity/Update/MoveRevisionMetadataFieldsUpdateTest.php
    @@ -0,0 +1,91 @@
    + * @see https://www.drupal.org/node/2248983
    

    git blame is plenty no? Or should we put the issue nid in the test class name - have done that elsewhere?

  3. +++ b/core/modules/system/system.install
    @@ -1794,3 +1797,142 @@ function system_update_8203() {
    +
    +/**
    + * Move revision metadata fields to the revision table.
    + */
    +function system_update_8300(&$sandbox) {
    +  // Due to the fields from RevisionLogEntityTrait not being explicitly
    +  // mentioned in the storage they might have been installed wrongly in the base
    +  // table for revisionable untranslatable entities and in the data and revision
    +  // data tables for revisionable and translatable entities.
    

    This update makes this a major bugfix rather than a task. I'm assuming there's absolutely no way to separate the new feature out from the bug fix?

  4. +++ b/core/modules/system/system.install
    @@ -1794,3 +1797,142 @@ function system_update_8203() {
    +          // Calculate the number of revisions to process.
    +          $count = \Drupal::entityQuery($entity_type_id)
    +            ->allRevisions()
    +            ->count()
    +            ->execute();
    +
    

    This is missing an accessCheck(FALSE) which means it would skip inaccessible revisions not available to the anon user with an access module enabled. If this affected nodes, that'd be a really nasty upgrade path bug, but it's still a theoretical issue with other entity types.

  5. +++ b/core/modules/system/system.install
    @@ -1794,3 +1797,142 @@ function system_update_8203() {
    +
    +        // Collect the revision IDs to process.
    +        $revisions = \Drupal::entityQuery($entity_type_id)
    +          ->allRevisions()
    +          ->range($sandbox[$entity_type_id]['current'], $sandbox[$entity_type_id]['current'] + $steps)
    +          ->execute();
    

    Same with accessCheck(FALSE) here.

timmillwood’s picture

Status: Needs work » Needs review
FileSize
3.07 KB
240.84 KB

Updating all items from #199.

#199.2 I think git blame should be good enough.
#199.3 I don't think it's possible to split.

amateescu’s picture

Status: Needs review » Reviewed & tested by the community

The interdiff from #200 looks ok to me :)

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 200: 2248983-200.patch, failed testing.

The last submitted patch, 200: 2248983-200.patch, failed testing.

amateescu’s picture

Status: Needs work » Needs review
FileSize
240.97 KB
804 bytes

This is the fix for Drupal\Tests\views\Unit\EntityViewsDataTest.

Drupal\system\Tests\Entity\Update\MoveRevisionMetadataFieldsUpdateTest was broken by #2669802-119: Add a content entity form which allows to set revisions and I posted a small followup patch for that here: #2835588: Restore EntityTestRev's behavior to not implement RevisionLogInterface. This patch should be green again when the followup is committed.

Status: Needs review » Needs work

The last submitted patch, 204: 2248983-204.patch, failed testing.

amateescu’s picture

Ok, I took @Berdir's suggestion from #2835588-8: Restore EntityTestRev's behavior to not implement RevisionLogInterface and moved EntityTestWithRevisionLog and EntityTestMulWithRevisionLog to a separate module and recreated the test db dump using that module.

Regarding #2835588-9: Restore EntityTestRev's behavior to not implement RevisionLogInterface, there's just one test that uses the pre-existing EntityTestWithRevisionLog class, so I think it's ok to not duplicate those two entity types.

hchonov’s picture

+++ b/core/modules/system/tests/modules/entity_test_revlog/src/Entity/EntityTestMulWithRevisionLog.php
similarity index 58%
rename from core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithRevisionLog.php

rename from core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithRevisionLog.php
rename to core/modules/system/tests/modules/entity_test_revlog/src/Entity/EntityTestWithRevisionLog.php

I am not really sure that it is ok to do this as probably contrib and custom is using it for tests as well..

How can we ensure that those entities even in a different module would not be modified as if they get modified the dump would not be up to date anymore?

Berdir’s picture

We don't provide BC for test entities, and those are pretty special and not used as often as entity_test, entity_test_rev and so on I think. So moving them is probably OK.

To avoid changes, we could maybe clearly name the module as entity_test_upgrade_test or something. But if we do make changes, also have the option to add more update functions to actually make it pass again.

hchonov’s picture

#199.2

This update makes this a major bugfix rather than a task. I'm assuming there's absolutely no way to separate the new feature out from the bug fix?

Actually it is possible to separate the bug fix from the new feature and for this to happen the bug fix should first introduce the new known revision metadata fields to the hard coded lists and have the update function a little bit adjusted and then let the new feature come in. However I am against separating it as the new feature is part of the solution of the bug and a beautiful one plus it adds the possibility with the change record for the developers to add to the entity annotation the revision metadata keys before the update is run in order to cover field names used for the revision metadata fields which are not the default ones used in the core and by doing this the update function will place them in the correct tables as well.

#206

+++ b/core/modules/system/tests/modules/entity_test_revlog/src/Entity/EntityTestMulWithRevisionLog.php
similarity index 58%
rename from core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithRevisionLog.php

rename from core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithRevisionLog.php
rename to core/modules/system/tests/modules/entity_test_revlog/src/Entity/EntityTestWithRevisionLog.php

+++ b/core/modules/system/tests/modules/entity_test_revlog/src/Entity/EntityTestWithRevisionLog.php
@@ -1,6 +1,6 @@
-namespace Drupal\entity_test\Entity;
+namespace Drupal\entity_test_revlog\Entity;

@@ -12,23 +12,8 @@
- *   handlers = {
- *     "list_builder" = "Drupal\entity_test\EntityTestListBuilder",
- *     "view_builder" = "Drupal\entity_test\EntityTestViewBuilder",
- *     "access" = "Drupal\entity_test\EntityTestAccessControlHandler",
- *     "form" = {
- *       "default" = "Drupal\entity_test\EntityTestForm",
- *       "delete" = "Drupal\entity_test\EntityTestDeleteForm"
- *     },
- *     "translation" = "Drupal\content_translation\ContentTranslationHandler",
- *     "views_data" = "Drupal\views\EntityViewsData",
- *     "route_provider" = {
- *       "html" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider",
- *     },
- *   },
...
- *   admin_permission = "administer entity_test content",

@@ -42,12 +27,6 @@
- *   links = {
- *     "canonical" = "/entity_test_revlog/manage/{entity_test_revlog}",
- *     "delete-form" = "/entity_test/delete/entity_test_revlog/{entity_test_revlog}",
- *     "edit-form" = "/entity_test_revlog/manage/{entity_test_revlog}/edit",
- *     "revision" = "/entity_test_revlog/{entity_test_revlog}/revision/{entity_test_revlog_revision}/view",
- *   }

Ok the moving of the entity definition to a separate module is fine, but why do we have to remove parts of the entity annotation? I think that they are removed because they are not really needed but this is kinda out of scope of this issue, right?

Beside that it look good now :)

tstoeckler’s picture

Status: Needs review » Reviewed & tested by the community

Yeah, I agree there is no way to fix this without the feature. The problem is that different entity types use different field names while the storage hardcodes them. The solution is to make them properly configurable and have the storage utilize that configuration (not in the CMI way, in the general way). That's precisely what the patch does.

Regarding the tests, on the one hand I can see that being considered out of scope, on the other hand we do treat dead code as a bug, so not removing that part of the annotation would increase the code debt for the future, so I think the removal is fine for now. Of course, the committers may disagree, but it would be really nice to get this is in soon, as this is a blocker for so many things, so setting back to RTBC.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/lib/Drupal/Core/Entity/ContentEntityType.php
    @@ -41,4 +48,44 @@ protected function checkStorageClass($class) {
    +  public function getRevisionMetadataKeys($include_backwards_compatibility_field_names = TRUE) {
    +    // Provide backwards compatibility in case the revision metadata keys are
    +    // not defined in the entity annotation.
    +    if (!$this->revision_metadata_keys && $include_backwards_compatibility_field_names) {
    +      $base_fields = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions($this->id());
    

    Is the BC layer tested? I couldn't spot an obvious test.

  2. +++ b/core/lib/Drupal/Core/Entity/ContentEntityType.php
    @@ -41,4 +48,44 @@ protected function checkStorageClass($class) {
    +        @trigger_error('The revision_user revision metadata key is not set.', E_USER_DEPRECATED);
    ...
    +        @trigger_error('The revision_created revision metadata key is not set.', E_USER_DEPRECATED);
    ...
    +        @trigger_error('The revision_log_message revision metadata key is not set.', E_USER_DEPRECATED);
    

    We so need to add the php unit symfony bridge thing to test the errors are being triggered.

  3. +++ b/core/lib/Drupal/Core/Entity/ContentEntityTypeInterface.php
    @@ -6,4 +6,50 @@
    +   */
    +  public function getRevisionMetadataKeys($include_backwards_compatibility_field_names = TRUE);
    

    I think we should add an @deprecated tag here so we know that we need to clean up the $includes_backwards_compatility_field_names before 9.0.0. I'm not sure we've worked out the best way of deprecating params

  4. +++ b/core/modules/views/src/Entity/View.php
    @@ -302,6 +307,45 @@ public function preSave(EntityStorageInterface $storage) {
    +   * @deprecated in Drupal 8.3.0, will be removed in Drupal 9.0.0.
    

    Good to see this here - so we know to remove it.

amateescu’s picture

Status: Needs work » Reviewed & tested by the community

Re #211:

1. It is tested in \Drupal\Tests\Core\Entity\Sql\SqlContentEntityStorageTest::testGetTableMappingRevisionableWithFields()

2. Yup :)

3. The parameter is marked as deprecated in the method's doc block:

   * @param bool $include_backwards_compatibility_field_names
   *   (optional and deprecated) Whether to provide the revision keys on a
   *   best-effort basis by looking at the base fields defined by the entity
   *   type. Note that this parameter will be removed in Drupal 9.0.0. Defaults
   *   to TRUE.

I didn't add the '@' symbol before deprecated because that makes PHPStorm mark the entire method as deprecated..

4. +1 :)

catch’s picture

I think it's fine to just have the word 'deprecated', while it won't warn in IDEs, the @trigger_error() will eventually warn developers, and it's greppable for 9.x removal.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 206: 2248983-206.patch, failed testing.

tstoeckler’s picture

Status: Needs work » Reviewed & tested by the community

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 206: 2248983-206.patch, failed testing.

tstoeckler’s picture

Status: Needs work » Reviewed & tested by the community

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 206: 2248983-206.patch, failed testing.

tstoeckler’s picture

Test failure seemed legit unfortunately (https://www.drupal.org/pift-ci-job/551861), but nevertheless sending for a retest just to make sure.

amateescu’s picture

Status: Needs work » Reviewed & tested by the community

#204 does have a legit failure indeed, but the good/RTBC one is #206 :)

tstoeckler’s picture

Oh, you're right. Sorry, the testbot confused me...

michaellenahan’s picture

Issue summary: View changes

Small typo fix

catch’s picture

  1. +++ b/core/modules/system/system.install
    @@ -1794,3 +1797,144 @@ function system_update_8203() {
    +        $steps = 50;
    

    Should this be settable in $settings - some sites might want to do a bigger size.

  2. +++ b/core/modules/system/system.install
    @@ -1794,3 +1797,144 @@ function system_update_8203() {
    +        // Collect the revision IDs to process.
    +        $revisions = \Drupal::entityQuery($entity_type_id)
    +          ->allRevisions()
    +          ->range($sandbox[$entity_type_id]['current'], $sandbox[$entity_type_id]['current'] + $steps)
    +          ->accessCheck(FALSE)
    +          ->execute();
    +        $revisions = array_keys($revisions);
    

    Shouldn't this have an explicit order by?

timmillwood’s picture

#223.1 - Not sure how much a $setting will get used? but wondering if the default value should be higher than 50.
#223.2 - I guess an order by makes sense, just so each iteration is processed the same.

hchonov’s picture

A bigger step size might be useful, but even a small one will do the job. We could increase it, but I don't think we should offer a setting for the step size. I couldn't imagine that someone is going to use it and if we introduce it it should be only for this update.

Why would an "order by" be needed? I don't think that it matters in which order the revisions are processed.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
  1. I think the update batch size is okay since the batch system will just fire again if under a second anyway.
  2. +++ b/core/modules/system/system.install
    @@ -1794,3 +1797,144 @@ function system_update_8203() {
    +  if (!isset($sandbox['progress'])) {
    ...
    +    $sandbox['progress'] = 0;
    

    Why not just checking if $sandbox['current'] is set? progress is never used apart from here.

  3. +++ b/core/modules/system/system.install
    @@ -1794,3 +1797,144 @@ function system_update_8203() {
    +        $sandbox[$entity_type_id]['current'] += count($revisions);
    +        $sandbox[$entity_type_id]['finished'] = $sandbox[$entity_type_id]['current'] == $sandbox[$entity_type_id]['max'];
    

    This looks brittle. If something unexpected created a revision or maybe more likely deleted a revision whilst the update was happening this potentially could break. I think all we need to ensure is that we finish also if $revisions is empty.

amateescu’s picture

Status: Needs work » Needs review
FileSize
214.6 KB
1.59 KB

Here's an update for #223.2 and #226.

I quite liked the idea to have a generic setting for how many entities to update in a batch run and we could've used it in #2721313: Upgrade path between revisionable / non-revisionable entities as well, but it seems I'm the only one who liked it so I didn't include it in the patch.

timmillwood’s picture

Status: Needs review » Reviewed & tested by the community

The interdiff in #227 looks good so moving back to RTBC.

catch’s picture

@amateescu, I liked it ;) see #118 on that issue.

hchonov’s picture

Such a feature would be out of scope, right? I think that it might be useful, but it then deserves its own issue.

catch’s picture

@hchonov I don't really think it's out of scope, we're adding updates that could run on hundreds of thousands/millions of records, which will be non-optional once sites need to update to 8.3.0+. Until there's an option of 8-8 migration paths for large sites, we need to make sure updates are going to actually run successfully for them.
Having said that @alexpott's comment on batch running multiple times if a second hasn't elapsed yet is valid - main issue is whether the initial query itself is a bottleneck since increasing the batch size is an easy way to run less queries.

amateescu’s picture

FileSize
1.33 KB

Here's the interdiff for a variable step size that I had laying around.. maybe it helps us come to a conclusion either way :)

timmillwood’s picture

I can't see any harm in adding #232. I know in #224 I expressed hesitation because it might not be used, but for such a simple change it does make me think "why not?".

hchonov’s picture

Ok then, we could add it, but then I think we need at least a separate Change Record for this :).

hchonov’s picture

+++ b/core/modules/system/system.install
@@ -1862,7 +1862,7 @@ function system_update_8300(&$sandbox) {
+        $steps = Settings::get('entity_update_batch_size', 50);

+++ b/sites/default/default.settings.php
@@ -745,6 +745,16 @@
+ * The default number of entities to update in a batch process.
...
+$settings['entity_update_batch_size'] = 50;

I would prefer not using the word "entity". I think "row" would be better. What do you think?

alexpott’s picture

Are we sure that the size thing is really going to matter... batch set up is not that expense and no one is addressing #226.1

Imo if we want longer running batches we should just make if ($batch['progressive'] && Timer::read('batch_processing') > 1000) { configurable.

amateescu’s picture

@hchonov, I think it's important to use the word 'entity' because we want people to be aware of the heavy-duty processing that will take place (i.e. all kinds of entity hooks firing). 'Rows' makes me think of database-only operations, which are usually much faster than what Entity API is doing.

@alexpott, to be honest, I don't really understand #226.1 and #236, but it seems that @catch does, so I'll let you both decide that. That's why I posted only the interdiff in the first place :)

catch’s picture

Even if a single batch request processes multiple iterations, even if we only did a single query per batch, that's 1m / 50 = 20,000.

Some queries are always going to be per each individual item, but assuming most systems will be able to process 100 or 500 items, that's a big difference in total queries required to finish the job. Equally if not more relevant when you have updates loading multiple entities etc.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 227: 2248983-227.patch, failed testing.

timmillwood’s picture

Status: Needs work » Needs review
FileSize
214.51 KB

Just a reroll of #227 due to system.install conflict.

tstoeckler’s picture

Status: Needs review » Reviewed & tested by the community

Still looking good.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

xjm’s picture

Status: Reviewed & tested by the community » Needs review

There are several comments since #227 that need to be addressed as well, so setting NR. Thanks everyone!

amateescu’s picture

So the remaining discussion is only about whether we should apply the interdiff from #232 or not? FWIW, I can happily live without it :)

tstoeckler’s picture

Status: Needs review » Reviewed & tested by the community

To be perfectly honest I think a setting for that is out of scope for this issue. If we want something like that we should have a general setting for how many entities an update should save/process etc. We have more of those in core already and for custom sites this would be even a greater win. Would also be super useful for Drush, etc. So I'm 100% percent for such a setting, but 100% against doing it here simply because it will unnecessarily increase scope/contention/etc.

Hope that's alright ;-)

xjm’s picture

Status: Reviewed & tested by the community » Needs review

If I'm reading this correctly, @catch, who is a performance topic coordinator and release manager, has said he thinks it is in scope. See #231 and #238. Hopefully I'm not mistaken about that.

amateescu’s picture

Version: 8.4.x-dev » 8.3.x-dev
FileSize
215.33 KB

Then let's apply the interdiff from #232 and be done with it :)

Edit:

If we want something like that we should have a general setting for how many entities an update should save/process etc.

That's exactly what the interdiff provides.

Status: Needs review » Needs work

The last submitted patch, 247: 2248983-247.patch, failed testing.

timmillwood’s picture

Status: Needs review » Reviewed & tested by the community

I was just uploading the same patch, so lets RTBC it!

amateescu’s picture

Minor conflict in system.install.

The last submitted patch, 247: 2248983-247.patch, failed testing.

catch’s picture

Version: 8.3.x-dev » 8.4.x-dev

OK I'm happy with this now, however given the upgrade path I think it's better to get it into 8.4.x very early than 8.3.x relatively later. Going to try to have one more look over before commit.

amateescu’s picture

I gave it another look as well and found that the update function was relying on latest entity type definitions from code, which is a big no-no :)

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 253: 2248983-253.patch, failed testing.

amateescu’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
215.5 KB
1.14 KB

Right, we need to exclude entity types for which we're not storing the last installed definition, like config entities.

hchonov’s picture

Now that #2808335: Changed time not updated when only non-translatable fields are changed on a translated entity got in we should use the new method of retrieving the revision metadata fields in ContentEntityBase:: getFieldsToSkipFromTranslationChangesCheck() as well.

amateescu’s picture

Yup, let's to do that.

  • catch committed 4b325ae on 8.4.x
    Issue #2248983 by hchonov, amateescu, paranojik, timmillwood, tstoeckler...
catch’s picture

Status: Reviewed & tested by the community » Fixed

Opened #2855315: Remove hard-coding of revision metadata fields in ContentEntityBase::getFieldsToSkipFromTranslationChangesCheck() for that change.

Thanks for all the work here, afaik this is the first patch to change entity storage anywhere on this scale, we've got 7 months to find bugs in the upgrade path so hopefully more people will test 8.4.x with real databases before then too.

Committed/pushed to 8.4.x, thanks!

edit: cross-posted - I think it's completely fine to do that in a follow-up, the original issue didn't have the API available, this issue adds it.

amateescu’s picture

Status: Fixed » Needs review
FileSize
856 bytes

@catch also realized in IRC that we should change the update function number if this patch won't end up in 8.3.x, so here's a small followup patch for that.

The last submitted patch, 257: 2248983-257.patch, failed testing.

catch’s picture

Status: Needs review » Reviewed & tested by the community

Committed/pushed the update rename to 8.4.x, thanks!

  • catch committed 67b8129 on 8.4.x
    Issue #2248983 by hchonov, amateescu, paranojik, timmillwood, tstoeckler...
xjm’s picture

Status: Reviewed & tested by the community » Fixed
jibran’s picture

Status: Fixed » Reviewed & tested by the community
+++ b/core/modules/system/src/Tests/Entity/Update/MoveRevisionMetadataFieldsUpdateTest.php
@@ -0,0 +1,83 @@
+  public function testSystemUpdate3000() {

Shouldn't this be 8400?

jibran’s picture

Status: Reviewed & tested by the community » Fixed

x-post sorry.

hchonov’s picture

#265 that is correct.

I hope that we do not need a separate issue for this patch?

hchonov’s picture

Status: Fixed » Needs review

If it couldn't be committed here feel free to close it again and I am sorry for re-opening it.

amateescu’s picture

Status: Needs review » Fixed
+++ b/core/modules/system/src/Tests/Entity/Update/MoveRevisionMetadataFieldsUpdateTest.php
@@ -26,7 +26,7 @@ public function setDatabaseDumpFiles() {
+  public function testSystemUpdate4000() {

Actually, this should be testSystemUpdate8400(), but since we already have a followup issue (#2855315: Remove hard-coding of revision metadata fields in ContentEntityBase::getFieldsToSkipFromTranslationChangesCheck()), I propose to fix it there.

Status: Fixed » Closed (fixed)

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

amateescu’s picture

We forgot to publish the change record for this issue. Just did that now :)

tstoeckler’s picture

Note to archeologists: This caused an interesting problem at #2976040: default_revision can be left around in data table due to broken entity type CRUD.