#2554003: isComplete() should not rely on RESULT_COMPLETED is exposing some source plugin problems - once we properly define migration completion as "all source rows have been processed", we find that there are source plugins which are producing multiple source rows for a given unique source ID. Redundant rows mean redundant processing, but also mean that after processing the total number of source rows reported by the plugin is greater than the number reported as processed (since the redundant rows write to the same idmap row), and the new version of isComplete() (allRowsProcessed()) is FALSE when it shouldn't be.

I've identified the following instances:

  • d7_comment_type - there are 6 distinct comment types in the database, but one of them has two fields, so 7 rows are returned (two of them identical). A simple distinct() fixes this.
  • d7_field is similar - it joins to the instance table to validate the fields are actually being used, but when a field is used in multiple places that creates duplicate rows. Again, distinct() to the rescue.
  • d6_field is the problem child. It actually pulls data from the instance table, which is not identical across instances, so distinct() does not remove the duplicates. I need to look more deeply into why the base field migration is using instance data, which is a WTF on the face of it. If the D8 base field really should hold former instance data, then we have a real problem where there are contradictory instances.

Comments

mikeryan created an issue. See original summary.

mikeryan’s picture

Had some discussion with @phenaproxima, who has dug into this area before. As I understand it, in Drupal 6 the widget_type affected the actual database representation, which was fine when it was creating separate tables, but in Drupal 8 the (well, some) distinctions between field types that D6 had at the widget level are now in the base field. So, we actually do need to account for the D6 widget type in creating the D8 field. The problem is if a D6 site used different widget types in different instances of the same field - there's only going to be one field in D8, so we can't apply both widget types. Right now, if I'm not mistaken, the last instance processed takes precedence.

So, the first pass at this, my plan is to take the instance join out of the base query and pull the "first" widget_type in prepareRow() - this fixes the isComplete() problem without making handling conflicts between instances any worse. I'll also detect the situation of conflicting instances and generate a message, so (unlike today) the migrator will get some clue that things aren't 100% hunky-dory. @phenaproxima suggests maybe trying to normalize conflicts to a lower-common-denominator field type - that may be best left to a follow-up issue. The question is, how many real-world D6 sites have instance conflicts that matter?

I presume this issue must have been dealt with in the D6 CCK->D7 field upgrade path, we should investigate that.

mikeryan’s picture

Status: Active » Needs review
StatusFileSize
new3.33 KB

A fail patch to demonstrate the issues.

Status: Needs review » Needs work

The last submitted patch, 3: some_source_plugins-2577155-3-FAIL.patch, failed testing.

mikeryan’s picture

Yep, exactly the expected failures.

benjy’s picture

If we're getting duplicates from that query because of the join on CNFI, lets just group by field_name, that will remove the duplicates and then we'll be using the widget type from the first match which is fine. If you reuse the same field across bundles in D6 with two different widgets then we'll only be looking at one but that's no worse than what we're doing now.

benjy’s picture

Status: Needs work » Needs review
StatusFileSize
new5.42 KB
new2.08 KB

Added a couple of distinct calls for comment and d7 field.

With the d6 field source, it wasn't so easy to just add a groupBy and make it full with all the fields/map fields in the select so i used another join with a sub query that did the group by and used MAX(widget_settings) to single out a row between the duplicates, not sure about that part, will sleep on it.

Lets see what the bot thinks.

mikeryan’s picture

widget_settings isn't actually needed, only widget_type is used to determine the ultimate field type, I was removing it in my patch.

The advantage of pulling the widget_type from the instance in prepareRow(), rather than selecting an arbitrary type in the query, is that it allows us to warn the user that something's missing.

mikeryan’s picture

StatusFileSize
new9.29 KB
new4.63 KB

Removing widget_settings, which enables some simplification of the query.

mikeryan’s picture

After discussion with phenaproxima, I'm going to go with pulling the widget_type in prepareRow(), so we can produce a message when we detect a conflict in instance widget_types.

mikeryan’s picture

Status: Needs review » Needs work
mikeryan’s picture

Status: Needs work » Needs review
StatusFileSize
new10.12 KB
new2.11 KB

Here it is... The one thing it's lacking is a test for the message coming from conflicting widget_types, because in the current data the instances that were generating the extra source count differed only in widget_settings, they had the same widget_type. So, another instance needs to be added to the source data to be able to test the message...

mikeryan’s picture

So, the existing D6 dump has two instances of field_test (on the story and test_page node types), both with widget_type text_textfield. To test dealing with instances of one field with different widget_types, I created another instance on the test_planet node type, with widget_type optionwidgets_onoff. My test of this functionality succeeded - yay! But, now MigrateFieldInstanceTest fails when testing story's instance of field_test. That's because optionwidgets_onoff comes before text_textfield alphabetically, so that the base field_test becomes a boolean field - not at all what this test is expecting.

The problem here is a basic WTF of migrating D6 CCK to either D7 or D8 - why, here's what the D7 content_migrate module tells you if you have this situation:

        $field_value['messages'][] = '<span class="error">' . t("Caution: The '@field' field is a shared field that uses different widgets. The '@widget' widget is sometimes used to determine the destination field type for the new field. The migration may not work correctly if other widgets used by this shared field would create different results.", array('@field' => $field_value['field_name'], '@widget' => $row['widget_type'])) .'</span>';

I.e., if you used different widget types on different instances of a field on D6, you're fucked. Frankly, if you used a text widget for one instance of a field and a radio button/checkbox for another, you deserve it - the saving grace is that this is probably (I hope) a very rare condition.

