An issue has been created for Drupal 8 already, but this is for Drupal 7 particularly.

Problem/Motivation

MySQL 8 has been release and includes performance enhancements. It would be nice to integrate those enhancements with Drupal 7.

Proposed resolution

Test a variety of Drupal 7 distributions running with MySQL 8 and fix bugs as they appear.

Remaining tasks

  • Add support

User interface changes

  • Note during the site install that MySQL 8 is supported.

API changes

N/A

Data model changes

N/A

I have created a workaround patch that serves as a temporary workaround.

CommentFileSizeAuthor
#298 2978575-298.patch23.95 KBmcdruid
#298 interdiff-2978575-290-298.txt1.35 KBmcdruid
#293 Selection_087.png63.28 KBmcdruid
#290 2978575-290.patch23.75 KBmcdruid
#290 interdiff-2978575-275-290.txt1022 bytesmcdruid
#288 2978575-287.patch24.08 KBmcdruid
#288 interdiff-2978575-275-287.txt1.27 KBmcdruid
#275 2978575-275.patch23.54 KBmcdruid
#275 interdiff-2978575-274-275.txt1.64 KBmcdruid
#274 2978575-274.patch22.81 KBmcdruid
#274 interdiff-2978575-271-274.txt917 bytesmcdruid
#271 2978575-271.patch22.81 KBmcdruid
#271 interdiff-2978575-255-271.txt1.41 KBmcdruid
#255 2978575-255.patch22.38 KBmcdruid
#255 interdiff-2978575-248-255.txt3.37 KBmcdruid
#253 Selection_002.png273.6 KBmcdruid
#248 2978575-248.patch21.97 KBmcdruid
#248 interdiff-2978575-245-248.txt1.13 KBmcdruid
#245 2978575-245.patch21.97 KBmcdruid
#245 interdiff-2978575-218-245.txt3.55 KBmcdruid
#228 2978575-207-to-218-diff.txt11.09 KBizmeez
#218 interdiff-2978575-217-218.txt2.02 KBmcdruid
#218 2978575-218.patch20.62 KBmcdruid
#217 interdiff-2978575-216-217.txt5.21 KBmcdruid
#217 2978575-217.patch20.62 KBmcdruid
#216 interdiff-2978575-207-216.txt2.96 KBmcdruid
#216 2978575-216.patch18.17 KBmcdruid
#207 2978575-207.patch19.35 KBmcdruid
#207 interdiff-2978575-205-207.txt1.05 KBmcdruid
#205 2978575-205.patch19.24 KBmcdruid
#205 interdiff-2978575-203-205.txt10.93 KBmcdruid
#203 2978575-203.patch21.19 KBmcdruid
#203 interdiff-2978575-201-203.txt2.62 KBmcdruid
#201 2978575-201.patch19.9 KBmcdruid
#201 interdiff-2978575-199-201.txt2.14 KBmcdruid
#199 2978575-199.patch18.87 KBmcdruid
#199 interdiff-2978575-196-199.txt4.8 KBmcdruid
#196 2978575-194-196-interdiff.txt1.35 KBayesh
#196 2978575-196.patch17.49 KBayesh
#194 2978575-194.patch16.63 KBmcdruid
#194 interdiff-2978575-188-194.txt3.67 KBmcdruid
#191 2978575-interdiff-185-190.txt1.42 KBayesh
#190 2978575-190.patch16.79 KBayesh
#189 2978575-interfix-188-189.txt2.24 KBayesh
#189 2978575-189.patch16.76 KBayesh
#188 2978575-188.patch16.82 KBmcdruid
#188 interdiff-2978575-185-188.txt1.2 KBmcdruid
#187 2978575-interfix-185-187.txt2.26 KBayesh
#187 2978575-187.patch16.66 KBayesh
#186 2978575-186.patch14.05 KBmcdruid
#186 interdiff-2978575-185-186.txt3.93 KBmcdruid
#185 2978575-185.patch16.72 KBmcdruid
#185 interdiff-2978575-183-185.txt1.06 KBmcdruid
#183 2978575-183.patch16.7 KBmcdruid
#183 interdiff-2978575-182-183.txt1.14 KBmcdruid
#182 2978575-182.patch15.79 KBmcdruid
#182 interdiff-2978575-180-182.txt879 bytesmcdruid
#180 2978575-180.patch15.54 KBmcdruid
#180 interdiff-2978575-178-180.txt6.28 KBmcdruid
#178 2978575-177--178-156-rebase-interdiff.txt14.13 KBayesh
#178 2978575-178.patch14.13 KBayesh
#177 2978575-177.patch13.93 KBayesh
#170 2978575-170.patch13.82 KBayesh
#169 2978575-166---168-interdiff.txt1.17 KBayesh
#169 2978575-168.patch14.13 KBayesh
#166 2978575-interdiff-162--166.txt5.86 KBayesh
#166 2978575-166.patch14.54 KBayesh
#162 2978575-162.patch14.34 KBmcdruid
#162 interdiff-2978575-161-162.txt573 bytesmcdruid
#161 2978575-161.patch14.39 KBmcdruid
#161 interdiff-2978575-159-161.txt1.16 KBmcdruid
#159 2978575-159.patch14.01 KBmcdruid
#159 interdiff-2978575-157-159.txt5.77 KBmcdruid
#158 2978575-158_noop.patch289 bytesmcdruid
#157 2978575-157.patch12.72 KBmcdruid
#157 interdiff-2978575-154-157.txt1.23 KBmcdruid
#154 2978575-154.patch12.47 KBmcdruid
#154 interdiff-2978575-149-154.txt956 bytesmcdruid
#149 2978575-149.patch12.36 KBmcdruid
#149 interdiff-2978575-144-149.txt576 bytesmcdruid
#144 2978575-144.patch12.08 KBmcdruid
#144 interdiff-2978575-142-144.txt664 bytesmcdruid
#142 2978575-142.patch12.08 KBmcdruid
#142 interdiff-2978575-137-142.txt1.85 KBmcdruid
#141 database-folder-patched-124.zip105.74 KBtfranz
#141 core.zip687 bytestfranz
#137 mysql-8-2978575-137.patch13.25 KBronino
#137 mysql-8-2978575-136-137-interdiff.txt1.13 KBronino
#136 mysql-8-2978575-136.patch12.13 KBronino
#136 mysql-8-2978575-124-136-interdiff.txt1.1 KBronino
#134 2020-06-18 16_15_44-StrokesPlus.png21.37 KBghost of drupal past
#128 interdiff-112-118.txt1001 bytestr
#126 mysql-8-2978575-118-124-interdiff.txt549 bytesronino
#124 mysql-8-2978575-124.patch12.13 KBronino
#118 mysql-8-2978575-118.patch12.15 KBMixologic
#112 mysql-8-2978575-112.patch11.08 KBronino
#112 mysql-8-2978575-109-112-interdiff.txt1.09 KBronino
#109 mysql-8-2978575-109.patch11.09 KBronino
#109 mysql-8-2978575-84-109-interdiff.txt1.09 KBronino
#84 2978575-84.patch11.38 KBronino
#84 interdiff.txt1.14 KBronino
#67 2978575-67.patch10.86 KBayesh
#59 mysql-8-2978575-59.patch4.03 KBfietserwin
#59 interdiff.txt2.83 KBfietserwin
#57 mysql-8-2978575-57.patch2.9 KBfietserwin
#57 interdiff.txt1.93 KBfietserwin
#53 mysql-8-2978575-53.patch2.77 KBfietserwin
#5 mysql8_no_auto_create_user_only.patch1.51 KBemilcarpenter
#14 2978575-14.patch10.7 KBalmaudoh
#4 mysql8_no_auto_create_user.patch1.92 KBemilcarpenter
#50 mysql-8-2978575-50.patch3.08 KBronino
#24 mysql-8-2978575-24.patch1.51 KBberenddeboer
#7 core-mysql8-compatibility-2978575-7-D7.patch2.06 KBemilcarpenter
mysql8-patch.patch1.33 KBelijahoyekunle

Issue fork drupal-2978575

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

elijahoyekunle created an issue. See original summary.

elijahoyekunle’s picture

It seems there is an error with the patch. I think I need help here, it worked on my computer.

shenzhuxi’s picture

I installed Drupal 7 with MySQL 8 successfully with this patch.

emilcarpenter’s picture

StatusFileSize
new1.92 KB

Added server version check for MySQL 8.0.11 and an if condition based on that.

emilcarpenter’s picture

StatusFileSize
new1.51 KB

Removed the line
$sql = str_replace("{system}", "{`system`}", $sql);
introduced in the original patch by elijahoyekunle.

I do not know what that line is good for. Works for me without that line.

emilcarpenter’s picture

Category: Support request » Feature request
Status: Active » Needs review
emilcarpenter’s picture

Reinserted the line

$sql = str_replace("{system}", "{`system`}", $sql);

and wrapped also that in a version check. It is indeed needed for running D7 with MySQL 8.

I have now tested this patch on MySQL 5.6.40 and MySQL 8.0.11. (Without that line, on 8.0.11, I get an error on Cache Clear All.)

A drupal.org forum page about Drupal 7 with MySQL 8 is here and a there is a link with an explanation.

Status: Needs review » Needs work

The last submitted patch, 7: core-mysql8-compatibility-2978575-7-D7.patch, failed testing. View results

mmjvb’s picture

That line is required because system became a reserved word in MySQL 8. The backticks allow it to be used as table name.

Consider the conditional setting of sql-mode the right thing to implement this issue. Would prefer a different implementation:
- initialize without
- extend when appropriate ( not 8 )
- append to init command.

In addition you might want to check the manipulation of information schema in the D8 patch. Several additions of `as .....`.

andyrandom’s picture

This fix got Drupal working, but Drush is still problematic. That's because of the drush_module_list function which calls:
drush_db_select('system', 'name', 'type=:type AND status=:status', array(':type' => 'module', ':status' => 1));
And this aliases the "system" table as "system," so the query ends up looking like:
SELECT system.name AS name FROM {system} system WHERE (type='module' AND status='1');
So the patch escapes the {system} in braces, but not the system alias.
I "solved" this by adding a line before the patch:
$sql = str_replace("{system} system", "{system}", $sql);
So now Drush works, and nothing else appears to be broken, but this probably isn't the way to do it.
Anybody got a more effective solution?

mmjvb’s picture

Each module needs to fix its own bugs. Suggest to check drush queue and raise an issue there.

You might try replacing 'system' with '`system`'

Not convinced the solution for reserved word system works properly. It shouldn't replace {system} with {`system`}, which only works with particular prefixes. It should translate {system} into `system` when table prefix is not a database prefix or no prefix provided.

sjerdo’s picture

The current patch will break other database drivers like PostgreSQL which do not use backticks to quote identifiers. Also this patch will break table prefixes.

I suggest we'll backport the patches in #2966523: MySQL 8 Support and #2986452: Database reserved keywords need to be quoted as per the ANSI standard once these are committed in D8.

xpiku’s picture

Also using

replace {system} with {`system`}

will brake core simpletest
so it would rather work this way:

$sql = str_replace("{system}", "`{system}`", $sql);
almaudoh’s picture

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

Ported some part of the patch in #2966523: MySQL 8 Support to Drupal 7. This patch still needs to resolve escaping table names that are reserved keywords particularly the `system` table which doesn't exist in Drupal 8. Still work in progress...

bmiro-apsl’s picture

Hi,

There is any update on this?

Is still planned to support MySQL 8 in Drupal 7 or it has been dismissed?

Thanks

back-2-95’s picture

Hello,

If anyone would like to use Drupal 7 with Digital Ocean's Mysql 8 DBaaS, this needs to be fixed.

pol’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: +Drupal 7.68 target

Hi,

Tested with success as well on my side.

joseph.olstad’s picture

To increase our confidence in the test results for the above issue, these two should go in first so we can re-queue php 7.3 and php 5.3 tests.
#3047844: [Regression] Tests fail on PHP 5.3
#3025335: session_id() cannot be changed after session is started

fabianx’s picture

Status: Reviewed & tested by the community » Needs work

To clarify some things:

- We will be supporting MySQL 8 in Drupal 7 — it is just not yet clear when.

There is several concerns with the patch and in the worst case we can commit it in stages:

- We should document that not only is NO_AUTO_CREATE_USER removed from MySQL 8, but also that it’s the new default since 5.7 (so no longer needed)

- We definitely need the tests ported from D8 as well

- We might want some more tests, too: If we say something is supported and old legacy D7 code breaks when switching due to an oversight, then we don’t actually support.

Main questions I have here are:

db_query() + reserved key words.
Expressions + keywords
Table prefixes and keywords

The information schema parts are fine and easy to commit though.

mcdruid’s picture

@Fabianx and I also reviewed this:

+    $insert_fields = array_map(function ($field) {
+      return $this->connection->escapeField($field);
+    }, $insert_fields);

...which is the same as (or very similar to) the D8 patch, but we don't generally use this syntax (anonymous function as the callback) in D7. It was introduced in PHP 5.3, so should work pretty much everywhere nowadays, but this would be the first usage of it to be introduced to D7 core.

berenddeboer’s picture

What I don't get is how difficult we make this. Why can't we not simply use back ticks for every table and column?

berenddeboer’s picture

StatusFileSize
new1.51 KB

Here's my much simpler patch to advance the discussion. This one doesn't require any fixes to Drush.

ayesh’s picture

Why can't we not simply use back ticks for every table and column

If we were to have a full ORM, we surely can inspect each and every table/field/alias and quote them with backticks. What we have is a half-baked system, that queries might come from structured data (which we can quote properly), and raw SQL queries which we cannot properly quote.

For example, the following is a valid SQL query:

SELECT `where` FROM `where` WHERE `where` = 'where' 

Unless we parse the query, there is no bullet-proof way to decide which ones need backticks, which ones should not. That is why we have this solution to individually process tables, fields, and aliases. I personally think that we can improve the performance of the escaping step (for example, using a single strtr call instead of an array_reduce + lambda calls for each reserved keyword), but I don't think we can have a more simpler solution.

berenddeboer’s picture

I don't think we need to parse:

  1. We can quote tables in any query using '{..}' which they already should.
  2. We can quote stuff build with fields() etc, in those cases we know exactly what the column is.

What we currently don't handle is arbitrary quoting of arbitrary SQL. That's fine. I don't think any patch will ever do that. We can leave that stuff alone to either use db_select() or add their own quotes.

I.e. only quote the stuff we know for sure can be quoted. Leave the rest alone.

ayesh’s picture

I'm also in favor of quoting all fields/tables with backticks just so we can avoid iterating the rather big reserved keywords list and the future-proofing it provides with future reserved keywords MySQL might add.

Some related issues: #3020839: Drupal\Core\Database\Driver\mysql\Connection::quoteIdentifier in_array performance improvements, #2986452: Database reserved keywords need to be quoted as per the ANSI standard.

mustanggb’s picture

Issue tags: -Drupal 7.68 target +Drupal 7.69 target
ronlee’s picture

Any update on the status of this critical issue?

I'm not a strong enough programmer to help, but would love to start using MySQL 8. :)

Does this patch affect performance? Can I use it in production? mysql-8-2978575-24.patch

ayesh’s picture

Realistically, I suppose a lot more tests need to be done and written before this lands in Drupal core because there can be a lot of edge cases considering `system` is a reserved keyword in MySQL 8, which Drupal uses extensively.

I use a patched Drupal 7 core in several of my web sites, none of them fortune 500, but quite important ones. So far, it seems to be working well. Drush 8 is not working properly, so you will need some tinkering around to get it working.

Other than that, I encountered MySQL server has gone away errors with PHP 7.4. Not related to Drupal in particular though. See the linked article on a fix.

I'm not a strong enough programmer to help, but would love to start using MySQL 8. :)

I'd say go for it. If you encounter any errors, post here, and the others will gladly work together for a fix, including myself.

Does this patch affect performance? Can I use it in production? mysql-8-2978575-24.patch

I'm a bit concerned the excessive array scanning for reserved keywords. See #3020839: Drupal\Core\Database\Driver\mysql\Connection::quoteIdentifier in_array performance improvements. I use a similar patch from the linked issue (I'm the author), but the issue did not receive any attention. PHP's opcache should provide some optimizations given the pseudo-static nature of the reserved keywords array. This will be a micro-optimization and it will be unlikely a bottleneck for performance.

This comment was regarding the patch in #14; not 24.

ronlee’s picture

Thanks for your reply :)

I use a patched Drupal 7 core in several of my web sites, none of them fortune 500, but quite important ones. So far, it seems to be working well. Drush 8 is not working properly, so you will need some tinkering around to get it working.

Sorry, which mysql 8 patch are you using on your websites?

ayesh’s picture

Sorry, which mysql 8 patch are you using on your websites?

Patch in #14.

ronlee’s picture

@Ayesh Thank you!

FYI, patch #14 did not work for me, but patch #24 works...so far.

I'm running PHP 7.2.25 and MySQL 8.0.18.

izmeez’s picture

@Ayesh "Drush 8 is not working properly, so you will need some tinkering around to get it working." Can you elaborate? We recently upgraded to drush 8 on php 7.3 because drush 7 fails. The only issue we encountered was the dependence on needing the php extension fileino for now, see #3085098: PHP Phar Stream Wrapper should be updated (PHP7.4 support, drop fileinfo dependency). Is there something else? So far drush 8.3.2 appears to be working fine for us. We are using MariaDB 10.

ayesh’s picture

This is the error I'm getting from Drush (for drush up):

WD php: PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check [error]
the manual that corresponds to your MySQL server version for the right syntax to use near 'system
WHERE  (type='module' AND status='1')' at line 3: SELECT system.name AS name
FROM
{system} system
WHERE  (type=:type AND status=:status) ; Array
(
    [:type] => module
    [:status] => 1
)
 in drush_db_select() (line 99 of /root/.config/composer/vendor/drush/drush/includes/dbtng.inc).
PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'system
WHERE  (type='module' AND status='1')' at line 3: SELECT system.name AS name
FROM
{system} system
WHERE  (type=:type AND status=:status) ; Array
(
    [:type] => module
    [:status] => 1
)
 in drush_db_select() (line 99 of /root/.config/composer/vendor/drush/drush/includes/dbtng.inc).
Drush command terminated abnormally due to an unrecoverable error.                                                        [error]
Error: Array and string offset access syntax with curly braces is deprecated in
/root/.config/composer/vendor/drush/drush/includes/sitealias.inc, line 176
izmeez’s picture

Have you confirmed that the php extension fileinfo is loaded, php -m

ayesh’s picture

Yes it's loaded.

izmeez’s picture

From the findings it looks like there are two issues; (1) related to use of mysql 8 with drupal 7 itself, and (2) related to the use of drush.

izmeez’s picture

@ayesh did you try the patch in #24 that does not affect drush?

izmeez’s picture

Although it does have a hunk to make drush work.

izmeez’s picture

Patch in #24 applies without difficulty to the latest drupal 7.69 and appears to work fine with MariaDB 5.5.5-10.2.29 and drush 8.3.2 with php 7.3.12 does not give errors with "drush up".

mustanggb’s picture

Issue tags: -Drupal 7.69 target +Drupal 7.70 target
seamus_lee’s picture

I have tested Patch #24 and appears to work for me and with drush 8

rjt1224’s picture

Also make sure settings.php configuration has

