Problem/Motivation

In #3109534: Raise the minimum MySQL version to 5.7.8 and MariaDB version to 10.2.7 in Drupal 9, we raised the minimum MySQL version to 5.7 for Drupal 9.0. However, MySQL 5.6 isn't EOL until Feb. 2021. We should therefore make it possible for people to have a custom driver that allows MySQL 5.6, at least for a little while. At some point, possibly as early as Drupal 9.1, we might start using MySQL 5.7 features, such as JSON, that'll be hard for a 5.6 driver to implement. But even if a MySQL 5.6 custom driver is only functional for Drupal 9.0 (and not 9.1), Drupal 9.0 will be supported until after Feb. 2021, so even a trivial custom driver can get people past MySQL 5.6's EOL.

To a large extent, core already allows custom drivers to extend the core ones. Within settings.php, you can have:

$databases['default']['default'] = array (
  'namespace' => 'Drupal\\Driver\\Database\\mysql',
  'driver' => 'mysql',
  ...
);

which works as expected.

However, there are two places where the above namespace from the above connection info array is not checked:

  1. In db_installer_object(), used by the installer before settings.php has been written.
  2. In Database::convertDbUrlToConnectionInfo(), which can be called from places that only have a db URL and not a connection info array. In core, that's just tests and console commands.

In these two places, HEAD currently checks the core namespace first, and only checks the custom namespace for drivers that aren't in core. This would prevent, for example, a custom MySQL driver from being used during installation and tests.

Proposed resolution

Change the above two places to check for a driver in the custom namespace first, and then fall back to the core namespace.

Remaining tasks

User interface changes

API changes

Data model changes

Release notes snippet

Comments

effulgentsia created an issue. See original summary.

effulgentsia’s picture

Here's 3 patches...

  • The 1st is just an example custom MySQL driver that overrides MYSQL_MINIMUM_VERSION to 5.6. It should pass tests on MySQL 5.6, except the problems identified in the issue summary prevent that.
  • The 2nd is the same example driver along with the two core fixes. It should pass tests on MySQL 5.6.
  • The 3rd is just the core fixes without the example driver, since we don't want to commit an actual custom driver to core.

The last submitted patch, 2: db-prioritize-custom-driver-3118832-with-example-driver.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

effulgentsia’s picture

StatusFileSize
new4.8 KB
new1.99 KB

Success! The 1 failure in the db-prioritize-custom-driver-3118832-with-example-driver patch is the test that explicitly checks that the mysql driver resolves to the core namespace, and that is the exact behavior that we want to change in this issue. However, it's a behavior change that's only surfaced when there's a driver with the same name in the custom namespace. So I don't think that we need to change the test, since we're not planning to commit the custom driver to core's codebase. The fact that there are no other failures on 5.6 is what matters here.

Meanwhile, db-prioritize-custom-driver-3118832.patch itself passes tests (on 5.7 only, since that's what's required by the core driver).

Here's an added unit test case for the URL conversion test to ensure that the custom namespace takes precedence.

We might want to also add a unit test for db_installer_object() since that's being modified by this patch, but I'm not sure how best to do that yet.

xjm’s picture

Status: Needs review » Needs work
Issue tags: +Needs tests

We need some test coverage here.

xjm’s picture

Version: 9.0.x-dev » 8.9.x-dev
larowlan’s picture

