Problem/Motivation

In #2978575: Mysql 8 Support on Drupal 7 we've come across a problem where \MigrateSQLMap::getQualifiedMapTable prefixes table names with dbname. when table prefixes are not in place.

Because MySQL 8 support will require quoting of table names in core, this prefixing / qualifying can cause problems depending on exactly how the quoting is implemented. See #2978575-221: Mysql 8 Support on Drupal 7 and previous comments around #50

It looks like this was introduced to migrate in #1480762: Join of legacy data to default database map table does not work.

Steps to reproduce

This may require manual steps to reproduce, as the qualifying / prefixing is skipped when table prefixes are in place, which is always the case in simpletest.

That means migrate's tests pass without the dbname. prefixing.

Proposed resolution

It sounds like this could be more of an optimisation than something that's essential; if so would it be possible to back this change out of migrate and fall back to the less-optimised solution (which sounds like what has to be done when table prefixing is in place anyway)?

Remaining tasks

Identify and implement a solution.

User interface changes

??

API changes

??

Data model changes

??

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

mcdruid created an issue. See original summary.

joseph.olstad’s picture

Priority: Normal » Major
Status: Active » Needs review
Issue tags: +blocker

I have contacted @pifagor via the drupal.org contact form to make him aware of this issue.
Lets follow up in a few days if no response.

mcdruid’s picture

Priority: Major » Normal
Status: Needs review » Active

There's no patch to review yet, so back to Active (or could be NW).

I've reproduced the problem by setting up a migrate_d2d migration between two vanilla D7 sites with a handful of nodes in the source db created by devel_generate.

With the (current) latest patch from #2978575-218: Mysql 8 Support on Drupal 7 applied to core, migrations fail with an error like this (where d7target is the name of my target db):

Migration for DrupalFile7Migration failed with source plugin exception: SQLSTATE[42S02]: Base table or view not found: 1146 Table 'd7target.`d7target`.migrate_map_d7sourcefile' doesn't exist, in /path/to/drupal-7.x/includes/database/database.inc:2227