<?php
'init_commands' => array (
        'isolation' => "SET SESSION transaction_isolation='READ-COMMITTED'",
.....
?>

MySQL 8 renamed tx_isolation to transaction_isolation

<?php
'init_commands' => array (
        'isolation' => "SET SESSION tx_isolation='READ-COMMITTED'",
.....
?>
rjt1224’s picture

#24 works great with Drush 8.1.x and MySQL 8.0.x & PHP 7.2.x and Drupal 7.69.
Will this release be sent with the latest Drupal 7 Core? It shows Drupal 7.70 target.

lakshmi_a’s picture

I have updated mysql 8.0.18 version with drupal 7.56... after update version i am facing mysql syntax error.I am applied path #24 after that my application issue has been resolved. thank you soomuch...

I am added
includes/database/mysql/database.inc
public function query($query, array $args = array(), $options = array()) {
$query = preg_replace('/{([^}]+)}/', '`\1`', $query);
// This to make Drush work
$query = str_replace(' system.', ' `system`.', $query);
$query = str_replace('`system` system', '`system` `system`', $query);
return parent::query($query, $args, $options);
}

And Setting.php i have added
'sql_mode' => "SET sql_mode = 'REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO'",
);
mysql 80.18 PORT :3308
within database connection. it's working fine for me.

sivaprasadc’s picture

@berenddeboer

Thanks for the patch #24. It work great with Drupal 7.69, MySQL 8.0.x and PHP 7.3.x.

izmeez’s picture

Would it be fair to say this is RTBC or is there something else left to do?

vensires’s picture

Status: Needs work » Reviewed & tested by the community

From my experience with the patch in #24 and the comments of the other users I set this as RTBC.

ronino’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new3.08 KB

I have improved patch #24 to also handle queries that specify the database along with the table, like in "{db.table}". Those must be quoted to `db`.`table` instead of `db.table`. This is required e.g. by migrate when a different source database is queried and probably always if a database connection other than the default one is used. Without it, the query fails.

The quoting is also only done if we're indeed on MySQL 8 as it's unnecessary for version 5.

I reintroduced the conditional addition of NO_AUTO_CREATE_USER from patch #14 as it has gone into Drupal 8, too (see #2966523: MySQL 8 Support).

Before this can be RTBC, I guess we need test functions, like stated in #21.

ronino’s picture

Status: Needs review » Needs work

The test on PHP 7.3 & MySQL 8 fails as DrupalTestCase::insertAssert() wants to save a field "function" to the simpletest table which doesn't work as "function" is a reserved keyword. Seems like quoting table names is not enough and changes from patch #14 need to be incorporated.

shaneonabike’s picture

This is a fairly critical for anyone using Ubuntu 19.04 since it is no longer being supported. Upgrading would result in Mysql 8 which is throwing bad errors :/

fietserwin’s picture

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

Patches from #24 and #50 don't work when a table prefix has been set.

To me it seems more logical to have the code that replaces the { and } should add the back ticks, i.e. override method DatabaseConnection::setPrefix() instead of changing DatabaseConnection_mysql::query() as these patches do. So, patch #14 looks better, though if we override DatabaseConnection::setPrefix() to always back tick a table name, regardless if it is a reserved keyword or not, that code would be less complicated, see attached patch.

This patch solved for me:
- The problem with NO_AUTO_CREATE_USER being removed
- The error introduced by the patches from #24 and #50, namely something like "Table semaphore does not exist in ..." (I am using a database prefix, thus the table semaphore indeed does not exist, it should be prepended with the prefix).

This patch should solve for those who do not use a prefix:
- Tables having the same name as a reserved keyword giving errors because the table name is not being quoted.

Other problems with MySQL8 (quoting fields that are a reserved keyword?) are not solved by this patch.

mustanggb’s picture

We seem to be split between people not liking the complexity of the #14 route even though it keeps us closer in-sync with D8, and people not liking the divergence and less complete solution of the #24 route.

So why not try to make both camps happy and do a backport of #2986452: Database reserved keywords need to be quoted as per the ANSI standard instead?

That said, #14 is working fine over here, with a very minor Drush patch (https://github.com/drush-ops/drush/pull/4313).

joergm’s picture

#53 works fine for me.
However drush 8 is not working with #53.

I appreciate if someone can extend #53 so drush 8 is working, thx.

tr’s picture

Priority: Normal » Major
Status: Needs review » Needs work

#53 fails for me when I visit /admin/config

PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'system WHERE type = 'module'' at line 1: SELECT name, schema_version FROM {system} WHERE type = :type; Array ( [:type] => module ) in drupal_get_installed_schema_version() (line 155 of /var/www/d7.example.com/public_html/includes/install.inc).

The line it's failing on is clearly one case this patch was intended to fix, as it is accessing the {system} table:
$result = db_query("SELECT name, schema_version FROM {system} WHERE type = :type", array(':type' => 'module'));

I made the priority higher because this started failing for me when I upgraded Ubuntu from 19.04 to 19.10. The latter is already 6 months old, and Ubuntu is about to release 20.04, but one must upgrade from 19.04 to 19.10 first before moving to 20.04. That said, I anticipate quite a few users encountering this problem over the next few months after 20.04 comes out. Likewise, the effort to make D7 run with PHP 7.4 won't be of much use if MySql 8 can't be used at the same time; most systems won't have a default configuration that includes a new version of PHP along with an old version of MySql - they will either have both new or both old.

As a temporary fix, I hacked core and changed the above query to use {`system`}. That's not a good solution of course, but it will allow me to continue to test #53 to see if I encounter any other problems.

EDIT: Yeah, there are a lot of problems still. Here's another, when visiting /admin/config/developement/testing (Simpletest):

PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'system' at line 1: SELECT name, filename FROM {system}; Array ( ) in simpletest_test_get_all() (line 336 of /var/www/d7.example.com/public_html/modules/simpletest/simpletest.module).

Again, this is clearly a query that should have been fixed by the patch. Again I hacked core, and that simply got me to the next similar error. I don't think it makes sense to keep listing them here - just a few minutes of manual testing shows that #53 only works for some things, but not nearly enough.

fietserwin’s picture

Status: Needs work » Needs review
StatusFileSize
new2.9 KB
new1.93 KB

EDIT: forget this one, continue with #59

Status: Needs review » Needs work

The last submitted patch, 57: mysql-8-2978575-57.patch, failed testing. View results

fietserwin’s picture

Status: Needs work » Needs review
StatusFileSize
new4.03 KB
new2.83 KB

The patch did not work correctly due to the order of setting isMySql8 and calling setPrefix(). I tested with a prefix that obscured that (Tip: as a workaround work with a table prefix :))

Attached a correction of the patch form #53. Note that this still does not contain a fix for fields named after reserved words, though that still seems to be allowed in the query mentioned in #56 (type is a reserved word!).

tr’s picture

I triggered the testbot to test #59 against MySql 8, since that's the whole point of this issue....

joseph.olstad’s picture

Status: Needs review » Needs work

Fails mysql 8

PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'function, line, file) VALUES ('1', 'ActionsConfigurationTestCase', 'fail', 'The ' at line 1 in /var/www/html/includes/database/database.inc:2227
Stack trace:
#0 /var/www/html/includes/database/database.inc(2227): PDOStatement->execute(Array)
#1 /var/www/html/includes/database/database.inc(697): DatabaseStatementBase->execute(Array, Array)
#2 /var/www/html/includes/database/mysql/database.inc(147): DatabaseConnection->query('INSERT INTO `si...', Array, Array)
#3 /var/www/html/includes/database/mysql/query.inc(36): DatabaseConnection_mysql->query('INSERT INTO `si...', Array, Array)
#4 /var/www/html/modules/simpletest/drupal_web_test_case.php(229): InsertQuery_mysql->execute()
#5 /var/www/html/modules/simpletest/drupal_web_test_case.php(523): DrupalTestCase::insertAssert('1', 'ActionsConfigur...', 'fail', 'The test did no...', 'Completion chec...', Array)
tr’s picture

@joseph.olstad: Is that output from local testing? The testbot output in #59 shows that the failure is in drush_db_select(), since drush is used by the testbot to set up the test environment. The testbot never gets around to testing this patch because the environment setup fails.

But that does bring up once again that drush also needs to be fixed to support MySql 8, and that seems to be a prerequisite for getting this patch tested by the testbot. This has been mentioned in #10, #11, #30, #34, #38, and #55. Patch #24 and its derivatives make a special fix to accomodate drush, but as mentioned in #11 this should be fixed in drush instead - we can't be adding special cases for contributed modules into core, that should be fixed in the contributed module (drush).

joseph.olstad’s picture

ok so there's a fix for drush that hasn't gone in. Perhaps someone who knows more about this issue should perhaps follow up by creating a new issue in the drupalci_testbot queue with helpful explanation and instructions for the drupalci_testbot maintainer. Such as which drush patch to apply to the mysql 8 runner.

https://www.drupal.org/project/issues/drupalci_testbot

So , which patch does the drupalci_testbot drush require in order to be able to properly test against mysql 8?

I imagine this will cause some issues as the same version of drush might be used in other runners.

tr’s picture

@joseph.olstad: Do you have a link to that drush fix? What I'm seeing in github is that this is a problem with Drush 8, and since Drush 8 is not supported there hasn't been much traction to fix this: https://github.com/drush-ops/drush/issues/3830

DrupalCI needs to use Drush 8 because Drush 9 doesn't work with Drupal 7.

joseph.olstad’s picture

ok looks like @MustangGB already created a pull request for Drush 8.x

I suggest that if the drush people do not respond in a timely manner, an option would be to fork drush 8 , OR for the drupalci_testbot project to patch their drush 8.x

here is the link to the pull request for drush 8.x
https://github.com/drush-ops/drush/pull/4313

**begin update**

I have commented on the upstream pull request and issue.

**end of update**

fietserwin’s picture

Some thoughts:

  • This looks like a chicken and egg problem: drush depends on Drupal and can thus not be tested on MySQL8, Drupal testbot depends on Drush and thus patches cannot be tested on MySQL8
  • As already indicated, my patch from #59 only solves the set sql mode problem and non-quoted tables. It does not tackle the problem of quoting field names that are reserved words. (However, so far it seems to be working fine for me).
  • Many test failures are to be expected, especially in the tests that test query construction. How do we adapt these tests (the test should know if we are using mysql8 or we should always quote table names).

To move forward: would it be an idea to split this issue in multiple smaller issues:

  • The set mode problem.
  • Table name quoting in MySql8 (and correcting the query construction tests).
  • Quoting field names in queries build using SelectQuery and other query building classes (where the fields are known).
  • Quoting field names in queries written using db_query() and similar functions (and as far as they fail).
  • Other ...?
ayesh’s picture

StatusFileSize
new10.86 KB

Thank you for your input @fietserwin. I also agree that this issue has become more complicated than it should have.

The set mode problem.

I propose that NO_AUTO_CREATE_USER mode should not be used at all. It provides questionable security, and it is deprecated in MySQL 5.7 anyway. It will make out patch cleaner as well, because we do not need version-compare before adding the flag.

Table name quoting in MySql8 (and correcting the query construction tests).

I think this is where the most of out bikeshedding happens. Drupal 8's approach is more safer, as it has a known list of MySQL reserved keywords. It provides some sort of overhead compared to the regex approach in #24. It's worth noting that even if a table name is not a reserved keyword, there is absolutely no hard in quoting them with back-ticks either.

Quoting field names in queries build using SelectQuery and other query building classes (where the fields are known).

We can follow the D8 approach here, with the known list of keywords (although I personally would like to see use if isset() as opposed to in_array). Patch in #24 does not provide protection for this.

Quoting field names in queries written using db_query() and similar functions (and as far as they fail).

This is where I really like #24's regex approach. We can easily replace table names as long as they use {table} pattern, which is recommended way anyway.

We cannot fix db_query() with a 100% assurance, because it's arbitrary SQL. This is the case for Drupal 8 as well.

What I have is a hybrid of #14 and #24, that it tries to escape known database fields and tables, but also runs a a regex to catch the ones that did not make it (i.e came via db_query()). I will attach the Sparta comment below, but it is not to be taken seriously. It however works with pretty much every scenario I throw at it, even with Drush.

tr’s picture

Status: Needs work » Needs review
mcdruid’s picture

I've not had a chance to dig into the details of this one yet I'm afraid, but just wanted to mention that I think we'll need a very good reason to approach this differently to what's been committed to D8.

However it looks like #2986452: Database reserved keywords need to be quoted as per the ANSI standard has only gone into D9 and won't be backported to D8.9

So thanks for all the thorough comments and contributions so far... I think we'll need to approach this with the backport policy in mind and therefore thinking about how we can use the code (/ approach) that's already been committed to newer Drupal versions, and where possible avoid coming up with anything novel/different for D7.

Apologies if that's missing the point of recent comments; as mentioned I've not got into the details yet.

mustanggb’s picture

I think we'll need a very good reason to approach this differently to what's been committed to D8

There is no good reason, other than "too complicated".

The first pass backport of D8 is in #14, and I've not run into any problems with it, other than Drush which is a simple fix on their side.

joseph.olstad’s picture

waqarit’s picture

#67 has worked for me for latest drupal 7.69 with Mysql 8.0

arborrow’s picture

With the release of Ubuntu 20.04 LTS which ships with MySQL 8, it would seem that the urgency to get a fix into core would perhaps be more pressing. I have a production server running D7 and CiviCRM that I'd like to get up to Ubuntu 20.04; however, I don't want to have to back install MySQL to make things work. Is there anything I can do to help move this issue forward? Peace - Anthony

mcdruid’s picture

To be as transparent as possible about prioritisation of issues for the 7.x branch, compatibility with newer PHP versions is a higher priority at the moment than this, because it's more likely that D7 sites find themselves forced to update to new versions of PHP 7 than to upgrade to MySQL 8.

This is reflected in the Drupal 7 roadmap and release schedule at:

https://www.drupal.org/core/drupal7-roadmap-release-schedule#release-sch...

...where PHP and MySQL EOL dates are included for context.

mustanggb’s picture

Latest Drush 8.3.3 release should resolve things over there, so D8 backport should be good to go.

joseph.olstad’s picture

If for some reason there's still an issue with the test runner using the newly tagged drush 8.3.3, please follow up here: #3128406: Drupal 7 MySQL 8 test runner needs a new version of drush 8.x

tr’s picture

... it's more likely that D7 sites find themselves forced to update to new versions of PHP 7 than to upgrade to MySQL 8.

@mcruid: As I mentioned in #56,

... the effort to make D7 run with PHP 7.4 won't be of much use if MySql 8 can't be used at the same time; most systems won't have a default configuration that includes a new version of PHP along with an old version of MySql - they will either have both new or both old.

Specifically in the context of Ubuntu, where people who have been using the 18.04 LTS for the past two years will soon be upgrading to the new 20.04 LTS (the upgrade path for the LTS release means this will happen in July when 20.04.01 is released). Ubuntu switched to MySql 8 last year - EVERYONE who upgrades to 20.04 LTS will be using PHP 7.4 and MySql 8.

I don't know about other distributions and their timelines, but Ubuntu is a big one ...

fietserwin’s picture

