Problem/Motivation

While working on #1980556: Allow NULL default values in @Plugin annotations I found that DrupalSqlbase, in checkRequirements, uses the property $minimum_schema_version which is not set anywhere and that MigrateSource defines a property $minimum_version which is not used anywhere. Reading the doc bloc suggests that migrate drupal source plugins should be using $minimum_version.

  /**
   * Specifies the minimum version of the source provider.
   *
   * This can be any type, and the source plugin itself determines how it is
   * used. For example, Migrate Drupal's source plugins expect this to be an
   * integer representing the minimum installed database schema version of the
   * module specified by source_module.
   *
   * @var mixed
   */
  public $minimum_version;

See comment 1980556-Comment #42 for more details.

Proposed resolution

Change $minimum_schema_version in DrupalSqlBase to $minimum_version and of course add a test. Add a comment and change the logic to be more readable.

No deprecation or errors added because this is so broken the exception was never thrown.

Remaining tasks

User interface changes

API changes

Data model changes

Release notes snippet

Comments

quietone created an issue. See original summary.

quietone’s picture

Status: Active » Needs review
Related issues: +#3176393: Use SourcePluginBase::getSourceModule() in DrupalSqlBase::checkRequirements()
StatusFileSize
new1.81 KB
new4.31 KB
new5.64 KB

I double checked that minimum_schema_version is only used in DrupalSqlBase.

