Problem/Motivation

Background

Scenario: Source site with N content types. All CT have a node reference field configured to allow referencing nodes of each other CT.

For example:

  • Content types: `news`, `events`, `gallery`.
  • All of them with a node reference field `field_related_content` enabled to reference bundles: news, events, gallery.

So we create a migration setup with three migrations: myd6_news, myd6_events, myd6_gallery. In order to migrate the node reference field each migration includes this process:

field_related_content:
  plugin: iterator
  source: field_related_content
  process:
    target_id:
      plugin: migration
      migration:
        - myd6_news
        - myd6_events
        - myd6_gallery
      source: nid

Since any content type can reference each other, we can't enforce a precedence between migrations. As known, a stub row will be created for referenced nodes not migrated yet. Afterwards, as each migration runs, the stub rows will be properly migrated with the actual contents from the source.

Problems

  1. The current migration is always used if it is found in the process configuration (ref: https://github.com/drupal/drupal/blob/8.3.x/core/modules/migrate/src/Plu...). In the exposed scenario: when running the `myd6_event` migration, `myd6_event` migration will be chosen to create the stub row.
  2. Because of 1), the stub_id configuration is ignored.
  3. Because of 1), the stub row is added to the map of the current migration. Later, when the proper migration for the stubbed source row is run, it doesn't find a stub row in its map, because it is in the former migration map, and a second migration of the same source row is performed.
  4. OTOH the MigrateExecutable used to perform the import of the stub row is the one of the current migration in spite of the stub migration selected. So the stub row is created using a different process pipeline, leading to errors because it may not be prepared to create stub rows with default values and so. (Related: #2800279: Document that migrations used for stubbing need to deal with empty source values)

Let's ilustrate this with an example:

  1. Source: node 2 (news) references node 1 (event).
  2. Run migration myd6_news. It finds a reference to node 1 that can't solve. So it creates a stub row for source node 1 with destid 101.
  3. The stub row is created using the myd6_news executable (because 1, since myd6_news is present in the process configuration it is always selected and 2, in spite of the selected migration, the MigrateExecutable of the current migration (myd6_news) is always used) and 1:101 is added to the map of myd6_news migration.
  4. Source node 2 is migrated with destid 102. 2:102 is added to the map of myd6_news migration.
  5. Run migration myd6_event. This migration doesn't know about the stub row created by myd6_news because it is not in its map. Source node 1 is migrated again with destid 103. 1:103 is added to the map of myd6_event migration.

Proposed resolution

  1. (?) Respect stub_id configuration or fix documentation.
  2. Break up Drupal\migrate\Plugin\migrate\process\Migration::transform() in discrete methods, so it is easy to extend and override.
  3. (?)Add alter hooks to some of the new methods.
  4. Set the MigrateExecutable to use the the stubbing migration.

Note: The proposed solution doesn't fix the problem, because we can't know out-of-the-box which migration corresponds to any source row. It only enables developers to build their own solutions.

Remaining tasks

Agree on a resolution.
Write a patch.

User interface changes

None.

API changes

No API changes.

API additions: new public methods in Drupal\migrate\Plugin\migrate\process\Migration::transform().

Data model changes

None.

Comments

jonhattan created an issue. See original summary.

jonhattan’s picture

Issue summary: View changes
jonhattan’s picture

jonhattan’s picture

jonhattan’s picture

Status: Active » Needs review
StatusFileSize
new8.9 KB

Ok here's a refactoring of the transform() method. It addresses points 2 and 4 of the proposed resolution.

jonhattan’s picture

Issue summary: View changes
jonhattan’s picture

StatusFileSize
new1.13 KB
new9.43 KB
new1.13 KB

Catch exception in processRow().

jonhattan’s picture

heddn’s picture

Status: Needs review » Needs work
Issue tags: +Needs issue summary update, +Needs tests

Can I get an update of the proposed solution? And some tests?

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.

mikeryan’s picture

Issue tags: +Migrate BC break
+++ b/core/modules/migrate/src/MigrateExecutableInterface.php
@@ -7,6 +7,11 @@
+  public function setMigration(MigrationInterface $migration);

I haven't looked closely at changes to the migration process plugin itself, but this is a BC break to the migrate module (which is currently in experimental beta status).

heddn’s picture

#2835586: Allow customization of stub rows from Migration process plugin is making more progress as it just went RTBC. If/when it gets committed, this will need a re-roll.

jonhattan’s picture

Issue tags: +Needs reroll
StatusFileSize
new9.51 KB
new3.02 KB

Return NULL instead of [] when no dest ids found. This is a change to maintain compatiblity. Patch for 8.2.x. Needs reroll on 8.4.x and tests.

jofitz’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll

Status: Needs review » Needs work
jofitz’s picture

Status: Needs work » Needs review
StatusFileSize
new10.04 KB

Re-roll of #13 for 8.4.x including improved PHPdocs.

pritishkumar’s picture

StatusFileSize
new10.02 KB
new1.27 KB
jofitz’s picture

@pritish.kumar Thanks for your corrections and especially for including an interdiff. Next time it would also be useful if you could add a short comment describing the change(s) to save us having to investigate the patch and/or interdiff.

joelpittet’s picture

@Jo Fitzgerald this patch may need a re-roll once #2864563: Migration lookup process plugin doesn't call setMessage on the migration idMap They are conflicting with these lines currently:

patching file core/modules/migrate/src/Plugin/migrate/process/MigrationLookup.php
Hunk #1 FAILED at 3.
Hunk #2 succeeded at 167 (offset 1 line).
Hunk #3 FAILED at 190.
Hunk #4 succeeded at 310 (offset 3 lines).
joelpittet’s picture

Issue tags: -Migrate BC break

Adding a public method isn't a "BC Break" @mikeryan, it's an API addition, or am I missing something?

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

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

heddn’s picture

Assigned: Unassigned » heddn

I'll take a look at reviewing this week.

heddn’s picture

Assigned: heddn » Unassigned
Issue tags: +Needs reroll
heddn’s picture

Status: Needs review » Needs work
jofitz’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
StatusFileSize
new9.78 KB

Re-rolled.

heddn’s picture

Assigned: Unassigned » heddn

Going to review again.

heddn’s picture

Assigned: heddn » Unassigned
Status: Needs review » Needs work

Can we get a test only patch demonstrating the problem? The problem with the migrate_lookup plugin, is it is so complicated, touching it with a 10 foot pole is dangerous.

heddn’s picture

Adding something to the executable interface is a break. But I think we can do this in a non-breaking method by adding a new interface that is implemented by Core's executable. Then do instance_of check. Not clean, but it keeps BC.

heddn’s picture

+++ b/core/modules/migrate/src/MigrateExecutable.php
@@ -103,10 +103,9 @@ class MigrateExecutable implements MigrateExecutableInterface {
   public function __construct(MigrationInterface $migration, MigrateMessageInterface $message = NULL, EventDispatcherInterface $event_dispatcher = NULL) {
...
+    $this->setMigration($migration);

Alternatively, we could just new up a new executable, no?

jofitz’s picture

Status: Needs work » Needs review
StatusFileSize
new9.77 KB

I had hoped to address @heddn's comments in #28 & #29, but with the code in front of me, I realise that I don't really understand what is required.

...so here is a re-roll because the patch in #25 no longer applies.

@heddn Would you mind expanding on your suggestions, please.

heddn’s picture

Status: Needs review » Needs work

#28 suggests that we add a new interface and check if the executable implements this interface before calling setMigration. Let's call this option #1. Alternatively, in #29 I suggest we could just new up a new executable. That would sorta work because that object isn't a service. Its the runner. To determine that, we can run get_class() against the executable, then new up a new one passing into it the migration. Do this where ever we want to do use setMigration. So, 2 options.

Which is better? Why don't we do both? And use the second (more hacky) solution as a fallback. Then we can sunset that approach in 9.x when we can do things more properly.

Version: 8.5.x-dev » 8.6.x-dev

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

joachim’s picture

This looks like the problem I am currently seeing, though it's not entirely clear from the summary.

Here's what I have:

1. The main migration is running. One of its process mappings uses the migration_lookup plugin, configured to use stubs.

2. MigrationLookup::transform() wants to create a stub row, and then run it through the normal migration process:

      $process = $migration->getProcess();
...
      $migrate_executable->processRow($stub_row, $process);

(That's the lookup $migration, not the main one! See #2963811: Improve variable names in MigrationLookup for DX!)

3. MigrateExecutable::processRow() is ok to receive a $process pipeline as its parameter:

  public function processRow(Row $row, array $process = NULL, $value = NULL) {

So far so good!

4. Here is where problems start. MigrateExecutable thinks it knows the migration it is working with -- the main one. It's not told any different.

  public function processRow(Row $row, array $process = NULL, $value = NULL) {
    foreach ($this->migration->getProcessPlugins($process) as $destination => $plugins) {

Uh-oh. It's calling getProcessPlugins() on the *main* migration, with the $process pipeline configuration from the lookup migration!

5. The Migration plugin class is ok to receive a $process for getProcessPlugins(). That's to allow for the SubProcess process plugin, where an inner process pipeline is used. In this case though, it's a WTF! Migration A should not be asked to do anything with Migration B's process pipeline!

  public function getProcessPlugins(array $process = NULL) {

6. And now the real problem. In Migration::getProcessPlugins(), every process plugin gets the migration it is working with passed in as a constructor parameter. That's the $this in these two lines:

            $this->processPlugins[$index][$property][] = $this->processPluginManager->createInstance('get', $configuration, $this);
...
            $this->processPlugins[$index][$property][] = $this->processPluginManager->createInstance($configuration['plugin'], $configuration, $this);

Of course, this is the WRONG migration now!

joachim’s picture

+++ b/core/modules/migrate/src/MigrateExecutable.php
@@ -118,6 +117,14 @@ public function __construct(MigrationInterface $migration, MigrateMessageInterfa
+   * {@inheritdoc}
+   */
+  public function setMigration(MigrationInterface $migration) {
+    $this->migration = $migration;
+    $this->migration->getIdMap()->setMessage($this->message);
+  }
+

I think this is too convoluted. It's going to mean that whenever we jump out of an inner migration, such as a lookup, we have to ensure we restore the previous migration. I see bugs arising from times when we forget to do that.

I was thinking we could add an optional $migration parameter to MigrateExecutable::processRow(), and then this call uses that if it's provided:

    foreach ($this->migration->getProcessPlugins($process) as $destination => $plugins) {

That would fix things pretty simply, with the addition of an optional parameter to the interface, which is allowed.

> Alternatively, we could just new up a new executable, no?

Do you mean have MigrationLookup do:

  $my_inner_executable = new MigrateExecutable($migration, $log, $options);
  $my_inner_executable->processRow($stub_row, $process);

That looks like a good approach too, and cleaner overall. I'm a bit concerned that there may be side-effects we don't know about to having two MigrateExecutable objects in play, since it's been designed to run as a singleton.

BTW:

> Adding something to the executable interface is a break.

As it's not tagged as @api, then this part of the policy at https://www.drupal.org/core/d8-bc-policy applies:

Interfaces within non-experimental, non-test modules not tagged with either @api or @internal
Interfaces that are not tagged with either @api or @internal can be safely used as type hints. No methods will be changed or removed from these interface in a breaking way.
However, we reserve the ability to add methods to these interfaces in minor releases to support new features. When implementing the interface, module authors are encouraged to either:

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

berdir’s picture

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

huzooka’s picture

We have written a replacement lookup plugin and released it in Migrate Magician. I think it does what you expect.

See https://www.drupal.org/project/drupal/issues/2891073#comment-14452582

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.