Anyway, since MigrateFieldInstanceTest was not designed to be a demonstration of this problem, I need to use different test data that doesn't overlap with instances explicitly used by existing tests.

mikeryan’s picture

StatusFileSize
new12.14 KB
new3.18 KB

Finally... Also tweaked the message with feedback from phenaproxima.

Status: Needs review » Needs work

The last submitted patch, 14: some_source_plugins-2577155-14.patch, failed testing.

mikeryan’s picture

Oh, of course, need to add another field table to the dump... Bah.

mikeryan’s picture

Or, I should say, add field columns to the node field table. Enough for me for today, I'll pick it up in the morning (unless someone else wants to jump in...)

The last submitted patch, 3: some_source_plugins-2577155-3-FAIL.patch, failed testing.

The last submitted patch, 14: some_source_plugins-2577155-14.patch, failed testing.

mikeryan’s picture

Ugh, without a dump that could be used to run a real D6 site so I can manipulate the fields through the UI and have all the DB tables end up in the right place, testing this is damned near impossible. Maybe #2562695: migrate-db.sh skips uid 1 but shouldn't will rescue me?

mikeryan’s picture

StatusFileSize
new18.35 KB
new7.18 KB
mikeryan’s picture

Status: Needs work » Needs review

With much struggling, managed to get the dumps files in the right place (I think).

phenaproxima’s picture

+++ b/core/modules/field/src/Plugin/migrate/source/d6/Field.php
@@ -69,12 +67,33 @@ public function fields() {
+          '@types' => implode(',', $widget_types),

Nit: Can the delimiter be ", " (note the space)?

Everything else looks good and right to me.

mikeryan’s picture

Sure - let's just wait and make sure the test results don't require any other changes.

Status: Needs review » Needs work

The last submitted patch, 21: some_source_plugins-2577155-21.patch, failed testing.

mikeryan’s picture

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

It caught me trying to hack the dump files to minimize the changes... Fine, whatever...

Status: Needs review » Needs work

The last submitted patch, 26: some_source_plugins-2577155-26.patch, failed testing.

mikeryan’s picture

Status: Needs work » Needs review
StatusFileSize
new28.04 KB
new798 bytes

For want of a space, the test was lost...

phenaproxima’s picture

To me, this patch looks good. I'd like @benjy's input for RTBC.

The last submitted patch, 21: some_source_plugins-2577155-21.patch, failed testing.

The last submitted patch, 26: some_source_plugins-2577155-26.patch, failed testing.

benjy’s picture

Ugh, without a dump that could be used to run a real D6 site so I can manipulate the fields through the UI and have all the DB tables end up in the right place, testing this is damned near impossible.

That's exactly how we're meant to reproduce the dumps, by importing them, using a D6 site with the codebase here https://www.drupal.org/sandbox/benjy/2405029 and the exporting again with migrate-db.sh. Is that how we created the new dump file?

  1. +++ b/core/modules/field/src/Plugin/migrate/source/d6/Field.php
    @@ -69,12 +67,33 @@ public function fields() {
       public function prepareRow(Row $row) {
    ...
    +    $widget_types = $this->select('content_node_field_instance', 'cnfi')
    +      ->fields('cnfi', ['widget_type'])
    

    Given that there aren't likely that many widget types, I wonder if we should query for all widget types in initializerIterator() just once and then have a lookup in an array of field_name => widgets here? Although performance probably isn't an issue with the field migration...

  2. +++ b/core/modules/field/src/Plugin/migrate/source/d6/Field.php
    @@ -69,12 +67,33 @@ public function fields() {
    +    assert(count($widget_types) > 0);
    

    Why an assert? Do we have docs on when to use these somewhere?

mikeryan’s picture

Is that how we created the new dump file?

Ah, I vaguely recall the sandbox... I created a D6 environment of my own with the necessary contrib modules and managed to get it into good enough shape to add the instance and export.

Although performance probably isn't an issue with the field migration...

Yeah, I think it's clearer to pull them directly here. Overriding initializeIterator() would be an unusual pattern and a "wait, what?" for future reviewers.

Why an assert? Do we have docs on when to use these somewhere?

Taking advantage of https://www.drupal.org/node/2569701 - not sure how aggressively we're supposed to use them, couldn't resist the shiny new feature;).

benjy’s picture

Status: Needs review » Reviewed & tested by the community

OK rest looks good, as discussed on IRC, can we remove the assert on commit and then @mikeryan, could you create a follow-up to discuss using asserts in migrate.

mikeryan’s picture

Given a general policy on asserts has an issue at #2548671: [policy, no patch] Define best practices for using and testing assertions and document them before adding assertions to core, I don't think we need a migrate-specific policy (at least unless/until that policy is in place and we decide we should make exceptions).

  • webchick committed 9eb5b8b on 8.0.x
    Issue #2577155 by mikeryan, benjy: Some source plugins produce duplicate...

  • webchick committed 203eb38 on 8.0.x
    Issue #2577155 follow-up: Remove assert() call.
    
webchick’s picture

Sorry, that second commit was because mikeryan had told me just before leaving that that line was somewhat contentious and asked to remove it before commit, but I forgot. :P

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 28: some_source_plugins-2577155-28.patch, failed testing.

phenaproxima’s picture

Status: Needs work » Fixed

Um, no...this is fixed.

Status: Fixed » Closed (fixed)

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