$ grep -r minimum_schema_version core
core/modules/migrate_drupal/src/Plugin/migrate/source/DrupalSqlBase.php:          if (isset($this->pluginDefinition['minimum_schema_version']) && !$this->getModuleSchemaVersion($this->pluginDefinition['source_module']) < $this->pluginDefinition['minimum_schema_version']) {
core/modules/migrate_drupal/src/Plugin/migrate/source/DrupalSqlBase.php:            throw new RequirementsException('Required minimum schema version ' . $this->pluginDefinition['minimum_schema_version'], ['minimum_schema_version' => $this->pluginDefinition['minimum_schema_version']]);

This patch changes minimum_schema_version to minimum_version and also fixes this if statement. The ! should be outside the test for less than. isset($this->pluginDefinition['minimum_schema_version']) && !$this->getModuleSchemaVersion($this->pluginDefinition['source_module']) < $this->pluginDefinition['minimum_schema_version']) {

There are two new tests for checkRequirements to test this logic, which had no tests before. The interesting thing here is that they success test also passes in the test with the fail patch. That is becuase of the error mentioned about. To help prove that the fail test includes a fourth test case where the data should throw an exception. The exception is not thrown and testMinimumVersionSuccess succeeds. I hope that makes sense to you.

The last submitted patch, 2: 3151732-2-fail.patch, failed testing. View results

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.

benjifisher’s picture

Status: Needs review » Needs work
  1. Based on the code comments, I agree with your description of the problem. Both $minimum_version and 'minimum_schema_version' were intended to do the same thing, and they should have bee named the same. But one is a class property and the other is a key in the plugin definition. (In Drupal\Component\Plugin\PluginBase::__construct(), $plugin_definition is a parameter, assigned to the class property $pluginDefinition.) They are not logically related: the results of your grep experiments show that.

  2. Both SqlBase and DrupalSqlBase are base classes, so we have to consider the implications for child classes. Can we simply ignore the minimum_schema_version key if it is defined in a derived source plugin? Suppose someone writing a contrib or custom source plugin needed a version constraint. First she read the documentation and used the property $minimum_version. That had no effect. So she looked at the code and added 'minimum_schema_version' to the plugin definition. If we adopt this patch, then we are breaking the check needed for that contrib or custom module.

    I think, if we want to make this change, we have to make sure that either 'minimum_version' or 'minimum_schema_version' version will work and throw a deprecation notice in the latter case.

  3. We might also want to set $this->minimum_version = $this->pluginDefinition['minimum_version'] ?? 0; in the constructor and then use the property in checkRequirements(). Or default to -1? This would have the advantage that we would not have to check isset($this->pluginDefinition['minimum_version']) in checkRequirements().

  4. Thanks for the explanation of the multiple changes in this line:

     +++ b/core/modules/migrate_drupal/src/Plugin/migrate/source/DrupalSqlBase.php
     @@ -105,8 +105,8 @@ public function checkRequirements() {
     ...
     -          if (isset($this->pluginDefinition['minimum_schema_version']) && !$this->getModuleSchemaVersion($this->pluginDefinition['source_module']) < $this->pluginDefinition['minimum_schema_version']) {
     +          if (isset($this->pluginDefinition['minimum_version']) && !($this->getModuleSchemaVersion($this->pluginDefinition['source_module']) <= $this->pluginDefinition['minimum_version'])) {

    I often ask for shorter lines of code, and this illustrates one of the reasons. When you make three changes to the same line of code, it is hard to review. (I am counting as one change s/minimum_schema_version/minimum_version/g even though the string appears twice. The other two changes are adding parentheses and changing < to <=.)

    Can we replace !(expr1 <= expr2) with expr 1 > expr2? This is not a trivial question, since getModuleSchemaVersion() can return FALSE. Assuming that the minimum version is an integer (if it is set at all), I think that FALSE will be cast to 0, so the answer is that we can.

    Am I confused, or is the inequality backwards? Looking at the test you added, I think I have it right. The test sets a minimum version of 7000 and an actual version of 7001 and expects a RequirementsException to be thrown. This does not make sense.

    Given all those questions, I consider it in scope for this issue to rewrite this block of code so that it has more, shorter lines that are easier to understand and maintain. The final conditional could be something like if ($actual_version < $minimum_version). At the same time, I think we can simplify the code by rearranging the cases and exiting early.

  5. The two test methods testMinimumVersionFail() and testMinimumVersionSuccess() are identical except for some extra blank lines in the second and the last 1 or 3 lines. Can we combine them into one test with a parameter for success/fail? Alternatively, add a helper method that is called by both test methods.

benjifisher’s picture

On second thought: I care less about backward compatibility (#5.2) given that the current behavior is so badly broken (#5.4).

quietone’s picture

Issue summary: View changes
Status: Needs work » Needs review
StatusFileSize
new4.43 KB

Am I confused, or is the inequality backwards?

Me too!

New patch that now has one new test method and some more test cases. Are more needed? Also the if statement is changed to what I think is something much easier to read. And as a bonus there is now a comment. I think that covers everything, since we aren't adding any deprecation, or have I missed something?

I didn't add an interdiff because this is small and the interdiff is larger than the patch.

benjifisher’s picture

Status: Needs review » Needs work
  1. This should work, and you can leave it like this if you prefer:

     +++ b/core/modules/migrate_drupal/src/Plugin/migrate/source/DrupalSqlBase.php
     @@ -105,8 +105,10 @@ public function checkRequirements() {
     ...
     +          // Check that the minimum schema version is not greater than the
     +          // current schema for the source module.
     +          if (isset($this->pluginDefinition['minimum_version']) && ($this->pluginDefinition['minimum_version'] > $this->getModuleSchemaVersion($this->pluginDefinition['source_module']))) {
     +            throw new RequirementsException('Required minimum version ' . $this->pluginDefinition['minimum_version'], ['minimum_version' => $this->pluginDefinition['minimum_version']]);

    But what I was suggesting at the end of #5.4 was something like this (inside enough if statements to make it safe):

     $minimum_version = $this->pluginDefinition['minimum_version'];
     $installed_version = $this->getModuleSchemaVersion($this->pluginDefinition['source_module']);
     if ($minimum_version > $installed_version) {
       throw ...
     }

    If we do that, then I think the code is clear enough that we do not need the comment. I think the reason this bug got here in the first place is that the line was so long and confusing that no one noticed the backwards comparison until now. The shorter lines of code also mean that we are unlikely to need an explanation like the one in #2 about multiple changes to one line of code. That is why I describe the shorter lines as "easier to understand and maintain".

  2. There are already two test methods in this class. Now that we are adding a third, we could decide that DRY is in scope. If you would rather defer that to a follow-up issue, we can. I want a follow-up issue anyway to remove the second namespace line and put all the use statements at the top. These lines could be moved to a helper method:

     +++ b/core/modules/migrate_drupal/tests/src/Unit/source/DrupalSqlBaseTest.php
     @@ -79,6 +79,83 @@ public function testSourceDatabaseError() {
     ...
     +  public function testMinimumVersion($success, $minimum_version, $schema_version) {
     ...
     +    /** @var \Drupal\Core\State\StateInterface $state */
     +    $state = $this->createMock('Drupal\Core\State\StateInterface');
     +    /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
     +    $entity_type_manager = $this->createMock('Drupal\Core\Entity\EntityTypeManagerInterface');
     +    $plugin = new TestDrupalSqlBase([], 'test', $plugin_definition, $this->getMigration(), $state, $entity_type_manager);

    Maybe we can even use a setUp() method, if the plugin definition can be set with a public method.

  3. Can we use MigrateTestCase::getDatabase(), same as testSourceProviderNotActive()?

     +    // Setup a connection object.
     +    $source_connection = $this->getMockBuilder('Drupal\Core\Database\Connection')
     +      ->disableOriginalConstructor()
     +      ->getMock();
     +    $source_options = ['driver' => 'mysql'];
     +    $source_connection->expects($this->never())
     +      ->method('getConnectionOptions')
     +      ->willReturn($source_options);
     +    $plugin->setDatabase($source_connection);
  4. Add a blank line after that block to separate it from the next lines:

     +    if (!$success) {
     +      $this->expectException(RequirementsException::class);
     +      $this->expectExceptionMessage("Required minimum version $minimum_version");
     +    }
     +    $plugin->checkRequirements();
  5. We cannot use {@inheritdoc} here:

     @@ -123,4 +200,11 @@ public function getIds() {
     ...
     +  /**
     +   * {@inheritdoc}
     +   */
     +  public function setSystemData($data) {
     +    $this->systemData = $data;
     +  }
quietone’s picture

Status: Needs work » Needs review
StatusFileSize
new3.37 KB
new3.9 KB

For #8
1. Changed.
2. Prefer followup for all that.
3. Fixed
4. Fixed
5. Refactored the test and this is no longer needed. :-) And the patch is a bit smaller.

benjifisher’s picture

Status: Needs review » Needs work

@quietone, thanks for the updates. I think both the test code and the "real" code are easier to read now.

I added a follow-up issue: #3191990: Simplify code in DrupalSqlBaseTest. I will also add a draft change record.

Getting back to #5.1, #5.3: there is no connection between the class property $minimum_version and the 'mimimum_version' key in the plugin definition. In #5.3, I suggested setting the property in the constructor based on the plugin definition. If we do that, then we can save one test in checkRequirements(). If we do not do that, then maybe we should deprecate the class property, since we do not use it anywhere. Any child class that wants to use it can declare it.

Can you upload a test-only patch? I am not sure how to test this issue, so I would like to see the failures in the test-only patch before calling it RTBC.

benjifisher’s picture

In #5.1 and #10, I was mixed up: minimum_version is declared in the annotation class. I was thinking it was a property of MigrateSource. I straightened myself out while drafting a CR.

Still NW for a test-only patch.

quietone’s picture

Status: Needs work » Needs review
StatusFileSize
new2.35 KB
new3.9 KB

Ah, I guess the fail patch in #2 is truly out of date.

New fail patch and re-upload and rename of the patch in #9 to #12. No interdiff since the success patch is the same as the patch in #9. And no interdiff between the fail patch and the success patch since there are only two files involved.

quietone’s picture

I read the CR and it looks good to me as does the followup.

@benjifisher, Thanks!

The last submitted patch, 12: 3151732-12-fail.patch, failed testing. View results

benjifisher’s picture

Status: Needs review » Needs work

@quietone:

Sorry, I did not notice this until I was reviewing the failing tests in the test-only patch: we need some @param comments for testMinimumVersion().

quietone’s picture

Status: Needs work » Needs review
StatusFileSize
new835 bytes
new4.21 KB

Neither did I. How is this?

benjifisher’s picture

Status: Needs review » Reviewed & tested by the community

Looks good. Thanks again!

  • catch committed 32d88bf on 9.2.x
    Issue #3151732 by quietone, benjifisher: DrupalSqlBase::...

  • catch committed 679e57f on 9.1.x
    Issue #3151732 by quietone, benjifisher: DrupalSqlBase::...
catch’s picture

Version: 9.2.x-dev » 9.1.x-dev
Status: Reviewed & tested by the community » Fixed

Committed/pushed to 9.2.x and cherry-picked to 9.1.x, thanks!

Status: Fixed » Closed (fixed)

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

wim leers’s picture