...which matches the report by @Ronino in the core issue (see #221).

If, however, I short-circuit the db qualifying in getQualifiedMapTable with something as simple as this:

  public function getQualifiedMapTable() {                                         
    $options = $this->connection->getConnectionOptions();                          
    $prefix = $this->connection->tablePrefix($this->mapTable);                     
    if (TRUE || $prefix) {                                                         
      return $this->mapTable;                                                      
    } 

...then the migration works fine.

We know that migrations where the (target?) database uses table prefixes work okay without adding the dbname to qualify the table name (as is the case with the module's simpletests).

So this all suggests to me that migrate could function without doing this table prefixing / qualifying.

However, I don't understand the reason this was added sufficiently well to know exactly what the consequences of removing it would be.

mcdruid’s picture

Priority: Normal » Major

Sorry, didn't mean to revert the Priority.

This is a potential blocker for the current top-priority D7 core issue.

mcdruid’s picture

Status: Active » Needs review
FileSize
56 bytes

Looking more closely, it's actually the backticks that migrate is placing around the dbname for mysql databases that are causing the problem.

If we remove those lines, it looks like migrate works okay when it adds the dbname to the table name.

I think this is because D7 core (with the latest patches from the MySQL 8 issue) can cope with e.g. {d7target.migrate_map_d7sourcefile} as long as it doesn't contain backticks.

That now gets wrapped in quotes e.g. "d7target.migrate_map_d7sourcefile" so the original reason for adding the backticks no longer applies (once table name quoting is added to core).

So I think we can simply remove the lines that add the backticks - here's a patch which does that.

mcdruid’s picture

Status: Needs review » Needs work

Oops the patch was a snafu, and I think I may have jumped to an incorrect conclusion anyway.

...looking at this some more.

edit - the patch should have been this:

diff --git a/plugins/sources/sqlmap.inc b/plugins/sources/sqlmap.inc
index 247bf36..c5c11cd 100644
--- a/plugins/sources/sqlmap.inc
+++ b/plugins/sources/sqlmap.inc
@@ -39,13 +39,7 @@ class MigrateSQLMap extends MigrateMap {
       return $this->mapTable;
     }
     else {
-      $dbname = $options['database'];
-
-      if (!empty($options['driver']) && $options['driver'] == 'mysql') {
-        // Backtick the db name on mysql to ensure dbs with hyphens work.
-        $dbname = "`{$dbname}`";
-      }
-      return $dbname . '.' . $this->mapTable;
+      return $options['database'] . '.' . $this->mapTable;
     }
   }
mcdruid’s picture

Yes, looks like I got that wrong; in my manual testing the first migration I'm running always succeeds, and that's because getQualifiedMapTable only gets called on subsequent migrations when \MigrateSource::rewind is called.

It's only then that this fails with the MySQL 8 patches when it adds the dbname. qualifier to the table name.

So unfortunately I'm back to thinking this won't work with the latest MySQL 8 patches; and I'm inclined not to add potentially expensive workarounds in core.

pifagor’s picture

pifagor’s picture

Oh, I see your message, just did not update the page.

@joseph.olstad - I somewhat overloaded, as soon as I can allocate time for tasks, I will do it.
Of course help and patch are welcome

mcdruid’s picture

getQualifiedMapTable is called from two places, and one of them does this:

https://git.drupalcode.org/project/migrate/-/blob/7.x-2.11/plugins/sourc...

          $count_needs_update = db_query('SELECT COUNT(*) FROM {' .
            $this->activeMap->getQualifiedMapTable() . '} WHERE needs_update = 1')
            ->fetchField();

...which I think is where the problem is happening, because the curly braces are being wrapped around dbname.tablename.

Here's a patch which adds a param to indicate whether the table name (and only that) should be wrapped in braces to avoid this issue.

This works AFAICS, and I think it may actually mean it's possible to remove the logic which skips the qualiification of table names if prefixing is in place. I've not tried to do that in this patch, but it would reduce duplication in the method if we could.

mcdruid’s picture

I'm not certain why the tests are hitting composer problems.

However, the patch wasn't working as intended because of a silly PHP issue with double quotes, variable interpolation and the curly braces :face-palm:

With this version, tests are passing for me locally (PHP 7.2 and MySQL 5.6 without MySQL 8 core patches).

mcdruid’s picture

I've run the Migrate tests now with the following combinations:

  • PHP 7.2 and MySQL 5.6 (with and without the core MySQL 8 patch #218)
  • PHP 7.2 and MySQL 5.7 (with and without the core MySQL 8 patch #218)
  • PHP 7.2 and MySQL 8 (with the core MySQL 8 patch #218)

All tests pass in each case with #11 applied to migrate.

I've also done a manual test of the 2-stage d2d migration (so that rewind is called) with each of the MySQL versions, and I've not hit any errors.

So looks to me like this patch works.

As mentioned, I think it may actually be possible to remove the "skip if there's a prefix" logic from getQualifiedMapTable with this change, but I've not looked at that in any detail, and it might be one for a follow-up rather than allowing scope-creep here.

joseph.olstad’s picture

could be related to changes in the drupalci_testbot , here is a link to an issue that might be related.

Ronino’s picture

Status: Needs review » Needs work

Thanks for the patches, @mcdruid!

Unfortunately adding the braces in getQualifiedMapTable() doesn't change anything as they will be stripped anyways by DatabaseConnection::escapeTable():

  public function escapeTable($table) {
    if (!isset($this->escapedNames[$table])) {
      $this->escapedNames[$table] = preg_replace('/[^A-Za-z0-9_.]+/', '', $table);
    }
    return $this->escapedNames[$table];
  }

SelectQuery::__toString() will then surround whatever escapeTable() returns as table with { and }, still ending up with "{db.table}":

        $table_string = '{' . $this->connection->escapeTable($table['table']) . '}';

Update: The patch should fix performRewind()'s static db_query(), but doesn't fix the dynamic query that uses leftJoin().

mcdruid wrote:

All tests pass in each case with #11 applied to migrate.

I think there is no test that tests an external database using MigrateSourceSQL as the source class. Only Oracle is tested which uses MigrateSourceOracle which has its own unaffected performRewind() implementation.

mcdruid’s picture

I've also been manually testing with a migration between two D7 dbs.

IIUC the difference you're pointing out is:

if ($this->mapJoinable) {

https://git.drupalcode.org/project/migrate/-/blob/7.x-2.11/plugins/sourc...

vs.

if (!$this->mapJoinable) {

https://git.drupalcode.org/project/migrate/-/blob/7.x-2.11/plugins/sourc...

So seems this patch fixes the latter but not the former; I'm guessing then that in my migrate_d2d testing I don't have a mapJoinable mapping.

Noting for context that in #1480762-6: Join of legacy data to default database map table does not work (where the dbname. qualifying was added) @mikeryan said:

... seems to work for normal db-to-db migrations, and in simpletest. It will not work for a migration from an external database into a prefixed Drupal installation (but then it never did, so no regression) - in those cases you'll have to use mapJoinable => FALSE.

I'm not sufficiently familiar with all of this to judge whether that describes an acceptable workaround or whether more work is needed to try and fix:

$this->query->leftJoin($this->activeMap->getQualifiedMapTable(),
          'map', $map_join);

...when mapJoinable is TRUE.

Since the dbname is not added when a table prefix is in use, what would the consequences be of simply not adding it for the leftJoin?

Ronino’s picture

As leftJoin() needs the table name without the database, I added another parameter to getQualifiedMapTable() to achieve this. Now it works with either setting of mapJoinable.

Ronino’s picture

Status: Needs work » Needs review
mcdruid’s picture

#2978575: Mysql 8 Support on Drupal 7 was just committed to D7 and should be included in the release next week (which will almost certainly be Drupal 7.75).

anrikun’s picture

Patch at #16 works for me.

tomdearden’s picture

#16 working here, too, fixing a regression introduced when moving from 7.75 to 7.78.

rossb89’s picture

Status: Needs review » Reviewed & tested by the community

Yup, #16 working here for me to!

In case you have written your own bespoke migrate source plugin code then don't forget to change the same bits for the $count_needs_update bit in your migrate source code i.e. from

$count_needs_update = db_query('SELECT COUNT(*) FROM {' . $this->activeMap->getQualifiedMapTable() . '} WHERE needs_update = 1')

to

$count_needs_update = db_query('SELECT COUNT(*) FROM ' . $this->activeMap->getQualifiedMapTable(TRUE) . ' WHERE needs_update = 1')

after applying the patch!

cydharttha’s picture

Patch #16 didn't work for me. When looking at the error, it looks like it's trying to go to the source db for the migate_map table:

Migration for MyMigration failed with source plugin exception: [error]
SQLSTATE[42S02]: Base table or view not found: 1146 Table
'source_database.migrate_map_myentity' doesn't exist

I'm still able to do adds/updates to individual records by specifying an idlist.

hhvardan’s picture

Status: Reviewed & tested by the community » Needs work

I agree with cydharttha, the patch is causing another error.

izmeez’s picture

Is this related to #1349080: node_access filters out accessible nodes when node is left joined?

There is a patch for D7 in #1349080-473: node_access filters out accessible nodes when node is left joined and the backport for D7 is now in #3176634: [D7] node_access filters out accessible nodes when node is left joined. Although the original thread has a new comment that attempts to remove some awkwardness #1349080-480: node_access filters out accessible nodes when node is left joined this is not applicable to the D7 patch.

vinmassaro’s picture

#16 fixes the issue for me, thanks.

cydharttha’s picture

Looks like if I do

$conf['mysql_identifier_quote_character'] = ''; as per https://www.drupal.org/node/3185889#comment-13924331

and change from FALSE, FALSE to

$alias = $this->query->leftJoin($this->activeMap->getQualifiedMapTable(FALSE, TRUE),
          'map', $map_join);

on line 360 of sql.inc, my migrations are ok.

mcdruid’s picture

This issue (or at least a very closely related one) is being looked at in D9 core over in #3227361: Fix SqlBase::initializeIterator()'s cross-database joining when using particular DB/table names.

Perhaps the eventual fix there will be backport-able?

skylord’s picture

#16 works OK, thanks (cross-db without mapJoinable).

joseph.olstad’s picture

#16 is reported as working, I triggered new automated tests for this as we have some unrelated fixes gone in.

joseph.olstad’s picture

Status: Needs work » Active

  • joseph.olstad committed 382c9484 on 7.x-2.x
    Issue #3171091 by mcdruid, Ronino, cydharttha, pifagor, tomdearden,...

  • joseph.olstad committed 78b7be27 on 7.x-2.x
    Revert "Issue #3171091 by mcdruid, Ronino, cydharttha, pifagor,...
joseph.olstad’s picture

Status: Active » Needs work

Some previously reported #16 as not working, but then #28 @skylord and #25 @vinmassaro reports the patch 16 as working.

Holding off making any change here, look towards possible core fix or perhaps more reviews on #16.
#3227361: Fix SqlBase::initializeIterator()'s cross-database joining when using particular DB/table names

joseph.olstad’s picture

For now until the upstream core issue is resolved, I suggest following suggestion by @cydharttha on line #26
#3171091-26: \MigrateSQLMap::getQualifiedMapTable may break with MySQL 8 support in D7 core

OR

Alternatively, consider using patch #16 if that works for you:
#3171091-16: \MigrateSQLMap::getQualifiedMapTable may break with MySQL 8 support in D7 core