Problem/Motivation
\Drupal\migrate\MigrateExecutable::rollback() incorrectly assumes that Drupal\migrate\PluginMigrateMapInterface::getRowByDestination() returns an array with rollback_action key.
Nothing guarantees that the returned value has the key rollback_action (and right now the Sql plugin returns FALSE, violating MigrateMapInterface, see #3227549: Sql id map plugin's getRowByDestination shouldn't return FALSE).
This is not an issue on PHP 7.3 and below, but the most recent PHP versions are throwing notice if you try to access an offset of a non-array variable.
Proposed resolution
Check whether the rollback_action key exists before comparing its value.
Remaining tasks
| Comment | File | Size | Author |
|---|---|---|---|
| #14 | 3227660-14.patch | 12.07 KB | quietone |
| #14 | interdiff-5-14.txt | 1.8 KB | quietone |
| #5 | core-fix_migrateexecutable_rollback-3227660-5--complete.patch | 11.86 KB | huzooka |
| #5 | core-fix_migrateexecutable_rollback-3227660-5--test-only.patch | 10.98 KB | huzooka |
Comments
Comment #2
huzookaLet's see.
I wasn't able to find a rollback test, so I added a new test to
\Drupal\Tests\migrate\Unit\MigrateExecutableTestand slightly modified\Drupal\Tests\migrate\Unit\MigrateTestCase::getMigration().Comment #3
huzookaComment #4
huzookaSubProcessTestfailed.Comment #5
huzookaSubProcessTestwas misusing\Drupal\Tests\migrate\Unit\MigrateTestCase::getMigration().Comment #6
huzookaForgot the inderdiff. Here it is!
Comment #8
huzookaComment #9
wim leersPer
\Drupal\migrate\Plugin\MigrateIdMapInterface::getRowByDestination()'s docs,$map_rowmust indeed ben an array:And indeed, until #3227549: Sql id map plugin's getRowByDestination shouldn't return FALSE lands, it actually could be
FALSEinstead of an array.👍 But even after that lands, it's indeed unguaranteed that a
rollback_actionkey actually exists.This is the one that's failing, the second time we call
with
$destination_key = ['destination' => '1'];… but 100% of these ID map record have
rollback_actionspecified. So … not sure I 100% understand what's going on here yet …Aha! So this is mocking the behavior of
\Drupal\migrate\Plugin\MigrateIdMapInterface::getRowByDestination()and really of\Drupal\migrate\Plugin\migrate\id_map\Sql::getRowByDestination().I had to step through this with a debugger to understand.
What this is doing is reducing the original records in the data provider to just the ones that matched. Which means that a repeat call for the same destination will yield a different result: it'll have been reduced away…
🤔 This does not seem to really match the real-world behavior?
So … +1 for the hardening, but I still have doubts about the test coverage. @huzooka, could you clarify? 😊🙏
Comment #10
wim leers@huzooka just walked me through this!~ Even though this seems weird and wrong, it actually is accurately mocking the behavior of
\Drupal\migrate\MigrateExecutable::rollback(), where it does:That last line is deleting every row from the mapping table that contains the given destination key (which btw is an array of destination IDs, not a single key — so the name is pretty misleading), which is equivalent to the mocked behavior that I quoted in my previous comment! 👍👍👍
So … while this is perhaps a bit complicated/weird mock, it really is due to the weirdness in:
\Drupal\migrate\Plugin\migrate\id_map\Sql::getRowByDestination()which is not aware of the fact that multiple record can have the same destination ID — which particularly ind7_field_formatter_settingsis not a correct assumption\Drupal\migrate\Plugin\migrate\id_map\Sql::deleteDestination()\Drupal\migrate\MigrateExecutable::rollback()and the way these three interact.
Comment #11
larowlanLooks good, just a micro-optimisation suggestion
Now that we have null coalesce, we could write this as
See https://3v4l.org/C6dJp
Comment #12
quietone commented#10.3, Can anyone provide an example where the map for d7_field_formatter_settings can can multiple records with the same destination IDs?
Comment #13
huzookaRe #11:
At the time I wrote that, #3227549: Sql id map plugin's getRowByDestination shouldn't return FALSE wasn't committed. Now I prefer keeping array_key_exists only.
Re #12:
I made a rollbackable destination plugin which is able to restore the original state of the appropriate entity display after rolling back the migration. This source plugin identifies the destination entity display component based on the target entity type, bundle, display mode, and the name of the field.
But in d7_field_formatter_settings, formatter settings source records of a field are overriding each other if one of the settings we migrate comes from the default, and the other one comes from the full view mode. In this case, my destination IDs are the same (this is obvious since the target is the same thing). And since on rollback the removal of the ID map records happens based on the saved destination IDs, on a subsequent loop it can happen that the ID map plugin returns an empty array (or FALSE with older core versions).
But the root cause is at #3227391: d7_field_formatter_settings should migrate the default view mode to default not full.
Comment #14
quietone commentedI was re reading this change and think that using the rollback constants defined in MigrateIdMapInterface make the test a bit more readable, even though the comments are excellent. So I made that change.
Comment #15
quietone commentedThis was to be marked a duplicate in [#3239298#comment-36]. Doing that now.
Comment #16
wim leersThis was completely absorbed by #3239298: Fix \Drupal\migrate\Plugin\migrate\destination\EntityConfigBase::updateEntity() so that config translation migrations can be rolled back, which has been committed! 👍🥳