DESCRIPTION
When upgrading to latest 8.5.x alpha, the block_content_update_8400 hook results in an incorrect schema being generated for the block_content_revision table if no data is in the table when the updateEntityType() fires. See the attached files for a before and after picture of the block_content_revision table.
REPRODUCTION STEPS
1. Using an 8.4.x install, update composer.json to use 8.5.x-dev or 8.5.x-alpha1 for drupal/core. (we had some trouble with the alpha1 tag so we moved to the devx branch)
2. Add the following patch: https://www.drupal.org/files/issues/2936511-45.patch
3. Inspect the block_content_revision and see that it has revision_user, revision_log, revision_created columns.
4. Run drush updb
5. Inspect the block_content_revision and see that these columns are gone.
6. Try to login or access admin and get errors like the following (screenshot of full error with stack attached)
The website encountered an unexpected error. Please try again later.</br></br><em class="placeholder">Drupal\Core\Database\DatabaseExceptionWrapper</em>: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'revision.revision_user' in 'field list': SELECT revision.revision_id AS revision_id, revision.langcode AS langcode, revision.revision_user AS revision_user, revision.revision_created AS revision_created, revision.revision_log AS revision_log, revision.revision_default AS revision_default, 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->getFromStorage()</em> (line <em class="placeholder">467</em> of <em class="placeholder">core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php</em>). <pre class="backtrace">Drupal\Core\Database\Statement->execute(Array, Array) (Line: 625)
Drupal\Core\Database\Connection->query('SELECT revision.revision_id AS revision_id, revision.langcode AS langcode, revision.revision_user AS revision_user, revision.revision_created AS revision_created, revision.revision_log AS revision_log, revision.revision_default AS revision_default, 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, Array) (Line: 87)
Drupal\Core\Database\Driver\mysql\Connection->query('SELECT revision.revision_id AS revision_id, revision.langcode AS langcode, revision.revision_user AS revision_user, revision.revision_created AS revision_created, revision.revision_log AS revision_log, revision.revision_default AS revision_default, 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, Array) (Line: 510)
ANALYSIS
I believe that patch 2936511 adds behavior which doesn't play well with some code in the EntityDefinitionUpdateManager which uses the PHP "clone" keyword to make a copy of an entity type definition. The clone causes a loss of information resulting in an incorrect table schema being generated for the block_content_revision table (possibly others as well) when hook block_content_update_8400 runs. I actually have a suspicion that any revisionable entity type would suffer from this problem when EntityDefinitionUpdateManager::updateEntityType() is called with this patch installed although I have not confirmed that. The relevant files and lines are as follows.
ContentEntityType::__construct() Lines 39-55
// Only new instances should provide the required revision metadata keys.
// The cached instances should return only what already has been stored
// under the property $revision_metadata_keys. The BC layer in
// ::getRevisionMetadataKeys() has to detect if the revision metadata keys
// have been provided by the entity type annotation therefore we add keys to
// the property $requiredRevisionMetadataKeys only if those keys aren't set
// in the entity type annotation.
if (!isset($this->revision_metadata_keys['revision_default'])) {
$this->requiredRevisionMetadataKeys['revision_default'] = 'revision_default';
}
// Add the required revision metadata fields here instead in the getter
// method, so that they are serialized as part of the object even if the
// getter method doesn't get called, which allows us to further extend the
// list and let only new instances of the class to contain the new list
// while the cached instances contain the previous version of the list.
$this->revision_metadata_keys += $this->requiredRevisionMetadataKeys;
The gist of this code (I think) is to ensure that the new 'revision_default' key gets added to the revision_metadata_keys field when a new instance is created but not when a cached version is deserialized. The Plugin/Discovery code will end up adding the revision_metadata_keys from the @ContentEntityType annotation when generating a new definition b/c of a cache miss. If the 'revision_default' is not in that definition, we want to make sure it still gets added. I believe that the 'revision_default' base field was added as part of 2860097 for allowing content translations to be moderated independently.
The code above assumes that it will be executed in one of two cases: when the constructor is invoked programatically to create a new instance as part of something like plugin discovery or when a cached instance is deserialized and then initialized. However there is a third case and that is where the problem we are seeing stems from.
EntityDefinitionUpdateManager::getEntityType() Lines 25-128
public function getEntityType($entity_type_id) {
$entity_type = $this->entityManager->getLastInstalledDefinition($entity_type_id);
return $entity_type ? clone $entity_type : NULL;
}
I am not entirely certain why this code does what it does but it is pretty self evident. If it finds a definition of the entity type in the last installed definition repository, it then uses `clone $entity_type` to clone the definition and returns that clone. When this code is executed with the constructor code from patch 2936511 in place, the constructor will run, the revision_default key will be added, but since we are not going through the plugin/discovery manager, none of the fields from the revision_metadata_keys annotation will be added.
In our case, this incorrect definition is passed on through onEntityTypeUpdate and inside the SqlContentEntityStorageSchema::onEntityTypeUpdate it executes the branch where there is no data in the table which drops and recreates the entity tables. When the tables are recreated in SqlContentEntityStorageSchema::onEntityTypeCreate, the incorrect schema is used, resulting in the loss of the columns stated above.
SOLUTION
I have not had time to consider what a solution would be yet. I am happy to work on a patch if we can come up with a good solution.
| Comment | File | Size | Author |
|---|---|---|---|
| #16 | test-last-installed.txt | 648 bytes | sdewitt |
| Screen Shot 2018-02-02 at 1.52.03 PM.png | 546.39 KB | sdewitt | |
| Screen Shot 2018-02-02 at 1.35.57 PM.png | 130.16 KB | sdewitt | |
| Screen Shot 2018-02-02 at 1.35.48 PM.png | 129.07 KB | sdewitt |
Comments
Comment #2
xjmThanks for the issue report @sdewitt!
Comment #3
xjmPromoting to critical since this results in data loss when it does happen.
Comment #4
wim leersWhat an excellent bug report, with detailed analysis — thank you!
Comment #5
sdewitt commentedComment #6
sdewitt commentedI had a look through the history of EntityDefinitionUpdateManager.php. It looks like getEntityUpdate() was introduced in this commit: http://cgit.drupalcode.org/drupal/commit/core/lib/Drupal/Core/Entity/Ent... for this issue: https://www.drupal.org/project/drupal/issues/2542748. Having a read through the issue solution.
Comment #7
sdewitt commentedActually, I am not sure my previous theory makes sense. If the `revision_metadata_keys` were correct on the last installed definition, the `clone` operation should not affect that since during object cloning properties are shallow copied. So, it may be that the installed definitions are somehow getting corrupted? Will continue to investigate.
Comment #8
sdewitt commentedI wrote this small snippet to execute with `drush scr` and interestingly enough it looks like the `revision_metadata_keys` contributed by the annotation are not present in the last installed definition. The `node` entity type shows the same thing. Only the `revision_default shows` up in the last installed definition.
Is this expected behavior?
Comment #9
berdirI'm wondering why you are reporting this as a separate issue instead of commenting on #2936511: ContentEntityType::getRevisionMetadataKeys() never considers the BC logic because of the new "revision_default" key. If it's caused by that patch, then we need to fix it there in that issue as it is not yet committed.
Also, that issue had many different patches and possible approaches and yours is not the most recent one anymore. It would be very helpful if you could test multiple patches with your scenario to see if how they behave. Is there also a problem without the patch with block_content? (there will likely be problems with contrib modules, but that's a different question.
Comment #10
sdewitt commentedIt is unclear to me that these are the same issues. Feel free to dup this one out if you think it's the same issue.
Not entirely sure what you mean here. Can you clarify?
Comment #11
berdirWell, you applied a patch and then that patch causes a problem. Usually, that means it should be reported in the issue where that patch is being implemented. Unless you can reproduce the same problem also without the patch.
Patches are not allowed to introduce known regressions, so we can't commit it without fixing this problem.
What I mean with the second part is that you reported this with the patch from comment #45. There are probably at least 10 different patches there that might or might not cause the same bug. Try the one from #41 to see if that causes the same problem for example.
Comment #12
sdewitt commented@berdir it does happen without that patch from #2936511: ContentEntityType::getRevisionMetadataKeys() never considers the BC logic because of the new "revision_default" key. The reason we initially tried the patch from 2936511 is because it was suggested it might fix the issue.
Actually, as I stated in comment #7 and #8, I think my initial analysis was incorrect. I don't think it has anything to do with the `clone` and the fact that the revision_default metadata key was moved to the constructor in the patch. That wouldn't make sense. What seems to be more likely an issue is that the revision_user/etc keys are not part of the lastInstalledRevision. Maybe this is intentional? I saw some comments on #2936511 about this and not sure if they confirm this. However, if this *is* intentional, the code in the EntityDefinitionUpdateManager#getEntityType() somehow needs to fill in the actual revision metadata keys from the annotation definitions for the entity type before it passes it on down the line on the onEntityTypeUpdate call b/c as it is, this is not done and that is how we end up with the schema corruption.
Comment #13
berdirI can't reproduce this when installing with 8.4.x because then the BlockContent entity type annotation has those keys already, so does the last installed definition. I also tried with 8.3.x, where I could also not reproduce but I did have some errors because the order of update functions is problematic, system_update_8400() did run first, before block_content_update_8400() and then threw an exception and only run through after I run drush updb a second time. (This is why I think #2942096: [policy, no patch] Remove old update hooks prior to each major version (Drupal 10 and later) , the order of update functions from 8.0 to 8.5 is going to be pretty arbitrary, wondering how that even passes).
One problematic thing that I see is that we indeed rely on the BC layer being executed and initializing those metadata keys and we don't do it explicitly like the revision_default key in system_update_8500(). But might also be tricky to do it, among other things, each entity type would probably require it. That might be what you are seeing in #8, but calling getRevisionMetadataKeys() should trigger BC and initialize it. Unless somehow your update functions are running in a different order than mine. Please post the full output of the updb command. Is maybe system_update_8501() running first, which does indeed explicitly define revision_default, which then could result in the other keys not being defined?
Also don't know why only block_content should be affected by this, what about node_revision?
Comment #14
hchonov@sdewitt, could you please give it another try with the most recent patch from #2936511-67: ContentEntityType::getRevisionMetadataKeys() never considers the BC logic because of the new "revision_default" key?
Comment #15
sdewitt commented@berdir Thanks for your continued help and attention on this one.
system_update_8501 is *absolutely* running before block_content_update_8400 every time on our installation. I have tried a lot of different things as I've been trying to debug this and I do believe that last week I did try manually running 8400 before 8501 and in that case everything was fine. Although, I did not check the last installed defn at that time but Drupal at least did operate normally. I did not save a copy of the output from running the updb but I did it about 20 times last week and I can guarantee you 8501 always ran before 8400.
I developed a small snippet I could run with drush scr to show me the result of using reflection to look at the internal revision_metadata_keys of the ContentEntityType returned from the getLastInstalledDefinition() and also to look at the getRevisionMetadataKeys() from the same ContentEntityType. I have attached the script to the issue. Because of your last comment about not just block_content being affected, I ran it for node, block_content, taxonomy_term, and user. The results before we run system_update_8501() show the following. NOTE: I did the below WITHOUT the patch https://www.drupal.org/project/drupal/issues/2936511 installed.
(The first array is the result of using reflection and the second the result of getRevisionMetadataKeys()).
Then I ran system_update_8501() and then again ran the script. Here is the output.
So for all the types I queried, they do seem to have lost the revision_user, etc keys.
At this point, I have two questions.
Comment #16
sdewitt commentedComment #17
sdewitt commented#67 on #2936511 ContentEntityType::getRevisionMetadataKeys() never considers the BC logic because of the new "revision_default" key looks like it might resolve #2941733: block_content_revision loses columns due to incorrect entity definition with patch 2936511. Trying it now.
Comment #18
wim leers@sdewitt++
Comment #19
sdewitt commented#67 on #2936511 ContentEntityType::getRevisionMetadataKeys() never considers the BC logic because of the new "revision_default" key resolves it. We can close this as a dup.
Comment #20
sdewitt commentedComment #21
sdewitt commentedComment #22
plachClosing as a duplicate per #2936511-74: ContentEntityType::getRevisionMetadataKeys() never considers the BC logic because of the new "revision_default" key.
Comment #23
berdirGreat!
Good that the latest patch there resolves it. I still find it very strange/worrying that your last installed definition is missing those keys. Either you install 8.4, then you should get them because they are already defined with that version or if you installed with an earlier version, then it should have executed previous updates like system_update_8400() that initialized the keys through the BC layer.
Hm, that said, looking at system_update_8400(), while it calls getRevisionMetadataKeys(), it does not set the updated entity type as the last installed, so it doesn't get persisted. Not sure if we can/want to do something about still.
And we also have #2942533: "The entity type block_content does not have a "published" entity key." notices when updating from 8.4 to 8.5 now as a related issue.
Comment #24
quietone commentedChanging tags per Issue tags field and Issue tags -- special tags for issue #3565085: Drupal core issue tag cleanup.