+++ b/includes/database/mysql/database.inc
@@ -86,21 +357,88 @@ public function __construct(array $connection_options = array()) {
+  public function prefixTables($sql) {
+    // Escape reserved mysql keywords used as table names.
+    $sql = array_reduce($this->reservedKeyWords, function ($original_sql, $keyword) {
+      return str_replace('{' . $keyword . '}', '`{' . $keyword . '}`', $original_sql);
+    }, $sql);
+    return parent::prefixTables($sql);
+  }
+

The problem with this piece of code is that it still can go wrong: If I have a table 'fill' and a prefix 'zero', this query:
select * from {fill}
will end up as:
select * from zerofill

(I understand that this is thus also a minor bug in D8/9.)

Furthermore, the array_reduce is over about 270 entries so with 100 queries on a page (not uncommon) we are talking 27,000 str_replace calls and 100,000+ string concatenations. In the light of 100 DB queries probably nothing but still.

Oh and that list is probably a running target that may change again in the future.

So, I would definitely go with just escaping all tables: easier, faster, more robust.

ronlee’s picture

@fietserwin Nice forwarding looking suggestion. I believe this is a valid concern we need to take seriously. Speed and future maintenance cannot be hindered. Let's do this right. :)

andrés chandía’s picture

#67 has worked for me upgrade to latest drupal 7.70 with Mysql 8.0.20 and PHP 7.4.3

joseph.olstad’s picture

I agree with 78 and 79 , so we need a D8 issue for this and this means it will require waiting for the D8/D9 fix to go in first according to policy. So please lets create the D8/D8 issue.

mustanggb’s picture

I believe the D8/D9 issue is the one I mentioned above, namely #2986452: Database reserved keywords need to be quoted as per the ANSI standard.

joseph.olstad’s picture

thanks @MustangGB, ok perfect so we're all set for doing the backport of that strategy.

ronino’s picture

StatusFileSize
new11.38 KB
new1.14 KB

#67 works for me, apart from queries that specify the database along with the table, like {db.table}. As I wrote in #50, those must be quoted to `db`.`table` instead of `db.table`. This is required e.g. by migrate when a different source database is queried and probably always if a database connection other than the default one is used. Without it, the query fails.

I have merged the changes to DatabaseConnection_mysql::query from my patch #50 into #67 to fix this. There might be a more efficient way than doing this with preg_replace_callback() though.

fietserwin’s picture

This seems like a use case that is not supported as with current behavior this will become prefixdb.table instead of db.prefixtable???

ayesh’s picture

Please note that the patch in #67 is not idea because it tries to fix the problem with both approaches. I'm personally in favor of escaping all keywords in the queries when possible, which in theory should work faster due to lack of vigorous in_array calls.

It works for now, but ideally I think we should discuss on one correct approach and stick with it.

andrés chandía’s picture

I had to apply again #67 patch to upgrade to latest drupal 7.71 with Mysql 8.0.20 and PHP 7.4.3

buddym’s picture

I had to install patch #67 after upgrading to Ubuntu 20.04 LTS with MySQL 8.0.20 and getting this message on Drupal 7.70:

PDOException: SQLSTATE[42000]: Syntax error or access violation: 1231 Variable 'sql_mode' can't be set to the value of 'NO_AUTO_CREATE_USER' in lock_may_be_available() (line 167 of /var/www/html/drupal/includes/lock.inc).

Thanks Ayesh, I was able to connect and all seems well even though there was one error applying the patch... Hunk #1 FAILED at 343.

$ more schema.inc.rej

--- schema.inc
+++ schema.inc
@@ -343,7 +343,7 @@ public function findTables($table_expression) {
     // couldn't use db_select() here because it would prefix
     // information_schema.tables and the query would fail.
     // Don't use {} around information_schema.tables table.
-    return $this->connection->query("SELECT table_name FROM information_schema.
tables WHERE " . (string) $condition, $condition->arguments())->fetchAllKeyed(0,
 0);
+    return $this->connection->query("SELECT table_name as table_name FROM infor
mation_schema.tables WHERE " . (string) $condition, $condition->arguments())->fe
tchAllKeyed(0, 0);
   }

   /**

Any idea how this error will affect my D7 site?

As Andrés said above, in order to upgrade from 7.70 to 7.71 running on PHP 7.4.3 and MySQL 8.0.20 this patch needs to be applied before running update.php. Otherwise, you will get this error message...

PDOException: SQLSTATE[42000]: Syntax error or access violation: 1231 Variable 'sql_mode' can't be set to the value of 'NO_AUTO_CREATE_USER' in drupal_get_installed_schema_version() (line 155 of /var/www/html/drupal/includes/install.inc).

ronlee’s picture

I'm unable to update drupal to 7.71 due to MySQL 8 issues.

Which patch is recommended? Patch #67 doesn't seem to exist on this page anymore.

ronino’s picture

@ronlee wrote:

Which patch is recommended?

Please try the latest.

Patch #67 doesn't seem to exist on this page anymore.

Somehow patches got out of order. I've rearranged them.

gisle’s picture

Issue tags: -Drupal 7.70 target +Drupal 7.72 target

Patch in #84 applies cleanly and lets me install 7.x-dev on Ubuntu 20.04 LTS (MySQL ver 8.0.20) without a hitch.

Proceeding to test with some of the extensions I use on major production sites.

Changing tags, 7.70 is history. Let us try to get this one into Drupal 7.72.

saxmeister’s picture

Any release date for 7.72?

I'm stuck in a weird situation where I had to migrate a Drupal 7 site to PHP 7.2/MySQL 8 before I could get the migration to Drupal 8 out of the way. Now I have a Drupal 7 database from MySQL 8 that is incompatible with the Migrate Upgrade module (and several others) and I cannot create a migration properly due to the error that the module thinks that the D7 database is actually a D8 database.

I tried the patches above and none of them seemed to match or work. I've taken to creating my own patches at this point in an attempt to fix the issue. But, before I get through this does anyone even know if it will work - upgrading/migrating a D7 database to a D8 site on MySQL 8.0.20?

If I create a patch that works with the latest release I will contribute the product of the experiment.

Thanks!

gisle’s picture

Again: Patch in #84 applies cleanly to the HEAD of 7.x-dev and works fine. I've now tested this combo on 14 different Drubal 7 sites and no glitches so far.

tr’s picture

@saxmeister: You're facing two distinct problems:
1) D7 doesn't work with MySql 8.
2) Migrate doesn't work in D8 with MySql 8. See #3093565: Migrate from Drupal 7 fails with mysql 8.0.3 or later: syntax error on select s.* from system

The patch here can probably fix 1) but doesn't help with 2).

pyqlo’s picture

In my case, Patch #84 applied cleany to an existing site with Drupal 7.67 - using NO table prefixes
But applies not to an existing site (Drupal 7.70) - using table prefixes.

Error: PDOException: SQLSTATE[42S02]: Base table or view not found: 1146 Table 'juz.semaphore' doesn't exist: SELECT expire, value FROM `semaphore` WHERE name = :name; Array ( [:name] => variable_init ) in lock_may_be_available() (line 167 .... includes\lock.inc).

The table prefix ist false, in settings.php it is set as: 'prefix' => 'juz_'

Any ideas on this are very welcome. Thank! :-)

gisle’s picture

pyQlo, IMHO, It makes no sense to apply a patch to any other versions than the version of the dev-branch the patch was rolled against. What you try to do here is extremely unlikely to work.

This is about the dev-brach of Drupal 7. Don't try this on a production site running a tagged release.

pyqlo’s picture

Yes, I totally agree. Unfortunately a very popular german hoster updates to MySQL 8 next week (announcement was 3 days ago...) - so I have to react.... :-(
Of course I didn't try it in production environment, but in my local dev site.

Updated to Drupal 7.71 and got the same error using table prefixes.

gisle’s picture

pyQlo,
this works for me:
1. Use git to clone the project.
2. Check out HEAD of the 7.x-dev branch.
3. Apply the patch in #84. It should apply cleanly.
4. Install the patched version on the site running MySQL 8.

pyqlo’s picture

gisle, thanks - I'll try that!

mmjvb’s picture

pyQlo, IMHO, It makes no sense to apply a patch to any other versions than the version of the dev-branch the patch was rolled against. What you try to do here is extremely unlikely to work.

Normally that is the case for the active branch. For the D7 branch the files involved haven't been touched in 3 years. The patch applies to any release since then. That would be 7.65+ on the save side.

So, it is guaranteed to work as there have been no changes for so long.

Regarding table prefixes, are they pointing to a different database?
When the prefix uses a '.' it points to a different database. When '_' it refers tot the same database, just a different table name. Obviously, ending the prefix in a '.' is not going to solve the reserved words issue for system/groups as it only points to a different database. Having a prefix without ending in '.' should solve that, unless the new table name ends up being a reserved word.

pyqlo’s picture

Thanks mmjvb.

I can also report back, that Patch #84 applies cleanly for dev and installation (even with table prefixes) worked.
For testing purposes I changed an existing site (7.71 - the one I mentioned above with prefix "juz") to use no table prefixes and here too #84 worked and the site runs without errors.

I understood, that this thread focusses on dev-branch, but it would be awesome, if the patch could be used for existing sites that are using table prefixes.

pyqlo’s picture

mmjvb - there ist only one datebase and the prefix was 'prefix' => 'juz_'.
No '.' is used even when the error looks like:

PDOException: SQLSTATE[42S02]: Base table or view not found: 1146 Table 'juz.semaphore' doesn't exist: SELECT expire, value FROM `semaphore` WHERE name = :name; Array ( [:name] => variable_init ) in lock_may_be_available() (line 167 .... includes\lock.inc).

mmjvb’s picture

Does your database happen to be 'juz' ?
What does line 167 of lock.inc say? Does it say {semaphore} ?
Do you have a table juz_semaphore ?

You might be better of undoing the patch, override sql-mode in settings.php, use table prefix for tables that are considered reserved words without them.

pyqlo’s picture

Database is not called juz. Yes - 167 of lock.inc says {semaphore}. Yes, I have a table "juz_semaphore". Checked these points before.
The prefix was inserted during installation process years ago.
Yes, perhaps eliminating the prefix is the easiest way.

saxmeister’s picture

Thanks for the quick responses. I assumed that was the current state of things but wanted to make sure.

mmjvb’s picture

You would need the prefix for table system or use the patch.

mmjvb’s picture

// Quote the table name.
+      $quoted .= '`' . $matches[3] . '`';

Should have {} around it.
Like:
$quoted .= '`{' . $matches[3] . '}`';

mmjvb’s picture

Looks like things are implemented in the wrong order. No need for checking for db.table format and ignoring prefix. Don't know about D7, but expect the prefix to introduce the db.table format. Not expecting existing code having {db.table}.

So, it should be the other way around. Change {table} to `{table}`, replace prefix, fix db.table format.

ronino’s picture

StatusFileSize
new1.09 KB
new11.09 KB

mmjvb wrote:

Should have {} around it.
Like:

$quoted .= '`{' . $matches[3] . '}`';

Good catch. It's not DatabaseConnection_mysql::query()'s job to remove the curly brackets from the query (DatabaseConnection::prefixTables() does that). After changing that, I also had to remove DatabaseConnection_mysql::prefixTables() which escaped table names that use reserved keywords (otherwise you'd get errors about "incorrect table names") - they are already escaped/quoted in query().

@pyQlo: This should now work with prefixes.

pyqlo’s picture

I'm in a hurry, but I did a quick test and #109 worked in another site using table prefixes. I'll test in detail later.
Great - thanks!!

Status: Needs review » Needs work

The last submitted patch, 109: mysql-8-2978575-109.patch, failed testing. View results

ronino’s picture

Fixed the regular expression in DatabaseConnection_mysql::query() so that tests should pass now.

Successfully tested with and without table prefixes.

ronino’s picture

Status: Needs work » Needs review
tr’s picture

Without a patch, my D7/MySql 8 site won't even run ...

I have applied #112 and after about 20 minutes of clicking around on my site I haven't encountered any errors.

(This is already far better than the previous patches, where I quickly encountered fatal errors ... see #56)

So far so good.

buddym’s picture

After Drupal core 7.72 upgrade I reapplied patch 2978575-67. All is well again it seems, but there was one error when applying the patch... Hunk #1 FAILED at 343. I am still not sure what this affects. I am not seeing any issues on the site.

$ more schema.inc.rej

--- schema.inc
+++ schema.inc
@@ -343,7 +343,7 @@ public function findTables($table_expression) {
     // couldn't use db_select() here because it would prefix
     // information_schema.tables and the query would fail.
     // Don't use {} around information_schema.tables table.
-    return $this->connection->query("SELECT table_name FROM information_schema.
tables WHERE " . (string) $condition, $condition->arguments())->fetchAllKeyed(0,
 0);
+    return $this->connection->query("SELECT table_name as table_name FROM infor
mation_schema.tables WHERE " . (string) $condition, $condition->arguments())->fe
tchAllKeyed(0, 0);
   }

   /**

Just for kicks, I tried the latest patch and same failure... Hunk #1 FAILED at 343. All seems well on the site, despite this error.

$ more schema.inc.rej

--- schema.inc
+++ schema.inc
@@ -343,7 +343,7 @@ abstract class DatabaseSchema implements QueryPlaceholderInt
erface {
     // couldn't use db_select() here because it would prefix
     // information_schema.tables and the query would fail.
     // Don't use {} around information_schema.tables table.
-    return $this->connection->query("SELECT table_name FROM information_schema.
tables WHERE " . (string) $condition, $condition->arguments())->fetchAllKeyed(0,
 0);
+    return $this->connection->query("SELECT table_name as table_name FROM infor
mation_schema.tables WHERE " . (string) $condition, $condition->arguments())->fe
tchAllKeyed(0, 0);
   }

   /**

Thanks for the patch!

tr’s picture

@buddym: #67 has known problems, including problems that you yourself pointed out in #88.

Don't use #67!

Use #112 - that is the latest patch under consideration, and it fixes the known problems with #67. Let's try to move forward with this issue - does #112 work for you?

buddym’s picture

The hunk failed error was due to my inexperience applying patches. I was moving the patch into the mysql directory and applying with a simple patch command like 'patch < patch.file'.

The solution was to move the patch.file to the root drupal directory and use this patch command 'patch -p1 < patch.file'.

The latest patch #112 applied without any failures. Thanks!

Mixologic’s picture

StatusFileSize
new12.15 KB

Here is the same patch as #112, with a demonstration of how to patch a custom drupalci.yml for d7 core tests. (includes core/drupalci.yml). This is only necessary until we are able to upgrade the drush version in the containers.

joseph.olstad’s picture

Just FYI Drush 8.3.3 contains the fixes needed so that Drush will run with MySql 8 and php 7.4.

However the latest release is 8.3.5

https://github.com/drush-ops/drush/releases/tag/8.3.5

tr’s picture

@Mixologic: Thanks! That's going to be really useful - now we can run tests and see if the patch works with all edge cases. That wasn't possible before. I don't think any of the patches over the past 4 months would have tested green (it was easy enough to manually find problems with them) so we've wasted a lot of time considering patches which weren't good enough.

Now with #112 we have a patch that fails only 1 out of 2053 cases - that gives me a lot of confidence that if we can fix that one tiny problem then this patch might be ready to commit.

What needs to be done now is to find and fix this 1 failure. ALL PATCHES posted here should now include the same drupalci.yml that is included with #118, and ALL PATCHES should run a test against MySql 8 - that's the whole point of this issue, to make sure this works with MySql 8!

gisle’s picture

Issue tags: -Drupal 7.72 target +Drupal 7.73 target

Changing tags. 7.72 is out.

tr’s picture

Category: Feature request » Bug report

This is a bug report at this point, right?

D7 is unusable on MySql 8 and upgrading to MySql 8 breaks sites.

MySql 8 has been out for 2 years and is currently the default in some major OS distributions.

If D7 errors on PHP 7.4, that's always been a bug, even before PHP 7.4 was officially supported. The DB version should get the same consideration.

Major or Critical? I'm leaning towards critical ...

mmjvb’s picture

'`system` system' => '`system` `system`',
Should probably be
'`{system}` system' => '`{system}` `system`',
to fix that one problem left.

ronino’s picture

StatusFileSize
new12.13 KB

mmjvb wrote:

    '`system` system' => '`system` `system`',

Should probably be

    '`{system}` system' => '`{system}` `system`',

to fix that one problem left.

That indeed does the trick! I added that to #118.

But shouldn't the line above

      ' system.' => ' `system`.',

then also become

      ' {system}.' => ' `{system}`.',

? Works both ways...

tr’s picture

@Ronino: This issue is large and complex - an interdiff would be greatly appreciated so we could see exactly what you changed without having to do a line-by-line comparison to your previous patch.

ronino’s picture

StatusFileSize
new549 bytes

@TR: That one time I didn't create an interdiff ;-). I changed exactly what mmjvb described, Here we go...

mmjvb’s picture

Suspect ' system.' to refer to the `system` in the line needing the change. So, it is the table alias used in a column reference. It shouldn't be prefixed.

Indeed, an interdiff would be welcome. Regardless whether you just add a file or change a line. It helps to see what was exactly changed. While you are at it, could you provide the missing one of #118 as well?

tr’s picture

StatusFileSize
new1001 bytes

@Ronino: Thanks for the interdiff - that makes it a lot easier to follow what's going on here.

@mmjvb: The difference between #112 and #118 is just the drupalci.yml file. Here is the interdiff.

tr’s picture

My only remaining question about #124 is why do we still need to have special-case code for Drush? If it's only a problem with Drush, this sounds like something that should be fixed in Drush.

+    // Make Drush work.
+    $query = strtr($query, array(
+      ' system.' => ' `system`.',
+      '`{system}` system' => '`{system}` `system`',
+    )); 
mmjvb’s picture

Trusted that to be the case, but the interdiff helps, thanks

Looked into the pattern for 'db.table' and noticed it is not using \w, any reason for that? In addition I would have expected the same expression for db and table, but that doesn't work, any idea?

Finally, nobody addressed my concern of introducing 'db.table' by prefix. Any users using the prefix to refer to tables in different database that can confirm this works for them?

tr’s picture

Finally, nobody addressed my concern of introducing 'db.table' by prefix. Any users using the prefix to refer to tables in different database that can confirm this works for them?

Is this not covered by existing test cases? Do we need a new test case to ensure this works?

andrés chandía’s picture

Upgrading to D7.72, MySql 8.0.20-0ubuntu0.20.04.1, PHP 7.4.3, Apache/2.4.41 with patch #124 worked like a charm!! Thanks Ronino

trevorbradley’s picture

Similarly, brand new Ubuntu 20.04LTS cloud server with D7.72, Mysql 8, PHP 7.4 - #124 also working for me.

ghost of drupal past’s picture

Issue summary: View changes
StatusFileSize
new21.37 KB

I fed $query = '{foobar.baz}'; into the regex and got the expected `foobar.`.`{baz}`.

If I had any say left in these matters -- I am not sure I do -- I would strongly advocate for removing this regex and move the escaping into the prefixing code. Yes, it won't support database but that should be a followup -- are you sure you want to preg_replace_replace every fixed string query on every D7 page load out there just for the sake of the miniscule amount of queries hardwiring a database?? Perhaps it should be gated by a variable. Anyways, I would say it should be a followup so it doesn't hold up this any longer, it's been long enough.

mmjvb’s picture

To fix either chop the match db. to get rid of the trailing .
or revert pattern to make a reference to db in addition to db. as it was before.

Haven't found a test case that uses per table prefix, but it appears that simpletest uses database prefix pointing to separate db. Don't understand why it works, but when tests are not failing. So, no need for additional test as every test is already testing it.

That also sounds like dealing with the db.table is needed for testing to work. So, not optional for later.

ronino’s picture

Charlie ChX Negyesi wrote:

I fed $query = '{foobar.baz}'; into the regex and got the expected `foobar.`.`{baz}`.

Thanks! Here's a fix.

It seems like no test covers this...

ronino’s picture

Added a test to DatabaseQueryTestCase to make sure that "{db.table}" is correctly prefixed.

Mixologic’s picture

The php containers now include drush 8.3.5, so future patches can remove the /core/drupalci.yml file.

Scoville78’s picture

Thanks to everyone working on this topic! I'd really appreciate a quick 7.73 core update including this feature!

I don't understand why this wasn't tackled earlier, and still is not being considered as critical. My hosting provider announced a OS update on July 1st (from Debian 8 LTS to Debian 10) which also contains the update to MySQL 8. I still have some older projects running there for which an update to Drupal 8 is out of question. It's hard to explain to customers that there's a chance that there won't be a stable core update by then, and we'll probably have to apply some patch, which is still being developed, evaluated and subject to further change by the community. Things like this are exactly why I shifted more and more projects to WordPress in the last years.

pepeve’s picture

hello, we have a drupal 7 page for our sports club. the friend who made the website back then is unfortunately no longer with us. Unfortunately I don't understand how I can get the Drupal website up and running again. Is there a way to download the patched files? I also do not find a folder called Core in the Drupal 7 installation.

tfranz’s picture

StatusFileSize
new687 bytes
new105.74 KB

@pepeve: I uploaded the patched files for you (Drupal 7.72)

Unzip "core.zip" and put the content as folder /core/ in the root-folder (next to the folders "includes", "misc", "modules", ...).
Unzip "database-folder-patched-124.zip" and replace the folder /includes/database/

Hope it helps ...

mcdruid’s picture

StatusFileSize
new1.85 KB
new12.08 KB

Removed drupalci.yml per #138, thanks @Mixologic

Removed the workaround for drush as it's no longer necessary now that https://github.com/drush-ops/drush/pull/4313 is in recent drush releases (h/t @MustangGB). I suppose this means anyone using earlier versions of drush will have problems, but I don't think we should keep the workaround in core for that reason.

Only other change I've made is that $reservedKeyWords needs to use the old school array() syntax (to cater for all the sites that run PHP 5.3 along with MySQL 8 ;)

Some other thoughts:

1) As mentioned a few times in this issue, D9 no longer uses the list of reserved words but instead quotes everything. Just to see what would happen, I commented out the checking of the reserved words so that identifiers are always quoted, e.g.:

    //if (in_array(strtolower($identifier), $this->reservedKeyWords, TRUE)) {
      // Quote the string for MySQL reserved keywords.
      $identifier = '"' . $identifier . '"';
    //}

...and nothing bad happened AFAICS. I didn't test this extensively though. What would be wrong with this approach?

2) We're completely dropping NO_AUTO_CREATE_USER in the recent patches, as proposed in #67 by @Ayesh, but that's not what happens in D8/9. The simplicity of omitting it is appealing though.

3) Thanks for adding the test in #137 @Ronino. I believe there are more tests in the D8 issue that we should backport too.

4) The concerns expressed in #134 are concerning me.

5) Less of a concern, but I mentioned in #22 that the anonymous function as a callback here:

+    $insert_fields = array_map(function ($field) {
+      return $this->connection->escapeField($field);
+    }, $insert_fields);

...would be the first usage of this pattern in D7 core. How easy would that be to refactor?

6) Nit: I'd like to see the keyword AS capitalised in the schema queries, e.g.:

return $this->connection->query("SELECT table_name as table_name FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchAllKeyed(0, 0);

...however, it looks like it's been committed lowercase to D8/9. I'll file a follow-up for that.

ayesh’s picture

5) Less of a concern, but I mentioned in #22 that the anonymous function as a callback here:

+ $insert_fields = array_map(function ($field) {
+ return $this->connection->escapeField($field);
+ }, $insert_fields);

...would be the first usage of this pattern in D7 core. How easy would that be to refactor?

It should be trivially possible to refactor this into a standard class method, and refer it in the array_map() call.

For example:

$insert_fields = array_map(array($this, 'escapeCallback'));

protected function escapeCallback($field) {
  return $this->connection->escapeField($field);
}

If I recall correctly, we made some changes to D7 core that is not compatible with PHP 5.2, so I'd vote to keep the anonymous function as it makes the code much cleaner.

mcdruid’s picture

StatusFileSize
new664 bytes
new12.08 KB

Thanks @Ayesh. I don't think the anonymous function's a problem, but noteworthy if it's the first in D7 core; will see if @Fabianx is happy with it.

There's some syntax in the new test that PHP 5.3 doesn't like; hopefully this tweak will fix that.

The MySQL 8 test is also failing - that looks like the error from drush again doesn't it?

ayesh’s picture

Unrelated: DrupalCI probably should `drush --version` right after the existing `php -v` so we know exactly what's going on.

mcdruid’s picture

Yes, that'd be helpful.

PHP Fatal error:  Using $this when not in object context in /var/www/html/includes/database/mysql/query.inc on line 52

It looks like PHP 5.3 might not like the reference to $this inside the anonymous function? Perhaps we'll need to refactor that after all.

However, it looks like recent versions of drush8 don't work with PHP 5.3 (because of [] array syntax AFAICS) so I'm struggling to try and reproduce the error the testbot seems to be hitting doing the initially install, e.g.:

drupal-7.x# ./drush-8.3.5.phar si
PHP Parse error:  syntax error, unexpected '[' in phar:///path/to/drush-8.3.5.phar/includes/startup.inc on line 63

So yes, I'm not sure what version of drush the PHP 5.3 testbot is using, but it seems unlikely it'll work with the MySQL 8 patch without the drush workaround included.

tr’s picture

In #138 Mixologic said:

The php containers now include drush 8.3.5

joseph.olstad’s picture

is it possible to have a different version of drush run for php 5.3 ? IMHO, we can still support php 5.3, but not support php 5.3 with mysql 8
For my clients still using php 5.3, they are very unlikely to upgrade to mysql 8 without upgrading php.

I think it's reasonable to support mysql 8 for all versions of php except php 5.3. The key would be to change the drupal ci to use the older version of drush for ONLY php 5.3 and use drush 8.3.5 for all other versions of php.

automated testing for php 5.3 should be limited to mysql 5.5, 5.6, 5.7
automated testing for other php versions should include testing for mysql 8

mcdruid’s picture

StatusFileSize
new576 bytes
new12.36 KB

AFAICS the current patch breaks PHP 5.3 regardless of the MySQL version, so we'll need to dig into that some more.

Here's a patch which restores the drush workaround in \DatabaseConnection_mysql::query() conditionally.

Let's see if doing so gets the MySQL 8 test passing again.

mcdruid’s picture

I'll have a look at what's going on with that one failing test.

Another issue in the meantime though; with all of the recent patches (e.g. back to #137), it looks like drush site-install will work if there's no existing database (which I think is what happens when the testbot runs). However, if there's already a db in place, I'm getting this error:

$ drush si
You are about to DROP all tables in your 'drupal7x' database. Do you want to continue? (y/n): y
Failed to create database: ERROR 1064 (42000) at line 1: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near &#039;system, taxonomy_index, taxonomy_term_data,                   [error]
taxonomy_term_hierarchy, taxonomy_vo&#039; at line 1

Running drush sql-drop seems to work fine, and drush site-install works okay after that.

mcdruid’s picture

It turns out that the "drush workaround" wasn't only fixing things for old versions of drush.

DatabaseTemporaryQueryTestCase does this:

    // Now try to run two db_query_temporary() in the same request.
    $table_name_system = db_query_temporary('SELECT status FROM {system}', array());
    $table_name_users = db_query_temporary('SELECT uid FROM {users}', array());

    $this->assertEqual($this->countTableRows($table_name_system), $this->countTableRows("system"), 'A temporary table was created successfully in this request.');

https://git.drupalcode.org/project/drupal/-/blob/7.72/modules/simpletest...

I believe that's the code that's hitting an error if we don't have the workaround (unconditionally) for the system table in \DatabaseConnection_mysql::query().

Specifically, it's this substitution which allows that test to run:

'`{system}` system' => '`{system}` `system`',

So we can either put that workaround back in (and not label it as being drush-specific), or figure out why this is necessary and see if it can be addressed another way.

It concerns me a little to have such a specific workaround; I'd imagine the problem could happen for other queries if there's an alias that is a reserved word which is not being quoted.

mcdruid’s picture

The test is failing when this happens in the assertion in \DatabaseTemporaryQueryTestCase::testTemporaryQuery

$this->countTableRows("system")

That does (comments added to show param values):

db_select($table_name)->countQuery()->execute()->fetchField(); // $table_name = 'system'

The db_select() call results in the \SelectQuery::__construct() being called with $table = 'system' and $alias = NULL (the default).

The last line of that constructor calls \SelectQuery::addJoin() which then copies $table to $alias if the latter is empty.

It would appear that alias is not escaped / quoted unless that happens via the "drush workaround" in \DatabaseConnection_mysql::query.

Without the workaround, this query is executed:

SELECT COUNT(*) AS expression
FROM 
(SELECT 1 AS expression
FROM 
`{system}` system) subquery

...and that unquoted alias causes the error in MySQL 8 as it's a reserved word.

We can reproduce the error using drush:

$ drush ev "var_dump(db_select('system')->countQuery()->execute()->fetchField());"
PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'system) subquery' at line 5 in                        [error]
/path/to/drupal-7.x/includes/database/database.inc:2227
Stack trace:
#0 /path/to/drupal-7.x/includes/database/database.inc(2227): PDOStatement->execute(Array)
#1 /path/to/drupal-7.x/includes/database/database.inc(697): DatabaseStatementBase->execute(Array, Array)
#2 /path/to/drupal-7.x/includes/database/mysql/database.inc(446): DatabaseConnection->query('SELECT COUNT(*)...', Array, Array)
#3 /path/to/drupal-7.x/includes/database/select.inc(1280): DatabaseConnection_mysql->query('SELECT COUNT(*)...', Array, Array)

I'm not entirely sure what the best solution to this would be; I don't think leaving it as a hacky workaround is very satisfactory.

One reason why is that the workaround is obviously specific to the one table name "system". The problem would be the same though if a site had another table with a name that conflicts with reserved words. For example:

mysql> CREATE TABLE `table` (`id` int);
Query OK, 0 rows affected (0.10 sec)
$ drush ev "var_dump(db_select('table')->countQuery()->execute()->fetchField());"
PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'table) subquery' at line 5 in                         [error]
/path/to/drupal-7.x/drupal-7.x/includes/database/database.inc:2227

Exactly the same problem with the table name being copied as the alias with no quoting.

The code looks very similar, FWIW, in D8's \Drupal\Core\Database\Query\Select::addJoin.

fietserwin’s picture

Status: Needs review » Needs work

To solve, I see 2 options:
- Also always quote each and any alias that gets generated.
- Generate aliases that will never result in a reserved word, e.g. by letting it start with an underscore (seems to be allowed).

mcdruid’s picture

Status: Needs work » Needs review
StatusFileSize
new956 bytes
new12.47 KB

The "always quote each and any alias that gets generated" is part of what was done in #2986452: Database reserved keywords need to be quoted as per the ANSI standard IIUC, but that required such significant changes that it only went into D9 and wasn't backported to D8.

I'm not intimately familiar with every moving part of the DB API here, so I may well be missing something, but I'm not yet certain why the query causing the test fail is apparently not having its alias quoted already by \DatabaseConnection_mysql::escapeAlias(); it is a reserved word so if that method was being called, it would be quoted.

It may be because this particular way of constructing and executing a SelectQuery doesn't end up invoking \SelectQuery::__toString and therefore escapeAlias is not called. I'm not certain about that yet though.

I don't expect the approach in this patch to be the eventual solution, but I'd like to see whether with this all tests for MySQL 8 pass.

As suggested in #134 perhaps we could look at moving this into somewhere like \DatabaseConnection::setPrefix instead.

Oh, and I did also look at changing the alias being added in \SelectQuery::addJoin() when the table name is copied, but doing so broke a lot of queries as the field names being used no longer matched the alias. There's probably a better way of doing that if we wanted to take this approach though.

mcdruid’s picture

Status: Needs review » Needs work

AFAICS this might be a problem in 8.9.x too:

mysql> SHOW VARIABLES LIKE "version";
+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| version       | 8.0.19-0ubuntu4 |
+---------------+-----------------+

mysql> CREATE TABLE `table` (`id` int);
Query OK, 0 rows affected (0.23 sec)
$ drush st --fields='Drupal version'
 Drupal version   :  8.9.2-dev

$ drush ev "var_dump(db_select('table')->countQuery()->execute()->fetchField());"
PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'table table) subquery' at line 5 in                   [error]
/path/to/drupal-8.x/core/lib/Drupal/Core/Database/Statement.php:59
Stack trace:
#0 /path/to/drupal-8.x/core/lib/Drupal/Core/Database/Statement.php(59): PDOStatement->execute(Array)
#1 /path/to/drupal-8.x/core/lib/Drupal/Core/Database/Connection.php(640): Drupal\Core\Database\Statement->execute(Array, Array)
#2 /path/to/drupal-8.x/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php(357): Drupal\Core\Database\Connection->query('SELECT COUNT(*)...', Array, Array)
#3 /path/to/drupal-8.x/core/lib/Drupal/Core/Database/Query/Select.php(510): Drupal\Core\Database\Driver\mysql\Connection->query('SELECT COUNT(*)...', Array, Array)

[...snip...]

Next Drupal\Core\Database\DatabaseExceptionWrapper: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'table table)
subquery' at line 5: SELECT COUNT(*) AS expression
FROM
(SELECT 1 AS expression
FROM
{table} table) subquery; Array
(
)

So perhaps there's a difference in test coverage? (Trying hard to avoid any topical references about that here... ;)

We'll have to decide how we're going to approach this in 7.x

There seem to be a few things we'll need to address AFAICS; I'll try to summarise them in due course.

I've been contemplating implementing a different DB driver for MySQL 8, which I think may have some advantages for D7; it would hopefully avoid any regressions (performance or functionality) for sites that remain on MySQL 5.x

mcdruid’s picture

mcdruid’s picture

Status: Needs work » Needs review
StatusFileSize
new1.23 KB
new12.72 KB

I think I've made some progress in the 8.9.x issue, and hopefully this patch will get tests passing without any hacky workarounds to fix reserved word tablenames with unquoted aliases.

mcdruid’s picture

StatusFileSize
new289 bytes

So I think it was definitely a bug in includes/database/select.inc's \SelectQuery::__toString where the alias was being passed through escapeTable rather than escapeAlias and therefore the mysql driver wasn't quoting the alias if it was a reserved word. I believe that's actually why drush was breaking; with the latest patch (#157) even older versions of drush seem to work okay. The D8 issue for that may need more work changing the type of test etc... but I think we should definitely keep the change in the overall MySQL 8 patch here. (See the interdiff in #157 for details).

I mentioned there being a few more issues I think we need to address; here are some of them:

Much as I can see the appeal in the simplicity of doing this:

     $connection_options['init_commands'] += array(
-      'sql_mode' => "SET sql_mode = 'REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER'",
+      'sql_mode' => "SET sql_mode = 'REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO'",
     );

...I think we ought to make that change conditional on the MySQL version. That's what D8 does, and it reduces the risk of unintended consequences of making this change unconditionally.

I have some concerns in general about the performance cost of the changes we're making here - e.g. running all queries through a regex to quote table names (see #134).

If there was no need to do this for MySQL versions before 8, I think we should avoid doing it for older versions if we can.

I tried to do some performance testing locally but it takes a really long time for the whole test suite to run for me; perhaps we can try a few A/B tests with the testbot infra to see if it looks like there is a noticeable performance degradation introduced by the recent patches. That would be interesting to know.

For that purpose, I'm attaching a noop patch which we can use to compare how long the test suite takes to run without the changes in e.g. #157.

Similarly, it looks like some of the changes being introduced break PHP 5.3, e.g.:

includes/database/mysql/query.inc
@@ -48,6 +48,10 @@ class InsertQuery_mysql extends InsertQuery {
     // Default fields are always placed first for consistency.
     $insert_fields = array_merge($this->defaultFields, $this->insertFields);
 
+    $insert_fields = array_map(function ($field) {
+      return $this->connection->escapeField($field);
+    }, $insert_fields);
+

...again if we don't need to do this for older versions of MySQL, I wonder if we can avoid the PHP 5.3 problem by making it conditional on the MySQL version. That may or may not work as some syntax issues will cause problems whether the code is actually executed or not.

There is #3155190: [meta] Should Drupal 7 drop support for older PHP versions? if anyone has thoughts on continued support for older PHP versions.

I did look at the option of having a completely separate driver for MySQL 8 (or perhaps MySQL > 5). It wasn't hard to get that working for core, but my initial experiments ran into problems with drush not being able to find the appropriate SQL class, which would be a deal breaker. It may be that it's possible to work around it, but I didn't carry on much beyond establishing that using a different driver name than mysql seemed to make drush (8) break.

All that said, I think it would be good to make any of the changes we're making inside the mysql driver conditional on the MySQL version. If we can do that cheaply, I think there may be benefits in terms of avoiding (performance and functionality) regressions with older versions of both PHP and MySQL (which D8/9 are not as concerned about as D7).

I would also like to see more tests.

mcdruid’s picture

StatusFileSize
new5.77 KB
new14.01 KB

Quick attempt to (re-)implement the approach of making changes in the mysql driver conditional on whether the db is actually MySQL 8 (or later).

One potential problem is that the getter method $this->connection->isMySQL8OrLater() only exists in the mysql driver's implementation of this class. I don't think practically that should be a problem, but my IDE at least is trying to warn me about this being potentially polymorphic.

Let's see if it actually works before worrying too much about that.

Also, naming things is hard :) I'm not especially keen on $this->mysql8OrLater and am certainly open to alternative suggestions.

Status: Needs review » Needs work

The last submitted patch, 159: 2978575-159.patch, failed testing. View results

mcdruid’s picture

Status: Needs work » Needs review
StatusFileSize
new1.16 KB
new14.39 KB

Hmm, more speed lest haste. Let's try that again.

mcdruid’s picture

StatusFileSize
new573 bytes
new14.34 KB

...and again without the [] array copy-pasta.

The last submitted patch, 161: 2978575-161.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 162: 2978575-162.patch, failed testing. View results

mcdruid’s picture

Interesting, the only test which is failing is the new one introduced with these patches.

      // "{table}" as well as "{db.table}" need to work.
      db_query("SELECT * FROM {" . $database . ".test} LIMIT 1")->fetchObject();
      $this->pass('Correctly apply prefixes if the table is specified along with the database.');

Now that we've made this conditional on the MySQL version:

  public function query($query, array $args = array(), $options = array()) {
    if ($this->mysql8OrLater) {
      // Match {table} as well as {db.table} and quote both DB and table with
      // backticks.
      $query = preg_replace_callback('/\{(([^.\}]+)\.)?(.*?)\}/', function ($matches) {

...that test isn't going to work with MySQL 5.x.

Is this testing the right thing? Being able to specify both {db.table} within the curly braces doesn't actually seem like part of adding MySQL 8 support to me, unless I'm missing something?

I appreciate that if we're quoting table names for MySQL 8 then that code has to work if the db has been specified too, but does this test failing with MySQL 5.x reflect something that's actually broken? If so, I'd think that would be a separate issue.

ayesh’s picture

Status: Needs work » Needs review
StatusFileSize
new14.54 KB
new5.86 KB

Adding a new patch to queue and run tests in Drupal infra.

- Refactored the regular expression to not use backslashes where not necessary.
- Made the database name match (group 2) a lazy qualifier for performance.
- Renamed `isMySQL8OrLater` to `needsReservedKeywordEscape` to make the intend clear. This follows other class properties available.
- Added a new `escapeFields` method to connection class that accepts an array of fields, and runs `escapeField` on it. This avoids the closure usage.
- Refactored table escaping to a separate method. It's currently a protected method, it can probably make it easier to run tests in unit-test style because it's database connection-independent.
- Updated the test run `SELECT.... {table}` and `SELECT... {database.table}` style queries, so the tests are more verbose.

ayesh’s picture

Is this testing the right thing? Being able to specify both {db.table} within the curly braces doesn't actually seem like part of adding MySQL 8 support to me, unless I'm missing something?

I appreciate that if we're quoting table names for MySQL 8 then that code has to work if the db has been specified too, but does this test failing with MySQL 5.x reflect something that's actually broken? If so, I'd think that would be a separate issue

I think this brings an interesting point, as to if we should even bother with {db.table} style keywords. This is not the correct practices, and Drupal should be let to add the prefix, or the caller should take care to properly add database AND prefix. Right now, we let Drupal prefix a table when a database name is explicitly mentioned, which might not be the same database Drupal knows the prefix for.

Status: Needs review » Needs work

The last submitted patch, 166: 2978575-166.patch, failed testing. View results

ayesh’s picture

Status: Needs work » Needs review
StatusFileSize
new14.13 KB
new1.17 KB

Trying this again with {db.table} support removed.

ayesh’s picture

StatusFileSize
new13.82 KB

The last submitted patch, 169: 2978575-168.patch, failed testing. View results

ayesh’s picture

ronino’s picture

mcdruid wrote in #165:

Is this testing the right thing? Being able to specify both {db.table} within the curly braces doesn't actually seem like part of adding MySQL 8 support to me, unless I'm missing something?

The reason this must be handled for MySQL 8 is that every table name is quoted and you cannot quote {db.table} to `db.table`, but rather need to make it `db`.`table`. This special handling is not necessary for MySQL 5 as there's no quotation.

I appreciate that if we're quoting table names for MySQL 8 then that code has to work if the db has been specified too, but does this test failing with MySQL 5.x reflect something that's actually broken? If so, I'd think that would be a separate issue.

Patches up to #157 worked fine with both MySQL 5 and 8.

Ayesh wrote in #169:

Trying this again with {db.table} support removed.

The migrate module relies on that to query a migration database that is different from the Drupal database. Specifying db.table is perfectly valid SQL syntax, worked with MySQL 5 and must work with MySQL 8, too.

mmjvb’s picture

Agree with the above mentioned in #173.

Do think changing db.table is easier after applying prefix instead of before. A matter of replacing . with `.` when there is a . within ``. The benefit would be when the per table prefix is used to refer to a table in different database it will be treated correctly.

mcdruid’s picture

Status: Needs review » Needs work

@Ayesh thanks, I like a lot of your improvements, particularly needsReservedKeywordEscape :)

@Ronino, @mmjvb yup okay, I can see that if we're quoting all table names for MySQL that we don't want to end up with `db.table` (which IIUC might be why the most recent patches failed?)

I wasn't sure though why the new test failed for #162 when there should have been no quoting happening.

AFAICS in 7.x without any of the patches from this issue (and a MySQL 5 db), these all work:

  • db_query("SELECT COUNT(1) FROM {drupal7x.users};")
  • db_query("SELECT COUNT(1) FROM drupal7x.{users};")
  • db_query("SELECT COUNT(1) FROM {drupal7x}.users;")

...and produce the same SQL queryString, namely: SELECT COUNT(1) FROM drupal7x.users;

However, if we use a prefix for the db tables db_query("SELECT COUNT(1) FROM drupal7x.{users};") works okay.

...but drush ev 'var_dump(db_query("SELECT COUNT(1) FROM {drupal7x.users};"));' doesn't.

That seems to be because Drupal tries to apply the prefix to the db name e.g. "SELECT COUNT(1) FROM drupal7x.foousers;" is the working SQL but {drupal7x.users} gives us:

PDOException: SQLSTATE[42S02]: Base table or view not found: 1146 Table 'foodrupal7x.users' doesn't exist

Whereas with the quoting code in place db_query("SELECT COUNT(1) FROM {drupal7x.users};") produces:

SELECT COUNT(1) FROM `drupal7x`.`foousers`;

So that'd be why the new test failed for {db.table} without the quoting for MySQL 8; when prefixes are being used (which they always are on the testbot) {db.table} doesn't work in D7 as it stands. I think the quoting is almost fixing that by accident (apologies if not and others were aware of exactly how this was working already!)

I think that's more what I meant by "Is this testing the right thing?". If e.g. migrate uses db.table syntax now (which makes sense) I'm guessing it doesn't wrap both in curly braces.

ayesh’s picture

Thanks for the explanations @mcdruid, @Ronino, @mmjvb.

It's probably better if we establish some test strings, so the query escape method can be tested easily.

1. SELECT * FROM system -> SELECT * FROM system (we cannot do this with a regex query easily)
2. SELECT * FROM {system} -> SELECT * FROM `{system}` (parent class should add the prefix)
3. SELECT * FROM {db.system} -> SELECT * FROM `db`.`{system}` (Is this right? This seems to be the correct syntax for parent class to add prefix)
4. SELECT * FROM db.system -> SELECT * FROM db.system : Same as #1

Current patches until #166 appears to do this string transformation correctly: https://3v4l.org/YtN8T

ayesh’s picture

StatusFileSize
new13.93 KB

Trying this again, with flexible {db.table} and {table}

ayesh’s picture

StatusFileSize
new14.13 KB
new14.13 KB

178th time's the charm.
Rebased the patch from 157, where the last time we had MySQL 8 builds passing.

mcdruid’s picture

I think recent patches are missing this fix (from #157):

includes/database/select.inc
		@@ -1555,7 +1555,7 @@ class SelectQuery extends Query implements SelectQueryInterface {
		 
		       // Don't use the AS keyword for table aliases, as some
		       // databases don't support it (e.g., Oracle).
		-      $query .=  $table_string . ' ' . $this->connection->escapeTable($table['alias']);
		+      $query .=  $table_string . ' ' . $this->connection->escapeAlias($table['alias']);

...which is why we'd be seeing MySQL 8 fails like:

SELECT COUNT(*) AS expression
FROM 
(SELECT 1 AS expression
FROM 
`{system}` system) subquery;
mcdruid’s picture

Status: Needs work » Needs review
StatusFileSize
new6.28 KB
new15.54 KB

@Ronino said in #173

The migrate module relies on that to query a migration database that is different from the Drupal database. Specifying db.table is perfectly valid SQL syntax, worked with MySQL 5 and must work with MySQL 8, too.

Yes db.table has to work, but {db.table} doesn't work in D7 if prefixes are in use, hence the reason that the new test for that is failing when we don't do quoting for MySQL 5 (i.e. don't change the existing behaviour for MySQL 5).

Does migrate - or anything else - actually use {db.table} where the db name is inside the curly braces? If it does then it seems to me that would not work with table prefixes. If not, I think we could simplify the quoting, possibly doing away with the regex entirely and going back to adding quotes as part of the prefixing code (e.g. #59).

AFAICS the only reason {db.table} is working when the regex-based quoting code is added is that we're removing the curly braces and replacing them only around the table name.

I've got a patch working which goes back to the approach in #59 whereby \DatabaseConnection_mysql::setPrefix is used to inject backticks into the prefix replacement patterns.

One slight annoyance is that this method is called before we've had a chance to set the needsReservedKeywordEscape property in the constructor, so I've moved the order of that around a little. An alternative to this would be to always put the backticks in without checking the MySQL version; that would mean we wouldn't have to change the order of the function calls in \DatabaseConnection::__construct which might be preferable.

This means we don't need to implement the query method in the mysql driver to run all queries through a regex.

So yes in this case {db.table} will not work, but it doesn't work now (at least it's not compatible with table prefixes) with MySQL 5.

I've updated the new tests to reflect what should and should not be working in terms of the db.table syntax.

@Ronino you've said a few times that {db.table} needs to work; can you provide some examples of where that pattern is used? If migrate does that, I think we could argue it's a bug in migrate as I don't think that'll work today if table prefixes are in place.

Before I forget, there's a comment in \DatabaseConnection_mysql::quoteIdentifier which I think comes from the D8 implementation, and I'm not sure it's actually relevant / accurate for D7? Let's check that. That method - incidentally - takes care of the db.table syntax if it's passed an identifier that includes a dot; perhaps that's sufficient?

I've also got a patch for drush 8 without which some commands don't work under MySQL 8; the patch wraps backticks around each table name when drush uses SHOW TABLES; in the listTables method of its mysql class. Otherwise you can't e.g drop all tables which means site-install only works if the db is empty to begin with. I'll submit that patch to drush and link the PR here. With any luck this doesn't cause a problem on the testbot as we start with an empty db.

Also, more tests would be good :)

mcdruid’s picture

https://github.com/drush-ops/drush/pull/4456 "Wrap mysql table names in backticks to avoid errors in MySQL 8"

mcdruid’s picture

StatusFileSize
new879 bytes
new15.79 KB

I think this fixes the first failing test; we were ending up with table names like 'simpletest858168`foo` if the prefix is already wrapped in backticks, so this removes a backtick at the end of the prefix replacement pattern.

I am not certain how we end up with a prefix already wrapped in backticks though, and that would be good to know.

mcdruid’s picture

StatusFileSize
new1.14 KB
new16.7 KB

I think this should hopefully fix the next test fail; some schema queries such as those called by db_find_tables() don't want the backticks to be wrapped around table names, so this removes any that are there.

Restoring the getter for needsReservedKeywordEscape makes this hack a little cleaner, but technically we're back to the polymorphic problem.

Again, if we could avoid putting the backticks there in the first place that'd be better.

mcdruid’s picture

Status: Needs review » Needs work

Looks like all the remaining failures are EntityFieldQuery related.

I'm not sure what's going on there yet, but FWIW here's an example of the query that was generated locally by the "Test sort entity entity_id property in descending order, with a field condition" test:

SELECT DISTINCT field_data_yilecucz_field_name0.entity_type AS entity_type, field_data_yilecucz_field_name0.entity_id AS entity_id, field_data_yilecucz_field_name0.revision_id AS revision_id, field_data_yilecucz_field_name0.bundle AS bundle
FROM 
`simpletest175103field_data_yilecucz_field_name` field_data_yilecucz_field_name0
INNER JOIN `simpletest175103test_entity_bundle_key` test_entity_bundle_key ON test_entity_bundle_key.ftid = field_data_yilecucz_field_name0.entity_id
WHERE  (field_data_yilecucz_field_name0.yilecucz_field_name_value > :db_condition_placeholder_0) AND (field_data_yilecucz_field_name0.deleted = :db_condition_placeholder_1) AND (field_data_yilecucz_field_name0.entity_type = :db_condition_placeholder_2) 
ORDER BY test_entity_bundle_key.ftid DESC

The error message is:

fail: [Other] Line 333 of modules/simpletest/tests/entity_query.test:
Exception thrown: SQLSTATE[HY000]: General error: 3065 Expression #1 of ORDER BY clause is not in SELECT list, references column 'jenkins_drupal_d7_78663.test_entity_bundle_key.ftid' which is not in SELECT list; this is incompatible with DISTINCT

So yes, test_entity_bundle_key.ftid is not in the SELECT list. I am not sure why this is happening yet though. The prefixing and quoting actually looks like it's working okay, but perhaps something's interfering with the logic when the EntityQuery is being assembled?

mcdruid’s picture

Status: Needs work » Needs review
StatusFileSize
new1.06 KB
new16.72 KB

Oh dear, I am an idiot.

In #159 I took the $sql_mode code from D8 and the string is quite different to D7. I'm actually surprised it didn't break more things, but that's the reason that EntityQuery tests have been failing since then; specifically it's ONLY_FULL_GROUP_BY which has been causing the "Expression #1 of ORDER BY clause is not in SELECT list, references column $XYZ which is not in SELECT list; this is incompatible with DISTINCT" errors. Sorry about that.

So, with that fixed we should hopefully be back to looking at what I think are two options.

1) Use \DatabaseConnection_mysql::setPrefix to put backticks into the prefixReplace patterns so that when prefixing is applied to table names the curly braces are effectively replaced by backticks (plus any actual prefix).

This has the advantage that we're not running every single query through a regex; in fact we're not really adding any extra processing to the SQL.

It has meant making a few seemingly hacky changes to make tests pass (see interdiffs for #182 and #183). I would perhaps like to understand why we're having to do those workarounds better, but AFAICS they may be down to tests doing slightly unusual things with the prefixing anyway.

2) Go with a regex in \DatabaseConnection_mysql::query which searches for {table} in SQL and wraps that in backticks before the prefix step.

That approach doesn't seem to require the same workarounds to get tests passing, but it's potentially quite expensive. See #134

We need to establish whether we actually need to accommodate {db.table} in the regex; see discussion of that in e.g. #180

This patch uses option 1. I'll also get one ready that does 2.

mcdruid’s picture

StatusFileSize
new3.93 KB
new14.05 KB

...and here's a patch using option 2 outlined above.

This undoes the workaround to remove backticks from schema queries which seems to be necessary with the prefix approach.

It's also not necessary to change the order of calls in \DatabaseConnection::__construct (which is required if we override setPrefix for mysql).

This patch uses the simple preg_replace which only looks for {table} and not also {db.table}.

ayesh’s picture

StatusFileSize
new16.66 KB
new2.26 KB

Thanks a lot @mcdruid for working on this. I would have never figured out the SQL mode changes which lead to the MySQL 8 failures.

I would personally, and strongly prefer the approach in #1 (with setPrefix) because it looks cleaner on the long run. It looks a bit like a hack to get those table prefixes applied (i.e using str_replace), but I think it aligns with how rest of the things work in core. It's also a one-time cost, so we definitely score some points in the performance front.

I also like the fact that we don't even need to override the quote() method.

---

In this patch, I worked a little bit on #185 patch (approach #1).

- Moved $this->needsReservedKeywordEscape = version_compare($mysql_server_version, '8', '>='); to constructor. This makes the property is available for all other purposes. I'm not 100% sure about this, but I think Drupal infra tests pass because it always contains the prefix.

- Replaced a subtr call with an string-array-access call (micro performance improvement)

- Removed a $info = $this->connection->getConnectionOptions(); because we no longer use this variable.

As Chx mentioned about the regular expression, I would like to propose that we take a look at our excessive in_array() calls. As of now, it is called for every quoteIdentifier, which can about as many, if not multiple times more, of the number of queries we run during a page load. The array contains ~260 items, and because they are reserved keywords, it is likely that the in_array() yields no results.

in_array() calls can be optimized easily with a flipped array lookup with isset(). in_array() is an O(n) operation while isset() is O(1). isset() is about 50-100 times faster: https://3v4l.org/2R4Dg

If there is a room to improve it, I like to propose that we make that optimization at the same time on similar grounds that we find the regex call concerning. I will attach a patch for reference. In my sites, I already use it with isset() tweaks (base patch #65).

mcdruid’s picture

StatusFileSize
new1.2 KB
new16.82 KB

Thanks @Ayesh, but I cross posted this with your patch. I need to stop for the day now so I'll repost this as-is.

Any of #187's changes would need to be re-applied to this patch; the change I'm making here is very simple and specific, but this is certainly not me rejecting anything from 187... I just don't have time to look at it and merge changes right now.

Original comment:

The reason we're having to do the extra checking for a prefix which has already been quoted in backticks (see interdiff in #182) is that \DrupalUnitTestCase::setUp does this:

    // Generate temporary prefixed database to ensure that tests have a clean starting point.
    $this->databasePrefix = Database::getConnection()->prefixTables('{simpletest' . mt_rand(1000, 1000000) . '}');

...so that prefix is wrapped in curly braces so that it itself will be prefixed :) As we're using \DatabaseConnection_mysql::setPrefix to put backticks into the prefixReplace patterns we end up with weird table names like 'simpletest858168`foo`.

That's very specific to DrupalUnitTestCase though, so if we want to avoid the added complexity in the mysql driver's setPrefix method, we could look to adjust the way that DrupalUnitTestCase is setting up the prefixed db for testing and try to avoid the situation where the prefix is wrapped in backticks before we try to add our own.

On the other hand, the setPrefix method doesn't get called many times, so a little extra logic in there to ensure that MySQL unit tests keep working isn't the end of the world.

This patch is - I think - a more correct way of avoiding problems with DrupalUnitTestCase; the previous approach of just removing a backtick at the end of the prefix didn't work if the site running the tests also uses a prefix.

Interdiff against 185 rather than 186 because this is an enhancement to approach 1 as outlined there; pretty much a different feature branch to 186.

ayesh’s picture

StatusFileSize
new16.76 KB
new2.24 KB

Thanks @mcdruid.
I rebased the patch in #187 for #188.

ayesh’s picture

StatusFileSize
new16.79 KB
ayesh’s picture

StatusFileSize
new1.42 KB

Turns out moving the version_compare() call to construct failed the tests.

Patch in #190 is based on #185.

- Replaced a subtr call with an string-array-access call (micro performance improvement)

- Removed a $info = $this->connection->getConnectionOptions(); because we no longer use this variable.

ayesh’s picture

Giving up :)
I vote for the one in #188 where all tests work for all PHP versions tested against.

mcdruid’s picture

Thanks @Ayesh. Yeah, I think setPrefix is called earlier than where we were setting $this->needsReservedKeywordEscape in the constructor, which is why I moved that assignment there.

We could check the PDO attribute directly within setPrefix and still move the assignment back into the constructor, which might be cleaner to read. I didn't want to do the assignment twice though.

Great that we're passing tests again. Remaining todos AFAICS:

  • More tests - check what D8 added and see if we can do the same.
  • Verify we don't need {db.table} to work.
  • Check the comment in \DatabaseConnection_mysql::quoteIdentifier which I think comes from D8 and may not apply in D7.
  • Check comments in general - should we add any more explanation of changes we're making?

I agree that it's not ideal that we're iterating through the array of reserved words, but that's what D8's doing. I don't see a massive performance impact from the timings of test runs, but haven't looked in too much detail. If we wanted to change that implementation (e.g. isset instead of in_array) that might be something to address in D8 first and backport.

Anything else?

Would be good to get some testing of #188 on some real sites before we mark this as RTBC.

mcdruid’s picture

StatusFileSize
new3.67 KB
new16.63 KB

Little bit of tidying up as mentioned in the last comment:

  • Move assignment of $this->needsReservedKeywordEscape to the constructor, and check the MySQL version directly in setPrefix.
  • Comments tidy up, including providing a D7 example of table.column being passed as an identifier to \DatabaseConnection_mysql::quoteIdentifier.
  • Simplify the logic in setPrefix which removes any backticks already in the prefix.

More tests is still a todo.

Interdiff with #188 per #192

mcdruid’s picture

ayesh’s picture

StatusFileSize
new17.49 KB
new1.35 KB

I'm using the latest patch #194 on MySQL 8.0.20 on a test site (with PHP 7.4), and everything looks to be working great. I agree that we might need more tests added though.

Submitting a patch with a new test containing 3 more assertions.

Status: Needs review » Needs work

The last submitted patch, 196: 2978575-196.patch, failed testing. View results

kevin morse’s picture

FWIW I've been running the patch from #141 on a production site for the last week with no issues. Would be happy to try a newer patch but I might need some help applying it.

The server is Ubuntu 20.04 so am running PHP 7.4.3 with MySQL 8.0.20-0ubuntu0.20.04.1

Would be great to get this into core.

mcdruid’s picture

Status: Needs work » Needs review
StatusFileSize
new4.8 KB
new18.87 KB

I've moved the new tests into their own test case, and added a few more which involve a test table that uses reserved keywords that MySQL 8 doesn't like if they're not escaped.

It's slightly tricky (as we saw with the test fail in #196) to generate SQL that MySQL 5 tolerates but that would fail in MySQL 8 without the escaping.

I'm also not sure what other dbs (postgres / SQLite) are going to make of these tests; we could look at skipping the tests if the db driver is not mysql, which I think happens elsewhere.

mcdruid’s picture

Ah, I think I've come across a problem when working on improving the tests using the new test table.

$ drush ev "print (string) db_select('virtual')->fields('virtual', array('function'));"
SELECT virtual."function" AS "function"
FROM 
{virtual} "virtual"

Looks like the alias in the SELECT list is not being quoted / escaped there, and indeed it doesn't work:

$ drush ev "var_dump(db_select('virtual')->fields('virtual', array('function'))->execute());"
PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'virtual."function" AS "function"                      [error]
FROM 
`virtual` "virtual"' at line 1 in ...

I'll get that query into the test, but will also have a look at what we need to do to make it pass.

mcdruid’s picture

StatusFileSize
new2.14 KB
new19.9 KB

Hmm, I think passing the table name in the select query through escapeAlias fixes this, but I feel a bit uncomfortable about it. It's not really an alias, it is the actual table name as things stand.

We can't implement escapeTable in the mysql driver and pass that through \DatabaseConnection_mysql::quoteIdentifier as that breaks pretty much everything AFAICS.

Let's see what happens with the overall test suite, and possibly for other databases.

mcdruid’s picture

Been looking at the table/alias escaping in 8.9.x in #3155563: select query should quote aliases which are reserved words in MySQL.

I don't think I was correct saying:

It's not really an alias, it is the actual table name as things stand.

__toString for select queries is pretty much exactly the same in D8, and I believe in D7 and D8 there are three places where table aliases are incorrectly escaped by escapeTable rather than escapeAlias.

The changes are already there in #201, but I think they can be considered a backport (although we'll see how we get on getting the change committed to D8).

https://github.com/drush-ops/drush/pull/4456 is also passing tests now, to get better support for escaping table names into Drush 8.

Unfortunately it looks like 7.x tests don't pass at present for PostgreSQL or SQLite; that's the case with or without this patch (same fails can be seen in the noop patch in #158). I spoke about this with @Mixologic and apparently the problem may be the test suite with the other dbs "not handling the parallelism we throw at it during testing properly" as opposed to something in core being actually broken. We'll try to look into that some more, but it's unfortunate not being able to verify that changes we're making here outside of the mysql driver are not causing problems with the other dbs.

So - please test #201. If anyone can test it on PostgreSQL and/or SQLite that'd also be great! I'll have a look at running tests for them locally but it's likely to be pretty slow I think.

mcdruid’s picture

StatusFileSize
new2.62 KB
new21.19 KB

Good news and bad news.

We're making progress in #3155563: select query should quote aliases which are reserved words in MySQL and hopefully with the drush PRs too (https://github.com/drush-ops/drush/pull/4456 and https://github.com/drush-ops/drush/pull/4458).

New patch here only adds to tests; adding the tests from the D8/9 issue above so that table aliases in select queries are well covered.

If anyone's testing #201 on a site, no need to update to this patch as the additional tests are the only difference.

The problem that the extra testing has revealed is that reserved keyword aliases that are not escaped / quoted can be a problem in MySQL 5.7 as well as MySQL 8 (this is illustrated by the test fail for MySQL 5.7 in #201).

So where does that leave us when the current patch has this logic?

    $mysql_server_version = $this->getAttribute(PDO::ATTR_SERVER_VERSION);
    $this->needsReservedKeywordEscape = version_compare($mysql_server_version, '8', '>=');

We could drop that version number to 5.7, which I think would probably be okay to do.

Or we could go back to just doing the escaping unconditionally.

Personally, I am still inclined to reduce the risk of introducing changes on sites that don't need it - so anything running MySQL 5.6 or earlier.

Any thoughts?

Status: Needs review » Needs work

The last submitted patch, 203: 2978575-203.patch, failed testing. View results

mcdruid’s picture

Status: Needs work » Needs review
StatusFileSize
new10.93 KB
new19.24 KB

I have pondered this a fair bit.

The test failures for all MySQL 5.x versions for #203 show that queries with certain table aliases can fail going back several versions.

We could remove those particular tests and treat that as a separate issue, but that's not going to be fixable without doing the escaping of identifiers for versions earlier than MySQL 8.

I was a little nervous about removing the conditional escaping that we introduced several patches back, so I did some rudimentary performance testing. I took a vanilla D7 install + views with a few thousand article nodes via devel generate, then used ab to test the performance of the /frontpage views display listing the teasers of 50 articles, requested a thousand times. That's not a sophisticated test, but would hopefully show if introducing the checking of identifiers against the reserved keywords list was introducing a horrible performance regression. My conclusion is that it looks like it does not; in fact it hardly made any noticeable difference at all. I tested MySQL 5.6, 5.7 and 8 (although the latter obviously doesn't work at all without the escaping).

So here's a patch which does the escaping for all MySQL versions. Again :)

I've also tidied up comments a little in various places, and split the tests out (more like the D8/9 patches for the table alias problems) in the hope that this will make it clearer what's failing if anything does.

mcdruid’s picture

Having said all that I've realised I had this a bit wrong in my head; we weren't making the quoting of identifiers conditional anyway, so it's not likely to have changed the performance to make the table name escaping unconditional (checking identifiers against the array of keywords is the potentially expensive thing). Oops.

My tests didn't suggest that adding recent patches made performance worse on MySQL 5.6 and 5.7, so that's reassuring anyway.

mcdruid’s picture

StatusFileSize
new1.05 KB
new19.35 KB

Same change as the D8 patch in #3155563-45: select query should quote aliases which are reserved words in MySQL which hopefully makes it clearer why $field['table'] is the table alias and should be escaped as such.

mcdruid’s picture

I think I'm pretty happy with this patch now, but we may get some more feedback from the D8/9 issue mentioned above.

In the meantime, I'd welcome testing on real sites and/or reviews to get it to RTBC before I put it forward for final review from our illustrious Framework Manager @Fabianx.

Oh, and the drush PRs were merged so hopefully the next releases will work better with MySQL 8.

joseph.olstad’s picture

wow, impressive, you even fixed php 5.3.x!

andrew_rs’s picture

#207 did the trick for me on Ubuntu 20.04, PHP 7.4.3, and MySQL 8.0.20.

mcdruid’s picture

Thanks @andrew_rs!

The D8/9 patches were committed so... "Needs review" applies :)

fabianx’s picture

Here is the review from the illustrious framework manager :D - Thx mcdruid for spearheading this work!

  1. +++ b/includes/database/database.inc
    @@ -311,8 +311,6 @@ abstract class DatabaseConnection extends PDO {
    -    // Initialize and prepare the connection prefix.
    -    $this->setPrefix(isset($this->connectionOptions['prefix']) ? $this->connectionOptions['prefix'] : '');
    
    @@ -320,6 +318,9 @@ abstract class DatabaseConnection extends PDO {
    +    // Initialize and prepare the connection prefix.
    +    $this->setPrefix(isset($this->connectionOptions['prefix']) ? $this->connectionOptions['prefix'] : '');
    +
    

    This change should be unnecessary - now that the check for the MySQL version has been removed.

    This is a far-fetching change that affects also third party drivers like Oracle.

    In fact, let's add a comment here so that it never gets moved before the __construct again -- if it is crucial to be in this order.

  2. +++ b/includes/database/mysql/database.inc
    @@ -86,15 +357,90 @@ class DatabaseConnection_mysql extends DatabaseConnection {
    +
    +    $sql_mode = 'REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO';
    +    // NO_AUTO_CREATE_USER is removed in MySQL 8.0.11
    +    // https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-11.html#mysqld-8-0-11-deprecation-removal
    +    if (version_compare($this->getAttribute(PDO::ATTR_SERVER_VERSION), '8.0.11', '<')) {
    +      $sql_mode .= ',NO_AUTO_CREATE_USER';
    +    }
         $connection_options['init_commands'] += array(
    -      'sql_mode' => "SET sql_mode = 'REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER'",
    +      'sql_mode' => "SET sql_mode = '$sql_mode'",
         );
    +
    

    Thx - Nice cleanup compared to my first review :)

  3. +++ b/includes/database/mysql/database.inc
    @@ -86,15 +357,90 @@ class DatabaseConnection_mysql extends DatabaseConnection {
    +    foreach ($this->prefixSearch as $i => $prefixSearch) {
    +      if (substr($prefixSearch, 0, 1) === '{') {
    +        // If the prefix already contains one or more backticks, remove them.
    +        // This can happen when - for example - DrupalUnitTestCase sets up a
    +        // "temporary prefixed database".
    +        $this->prefixReplace[$i] = '`' . str_replace('`', '', $this->prefixReplace[$i]);
    +      }
    +      if (substr($prefixSearch, -1) === '}') {
    +        $this->prefixReplace[$i] .= '`';
    +      }
    

    This whole prefix business vs non-prefix also tripped me up a lot for the Oracle driver for D8.

    That said:

    This needs some more comments to why prefixSearch could start with {, but not end with }.

    Also is this the right function to do so?

    Isn't prefixTables() better as that has more control about things and is nearer to the actual replacement of {table}?

  4. +++ b/includes/database/mysql/database.inc
    @@ -86,15 +357,90 @@ class DatabaseConnection_mysql extends DatabaseConnection {
    +  public function escapeField($field) {
    +    $field = parent::escapeField($field);
    +    return $this->quoteIdentifier($field);
    +  }
    

    I presume this and the following functions is verbatim how PostgresSQL driver does it right?

  5. +++ b/includes/database/mysql/database.inc
    @@ -86,15 +357,90 @@ class DatabaseConnection_mysql extends DatabaseConnection {
    +      // Quote the string for MySQL reserved keywords.
    +      $identifier = '"' . $identifier . '"';
    

    We quote always with '"'?

    And backticks was a MySQL extension only used for table names, right?

    Double-checking as I can't remember the most safe quoting rules right now.

    In theory that should not matter, but in practice it might.

    That said if Postgres driver also uses that, it would be pretty safe as there was still some fair amount of complex sites on Postgres.

  6. +++ b/includes/database/mysql/query.inc
    @@ -48,6 +48,10 @@ class InsertQuery_mysql extends InsertQuery {
    +    if (method_exists($this->connection, 'escapeFields')) {
    +      $insert_fields = $this->connection->escapeFields($insert_fields);
    +    }
    +
    

    We don't need to update the UpdateQuery class as it piggy backs on insert, right?

    I had HUGE problems with UPDATE and reserved keywords in Oracle for D8 - hence double checking again.

  7. +++ b/includes/database/mysql/schema.inc
    @@ -57,6 +57,10 @@ class DatabaseSchema_mysql extends DatabaseSchema {
    +    // Ensure the table name is not quoted with backticks as that is not
    +    // appropriate for schema queries.
    +    $table_name = str_replace('`', '', $table_name);
    +
    

    That's the thing where I dislike the automatic escaping, but it's really hard to know when it should be quoted for which purpose.

    I think this is good enough however.

  8. +++ b/includes/database/mysql/schema.inc
    @@ -494,11 +498,11 @@ class DatabaseSchema_mysql extends DatabaseSchema {
    -      return $this->connection->query("SELECT column_comment FROM information_schema.columns WHERE " . (string) $condition, $condition->arguments())->fetchField();
    +      return $this->connection->query("SELECT column_comment as column_comment FROM information_schema.columns WHERE " . (string) $condition, $condition->arguments())->fetchField();
    ...
    -    $comment = $this->connection->query("SELECT table_comment FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchField();
    +    $comment = $this->connection->query("SELECT table_comment as table_comment FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchField();
    
    +++ b/includes/database/schema.inc
    @@ -343,7 +343,7 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
    -    return $this->connection->query("SELECT table_name FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchAllKeyed(0, 0);
    +    return $this->connection->query("SELECT table_name as table_name FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchAllKeyed(0, 0);
    

    Why are those changes needed?

    Also should 'AS' not be uppercase if we introduce this newly?

  9. +++ b/includes/database/select.inc
    @@ -1520,13 +1520,16 @@ class SelectQuery extends Query implements SelectQueryInterface {
    +      // Note that $field['table'] holds the table alias.
    +      // @see \SelectQuery::addField
    +      $table = isset($field['table']) ? $this->connection->escapeAlias($field['table']) . '.' : '';
           // Always use the AS keyword for field aliases, as some
           // databases require it (e.g., PostgreSQL).
    -      $fields[] = (isset($field['table']) ? $this->connection->escapeTable($field['table']) . '.' : '') . $this->connection->escapeField($field['field']) . ' AS ' . $this->connection->escapeAlias($field['alias']);
    +      $fields[] = $table . $this->connection->escapeField($field['field']) . ' AS ' . $this->connection->escapeAlias($field['alias']);
         }
    

    Maybe we should use $table_alias to make it clear what it is?

  10. +++ b/includes/database/select.inc
    @@ -1555,7 +1558,7 @@ class SelectQuery extends Query implements SelectQueryInterface {
           // Don't use the AS keyword for table aliases, as some
           // databases don't support it (e.g., Oracle).
    -      $query .=  $table_string . ' ' . $this->connection->escapeTable($table['alias']);
    +      $query .=  $table_string . ' ' . $this->connection->escapeAlias($table['alias']);
    

    How do we solve the conflict here?

    Do we need something that adds the 'AS' or was that comment only relevant for fields and it works fine for MySQL without AS for table names + aliases?

Overall looks good, but needs some more scrutinizing :)

Thanks everyone for the great work on this difficult issue so far - this has come miles since I last reviewed it!

ronlee’s picture

Wow! We're almost there!

Great job, everyone.

Love drupal 7 :)

tr’s picture

I have been using #207 on my test site for a week now with no apparent problems. I've done a lot of clicking around, using the UI to see if any errors are generated - this tests different things than the test cases and this is the method that quickly revealed shortcomings in the previous patches (see for example #56) even when those previous patches tested green on DrupalCI.

I've also done some non-scientific profiling using the test cases, and I haven't seen any significant differences in runtime with/without this patch (within the large variation of how long it takes to run the tests with other things going on - as I said, non-scientific because I haven't strictly controlled the environment). I have noticed however that the MySql 8 tests are consistently faster than the MySql 5 tests, so being able to move to MySql 8 is a win.

ronlee’s picture

I'm not seeing much issues, is anyone else?

mcdruid’s picture

StatusFileSize
new18.17 KB
new2.96 KB

Thanks for the review in #212 @Fabianx.

1) Well spotted; reverted.

2) :)

3) Sorry, I don't understand this suggestion:

This needs some more comments to why prefixSearch could start with {, but not end with }.

As for:

Isn't prefixTables() better as that has more control about things and is nearer to the actual replacement of {table}?

setPrefix() sets up the search/replace patterns that prefixTables() uses to swap out {table}. I'm not sure how we'd wrap the backticks around the tablenames here:

  public function prefixTables($sql) {
    return str_replace($this->prefixSearch, $this->prefixReplace, $sql);
  }

... in a better way than using prefixSearch and prefixReplace to swap { and } both for `?

4) The PostgresSQL driver doesn't implement escapeField or have a quoteIdentifier method (nor does SQLite).

5) in D8 \Drupal\Core\Database\Driver\mysql\Connection::quoteIdentifier uses double quotes.

The quoting of table names was implemented in D9 (#2986452: Database reserved keywords need to be quoted as per the ANSI standard) and also uses double quotes for all 3 RDMS supported by core, e.g.:

https://git.drupalcode.org/project/drupal/-/blob/9.0.x/core/lib/Drupal/C...

So yes, we could try using double quotes around table names instead of backticks; comments in D8/9 mention double quotes being the ANSI standard whereas I think backticks are more of a MySQL-ism.

Happy to try using " instead of ` around table names for consistency.

Note, however, that we've now added backticks around table names in MySQL in drush but that shouldn't conflict with what we're doing here.

I'll run tests on an updated patch with the first 5 points addressed, and will look at the remaining items after that.

mcdruid’s picture

StatusFileSize
new20.62 KB
new5.21 KB

Okay, looks like double quotes around table names works fine (as we'd expect).

212.6 was a great catch, thanks @Fabianx

Turns out we should override UpdateQuery in order to escape its fields (which wasn't as simple as a straight copy from InsertQuery as the field names we need to escape are array keys in this case).

We don't need to do anything to MergeQuery as it's the one that piggy backs on Insert and/or Update.

I've added explicit tests for all of the query classes in includes/database/query.inc which helped figure out what we needed to do for Update.

Running tests again and will address the remaining points ASAP.

mcdruid’s picture

StatusFileSize
new20.62 KB
new2.02 KB

212.7) Yes, agreed this feels a bit messy but I don't think we have much choice but to make small compromises retro-fitting table name quoting into D7.

212.8)

Why are those changes needed?

...is a good question. I wasn't sure and had to dig into this a little...

#2966523: MySQL 8 Support still has this listed under "Remaining tasks":

  • core/lib/Drupal/Core/Database/Schema.php (202): change "table_name" to "TABLE_NAME as table_name"

...and indeed the information_schema.tables column names are uppercase in all versions of MySQL I've looked at; perhaps that was the motivation, but it's not actually been implemented exactly like that.

There is some discussion of the table alias being required for kernel tests to pass around #2966523-10: MySQL 8 Support.

I haven't verified whether not adding the table alias causes any problems in D7, but I'd vote to keep this change for consistency with D8/9 if nothing else.

Also should 'AS' not be uppercase if we introduce this newly?

That was bothering me :) #3159982: AS keyword should be capitalised in SQL queries has been committed to D8/9 so yes let's make that change in our patch too.

212.9) Yes maybe $table_alias would make it clearer. However, that would diverge from the changes we've recently made to D8/9 in the same code - see #3155563: select query should quote aliases which are reserved words in MySQL.

I'd vote to leave that for a follow-up (against D9 for backport to D7 and maybe D8).

212.10) I think that comment just means "don't include an AS keyword between table name and table alias because of compatibility problems e.g. with Oracle". AFAICS it's always worked without the "AS" for all RDMS and we don't need to change that. All we're changing is passing the alias through the appropriate escape method as it is an alias and not a table name here.

So the only change in this new patch is to uppercase "AS" in a few schema queries.

I think I've covered all your points from #212 @Fabianx, so back to you for re-review.

aparna.kondala’s picture

So far I haven't seen any issues after applying patch to Drupal 7.72. I think we can apply to core.

bernig’s picture

On a fresh Drupal 7.72, I cannot install any contrib module with the patch 2978575-218.
Reverting, and applying patch 2978575-14 works.

Tried with drush and via the BO.

Mysql version: 8.0.20
PHP : 7.2
OS : Ubuntu 19.10

The error I'm getting is:

PHP Fatal error:  Uncaught exception 'PDOException' with message 'SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '"semaphore" WHERE name = 'schema:runtime::cache'' at line 1' in /var/www/seminaires-ifr.fr/includes/database/database.inc:2227
Stack trace:
#0 /var/www/seminaires-ifr.fr/includes/database/database.inc(2227): PDOStatement->execute(Array)
#1 /var/www/seminaires-ifr.fr/includes/database/database.inc(697): DatabaseStatementBase->execute(Array, Array)
#2 /var/www/seminaires-ifr.fr/includes/database/database.inc(2406): DatabaseConnection->query('SELECT expire, ...', Array, Array)
#3 /var/www/seminaires-ifr.fr/includes/lock.inc(167): db_query('SELECT expire, ...', Array)
#4 /var/www/seminaires-ifr.fr/includes/lock.inc(146): lock_may_be_available('schema:runtime:...')
#5 /var/www/seminaires-ifr.fr/includes/bootstrap.inc(438): lock_acquire('schema:runtime:...')
#6 /var/www/seminaires in /var/www/seminaires-ifr.fr/includes/database/database.inc on line 2227
PHP Stack trace:
PHP   1. _drupal_exception_handler() /var/www/seminaires-ifr.fr/includes/bootstrap.inc:0
PHP   2. _drupal_log_error() /var/www/seminaires-ifr.fr/includes/bootstrap.inc:2621
PHP   3. drush_return_status() /usr/share/php/drush/includes/bootstrap.inc:0
PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near &#039;"registry" r
WHERE  (r.name LIKE &#039;drush\\_drupal\\_environment&#039; ESCAPE &#039;\\&#039;) AND&#039; at line 3: SELECT r.filename AS filename
FROM 
{registry} r
WHERE  (r.name LIKE :db_condition_placeholder_0 ESCAPE &#039;\\&#039;) AND (r.type = :db_condition_placeholder_1) ; Array
(
    [:db_condition_placeholder_0] => drush\_drupal\_environment
    [:db_condition_placeholder_1] => trait
)
 in _registry_check_code() (line 3507 of /var/www/seminaires-ifr.fr/includes/bootstrap.inc).
<h1>Uncaught exception thrown in shutdown function.</h1><p>PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near &amp;#039;&amp;quot;semaphore&amp;quot; 
WHERE  (value = &amp;#039;10237218125f564b45b4e868.09644358&amp;#039;)&amp;#039; at line 1: DELETE FROM {semaphore} 
WHERE  (value = :db_condition_placeholder_0) ; Array
(
    [:db_condition_placeholder_0] =&amp;gt; 10237218125f564b45b4e868.09644358
)
 in lock_release_all() (line 269 of /var/www/seminaires-ifr.fr/includes/lock.inc).</p><hr />

Maybe something is wrong with my environment, but the fact is that the new patch fails in my case.

ronino’s picture

This refers to the discussion on whether the "{db.table}" syntax should be supported.

I investigated a bit more thoroughly what migrate actually does so a migration fails when using the latest patch(es) with something like this:

SQLSTATE[42S02]: Base table or view not found: 1146 Table 'db.db.migrate_map_migrateclass' doesn't exist (note the double "db").

Now what does migrate do? MigrateSourceSQL::performRewind() adds a left join to a query on a table that includes the database if no prefixes are used (see MigrateSQLMap::getQualifiedMapTable()). So a string like "`db`.table" will be generated, then reduced to "db.table" by DatabaseConnection::escapeTable() (which is well aware that there can be a DOT in a table name, most likely for a given database...).

SelectQuery::__toString() will then convert this to '{db.table}' which will be quoted to '"db.table"' by DatabaseConnection::prefixTables() as DatabaseConnection_mysql::setPrefix() now ads double quotes to $this->prefixReplace in the latest patches. This will make MySQL think that "db.table" is indeed just the table name as it's quoted.

I'm not sure if it's migrate's fault here, as it used to work pre MySQL 8 when there was no quoting. So passing "db.table" to SelectQuery::leftJoin() was perfectly fine. I don't know about other modules doing this.

Possible solutions:

1. Make SelectQuery::__toString() convert table names to "db.{table}" instead of "{db.table}" (and probably in other classes like DeleteQuery).

2. Make the migrate module's MigrateSQLMap::getQualifiedMapTable() not prepend a database. I don't know what side effects this would have.

mcdruid’s picture

Thanks for digging into that @Ronino - my impression is that it seems like quite an edge case in migrate and I don't think we should introduce expensive code into core to work around it. In other words, I'd vote for your option 2.

I've filed #3171091: \MigrateSQLMap::getQualifiedMapTable may break with MySQL 8 support in D7 core in migrate's queue about this.

I don't know about other modules doing this.

...is an interesting point. Anyone else know of other modules that do this? I'm inclined to think it's pretty unusual; it sounds like in migrate it's an optimisation that only works in the absence of table prefixing.

mcdruid’s picture

I've posted a patch for migrate in #3171091-5: \MigrateSQLMap::getQualifiedMapTable may break with MySQL 8 support in D7 core - I believe the problem is actually migrate wrapping the dbname in backticks (which shouldn't be necessary any more once core itself quotes MySQL table names).

So - unless I'm missing something - that issue doesn't represent a blocker for this one. Thanks again for digging into it @Ronino

mcdruid’s picture

My previous comment about backticks in the migrate code was incorrect, but there is a working (I think) patch for migrate now.

So I don't think we're blocked here.

izmeez’s picture

Similar problem as described in comment #220.

Using Centos7, php 7.4.10 and 5.5.5-10.2.33-MariaDB.

Updated system to Drupal core 7.73 with patches including the patch 2978575-218.

Clear all caches with drush or drupal UI results in errors as described in comment #220.

izmeez’s picture

Working backwards, same problem with patch 2978575-217.patch

PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check [error]
the manual that corresponds to your MariaDB server version for the right syntax to use near '"registry" r
WHERE  (r.name LIKE 'drush\\_drupal\\_environment' ESCAPE '\\') ...' at line 3 in
includes/database/database.inc:2237
Stack trace:
#0 includes/database/database.inc(2237):
PDOStatement->execute(Array)
#1 includes/database/database.inc(697):
DatabaseStatementBase->execute(Array, Array)
#2 includes/database/select.inc(1283):
DatabaseConnection->query('SELECT r.filena...', Array, Array)
#3 includes/bootstrap.inc(3526): SelectQuery->execute()
#4 includes/bootstrap.inc(3442):
_registry_check_code('class', 'drush_drupal_en...')
#5 [internal function]: drupal_autoload_class('drush_drupal_en...')
#6 [internal function]: spl_autoload_call('drush_drupal_en...')
#7 phar:///usr/local/share/drush_8.3.5/drush/includes/engines.inc(525): class_exists('drush_drupal_en...')
#8 phar:///usr/local/share/drush_8.3.5/drush/includes/drupal.inc(121): drush_include_engine('drupal',
'environment')
#9 includes/module.inc(965): system_watchdog(Array)
#10 includes/bootstrap.inc(2043):
module_invoke_all('watchdog', Array)
#11 includes/bootstrap.inc(1977): watchdog('menu', '%type:
!message...', Array, 3, NULL)
#12 includes/menu.inc(2810): watchdog_exception('menu',
Object(PDOException))
#13 includes/common.inc(7798): menu_rebuild()
#14 phar:///usr/local/share/drush_8.3.5/drush/commands/core/drupal/cache.inc(99): drupal_flush_all_caches()
#15 phar:///usr/local/share/drush_8.3.5/drush/includes/drush.inc(726): drush_cache_clear_both()
#16 phar:///usr/local/share/drush_8.3.5/drush/includes/drush.inc(713):
drush_call_user_func_array('drush_cache_cle...', Array)
#17 phar:///usr/local/share/drush_8.3.5/drush/commands/core/cache.drush.inc(144): drush_op('drush_cache_cle...')
#18 phar:///usr/local/share/drush_8.3.5/drush/includes/command.inc(422): drush_cache_command_clear('all')
#19 phar:///usr/local/share/drush_8.3.5/drush/includes/command.inc(231): _drush_invoke_hooks(Array, Array)
#20 phar:///usr/local/share/drush_8.3.5/drush/includes/command.inc(199): drush_command('all')
#21 phar:///usr/local/share/drush_8.3.5/drush/lib/Drush/Boot/BaseBoot.php(67): drush_dispatch(Array)
#22 phar:///usr/local/share/drush_8.3.5/drush/includes/preflight.inc(67):
Drush\Boot\BaseBoot->bootstrap_and_dispatch()
#23 phar:///usr/local/share/drush_8.3.5/drush/includes/startup.inc(465): drush_main()
#24 phar:///usr/local/share/drush_8.3.5/drush/includes/startup.inc(369): drush_run_main(false, '/', 'Phar
detected. ...')
#25 phar:///usr/local/share/drush_8.3.5/drush/drush(114): drush_startup(Array)
#26 /usr/local/share/drush_8.3.5/drush(10): require('phar:///usr/loc...')
#27 {main}
<h1>Uncaught exception thrown in shutdown function.</h1><p>PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near &amp;#039;&amp;quot;semaphore&amp;quot; 
WHERE  (value = &amp;#039;11450774795f6280a5639616.41807728&amp;#039;)&amp;#039; at line 1: DELETE FROM {semaphore} 
WHERE  (value = :db_condition_placeholder_0) ; Array
(
    [:db_condition_placeholder_0] =&amp;gt; 11450774795f6280a5639616.41807728
)
 in lock_release_all() (line 269 of includes/lock.inc).</p><hr />
izmeez’s picture

Working backwards, same problem with 2978575-216.patch

However, 2978575-207.patch works.

izmeez’s picture

StatusFileSize
new11.09 KB

Taking another look with patch 2978575-218 and the error in drupal UI flush caches on Centos7, php 7.4.10 and 5.5.5-10.2.33-MariaDB. Attached is the system diff between applying patch in 207 that works and the patch in 218 that results in the following error:

Additional uncaught exception thrown while handling exception.

Original
PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '"semaphore" WHERE (name = 'menu_rebuild') AND (value = '7679125235f631d4857...' at line 1: DELETE FROM {semaphore} WHERE (name = :db_condition_placeholder_0) AND (value = :db_condition_placeholder_1) ; Array ( [:db_condition_placeholder_0] => menu_rebuild [:db_condition_placeholder_1] => 7679125235f631d4857fec9.74729041 ) in lock_release() (line 254 of includes/lock.inc).

Additional
PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '"filter_format" ff WHERE (status = '1') ORDER BY weight ASC' at line 3: SELECT ff.* FROM {filter_format} ff WHERE (status = :db_condition_placeholder_0) ORDER BY weight ASC; Array ( [:db_condition_placeholder_0] => 1 ) in filter_formats() (line 434 of modules/filter/filter.module).

Uncaught exception thrown in session handler.
PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '"sessions" sessions WHERE ( (sid = 'EtVgaLi_DYMNxww0zhsYZXoPKPObGBcTRqpVaIBl8...' at line 3: SELECT 1 AS expression FROM {sessions} sessions WHERE ( (sid = :db_condition_placeholder_0) AND (ssid = :db_condition_placeholder_1) ); Array ( [:db_condition_placeholder_0] => EtVgaLi_DYMNxww0zhsYZXoPKPObGBcTRqpVaIBl8QQ [:db_condition_placeholder_1] => EtVgaLi_DYMNxww0zhsYZXoPKPObGBcTRqpVaIBl8QQ ) in _drupal_session_write() (line 209 of includes/session.inc).

Uncaught exception thrown in shutdown function.
PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '"semaphore" WHERE (value = '7679125235f631d4857fec9.74729041')' at line 1: DELETE FROM {semaphore} WHERE (value = :db_condition_placeholder_0) ; Array ( [:db_condition_placeholder_0] => 7679125235f631d4857fec9.74729041 ) in lock_release_all() (line 269 of includes/lock.inc).

Hope this is more helpful.

mcdruid’s picture

@izmeez @bernig what drush version are you using when you hit these errors?

I cannot reproduce with drush 8.4.0 (and PHP 7.2 / MySQL 8.0.21).

The paths in #226 suggest you may have been using drush 8.3.5 ? If so, there were some MySQL 8 fixes included in the release after that.

I'm not certain it'll fix the problem you're seeing, but please try updating drush (looks like 8.4.2 was released a few days ago) and try again.

If you're still getting the same errors after that... I am curious - could you please try swapping the double quotes back to backticks in \DatabaseConnection_mysql::setPrefix? There would be a couple of other places the change would need to be made to revert that properly, but I am curious whether it'd prevent the particular error you're seeing.

It's hard to debug further without being able to reproduce the issue; any more details that might help?

izmeez’s picture

@mcdruid Thanks for reply.

I'm pretty sure it's not a drush thing because it happens within the drupal UI "flush caches" but will update to drush 8.4.2 and test again.

I am wondering if it is related to php 7.4.10 which is more strict on errors. But, I will try your suggestions and report back.

izmeez’s picture

Good news.
Update to drush 8.4.2 makes no difference but is still worthwhile.

Using patch 2978575-218 and reverting following section of patch to backticks in

--- a/includes/database/mysql/database.inc
+++ b/includes/database/mysql/database.inc
@@ -381,16 +381,16 @@ class DatabaseConnection_mysql extends DatabaseConnection {
     parent::setPrefix($prefix);
     // Successive versions of MySQL have become increasingly strict about the
     // use of reserved keywords as table names. Drupal 7 uses at least one such
-    // table (system). Therefore we surround all table names with backticks (`).
+    // table (system). Therefore we surround all table names with double quotes.
     foreach ($this->prefixSearch as $i => $prefixSearch) {
       if (substr($prefixSearch, 0, 1) === '{') {
-        // If the prefix already contains one or more backticks, remove them.
+        // If the prefix already contains one or more double quotes remove them.
         // This can happen when - for example - DrupalUnitTestCase sets up a
         // "temporary prefixed database".
-        $this->prefixReplace[$i] = '`' . str_replace('`', '', $this->prefixReplace[$i]);
+        $this->prefixReplace[$i] = '"' . str_replace('"', '', $this->prefixReplace[$i]);
       }
       if (substr($prefixSearch, -1) === '}') {
-        $this->prefixReplace[$i] .= '`';
+        $this->prefixReplace[$i] .= '"';
       }
     }
   }

allows "drush cc all" and also drupal UI "Flush all caches" to work.

Will await new patch with additional changes you alluded to for testing.
Thanks.

mcdruid’s picture

Thanks @izmeez

I need to dig into this some more - not sure if it's related to the way that PDO works or something like that, but my MySQL 8 db seems to prefer backticks:

mysql> SELECT VERSION();
+-------------------------+
| VERSION()               |
+-------------------------+
| 8.0.21-0ubuntu0.20.04.4 |
+-------------------------+

mysql> SELECT COUNT(1) FROM system;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'system' at line 1

mysql> SELECT COUNT(1) FROM "system";
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '"system"' at line 1

mysql> SELECT COUNT(1) FROM `system`;
+----------+
| COUNT(1) |
+----------+
|      148 |
+----------+
izmeez’s picture

@mcdruid

I'm confused. You say

I need to dig into this some more - not sure if it's related to the way that PDO works or something like that, but my MySQL 8 db seems to prefer backticks:

That's what I just did on your suggestion, reverted double quotes to backticks and it worked for MariaDB.

mfb’s picture

Sounds like maybe sql_mode is incorrect? SELECT COUNT(1) FROM "system"; should work fine as long as you first run SET sql_mode = 'ANSI_QUOTES';

mcdruid’s picture

Status: Needs review » Needs work

It definitely seems that backticks are the default quoting syntax for MySQL:

The identifier quote character is the backtick (`):

https://dev.mysql.com/doc/refman/8.0/en/identifiers.html

However, D8/9 have gone with double quotes which are the ANSI standard (see #216 5).

MySQL optionally supports the ANSI standard (same source):

If the ANSI_QUOTES SQL mode is enabled, it is also permissible to quote identifiers within double quotation marks:

...and Drupal tries to turn that on:

    $connection_options['init_commands'] += array(
      'sql_mode' => "SET sql_mode = 'REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER'",
    );

https://git.drupalcode.org/project/drupal/-/blob/7.73/includes/database/...

So I'm not sure why double quotes instead of backticks would apparently sometimes cause problems; it looks like Drupal should be setting the SQL mode such that they'd work (not always the case when using the MySQL cli directly e.g. #232).

There are advantages to staying consistent with D8/9 which is why we made the change around #216 after @Fabianx's review in #212 5.

However, if that's causing problems in some cases we should consider going back to using backticks.

I'm not comfortable diverging from what's been done in D8/9 without good reason though, which is what I meant by "dig into this some more".

I'll work on a patch which reverts to backticks instead of double quotes, unless someone beats me to it :)

mcdruid’s picture

@mfb sorry I cross-posted with you - yes, I think you're right.

Not sure why double quotes would apparently not work sometimes when Drupal's set up the connection with ANSI_QUOTES but on the other hand, why wouldn't we just go with the MySQL default to avoid such problems?

mfb’s picture

The only thing I could think of is that folks manually changed init_commands sql_mode in settings.php to disable ANSI_QUOTES? There are advantages of using ANSI syntax - portability of the SQL - and I suppose of the MySQL syntax too (portability of the SQL to MySQL w/ default sql_mode :)

mcdruid’s picture

@izmeez @bernig can you please run something like this in the environment where you had the errors?

$ drush ev 'print_r(db_query("SHOW VARIABLES LIKE '\''%sql_mode%'\''")->fetchAssoc());'
Array
(
    [Variable_name] => sql_mode
    [Value] => REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO
)

It'd be good to confirm whether you have ANSI_QUOTES and/or anything else other than what D7 sets as the connection options.

gisle’s picture

This may be an extremely stupid question, but please bear with me.

To me, it looks like the really difficult issue to resolve is captured in this comment in the code:

Successive versions of MySQL have become increasingly strict about the use of reserved keywords as table names. Drupal 7 uses at least one such table (system). Therefore we surround all table names with [something].

Finding the right formula for escaping "system" (and any other reserved keyword) seems to be pretty hard, given the requirement that it should work not only on the latest version of MySQL, but also work on the ancient versions of MySQL that is still part of CentOS, and work on PostgreSQL, which has other escaping mechanisms.

Here is my question: Why don't we change the schema so that table names do not collide with reserved keywords?

For instance, can we change the name the "system" table to "drupalsystemtable", and then escape (pun intended) from the need to escape the table name?

izmeez’s picture

@mcdruid In followup to #238

drush ev 'print_r(db_query("SHOW VARIABLES LIKE '\''%sql_mode%'\''")->fetchAssoc());'

returns:

Array
(
    [Variable_name] => sql_mode
    [Value] => REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER
)

Looks the same as the example except the extra NO_AUTO_CREATE_USER.

mcdruid’s picture

Status: Needs work » Needs review

@izmeez thanks. I am curious why you (and @bernig) seem to have hit errors with the ANSI_QUOTES when it looks like your config should be switching that option on. I suppose if you have NO_AUTO_CREATE_USER you either don't have the patch applied or are not using MySQL 8 (or equivalent) though? (edit: I see in #225 you mentioned 5.5.5-10.2.33-MariaDB)

I am moving this back to NR as I'm in two minds about reverting to using backticks; I can see an argument for doing so as they are the default for MySQL but the double quotes should work, are the ANSI standard, and have been implemented in D9.

If we don't understand the problem you're hitting, I'm not sure we should be changing the patch to try and work around a mystery issue.

@gisle I don't think it's stupid to suggest that we avoid using reserved words for table names, but on the other hand I'm not convinced that changing the name of the system table is a viable alternative to quoting table names.

As has been mentioned before retro-fitting table name quoting to D7 is not without its problems, but I think it's the right solution.

izmeez’s picture

@mcdruid Fair enough, we can stay at patch #207 while keeping an eye on this issue. At the time of running the "drush ev" command the patch from #207 was in effect. The "NO_AUTO_CREATE_USER" may be a MariaDB default setting. Correct this is a system that is not running MySQL8 and the Maria10 version is the latest update available but may need further fixes for ANSI_QUOTES. Either way, I'd assume the patch should be backward compatible, otherwise this will require some sort of conditional.

izmeez’s picture

More on MariaDB, https://mariadb.com/kb/en/sql-mode/

ANSI

Changes the SQL syntax to be closer to ANSI SQL.

Sets: REAL_AS_FLOAT, PIPES_AS_CONCAT, ANSI_QUOTES, IGNORE_SPACE.

It also adds a restriction: an error will be returned if a subquery uses an aggregating function with a reference to a column from an outer query in a way that cannot be resolved.

ANSI_QUOTES

Changes " to be treated as `, the identifier quote character. This may break old MariaDB applications which assume that " is used as a string quote character.

izmeez’s picture

Maybe this is beyond the scope of this particular thread, however, I have executed grep and found that the contrib module backup_migrate is also using sql_mode in the following:

$ grep -r "sql_mode"
includes/database/mysql/database.inc:    $sql_mode = 'REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO';
includes/database/mysql/database.inc:      $sql_mode .= ',NO_AUTO_CREATE_USER';
includes/database/mysql/database.inc:      'sql_mode' => "SET sql_mode = '$sql_mode'",
sites/all/modules/contrib/backup_migrate/includes/destinations.db.mysql.inc:    $sql_mode = $this->query("SELECT @@SESSION.sql_mode")->fetchField();
sites/all/modules/contrib/backup_migrate/includes/destinations.db.mysql.inc:    $this->query("SET sql_mode = 'ANSI'");
sites/all/modules/contrib/backup_migrate/includes/destinations.db.mysql.inc:    $this->query("SET SQL_mode = :mode", array(':mode' => $sql_mode));
sites/all/modules/contrib/backup_migrate/includes/destinations.db.mysql.inc:      $out .= "SET sql_mode = 'ANSI';\n";
sites/all/modules/contrib/backup_migrate/includes/destinations.db.mysql.inc:      $out .= "SET sql_mode = '$sql_mode';\n";
sites/all/modules/contrib/backup_migrate/includes/destinations.db.mysql.inc:      $this->connection->exec("SET sql_mode=''");
sites/all/modules/contrib/backup_migrate/includes/sources.db.mysql.inc:    $sql_mode = $this->query("SELECT @@SESSION.sql_mode")->fetchField();
sites/all/modules/contrib/backup_migrate/includes/sources.db.mysql.inc:    $this->query("SET sql_mode = 'ANSI'");
sites/all/modules/contrib/backup_migrate/includes/sources.db.mysql.inc:    $this->query("SET SQL_mode = :mode", array(':mode' => $sql_mode));
sites/all/modules/contrib/backup_migrate/includes/sources.db.mysql.inc:      $out .= "SET sql_mode = 'ANSI';\n";
sites/all/modules/contrib/backup_migrate/includes/sources.db.mysql.inc:      $out .= "SET sql_mode = '$sql_mode';\n";
sites/all/modules/contrib/backup_migrate/includes/sources.db.mysql.inc:      $this->connection->exec("SET sql_mode=''");
CHANGELOG.txt:- Spelled out what TRADITIONAL means in MySQL sql_mode.

I am wondering if anyone can spot where the conflict with these settings and the patch may be arising. Thanks.

mcdruid’s picture

Issue tags: -Drupal 7.73 target
StatusFileSize
new3.55 KB
new21.97 KB

ANSI was changed to a specific list of sql_mode settings in #2545480: Don't use the ANSI SQL mode since it has different meanings for different MySQL versions (and breaks MySQL 5.7 support in Drupal 7) but I believe both should effectively enable ANSI_QUOTES.

I tried the patch from #218 with mariadb 5.5 on centos7 and had no problems, so I don't understand the issue that you're encountering @izmeez

It's obviously a bit of a concern that we might introduce a change into D7 which would cause catastrophic problems for sites that don't really need the identifier quoting; e.g. anything on earlier versions of MySQL like 5.5, 5.6 and perhaps even 5.7.

With this in mind, I've updated the patch to use a variable to determine the character that should be used to quote identifiers. This would allow sites to set this variable in settings.php in order to use backticks instead of double quotes if that works better for them.

It's also possible to disable identifier quoting altogether by setting the mysql_identifier_quote_character variable to an empty string. We could tweak a couple of places we use the variable to check specifically for this condition e.g. \DatabaseSchema_mysql::buildTableNameCondition where we could avoid a pointless call to str_replace() if the variable is empty. It seems to work without doing that though.

Obviously I want to ensure nothing breaks in the tests with this change in place, but I'm hopeful that it might provide some flexibility for any sites that encounter problems for whatever reason with the introduction of identifier quoting.

@izmeez could you give this a try with the variable set to a backtick (and/or an empty string) in settings.php please?

izmeez’s picture

@mcdruid Tested patch in #245. The default fails as before with patch #218. Using variable in settings.php set to backtick and empty string both work. Looking at that section of the patch,

+/**
+ * Quoting of identifiers in MySQL.
+ *
+ * To allow compatibility with newer versions of MySQL, Drupal will quote table
+ * names and some other identifiers. The ANSI standard character for identifier
+ * quoting is the double quote (") so that is used by default, along with the
+ * sql_mode setting of ANSI_QUOTES. However, MySQL's own default is to use
+ * backticks (`). If you need to use backticks instead, you can do so via the
+ * mysql_identifier_quote_character variable. It's also possible to switch off
+ * identifier quoting altogether by setting this variable to an empty string.
+ *
+ * @see https://dev.mysql.com/doc/refman/8.0/en/identifiers.html
+ * @see \DatabaseConnection_mysql::setPrefix
+ * @see \DatabaseConnection_mysql::quoteIdentifier
+ */
+# $conf['mysql_identifier_quote_character'] = '';
+

I would suggest change to include backtick (the default) in example for improved readability
+# $conf['mysql_identifier_quote_character'] = '`';

I also find it confusing that using an empty string will switch this off and yet things still work, is this because drupal is not conflicting with reserved words in earlier versions of MySQL as it is with reserved words in MySQL8?

Also, thank you for testing a similar environment with Centos7 and MariaDB 5.5.x(10.2.x) and reporting no problems. This leaves me wondering if another core patch that we are using is resulting in the conflict. This will require more sleuthing.

tr’s picture

With this in mind, I've updated the patch to use a variable to determine the character that should be used to quote identifiers. This would allow sites to set this variable in settings.php in order to use backticks instead of double quotes if that works better for them.

It's also possible to disable identifier quoting altogether by setting the mysql_identifier_quote_character variable to an empty string. We could tweak a couple of places we use the variable to check specifically for this condition e.g. \DatabaseSchema_mysql::buildTableNameCondition where we could avoid a pointless call to str_replace() if the variable is empty. It seems to work without doing that though.

@mcdruid: Since this patch is in very good shape at this point, can you write a draft change notice that outlines the changes made here and includes the above instructions for changing the backticks/disabling the quoting?

mcdruid’s picture

StatusFileSize
new1.13 KB
new21.97 KB

@izmeez thanks for testing; yes the example value in default.settings.php was meant to be a backtick; good catch.

I've also fixed wording in one comment in the patch.

I also find it confusing that using an empty string will switch this off and yet things still work, is this because drupal is not conflicting with reserved words in earlier versions of MySQL as it is with reserved words in MySQL8?

Yes, the identifier quoting is required for MySQL 8, mainly because of the system table, but sites may have problems with other parts of their schema provided by contrib or custom modules etc..

Earlier versions of MySQL typically don't require quoting, although it is possible to name things in the schema such that they cause issues. At least one of the tests we're adding here will fail on MySQL 5.7 without identifier quoting.

@TR yes, we'll definitely want a Change Record / Notice for this. I'll draft one soon (unless anybody beats me to it).

izmeez’s picture

@mcdruid Thanks for update to patch in #248.

The patch looks good, applies without difficulty and works on 5.5.5-10.2.33-MariaDB, equivalent to older version of MySQL, without breaking things. In our case the settings.php variable to set identifier quoting to backtick is essential to avoid pdo exception error with "drush cc all" or drupal UI "flush all caches". Testing on our setup with drupal 7.73 and only this patch along with the contrib modules and theme in use still require the identifier quoting to be a backtick. The other core patches we use are not a source of conflict with this patch which is a relief.

I would like to change the status to RTBC but would like to see others weigh in especially on testing with MySQL 8 and possibly if the change introduced also helps with PostgreSQL.

Thanks for all your efforts.

gisle’s picture

I am testing this on Ubuntu 20.04 LTS with the following configuration.

Drupal: HEAD of 7.74-dev + patch from #248
PHP:    7.4.3
MySQL:  8.0.21
Drush:  8.4.2

The following line (from default.settings.php) was added to settings.php:

$conf['mysql_identifier_quote_character'] = '`';

Testing with drush cc all works as expected.

So I will say that this one is good to go for MySQL 8.

Not setting $conf[mysql_identifier_quote_character] (i.e. using the ANSI quote character (")) won't fly. Without adding that line to settings.php, drush cc all triggers the PDOException.

There probably should be release notes with instructions for all DBMSes that don't work with the ANSI standard character. For MySQL, I suggest the following text:

If you are running MySQL, read the comment block about "Quoting of identifiers in MySQL" in "default.settings.php", and make sure line that sets $conf['mysql_identifier_quote_character'] to a backtick is in your "settings.php".

mcdruid’s picture

Thanks @izmeez and @gisle for testing and providing detailed feedback.

However, it's a bit of a concern that with sample size of two, 100% of D7 sites have had to change to backticks to avoid an apparent problem with ANSI_QUOTES.

I'd like to understand what's going on but have been unable to reproduce the problem with just core. Could you please share a list of the modules you have enabled (e.g. drush pml --status=Enabled)? Also, do you have any customisations to the db settings such as custom init_commands etc...? You can PM me in Drupal slack or use my d.o contact form if you'd prefer not to share details here.

In the absence of a simple explanation / fix for the issues you're hitting, I'm inclined to say we should default to using backticks in D7, but retain the mysql_identifier_quote_character variable so that sites can use ANSI_QUOTES if they prefer to for some reason, and more importantly opt-out of identifier quoting if they don't need it and it causes problems.

gisle’s picture

You're right, it can't be reproduced by the core alone.

The site I am testing this on is a clean install of Drupal 7.74-dev + patch with absolutely no customizations.

I use a single contributed module: Backup and Migrate. To reproduce, just install that. (Also, see comment #244 above).

As this module has nearly a quarter million installs and MySQL probably by far the most popular DBMS among Drupalers, I guess if you make ANSI_QUOTES the default, we are going to see a Drupal 7 version of a forum topic that is already very popular with the Drupal 8 community 😊.

Here is the output of the drush command you wanted to see:

$ drush pml --status=enabled
 Package  Name                                   Type    Version  
 Core     Block (block)                          Module  7.74-dev 
 Core     Color (color)                          Module  7.74-dev 
 Core     Comment (comment)                      Module  7.74-dev 
 Core     Contextual links (contextual)          Module  7.74-dev 
 Core     Dashboard (dashboard)                  Module  7.74-dev 
 Core     Database logging (dblog)               Module  7.74-dev 
 Core     Field (field)                          Module  7.74-dev 
 Core     Field SQL storage (field_sql_storage)  Module  7.74-dev 
 Core     Field UI (field_ui)                    Module  7.74-dev 
 Core     File (file)                            Module  7.74-dev 
 Core     Filter (filter)                        Module  7.74-dev 
 Core     Help (help)                            Module  7.74-dev 
 Core     Image (image)                          Module  7.74-dev 
 Core     List (list)                            Module  7.74-dev 
 Core     Menu (menu)                            Module  7.74-dev 
 Core     Node (node)                            Module  7.74-dev 
 Core     Number (number)                        Module  7.74-dev 
 Core     Options (options)                      Module  7.74-dev 
 Core     Overlay (overlay)                      Module  7.74-dev 
 Core     Path (path)                            Module  7.74-dev 
 Core     RDF (rdf)                              Module  7.74-dev 
 Core     Search (search)                        Module  7.74-dev 
 Core     Shortcut (shortcut)                    Module  7.74-dev 
 Core     System (system)                        Module  7.74-dev 
 Core     Taxonomy (taxonomy)                    Module  7.74-dev 
 Core     Text (text)                            Module  7.74-dev 
 Core     Toolbar (toolbar)                      Module  7.74-dev 
 Core     Update manager (update)                Module  7.74-dev 
 Core     User (user)                            Module  7.74-dev 
 Other    Backup and Migrate (backup_migrate)    Module  7.x-3.7  
 Core     Bartik (bartik)                        Theme   7.74-dev 
 Core     Seven (seven)                          Theme   7.74-dev 
mcdruid’s picture

StatusFileSize
new273.6 KB

Thanks @gisle, it looks like we have an answer. Noting that @izmeez also mentioned backup_migrate in #244.

It looks like backup_migrate sets an empty sql_mode during cache clear:

https://git.drupalcode.org/project/backup_migrate/-/blob/7.x-3.9/include...

backup_migrate nukes sql_mode

...and that's obviously a problem when all db queries are relying on ANSI_QUOTES being in place.

A git bisect suggests that this was initially introduced in backup_migrate - sort of - in #1128620: Module needs to back table properties up as well albeit that introduced the sql_mode reset into includes/destinations.db.mysql.inc.

The problem we're hitting is in the corresponding includes/sources.db.mysql.inc and it looks like it was introduced in this commit: https://git.drupalcode.org/project/backup_migrate/-/commit/9c0e6d3b7b3ae...

...but (to me at least) the history behind that is not entirely clear.

This looks like a bug in backup_migrate. It's setting sql_mode to an empty string on cache clear but it looks like the intention was to enable the module to capture certain metadata about the database before making a backup. I'm surprised this hasn't caused more problems.

Where does that leave us?

I'll file an issue for backup_migrate, similar to the one for migrate.

However, pragmatically we may want to go with the option of using backticks by default in D7 rather than relying on ANSI_QUOTES when we know at least one very popular module will cause that to break sites.

mcdruid’s picture

Filed #3172388: backup_migrate resets sql_mode causing problems with D7's MySQL 8 support for backup_migrate.

I think it would probably be a bad idea to block this core issue on resolution of that though, when the alternative is fairly simple (backticks by default, keep the variable).

mcdruid’s picture

StatusFileSize
new3.37 KB
new22.38 KB

Changed the default to backticks.

I've introduced a constant to make it easier to change the default.

Here's a thought; would the lowest-risk approach be to default to an empty string (i.e. no identifier quoting) and require sites to set their preferred mysql_identifier_quote_character in settings.php if they need it?

I actually think backticks are pretty safe as they work by default with all versions of MySQL. The exception will be where contrib/custom modules do unusual things in their SQL like we saw with migrate around #221.

trevorbradley’s picture

A few days late, but a quick note that my Drupal 7+mysql 8 sites were working great until the recent Drupal 7.73 update. I installed the patch from #218, cleared the cache and they're all working great again!

mcdruid’s picture

Thanks for the feedback @TrevorBradley, however the 7.73 release didn't touch anything to do with the database:

https://git.drupalcode.org/project/drupal/-/compare/7.72...7.73

Are you able to test the most recent patch and provide feedback?

izmeez’s picture

I'm not sure about the change suggested in #255.

First, it complicates the language with a variable mysql_identifier_quote_character and also a constant MYSQL_IDENTIFIER_QUOTE_CHARACTER_DEFAULT the naming of which grates with all the discussion on MySQL default identifier and ANSI_QUOTES identifier. It is just another layer of wording that doesn't really add much.

Secondly, even with just a variable, a module (such as backup_migrate) could temporarily change the variable if needed to a backtick or an empty variable or whatever is needed.

I still think the introduction of the variable in settings.php is good and if the issue in backup_migrate can be resolved the default could be double quotes in keeping with ANSI_QUOTES.

izmeez’s picture

@TrevorBradley Are your sites using the backup_migrate module?

mustanggb’s picture

Changed the default to backticks.

I actually think backticks are pretty safe as they work by default with all versions of MySQL.

This was going to be my suggestion, MySQL is the de facto D7 database, so I'd have thought it would have the most frictionless "out of the box" experience. Then other databases can tweak $conf or sql_mode if needed.

mcdruid’s picture

@izmeez re. #258 the constant is just a convenience for core; it makes it easier if we want to change the default again, but sites shouldn't change it. (Think of the kittens ;) It makes no difference to the functionality, it's just arguably neater than maintaining the default in every relevant call to variable_get() (of which there are only 3 at present, so we could easily live without the constant).

-    $quote_char = variable_get('mysql_identifier_quote_character', '"');
+    $quote_char = variable_get('mysql_identifier_quote_character', MYSQL_IDENTIFIER_QUOTE_CHARACTER_DEFAULT);

@MustangGB yup, backticks are the MySQL default so should be safe. One of the main reasons I think we might be better defaulting to them is that I think it's highly likely there will be other contrib / custom modules that do things that have always "just worked" with MySQL but that break if we use double quotes and rely on the corresponding ANSI_QUOTES setting in sql_mode.

We've found two issues with popular contrib modules more-or-less by luck based on helpful contributors here testing patches with more than just core. I don't think we can be very confident that we've found them all though.

So yes, backticks by default would hopefully be more "frictionless" as you say, rather than relying on (potentially a lot of) sites to un-break themselves, even if we have made that relatively easy with the variable. (I think in many cases problems really ought to be fixed in the non-core code, but that can be pretty far from "frictionless".)

In general, the identifier quoting and sql_mode only relate to MySQL. We're not introducing any quoting for other dbs.

It's concerning that the status of automated tests on SQLite and PostgreSQL is somewhat unknown (see #202). I will try and look into that some more, but it looks like they're not working against 7.x HEAD on the drupalci system at present without any of the patches here.

This issue does have some changes to the database code outside of the MySQL driver, but identifier quoting and sql_mode changes are MySQL-specific.

With any luck sites that use non-MySQL databases shouldn't have to change anything.

izmeez’s picture

@mcdruid Constant makes sense, thanks for detailed response. Default to backtick will be less disruptive to contrib modules.

Thanks for test patch in #3172388: backup_migrate resets sql_mode causing problems with D7's MySQL 8 support. It isn't needed with the current patch in #255 but demonstrates the fix needed in that contrib module if ansi double quotes are to be used.

mcdruid’s picture

I had a look into testing on SQLite and came across #1713332: The SQLite database driver fails to drop simpletest tables which still needs to be fixed in D7.

Before finding that, I'd applied a crude hack (based on $connection_info['default']['driver'] == 'sqlite') to avoid this failing every time on SQLite:

    // Remove all prefixed tables.
    $tables = db_find_tables($this->databasePrefix . '%');
    $connection_info = Database::getConnectionInfo('default');
    $tables = db_find_tables($connection_info['default']['prefix']['default'] . '%');
    if (empty($tables)) {
      $this->fail('Failed to find test tables to drop.');
    }

https://git.drupalcode.org/project/drupal/-/blob/7.73/modules/simpletest...

...and with that in place was able to get most tests passing locally.

Results with patch #255 in place, results were:

  • 41439 passes, 439 fails

Re-running just the 24 tests which had at least one fail, but this time without this issue's patch, results were:

  • 2978 passes, 439 fails

Then re-applying patch #255 and running those 24 tests again:

  • 2971 passes, 439 fails

So all a bit crude and messy, but it looks like the fails were consistent whether we had the patch from this issue in place or not.

I'd prefer cleaner results obviously (and I think we should aim to get them passing properly), but this is at least somewhat reassuring that the recent patches are not significantly breaking other databases.

In general, clicking around the site with the SQLite db seemed to work okay.

trevorbradley’s picture

@TrevorBradley Are your sites using the backup_migrate module?

@izmeez, no, just plain old regular D7 sites that stopped working after the 7.73 update until I patched them.

mustanggb’s picture

I think in many cases problems really ought to be fixed in the non-core code, but that can be pretty far from "frictionless".

Just wondering in what versions of MySQL these contrib modules break.

I mean ideally they would only have problems with MySQL 8, and still work fine with MySQL 5 even with this patch here.

Do we know if this is the case?

mcdruid’s picture

@MustangGB the problems we've seen with migrate and backup_migrate didn't only affect MySQL 8, they affected all MySQL versions.

This is because we're doing quoting for all MySQL versions.

Around #158 we started (not for the first time) trying to keep the MySQL 8 changes separate and leave earlier versions unchanged to reduce this sort of risk.

However, around #203 we worked out that sometimes MySQL 5.7 may need quoting (e.g. reserved words used as aliases) - we briefly pondered drawing a line in the sand at MySQL 5.7 in terms of quoting but by #205 we were back to doing the same quoting for all MySQL versions.

Lots of special-casing everywhere for different MySQL versions is messy and hard to maintain. I did look at having a completely separate driver for MySQL 8 (would that have had to become MySQL 5.7?) and later, and it wasn't too hard to get something working in core, but immediately drush was broken because it didn't have a corresponding "new MySQL" driver.

So that's why we've ended up with quoting for all MySQL versions, and why there's a risk that there may be more gremlins hiding in contrib.

Backticks by default likely reduces that risk, and giving sites an easy way to switch off quoting if it causes problems and they don't need it hopefully mitigates it further still.

I suppose you could look at doing something like overriding mysql_identifier_quote_character in $conf to turn off quoting if the site's on a version of MySQL < 5.7. IIRC it might not be as simple as it sounds because of the order that things happen in the db driver constructors (look at earlier patches that made more of the logic conditional). I'm not certain how much I like that idea in general, but will have a think about it.

mcdruid’s picture

Little update here; #3172878: Fix SQLite tests in Drupal 7 looks like there are a few things we need to do in D7 and at least one patch to drupalci to get SQLite tests passing.

However, it seems quite achievable - I'm seeing fewer than 5 test fails with SQLite with a few patches in place.

I think it's worthwhile pursuing these in parallel as we gain some reassurance around changing shared db code.

On the issue of defaulting to no identifier quoting for older versions of MySQL (probably < 5.7) what does anyone else think? 5.6 is soon to be EoL but obviously there will be distros that support older versions for several more years.

gisle’s picture

I am testing this on Ubuntu 20.04 LTS with the following configuration:

Drupal core: HEAD of 7.74-dev + patch from #255
Extensions to core: Backup and Migrate 7.x-3.9
PHP:    7.4.3
MySQL:  8.0.21
Drush:  8.4.5

I did not modify settings.php

Testing with drush cc all now works as expected.

So I will say that this one is good to go for MySQL 8.

However, I am not moving to RTBC yet, as it should be tested on other configurations (MySQL 5, PostgreSQL, etc.) as well.

izmeez’s picture

The patch in #255 passes all the tests and has some community testing successes so is it ready for RTBC, yet? I wonder who else wants to pipe in with their success comment?

mcdruid’s picture

Now that SQLite tests are passing for D7 core, I tested #255 with SQLite.

There was one error, but it looks like it's in the new tests we're introducing:

testTableNameQuoting

exception: [Uncaught exception] Line 262 of includes/database/sqlite/database.inc:
PDOException: SQLSTATE[HY000]: General error: 1 near "/": syntax error: SELECT * FROM sites/default/files/db.sqlite.{system} LIMIT 1; Array
(
)
 in DrupalTestCase->run() (line 527 of /var/www/html/modules/simpletest/drupal_web_test_case.php).

I'll take a closer look at that, unless anyone beats me to it.

mcdruid’s picture

StatusFileSize
new1.41 KB
new22.81 KB

I think we're going to have to skip that particular test for SQLite; I've explained why in a comment (see interdiff).

joseph.olstad’s picture

Interdiff #271 looks great, totally makes sense and great detective work on that. The only thing I can say about it is maybe punctuation for the comments. I think there's a missing period or two and capitalization to start a new line.
I'm not going to complain though just though just mentioning. I myself have a hard time understanding the coding standard when it comes to comment blocks and comments. I am with Linus Torvalds on the 80 column limit, I think it's time to increase that.

izmeez’s picture

I wouldn't say anything about the punctuation myself as when comments are about code sometimes periods joining words causes issues with formal sentence structure, even a comma ends up looking odd. I guess the only other option is to drop the capitalization of "So ..." and have a long run on sentence.

+      // for example: simpletest124904.system
+      // So ...

Not really an issue, I think the way it is is not uncommon in comments about code.

mcdruid’s picture

StatusFileSize
new917 bytes
new22.81 KB

Hopefully the comment's better. Thanks for the feedback.

mcdruid’s picture

StatusFileSize
new1.64 KB
new23.54 KB

Oh, quite a few problems with the MySQL 8 tests now.

I think what's happened is that in #1713332: The SQLite database driver fails to drop simpletest tables we backported the newer implementation of \Drupal\Core\Database\Schema::findTables (which is now \DatabaseSchema::findTablesD8 in D7) but findTables was later tweaked in the original #2966523: MySQL 8 Support

So we need to backport that change into the new findTablesD8. As far as I can see that's actually a very simple change (which we'd already made to what's now the BC findTables function).

(I have also moved a comma in the comment for the SQLite test skipping).

izmeez’s picture

@mcdruid, I am sure you are clear on this but I it is a little confusing for me. It looks like the findTablesD8 was a transient element that was then backported to D7 and is now gone with only findTables remaining, is that correct? It surprises me there was ever an introduction of naming with D8 in the name, what about D9, D10, etc. Maybe I'm missing something.

mcdruid’s picture

Status: Needs review » Reviewed & tested by the community

Marking this as RTBC as I believe it's ready for re-review by @Fabianx

To make that a bit easier, here's a brief summary since the last Framework Manager review.

Given the problems we've come across in two popular contrib modules, now thinking we should default to backticks for identifier quoting; Drupal 9 uses double quotes per the ANSI standard, but that relies on the sql_mode being set appropriately. Backticks are the MySQL default, and it seems a safer bet that contrib and custom code will use backticks if they've quoted anything. (see #235)

#263 to #271 getting SQLite tests to pass; there's one new test in this issue which we have to skip with SQLite (see #271).

One remaining question is "what do we use for the default quoting character?". I am now inclined to say that we use a backtick by default in order to minimise the risk.

You could argue we could really keep changes to a minimum by defaulting to no quoting (empty string) for MySQL 5.6 and below, and default to backticks for MySQL 5.7 and above.

The issues that we've filed for migrate and backup_migrate are up to those projects now, but using backticks means backup_migrate won't break sites when they update core. If we use double quotes, it will cause problems unless the backup_migrate issue is fixed and sites update the module before they update core.

My vote is default to backticks, and obviously we'll have a Change Record and a Release Note to make it clear how sites can disable identifier quoting (set the variable to an empty string) if it causes them problems and they don't need it.

joseph.olstad’s picture

@mcdruid
you might want to have a look at #2003746: field_read_fields() omits indexes from hook_field_schema
maybe related to this

alexpott’s picture

+++ b/includes/database/mysql/database.inc
@@ -86,15 +362,92 @@ class DatabaseConnection_mysql extends DatabaseConnection {
+    if (in_array(strtolower($identifier), $this->reservedKeyWords, TRUE)) {
+      // Quote the string for MySQL reserved keywords.
+      $quote_char = variable_get('mysql_identifier_quote_character', MYSQL_IDENTIFIER_QUOTE_CHARACTER_DEFAULT);
+      $identifier = $quote_char . $identifier . $quote_char;
+    }

I think it is important to note in the CR / advice given is that only identifiers are reserved word in MySQL8 will be quoted. IN D9 we just quote every identifier as this makes it much simpler in terms of db differences.

izmeez’s picture

Patch in #274 applies without difficulty to drupal 7.73 but patch in #275 fails which was a bit of a surprise.

patch --dry-run -p1 < 2978575-275.patch
checking file includes/database/mysql/database.inc
checking file includes/database/mysql/query.inc
checking file includes/database/mysql/schema.inc
checking file includes/database/schema.inc
Hunk #1 succeeded at 343 (offset -5 lines).
Hunk #2 FAILED at 379.
1 out of 2 hunks FAILED
checking file includes/database/select.inc
checking file modules/simpletest/tests/database_test.install
checking file modules/simpletest/tests/database_test.test
Hunk #3 succeeded at 4243 (offset -2 lines).
checking file sites/default/default.settings.php

But, it applies cleanly to drupal 7.x-dev as expected.

joseph.olstad’s picture

izmeez, your local was not clean when you ran this dry-run

I just tested 275 :

patch --dry-run -p1 < 2978575-275.patch
checking file includes/database/mysql/database.inc
checking file includes/database/mysql/query.inc
checking file includes/database/mysql/schema.inc
checking file includes/database/schema.inc
checking file includes/database/select.inc
checking file modules/simpletest/tests/database_test.install
checking file modules/simpletest/tests/database_test.test
checking file sites/default/default.settings.php

275 is clean, no issues, please make sure to delete any unstaged files and reset any others before reapplying

Mixologic’s picture

@izmeez patches are not intended to be used against tags, they are intended to work against 7.x branch. You should not expect them to apply to 7.73.

izmeez’s picture

Thanks guys.

Yes, I understand patches are intended to work against 7.x HEAD. But some workflows may only use tagged release for production and may encounter this.

The patch --dry-run tests were done on a fresh clone of drupal with checkout of 7.73 and 7.x and, while it is not an issue with the patch in #275, there are a number of changes in /includes/database/schema.inc in 7.x including the section where the new second hunk applies:

@@ -379,7 +379,7 @@ abstract class DatabaseSchema implements QueryPlaceholderInterface {
     // couldn't use db_select() here because it would prefix
     // information_schema.tables and the query would fail.
     // Don't use {} around information_schema.tables table.
-    $results = $this->connection->query("SELECT table_name FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments());
+    $results = $this->connection->query("SELECT table_name AS table_name FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments());
     foreach ($results as $table) {
       // Take into account tables that have an individual prefix.
       if (isset($individually_prefixed_tables[$table->table_name])) {

I don't think there is anything wrong with the patch in #275, it just looks like we are at the stage where this patch needs to be finalized and committed for those who want to use it on production.

ronino’s picture

#274 works great for me on D7.73 (#275 should, too, but I didn't test HEAD), even with the migrate module after applying #3171091-16: \MigrateSQLMap::getQualifiedMapTable may break with MySQL 8 support in D7 core.

fabianx’s picture

Issue tags: +Pending Drupal 7 commit

This is good to go and approved.

I am okay with backticks by default and under the rare circumstance that it breaks any sites using MySQL 5.6 or similar, they can always set the variable to '' to circumvent the issue.

We are fixing a bug here as reserved keywords should really not be used as is for certain things, so that is why it is okay to change the default behavior even if it potentially risks breaking a site somewhere.

It is just important that the release notes very clearly state this.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/includes/database/mysql/database.inc
@@ -86,15 +362,92 @@ class DatabaseConnection_mysql extends DatabaseConnection {
+    $sql_mode = 'REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO';
+    // NO_AUTO_CREATE_USER was removed in MySQL 8.0.11
+    // https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-11.html#mysqld-8-0-11-deprecation-removal
+    if (version_compare($this->getAttribute(PDO::ATTR_SERVER_VERSION), '8.0.11', '<')) {
+      $sql_mode .= ',NO_AUTO_CREATE_USER';
+    }
     $connection_options['init_commands'] += array(
-      'sql_mode' => "SET sql_mode = 'REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER'",
+      'sql_mode' => "SET sql_mode = '$sql_mode'",
     );

We're in the process of changing this in Drupal 8 - see #3089902: "Azure Database for MySQL server" reports wrong database version

the tldr; is that we're removing NO_AUTO_CREATE_USER from the list. With Drupal 7 you might want to go the other way and check for mysql smaller the 5.7 and add the NO_AUTO_CREATE_USER.

mcdruid’s picture

Thanks @alexpott

So do we need to override \DatabaseConnection::version for MySQL too? Similar to:

core/lib/Drupal/Core/Database/Driver/mysql/Connection.php

 @@ -286,7 +286,7 @@ class Connection extends DatabaseConnection {
    *  The PDO server version.
    */
    protected function getServerVersion(): string {
  -  return $this->connection->getAttribute(\PDO::ATTR_SERVER_VERSION);
  +  return $this->connection->query('SELECT VERSION()')->fetchColumn();
    }

We'd probably also need to do the same replacement in \DatabaseConnection_mysql::__construct()

mcdruid’s picture

Status: Needs work » Needs review
StatusFileSize
new1.27 KB
new24.08 KB

Assuming we do want to query MySQL directly for the version rather than trusting the PDO constant... let's see what the tests say.

alexpott’s picture

@mcdruid I wouldn't do the query for the version - that's an extra query on every request. The Azure issue is that their MySQL 8 reports 5.7 via the PDO param so using the PDO param should be okay here.

mcdruid’s picture

StatusFileSize
new1022 bytes
new23.75 KB

Okay, makes sense. Thanks.

n.b. interdiff against #275

trevorbradley’s picture

Status: Needs review » Needs work

I just went to update my D7+MySQL 8 sites with the latest D7.74 security update, previously patched with 2978575-218.patch

I hit some real problems with #290.

$ patch -p1 < 2978575-290.patch 
patching file includes/database/mysql/database.inc
patching file includes/database/mysql/query.inc
patching file includes/database/mysql/schema.inc
patching file includes/database/schema.inc
Hunk #1 succeeded at 343 (offset -5 lines).
Hunk #2 FAILED at 379.
1 out of 2 hunks FAILED -- saving rejects to file includes/database/schema.inc.rej
patching file includes/database/select.inc
patching file modules/simpletest/tests/database_test.install
patching file modules/simpletest/tests/database_test.test
Hunk #3 succeeded at 4256 with fuzz 2 (offset 11 lines).
patch: **** Can't create temporary file sites/default/default.settings.php.ob5vYlp : Permission denied

I was able to download a fresh copy of Drupal 7.74, replace includes and modules/simpletest, and then apply #218 again, which seems to work fine.

Just to make sure, I downloaded a fresh D7.74, and attempted to apply #290 against it, and it similarly fails.

Unfortunately, this needs to need to go back to "Needs work" after the latest D7.74 update.

mfb’s picture

Status: Needs work » Needs review

Setting back to needs review as patch applies to 7.x branch. It fails on 7.74 because 7.x branch has a new method committed last month: https://git.drupalcode.org/project/drupal/-/commit/6d33276740c0f8c9f4cfb...

mcdruid’s picture

StatusFileSize
new63.28 KB

Yup, security releases are not tagged from the head of the branch, they're based on the previous tag.

gitk showing recent commits and tags

So all the commits from #3172878: Fix SQLite tests in Drupal 7 (and one or two others) are in 7.x but not 7.74

Patches need to apply to the branch (7.x) but may not apply to the most recent tag especially if that was a security release.

I'm hopeful that we'll commit this soon and it'll be in the next release due on 2020-12-02.

joseph.olstad’s picture

@mcdruid , thanks for the illustration. Looking forward to 2020-12-02! It's shaping up nicely thanks for spending so much time on this release.

Once we get these top priority bug fixes in and mysql 8 support in then we can focus on php 8.x support. Hopefully php 8.x support will be easier than mysql 8 support and php 7.4.x support (that was a big one too).

izmeez’s picture

Yes and then it will include the commit acknowledging mcdruid's promotion to a full D7 maintainer, well deserved :-)

joseph.olstad’s picture

composer issues may be related to packagist forcing composer 2.x on everyone.

I recently fixed composer issues on a project by adding the flag option for composer self update as follows:

composer self-update --1 see MR :

if this is suspected to be an issue, perhaps open a new issue in one of these issue queues:
https://www.drupal.org/project/drupalci_testbot
https://www.drupal.org/project/drupalci

alexpott’s picture

+++ b/includes/database/mysql/database.inc
@@ -86,15 +362,95 @@ class DatabaseConnection_mysql extends DatabaseConnection {
+
+    $sql_mode = 'REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO';
+    // NO_AUTO_CREATE_USER was removed in MySQL 8.0.11
+    // https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-11.html#mysqld-8-0-11-deprecation-removal
+    // However, some platforms report the MySQL version incorrectly so only add
+    // this option back into sql_mode for versions before MySQL 5.7.
+    // @see https://www.drupal.org/project/drupal/issues/3089902
+    if (version_compare($this->getAttribute(PDO::ATTR_SERVER_VERSION), '5.7', '<')) {
+      $sql_mode .= ',NO_AUTO_CREATE_USER';
+    }
     $connection_options['init_commands'] += array(
-      'sql_mode' => "SET sql_mode = 'REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER'",
+      'sql_mode' => "SET sql_mode = '$sql_mode'",
     );

So this bit has got a bit more complex with further discussions on #3089902: "Azure Database for MySQL server" reports wrong database version. I'm not 100% sure of the impacts here. Azure will report a version 5.6!!!!! So on Drupal 7 it'll be installable because you min version allows it, but with the current code you'll still set NO_AUTO_CREATE_USER.

I think you're fine but it might need documenting. I think in such cases the user will need to override the sql_mode init command by setting it on the connection options... in the database array.

Something like https://3v4l.org/8h9vH

mcdruid’s picture

StatusFileSize
new1.35 KB
new23.95 KB

Hmm, that's pretty messy.

So IIUC Drupal's sometimes connecting to a db proxy (e.g. in Azure) which can mean PDO::ATTR_SERVER_VERSION can be completely wrong; e.g. reporting 5.6 when the actual backend db server is MySQL 8.

I'm inclined to go back to #275 where we do:

version_compare($this->getAttribute(PDO::ATTR_SERVER_VERSION), '8.0.11', '<')

...and ensure that we document overriding sql_mode in settings.php if sites have problems with Azure or other proxied MySQL setups.

Or we could borrow from #3089902-69: "Azure Database for MySQL server" reports wrong database version and query the db for the version, along with some static caching to avoid doing so more than once.

Adding one simple query is likely not that significant for a D7 site.

How about we do both? Here's a patch which adds a \DatabaseConnection_mysql::version() method (with a static cache) - overriding the parent method, which we now call from the constructor. In the constructor, the version comparison is back to checking for < 8.0.11 which I believe should be correct if we're asking MySQL directly for the version.

Is there a reason we shouldn't call this method from the constructor? Doing so seems to work fine AFAICS.

mcdruid’s picture

Status: Needs review » Reviewed & tested by the community

Discussed this in slack with @alexpott and we concluded that going back to #275 is probably the best option.

So we're not adding more queries, and we're checking against the correct version details for the change in MySQL 8.

We'll want to make sure that the potential problem with proxies is mentioned in the release notes / CR with a link to docs on how to change sql_mode in settings.php if a D7 site encounters the problem with the PDO constant misreporting the MySQL version e.g. on Azure.

#275 is the patch that @Fabianx reviewed and approved, so I think we're okay to commit that in the next day or so before the release next week.

Thanks for bringing this nasty gotcha to our attention though @alexpott!

izmeez’s picture

In terms of

We'll want to make sure that the potential problem with proxies is mentioned in the release notes / CR with a link to docs on how to change sql_mode in settings.php if a D7 site encounters the problem with the PDO constant misreporting the MySQL version e.g. on Azure.

is it important enough to also include a note in default.settings.php ?

  • mcdruid committed e189264 on 7.x
    Issue #2978575 by mcdruid, Ayesh, Ronino, fietserwin, emilcarpenter,...
mcdruid’s picture

Status: Reviewed & tested by the community » Fixed
Issue tags: -Pending Drupal 7 commit

Thank you to the many contributors here!

The CR / release notes will need some work, but it's great to get this committed to D7.

mustanggb’s picture

Thanks everyone!

PHP 8 next :P

joseph.olstad’s picture

Fantastic work, it was a doozy, I helped out with the drush upstream.

Yes for PHP 8

gisle’s picture

Thank you so much! Great work.

jan-e’s picture

Quote from https://www.drupal.org/project/drupal/issues/2978575#comment-13701126

Regarding table prefixes, are they pointing to a different database?
When the prefix uses a '.' it points to a different database. When '_' it refers tot the same database, just a different table name. Obviously, ending the prefix in a '.' is not going to solve the reserved words issue for system/groups as it only points to a different database. Having a prefix without ending in '.' should solve that, unless the new table name ends up being a reserved word.

This seems to be broken in Drupal 7.76. See my comment here: https://www.drupal.org/node/3185889#comment-13924331

mcdruid’s picture

@Jan-E yes it looks like identifier quoting will cause problems with table prefixing if you're using a dot in the prefix to refer to a different schema / database.

However, it also looks like you can avoid this by setting the variable to an empty string as per the example in default.settings.php

$conf['mysql_identifier_quote_character'] = '';

The Change Record mentions:

Any sites running older versions of MySQL which experience problems because of identifier quoting (typically this will mean SQL errors) should set this variable to an empty string in settings.php to disable identifier quoting.

It looks like you may need to do that (for now at least) if you want to use prefixes including dots.

Can we file a new issue for this please?

jan-e’s picture

@mcdruid Thanks. I can confirm adding $conf['mysql_identifier_quote_character'] = ''; works. However, I would not know what the relation to identifier quoting is, so it did not ring a bell when I read the Change Record. I assumed it was an unexpected side effect of the support for MySQL 8.

Status: Fixed » Closed (fixed)

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

marcinem789 made their first commit to this issue’s fork.