+++ b/core/includes/install.inc
@@ -1087,12 +1087,12 @@ function install_profile_info($profile, $langcode = 'en') {
+  $task_class = "Drupal\\Driver\\Database\\{$driver}\\Install\\Tasks";
   if (class_exists($task_class)) {

+++ b/core/lib/Drupal/Core/Database/Database.php
@@ -457,14 +457,14 @@ public static function convertDbUrlToConnectionInfo($url, $root) {
-    // Discover if the URL has a valid driver scheme. Try with core drivers
-    // first.
-    $connection_class = "Drupal\\Core\\Database\\Driver\\{$driver}\\Connection";
+    // Discover if the URL has a valid driver scheme. Try with custom drivers
+    // first, since those can override/extend the core ones.

is it worth abstracting this logic to a helper method somewhere?

Other than that +1 on the idea, and +1 for tests

effulgentsia’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests
StatusFileSize
new8.76 KB
new3.22 KB

#4 already had tests for the change to convertDbUrlToConnectionInfo(). This now also adds tests for db_installer_object(). I considered moving that code into a class, so that the new unit test would test a class rather than a global function, but decided against it because that would bring us to...

is it worth abstracting this logic to a helper method somewhere?

Yes, but let's do that in a followup, because we'll need to pick a name for that helper method. The obvious choice would be Database::getDriverClass() except that might cause confusion with Database::getConnection()->getDriverClass(), so worth some discussion whether that parity would be good or bad.

effulgentsia’s picture

StatusFileSize
new8.66 KB
new537 bytes

Removing a copy/paste leftover.

The last submitted patch, 8: db-prioritize-custom-driver-3118832-8.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Status: Needs review » Needs work

The last submitted patch, 9: db-prioritize-custom-driver-3118832-9.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

effulgentsia’s picture

Status: Needs work » Needs review
StatusFileSize
new9.58 KB
new3.57 KB

Yay, the new test added in #3118477: RegistryTest, RegistryLegacyTest both define the same class, use mock instead did its job. This fixes that test failure, and also the coding standards violations.

alexpott’s picture

Status: Needs review » Needs work
  1. +++ b/core/tests/Drupal/Tests/Core/Database/InstallerObjectTest.php
    @@ -0,0 +1,62 @@
    +  /**
    +   * {@inheritdoc}
    +   */
    +  protected function setUp() {
    +    parent::setUp();
    +    require_once __DIR__ . '/../../../../../includes/install.inc';
    +    $additional_class_loader = new ClassLoader();
    +    $additional_class_loader->addPsr4("Drupal\\Driver\\Database\\fake\\", __DIR__ . "/fixtures/driver/fake");
    +    $additional_class_loader->addPsr4("Drupal\\Core\\Database\\Driver\\corefake\\", __DIR__ . "/fixtures/core-driver/corefake");
    +    $additional_class_loader->addPsr4("Drupal\\Driver\\Database\\corefake\\", __DIR__ . "/fixtures/driver/corefake");
    +    $additional_class_loader->register(TRUE);
    +  }
    

    This will affect all the other unit tests that run afterwards. Well not on DrupalCI because that runs all tests in their own process but as this is very much futzing with classloading it might be a good idea to always run this test in s separate process - there's an annotation for this - @runTestsInSeparateProcesses - see UrlConversionTest for an example.

  2. +++ b/core/tests/Drupal/Tests/Core/Database/InstallerObjectTest.php
    @@ -0,0 +1,62 @@
    +    return [
    +      // A driver only in the core namespace.
    +      ['mysql', MysqlInstallTasks::class],
    +
    +      // A driver only in the custom namespace.
    ...
    +
    +      // A driver in both namespaces. The custom one takes precedence.
    +      ['corefake', CustomCoreFakeInstallTasks::class],
    +    ];
    

    If we hard coded the class name I don't think we wouldn't need the ugly require_once in the classes.

effulgentsia’s picture

Status: Needs work » Needs review
StatusFileSize
new9.75 KB
new668 bytes

This addresses #13.1. For #13.2, hard coding the class names wouldn't remove the need for require_once, because that's for when PHPUnit performs its --list-tests scanning, where it loads the fixture classes apart from running the test itself. What would prevent the need for require_once is if there's a way to tell PHPUnit to not scan for tests inside of fixture directories.

alexpott’s picture

@effulgentsia ah I hadn't spotted that. That's why core/tests/fixtures/ exists.

effulgentsia’s picture

StatusFileSize
new9.48 KB
new6.64 KB

Nice! This moves the fixtures into there.

Because a bunch of file moves are involved, I'm attaching a git increment instead of an interdiff.

daffie’s picture

Can we all stop for a moment and take a look at the bigger picture, regarding the workflow of contrib database drivers.
Before I start with that, I would like to thank everybody who is taking an interest in improving contrib database drivers very very much. It is really appreciated! :)

Now for that bigger picture

There are 2 kinds of contrib database drivers:
- the first has only a driver directory. Lets call them type A;
- the second one has a driver directory and a module part. Lets call those type B.

I am working on creating a fallback driver for PostgreSQL and lets take that as an example. At the moment it only has a driver directory and therefor its a type A. When the by core supported driver for PostgreSQL starts using the pg_trgm extension, the contrib driver will need to undo that and therefor needs a module part and becomes a contrib driver of type B.

The workflow of installing a contrib database driver

Installing the contrib driver is as follows:
1. Download/require the contrib driver;
2. Copy the contents of the "drivers" directory from the module to the root of the Drupal project/installation.
3. If the contrib driver has a module part, the module must be enabled.

I am not sure when working with composer if this can be automated from the contrib driver or does the site builder/owner has to use scaffolding in their composer.json to copy the "drivers" directory them self. This is how it works now. Is this how we want it to be?

The workflow of updating a contrib database driver

Updating the contrib driver is as follows:
1. Downloading the updated contrib driver;
2. Copy the contents of the "drivers" directory from the module to the root of the Drupal project/installation and replacing the existing one.
3. Running the site updates script.

Site builders/owner are not used to coping files after they have run the command composer update. If they forget they will still be working with the old version of the driver directory. If they have a type B contrib driver they will be using 2 different versions. One for the driver directory and one for the module part of the contrib driver. If a site builder/owner runs the update script before copying the update to the "drivers" directory, a lot can go wrong. Is this how we want it to be?

The workflow of moving from using a contrib driver to using a by core supported driver

Moving to using the by core supported driver (with the current patch):
1. Running the update process that will be provided by the contrib driver (creating the pg_trgm indexes);
2. Deleting the contents of the "drivers" directory in the root of the Drupal project/installation;
3. Disabling the contrib module (if type B).

Testing the transition is for a site builder/owner not very easy to do.
If somebody wants to know if a site is using a contrib driver, the only way to check for that is looking if the "drivers" directory in the root of the Drupal project/installation is used or not. The driver setting in the settings.php will be the same for contrib and the by core supported drivers.

Moving to using the by core supported driver (without the current patch):
1. Running the update process that will be provided by the contrib driver (creating the pg_trgm indexes);
2. Change the driver setting from "pgsql-fallback" to "pgsql" in the settings.php file.

Can we take a look at the big picture of how we want the workflow to be for contrib database drivers before we start implementing it. Again a very big thank you to everybody who is taking an interest in contrib database drivers!

alexpott’s picture

@daffie +1 (and a bit more). The whole process of registering and moving code about for contrib db drivers is extremely convoluted. It's driven by the use of magic driver namespaces - ie. Drupal\\Driver\\Database\\{$driver}\\Connection and you're 100% right that the whole process is very clunky and fraught with possible ways to break. For example the update process in #17 will is likely fail if the contrib driver removes a file.

I think we need to come up with a method that doesn't require anyone to move code about anywhere. #3112476: Always set $info['namespace'] on database connection info is going to set a namespace on all connections including core and think this is the start of this. Once we do that we need away of providing the namespace in the URL instead of the driver. And simple instructions for how to add the contrib db driver to the autoloader in settings.php.

alexpott’s picture

That said I don't think that #17 and #18 block this issue. Is only that looking at this issue makes us aware of just how painful all of this is.

I think the changes here are okay and will help us provide BC drivers in the short term but we should also continue to work on making things much easier for contrib drivers.

effulgentsia’s picture

Thanks for the detail in #17!

First of all, I have no objection to some contrib drivers that extend core ones from using a different name. There might be great reasons for pgsql-fallback to be named that.

But I want to make sure that it's also ok for contrib drivers to choose to use the same name. I created https://www.drupal.org/project/mysql56 and that's what I'm doing so far there. I named the d.o. project mysql56 for clarity on d.o., but the driver name is still mysql.

Moving to using the by core supported driver (without the current patch):
1. Running the update process that will be provided by the contrib driver (creating the pg_trgm indexes);
2. Change the driver setting from "pgsql-fallback" to "pgsql" in the settings.php file.

Except that's not all you'd need to do. If you installed pgsql-fallback to begin with, then the installer added:

'namespace' => 'Drupal\Driver\Database\pgsql_fallback',

into your settings.php. So for step 2, you'd also need to remove that line. Which means you're changing two values in settings.php: 'driver' and 'namespace'.

Whereas if the driver is named the same (e.g., 'mysql'), then you only need to change/remove the 'namespace' value.

But that's not the only benefit of having the same name. Another one is that on the installer screen you're presented with a single choice for "MySQL" instead of having to choose between two different MySQL options, one of which (the core one) will give you an error screen if you choose it (assuming that if you added the mysql56 project to your codebase, it's because you're on MySQL 5.6).

This issue doesn't change any behavior in regards to how $databases['default']['default'] from settings.php is interpreted, which means it doesn't change the runtime behavior of Drupal requests that use that for connecting to the database. With or without this patch, you can toggle 'namespace' between 'Drupal\Driver\Database\mysql' and 'Drupal\Core\Database\Driver\mysql', and the driver from the corresponding namespace will get used for that request.

What this patch does is just give the custom one precedence during installation (prior to settings.php existing) and during console commands and tests where a db_url is provided instead of a settings.php connection info array.

I think the rest of #17 is pointing out how cumbersome it is to have to place custom drivers in the particular directory that they need to go in.

I am not sure when working with composer if this can be automated

It can. That's what composer/installers is for. But we need to register a new type in that project, which I opened a PR for, and we need to register the path for that type in our Composer project templates, which I opened #3119853: Add database driver installer path to Composer project templates for. With that, composer require drupal/mysql56 will put that driver into the correct directory, composer update will update it in that directory with whatever the latest code is, and composer remove drupal/mysql56 will remove it from that directory. Basically all workflow is then the same as for modules, just that ones whose composer.json specifies "type": "drupal-database-driver" go into the correct directory for drivers instead of the directory for modules.

the second one has a driver directory and a module part. Lets call those type B.

These won't be able to use "type": "drupal-database-driver" alone, since now you have code that needs to go into two different places: drivers and modules. There's a couple ways this could be handled: one is you can make two different projects: pgsql_fallback and pgsql_fallback_module. The first can specify the second as a dependency (or vice versa) in its composer.json. Or if you really want to keep them in the same project, then another way is you can create a custom installer.

a very big thank you to everybody who is taking an interest in contrib database drivers!

And a big thank you to you as well for that!

alexpott’s picture

@effulgentsia there’s no need for another composer installers Drupal type. I scoped out a plan with @daffie a couple of days ago and he’s going to put it in a new issue as I don’t think any of this needs to block us here.

effulgentsia’s picture

I scoped out a plan with @daffie a couple of days ago and he’s going to put it in a new issue

Great! I look forward to seeing it.

I don’t think any of this needs to block us here

Great! Given that, I created a draft CR: https://www.drupal.org/node/3119933.

daffie’s picture

Status: Needs review » Reviewed & tested by the community

All the code changes look good to me.
For all code changes there is testing.
All the comment changes also look in order.
The moving of the fixture is to me also a good idea.
The change record is there.
For me it is RTBC.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Re #7 I don't think so. @daffie and I are working on a new way to autoload DB drivers that will mean that custom and contrib drivers no longer need to move code and it's likely this code will be refactored further so let's not preempt that work. @daffie hasn't yet (as far as I know created an issue on d.o for this work but that will happen in time).

Committed and pushed c4f0d96291 to 9.0.x and c3b1069774 to 8.9.x. Thanks!

  • alexpott committed c4f0d96 on 9.0.x
    Issue #3118832 by effulgentsia, alexpott, daffie: Allow custom database...

  • alexpott committed c3b1069 on 8.9.x
    Issue #3118832 by effulgentsia, alexpott, daffie: Allow custom database...
daffie’s picture

Status: Fixed » Closed (fixed)

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