From 934f42ae8706419708d2d0072520554a353b8964 Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Wed, 29 Dec 2021 17:14:14 +0000
Subject: [PATCH] Issue #3129043 by mondrake, daffie, ankithashetty,
 ravi.shankar, alexpott, Beakerboy, catch: Move core database drivers to
 modules of their own

---
 composer.lock                                 |    8 +-
 .../scaffold/files/default.settings.php       |    6 +-
 core/composer.json                            |    6 +-
 core/includes/install.inc                     |    7 +-
 .../lib/Drupal/Core/Command/DbDumpCommand.php |    2 +-
 core/lib/Drupal/Core/Database/Database.php    |   54 +-
 .../Core/Database/Driver/mysql/Connection.php |  495 +-------
 .../Driver/mysql/ExceptionHandler.php         |   60 +-
 .../Core/Database/Driver/mysql/Insert.php     |   66 +-
 .../Database/Driver/mysql/Install/Tasks.php   |  215 +---
 .../Core/Database/Driver/mysql/Schema.php     |  714 +----------
 .../Core/Database/Driver/mysql/Upsert.php     |   44 +-
 .../Core/Database/Driver/pgsql/Connection.php |  374 +-----
 .../Core/Database/Driver/pgsql/Delete.php     |   30 +-
 .../Core/Database/Driver/pgsql/Insert.php     |  157 +--
 .../Database/Driver/pgsql/Install/Tasks.php   |  295 +----
 .../Core/Database/Driver/pgsql/Schema.php     | 1083 +---------------
 .../Core/Database/Driver/pgsql/Select.php     |  159 +--
 .../Core/Database/Driver/pgsql/Truncate.php   |   30 +-
 .../Core/Database/Driver/pgsql/Update.php     |   84 +-
 .../Core/Database/Driver/pgsql/Upsert.php     |  125 +-
 .../Database/Driver/sqlite/Connection.php     |  529 +-------
 .../Core/Database/Driver/sqlite/Insert.php    |   51 +-
 .../Database/Driver/sqlite/Install/Tasks.php  |  116 +-
 .../Core/Database/Driver/sqlite/Schema.php    |  836 +------------
 .../Core/Database/Driver/sqlite/Select.php    |   18 +-
 .../Core/Database/Driver/sqlite/Statement.php |  150 +--
 .../Core/Database/Driver/sqlite/Truncate.php  |   21 +-
 .../Core/Database/Driver/sqlite/Upsert.php    |   46 +-
 .../Core/Database/StatementInterface.php      |    8 +-
 .../Core/Database/StatementPrefetch.php       |    2 +-
 core/lib/Drupal/Core/Site/Settings.php        |   37 +
 .../Core/Test/FunctionalTestSetupTrait.php    |    1 +
 .../src/Unit/CommentStatisticsUnitTest.php    |    2 +-
 .../src/Functional/ConfigImportAllTest.php    |    8 +-
 .../src/Kernel/MigrateSqlSourceTestBase.php   |    4 +-
 .../tests/src/Kernel/QueryBatchTest.php       |    4 +-
 .../tests/src/Unit/MigrateSqlIdMapTest.php    |    4 +-
 .../tests/src/Unit/MigrateTestCase.php        |    4 +-
 core/modules/mysql/mysql.info.yml             |    5 +
 core/modules/mysql/mysql.module               |   22 +
 .../src/Driver/Database/mysql/Connection.php  |  495 ++++++++
 .../Database/mysql/ExceptionHandler.php       |   59 +
 .../src/Driver/Database/mysql/Insert.php      |   65 +
 .../Driver/Database/mysql/Install/Tasks.php   |  214 ++++
 .../src/Driver/Database/mysql/Schema.php      |  715 +++++++++++
 .../src/Driver/Database/mysql/Upsert.php      |   43 +
 core/modules/pgsql/pgsql.info.yml             |    5 +
 core/modules/pgsql/pgsql.module               |   22 +
 .../src/Driver/Database/pgsql/Connection.php  |  375 ++++++
 .../src/Driver/Database/pgsql/Delete.php      |   29 +
 .../src/Driver/Database/pgsql/Insert.php      |  158 +++
 .../Driver/Database/pgsql/Install/Tasks.php   |  294 +++++
 .../src/Driver/Database/pgsql/Schema.php      | 1084 +++++++++++++++++
 .../src/Driver/Database/pgsql/Select.php      |  160 +++
 .../src/Driver/Database/pgsql/Truncate.php    |   29 +
 .../src/Driver/Database/pgsql/Update.php      |   83 ++
 .../src/Driver/Database/pgsql/Upsert.php      |  126 ++
 core/modules/sqlite/sqlite.info.yml           |    5 +
 core/modules/sqlite/sqlite.module             |   22 +
 .../src/Driver/Database/sqlite/Connection.php |  528 ++++++++
 .../src/Driver/Database/sqlite/Insert.php     |   52 +
 .../Driver/Database/sqlite/Install/Tasks.php  |  115 ++
 .../src/Driver/Database/sqlite/Schema.php     |  837 +++++++++++++
 .../src/Driver/Database/sqlite/Select.php     |   17 +
 .../src/Driver/Database/sqlite/Statement.php  |  151 +++
 .../src/Driver/Database/sqlite/Truncate.php   |   22 +
 .../src/Driver/Database/sqlite/Upsert.php     |   47 +
 core/modules/system/system.install            |   18 +-
 core/modules/system/system.post_update.php    |   27 +
 .../src/mysql/Connection.php                  |    2 +-
 .../src/mysql/Install/Tasks.php               |    2 +-
 .../src/pgsql/Connection.php                  |    2 +-
 .../src/pgsql/Install/Tasks.php               |    2 +-
 .../src/sqlite/Connection.php                 |    2 +-
 .../src/sqlite/Install/Tasks.php              |    2 +-
 .../database_test/database_test.install       |    2 +-
 .../Database/DrivertestMysql/Connection.php   |    4 +-
 .../Database/DrivertestMysql/Insert.php       |    4 +-
 .../DrivertestMysql/Install/Tasks.php         |    4 +-
 .../Database/DrivertestMysql/Schema.php       |    4 +-
 .../Database/DrivertestMysql/Upsert.php       |    4 +-
 .../Connection.php                            |    4 +-
 .../Insert.php                                |    4 +-
 .../Install/Tasks.php                         |    4 +-
 .../Schema.php                                |    4 +-
 .../Upsert.php                                |    4 +-
 .../Database/DrivertestPgsql/Connection.php   |    4 +-
 .../Database/DrivertestPgsql/Delete.php       |    4 +-
 .../Database/DrivertestPgsql/Insert.php       |    4 +-
 .../DrivertestPgsql/Install/Tasks.php         |    4 +-
 .../Database/DrivertestPgsql/Schema.php       |    4 +-
 .../Database/DrivertestPgsql/Select.php       |    4 +-
 .../Database/DrivertestPgsql/Truncate.php     |    4 +-
 .../Database/DrivertestPgsql/Update.php       |    4 +-
 .../Database/DrivertestPgsql/Upsert.php       |    4 +-
 ...UpdateEnableProviderDatabaseDriverTest.php |   45 +
 .../Plugin/views/argument/StringArgument.php  |    2 +-
 .../src/Plugin/views/query/SqliteDateSql.php  |    2 +-
 .../Framework/Tests/HtRouterTest.php          |    2 +-
 ...yleDatabaseConnectionInSettingsPhpTest.php |   59 +
 .../Installer/InstallerTest.php               |   29 +
 .../Core/Database/MysqlDriverLegacyTest.php   |   88 ++
 .../KernelTests/Core/Database/NextIdTest.php  |    4 +-
 .../Core/Database/PgsqlDriverLegacyTest.php   |  115 ++
 .../Core/Database/PrefixInfoTest.php          |    2 +-
 .../KernelTests/Core/Database/QueryTest.php   |    2 +-
 .../KernelTests/Core/Database/SchemaTest.php  |    8 +-
 .../Core/Database/SelectComplexTest.php       |    2 +-
 .../Core/Database/SqliteDriverLegacyTest.php  |  105 ++
 .../Core/Database/TransactionTest.php         |    2 +-
 .../Drupal/KernelTests/KernelTestBaseTest.php |   10 +
 .../Tests/Core/Command/GenerateThemeTest.php  |    2 +-
 .../Tests/Core/Command/QuickStartTest.php     |    2 +-
 .../Tests/Core/Database/DatabaseTest.php      |    2 +-
 .../Database/Driver/mysql/ConnectionTest.php  |    6 +-
 .../Driver/mysql/install/TasksTest.php        |   14 +-
 .../Driver/pgsql/PostgresqlSchemaTest.php     |    8 +-
 .../Database/Driver/sqlite/ConnectionTest.php |    4 +-
 .../Core/Database/InstallerObjectTest.php     |    4 +-
 .../Tests/Core/Database/UrlConversionTest.php |   95 +-
 .../Compiler/BackendCompilerPassTest.php      |    2 +-
 .../Tests/Core/Test/TestSetupTraitTest.php    |    5 +-
 sites/default/default.settings.php            |    6 +-
 124 files changed, 6742 insertions(+), 5654 deletions(-)
 create mode 100644 core/modules/mysql/mysql.info.yml
 create mode 100644 core/modules/mysql/mysql.module
 create mode 100644 core/modules/mysql/src/Driver/Database/mysql/Connection.php
 create mode 100644 core/modules/mysql/src/Driver/Database/mysql/ExceptionHandler.php
 create mode 100644 core/modules/mysql/src/Driver/Database/mysql/Insert.php
 create mode 100644 core/modules/mysql/src/Driver/Database/mysql/Install/Tasks.php
 create mode 100644 core/modules/mysql/src/Driver/Database/mysql/Schema.php
 create mode 100644 core/modules/mysql/src/Driver/Database/mysql/Upsert.php
 create mode 100644 core/modules/pgsql/pgsql.info.yml
 create mode 100644 core/modules/pgsql/pgsql.module
 create mode 100644 core/modules/pgsql/src/Driver/Database/pgsql/Connection.php
 create mode 100644 core/modules/pgsql/src/Driver/Database/pgsql/Delete.php
 create mode 100644 core/modules/pgsql/src/Driver/Database/pgsql/Insert.php
 create mode 100644 core/modules/pgsql/src/Driver/Database/pgsql/Install/Tasks.php
 create mode 100644 core/modules/pgsql/src/Driver/Database/pgsql/Schema.php
 create mode 100644 core/modules/pgsql/src/Driver/Database/pgsql/Select.php
 create mode 100644 core/modules/pgsql/src/Driver/Database/pgsql/Truncate.php
 create mode 100644 core/modules/pgsql/src/Driver/Database/pgsql/Update.php
 create mode 100644 core/modules/pgsql/src/Driver/Database/pgsql/Upsert.php
 create mode 100644 core/modules/sqlite/sqlite.info.yml
 create mode 100644 core/modules/sqlite/sqlite.module
 create mode 100644 core/modules/sqlite/src/Driver/Database/sqlite/Connection.php
 create mode 100644 core/modules/sqlite/src/Driver/Database/sqlite/Insert.php
 create mode 100644 core/modules/sqlite/src/Driver/Database/sqlite/Install/Tasks.php
 create mode 100644 core/modules/sqlite/src/Driver/Database/sqlite/Schema.php
 create mode 100644 core/modules/sqlite/src/Driver/Database/sqlite/Select.php
 create mode 100644 core/modules/sqlite/src/Driver/Database/sqlite/Statement.php
 create mode 100644 core/modules/sqlite/src/Driver/Database/sqlite/Truncate.php
 create mode 100644 core/modules/sqlite/src/Driver/Database/sqlite/Upsert.php
 create mode 100644 core/modules/system/tests/src/Functional/Update/UpdateEnableProviderDatabaseDriverTest.php
 create mode 100644 core/tests/Drupal/FunctionalTests/ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest.php
 create mode 100644 core/tests/Drupal/KernelTests/Core/Database/MysqlDriverLegacyTest.php
 create mode 100644 core/tests/Drupal/KernelTests/Core/Database/PgsqlDriverLegacyTest.php
 create mode 100644 core/tests/Drupal/KernelTests/Core/Database/SqliteDriverLegacyTest.php

diff --git a/composer.lock b/composer.lock
index cf7affcdae79..ff85096117ac 100644
--- a/composer.lock
+++ b/composer.lock
@@ -452,7 +452,7 @@
             "dist": {
                 "type": "path",
                 "url": "core",
-                "reference": "3009c9eaa73ac4f9ff0d92a18714a2f9ac7a8877"
+                "reference": "fe303578f231198d09504af69ee768be7c455b06"
             },
             "require": {
                 "asm89/stack-cors": "^1.1",
@@ -583,12 +583,14 @@
                 "drupal/migrate_drupal_multilingual": "self.version",
                 "drupal/migrate_drupal_ui": "self.version",
                 "drupal/minimal": "self.version",
+                "drupal/mysql": "self.version",
                 "drupal/node": "self.version",
                 "drupal/olivero": "self.version",
                 "drupal/options": "self.version",
                 "drupal/page_cache": "self.version",
                 "drupal/path": "self.version",
                 "drupal/path_alias": "self.version",
+                "drupal/pgsql": "self.version",
                 "drupal/quickedit": "self.version",
                 "drupal/rdf": "self.version",
                 "drupal/responsive_image": "self.version",
@@ -598,6 +600,7 @@
                 "drupal/settings_tray": "self.version",
                 "drupal/seven": "self.version",
                 "drupal/shortcut": "self.version",
+                "drupal/sqlite": "self.version",
                 "drupal/standard": "self.version",
                 "drupal/stark": "self.version",
                 "drupal/statistics": "self.version",
@@ -668,9 +671,6 @@
                     "lib/Drupal/Core/Cache/DatabaseCacheTagsChecksum.php",
                     "lib/Drupal/Core/Database/Connection.php",
                     "lib/Drupal/Core/Database/Database.php",
-                    "lib/Drupal/Core/Database/Driver/mysql/Connection.php",
-                    "lib/Drupal/Core/Database/Driver/pgsql/Connection.php",
-                    "lib/Drupal/Core/Database/Driver/sqlite/Connection.php",
                     "lib/Drupal/Core/Database/Statement.php",
                     "lib/Drupal/Core/Database/StatementInterface.php",
                     "lib/Drupal/Core/DependencyInjection/Container.php",
diff --git a/core/assets/scaffold/files/default.settings.php b/core/assets/scaffold/files/default.settings.php
index 718890224b48..4768843bcf07 100644
--- a/core/assets/scaffold/files/default.settings.php
+++ b/core/assets/scaffold/files/default.settings.php
@@ -170,9 +170,9 @@
  * information on these defaults and the potential issues.
  *
  * More details can be found in the constructor methods for each driver:
- * - \Drupal\Core\Database\Driver\mysql\Connection::__construct()
- * - \Drupal\Core\Database\Driver\pgsql\Connection::__construct()
- * - \Drupal\Core\Database\Driver\sqlite\Connection::__construct()
+ * - \Drupal\mysql\Driver\Database\mysql\Connection::__construct()
+ * - \Drupal\pgsql\Driver\Database\pgsql\Connection::__construct()
+ * - \Drupal\sqlite\Driver\Database\sqlite\Connection::__construct()
  *
  * Sample Database configuration format for PostgreSQL (pgsql):
  * @code
diff --git a/core/composer.json b/core/composer.json
index a303372e2d33..fc487126854f 100644
--- a/core/composer.json
+++ b/core/composer.json
@@ -132,12 +132,14 @@
         "drupal/migrate_drupal": "self.version",
         "drupal/migrate_drupal_multilingual": "self.version",
         "drupal/migrate_drupal_ui": "self.version",
+        "drupal/mysql": "self.version",
         "drupal/node": "self.version",
         "drupal/olivero": "self.version",
         "drupal/options": "self.version",
         "drupal/page_cache": "self.version",
         "drupal/path": "self.version",
         "drupal/path_alias": "self.version",
+        "drupal/pgsql": "self.version",
         "drupal/quickedit": "self.version",
         "drupal/rdf": "self.version",
         "drupal/responsive_image": "self.version",
@@ -147,6 +149,7 @@
         "drupal/settings_tray": "self.version",
         "drupal/seven": "self.version",
         "drupal/shortcut": "self.version",
+        "drupal/sqlite": "self.version",
         "drupal/standard": "self.version",
         "drupal/stark": "self.version",
         "drupal/statistics": "self.version",
@@ -189,9 +192,6 @@
             "lib/Drupal/Core/Cache/DatabaseCacheTagsChecksum.php",
             "lib/Drupal/Core/Database/Connection.php",
             "lib/Drupal/Core/Database/Database.php",
-            "lib/Drupal/Core/Database/Driver/mysql/Connection.php",
-            "lib/Drupal/Core/Database/Driver/pgsql/Connection.php",
-            "lib/Drupal/Core/Database/Driver/sqlite/Connection.php",
             "lib/Drupal/Core/Database/Statement.php",
             "lib/Drupal/Core/Database/StatementInterface.php",
             "lib/Drupal/Core/DependencyInjection/Container.php",
diff --git a/core/includes/install.inc b/core/includes/install.inc
index dcb7ffa6911f..da4a89049f57 100644
--- a/core/includes/install.inc
+++ b/core/includes/install.inc
@@ -171,12 +171,13 @@ function drupal_get_database_types() {
   // The internal database driver name is any valid PHP identifier.
   $mask = ExtensionDiscovery::PHP_FUNCTION_PATTERN;
 
-  // Find drivers in the Drupal\Core and Drupal\Driver namespaces.
+  // Find drivers in the Drupal\Driver namespace.
+  // @todo remove discovering in the Drupal\Driver namespace in D10.
   /** @var \Drupal\Core\File\FileSystemInterface $file_system */
   $file_system = \Drupal::service('file_system');
-  $files = $file_system->scanDirectory(DRUPAL_ROOT . '/core/lib/Drupal/Core/Database/Driver', $mask, ['recurse' => FALSE]);
+  $files = [];
   if (is_dir(DRUPAL_ROOT . '/drivers/lib/Drupal/Driver/Database')) {
-    $files += $file_system->scanDirectory(DRUPAL_ROOT . '/drivers/lib/Drupal/Driver/Database/', $mask, ['recurse' => FALSE]);
+    $files = $file_system->scanDirectory(DRUPAL_ROOT . '/drivers/lib/Drupal/Driver/Database/', $mask, ['recurse' => FALSE]);
   }
   foreach ($files as $file) {
     if (file_exists($file->uri . '/Install/Tasks.php')) {
diff --git a/core/lib/Drupal/Core/Command/DbDumpCommand.php b/core/lib/Drupal/Core/Command/DbDumpCommand.php
index c1b467289546..6b2cc7f7029e 100644
--- a/core/lib/Drupal/Core/Command/DbDumpCommand.php
+++ b/core/lib/Drupal/Core/Command/DbDumpCommand.php
@@ -272,7 +272,7 @@ protected function getTableIndexes(Connection $connection, $table, &$definition)
    */
   protected function getTableCollation(Connection $connection, $table, &$definition) {
     // Remove identifier quotes from the table name. See
-    // \Drupal\Core\Database\Driver\mysql\Connection::$identifierQuotes.
+    // \Drupal\mysql\Driver\Database\mysql\Connection::$identifierQuotes.
     $table = trim($connection->prefixTables('{' . $table . '}'), '"');
     $query = $connection->query("SHOW TABLE STATUS WHERE NAME = :table_name", [':table_name' => $table]);
     $data = $query->fetchAssoc();
diff --git a/core/lib/Drupal/Core/Database/Database.php b/core/lib/Drupal/Core/Database/Database.php
index 2771166e48f9..2394c5eff6ef 100644
--- a/core/lib/Drupal/Core/Database/Database.php
+++ b/core/lib/Drupal/Core/Database/Database.php
@@ -238,7 +238,7 @@ final public static function parseConnectionInfo(array $info) {
 
     // Fallback for Drupal 7 settings.php if namespace is not provided.
     if (empty($info['namespace'])) {
-      $info['namespace'] = 'Drupal\\Core\\Database\\Driver\\' . $info['driver'];
+      $info['namespace'] = 'Drupal\\' . $info['driver'] . '\\Driver\\Database\\' . $info['driver'];
     }
 
     return $info;
@@ -465,38 +465,42 @@ public static function convertDbUrlToConnectionInfo($url, $root) {
     $driver = $matches[1];
 
     // Determine if the database driver is provided by a module.
+    // @todo https://www.drupal.org/project/drupal/issues/3250999. Refactor when
+    // all database drivers are provided by modules.
     $module = NULL;
     $connection_class = NULL;
     $url_components = parse_url($url);
-    if (isset($url_components['query'])) {
-      parse_str($url_components['query'], $query);
-      if (isset($query['module']) && $query['module']) {
-        $module = $query['module'];
-        // Set up an additional autoloader. We don't use the main autoloader as
-        // this method can be called before Drupal is installed and is never
-        // called during regular runtime.
-        $namespace = "Drupal\\$module\\Driver\\Database\\$driver";
-        $psr4_base_directory = Database::findDriverAutoloadDirectory($namespace, $root, TRUE);
-        $additional_class_loader = new ClassLoader();
-        $additional_class_loader->addPsr4($namespace . '\\', $psr4_base_directory);
-        $additional_class_loader->register(TRUE);
-        $connection_class = $custom_connection_class = $namespace . '\\Connection';
-      }
+    $url_component_query = $url_components['query'] ?? '';
+    parse_str($url_component_query, $query);
+
+    // Add the module key for core database drivers when the module key is not
+    // set.
+    if (!isset($query['module']) && in_array($driver, ['mysql', 'pgsql', 'sqlite'], TRUE)) {
+      $query['module'] = $driver;
+    }
+
+    if (isset($query['module']) && $query['module']) {
+      $module = $query['module'];
+      // Set up an additional autoloader. We don't use the main autoloader as
+      // this method can be called before Drupal is installed and is never
+      // called during regular runtime.
+      $namespace = "Drupal\\$module\\Driver\\Database\\$driver";
+      $psr4_base_directory = Database::findDriverAutoloadDirectory($namespace, $root, TRUE);
+      $additional_class_loader = new ClassLoader();
+      $additional_class_loader->addPsr4($namespace . '\\', $psr4_base_directory);
+      $additional_class_loader->register(TRUE);
+      $connection_class = $namespace . '\\Connection';
     }
 
     if (!$module) {
       // Determine the connection class to use. Discover if the URL has a valid
-      // driver scheme. Try with Drupal 8 style custom drivers first, since
-      // those can override/extend the core ones.
-      $connection_class = $custom_connection_class = "Drupal\\Driver\\Database\\{$driver}\\Connection";
-      if (!class_exists($connection_class)) {
-        // If the URL is not relative to a custom driver, try with core ones.
-        $connection_class = "Drupal\\Core\\Database\\Driver\\{$driver}\\Connection";
-      }
+      // driver scheme for a Drupal 8 style custom driver.
+      // @todo Remove this in Drupal 10.
+      $connection_class = "Drupal\\Driver\\Database\\{$driver}\\Connection";
     }
 
     if (!class_exists($connection_class)) {
-      throw new \InvalidArgumentException("Can not convert '$url' to a database connection, class '$custom_connection_class' does not exist");
+      throw new \InvalidArgumentException("Can not convert '$url' to a database connection, class '$connection_class' does not exist");
     }
 
     $options = $connection_class::createConnectionOptionsFromUrl($url, $root);
@@ -641,8 +645,8 @@ protected static function getDatabaseDriverNamespace(array $connection_info) {
     if (isset($connection_info['namespace'])) {
       return $connection_info['namespace'];
     }
-    // Fallback for Drupal 7 settings.php.
-    return 'Drupal\\Core\\Database\\Driver\\' . $connection_info['driver'];
+    // Fallback for when the namespace is not provided in settings.php.
+    return 'Drupal\\' . $connection_info['driver'] . '\\Driver\\Database\\' . $connection_info['driver'];
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php
index ef6027256ccc..8ae316c821a7 100644
--- a/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php
+++ b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php
@@ -2,495 +2,16 @@
 
 namespace Drupal\Core\Database\Driver\mysql;
 
-use Drupal\Core\Database\DatabaseAccessDeniedException;
-use Drupal\Core\Database\IntegrityConstraintViolationException;
-use Drupal\Core\Database\DatabaseExceptionWrapper;
-use Drupal\Core\Database\StatementInterface;
-use Drupal\Core\Database\StatementWrapper;
-use Drupal\Core\Database\Database;
-use Drupal\Core\Database\DatabaseNotFoundException;
-use Drupal\Core\Database\DatabaseException;
-use Drupal\Core\Database\Connection as DatabaseConnection;
-use Drupal\Core\Database\TransactionNoActiveException;
+use Drupal\mysql\Driver\Database\mysql\Connection as MysqlConnection;
 
-/**
- * @addtogroup database
- * @{
- */
+@trigger_error('\Drupal\Core\Database\Driver\mysql\Connection is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED);
 
 /**
  * MySQL implementation of \Drupal\Core\Database\Connection.
+ *
+ * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL
+ *   database driver has been moved to the mysql module.
+ *
+ * @see https://www.drupal.org/node/3129492
  */
-class Connection extends DatabaseConnection {
-
-  /**
-   * Error code for "Unknown database" error.
-   */
-  const DATABASE_NOT_FOUND = 1049;
-
-  /**
-   * Error code for "Access denied" error.
-   */
-  const ACCESS_DENIED = 1045;
-
-  /**
-   * Error code for "Can't initialize character set" error.
-   */
-  const UNSUPPORTED_CHARSET = 2019;
-
-  /**
-   * Driver-specific error code for "Unknown character set" error.
-   */
-  const UNKNOWN_CHARSET = 1115;
-
-  /**
-   * SQLSTATE error code for "Syntax error or access rule violation".
-   */
-  const SQLSTATE_SYNTAX_ERROR = 42000;
-
-  /**
-   * {@inheritdoc}
-   */
-  protected $statementClass = NULL;
-
-  /**
-   * {@inheritdoc}
-   */
-  protected $statementWrapperClass = StatementWrapper::class;
-
-  /**
-   * Flag to indicate if the cleanup function in __destruct() should run.
-   *
-   * @var bool
-   */
-  protected $needsCleanup = FALSE;
-
-  /**
-   * Stores the server version after it has been retrieved from the database.
-   *
-   * @var string
-   *
-   * @see \Drupal\Core\Database\Driver\mysql\Connection::version
-   */
-  private $serverVersion;
-
-  /**
-   * The minimal possible value for the max_allowed_packet setting of MySQL.
-   *
-   * @link https://mariadb.com/kb/en/mariadb/server-system-variables/#max_allowed_packet
-   * @link https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_max_allowed_packet
-   *
-   * @var int
-   */
-  const MIN_MAX_ALLOWED_PACKET = 1024;
-
-  /**
-   * {@inheritdoc}
-   */
-  protected $identifierQuotes = ['"', '"'];
-
-  /**
-   * {@inheritdoc}
-   */
-  public function __construct(\PDO $connection, array $connection_options) {
-    // If the SQL mode doesn't include 'ANSI_QUOTES' (explicitly or via a
-    // combination mode), then MySQL doesn't interpret a double quote as an
-    // identifier quote, in which case use the non-ANSI-standard backtick.
-    //
-    // Because we still support MySQL 5.7, check for the deprecated combination
-    // modes as well.
-    //
-    // @see https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_ansi_quotes
-    $ansi_quotes_modes = ['ANSI_QUOTES', 'ANSI', 'DB2', 'MAXDB', 'MSSQL', 'ORACLE', 'POSTGRESQL'];
-    $is_ansi_quotes_mode = FALSE;
-    foreach ($ansi_quotes_modes as $mode) {
-      // None of the modes in $ansi_quotes_modes are substrings of other modes
-      // that are not in $ansi_quotes_modes, so a simple stripos() does not
-      // return false positives.
-      if (stripos($connection_options['init_commands']['sql_mode'], $mode) !== FALSE) {
-        $is_ansi_quotes_mode = TRUE;
-        break;
-      }
-    }
-    if ($this->identifierQuotes === ['"', '"'] && !$is_ansi_quotes_mode) {
-      $this->identifierQuotes = ['`', '`'];
-    }
-    parent::__construct($connection, $connection_options);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function handleQueryException(\PDOException $e, $query, array $args = [], $options = []) {
-    // In case of attempted INSERT of a record with an undefined column and no
-    // default value indicated in schema, MySql returns a 1364 error code.
-    // Throw an IntegrityConstraintViolationException here like the other
-    // drivers do, to avoid the parent class to throw a generic
-    // DatabaseExceptionWrapper instead.
-    if (!empty($e->errorInfo[1]) && $e->errorInfo[1] === 1364) {
-      @trigger_error('Connection::handleQueryException() is deprecated in drupal:9.2.0 and is removed in drupal:10.0.0. Get a handler through $this->exceptionHandler() instead, and use one of its methods. See https://www.drupal.org/node/3187222', E_USER_DEPRECATED);
-      $query_string = ($query instanceof StatementInterface) ? $query->getQueryString() : $query;
-      $message = $e->getMessage() . ": " . $query_string . "; " . print_r($args, TRUE);
-      throw new IntegrityConstraintViolationException($message, is_int($e->getCode()) ? $e->getCode() : 0, $e);
-    }
-
-    parent::handleQueryException($e, $query, $args, $options);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function open(array &$connection_options = []) {
-    if (isset($connection_options['_dsn_utf8_fallback']) && $connection_options['_dsn_utf8_fallback'] === TRUE) {
-      // Only used during the installer version check, as a fallback from utf8mb4.
-      $charset = 'utf8';
-    }
-    else {
-      $charset = 'utf8mb4';
-    }
-    // The DSN should use either a socket or a host/port.
-    if (isset($connection_options['unix_socket'])) {
-      $dsn = 'mysql:unix_socket=' . $connection_options['unix_socket'];
-    }
-    else {
-      // Default to TCP connection on port 3306.
-      $dsn = 'mysql:host=' . $connection_options['host'] . ';port=' . (empty($connection_options['port']) ? 3306 : $connection_options['port']);
-    }
-    // Character set is added to dsn to ensure PDO uses the proper character
-    // set when escaping. This has security implications. See
-    // https://www.drupal.org/node/1201452 for further discussion.
-    $dsn .= ';charset=' . $charset;
-    if (!empty($connection_options['database'])) {
-      $dsn .= ';dbname=' . $connection_options['database'];
-    }
-    // Allow PDO options to be overridden.
-    $connection_options += [
-      'pdo' => [],
-    ];
-    $connection_options['pdo'] += [
-      \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
-      // So we don't have to mess around with cursors and unbuffered queries by default.
-      \PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => TRUE,
-      // Make sure MySQL returns all matched rows on update queries including
-      // rows that actually didn't have to be updated because the values didn't
-      // change. This matches common behavior among other database systems.
-      \PDO::MYSQL_ATTR_FOUND_ROWS => TRUE,
-      // Because MySQL's prepared statements skip the query cache, because it's dumb.
-      \PDO::ATTR_EMULATE_PREPARES => TRUE,
-      // Limit SQL to a single statement like mysqli.
-      \PDO::MYSQL_ATTR_MULTI_STATEMENTS => FALSE,
-      // Convert numeric values to strings when fetching. In PHP 8.1,
-      // \PDO::ATTR_EMULATE_PREPARES now behaves the same way as non emulated
-      // prepares and returns integers. See https://externals.io/message/113294
-      // for further discussion.
-      \PDO::ATTR_STRINGIFY_FETCHES => TRUE,
-    ];
-
-    try {
-      $pdo = new \PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
-    }
-    catch (\PDOException $e) {
-      if ($e->getCode() == static::DATABASE_NOT_FOUND) {
-        throw new DatabaseNotFoundException($e->getMessage(), $e->getCode(), $e);
-      }
-      if ($e->getCode() == static::ACCESS_DENIED) {
-        throw new DatabaseAccessDeniedException($e->getMessage(), $e->getCode(), $e);
-      }
-      throw $e;
-    }
-
-    // Force MySQL to use the UTF-8 character set. Also set the collation, if a
-    // certain one has been set; otherwise, MySQL defaults to
-    // 'utf8mb4_general_ci' (MySQL 5) or 'utf8mb4_0900_ai_ci' (MySQL 8) for
-    // utf8mb4.
-    if (!empty($connection_options['collation'])) {
-      $pdo->exec('SET NAMES ' . $charset . ' COLLATE ' . $connection_options['collation']);
-    }
-    else {
-      $pdo->exec('SET NAMES ' . $charset);
-    }
-
-    // Set MySQL init_commands if not already defined.  Default Drupal's MySQL
-    // behavior to conform more closely to SQL standards.  This allows Drupal
-    // to run almost seamlessly on many different kinds of database systems.
-    // These settings force MySQL to behave the same as postgresql, or sqlite
-    // in regards to syntax interpretation and invalid data handling.  See
-    // https://www.drupal.org/node/344575 for further discussion. Also, as MySQL
-    // 5.5 changed the meaning of TRADITIONAL we need to spell out the modes one
-    // by one.
-    $connection_options += [
-      'init_commands' => [],
-    ];
-
-    $connection_options['init_commands'] += [
-      'sql_mode' => "SET sql_mode = 'ANSI,TRADITIONAL'",
-    ];
-
-    // Execute initial commands.
-    foreach ($connection_options['init_commands'] as $sql) {
-      $pdo->exec($sql);
-    }
-
-    return $pdo;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function __destruct() {
-    if ($this->needsCleanup) {
-      $this->nextIdDelete();
-    }
-    parent::__destruct();
-  }
-
-  public function queryRange($query, $from, $count, array $args = [], array $options = []) {
-    return $this->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function queryTemporary($query, array $args = [], array $options = []) {
-    @trigger_error('Connection::queryTemporary() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. There is no replacement. See https://www.drupal.org/node/3211781', E_USER_DEPRECATED);
-    $tablename = $this->generateTemporaryTableName();
-    $this->query('CREATE TEMPORARY TABLE {' . $tablename . '} Engine=MEMORY ' . $query, $args, $options);
-    return $tablename;
-  }
-
-  public function driver() {
-    return 'mysql';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function version() {
-    if ($this->isMariaDb()) {
-      return $this->getMariaDbVersionMatch();
-    }
-
-    return $this->getServerVersion();
-  }
-
-  /**
-   * Determines whether the MySQL distribution is MariaDB or not.
-   *
-   * @return bool
-   *   Returns TRUE if the distribution is MariaDB, or FALSE if not.
-   */
-  public function isMariaDb(): bool {
-    return (bool) $this->getMariaDbVersionMatch();
-  }
-
-  /**
-   * Gets the MariaDB portion of the server version.
-   *
-   * @return string
-   *   The MariaDB portion of the server version if present, or NULL if not.
-   */
-  protected function getMariaDbVersionMatch(): ?string {
-    // MariaDB may prefix its version string with '5.5.5-', which should be
-    // ignored.
-    // @see https://github.com/MariaDB/server/blob/f6633bf058802ad7da8196d01fd19d75c53f7274/include/mysql_com.h#L42.
-    $regex = '/^(?:5\.5\.5-)?(\d+\.\d+\.\d+.*-mariadb.*)/i';
-
-    preg_match($regex, $this->getServerVersion(), $matches);
-    return (empty($matches[1])) ? NULL : $matches[1];
-  }
-
-  /**
-   * Gets the server version.
-   *
-   * @return string
-   *   The PDO server version.
-   */
-  protected function getServerVersion(): string {
-    if (!$this->serverVersion) {
-      $this->serverVersion = $this->connection->query('SELECT VERSION()')->fetchColumn();
-    }
-    return $this->serverVersion;
-  }
-
-  public function databaseType() {
-    return 'mysql';
-  }
-
-  /**
-   * Overrides \Drupal\Core\Database\Connection::createDatabase().
-   *
-   * @param string $database
-   *   The name of the database to create.
-   *
-   * @throws \Drupal\Core\Database\DatabaseNotFoundException
-   */
-  public function createDatabase($database) {
-    // Escape the database name.
-    $database = Database::getConnection()->escapeDatabase($database);
-
-    try {
-      // Create the database and set it as active.
-      $this->connection->exec("CREATE DATABASE $database");
-      $this->connection->exec("USE $database");
-    }
-    catch (\Exception $e) {
-      throw new DatabaseNotFoundException($e->getMessage());
-    }
-  }
-
-  public function mapConditionOperator($operator) {
-    // We don't want to override any of the defaults.
-    return NULL;
-  }
-
-  public function nextId($existing_id = 0) {
-    $new_id = $this->query('INSERT INTO {sequences} () VALUES ()', [], ['return' => Database::RETURN_INSERT_ID]);
-    // This should only happen after an import or similar event.
-    if ($existing_id >= $new_id) {
-      // If we INSERT a value manually into the sequences table, on the next
-      // INSERT, MySQL will generate a larger value. However, there is no way
-      // of knowing whether this value already exists in the table. MySQL
-      // provides an INSERT IGNORE which would work, but that can mask problems
-      // other than duplicate keys. Instead, we use INSERT ... ON DUPLICATE KEY
-      // UPDATE in such a way that the UPDATE does not do anything. This way,
-      // duplicate keys do not generate errors but everything else does.
-      $this->query('INSERT INTO {sequences} (value) VALUES (:value) ON DUPLICATE KEY UPDATE value = value', [':value' => $existing_id]);
-      $new_id = $this->query('INSERT INTO {sequences} () VALUES ()', [], ['return' => Database::RETURN_INSERT_ID]);
-    }
-    $this->needsCleanup = TRUE;
-    return $new_id;
-  }
-
-  public function nextIdDelete() {
-    // While we want to clean up the table to keep it up from occupying too
-    // much storage and memory, we must keep the highest value in the table
-    // because InnoDB uses an in-memory auto-increment counter as long as the
-    // server runs. When the server is stopped and restarted, InnoDB
-    // reinitializes the counter for each table for the first INSERT to the
-    // table based solely on values from the table so deleting all values would
-    // be a problem in this case. Also, TRUNCATE resets the auto increment
-    // counter.
-    try {
-      $max_id = $this->query('SELECT MAX(value) FROM {sequences}')->fetchField();
-      // We know we are using MySQL here, no need for the slower ::delete().
-      $this->query('DELETE FROM {sequences} WHERE value < :value', [':value' => $max_id]);
-    }
-    // During testing, this function is called from shutdown with the
-    // simpletest prefix stored in $this->connection, and those tables are gone
-    // by the time shutdown is called so we need to ignore the database
-    // errors. There is no problem with completely ignoring errors here: if
-    // these queries fail, the sequence will work just fine, just use a bit
-    // more database storage and memory.
-    catch (DatabaseException $e) {
-    }
-  }
-
-  /**
-   * Overridden to work around issues to MySQL not supporting transactional DDL.
-   */
-  protected function popCommittableTransactions() {
-    // Commit all the committable layers.
-    foreach (array_reverse($this->transactionLayers) as $name => $active) {
-      // Stop once we found an active transaction.
-      if ($active) {
-        break;
-      }
-
-      // If there are no more layers left then we should commit.
-      unset($this->transactionLayers[$name]);
-      if (empty($this->transactionLayers)) {
-        $this->doCommit();
-      }
-      else {
-        // Attempt to release this savepoint in the standard way.
-        try {
-          $this->query('RELEASE SAVEPOINT ' . $name);
-        }
-        catch (DatabaseExceptionWrapper $e) {
-          // However, in MySQL (InnoDB), savepoints are automatically committed
-          // when tables are altered or created (DDL transactions are not
-          // supported). This can cause exceptions due to trying to release
-          // savepoints which no longer exist.
-          //
-          // To avoid exceptions when no actual error has occurred, we silently
-          // succeed for MySQL error code 1305 ("SAVEPOINT does not exist").
-          if ($e->getPrevious()->errorInfo[1] == '1305') {
-            // If one SAVEPOINT was released automatically, then all were.
-            // Therefore, clean the transaction stack.
-            $this->transactionLayers = [];
-            // We also have to explain to PDO that the transaction stack has
-            // been cleaned-up.
-            $this->doCommit();
-          }
-          else {
-            throw $e;
-          }
-        }
-      }
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function rollBack($savepoint_name = 'drupal_transaction') {
-    // MySQL will automatically commit transactions when tables are altered or
-    // created (DDL transactions are not supported). Prevent triggering an
-    // exception to ensure that the error that has caused the rollback is
-    // properly reported.
-    if (!$this->connection->inTransaction()) {
-      // On PHP 7 $this->connection->inTransaction() will return TRUE and
-      // $this->connection->rollback() does not throw an exception; the
-      // following code is unreachable.
-
-      // If \Drupal\Core\Database\Connection::rollBack() would throw an
-      // exception then continue to throw an exception.
-      if (!$this->inTransaction()) {
-        throw new TransactionNoActiveException();
-      }
-      // A previous rollback to an earlier savepoint may mean that the savepoint
-      // in question has already been accidentally committed.
-      if (!isset($this->transactionLayers[$savepoint_name])) {
-        throw new TransactionNoActiveException();
-      }
-
-      trigger_error('Rollback attempted when there is no active transaction. This can cause data integrity issues.', E_USER_WARNING);
-      return;
-    }
-    return parent::rollBack($savepoint_name);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function doCommit() {
-    // MySQL will automatically commit transactions when tables are altered or
-    // created (DDL transactions are not supported). Prevent triggering an
-    // exception in this case as all statements have been committed.
-    if ($this->connection->inTransaction()) {
-      // On PHP 7 $this->connection->inTransaction() will return TRUE and
-      // $this->connection->commit() does not throw an exception.
-      $success = parent::doCommit();
-    }
-    else {
-      // Process the post-root (non-nested) transaction commit callbacks. The
-      // following code is copied from
-      // \Drupal\Core\Database\Connection::doCommit()
-      $success = TRUE;
-      if (!empty($this->rootTransactionEndCallbacks)) {
-        $callbacks = $this->rootTransactionEndCallbacks;
-        $this->rootTransactionEndCallbacks = [];
-        foreach ($callbacks as $callback) {
-          call_user_func($callback, $success);
-        }
-      }
-    }
-    return $success;
-  }
-
-}
-
-
-/**
- * @} End of "addtogroup database".
- */
+class Connection extends MysqlConnection {}
diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/ExceptionHandler.php b/core/lib/Drupal/Core/Database/Driver/mysql/ExceptionHandler.php
index 5d16c5b4f978..95c7be731f41 100644
--- a/core/lib/Drupal/Core/Database/Driver/mysql/ExceptionHandler.php
+++ b/core/lib/Drupal/Core/Database/Driver/mysql/ExceptionHandler.php
@@ -2,58 +2,16 @@
 
 namespace Drupal\Core\Database\Driver\mysql;
 
-use Drupal\Component\Utility\Unicode;
-use Drupal\Core\Database\DatabaseExceptionWrapper;
-use Drupal\Core\Database\ExceptionHandler as BaseExceptionHandler;
-use Drupal\Core\Database\IntegrityConstraintViolationException;
-use Drupal\Core\Database\StatementInterface;
+use Drupal\mysql\Driver\Database\mysql\ExceptionHandler as MysqlExceptionHandler;
+
+@trigger_error('\Drupal\Core\Database\Driver\mysql\ExceptionHandler is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED);
 
 /**
  * MySql database exception handler class.
+ *
+ * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL
+ *   database driver has been moved to the mysql module.
+ *
+ * @see https://www.drupal.org/node/3129492
  */
-class ExceptionHandler extends BaseExceptionHandler {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function handleExecutionException(\Exception $exception, StatementInterface $statement, array $arguments = [], array $options = []): void {
-    if (array_key_exists('throw_exception', $options)) {
-      @trigger_error('Passing a \'throw_exception\' option to ' . __METHOD__ . ' is deprecated in drupal:9.2.0 and is removed in drupal:10.0.0. Always catch exceptions. See https://www.drupal.org/node/3201187', E_USER_DEPRECATED);
-      if (!($options['throw_exception'])) {
-        return;
-      }
-    }
-
-    if ($exception instanceof \PDOException) {
-      // Wrap the exception in another exception, because PHP does not allow
-      // overriding Exception::getMessage(). Its message is the extra database
-      // debug information.
-      $code = is_int($exception->getCode()) ? $exception->getCode() : 0;
-
-      // If a max_allowed_packet error occurs the message length is truncated.
-      // This should prevent the error from recurring if the exception is logged
-      // to the database using dblog or the like.
-      if (($exception->errorInfo[1] ?? NULL) === 1153) {
-        $message = Unicode::truncateBytes($exception->getMessage(), Connection::MIN_MAX_ALLOWED_PACKET);
-        throw new DatabaseExceptionWrapper($message, $code, $exception);
-      }
-
-      $message = $exception->getMessage() . ": " . $statement->getQueryString() . "; " . print_r($arguments, TRUE);
-
-      // SQLSTATE 23xxx errors indicate an integrity constraint violation. Also,
-      // in case of attempted INSERT of a record with an undefined column and no
-      // default value indicated in schema, MySql returns a 1364 error code.
-      if (
-        substr($exception->getCode(), -6, -3) == '23' ||
-        ($exception->errorInfo[1] ?? NULL) === 1364
-      ) {
-        throw new IntegrityConstraintViolationException($message, $code, $exception);
-      }
-
-      throw new DatabaseExceptionWrapper($message, 0, $exception);
-    }
-
-    throw $exception;
-  }
-
-}
+class ExceptionHandler extends MysqlExceptionHandler {}
diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Insert.php b/core/lib/Drupal/Core/Database/Driver/mysql/Insert.php
index c65642aed0df..6fca6386e138 100644
--- a/core/lib/Drupal/Core/Database/Driver/mysql/Insert.php
+++ b/core/lib/Drupal/Core/Database/Driver/mysql/Insert.php
@@ -2,64 +2,16 @@
 
 namespace Drupal\Core\Database\Driver\mysql;
 
-use Drupal\Core\Database\Query\Insert as QueryInsert;
+use Drupal\mysql\Driver\Database\mysql\Insert as MysqlInsert;
+
+@trigger_error('\Drupal\Core\Database\Driver\mysql\Insert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED);
 
 /**
  * MySQL implementation of \Drupal\Core\Database\Query\Insert.
+ *
+ * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL
+ *   database driver has been moved to the mysql module.
+ *
+ * @see https://www.drupal.org/node/3129492
  */
-class Insert extends QueryInsert {
-
-  public function execute() {
-    if (!$this->preExecute()) {
-      return NULL;
-    }
-
-    // If we're selecting from a SelectQuery, finish building the query and
-    // pass it back, as any remaining options are irrelevant.
-    if (empty($this->fromQuery)) {
-      $max_placeholder = 0;
-      $values = [];
-      foreach ($this->insertValues as $insert_values) {
-        foreach ($insert_values as $value) {
-          $values[':db_insert_placeholder_' . $max_placeholder++] = $value;
-        }
-      }
-    }
-    else {
-      $values = $this->fromQuery->getArguments();
-    }
-
-    $last_insert_id = $this->connection->query((string) $this, $values, $this->queryOptions);
-
-    // Re-initialize the values array so that we can re-use this query.
-    $this->insertValues = [];
-
-    return $last_insert_id;
-  }
-
-  public function __toString() {
-    // Create a sanitized comment string to prepend to the query.
-    $comments = $this->connection->makeComment($this->comments);
-
-    // 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);
-
-    // If we're selecting from a SelectQuery, finish building the query and
-    // pass it back, as any remaining options are irrelevant.
-    if (!empty($this->fromQuery)) {
-      $insert_fields_string = $insert_fields ? ' (' . implode(', ', $insert_fields) . ') ' : ' ';
-      return $comments . 'INSERT INTO {' . $this->table . '}' . $insert_fields_string . $this->fromQuery;
-    }
-
-    $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
-
-    $values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
-    $query .= implode(', ', $values);
-
-    return $query;
-  }
-
-}
+class Insert extends MysqlInsert {}
diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php b/core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php
index dfac0158b79f..2cb93a9b4f6a 100644
--- a/core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php
+++ b/core/lib/Drupal/Core/Database/Driver/mysql/Install/Tasks.php
@@ -2,213 +2,16 @@
 
 namespace Drupal\Core\Database\Driver\mysql\Install;
 
-use Drupal\Core\Database\ConnectionNotDefinedException;
-use Drupal\Core\Database\Database;
-use Drupal\Core\Database\Install\Tasks as InstallTasks;
-use Drupal\Core\Database\Driver\mysql\Connection;
-use Drupal\Core\Database\DatabaseNotFoundException;
+use Drupal\mysql\Driver\Database\mysql\Install\Tasks as MysqlTasks;
+
+@trigger_error('\Drupal\Core\Database\Driver\mysql\Install\Tasks is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED);
 
 /**
  * Specifies installation tasks for MySQL and equivalent databases.
+ *
+ * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL
+ *   database driver has been moved to the mysql module.
+ *
+ * @see https://www.drupal.org/node/3129492
  */
-class Tasks extends InstallTasks {
-
-  /**
-   * Minimum required MySQL version.
-   *
-   * 5.7.8 is the minimum version that supports the JSON datatype.
-   * @see https://dev.mysql.com/doc/refman/5.7/en/json.html
-   */
-  const MYSQL_MINIMUM_VERSION = '5.7.8';
-
-  /**
-   * Minimum required MariaDB version.
-   *
-   * 10.3.7 is the first stable (GA) release in the 10.3 series.
-   * @see https://mariadb.com/kb/en/changes-improvements-in-mariadb-103/#list-of-all-mariadb-103-releases
-   */
-  const MARIADB_MINIMUM_VERSION = '10.3.7';
-
-  /**
-   * Minimum required MySQLnd version.
-   */
-  const MYSQLND_MINIMUM_VERSION = '5.0.9';
-
-  /**
-   * Minimum required libmysqlclient version.
-   */
-  const LIBMYSQLCLIENT_MINIMUM_VERSION = '5.5.3';
-
-  /**
-   * The PDO driver name for MySQL and equivalent databases.
-   *
-   * @var string
-   */
-  protected $pdoDriver = 'mysql';
-
-  /**
-   * Constructs a \Drupal\Core\Database\Driver\mysql\Install\Tasks object.
-   */
-  public function __construct() {
-    $this->tasks[] = [
-      'arguments' => [],
-      'function' => 'ensureInnoDbAvailable',
-    ];
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function name() {
-    try {
-      if (!$this->isConnectionActive() || !$this->getConnection() instanceof Connection) {
-        throw new ConnectionNotDefinedException('The database connection is not active or not a MySql connection');
-      }
-      if ($this->getConnection()->isMariaDb()) {
-        return $this->t('MariaDB');
-      }
-      return $this->t('MySQL, Percona Server, or equivalent');
-    }
-    catch (ConnectionNotDefinedException $e) {
-      return $this->t('MySQL, MariaDB, Percona Server, or equivalent');
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function minimumVersion() {
-    if ($this->getConnection()->isMariaDb()) {
-      return static::MARIADB_MINIMUM_VERSION;
-    }
-    return static::MYSQL_MINIMUM_VERSION;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function connect() {
-    try {
-      // This doesn't actually test the connection.
-      Database::setActiveConnection();
-      // Now actually do a check.
-      try {
-        Database::getConnection();
-      }
-      catch (\Exception $e) {
-        // Detect utf8mb4 incompatibility.
-        if ($e->getCode() == Connection::UNSUPPORTED_CHARSET || ($e->getCode() == Connection::SQLSTATE_SYNTAX_ERROR && $e->errorInfo[1] == Connection::UNKNOWN_CHARSET)) {
-          $this->fail(t('Your MySQL server and PHP MySQL driver must support utf8mb4 character encoding. Make sure to use a database system that supports this (such as MySQL/MariaDB/Percona 5.5.3 and up), and that the utf8mb4 character set is compiled in. See the <a href=":documentation" target="_blank">MySQL documentation</a> for more information.', [':documentation' => 'https://dev.mysql.com/doc/refman/5.0/en/cannot-initialize-character-set.html']));
-          $info = Database::getConnectionInfo();
-          $info_copy = $info;
-          // Set a flag to fall back to utf8. Note: this flag should only be
-          // used here and is for internal use only.
-          $info_copy['default']['_dsn_utf8_fallback'] = TRUE;
-          // In order to change the Database::$databaseInfo array, we need to
-          // remove the active connection, then re-add it with the new info.
-          Database::removeConnection('default');
-          Database::addConnectionInfo('default', 'default', $info_copy['default']);
-          // Connect with the new database info, using the utf8 character set so
-          // that we can run the checkEngineVersion test.
-          Database::getConnection();
-          // Revert to the old settings.
-          Database::removeConnection('default');
-          Database::addConnectionInfo('default', 'default', $info['default']);
-        }
-        else {
-          // Rethrow the exception.
-          throw $e;
-        }
-      }
-      $this->pass('Drupal can CONNECT to the database ok.');
-    }
-    catch (\Exception $e) {
-      // Attempt to create the database if it is not found.
-      if ($e->getCode() == Connection::DATABASE_NOT_FOUND) {
-        // Remove the database string from connection info.
-        $connection_info = Database::getConnectionInfo();
-        $database = $connection_info['default']['database'];
-        unset($connection_info['default']['database']);
-
-        // In order to change the Database::$databaseInfo array, need to remove
-        // the active connection, then re-add it with the new info.
-        Database::removeConnection('default');
-        Database::addConnectionInfo('default', 'default', $connection_info['default']);
-
-        try {
-          // Now, attempt the connection again; if it's successful, attempt to
-          // create the database.
-          Database::getConnection()->createDatabase($database);
-          Database::closeConnection();
-
-          // Now, restore the database config.
-          Database::removeConnection('default');
-          $connection_info['default']['database'] = $database;
-          Database::addConnectionInfo('default', 'default', $connection_info['default']);
-
-          // Check the database connection.
-          Database::getConnection();
-          $this->pass('Drupal can CONNECT to the database ok.');
-        }
-        catch (DatabaseNotFoundException $e) {
-          // Still no dice; probably a permission issue. Raise the error to the
-          // installer.
-          $this->fail(t('Database %database not found. The server reports the following message when attempting to create the database: %error.', ['%database' => $database, '%error' => $e->getMessage()]));
-        }
-      }
-      else {
-        // Database connection failed for some other reason than a non-existent
-        // database.
-        $this->fail(t('Failed to connect to your database server. The server reports the following message: %error.<ul><li>Is the database server running?</li><li>Does the database exist or does the database user have sufficient privileges to create the database?</li><li>Have you entered the correct database name?</li><li>Have you entered the correct username and password?</li><li>Have you entered the correct database hostname and port number?</li></ul>', ['%error' => $e->getMessage()]));
-        return FALSE;
-      }
-    }
-    return TRUE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormOptions(array $database) {
-    $form = parent::getFormOptions($database);
-    if (empty($form['advanced_options']['port']['#default_value'])) {
-      $form['advanced_options']['port']['#default_value'] = '3306';
-    }
-
-    return $form;
-  }
-
-  /**
-   * Ensure that InnoDB is available.
-   */
-  public function ensureInnoDbAvailable() {
-    $engines = Database::getConnection()->query('SHOW ENGINES')->fetchAllKeyed();
-    if (isset($engines['MyISAM']) && $engines['MyISAM'] == 'DEFAULT' && !isset($engines['InnoDB'])) {
-      $this->fail(t('The MyISAM storage engine is not supported.'));
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function checkEngineVersion() {
-    parent::checkEngineVersion();
-
-    // Ensure that the MySQL driver supports utf8mb4 encoding.
-    $version = Database::getConnection()->clientVersion();
-    if (FALSE !== strpos($version, 'mysqlnd')) {
-      // The mysqlnd driver supports utf8mb4 starting at version 5.0.9.
-      $version = preg_replace('/^\D+([\d.]+).*/', '$1', $version);
-      if (version_compare($version, self::MYSQLND_MINIMUM_VERSION, '<')) {
-        $this->fail(t("The MySQLnd driver version %version is less than the minimum required version. Upgrade to MySQLnd version %mysqlnd_minimum_version or up, or alternatively switch mysql drivers to libmysqlclient version %libmysqlclient_minimum_version or up.", ['%version' => $version, '%mysqlnd_minimum_version' => self::MYSQLND_MINIMUM_VERSION, '%libmysqlclient_minimum_version' => self::LIBMYSQLCLIENT_MINIMUM_VERSION]));
-      }
-    }
-    else {
-      // The libmysqlclient driver supports utf8mb4 starting at version 5.5.3.
-      if (version_compare($version, self::LIBMYSQLCLIENT_MINIMUM_VERSION, '<')) {
-        $this->fail(t("The libmysqlclient driver version %version is less than the minimum required version. Upgrade to libmysqlclient version %libmysqlclient_minimum_version or up, or alternatively switch mysql drivers to MySQLnd version %mysqlnd_minimum_version or up.", ['%version' => $version, '%libmysqlclient_minimum_version' => self::LIBMYSQLCLIENT_MINIMUM_VERSION, '%mysqlnd_minimum_version' => self::MYSQLND_MINIMUM_VERSION]));
-      }
-    }
-  }
-
-}
+class Tasks extends MysqlTasks {}
diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php b/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php
index 283035b835b7..ef9a0f12926f 100644
--- a/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php
+++ b/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php
@@ -2,714 +2,16 @@
 
 namespace Drupal\Core\Database\Driver\mysql;
 
-use Drupal\Core\Database\SchemaException;
-use Drupal\Core\Database\SchemaObjectExistsException;
-use Drupal\Core\Database\SchemaObjectDoesNotExistException;
-use Drupal\Core\Database\Schema as DatabaseSchema;
-use Drupal\Component\Utility\Unicode;
+use Drupal\mysql\Driver\Database\mysql\Schema as MysqlSchema;
 
-/**
- * @addtogroup schemaapi
- * @{
- */
+@trigger_error('\Drupal\Core\Database\Driver\mysql\Schema is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED);
 
 /**
  * MySQL implementation of \Drupal\Core\Database\Schema.
+ *
+ * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL
+ *   database driver has been moved to the mysql module.
+ *
+ * @see https://www.drupal.org/node/3129492
  */
-class Schema extends DatabaseSchema {
-
-  /**
-   * Maximum length of a table comment in MySQL.
-   */
-  const COMMENT_MAX_TABLE = 60;
-
-  /**
-   * Maximum length of a column comment in MySQL.
-   */
-  const COMMENT_MAX_COLUMN = 255;
-
-  /**
-   * @var array
-   *   List of MySQL string types.
-   */
-  protected $mysqlStringTypes = [
-    'VARCHAR',
-    'CHAR',
-    'TINYTEXT',
-    'MEDIUMTEXT',
-    'LONGTEXT',
-    'TEXT',
-  ];
-
-  /**
-   * Get information about the table and database name from the prefix.
-   *
-   * @return
-   *   A keyed array with information about the database, table name and prefix.
-   */
-  protected function getPrefixInfo($table = 'default', $add_prefix = TRUE) {
-    $info = ['prefix' => $this->connection->tablePrefix($table)];
-    if ($add_prefix) {
-      $table = $info['prefix'] . $table;
-    }
-    if (($pos = strpos($table, '.')) !== FALSE) {
-      $info['database'] = substr($table, 0, $pos);
-      $info['table'] = substr($table, ++$pos);
-    }
-    else {
-      $info['database'] = $this->connection->getConnectionOptions()['database'];
-      $info['table'] = $table;
-    }
-    return $info;
-  }
-
-  /**
-   * Build a condition to match a table name against a standard information_schema.
-   *
-   * MySQL uses databases like schemas rather than catalogs so when we build
-   * a condition to query the information_schema.tables, we set the default
-   * database as the schema unless specified otherwise, and exclude table_catalog
-   * from the condition criteria.
-   */
-  protected function buildTableNameCondition($table_name, $operator = '=', $add_prefix = TRUE) {
-    $table_info = $this->getPrefixInfo($table_name, $add_prefix);
-
-    $condition = $this->connection->condition('AND');
-    $condition->condition('table_schema', $table_info['database']);
-    $condition->condition('table_name', $table_info['table'], $operator);
-    return $condition;
-  }
-
-  /**
-   * Generate SQL to create a new table from a Drupal schema definition.
-   *
-   * @param $name
-   *   The name of the table to create.
-   * @param $table
-   *   A Schema API table definition array.
-   *
-   * @return
-   *   An array of SQL statements to create the table.
-   */
-  protected function createTableSql($name, $table) {
-    $info = $this->connection->getConnectionOptions();
-
-    // Provide defaults if needed.
-    $table += [
-      'mysql_engine' => 'InnoDB',
-      'mysql_character_set' => 'utf8mb4',
-    ];
-
-    $sql = "CREATE TABLE {" . $name . "} (\n";
-
-    // Add the SQL statement for each field.
-    foreach ($table['fields'] as $field_name => $field) {
-      $sql .= $this->createFieldSql($field_name, $this->processField($field)) . ", \n";
-    }
-
-    // Process keys & indexes.
-    if (!empty($table['primary key']) && is_array($table['primary key'])) {
-      $this->ensureNotNullPrimaryKey($table['primary key'], $table['fields']);
-    }
-    $keys = $this->createKeysSql($table);
-    if (count($keys)) {
-      $sql .= implode(", \n", $keys) . ", \n";
-    }
-
-    // Remove the last comma and space.
-    $sql = substr($sql, 0, -3) . "\n) ";
-
-    $sql .= 'ENGINE = ' . $table['mysql_engine'] . ' DEFAULT CHARACTER SET ' . $table['mysql_character_set'];
-    // By default, MySQL uses the default collation for new tables, which is
-    // 'utf8mb4_general_ci' (MySQL 5) or 'utf8mb4_0900_ai_ci' (MySQL 8) for
-    // utf8mb4. If an alternate collation has been set, it needs to be
-    // explicitly specified.
-    // @see \Drupal\Core\Database\Driver\mysql\Schema
-    if (!empty($info['collation'])) {
-      $sql .= ' COLLATE ' . $info['collation'];
-    }
-
-    // Add table comment.
-    if (!empty($table['description'])) {
-      $sql .= ' COMMENT ' . $this->prepareComment($table['description'], self::COMMENT_MAX_TABLE);
-    }
-
-    return [$sql];
-  }
-
-  /**
-   * Create an SQL string for a field to be used in table creation or alteration.
-   *
-   * @param string $name
-   *   Name of the field.
-   * @param array $spec
-   *   The field specification, as per the schema data structure format.
-   */
-  protected function createFieldSql($name, $spec) {
-    $sql = "`" . $name . "` " . $spec['mysql_type'];
-
-    if (in_array($spec['mysql_type'], $this->mysqlStringTypes)) {
-      if (isset($spec['length'])) {
-        $sql .= '(' . $spec['length'] . ')';
-      }
-      if (isset($spec['type']) && $spec['type'] == 'varchar_ascii') {
-        $sql .= ' CHARACTER SET ascii';
-      }
-      if (!empty($spec['binary'])) {
-        $sql .= ' BINARY';
-      }
-      // Note we check for the "type" key here. "mysql_type" is VARCHAR:
-      elseif (isset($spec['type']) && $spec['type'] == 'varchar_ascii') {
-        $sql .= ' COLLATE ascii_general_ci';
-      }
-    }
-    elseif (isset($spec['precision']) && isset($spec['scale'])) {
-      $sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')';
-    }
-
-    if (!empty($spec['unsigned'])) {
-      $sql .= ' unsigned';
-    }
-
-    if (isset($spec['not null'])) {
-      if ($spec['not null']) {
-        $sql .= ' NOT NULL';
-      }
-      else {
-        $sql .= ' NULL';
-      }
-    }
-
-    if (!empty($spec['auto_increment'])) {
-      $sql .= ' auto_increment';
-    }
-
-    // $spec['default'] can be NULL, so we explicitly check for the key here.
-    if (array_key_exists('default', $spec)) {
-      $sql .= ' DEFAULT ' . $this->escapeDefaultValue($spec['default']);
-    }
-
-    if (empty($spec['not null']) && !isset($spec['default'])) {
-      $sql .= ' DEFAULT NULL';
-    }
-
-    // Add column comment.
-    if (!empty($spec['description'])) {
-      $sql .= ' COMMENT ' . $this->prepareComment($spec['description'], self::COMMENT_MAX_COLUMN);
-    }
-
-    return $sql;
-  }
-
-  /**
-   * Set database-engine specific properties for a field.
-   *
-   * @param $field
-   *   A field description array, as specified in the schema documentation.
-   */
-  protected function processField($field) {
-
-    if (!isset($field['size'])) {
-      $field['size'] = 'normal';
-    }
-
-    // Set the correct database-engine specific datatype.
-    // In case one is already provided, force it to uppercase.
-    if (isset($field['mysql_type'])) {
-      $field['mysql_type'] = mb_strtoupper($field['mysql_type']);
-    }
-    else {
-      $map = $this->getFieldTypeMap();
-      $field['mysql_type'] = $map[$field['type'] . ':' . $field['size']];
-    }
-
-    if (isset($field['type']) && $field['type'] == 'serial') {
-      $field['auto_increment'] = TRUE;
-    }
-
-    return $field;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFieldTypeMap() {
-    // Put :normal last so it gets preserved by array_flip. This makes
-    // it much easier for modules (such as schema.module) to map
-    // database types back into schema types.
-    // $map does not use drupal_static as its value never changes.
-    static $map = [
-      'varchar_ascii:normal' => 'VARCHAR',
-
-      'varchar:normal'  => 'VARCHAR',
-      'char:normal'     => 'CHAR',
-
-      'text:tiny'       => 'TINYTEXT',
-      'text:small'      => 'TINYTEXT',
-      'text:medium'     => 'MEDIUMTEXT',
-      'text:big'        => 'LONGTEXT',
-      'text:normal'     => 'TEXT',
-
-      'serial:tiny'     => 'TINYINT',
-      'serial:small'    => 'SMALLINT',
-      'serial:medium'   => 'MEDIUMINT',
-      'serial:big'      => 'BIGINT',
-      'serial:normal'   => 'INT',
-
-      'int:tiny'        => 'TINYINT',
-      'int:small'       => 'SMALLINT',
-      'int:medium'      => 'MEDIUMINT',
-      'int:big'         => 'BIGINT',
-      'int:normal'      => 'INT',
-
-      'float:tiny'      => 'FLOAT',
-      'float:small'     => 'FLOAT',
-      'float:medium'    => 'FLOAT',
-      'float:big'       => 'DOUBLE',
-      'float:normal'    => 'FLOAT',
-
-      'numeric:normal'  => 'DECIMAL',
-
-      'blob:big'        => 'LONGBLOB',
-      'blob:normal'     => 'BLOB',
-    ];
-    return $map;
-  }
-
-  protected function createKeysSql($spec) {
-    $keys = [];
-
-    if (!empty($spec['primary key'])) {
-      $keys[] = 'PRIMARY KEY (' . $this->createKeySql($spec['primary key']) . ')';
-    }
-    if (!empty($spec['unique keys'])) {
-      foreach ($spec['unique keys'] as $key => $fields) {
-        $keys[] = 'UNIQUE KEY `' . $key . '` (' . $this->createKeySql($fields) . ')';
-      }
-    }
-    if (!empty($spec['indexes'])) {
-      $indexes = $this->getNormalizedIndexes($spec);
-      foreach ($indexes as $index => $fields) {
-        $keys[] = 'INDEX `' . $index . '` (' . $this->createKeySql($fields) . ')';
-      }
-    }
-
-    return $keys;
-  }
-
-  /**
-   * Gets normalized indexes from a table specification.
-   *
-   * Shortens indexes to 191 characters if they apply to utf8mb4-encoded
-   * fields, in order to comply with the InnoDB index limitation of 756 bytes.
-   *
-   * @param array $spec
-   *   The table specification.
-   *
-   * @return array
-   *   List of shortened indexes.
-   *
-   * @throws \Drupal\Core\Database\SchemaException
-   *   Thrown if field specification is missing.
-   */
-  protected function getNormalizedIndexes(array $spec) {
-    $indexes = $spec['indexes'] ?? [];
-    foreach ($indexes as $index_name => $index_fields) {
-      foreach ($index_fields as $index_key => $index_field) {
-        // Get the name of the field from the index specification.
-        $field_name = is_array($index_field) ? $index_field[0] : $index_field;
-        // Check whether the field is defined in the table specification.
-        if (isset($spec['fields'][$field_name])) {
-          // Get the MySQL type from the processed field.
-          $mysql_field = $this->processField($spec['fields'][$field_name]);
-          if (in_array($mysql_field['mysql_type'], $this->mysqlStringTypes)) {
-            // Check whether we need to shorten the index.
-            if ((!isset($mysql_field['type']) || $mysql_field['type'] != 'varchar_ascii') && (!isset($mysql_field['length']) || $mysql_field['length'] > 191)) {
-              // Limit the index length to 191 characters.
-              $this->shortenIndex($indexes[$index_name][$index_key]);
-            }
-          }
-        }
-        else {
-          throw new SchemaException("MySQL needs the '$field_name' field specification in order to normalize the '$index_name' index");
-        }
-      }
-    }
-    return $indexes;
-  }
-
-  /**
-   * Helper function for normalizeIndexes().
-   *
-   * Shortens an index to 191 characters.
-   *
-   * @param array $index
-   *   The index array to be used in createKeySql.
-   *
-   * @see Drupal\Core\Database\Driver\mysql\Schema::createKeySql()
-   * @see Drupal\Core\Database\Driver\mysql\Schema::normalizeIndexes()
-   */
-  protected function shortenIndex(&$index) {
-    if (is_array($index)) {
-      if ($index[1] > 191) {
-        $index[1] = 191;
-      }
-    }
-    else {
-      $index = [$index, 191];
-    }
-  }
-
-  protected function createKeySql($fields) {
-    $return = [];
-    foreach ($fields as $field) {
-      if (is_array($field)) {
-        $return[] = '`' . $field[0] . '`(' . $field[1] . ')';
-      }
-      else {
-        $return[] = '`' . $field . '`';
-      }
-    }
-    return implode(', ', $return);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function renameTable($table, $new_name) {
-    if (!$this->tableExists($table)) {
-      throw new SchemaObjectDoesNotExistException("Cannot rename '$table' to '$new_name': table '$table' doesn't exist.");
-    }
-    if ($this->tableExists($new_name)) {
-      throw new SchemaObjectExistsException("Cannot rename '$table' to '$new_name': table '$new_name' already exists.");
-    }
-
-    $info = $this->getPrefixInfo($new_name);
-    $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO `' . $info['table'] . '`');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function dropTable($table) {
-    if (!$this->tableExists($table)) {
-      return FALSE;
-    }
-
-    $this->connection->query('DROP TABLE {' . $table . '}');
-    return TRUE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function addField($table, $field, $spec, $keys_new = []) {
-    if (!$this->tableExists($table)) {
-      throw new SchemaObjectDoesNotExistException("Cannot add field '$table.$field': table doesn't exist.");
-    }
-    if ($this->fieldExists($table, $field)) {
-      throw new SchemaObjectExistsException("Cannot add field '$table.$field': field already exists.");
-    }
-
-    // Fields that are part of a PRIMARY KEY must be added as NOT NULL.
-    $is_primary_key = isset($keys_new['primary key']) && in_array($field, $keys_new['primary key'], TRUE);
-    if ($is_primary_key) {
-      $this->ensureNotNullPrimaryKey($keys_new['primary key'], [$field => $spec]);
-    }
-
-    $fixnull = FALSE;
-    if (!empty($spec['not null']) && !isset($spec['default']) && !$is_primary_key) {
-      $fixnull = TRUE;
-      $spec['not null'] = FALSE;
-    }
-    $query = 'ALTER TABLE {' . $table . '} ADD ';
-    $query .= $this->createFieldSql($field, $this->processField($spec));
-    if ($keys_sql = $this->createKeysSql($keys_new)) {
-      // Make sure to drop the existing primary key before adding a new one.
-      // This is only needed when adding a field because this method, unlike
-      // changeField(), is supposed to handle primary keys automatically.
-      if (isset($keys_new['primary key']) && $this->indexExists($table, 'PRIMARY')) {
-        $query .= ', DROP PRIMARY KEY';
-      }
-
-      $query .= ', ADD ' . implode(', ADD ', $keys_sql);
-    }
-    $this->connection->query($query);
-    if (isset($spec['initial_from_field'])) {
-      if (isset($spec['initial'])) {
-        $expression = 'COALESCE(' . $spec['initial_from_field'] . ', :default_initial_value)';
-        $arguments = [':default_initial_value' => $spec['initial']];
-      }
-      else {
-        $expression = $spec['initial_from_field'];
-        $arguments = [];
-      }
-      $this->connection->update($table)
-        ->expression($field, $expression, $arguments)
-        ->execute();
-    }
-    elseif (isset($spec['initial'])) {
-      $this->connection->update($table)
-        ->fields([$field => $spec['initial']])
-        ->execute();
-    }
-    if ($fixnull) {
-      $spec['not null'] = TRUE;
-      $this->changeField($table, $field, $field, $spec);
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function dropField($table, $field) {
-    if (!$this->fieldExists($table, $field)) {
-      return FALSE;
-    }
-
-    // When dropping a field that is part of a composite primary key MySQL
-    // automatically removes the field from the primary key, which can leave the
-    // table in an invalid state. MariaDB 10.2.8 requires explicitly dropping
-    // the primary key first for this reason. We perform this deletion
-    // explicitly which also makes the behavior on both MySQL and MariaDB
-    // consistent with PostgreSQL.
-    // @see https://mariadb.com/kb/en/library/alter-table
-    $primary_key = $this->findPrimaryKeyColumns($table);
-    if ((count($primary_key) > 1) && in_array($field, $primary_key, TRUE)) {
-      $this->dropPrimaryKey($table);
-    }
-
-    $this->connection->query('ALTER TABLE {' . $table . '} DROP `' . $field . '`');
-    return TRUE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function indexExists($table, $name) {
-    // Returns one row for each column in the index. Result is string or FALSE.
-    // Details at http://dev.mysql.com/doc/refman/5.0/en/show-index.html
-    $row = $this->connection->query('SHOW INDEX FROM {' . $table . '} WHERE key_name = ' . $this->connection->quote($name))->fetchAssoc();
-    return isset($row['Key_name']);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function addPrimaryKey($table, $fields) {
-    if (!$this->tableExists($table)) {
-      throw new SchemaObjectDoesNotExistException("Cannot add primary key to table '$table': table doesn't exist.");
-    }
-    if ($this->indexExists($table, 'PRIMARY')) {
-      throw new SchemaObjectExistsException("Cannot add primary key to table '$table': primary key already exists.");
-    }
-
-    $this->connection->query('ALTER TABLE {' . $table . '} ADD PRIMARY KEY (' . $this->createKeySql($fields) . ')');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function dropPrimaryKey($table) {
-    if (!$this->indexExists($table, 'PRIMARY')) {
-      return FALSE;
-    }
-
-    $this->connection->query('ALTER TABLE {' . $table . '} DROP PRIMARY KEY');
-    return TRUE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function findPrimaryKeyColumns($table) {
-    if (!$this->tableExists($table)) {
-      return FALSE;
-    }
-    $result = $this->connection->query("SHOW KEYS FROM {" . $table . "} WHERE Key_name = 'PRIMARY'")->fetchAllAssoc('Column_name');
-    return array_keys($result);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function addUniqueKey($table, $name, $fields) {
-    if (!$this->tableExists($table)) {
-      throw new SchemaObjectDoesNotExistException("Cannot add unique key '$name' to table '$table': table doesn't exist.");
-    }
-    if ($this->indexExists($table, $name)) {
-      throw new SchemaObjectExistsException("Cannot add unique key '$name' to table '$table': unique key already exists.");
-    }
-
-    $this->connection->query('ALTER TABLE {' . $table . '} ADD UNIQUE KEY `' . $name . '` (' . $this->createKeySql($fields) . ')');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function dropUniqueKey($table, $name) {
-    if (!$this->indexExists($table, $name)) {
-      return FALSE;
-    }
-
-    $this->connection->query('ALTER TABLE {' . $table . '} DROP KEY `' . $name . '`');
-    return TRUE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function addIndex($table, $name, $fields, array $spec) {
-    if (!$this->tableExists($table)) {
-      throw new SchemaObjectDoesNotExistException("Cannot add index '$name' to table '$table': table doesn't exist.");
-    }
-    if ($this->indexExists($table, $name)) {
-      throw new SchemaObjectExistsException("Cannot add index '$name' to table '$table': index already exists.");
-    }
-
-    $spec['indexes'][$name] = $fields;
-    $indexes = $this->getNormalizedIndexes($spec);
-
-    $this->connection->query('ALTER TABLE {' . $table . '} ADD INDEX `' . $name . '` (' . $this->createKeySql($indexes[$name]) . ')');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function dropIndex($table, $name) {
-    if (!$this->indexExists($table, $name)) {
-      return FALSE;
-    }
-
-    $this->connection->query('ALTER TABLE {' . $table . '} DROP INDEX `' . $name . '`');
-    return TRUE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function introspectIndexSchema($table) {
-    if (!$this->tableExists($table)) {
-      throw new SchemaObjectDoesNotExistException("The table $table doesn't exist.");
-    }
-
-    $index_schema = [
-      'primary key' => [],
-      'unique keys' => [],
-      'indexes' => [],
-    ];
-
-    $result = $this->connection->query('SHOW INDEX FROM {' . $table . '}')->fetchAll();
-    foreach ($result as $row) {
-      if ($row->Key_name === 'PRIMARY') {
-        $index_schema['primary key'][] = $row->Column_name;
-      }
-      elseif ($row->Non_unique == 0) {
-        $index_schema['unique keys'][$row->Key_name][] = $row->Column_name;
-      }
-      else {
-        $index_schema['indexes'][$row->Key_name][] = $row->Column_name;
-      }
-    }
-
-    return $index_schema;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function changeField($table, $field, $field_new, $spec, $keys_new = []) {
-    if (!$this->fieldExists($table, $field)) {
-      throw new SchemaObjectDoesNotExistException("Cannot change the definition of field '$table.$field': field doesn't exist.");
-    }
-    if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
-      throw new SchemaObjectExistsException("Cannot rename field '$table.$field' to '$field_new': target field already exists.");
-    }
-    if (isset($keys_new['primary key']) && in_array($field_new, $keys_new['primary key'], TRUE)) {
-      $this->ensureNotNullPrimaryKey($keys_new['primary key'], [$field_new => $spec]);
-    }
-
-    $sql = 'ALTER TABLE {' . $table . '} CHANGE `' . $field . '` ' . $this->createFieldSql($field_new, $this->processField($spec));
-    if ($keys_sql = $this->createKeysSql($keys_new)) {
-      $sql .= ', ADD ' . implode(', ADD ', $keys_sql);
-    }
-    $this->connection->query($sql);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function prepareComment($comment, $length = NULL) {
-    // Truncate comment to maximum comment length.
-    if (isset($length)) {
-      // Add table prefixes before truncating.
-      $comment = Unicode::truncate($this->connection->prefixTables($comment), $length, TRUE, TRUE);
-    }
-    // Remove semicolons to avoid triggering multi-statement check.
-    $comment = strtr($comment, [';' => '.']);
-    return $this->connection->quote($comment);
-  }
-
-  /**
-   * Retrieve a table or column comment.
-   */
-  public function getComment($table, $column = NULL) {
-    $condition = $this->buildTableNameCondition($table);
-    if (isset($column)) {
-      $condition->condition('column_name', $column);
-      $condition->compile($this->connection, $this);
-      // Don't use {} around information_schema.columns table.
-      return $this->connection->query("SELECT column_comment AS column_comment FROM information_schema.columns WHERE " . (string) $condition, $condition->arguments())->fetchField();
-    }
-    $condition->compile($this->connection, $this);
-    // Don't use {} around information_schema.tables table.
-    $comment = $this->connection->query("SELECT table_comment AS table_comment FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchField();
-    // Work-around for MySQL 5.0 bug http://bugs.mysql.com/bug.php?id=11379
-    return preg_replace('/; InnoDB free:.*$/', '', $comment);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function tableExists($table) {
-    // The information_schema table is very slow to query under MySQL 5.0.
-    // Instead, we try to select from the table in question.  If it fails,
-    // the most likely reason is that it does not exist. That is dramatically
-    // faster than using information_schema.
-    // @link http://bugs.mysql.com/bug.php?id=19588
-    // @todo This override should be removed once we require a version of MySQL
-    //   that has that bug fixed.
-    try {
-      $this->connection->queryRange("SELECT 1 FROM {" . $table . "}", 0, 1);
-      return TRUE;
-    }
-    catch (\Exception $e) {
-      return FALSE;
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function fieldExists($table, $column) {
-    // The information_schema table is very slow to query under MySQL 5.0.
-    // Instead, we try to select from the table and field in question. If it
-    // fails, the most likely reason is that it does not exist. That is
-    // dramatically faster than using information_schema.
-    // @link http://bugs.mysql.com/bug.php?id=19588
-    // @todo This override should be removed once we require a version of MySQL
-    //   that has that bug fixed.
-    try {
-      $this->connection->queryRange("SELECT $column FROM {" . $table . "}", 0, 1);
-      return TRUE;
-    }
-    catch (\Exception $e) {
-      return FALSE;
-    }
-  }
-
-}
-
-/**
- * @} End of "addtogroup schemaapi".
- */
+class Schema extends MysqlSchema {}
diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Upsert.php b/core/lib/Drupal/Core/Database/Driver/mysql/Upsert.php
index 8eda775c0a74..e8b21ac7bf32 100644
--- a/core/lib/Drupal/Core/Database/Driver/mysql/Upsert.php
+++ b/core/lib/Drupal/Core/Database/Driver/mysql/Upsert.php
@@ -2,42 +2,16 @@
 
 namespace Drupal\Core\Database\Driver\mysql;
 
-use Drupal\Core\Database\Query\Upsert as QueryUpsert;
+use Drupal\mysql\Driver\Database\mysql\Upsert as MysqlUpsert;
+
+@trigger_error('\Drupal\Core\Database\Driver\mysql\Upsert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED);
 
 /**
  * MySQL implementation of \Drupal\Core\Database\Query\Upsert.
+ *
+ * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL
+ *   database driver has been moved to the mysql module.
+ *
+ * @see https://www.drupal.org/node/3129492
  */
-class Upsert extends QueryUpsert {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function __toString() {
-    // Create a sanitized comment string to prepend to the query.
-    $comments = $this->connection->makeComment($this->comments);
-
-    // 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);
-
-    $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
-
-    $values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
-    $query .= implode(', ', $values);
-
-    // Updating the unique / primary key is not necessary.
-    unset($insert_fields[$this->key]);
-
-    $update = [];
-    foreach ($insert_fields as $field) {
-      $update[] = "$field = VALUES($field)";
-    }
-
-    $query .= ' ON DUPLICATE KEY UPDATE ' . implode(', ', $update);
-
-    return $query;
-  }
-
-}
+class Upsert extends MysqlUpsert {}
diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php
index 9154a0188a9d..b015bda69416 100644
--- a/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php
+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php
@@ -2,374 +2,16 @@
 
 namespace Drupal\Core\Database\Driver\pgsql;
 
-use Drupal\Core\Database\Database;
-use Drupal\Core\Database\Connection as DatabaseConnection;
-use Drupal\Core\Database\DatabaseAccessDeniedException;
-use Drupal\Core\Database\DatabaseNotFoundException;
-use Drupal\Core\Database\StatementInterface;
-use Drupal\Core\Database\StatementWrapper;
+use Drupal\pgsql\Driver\Database\pgsql\Connection as PgsqlConnection;
 
-// cSpell:ignore ilike nextval
-
-/**
- * @addtogroup database
- * @{
- */
+@trigger_error('\Drupal\Core\Database\Driver\pgsql\Connection is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED);
 
 /**
  * PostgreSQL implementation of \Drupal\Core\Database\Connection.
+ *
+ * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL
+ *   database driver has been moved to the pgsql module.
+ *
+ * @see https://www.drupal.org/node/3129492
  */
-class Connection extends DatabaseConnection {
-
-  /**
-   * The name by which to obtain a lock for retrieve the next insert id.
-   */
-  const POSTGRESQL_NEXTID_LOCK = 1000;
-
-  /**
-   * Error code for "Unknown database" error.
-   */
-  const DATABASE_NOT_FOUND = 7;
-
-  /**
-   * Error code for "Connection failure" errors.
-   *
-   * Technically this is an internal error code that will only be shown in the
-   * PDOException message. It will need to get extracted.
-   */
-  const CONNECTION_FAILURE = '08006';
-
-  /**
-   * {@inheritdoc}
-   */
-  protected $statementClass = NULL;
-
-  /**
-   * {@inheritdoc}
-   */
-  protected $statementWrapperClass = StatementWrapper::class;
-
-  /**
-   * A map of condition operators to PostgreSQL operators.
-   *
-   * In PostgreSQL, 'LIKE' is case-sensitive. ILIKE should be used for
-   * case-insensitive statements.
-   */
-  protected static $postgresqlConditionOperatorMap = [
-    'LIKE' => ['operator' => 'ILIKE'],
-    'LIKE BINARY' => ['operator' => 'LIKE'],
-    'NOT LIKE' => ['operator' => 'NOT ILIKE'],
-    'REGEXP' => ['operator' => '~*'],
-    'NOT REGEXP' => ['operator' => '!~*'],
-  ];
-
-  /**
-   * {@inheritdoc}
-   */
-  protected $transactionalDDLSupport = TRUE;
-
-  /**
-   * {@inheritdoc}
-   */
-  protected $identifierQuotes = ['"', '"'];
-
-  /**
-   * Constructs a connection object.
-   */
-  public function __construct(\PDO $connection, array $connection_options) {
-    parent::__construct($connection, $connection_options);
-
-    // Force PostgreSQL to use the UTF-8 character set by default.
-    $this->connection->exec("SET NAMES 'UTF8'");
-
-    // Execute PostgreSQL init_commands.
-    if (isset($connection_options['init_commands'])) {
-      $this->connection->exec(implode('; ', $connection_options['init_commands']));
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function open(array &$connection_options = []) {
-    // Default to TCP connection on port 5432.
-    if (empty($connection_options['port'])) {
-      $connection_options['port'] = 5432;
-    }
-
-    // PostgreSQL in trust mode doesn't require a password to be supplied.
-    if (empty($connection_options['password'])) {
-      $connection_options['password'] = NULL;
-    }
-    // If the password contains a backslash it is treated as an escape character
-    // http://bugs.php.net/bug.php?id=53217
-    // so backslashes in the password need to be doubled up.
-    // The bug was reported against pdo_pgsql 1.0.2, backslashes in passwords
-    // will break on this doubling up when the bug is fixed, so check the version
-    // elseif (phpversion('pdo_pgsql') < 'version_this_was_fixed_in') {
-    else {
-      $connection_options['password'] = str_replace('\\', '\\\\', $connection_options['password']);
-    }
-
-    $connection_options['database'] = (!empty($connection_options['database']) ? $connection_options['database'] : 'template1');
-    $dsn = 'pgsql:host=' . $connection_options['host'] . ' dbname=' . $connection_options['database'] . ' port=' . $connection_options['port'];
-
-    // Allow PDO options to be overridden.
-    $connection_options += [
-      'pdo' => [],
-    ];
-    $connection_options['pdo'] += [
-      \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
-      // Prepared statements are most effective for performance when queries
-      // are recycled (used several times). However, if they are not re-used,
-      // prepared statements become inefficient. Since most of Drupal's
-      // prepared queries are not re-used, it should be faster to emulate
-      // the preparation than to actually ready statements for re-use. If in
-      // doubt, reset to FALSE and measure performance.
-      \PDO::ATTR_EMULATE_PREPARES => TRUE,
-      // Convert numeric values to strings when fetching.
-      \PDO::ATTR_STRINGIFY_FETCHES => TRUE,
-    ];
-
-    try {
-      $pdo = new \PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
-    }
-    catch (\PDOException $e) {
-      if (static::getSQLState($e) == static::CONNECTION_FAILURE) {
-        if (strpos($e->getMessage(), 'password authentication failed for user') !== FALSE) {
-          throw new DatabaseAccessDeniedException($e->getMessage(), $e->getCode(), $e);
-        }
-        elseif (strpos($e->getMessage(), 'database') !== FALSE && strpos($e->getMessage(), 'does not exist') !== FALSE) {
-          throw new DatabaseNotFoundException($e->getMessage(), $e->getCode(), $e);
-        }
-      }
-      throw $e;
-    }
-
-    return $pdo;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function query($query, array $args = [], $options = []) {
-    $options += $this->defaultOptions();
-
-    // The PDO PostgreSQL driver has a bug which doesn't type cast booleans
-    // correctly when parameters are bound using associative arrays.
-    // @see http://bugs.php.net/bug.php?id=48383
-    foreach ($args as &$value) {
-      if (is_bool($value)) {
-        $value = (int) $value;
-      }
-    }
-
-    // We need to wrap queries with a savepoint if:
-    // - Currently in a transaction.
-    // - A 'mimic_implicit_commit' does not exist already.
-    // - The query is not a savepoint query.
-    $wrap_with_savepoint = $this->inTransaction() &&
-      !isset($this->transactionLayers['mimic_implicit_commit']) &&
-      !(is_string($query) && (
-        stripos($query, 'ROLLBACK TO SAVEPOINT ') === 0 ||
-        stripos($query, 'RELEASE SAVEPOINT ') === 0 ||
-        stripos($query, 'SAVEPOINT ') === 0
-      )
-    );
-    if ($wrap_with_savepoint) {
-      // Create a savepoint so we can rollback a failed query. This is so we can
-      // mimic MySQL and SQLite transactions which don't fail if a single query
-      // fails. This is important for tables that are created on demand. For
-      // example, \Drupal\Core\Cache\DatabaseBackend.
-      $this->addSavepoint();
-      try {
-        $return = parent::query($query, $args, $options);
-        $this->releaseSavepoint();
-      }
-      catch (\Exception $e) {
-        $this->rollbackSavepoint();
-        throw $e;
-      }
-    }
-    else {
-      $return = parent::query($query, $args, $options);
-    }
-
-    return $return;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function prepareStatement(string $query, array $options, bool $allow_row_count = FALSE): StatementInterface {
-    // mapConditionOperator converts some operations (LIKE, REGEXP, etc.) to
-    // PostgreSQL equivalents (ILIKE, ~*, etc.). However PostgreSQL doesn't
-    // automatically cast the fields to the right type for these operators,
-    // so we need to alter the query and add the type-cast.
-    $query = preg_replace('/ ([^ ]+) +(I*LIKE|NOT +I*LIKE|~\*|!~\*) /i', ' ${1}::text ${2} ', $query);
-    return parent::prepareStatement($query, $options, $allow_row_count);
-  }
-
-  public function queryRange($query, $from, $count, array $args = [], array $options = []) {
-    return $this->query($query . ' LIMIT ' . (int) $count . ' OFFSET ' . (int) $from, $args, $options);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function queryTemporary($query, array $args = [], array $options = []) {
-    @trigger_error('Connection::queryTemporary() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. There is no replacement. See https://www.drupal.org/node/3211781', E_USER_DEPRECATED);
-    $tablename = $this->generateTemporaryTableName();
-    $this->query('CREATE TEMPORARY TABLE {' . $tablename . '} AS ' . $query, $args, $options);
-    return $tablename;
-  }
-
-  public function driver() {
-    return 'pgsql';
-  }
-
-  public function databaseType() {
-    return 'pgsql';
-  }
-
-  /**
-   * Overrides \Drupal\Core\Database\Connection::createDatabase().
-   *
-   * @param string $database
-   *   The name of the database to create.
-   *
-   * @throws \Drupal\Core\Database\DatabaseNotFoundException
-   */
-  public function createDatabase($database) {
-    // Escape the database name.
-    $database = Database::getConnection()->escapeDatabase($database);
-
-    // If the PECL intl extension is installed, use it to determine the proper
-    // locale.  Otherwise, fall back to en_US.
-    if (class_exists('Locale')) {
-      $locale = \Locale::getDefault();
-    }
-    else {
-      $locale = 'en_US';
-    }
-
-    try {
-      // Create the database and set it as active.
-      $this->connection->exec("CREATE DATABASE $database WITH TEMPLATE template0 ENCODING='utf8' LC_CTYPE='$locale.utf8' LC_COLLATE='$locale.utf8'");
-    }
-    catch (\Exception $e) {
-      throw new DatabaseNotFoundException($e->getMessage());
-    }
-  }
-
-  public function mapConditionOperator($operator) {
-    return static::$postgresqlConditionOperatorMap[$operator] ?? NULL;
-  }
-
-  /**
-   * Retrieve a the next id in a sequence.
-   *
-   * PostgreSQL has built in sequences. We'll use these instead of inserting
-   * and updating a sequences table.
-   */
-  public function nextId($existing = 0) {
-
-    // Retrieve the name of the sequence. This information cannot be cached
-    // because the prefix may change, for example, like it does in tests.
-    $sequence_name = $this->makeSequenceName('sequences', 'value');
-
-    // When PostgreSQL gets a value too small then it will lock the table,
-    // retry the INSERT and if it's still too small then alter the sequence.
-    $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
-    if ($id > $existing) {
-      return $id;
-    }
-
-    // PostgreSQL advisory locks are simply locks to be used by an
-    // application such as Drupal. This will prevent other Drupal processes
-    // from altering the sequence while we are.
-    $this->query("SELECT pg_advisory_lock(" . self::POSTGRESQL_NEXTID_LOCK . ")");
-
-    // While waiting to obtain the lock, the sequence may have been altered
-    // so lets try again to obtain an adequate value.
-    $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
-    if ($id > $existing) {
-      $this->query("SELECT pg_advisory_unlock(" . self::POSTGRESQL_NEXTID_LOCK . ")");
-      return $id;
-    }
-
-    // Reset the sequence to a higher value than the existing id.
-    $this->query("ALTER SEQUENCE " . $sequence_name . " RESTART WITH " . ($existing + 1));
-
-    // Retrieve the next id. We know this will be as high as we want it.
-    $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
-
-    $this->query("SELECT pg_advisory_unlock(" . self::POSTGRESQL_NEXTID_LOCK . ")");
-
-    return $id;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFullQualifiedTableName($table) {
-    $options = $this->getConnectionOptions();
-    $prefix = $this->tablePrefix($table);
-
-    // The fully qualified table name in PostgreSQL is in the form of
-    // <database>.<schema>.<table>, so we have to include the 'public' schema in
-    // the return value.
-    return $options['database'] . '.public.' . $prefix . $table;
-  }
-
-  /**
-   * Add a new savepoint with a unique name.
-   *
-   * The main use for this method is to mimic InnoDB functionality, which
-   * provides an inherent savepoint before any query in a transaction.
-   *
-   * @param $savepoint_name
-   *   A string representing the savepoint name. By default,
-   *   "mimic_implicit_commit" is used.
-   *
-   * @see Drupal\Core\Database\Connection::pushTransaction()
-   */
-  public function addSavepoint($savepoint_name = 'mimic_implicit_commit') {
-    if ($this->inTransaction()) {
-      $this->pushTransaction($savepoint_name);
-    }
-  }
-
-  /**
-   * Release a savepoint by name.
-   *
-   * @param $savepoint_name
-   *   A string representing the savepoint name. By default,
-   *   "mimic_implicit_commit" is used.
-   *
-   * @see Drupal\Core\Database\Connection::popTransaction()
-   */
-  public function releaseSavepoint($savepoint_name = 'mimic_implicit_commit') {
-    if (isset($this->transactionLayers[$savepoint_name])) {
-      $this->popTransaction($savepoint_name);
-    }
-  }
-
-  /**
-   * Rollback a savepoint by name if it exists.
-   *
-   * @param $savepoint_name
-   *   A string representing the savepoint name. By default,
-   *   "mimic_implicit_commit" is used.
-   */
-  public function rollbackSavepoint($savepoint_name = 'mimic_implicit_commit') {
-    if (isset($this->transactionLayers[$savepoint_name])) {
-      $this->rollBack($savepoint_name);
-    }
-  }
-
-}
-
-/**
- * @} End of "addtogroup database".
- */
+class Connection extends PgsqlConnection {}
diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Delete.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Delete.php
index 2cf178e7b2ed..0c3333003575 100644
--- a/core/lib/Drupal/Core/Database/Driver/pgsql/Delete.php
+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Delete.php
@@ -2,28 +2,16 @@
 
 namespace Drupal\Core\Database\Driver\pgsql;
 
-use Drupal\Core\Database\Query\Delete as QueryDelete;
+use Drupal\pgsql\Driver\Database\pgsql\Delete as PgsqlDelete;
+
+@trigger_error('\Drupal\Core\Database\Driver\pgsql\Delete is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED);
 
 /**
  * PostgreSQL implementation of \Drupal\Core\Database\Query\Delete.
+ *
+ * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL
+ *   database driver has been moved to the pgsql module.
+ *
+ * @see https://www.drupal.org/node/3129492
  */
-class Delete extends QueryDelete {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function execute() {
-    $this->connection->addSavepoint();
-    try {
-      $result = parent::execute();
-    }
-    catch (\Exception $e) {
-      $this->connection->rollbackSavepoint();
-      throw $e;
-    }
-    $this->connection->releaseSavepoint();
-
-    return $result;
-  }
-
-}
+class Delete extends PgsqlDelete {}
diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php
index 3f27f4c8c668..0702d38a6d58 100644
--- a/core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php
+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php
@@ -2,157 +2,16 @@
 
 namespace Drupal\Core\Database\Driver\pgsql;
 
-use Drupal\Core\Database\DatabaseExceptionWrapper;
-use Drupal\Core\Database\IntegrityConstraintViolationException;
-use Drupal\Core\Database\Query\Insert as QueryInsert;
+use Drupal\pgsql\Driver\Database\pgsql\Insert as PgsqlInsert;
 
-// cSpell:ignore nextval setval
-
-/**
- * @ingroup database
- * @{
- */
+@trigger_error('\Drupal\Core\Database\Driver\pgsql\Insert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED);
 
 /**
  * PostgreSQL implementation of \Drupal\Core\Database\Query\Insert.
+ *
+ * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL
+ *   database driver has been moved to the pgsql module.
+ *
+ * @see https://www.drupal.org/node/3129492
  */
-class Insert extends QueryInsert {
-
-  public function execute() {
-    if (!$this->preExecute()) {
-      return NULL;
-    }
-
-    $stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions);
-
-    // Fetch the list of blobs and sequences used on that table.
-    $table_information = $this->connection->schema()->queryTableInformation($this->table);
-
-    $max_placeholder = 0;
-    $blobs = [];
-    $blob_count = 0;
-    foreach ($this->insertValues as $insert_values) {
-      foreach ($this->insertFields as $idx => $field) {
-        if (isset($table_information->blob_fields[$field]) && $insert_values[$idx] !== NULL) {
-          $blobs[$blob_count] = fopen('php://memory', 'a');
-          fwrite($blobs[$blob_count], $insert_values[$idx]);
-          rewind($blobs[$blob_count]);
-
-          $stmt->getClientStatement()->bindParam(':db_insert_placeholder_' . $max_placeholder++, $blobs[$blob_count], \PDO::PARAM_LOB);
-
-          // Pre-increment is faster in PHP than increment.
-          ++$blob_count;
-        }
-        else {
-          $stmt->getClientStatement()->bindParam(':db_insert_placeholder_' . $max_placeholder++, $insert_values[$idx]);
-        }
-      }
-      // Check if values for a serial field has been passed.
-      if (!empty($table_information->serial_fields)) {
-        foreach ($table_information->serial_fields as $index => $serial_field) {
-          $serial_key = array_search($serial_field, $this->insertFields);
-          if ($serial_key !== FALSE) {
-            $serial_value = $insert_values[$serial_key];
-
-            // Sequences must be greater than or equal to 1.
-            if ($serial_value === NULL || !$serial_value) {
-              $serial_value = 1;
-            }
-            // Set the sequence to the bigger value of either the passed
-            // value or the max value of the column. It can happen that another
-            // thread calls nextval() which could lead to a serial number being
-            // used twice. However, trying to insert a value into a serial
-            // column should only be done in very rare cases and is not thread
-            // safe by definition.
-            $this->connection->query("SELECT setval('" . $table_information->sequences[$index] . "', GREATEST(MAX(" . $serial_field . "), :serial_value)) FROM {" . $this->table . "}", [':serial_value' => (int) $serial_value]);
-          }
-        }
-      }
-    }
-    if (!empty($this->fromQuery)) {
-      // bindParam stores only a reference to the variable that is followed when
-      // the statement is executed. We pass $arguments[$key] instead of $value
-      // because the second argument to bindParam is passed by reference and
-      // the foreach statement assigns the element to the existing reference.
-      $arguments = $this->fromQuery->getArguments();
-      foreach ($arguments as $key => $value) {
-        $stmt->getClientStatement()->bindParam($key, $arguments[$key]);
-      }
-    }
-
-    // Create a savepoint so we can rollback a failed query. This is so we can
-    // mimic MySQL and SQLite transactions which don't fail if a single query
-    // fails. This is important for tables that are created on demand. For
-    // example, \Drupal\Core\Cache\DatabaseBackend.
-    $this->connection->addSavepoint();
-    try {
-      $stmt->execute(NULL, $this->queryOptions);
-      if (isset($table_information->serial_fields[0])) {
-        $last_insert_id = $stmt->fetchField();
-      }
-      $this->connection->releaseSavepoint();
-    }
-    catch (\PDOException $e) {
-      $this->connection->rollbackSavepoint();
-      $message = $e->getMessage() . ": " . $stmt->getQueryString();
-      // Match all SQLSTATE 23xxx errors.
-      if (substr($e->getCode(), -6, -3) == '23') {
-        throw new IntegrityConstraintViolationException($message, $e->getCode(), $e);
-      }
-      else {
-        throw new DatabaseExceptionWrapper($message, 0, $e->getCode());
-      }
-    }
-    catch (\Exception $e) {
-      $this->connection->rollbackSavepoint();
-      throw $e;
-    }
-
-    // Re-initialize the values array so that we can re-use this query.
-    $this->insertValues = [];
-
-    return $last_insert_id ?? NULL;
-  }
-
-  public function __toString() {
-    // Create a sanitized comment string to prepend to the query.
-    $comments = $this->connection->makeComment($this->comments);
-
-    // Default fields are always placed first for consistency.
-    $insert_fields = array_merge($this->defaultFields, $this->insertFields);
-
-    $insert_fields = array_map(function ($f) {
-      return $this->connection->escapeField($f);
-    }, $insert_fields);
-
-    // If we're selecting from a SelectQuery, finish building the query and
-    // pass it back, as any remaining options are irrelevant.
-    if (!empty($this->fromQuery)) {
-      $insert_fields_string = $insert_fields ? ' (' . implode(', ', $insert_fields) . ') ' : ' ';
-      $query = $comments . 'INSERT INTO {' . $this->table . '}' . $insert_fields_string . $this->fromQuery;
-    }
-    else {
-      $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
-
-      $values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
-      $query .= implode(', ', $values);
-    }
-    try {
-      // Fetch the list of blobs and sequences used on that table.
-      $table_information = $this->connection->schema()->queryTableInformation($this->table);
-      if (isset($table_information->serial_fields[0])) {
-        // Use RETURNING syntax to get the last insert ID in the same INSERT
-        // query, see https://www.postgresql.org/docs/10/dml-returning.html.
-        $query .= ' RETURNING ' . $table_information->serial_fields[0];
-      }
-    }
-    catch (DatabaseExceptionWrapper $e) {
-      // If we fail to get the table information it is probably because the
-      // table does not exist yet so adding the returning statement is pointless
-      // because the query will fail. This happens for tables created on demand,
-      // for example, cache tables.
-    }
-    return $query;
-  }
-
-}
+class Insert extends PgsqlInsert {}
diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php
index 1c07dfaf382c..c7104cc0c131 100644
--- a/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php
+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php
@@ -2,293 +2,16 @@
 
 namespace Drupal\Core\Database\Driver\pgsql\Install;
 
-use Drupal\Core\Database\Database;
-use Drupal\Core\Database\Install\Tasks as InstallTasks;
-use Drupal\Core\Database\DatabaseNotFoundException;
+use Drupal\pgsql\Driver\Database\pgsql\Install\Tasks as PgsqlTasks;
+
+@trigger_error('\Drupal\Core\Database\Driver\pgsql\Install\Tasks is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED);
 
 /**
  * Specifies installation tasks for PostgreSQL databases.
+ *
+ * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL
+ *   database driver has been moved to the pgsql module.
+ *
+ * @see https://www.drupal.org/node/3129492
  */
-class Tasks extends InstallTasks {
-
-  /**
-   * Minimum required PostgreSQL version.
-   *
-   * The contrib extension pg_trgm is supposed to be installed.
-   *
-   * @see https://www.postgresql.org/docs/10/pgtrgm.html
-   */
-  const PGSQL_MINIMUM_VERSION = '10';
-
-  /**
-   * {@inheritdoc}
-   */
-  protected $pdoDriver = 'pgsql';
-
-  /**
-   * Constructs a \Drupal\Core\Database\Driver\pgsql\Install\Tasks object.
-   */
-  public function __construct() {
-    $this->tasks[] = [
-      'function' => 'checkEncoding',
-      'arguments' => [],
-    ];
-    $this->tasks[] = [
-      'function' => 'checkBinaryOutput',
-      'arguments' => [],
-    ];
-    $this->tasks[] = [
-      'function' => 'checkStandardConformingStrings',
-      'arguments' => [],
-    ];
-    $this->tasks[] = [
-      'function' => 'initializeDatabase',
-      'arguments' => [],
-    ];
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function name() {
-    return t('PostgreSQL');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function minimumVersion() {
-    return static::PGSQL_MINIMUM_VERSION;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function connect() {
-    try {
-      // This doesn't actually test the connection.
-      Database::setActiveConnection();
-      // Now actually do a check.
-      Database::getConnection();
-      $this->pass('Drupal can CONNECT to the database ok.');
-    }
-    catch (\Exception $e) {
-      // Attempt to create the database if it is not found.
-      if ($e instanceof DatabaseNotFoundException) {
-        // Remove the database string from connection info.
-        $connection_info = Database::getConnectionInfo();
-        $database = $connection_info['default']['database'];
-        unset($connection_info['default']['database']);
-
-        // In order to change the Database::$databaseInfo array, need to remove
-        // the active connection, then re-add it with the new info.
-        Database::removeConnection('default');
-        Database::addConnectionInfo('default', 'default', $connection_info['default']);
-
-        try {
-          // Now, attempt the connection again; if it's successful, attempt to
-          // create the database.
-          Database::getConnection()->createDatabase($database);
-          Database::closeConnection();
-
-          // Now, restore the database config.
-          Database::removeConnection('default');
-          $connection_info['default']['database'] = $database;
-          Database::addConnectionInfo('default', 'default', $connection_info['default']);
-
-          // Check the database connection.
-          Database::getConnection();
-          $this->pass('Drupal can CONNECT to the database ok.');
-        }
-        catch (DatabaseNotFoundException $e) {
-          // Still no dice; probably a permission issue. Raise the error to the
-          // installer.
-          $this->fail(t('Database %database not found. The server reports the following message when attempting to create the database: %error.', ['%database' => $database, '%error' => $e->getMessage()]));
-        }
-      }
-      else {
-        // Database connection failed for some other reason than a non-existent
-        // database.
-        $this->fail(t('Failed to connect to your database server. The server reports the following message: %error.<ul><li>Is the database server running?</li><li>Does the database exist, and have you entered the correct database name?</li><li>Have you entered the correct username and password?</li><li>Have you entered the correct database hostname and port number?</li></ul>', ['%error' => $e->getMessage()]));
-        return FALSE;
-      }
-    }
-    return TRUE;
-  }
-
-  /**
-   * Check encoding is UTF8.
-   */
-  protected function checkEncoding() {
-    try {
-      if (Database::getConnection()->query('SHOW server_encoding')->fetchField() == 'UTF8') {
-        $this->pass(t('Database is encoded in UTF-8'));
-      }
-      else {
-        $this->fail(t('The %driver database must use %encoding encoding to work with Drupal. Recreate the database with %encoding encoding. See <a href="INSTALL.pgsql.txt">INSTALL.pgsql.txt</a> for more details.', [
-          '%encoding' => 'UTF8',
-          '%driver' => $this->name(),
-        ]));
-      }
-    }
-    catch (\Exception $e) {
-      $this->fail(t('Drupal could not determine the encoding of the database was set to UTF-8'));
-    }
-  }
-
-  /**
-   * Check Binary Output.
-   *
-   * Unserializing does not work on Postgresql 9 when bytea_output is 'hex'.
-   */
-  public function checkBinaryOutput() {
-    $database_connection = Database::getConnection();
-    if (!$this->checkBinaryOutputSuccess()) {
-      // First try to alter the database. If it fails, raise an error telling
-      // the user to do it themselves.
-      $connection_options = $database_connection->getConnectionOptions();
-      // It is safe to include the database name directly here, because this
-      // code is only called when a connection to the database is already
-      // established, thus the database name is guaranteed to be a correct
-      // value.
-      $query = "ALTER DATABASE \"{$connection_options['database']}\" SET bytea_output = 'escape';";
-      try {
-        $database_connection->query($query);
-      }
-      catch (\Exception $e) {
-        // Ignore possible errors when the user doesn't have the necessary
-        // privileges to ALTER the database.
-      }
-
-      // Close the database connection so that the configuration parameter
-      // is applied to the current connection.
-      Database::closeConnection();
-
-      // Recheck, if it fails, finally just rely on the end user to do the
-      // right thing.
-      if (!$this->checkBinaryOutputSuccess()) {
-        $replacements = [
-          '%setting' => 'bytea_output',
-          '%current_value' => 'hex',
-          '%needed_value' => 'escape',
-          '@query' => $query,
-        ];
-        $this->fail(t("The %setting setting is currently set to '%current_value', but needs to be '%needed_value'. Change this by running the following query: <code>@query</code>", $replacements));
-      }
-    }
-  }
-
-  /**
-   * Verify that a binary data roundtrip returns the original string.
-   */
-  protected function checkBinaryOutputSuccess() {
-    $bytea_output = Database::getConnection()->query("SHOW bytea_output")->fetchField();
-    return ($bytea_output == 'escape');
-  }
-
-  /**
-   * Ensures standard_conforming_strings setting is 'on'.
-   *
-   * When standard_conforming_strings setting is 'on' string literals ('...')
-   * treat backslashes literally, as specified in the SQL standard. This allows
-   * Drupal to convert between bytea, text and varchar columns.
-   */
-  public function checkStandardConformingStrings() {
-    $database_connection = Database::getConnection();
-    if (!$this->checkStandardConformingStringsSuccess()) {
-      // First try to alter the database. If it fails, raise an error telling
-      // the user to do it themselves.
-      $connection_options = $database_connection->getConnectionOptions();
-      // It is safe to include the database name directly here, because this
-      // code is only called when a connection to the database is already
-      // established, thus the database name is guaranteed to be a correct
-      // value.
-      $query = "ALTER DATABASE \"" . $connection_options['database'] . "\" SET standard_conforming_strings = 'on';";
-      try {
-        $database_connection->query($query);
-      }
-      catch (\Exception $e) {
-        // Ignore possible errors when the user doesn't have the necessary
-        // privileges to ALTER the database.
-      }
-
-      // Close the database connection so that the configuration parameter
-      // is applied to the current connection.
-      Database::closeConnection();
-
-      // Recheck, if it fails, finally just rely on the end user to do the
-      // right thing.
-      if (!$this->checkStandardConformingStringsSuccess()) {
-        $replacements = [
-          '%setting' => 'standard_conforming_strings',
-          '%current_value' => 'off',
-          '%needed_value' => 'on',
-          '@query' => $query,
-        ];
-        $this->fail(t("The %setting setting is currently set to '%current_value', but needs to be '%needed_value'. Change this by running the following query: <code>@query</code>", $replacements));
-      }
-    }
-  }
-
-  /**
-   * Verifies the standard_conforming_strings setting.
-   */
-  protected function checkStandardConformingStringsSuccess() {
-    $standard_conforming_strings = Database::getConnection()->query("SHOW standard_conforming_strings")->fetchField();
-    return ($standard_conforming_strings == 'on');
-  }
-
-  /**
-   * Make PostgreSQL Drupal friendly.
-   */
-  public function initializeDatabase() {
-    // We create some functions using global names instead of prefixing them
-    // like we do with table names. This is so that we don't double up if more
-    // than one instance of Drupal is running on a single database. We therefore
-    // avoid trying to create them again in that case.
-    // At the same time checking for the existence of the function fixes
-    // concurrency issues, when both try to update at the same time.
-    try {
-      $connection = Database::getConnection();
-      // When testing, two installs might try to run the CREATE FUNCTION queries
-      // at the same time. Do not let that happen.
-      $connection->query('SELECT pg_advisory_lock(1)');
-      // Don't use {} around pg_proc table.
-      if (!$connection->query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'rand'")->fetchField()) {
-        $connection->query('CREATE OR REPLACE FUNCTION "rand"() RETURNS float AS
-          \'SELECT random();\'
-          LANGUAGE \'sql\'',
-          [],
-          ['allow_delimiter_in_query' => TRUE]
-        );
-      }
-
-      if (!$connection->query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'substring_index'")->fetchField()) {
-        $connection->query('CREATE OR REPLACE FUNCTION "substring_index"(text, text, integer) RETURNS text AS
-          \'SELECT array_to_string((string_to_array($1, $2)) [1:$3], $2);\'
-          LANGUAGE \'sql\'',
-          [],
-          ['allow_delimiter_in_query' => TRUE, 'allow_square_brackets' => TRUE]
-        );
-      }
-      $connection->query('SELECT pg_advisory_unlock(1)');
-
-      $this->pass(t('PostgreSQL has initialized itself.'));
-    }
-    catch (\Exception $e) {
-      $this->fail(t('Drupal could not be correctly setup with the existing database due to the following error: @error.', ['@error' => $e->getMessage()]));
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormOptions(array $database) {
-    $form = parent::getFormOptions($database);
-    if (empty($form['advanced_options']['port']['#default_value'])) {
-      $form['advanced_options']['port']['#default_value'] = '5432';
-    }
-    return $form;
-  }
-
-}
+class Tasks extends PgsqlTasks {}
diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php
index 59795d39210c..2dbe382ec58b 100644
--- a/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php
+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php
@@ -2,1083 +2,16 @@
 
 namespace Drupal\Core\Database\Driver\pgsql;
 
-use Drupal\Core\Database\SchemaObjectExistsException;
-use Drupal\Core\Database\SchemaObjectDoesNotExistException;
-use Drupal\Core\Database\Schema as DatabaseSchema;
+use Drupal\pgsql\Driver\Database\pgsql\Schema as PgsqlSchema;
 
-// cSpell:ignore adbin adnum adrelid adsrc attisdropped attname attnum attrdef
-// cSpell:ignore attrelid atttypid atttypmod bigserial conkey conname conrelid
-// cSpell:ignore contype fillfactor indexname indexrelid indisprimary indkey
-// cSpell:ignore indrelid nextval nspname regclass relkind relname relnamespace
-// cSpell:ignore schemaname setval
-
-/**
- * @addtogroup schemaapi
- * @{
- */
+@trigger_error('\Drupal\Core\Database\Driver\pgsql\Schema is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED);
 
 /**
  * PostgreSQL implementation of \Drupal\Core\Database\Schema.
+ *
+ * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL
+ *   database driver has been moved to the pgsql module.
+ *
+ * @see https://www.drupal.org/node/3129492
  */
-class Schema extends DatabaseSchema {
-
-  /**
-   * A cache of information about blob columns and sequences of tables.
-   *
-   * This is collected by Schema::queryTableInformation(), by introspecting the
-   * database.
-   *
-   * @see \Drupal\Core\Database\Driver\pgsql\Schema::queryTableInformation()
-   * @var array
-   */
-  protected $tableInformation = [];
-
-  /**
-   * The maximum allowed length for index, primary key and constraint names.
-   *
-   * Value will usually be set to a 63 chars limit but PostgreSQL allows
-   * to higher this value before compiling, so we need to check for that.
-   *
-   * @var int
-   */
-  protected $maxIdentifierLength;
-
-  /**
-   * PostgreSQL's temporary namespace name.
-   *
-   * @var string
-   */
-  protected $tempNamespaceName;
-
-  /**
-   * Make sure to limit identifiers according to PostgreSQL compiled in length.
-   *
-   * PostgreSQL allows in standard configuration identifiers no longer than 63
-   * chars for table/relation names, indexes, primary keys, and constraints. So
-   * we map all identifiers that are too long to drupal_base64hash_tag, where
-   * tag is one of:
-   *   - idx for indexes
-   *   - key for constraints
-   *   - pkey for primary keys
-   *   - seq for sequences
-   *
-   * @param string $table_identifier_part
-   *   The first argument used to build the identifier string. This usually
-   *   refers to a table/relation name.
-   * @param string $column_identifier_part
-   *   The second argument used to build the identifier string. This usually
-   *   refers to one or more column names.
-   * @param string $tag
-   *   The identifier tag. It can be one of 'idx', 'key', 'pkey' or 'seq'.
-   * @param string $separator
-   *   (optional) The separator used to glue together the aforementioned
-   *   identifier parts. Defaults to '__'.
-   *
-   * @return string
-   *   The index/constraint/pkey identifier.
-   */
-  protected function ensureIdentifiersLength($table_identifier_part, $column_identifier_part, $tag, $separator = '__') {
-    $info = $this->getPrefixInfo($table_identifier_part);
-    $table_identifier_part = $info['table'];
-    $identifierName = implode($separator, [$table_identifier_part, $column_identifier_part, $tag]);
-
-    // Retrieve the max identifier length which is usually 63 characters
-    // but can be altered before PostgreSQL is compiled so we need to check.
-    if (empty($this->maxIdentifierLength)) {
-      $this->maxIdentifierLength = $this->connection->query("SHOW max_identifier_length")->fetchField();
-    }
-
-    if (strlen($identifierName) > $this->maxIdentifierLength) {
-      $saveIdentifier = '"drupal_' . $this->hashBase64($identifierName) . '_' . $tag . '"';
-    }
-    else {
-      $saveIdentifier = $identifierName;
-    }
-    return $saveIdentifier;
-  }
-
-  /**
-   * Fetch the list of blobs and sequences used on a table.
-   *
-   * We introspect the database to collect the information required by insert
-   * and update queries.
-   *
-   * @param string $table
-   *   The non-prefixed name of the table.
-   *
-   * @return mixed|object
-   *   An object with two member variables:
-   *   - 'blob_fields' that lists all the blob fields in the table.
-   *   - 'sequences' that lists the sequences used in that table.
-   *
-   * @throws \Exception
-   *   Exception thrown when the query for the table information fails.
-   */
-  public function queryTableInformation($table) {
-    // Generate a key to reference this table's information on.
-    $key = $this->connection->prefixTables('{' . $table . '}');
-
-    // Take into account that temporary tables are stored in a different schema.
-    // \Drupal\Core\Database\Connection::generateTemporaryTableName() sets the
-    // 'db_temporary_' prefix to all temporary tables.
-    if (strpos($key, '.') === FALSE && strpos($table, 'db_temporary_') === FALSE) {
-      $key = 'public.' . $key;
-    }
-    else {
-      $key = $this->getTempNamespaceName() . '.' . $key;
-    }
-
-    if (!isset($this->tableInformation[$key])) {
-      $table_information = (object) [
-        'blob_fields' => [],
-        'sequences' => [],
-      ];
-      $this->connection->addSavepoint();
-
-      try {
-        // The bytea columns and sequences for a table can be found in
-        // pg_attribute, which is significantly faster than querying the
-        // information_schema. The data type of a field can be found by lookup
-        // of the attribute ID, and the default value must be extracted from the
-        // node tree for the attribute definition instead of the historical
-        // human-readable column, adsrc.
-        $sql = <<<'EOD'
-SELECT pg_attribute.attname AS column_name, format_type(pg_attribute.atttypid, pg_attribute.atttypmod) AS data_type, pg_get_expr(pg_attrdef.adbin, pg_attribute.attrelid) AS column_default
-FROM pg_attribute
-LEFT JOIN pg_attrdef ON pg_attrdef.adrelid = pg_attribute.attrelid AND pg_attrdef.adnum = pg_attribute.attnum
-WHERE pg_attribute.attnum > 0
-AND NOT pg_attribute.attisdropped
-AND pg_attribute.attrelid = :key::regclass
-AND (format_type(pg_attribute.atttypid, pg_attribute.atttypmod) = 'bytea'
-OR pg_get_expr(pg_attrdef.adbin, pg_attribute.attrelid) LIKE 'nextval%')
-EOD;
-        $result = $this->connection->query($sql, [
-          ':key' => $key,
-        ]);
-      }
-      catch (\Exception $e) {
-        $this->connection->rollbackSavepoint();
-        throw $e;
-      }
-      $this->connection->releaseSavepoint();
-
-      // If the table information does not yet exist in the PostgreSQL
-      // metadata, then return the default table information here, so that it
-      // will not be cached.
-      if (empty($result)) {
-        return $table_information;
-      }
-
-      foreach ($result as $column) {
-        if ($column->data_type == 'bytea') {
-          $table_information->blob_fields[$column->column_name] = TRUE;
-        }
-        elseif (preg_match("/nextval\('([^']+)'/", $column->column_default, $matches)) {
-          // We must know of any sequences in the table structure to help us
-          // return the last insert id. If there is more than 1 sequences the
-          // first one (index 0 of the sequences array) will be used.
-          $table_information->sequences[] = $matches[1];
-          $table_information->serial_fields[] = $column->column_name;
-        }
-      }
-      $this->tableInformation[$key] = $table_information;
-    }
-    return $this->tableInformation[$key];
-  }
-
-  /**
-   * Gets PostgreSQL's temporary namespace name.
-   *
-   * @return string
-   *   PostgreSQL's temporary namespace name.
-   */
-  protected function getTempNamespaceName() {
-    if (!isset($this->tempNamespaceName)) {
-      $this->tempNamespaceName = $this->connection->query('SELECT nspname FROM pg_namespace WHERE oid = pg_my_temp_schema()')->fetchField();
-    }
-    return $this->tempNamespaceName;
-  }
-
-  /**
-   * Resets information about table blobs, sequences and serial fields.
-   *
-   * @param $table
-   *   The non-prefixed name of the table.
-   */
-  protected function resetTableInformation($table) {
-    $key = $this->connection->prefixTables('{' . $table . '}');
-    if (strpos($key, '.') === FALSE) {
-      $key = 'public.' . $key;
-    }
-    unset($this->tableInformation[$key]);
-  }
-
-  /**
-   * Fetches the list of constraints used on a field.
-   *
-   * We introspect the database to collect the information required by field
-   * alteration.
-   *
-   * @param string $table
-   *   The non-prefixed name of the table.
-   * @param string $field
-   *   The name of the field.
-   * @param string $constraint_type
-   *   (optional) The type of the constraint. This can be one of the following:
-   *   - c: check constraint;
-   *   - f: foreign key constraint;
-   *   - p: primary key constraint;
-   *   - u: unique constraint;
-   *   - t: constraint trigger;
-   *   - x: exclusion constraint.
-   *   Defaults to 'c' for a CHECK constraint.
-   *   @see https://www.postgresql.org/docs/current/catalog-pg-constraint.html
-   *
-   * @return array
-   *   An array containing all the constraint names for the field.
-   *
-   * @throws \Exception
-   *   Exception thrown when the query for the table information fails.
-   */
-  public function queryFieldInformation($table, $field, $constraint_type = 'c') {
-    assert(in_array($constraint_type, ['c', 'f', 'p', 'u', 't', 'x']));
-    $prefixInfo = $this->getPrefixInfo($table, TRUE);
-
-    // Split the key into schema and table for querying.
-    $schema = $prefixInfo['schema'];
-    $table_name = $prefixInfo['table'];
-
-    $this->connection->addSavepoint();
-
-    try {
-      $checks = $this->connection->query("SELECT conname FROM pg_class cl INNER JOIN pg_constraint co ON co.conrelid = cl.oid INNER JOIN pg_attribute attr ON attr.attrelid = cl.oid AND attr.attnum = ANY (co.conkey) INNER JOIN pg_namespace ns ON cl.relnamespace = ns.oid WHERE co.contype = :constraint_type AND ns.nspname = :schema AND cl.relname = :table AND attr.attname = :column", [
-        ':constraint_type' => $constraint_type,
-        ':schema' => $schema,
-        ':table' => $table_name,
-        ':column' => $field,
-      ]);
-    }
-    catch (\Exception $e) {
-      $this->connection->rollbackSavepoint();
-      throw $e;
-    }
-
-    $this->connection->releaseSavepoint();
-
-    $field_information = $checks->fetchCol();
-
-    return $field_information;
-  }
-
-  /**
-   * Generate SQL to create a new table from a Drupal schema definition.
-   *
-   * @param string $name
-   *   The name of the table to create.
-   * @param array $table
-   *   A Schema API table definition array.
-   *
-   * @return array
-   *   An array of SQL statements to create the table.
-   */
-  protected function createTableSql($name, $table) {
-    $sql_fields = [];
-    foreach ($table['fields'] as $field_name => $field) {
-      $sql_fields[] = $this->createFieldSql($field_name, $this->processField($field));
-    }
-
-    $sql_keys = [];
-    if (!empty($table['primary key']) && is_array($table['primary key'])) {
-      $this->ensureNotNullPrimaryKey($table['primary key'], $table['fields']);
-      $sql_keys[] = 'CONSTRAINT ' . $this->ensureIdentifiersLength($name, '', 'pkey') . ' PRIMARY KEY (' . $this->createPrimaryKeySql($table['primary key']) . ')';
-    }
-    if (isset($table['unique keys']) && is_array($table['unique keys'])) {
-      foreach ($table['unique keys'] as $key_name => $key) {
-        $sql_keys[] = 'CONSTRAINT ' . $this->ensureIdentifiersLength($name, $key_name, 'key') . ' UNIQUE (' . implode(', ', $key) . ')';
-      }
-    }
-
-    $sql = "CREATE TABLE {" . $name . "} (\n\t";
-    $sql .= implode(",\n\t", $sql_fields);
-    if (count($sql_keys) > 0) {
-      $sql .= ",\n\t";
-    }
-    $sql .= implode(",\n\t", $sql_keys);
-    $sql .= "\n)";
-    $statements[] = $sql;
-
-    if (isset($table['indexes']) && is_array($table['indexes'])) {
-      foreach ($table['indexes'] as $key_name => $key) {
-        $statements[] = $this->_createIndexSql($name, $key_name, $key);
-      }
-    }
-
-    // Add table comment.
-    if (!empty($table['description'])) {
-      $statements[] = 'COMMENT ON TABLE {' . $name . '} IS ' . $this->prepareComment($table['description']);
-    }
-
-    // Add column comments.
-    foreach ($table['fields'] as $field_name => $field) {
-      if (!empty($field['description'])) {
-        $statements[] = 'COMMENT ON COLUMN {' . $name . '}.' . $field_name . ' IS ' . $this->prepareComment($field['description']);
-      }
-    }
-
-    return $statements;
-  }
-
-  /**
-   * Create an SQL string for a field to be used in table creation or
-   * alteration.
-   *
-   * @param $name
-   *   Name of the field.
-   * @param $spec
-   *   The field specification, as per the schema data structure format.
-   */
-  protected function createFieldSql($name, $spec) {
-    // The PostgreSQL server converts names into lowercase, unless quoted.
-    $sql = '"' . $name . '" ' . $spec['pgsql_type'];
-
-    if (isset($spec['type']) && $spec['type'] == 'serial') {
-      unset($spec['not null']);
-    }
-
-    if (in_array($spec['pgsql_type'], ['varchar', 'character']) && isset($spec['length'])) {
-      $sql .= '(' . $spec['length'] . ')';
-    }
-    elseif (isset($spec['precision']) && isset($spec['scale'])) {
-      $sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')';
-    }
-
-    if (!empty($spec['unsigned'])) {
-      $sql .= " CHECK ($name >= 0)";
-    }
-
-    if (isset($spec['not null'])) {
-      if ($spec['not null']) {
-        $sql .= ' NOT NULL';
-      }
-      else {
-        $sql .= ' NULL';
-      }
-    }
-    if (array_key_exists('default', $spec)) {
-      $default = $this->escapeDefaultValue($spec['default']);
-      $sql .= " default $default";
-    }
-
-    return $sql;
-  }
-
-  /**
-   * Set database-engine specific properties for a field.
-   *
-   * @param $field
-   *   A field description array, as specified in the schema documentation.
-   */
-  protected function processField($field) {
-    if (!isset($field['size'])) {
-      $field['size'] = 'normal';
-    }
-
-    // Set the correct database-engine specific datatype.
-    // In case one is already provided, force it to lowercase.
-    if (isset($field['pgsql_type'])) {
-      $field['pgsql_type'] = mb_strtolower($field['pgsql_type']);
-    }
-    else {
-      $map = $this->getFieldTypeMap();
-      $field['pgsql_type'] = $map[$field['type'] . ':' . $field['size']];
-    }
-
-    if (!empty($field['unsigned'])) {
-      // Unsigned data types are not supported in PostgreSQL 10. In MySQL,
-      // they are used to ensure a positive number is inserted and it also
-      // doubles the maximum integer size that can be stored in a field.
-      // The PostgreSQL schema in Drupal creates a check constraint
-      // to ensure that a value inserted is >= 0. To provide the extra
-      // integer capacity, here, we bump up the column field size.
-      if (!isset($map)) {
-        $map = $this->getFieldTypeMap();
-      }
-      switch ($field['pgsql_type']) {
-        case 'smallint':
-          $field['pgsql_type'] = $map['int:medium'];
-          break;
-
-        case 'int':
-          $field['pgsql_type'] = $map['int:big'];
-          break;
-      }
-    }
-    if (isset($field['type']) && $field['type'] == 'serial') {
-      unset($field['not null']);
-    }
-    return $field;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFieldTypeMap() {
-    // Put :normal last so it gets preserved by array_flip. This makes
-    // it much easier for modules (such as schema.module) to map
-    // database types back into schema types.
-    // $map does not use drupal_static as its value never changes.
-    static $map = [
-      'varchar_ascii:normal' => 'varchar',
-
-      'varchar:normal' => 'varchar',
-      'char:normal' => 'character',
-
-      'text:tiny' => 'text',
-      'text:small' => 'text',
-      'text:medium' => 'text',
-      'text:big' => 'text',
-      'text:normal' => 'text',
-
-      'int:tiny' => 'smallint',
-      'int:small' => 'smallint',
-      'int:medium' => 'int',
-      'int:big' => 'bigint',
-      'int:normal' => 'int',
-
-      'float:tiny' => 'real',
-      'float:small' => 'real',
-      'float:medium' => 'real',
-      'float:big' => 'double precision',
-      'float:normal' => 'real',
-
-      'numeric:normal' => 'numeric',
-
-      'blob:big' => 'bytea',
-      'blob:normal' => 'bytea',
-
-      'serial:tiny' => 'serial',
-      'serial:small' => 'serial',
-      'serial:medium' => 'serial',
-      'serial:big' => 'bigserial',
-      'serial:normal' => 'serial',
-      ];
-    return $map;
-  }
-
-  protected function _createKeySql($fields) {
-    $return = [];
-    foreach ($fields as $field) {
-      if (is_array($field)) {
-        $return[] = 'substr(' . $field[0] . ', 1, ' . $field[1] . ')';
-      }
-      else {
-        $return[] = '"' . $field . '"';
-      }
-    }
-    return implode(', ', $return);
-  }
-
-  /**
-   * Create the SQL expression for primary keys.
-   *
-   * Postgresql does not support key length. It does support fillfactor, but
-   * that requires a separate database lookup for each column in the key. The
-   * key length defined in the schema is ignored.
-   */
-  protected function createPrimaryKeySql($fields) {
-    $return = [];
-    foreach ($fields as $field) {
-      if (is_array($field)) {
-        $return[] = '"' . $field[0] . '"';
-      }
-      else {
-        $return[] = '"' . $field . '"';
-      }
-    }
-    return implode(', ', $return);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function tableExists($table) {
-    $prefixInfo = $this->getPrefixInfo($table, TRUE);
-
-    return (bool) $this->connection->query("SELECT 1 FROM pg_tables WHERE schemaname = :schema AND tablename = :table", [':schema' => $prefixInfo['schema'], ':table' => $prefixInfo['table']])->fetchField();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function findTables($table_expression) {
-    $individually_prefixed_tables = $this->connection->getUnprefixedTablesMap();
-    $default_prefix = $this->connection->tablePrefix();
-    $default_prefix_length = strlen($default_prefix);
-    $tables = [];
-
-    // Load all the tables up front in order to take into account per-table
-    // prefixes. The actual matching is done at the bottom of the method.
-    $results = $this->connection->query("SELECT tablename FROM pg_tables WHERE schemaname = :schema", [':schema' => $this->defaultSchema]);
-    foreach ($results as $table) {
-      // Take into account tables that have an individual prefix.
-      if (isset($individually_prefixed_tables[$table->tablename])) {
-        $prefix_length = strlen($this->connection->tablePrefix($individually_prefixed_tables[$table->tablename]));
-      }
-      elseif ($default_prefix && substr($table->tablename, 0, $default_prefix_length) !== $default_prefix) {
-        // This table name does not start the default prefix, which means that
-        // it is not managed by Drupal so it should be excluded from the result.
-        continue;
-      }
-      else {
-        $prefix_length = $default_prefix_length;
-      }
-
-      // Remove the prefix from the returned tables.
-      $unprefixed_table_name = substr($table->tablename, $prefix_length);
-
-      // The pattern can match a table which is the same as the prefix. That
-      // will become an empty string when we remove the prefix, which will
-      // probably surprise the caller, besides not being a prefixed table. So
-      // remove it.
-      if (!empty($unprefixed_table_name)) {
-        $tables[$unprefixed_table_name] = $unprefixed_table_name;
-      }
-    }
-
-    // Convert the table expression from its SQL LIKE syntax to a regular
-    // expression and escape the delimiter that will be used for matching.
-    $table_expression = str_replace(['%', '_'], ['.*?', '.'], preg_quote($table_expression, '/'));
-    $tables = preg_grep('/^' . $table_expression . '$/i', $tables);
-
-    return $tables;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function renameTable($table, $new_name) {
-    if (!$this->tableExists($table)) {
-      throw new SchemaObjectDoesNotExistException("Cannot rename '$table' to '$new_name': table '$table' doesn't exist.");
-    }
-    if ($this->tableExists($new_name)) {
-      throw new SchemaObjectExistsException("Cannot rename '$table' to '$new_name': table '$new_name' already exists.");
-    }
-
-    // Get the schema and tablename for the old table.
-    $old_full_name = str_replace('"', '', $this->connection->prefixTables('{' . $table . '}'));
-    [$old_schema, $old_table_name] = strpos($old_full_name, '.') ? explode('.', $old_full_name) : ['public', $old_full_name];
-
-    // Index names and constraint names are global in PostgreSQL, so we need to
-    // rename them when renaming the table.
-    $indexes = $this->connection->query('SELECT indexname FROM pg_indexes WHERE schemaname = :schema AND tablename = :table', [':schema' => $old_schema, ':table' => $old_table_name]);
-
-    foreach ($indexes as $index) {
-      // Get the index type by suffix, e.g. idx/key/pkey
-      $index_type = substr($index->indexname, strrpos($index->indexname, '_') + 1);
-
-      // If the index is already rewritten by ensureIdentifiersLength() to not
-      // exceed the 63 chars limit of PostgreSQL, we need to take care of that.
-      // cSpell:disable-next-line
-      // Example (drupal_Gk7Su_T1jcBHVuvSPeP22_I3Ni4GrVEgTYlIYnBJkro_idx).
-      if (strpos($index->indexname, 'drupal_') !== FALSE) {
-        preg_match('/^drupal_(.*)_' . preg_quote($index_type) . '/', $index->indexname, $matches);
-        $index_name = $matches[1];
-      }
-      else {
-        // Make sure to remove the suffix from index names, because
-        // $this->ensureIdentifiersLength() will add the suffix again and thus
-        // would result in a wrong index name.
-        preg_match('/^' . preg_quote($old_full_name) . '__(.*)__' . preg_quote($index_type) . '/', $index->indexname, $matches);
-        $index_name = $matches[1];
-      }
-      $this->connection->query('ALTER INDEX "' . $index->indexname . '" RENAME TO ' . $this->ensureIdentifiersLength($new_name, $index_name, $index_type) . '');
-    }
-
-    // Ensure the new table name does not include schema syntax.
-    $prefixInfo = $this->getPrefixInfo($new_name);
-
-    // Rename sequences if the table contains serial fields.
-    $info = $this->queryTableInformation($table);
-    if (!empty($info->serial_fields)) {
-      foreach ($info->serial_fields as $field) {
-        // The initial name of the sequence is generated automatically by
-        // PostgreSQL when the table is created, so we need to use
-        // pg_get_serial_sequence() to retrieve it.
-        $old_sequence = $this->connection->query("SELECT pg_get_serial_sequence('" . $old_full_name . "', '" . $field . "')")->fetchField();
-
-        // If the new sequence name exceeds the maximum identifier length limit,
-        // it will not match the pattern that is automatically applied by
-        // PostgreSQL on table creation, but that's ok because
-        // pg_get_serial_sequence() will return our non-standard name on
-        // subsequent table renames.
-        $new_sequence = $this->ensureIdentifiersLength($new_name, $field, 'seq', '_');
-
-        $this->connection->query('ALTER SEQUENCE ' . $old_sequence . ' RENAME TO ' . $new_sequence);
-      }
-    }
-    // Now rename the table.
-    $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO ' . $prefixInfo['table']);
-    $this->resetTableInformation($table);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function dropTable($table) {
-    if (!$this->tableExists($table)) {
-      return FALSE;
-    }
-
-    $this->connection->query('DROP TABLE {' . $table . '}');
-    $this->resetTableInformation($table);
-    return TRUE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function addField($table, $field, $spec, $new_keys = []) {
-    if (!$this->tableExists($table)) {
-      throw new SchemaObjectDoesNotExistException("Cannot add field '$table.$field': table doesn't exist.");
-    }
-    if ($this->fieldExists($table, $field)) {
-      throw new SchemaObjectExistsException("Cannot add field '$table.$field': field already exists.");
-    }
-
-    // Fields that are part of a PRIMARY KEY must be added as NOT NULL.
-    $is_primary_key = isset($new_keys['primary key']) && in_array($field, $new_keys['primary key'], TRUE);
-    if ($is_primary_key) {
-      $this->ensureNotNullPrimaryKey($new_keys['primary key'], [$field => $spec]);
-    }
-
-    $fixnull = FALSE;
-    if (!empty($spec['not null']) && !isset($spec['default']) && !$is_primary_key) {
-      $fixnull = TRUE;
-      $spec['not null'] = FALSE;
-    }
-    $query = 'ALTER TABLE {' . $table . '} ADD COLUMN ';
-    $query .= $this->createFieldSql($field, $this->processField($spec));
-    $this->connection->query($query);
-    if (isset($spec['initial_from_field'])) {
-      if (isset($spec['initial'])) {
-        $expression = 'COALESCE(' . $spec['initial_from_field'] . ', :default_initial_value)';
-        $arguments = [':default_initial_value' => $spec['initial']];
-      }
-      else {
-        $expression = $spec['initial_from_field'];
-        $arguments = [];
-      }
-      $this->connection->update($table)
-        ->expression($field, $expression, $arguments)
-        ->execute();
-    }
-    elseif (isset($spec['initial'])) {
-      $this->connection->update($table)
-        ->fields([$field => $spec['initial']])
-        ->execute();
-    }
-    if ($fixnull) {
-      $this->connection->query("ALTER TABLE {" . $table . "} ALTER $field SET NOT NULL");
-    }
-    if (isset($new_keys)) {
-      // Make sure to drop the existing primary key before adding a new one.
-      // This is only needed when adding a field because this method, unlike
-      // changeField(), is supposed to handle primary keys automatically.
-      if (isset($new_keys['primary key']) && $this->constraintExists($table, 'pkey')) {
-        $this->dropPrimaryKey($table);
-      }
-      $this->_createKeys($table, $new_keys);
-    }
-    // Add column comment.
-    if (!empty($spec['description'])) {
-      $this->connection->query('COMMENT ON COLUMN {' . $table . '}.' . $field . ' IS ' . $this->prepareComment($spec['description']));
-    }
-    $this->resetTableInformation($table);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function dropField($table, $field) {
-    if (!$this->fieldExists($table, $field)) {
-      return FALSE;
-    }
-
-    $this->connection->query('ALTER TABLE {' . $table . '} DROP COLUMN "' . $field . '"');
-    $this->resetTableInformation($table);
-    return TRUE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function fieldExists($table, $column) {
-    $prefixInfo = $this->getPrefixInfo($table);
-
-    return (bool) $this->connection->query("SELECT 1 FROM pg_attribute WHERE attrelid = :key::regclass AND attname = :column AND NOT attisdropped AND attnum > 0", [':key' => $prefixInfo['schema'] . '.' . $prefixInfo['table'], ':column' => $column])->fetchField();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function indexExists($table, $name) {
-    // Details https://www.postgresql.org/docs/10/view-pg-indexes.html
-    $index_name = $this->ensureIdentifiersLength($table, $name, 'idx');
-    // Remove leading and trailing quotes because the index name is in a WHERE
-    // clause and not used as an identifier.
-    $index_name = str_replace('"', '', $index_name);
-    return (bool) $this->connection->query("SELECT 1 FROM pg_indexes WHERE indexname = '$index_name'")->fetchField();
-  }
-
-  /**
-   * Helper function: check if a constraint (PK, FK, UK) exists.
-   *
-   * @param string $table
-   *   The name of the table.
-   * @param string $name
-   *   The name of the constraint (typically 'pkey' or '[constraint]__key').
-   *
-   * @return bool
-   *   TRUE if the constraint exists, FALSE otherwise.
-   */
-  public function constraintExists($table, $name) {
-    // ::ensureIdentifiersLength() expects three parameters, although not
-    // explicitly stated in its signature, thus we split our constraint name in
-    // a proper name and a suffix.
-    if ($name == 'pkey') {
-      $suffix = $name;
-      $name = '';
-    }
-    else {
-      $pos = strrpos($name, '__');
-      $suffix = substr($name, $pos + 2);
-      $name = substr($name, 0, $pos);
-    }
-    $constraint_name = $this->ensureIdentifiersLength($table, $name, $suffix);
-    // Remove leading and trailing quotes because the index name is in a WHERE
-    // clause and not used as an identifier.
-    $constraint_name = str_replace('"', '', $constraint_name);
-    return (bool) $this->connection->query("SELECT 1 FROM pg_constraint WHERE conname = '$constraint_name'")->fetchField();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function addPrimaryKey($table, $fields) {
-    if (!$this->tableExists($table)) {
-      throw new SchemaObjectDoesNotExistException("Cannot add primary key to table '$table': table doesn't exist.");
-    }
-    if ($this->constraintExists($table, 'pkey')) {
-      throw new SchemaObjectExistsException("Cannot add primary key to table '$table': primary key already exists.");
-    }
-
-    $this->connection->query('ALTER TABLE {' . $table . '} ADD CONSTRAINT ' . $this->ensureIdentifiersLength($table, '', 'pkey') . ' PRIMARY KEY (' . $this->createPrimaryKeySql($fields) . ')');
-    $this->resetTableInformation($table);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function dropPrimaryKey($table) {
-    if (!$this->constraintExists($table, 'pkey')) {
-      return FALSE;
-    }
-
-    $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT ' . $this->ensureIdentifiersLength($table, '', 'pkey'));
-    $this->resetTableInformation($table);
-    return TRUE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function findPrimaryKeyColumns($table) {
-    if (!$this->tableExists($table)) {
-      return FALSE;
-    }
-    return $this->connection->query("SELECT array_position(i.indkey, a.attnum) AS position, a.attname FROM pg_index i JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) WHERE i.indrelid = '{" . $table . "}'::regclass AND i.indisprimary ORDER BY position")->fetchAllKeyed();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function addUniqueKey($table, $name, $fields) {
-    if (!$this->tableExists($table)) {
-      throw new SchemaObjectDoesNotExistException("Cannot add unique key '$name' to table '$table': table doesn't exist.");
-    }
-    if ($this->constraintExists($table, $name . '__key')) {
-      throw new SchemaObjectExistsException("Cannot add unique key '$name' to table '$table': unique key already exists.");
-    }
-
-    $this->connection->query('ALTER TABLE {' . $table . '} ADD CONSTRAINT ' . $this->ensureIdentifiersLength($table, $name, 'key') . ' UNIQUE (' . implode(',', $fields) . ')');
-    $this->resetTableInformation($table);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function dropUniqueKey($table, $name) {
-    if (!$this->constraintExists($table, $name . '__key')) {
-      return FALSE;
-    }
-
-    $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT ' . $this->ensureIdentifiersLength($table, $name, 'key'));
-    $this->resetTableInformation($table);
-    return TRUE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function addIndex($table, $name, $fields, array $spec) {
-    if (!$this->tableExists($table)) {
-      throw new SchemaObjectDoesNotExistException("Cannot add index '$name' to table '$table': table doesn't exist.");
-    }
-    if ($this->indexExists($table, $name)) {
-      throw new SchemaObjectExistsException("Cannot add index '$name' to table '$table': index already exists.");
-    }
-
-    $this->connection->query($this->_createIndexSql($table, $name, $fields));
-    $this->resetTableInformation($table);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function dropIndex($table, $name) {
-    if (!$this->indexExists($table, $name)) {
-      return FALSE;
-    }
-
-    $this->connection->query('DROP INDEX ' . $this->ensureIdentifiersLength($table, $name, 'idx'));
-    $this->resetTableInformation($table);
-    return TRUE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function introspectIndexSchema($table) {
-    if (!$this->tableExists($table)) {
-      throw new SchemaObjectDoesNotExistException("The table $table doesn't exist.");
-    }
-
-    $index_schema = [
-      'primary key' => [],
-      'unique keys' => [],
-      'indexes' => [],
-    ];
-
-    // Get the schema and tablename for the table without identifier quotes.
-    $full_name = str_replace('"', '', $this->connection->prefixTables('{' . $table . '}'));
-    $result = $this->connection->query("SELECT i.relname AS index_name, a.attname AS column_name FROM pg_class t, pg_class i, pg_index ix, pg_attribute a WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND a.attnum = ANY(ix.indkey) AND t.relkind = 'r' AND t.relname = :table_name ORDER BY index_name ASC, column_name ASC", [
-      ':table_name' => $full_name,
-    ])->fetchAll();
-    foreach ($result as $row) {
-      if (preg_match('/_pkey$/', $row->index_name)) {
-        $index_schema['primary key'][] = $row->column_name;
-      }
-      elseif (preg_match('/_key$/', $row->index_name)) {
-        $index_schema['unique keys'][$row->index_name][] = $row->column_name;
-      }
-      elseif (preg_match('/_idx$/', $row->index_name)) {
-        $index_schema['indexes'][$row->index_name][] = $row->column_name;
-      }
-    }
-
-    return $index_schema;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function changeField($table, $field, $field_new, $spec, $new_keys = []) {
-    if (!$this->fieldExists($table, $field)) {
-      throw new SchemaObjectDoesNotExistException("Cannot change the definition of field '$table.$field': field doesn't exist.");
-    }
-    if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
-      throw new SchemaObjectExistsException("Cannot rename field '$table.$field' to '$field_new': target field already exists.");
-    }
-    if (isset($new_keys['primary key']) && in_array($field_new, $new_keys['primary key'], TRUE)) {
-      $this->ensureNotNullPrimaryKey($new_keys['primary key'], [$field_new => $spec]);
-    }
-
-    $spec = $this->processField($spec);
-
-    // Type 'serial' is known to PostgreSQL, but only during table creation,
-    // not when altering. Because of that, we create it here as an 'int'. After
-    // we create it we manually re-apply the sequence.
-    if (in_array($spec['pgsql_type'], ['serial', 'bigserial'])) {
-      $field_def = 'int';
-    }
-    else {
-      $field_def = $spec['pgsql_type'];
-    }
-
-    if (in_array($spec['pgsql_type'], ['varchar', 'character', 'text']) && isset($spec['length'])) {
-      $field_def .= '(' . $spec['length'] . ')';
-    }
-    elseif (isset($spec['precision']) && isset($spec['scale'])) {
-      $field_def .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')';
-    }
-
-    // Remove old check constraints.
-    $field_info = $this->queryFieldInformation($table, $field);
-
-    foreach ($field_info as $check) {
-      $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT "' . $check . '"');
-    }
-
-    // Remove old default.
-    $this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN "' . $field . '" DROP DEFAULT');
-
-    // Convert field type.
-    // Usually, we do this via a simple typecast 'USING fieldname::type'. But
-    // the typecast does not work for conversions to bytea.
-    // @see http://www.postgresql.org/docs/current/static/datatype-binary.html
-    $table_information = $this->queryTableInformation($table);
-    $is_bytea = !empty($table_information->blob_fields[$field]);
-    if ($spec['pgsql_type'] != 'bytea') {
-      if ($is_bytea) {
-        $this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $field_def . ' USING convert_from("' . $field . '"' . ", 'UTF8')");
-      }
-      else {
-        $this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $field_def . ' USING "' . $field . '"::' . $field_def);
-      }
-    }
-    else {
-      // Do not attempt to convert a field that is bytea already.
-      if (!$is_bytea) {
-        // Convert to a bytea type by using the SQL replace() function to
-        // convert any single backslashes in the field content to double
-        // backslashes ('\' to '\\').
-        $this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $field_def . ' USING decode(replace("' . $field . '"' . ", E'\\\\', E'\\\\\\\\'), 'escape');");
-      }
-    }
-
-    if (isset($spec['not null'])) {
-      if ($spec['not null']) {
-        $null_action = 'SET NOT NULL';
-      }
-      else {
-        $null_action = 'DROP NOT NULL';
-      }
-      $this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" ' . $null_action);
-    }
-
-    if (in_array($spec['pgsql_type'], ['serial', 'bigserial'])) {
-      // Type "serial" is known to PostgreSQL, but *only* during table creation,
-      // not when altering. Because of that, the sequence needs to be created
-      // and initialized by hand.
-      $seq = $this->connection->makeSequenceName($table, $field_new);
-      $this->connection->query("CREATE SEQUENCE " . $seq);
-      // Set sequence to maximal field value to not conflict with existing
-      // entries.
-      $this->connection->query("SELECT setval('" . $seq . "', MAX(\"" . $field . '")) FROM {' . $table . "}");
-      $this->connection->query('ALTER TABLE {' . $table . '} ALTER ' . $field . ' SET DEFAULT nextval(' . $this->connection->quote($seq) . ')');
-    }
-
-    // Rename the column if necessary.
-    if ($field != $field_new) {
-      $this->connection->query('ALTER TABLE {' . $table . '} RENAME "' . $field . '" TO "' . $field_new . '"');
-    }
-
-    // Add unsigned check if necessary.
-    if (!empty($spec['unsigned'])) {
-      $this->connection->query('ALTER TABLE {' . $table . '} ADD CHECK ("' . $field_new . '" >= 0)');
-    }
-
-    // Add default if necessary.
-    if (isset($spec['default'])) {
-      $this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN "' . $field_new . '" SET DEFAULT ' . $this->escapeDefaultValue($spec['default']));
-    }
-
-    // Change description if necessary.
-    if (!empty($spec['description'])) {
-      $this->connection->query('COMMENT ON COLUMN {' . $table . '}."' . $field_new . '" IS ' . $this->prepareComment($spec['description']));
-    }
-
-    if (isset($new_keys)) {
-      $this->_createKeys($table, $new_keys);
-    }
-    $this->resetTableInformation($table);
-  }
-
-  protected function _createIndexSql($table, $name, $fields) {
-    $query = 'CREATE INDEX ' . $this->ensureIdentifiersLength($table, $name, 'idx') . ' ON {' . $table . '} (';
-    $query .= $this->_createKeySql($fields) . ')';
-    return $query;
-  }
-
-  protected function _createKeys($table, $new_keys) {
-    if (isset($new_keys['primary key'])) {
-      $this->addPrimaryKey($table, $new_keys['primary key']);
-    }
-    if (isset($new_keys['unique keys'])) {
-      foreach ($new_keys['unique keys'] as $name => $fields) {
-        $this->addUniqueKey($table, $name, $fields);
-      }
-    }
-    if (isset($new_keys['indexes'])) {
-      foreach ($new_keys['indexes'] as $name => $fields) {
-        // Even though $new_keys is not a full schema it still has 'indexes' and
-        // so is a partial schema. Technically addIndex() doesn't do anything
-        // with it so passing an empty array would work as well.
-        $this->addIndex($table, $name, $fields, $new_keys);
-      }
-    }
-  }
-
-  /**
-   * Retrieve a table or column comment.
-   */
-  public function getComment($table, $column = NULL) {
-    $info = $this->getPrefixInfo($table);
-    // Don't use {} around pg_class, pg_attribute tables.
-    if (isset($column)) {
-      return $this->connection->query('SELECT col_description(oid, attnum) FROM pg_class, pg_attribute WHERE attrelid = oid AND relname = ? AND attname = ?', [$info['table'], $column])->fetchField();
-    }
-    else {
-      return $this->connection->query('SELECT obj_description(oid, ?) FROM pg_class WHERE relname = ?', ['pg_class', $info['table']])->fetchField();
-    }
-  }
-
-  /**
-   * Calculates a base-64 encoded, PostgreSQL-safe sha-256 hash per PostgreSQL
-   * documentation: 4.1. Lexical Structure.
-   *
-   * @param $data
-   *   String to be hashed.
-   *
-   * @return string
-   *   A base-64 encoded sha-256 hash, with + and / replaced with _ and any =
-   *   padding characters removed.
-   */
-  protected function hashBase64($data) {
-    $hash = base64_encode(hash('sha256', $data, TRUE));
-    // Modify the hash so it's safe to use in PostgreSQL identifiers.
-    return strtr($hash, ['+' => '_', '/' => '_', '=' => '']);
-  }
-
-  /**
-   * Determines whether the PostgreSQL extension is created.
-   *
-   * @param string $name
-   *   The name of the extension.
-   *
-   * @return bool
-   *   Return TRUE when the extension is created, FALSE otherwise.
-   *
-   * @internal
-   */
-  public function extensionExists($name): bool {
-    return (bool) $this->connection->query('SELECT installed_version FROM pg_available_extensions WHERE name = :name', [
-      ':name' => $name,
-    ])->fetchField();
-  }
-
-}
-
-/**
- * @} End of "addtogroup schemaapi".
- */
+class Schema extends PgsqlSchema {}
diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Select.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Select.php
index 2a9bc4a58726..169aaed84410 100644
--- a/core/lib/Drupal/Core/Database/Driver/pgsql/Select.php
+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Select.php
@@ -2,159 +2,16 @@
 
 namespace Drupal\Core\Database\Driver\pgsql;
 
-use Drupal\Core\Database\Query\Select as QuerySelect;
+use Drupal\pgsql\Driver\Database\pgsql\Select as PgsqlSelect;
 
-/**
- * @addtogroup database
- * @{
- */
+@trigger_error('\Drupal\Core\Database\Driver\pgsql\Select is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED);
 
 /**
  * PostgreSQL implementation of \Drupal\Core\Database\Query\Select.
+ *
+ * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL
+ *   database driver has been moved to the pgsql module.
+ *
+ * @see https://www.drupal.org/node/3129492
  */
-class Select extends QuerySelect {
-
-  public function orderRandom() {
-    $alias = $this->addExpression('RANDOM()', 'random_field');
-    $this->orderBy($alias);
-    return $this;
-  }
-
-  /**
-   * Overrides SelectQuery::orderBy().
-   *
-   * PostgreSQL adheres strictly to the SQL-92 standard and requires that when
-   * using DISTINCT or GROUP BY conditions, fields and expressions that are
-   * ordered on also need to be selected. This is a best effort implementation
-   * to handle the cases that can be automated by adding the field if it is not
-   * yet selected.
-   *
-   * @code
-   *   $query = \Drupal::database()->select('example', 'e');
-   *   $query->join('example_revision', 'er', '[e].[vid] = [er].[vid]');
-   *   $query
-   *     ->distinct()
-   *     ->fields('e')
-   *     ->orderBy('timestamp');
-   * @endcode
-   *
-   * In this query, it is not possible (without relying on the schema) to know
-   * whether timestamp belongs to example_revision and needs to be added or
-   * belongs to node and is already selected. Queries like this will need to be
-   * corrected in the original query by adding an explicit call to
-   * SelectQuery::addField() or SelectQuery::fields().
-   *
-   * Since this has a small performance impact, both by the additional
-   * processing in this function and in the database that needs to return the
-   * additional fields, this is done as an override instead of implementing it
-   * directly in SelectQuery::orderBy().
-   */
-  public function orderBy($field, $direction = 'ASC') {
-    // Only allow ASC and DESC, default to ASC.
-    // Emulate MySQL default behavior to sort NULL values first for ascending,
-    // and last for descending.
-    // @see http://www.postgresql.org/docs/9.3/static/queries-order.html
-    $direction = strtoupper($direction) == 'DESC' ? 'DESC NULLS LAST' : 'ASC NULLS FIRST';
-    $this->order[$field] = $direction;
-
-    if ($this->hasTag('entity_query')) {
-      return $this;
-    }
-
-    // If there is a table alias specified, split it up.
-    if (strpos($field, '.') !== FALSE) {
-      [$table, $table_field] = explode('.', $field);
-    }
-    // Figure out if the field has already been added.
-    foreach ($this->fields as $existing_field) {
-      if (!empty($table)) {
-        // If table alias is given, check if field and table exists.
-        if ($existing_field['table'] == $table && $existing_field['field'] == $table_field) {
-          return $this;
-        }
-      }
-      else {
-        // If there is no table, simply check if the field exists as a field or
-        // an aliased field.
-        if ($existing_field['alias'] == $field) {
-          return $this;
-        }
-      }
-    }
-
-    // Also check expression aliases.
-    foreach ($this->expressions as $expression) {
-      if ($expression['alias'] == $this->connection->escapeAlias($field)) {
-        return $this;
-      }
-    }
-
-    // If a table loads all fields, it can not be added again. It would
-    // result in an ambiguous alias error because that field would be loaded
-    // twice: Once through table_alias.* and once directly. If the field
-    // actually belongs to a different table, it must be added manually.
-    foreach ($this->tables as $table) {
-      if (!empty($table['all_fields'])) {
-        return $this;
-      }
-    }
-
-    // If $field contains characters which are not allowed in a field name
-    // it is considered an expression, these can't be handled automatically
-    // either.
-    if ($this->connection->escapeField($field) != $field) {
-      return $this;
-    }
-
-    // This is a case that can be handled automatically, add the field.
-    $this->addField(NULL, $field);
-    return $this;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function addExpression($expression, $alias = NULL, $arguments = []) {
-    if (empty($alias)) {
-      $alias = 'expression';
-    }
-
-    // This implements counting in the same manner as the parent method.
-    $alias_candidate = $alias;
-    $count = 2;
-    while (!empty($this->expressions[$alias_candidate])) {
-      $alias_candidate = $alias . '_' . $count++;
-    }
-    $alias = $alias_candidate;
-
-    $this->expressions[$alias] = [
-      'expression' => $expression,
-      'alias' => $this->connection->escapeAlias($alias_candidate),
-      'arguments' => $arguments,
-    ];
-
-    return $alias;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function execute() {
-    $this->connection->addSavepoint();
-    try {
-      $result = parent::execute();
-    }
-    catch (\Exception $e) {
-      $this->connection->rollbackSavepoint();
-      throw $e;
-    }
-    $this->connection->releaseSavepoint();
-
-    return $result;
-  }
-
-}
-
-/**
- * @} End of "addtogroup database".
- */
+class Select extends PgsqlSelect {}
diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Truncate.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Truncate.php
index 0d9947b96f28..01ed66c34455 100644
--- a/core/lib/Drupal/Core/Database/Driver/pgsql/Truncate.php
+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Truncate.php
@@ -2,28 +2,16 @@
 
 namespace Drupal\Core\Database\Driver\pgsql;
 
-use Drupal\Core\Database\Query\Truncate as QueryTruncate;
+use Drupal\pgsql\Driver\Database\pgsql\Truncate as PgsqlTruncate;
+
+@trigger_error('\Drupal\Core\Database\Driver\pgsql\Truncate is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED);
 
 /**
  * PostgreSQL implementation of \Drupal\Core\Database\Query\Truncate.
+ *
+ * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL
+ *   database driver has been moved to the pgsql module.
+ *
+ * @see https://www.drupal.org/node/3129492
  */
-class Truncate extends QueryTruncate {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function execute() {
-    $this->connection->addSavepoint();
-    try {
-      $result = parent::execute();
-    }
-    catch (\Exception $e) {
-      $this->connection->rollbackSavepoint();
-      throw $e;
-    }
-    $this->connection->releaseSavepoint();
-
-    return $result;
-  }
-
-}
+class Truncate extends PgsqlTruncate {}
diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Update.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Update.php
index 258379fff054..0e31b0dd2001 100644
--- a/core/lib/Drupal/Core/Database/Driver/pgsql/Update.php
+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Update.php
@@ -2,82 +2,16 @@
 
 namespace Drupal\Core\Database\Driver\pgsql;
 
-use Drupal\Core\Database\Query\Update as QueryUpdate;
-use Drupal\Core\Database\Query\SelectInterface;
+use Drupal\pgsql\Driver\Database\pgsql\Update as PgsqlUpdate;
+
+@trigger_error('\Drupal\Core\Database\Driver\pgsql\Update is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED);
 
 /**
  * PostgreSQL implementation of \Drupal\Core\Database\Query\Update.
+ *
+ * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL
+ *   database driver has been moved to the pgsql module.
+ *
+ * @see https://www.drupal.org/node/3129492
  */
-class Update extends QueryUpdate {
-
-  public function execute() {
-    $max_placeholder = 0;
-    $blobs = [];
-    $blob_count = 0;
-
-    // Because we filter $fields the same way here and in __toString(), the
-    // placeholders will all match up properly.
-    $stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions, TRUE);
-
-    // Fetch the list of blobs and sequences used on that table.
-    $table_information = $this->connection->schema()->queryTableInformation($this->table);
-
-    // Expressions take priority over literal fields, so we process those first
-    // and remove any literal fields that conflict.
-    $fields = $this->fields;
-    foreach ($this->expressionFields as $field => $data) {
-      if (!empty($data['arguments'])) {
-        foreach ($data['arguments'] as $placeholder => $argument) {
-          // We assume that an expression will never happen on a BLOB field,
-          // which is a fairly safe assumption to make since in most cases
-          // it would be an invalid query anyway.
-          $stmt->getClientStatement()->bindParam($placeholder, $data['arguments'][$placeholder]);
-        }
-      }
-      if ($data['expression'] instanceof SelectInterface) {
-        $data['expression']->compile($this->connection, $this);
-        $select_query_arguments = $data['expression']->arguments();
-        foreach ($select_query_arguments as $placeholder => $argument) {
-          $stmt->getClientStatement()->bindParam($placeholder, $select_query_arguments[$placeholder]);
-        }
-      }
-      unset($fields[$field]);
-    }
-
-    foreach ($fields as $field => $value) {
-      $placeholder = ':db_update_placeholder_' . ($max_placeholder++);
-
-      if (isset($table_information->blob_fields[$field]) && $value !== NULL) {
-        $blobs[$blob_count] = fopen('php://memory', 'a');
-        fwrite($blobs[$blob_count], $value);
-        rewind($blobs[$blob_count]);
-        $stmt->getClientStatement()->bindParam($placeholder, $blobs[$blob_count], \PDO::PARAM_LOB);
-        ++$blob_count;
-      }
-      else {
-        $stmt->getClientStatement()->bindParam($placeholder, $fields[$field]);
-      }
-    }
-
-    if (count($this->condition)) {
-      $this->condition->compile($this->connection, $this);
-
-      $arguments = $this->condition->arguments();
-      foreach ($arguments as $placeholder => $value) {
-        $stmt->getClientStatement()->bindParam($placeholder, $arguments[$placeholder]);
-      }
-    }
-
-    $this->connection->addSavepoint();
-    try {
-      $stmt->execute(NULL, $this->queryOptions);
-      $this->connection->releaseSavepoint();
-      return $stmt->rowCount();
-    }
-    catch (\Exception $e) {
-      $this->connection->rollbackSavepoint();
-      $this->connection->exceptionHandler()->handleExecutionException($e, $stmt, [], $this->queryOptions);
-    }
-  }
-
-}
+class Update extends PgsqlUpdate {}
diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Upsert.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Upsert.php
index 934a3d6625d7..e1eb3d95c54d 100644
--- a/core/lib/Drupal/Core/Database/Driver/pgsql/Upsert.php
+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Upsert.php
@@ -2,125 +2,16 @@
 
 namespace Drupal\Core\Database\Driver\pgsql;
 
-use Drupal\Core\Database\Query\Upsert as QueryUpsert;
+use Drupal\pgsql\Driver\Database\pgsql\Upsert as PgsqlUpsert;
 
-// cSpell:ignore nextval setval
+@trigger_error('\Drupal\Core\Database\Driver\pgsql\Upsert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED);
 
 /**
  * PostgreSQL implementation of \Drupal\Core\Database\Query\Upsert.
+ *
+ * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL
+ *   database driver has been moved to the pgsql module.
+ *
+ * @see https://www.drupal.org/node/3129492
  */
-class Upsert extends QueryUpsert {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function execute() {
-    if (!$this->preExecute()) {
-      return NULL;
-    }
-
-    $stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions, TRUE);
-
-    // Fetch the list of blobs and sequences used on that table.
-    $table_information = $this->connection->schema()->queryTableInformation($this->table);
-
-    $max_placeholder = 0;
-    $blobs = [];
-    $blob_count = 0;
-    foreach ($this->insertValues as $insert_values) {
-      foreach ($this->insertFields as $idx => $field) {
-        if (isset($table_information->blob_fields[$field]) && $insert_values[$idx] !== NULL) {
-          $blobs[$blob_count] = fopen('php://memory', 'a');
-          fwrite($blobs[$blob_count], $insert_values[$idx]);
-          rewind($blobs[$blob_count]);
-
-          $stmt->getClientStatement()->bindParam(':db_insert_placeholder_' . $max_placeholder++, $blobs[$blob_count], \PDO::PARAM_LOB);
-
-          // Pre-increment is faster in PHP than increment.
-          ++$blob_count;
-        }
-        else {
-          $stmt->getClientStatement()->bindParam(':db_insert_placeholder_' . $max_placeholder++, $insert_values[$idx]);
-        }
-      }
-      // Check if values for a serial field has been passed.
-      if (!empty($table_information->serial_fields)) {
-        foreach ($table_information->serial_fields as $index => $serial_field) {
-          $serial_key = array_search($serial_field, $this->insertFields);
-          if ($serial_key !== FALSE) {
-            $serial_value = $insert_values[$serial_key];
-
-            // Sequences must be greater than or equal to 1.
-            if ($serial_value === NULL || !$serial_value) {
-              $serial_value = 1;
-            }
-            // Set the sequence to the bigger value of either the passed
-            // value or the max value of the column. It can happen that another
-            // thread calls nextval() which could lead to a serial number being
-            // used twice. However, trying to insert a value into a serial
-            // column should only be done in very rare cases and is not thread
-            // safe by definition.
-            $this->connection->query("SELECT setval('" . $table_information->sequences[$index] . "', GREATEST(MAX(" . $serial_field . "), :serial_value)) FROM {" . $this->table . "}", [':serial_value' => (int) $serial_value]);
-          }
-        }
-      }
-    }
-
-    $options = $this->queryOptions;
-    if (!empty($table_information->sequences)) {
-      $options['sequence_name'] = $table_information->sequences[0];
-    }
-
-    // Re-initialize the values array so that we can re-use this query.
-    $this->insertValues = [];
-
-    // Create a savepoint so we can rollback a failed query. This is so we can
-    // mimic MySQL and SQLite transactions which don't fail if a single query
-    // fails. This is important for tables that are created on demand. For
-    // example, \Drupal\Core\Cache\DatabaseBackend.
-    $this->connection->addSavepoint();
-    try {
-      $stmt->execute(NULL, $options);
-      $this->connection->releaseSavepoint();
-      return $stmt->rowCount();
-    }
-    catch (\Exception $e) {
-      $this->connection->rollbackSavepoint();
-      $this->connection->exceptionHandler()->handleExecutionException($e, $stmt, [], $options);
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function __toString() {
-    // Create a sanitized comment string to prepend to the query.
-    $comments = $this->connection->makeComment($this->comments);
-
-    // 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);
-
-    $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
-
-    $values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
-    $query .= implode(', ', $values);
-
-    // Updating the unique / primary key is not necessary.
-    unset($insert_fields[$this->key]);
-
-    $update = [];
-    foreach ($insert_fields as $field) {
-      // The "excluded." prefix causes the field to refer to the value for field
-      // that would have been inserted had there been no conflict.
-      $update[] = "$field = EXCLUDED.$field";
-    }
-
-    $query .= ' ON CONFLICT (' . $this->connection->escapeField($this->key) . ') DO UPDATE SET ' . implode(', ', $update);
-
-    return $query;
-  }
-
-}
+class Upsert extends PgsqlUpsert {}
diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php
index 210b2a64088d..c4b130d1f045 100644
--- a/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php
+++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php
@@ -2,527 +2,16 @@
 
 namespace Drupal\Core\Database\Driver\sqlite;
 
-use Drupal\Core\Database\DatabaseNotFoundException;
-use Drupal\Core\Database\Connection as DatabaseConnection;
-use Drupal\Core\Database\StatementInterface;
+use Drupal\sqlite\Driver\Database\sqlite\Connection as SqliteConnection;
+
+@trigger_error('\Drupal\Core\Database\Driver\sqlite\Connection is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED);
 
 /**
  * SQLite implementation of \Drupal\Core\Database\Connection.
+ *
+ * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite
+ *   database driver has been moved to the sqlite module.
+ *
+ * @see https://www.drupal.org/node/3129492
  */
-class Connection extends DatabaseConnection {
-
-  /**
-   * Error code for "Unable to open database file" error.
-   */
-  const DATABASE_NOT_FOUND = 14;
-
-  /**
-   * {@inheritdoc}
-   */
-  protected $statementClass = NULL;
-
-  /**
-   * {@inheritdoc}
-   */
-  protected $statementWrapperClass = NULL;
-
-  /**
-   * Whether or not the active transaction (if any) will be rolled back.
-   *
-   * @var bool
-   */
-  protected $willRollback;
-
-  /**
-   * A map of condition operators to SQLite operators.
-   *
-   * We don't want to override any of the defaults.
-   */
-  protected static $sqliteConditionOperatorMap = [
-    'LIKE' => ['postfix' => " ESCAPE '\\'"],
-    'NOT LIKE' => ['postfix' => " ESCAPE '\\'"],
-    'LIKE BINARY' => ['postfix' => " ESCAPE '\\'", 'operator' => 'GLOB'],
-    'NOT LIKE BINARY' => ['postfix' => " ESCAPE '\\'", 'operator' => 'NOT GLOB'],
-  ];
-
-  /**
-   * All databases attached to the current database.
-   *
-   * This is used to allow prefixes to be safely handled without locking the
-   * table.
-   *
-   * @var array
-   */
-  protected $attachedDatabases = [];
-
-  /**
-   * Whether or not a table has been dropped this request.
-   *
-   * The destructor will only try to get rid of unnecessary databases if there
-   * is potential of them being empty.
-   *
-   * This variable is set to public because Schema needs to
-   * access it. However, it should not be manually set.
-   *
-   * @var bool
-   */
-  public $tableDropped = FALSE;
-
-  /**
-   * {@inheritdoc}
-   */
-  protected $transactionalDDLSupport = TRUE;
-
-  /**
-   * {@inheritdoc}
-   */
-  protected $identifierQuotes = ['"', '"'];
-
-  /**
-   * Constructs a \Drupal\Core\Database\Driver\sqlite\Connection object.
-   */
-  public function __construct(\PDO $connection, array $connection_options) {
-    parent::__construct($connection, $connection_options);
-
-    // Attach one database for each registered prefix.
-    $prefixes = $this->prefixes;
-    foreach ($prefixes as &$prefix) {
-      // Empty prefix means query the main database -- no need to attach
-      // anything.
-      if ($prefix !== '') {
-        $this->attachDatabase($prefix);
-        // Add a ., so queries become prefix.table, which is proper syntax for
-        // querying an attached database.
-        $prefix .= '.';
-      }
-    }
-
-    // Regenerate the prefixes replacement table.
-    $this->setPrefix($prefixes);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function open(array &$connection_options = []) {
-    // Allow PDO options to be overridden.
-    $connection_options += [
-      'pdo' => [],
-    ];
-    $connection_options['pdo'] += [
-      \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
-      // Convert numeric values to strings when fetching.
-      \PDO::ATTR_STRINGIFY_FETCHES => TRUE,
-    ];
-
-    try {
-      $pdo = new \PDO('sqlite:' . $connection_options['database'], '', '', $connection_options['pdo']);
-    }
-    catch (\PDOException $e) {
-      if ($e->getCode() == static::DATABASE_NOT_FOUND) {
-        throw new DatabaseNotFoundException($e->getMessage(), $e->getCode(), $e);
-      }
-      // SQLite doesn't have a distinct error code for access denied, so don't
-      // deal with that case.
-      throw $e;
-    }
-
-    // Create functions needed by SQLite.
-    $pdo->sqliteCreateFunction('if', [__CLASS__, 'sqlFunctionIf']);
-    $pdo->sqliteCreateFunction('greatest', [__CLASS__, 'sqlFunctionGreatest']);
-    $pdo->sqliteCreateFunction('least', [__CLASS__, 'sqlFunctionLeast']);
-    $pdo->sqliteCreateFunction('pow', 'pow', 2);
-    $pdo->sqliteCreateFunction('exp', 'exp', 1);
-    $pdo->sqliteCreateFunction('length', 'strlen', 1);
-    $pdo->sqliteCreateFunction('md5', 'md5', 1);
-    $pdo->sqliteCreateFunction('concat', [__CLASS__, 'sqlFunctionConcat']);
-    $pdo->sqliteCreateFunction('concat_ws', [__CLASS__, 'sqlFunctionConcatWs']);
-    $pdo->sqliteCreateFunction('substring', [__CLASS__, 'sqlFunctionSubstring'], 3);
-    $pdo->sqliteCreateFunction('substring_index', [__CLASS__, 'sqlFunctionSubstringIndex'], 3);
-    $pdo->sqliteCreateFunction('rand', [__CLASS__, 'sqlFunctionRand']);
-    $pdo->sqliteCreateFunction('regexp', [__CLASS__, 'sqlFunctionRegexp']);
-
-    // SQLite does not support the LIKE BINARY operator, so we overload the
-    // non-standard GLOB operator for case-sensitive matching. Another option
-    // would have been to override another non-standard operator, MATCH, but
-    // that does not support the NOT keyword prefix.
-    $pdo->sqliteCreateFunction('glob', [__CLASS__, 'sqlFunctionLikeBinary']);
-
-    // Create a user-space case-insensitive collation with UTF-8 support.
-    $pdo->sqliteCreateCollation('NOCASE_UTF8', ['Drupal\Component\Utility\Unicode', 'strcasecmp']);
-
-    // Set SQLite init_commands if not already defined. Enable the Write-Ahead
-    // Logging (WAL) for SQLite. See https://www.drupal.org/node/2348137 and
-    // https://www.sqlite.org/wal.html.
-    $connection_options += [
-      'init_commands' => [],
-    ];
-    $connection_options['init_commands'] += [
-      'wal' => "PRAGMA journal_mode=WAL",
-    ];
-
-    // Execute sqlite init_commands.
-    if (isset($connection_options['init_commands'])) {
-      $pdo->exec(implode('; ', $connection_options['init_commands']));
-    }
-
-    return $pdo;
-  }
-
-  /**
-   * Destructor for the SQLite connection.
-   *
-   * We prune empty databases on destruct, but only if tables have been
-   * dropped. This is especially needed when running the test suite, which
-   * creates and destroy databases several times in a row.
-   */
-  public function __destruct() {
-    if ($this->tableDropped && !empty($this->attachedDatabases)) {
-      foreach ($this->attachedDatabases as $prefix) {
-        // Check if the database is now empty, ignore the internal SQLite tables.
-        try {
-          $count = $this->query('SELECT COUNT(*) FROM ' . $prefix . '.sqlite_master WHERE type = :type AND name NOT LIKE :pattern', [':type' => 'table', ':pattern' => 'sqlite_%'])->fetchField();
-
-          // We can prune the database file if it doesn't have any tables.
-          if ($count == 0 && $this->connectionOptions['database'] != ':memory:' && file_exists($this->connectionOptions['database'] . '-' . $prefix)) {
-            // Detach the database.
-            $this->query('DETACH DATABASE :schema', [':schema' => $prefix]);
-            // Destroy the database file.
-            unlink($this->connectionOptions['database'] . '-' . $prefix);
-          }
-        }
-        catch (\Exception $e) {
-          // Ignore the exception and continue. There is nothing we can do here
-          // to report the error or fail safe.
-        }
-      }
-    }
-    parent::__destruct();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function attachDatabase(string $database): void {
-    // Only attach the database once.
-    if (!isset($this->attachedDatabases[$database])) {
-      // In memory database use ':memory:' as database name. According to
-      // http://www.sqlite.org/inmemorydb.html it will open a unique database so
-      // attaching it twice is not a problem.
-      $database_file = $this->connectionOptions['database'] !== ':memory:' ? $this->connectionOptions['database'] . '-' . $database : $this->connectionOptions['database'];
-      $this->query('ATTACH DATABASE :database_file AS :database', [':database_file' => $database_file, ':database' => $database]);
-      $this->attachedDatabases[$database] = $database;
-    }
-  }
-
-  /**
-   * Gets all the attached databases.
-   *
-   * @return array
-   *   An array of attached database names.
-   *
-   * @see \Drupal\Core\Database\Driver\sqlite\Connection::__construct()
-   */
-  public function getAttachedDatabases() {
-    return $this->attachedDatabases;
-  }
-
-  /**
-   * SQLite compatibility implementation for the IF() SQL function.
-   */
-  public static function sqlFunctionIf($condition, $expr1, $expr2 = NULL) {
-    return $condition ? $expr1 : $expr2;
-  }
-
-  /**
-   * SQLite compatibility implementation for the GREATEST() SQL function.
-   */
-  public static function sqlFunctionGreatest() {
-    $args = func_get_args();
-    foreach ($args as $v) {
-      if (!isset($v)) {
-        unset($args);
-      }
-    }
-    if (count($args)) {
-      return max($args);
-    }
-    else {
-      return NULL;
-    }
-  }
-
-  /**
-   * SQLite compatibility implementation for the LEAST() SQL function.
-   */
-  public static function sqlFunctionLeast() {
-    // Remove all NULL, FALSE and empty strings values but leaves 0 (zero) values.
-    $values = array_filter(func_get_args(), 'strlen');
-
-    return count($values) < 1 ? NULL : min($values);
-  }
-
-  /**
-   * SQLite compatibility implementation for the CONCAT() SQL function.
-   */
-  public static function sqlFunctionConcat() {
-    $args = func_get_args();
-    return implode('', $args);
-  }
-
-  /**
-   * SQLite compatibility implementation for the CONCAT_WS() SQL function.
-   *
-   * @see http://dev.mysql.com/doc/refman/5.6/en/string-functions.html#function_concat-ws
-   */
-  public static function sqlFunctionConcatWs() {
-    $args = func_get_args();
-    $separator = array_shift($args);
-    // If the separator is NULL, the result is NULL.
-    if ($separator === FALSE || is_null($separator)) {
-      return NULL;
-    }
-    // Skip any NULL values after the separator argument.
-    $args = array_filter($args, function ($value) {
-      return !is_null($value);
-    });
-    return implode($separator, $args);
-  }
-
-  /**
-   * SQLite compatibility implementation for the SUBSTRING() SQL function.
-   */
-  public static function sqlFunctionSubstring($string, $from, $length) {
-    return substr($string, $from - 1, $length);
-  }
-
-  /**
-   * SQLite compatibility implementation for the SUBSTRING_INDEX() SQL function.
-   */
-  public static function sqlFunctionSubstringIndex($string, $delimiter, $count) {
-    // If string is empty, simply return an empty string.
-    if (empty($string)) {
-      return '';
-    }
-    $end = 0;
-    for ($i = 0; $i < $count; $i++) {
-      $end = strpos($string, $delimiter, $end + 1);
-      if ($end === FALSE) {
-        $end = strlen($string);
-      }
-    }
-    return substr($string, 0, $end);
-  }
-
-  /**
-   * SQLite compatibility implementation for the RAND() SQL function.
-   */
-  public static function sqlFunctionRand($seed = NULL) {
-    if (isset($seed)) {
-      mt_srand($seed);
-    }
-    return mt_rand() / mt_getrandmax();
-  }
-
-  /**
-   * SQLite compatibility implementation for the REGEXP SQL operator.
-   *
-   * The REGEXP operator is natively known, but not implemented by default.
-   *
-   * @see http://www.sqlite.org/lang_expr.html#regexp
-   */
-  public static function sqlFunctionRegexp($pattern, $subject) {
-    // preg_quote() cannot be used here, since $pattern may contain reserved
-    // regular expression characters already (such as ^, $, etc). Therefore,
-    // use a rare character as PCRE delimiter.
-    $pattern = '#' . addcslashes($pattern, '#') . '#i';
-    return preg_match($pattern, $subject);
-  }
-
-  /**
-   * SQLite compatibility implementation for the LIKE BINARY SQL operator.
-   *
-   * SQLite supports case-sensitive LIKE operations through the
-   * 'case_sensitive_like' PRAGMA statement, but only for ASCII characters, so
-   * we have to provide our own implementation with UTF-8 support.
-   *
-   * @see https://sqlite.org/pragma.html#pragma_case_sensitive_like
-   * @see https://sqlite.org/lang_expr.html#like
-   */
-  public static function sqlFunctionLikeBinary($pattern, $subject) {
-    // Replace the SQL LIKE wildcard meta-characters with the equivalent regular
-    // expression meta-characters and escape the delimiter that will be used for
-    // matching.
-    $pattern = str_replace(['%', '_'], ['.*?', '.'], preg_quote($pattern, '/'));
-    return preg_match('/^' . $pattern . '$/', $subject);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function prepare($statement, array $driver_options = []) {
-    @trigger_error('Connection::prepare() is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Database drivers should instantiate \PDOStatement objects by calling \PDO::prepare in their Connection::prepareStatement method instead. \PDO::prepare should not be called outside of driver code. See https://www.drupal.org/node/3137786', E_USER_DEPRECATED);
-    return new Statement($this->connection, $this, $statement, $driver_options);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function handleQueryException(\PDOException $e, $query, array $args = [], $options = []) {
-    // The database schema might be changed by another process in between the
-    // time that the statement was prepared and the time the statement was run
-    // (e.g. usually happens when running tests). In this case, we need to
-    // re-run the query.
-    // @see http://www.sqlite.org/faq.html#q15
-    // @see http://www.sqlite.org/rescode.html#schema
-    if (!empty($e->errorInfo[1]) && $e->errorInfo[1] === 17) {
-      @trigger_error('Connection::handleQueryException() is deprecated in drupal:9.2.0 and is removed in drupal:10.0.0. Get a handler through $this->exceptionHandler() instead, and use one of its methods. See https://www.drupal.org/node/3187222', E_USER_DEPRECATED);
-      return $this->query($query, $args, $options);
-    }
-
-    parent::handleQueryException($e, $query, $args, $options);
-  }
-
-  public function queryRange($query, $from, $count, array $args = [], array $options = []) {
-    return $this->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function queryTemporary($query, array $args = [], array $options = []) {
-    @trigger_error('Connection::queryTemporary() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. There is no replacement. See https://www.drupal.org/node/3211781', E_USER_DEPRECATED);
-    // Generate a new temporary table name and protect it from prefixing.
-    // SQLite requires that temporary tables to be non-qualified.
-    $tablename = $this->generateTemporaryTableName();
-    $prefixes = $this->prefixes;
-    $prefixes[$tablename] = '';
-    $this->setPrefix($prefixes);
-
-    $this->query('CREATE TEMPORARY TABLE ' . $tablename . ' AS ' . $query, $args, $options);
-    return $tablename;
-  }
-
-  public function driver() {
-    return 'sqlite';
-  }
-
-  public function databaseType() {
-    return 'sqlite';
-  }
-
-  /**
-   * Overrides \Drupal\Core\Database\Connection::createDatabase().
-   *
-   * @param string $database
-   *   The name of the database to create.
-   *
-   * @throws \Drupal\Core\Database\DatabaseNotFoundException
-   */
-  public function createDatabase($database) {
-    // Verify the database is writable.
-    $db_directory = new \SplFileInfo(dirname($database));
-    if (!$db_directory->isDir() && !\Drupal::service('file_system')->mkdir($db_directory->getPathName(), 0755, TRUE)) {
-      throw new DatabaseNotFoundException('Unable to create database directory ' . $db_directory->getPathName());
-    }
-  }
-
-  public function mapConditionOperator($operator) {
-    return static::$sqliteConditionOperatorMap[$operator] ?? NULL;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function prepareStatement(string $query, array $options, bool $allow_row_count = FALSE): StatementInterface {
-    try {
-      $query = $this->preprocessStatement($query, $options);
-      $statement = new Statement($this->connection, $this, $query, $options['pdo'] ?? [], $allow_row_count);
-    }
-    catch (\Exception $e) {
-      $this->exceptionHandler()->handleStatementException($e, $query, $options);
-    }
-    return $statement;
-  }
-
-  public function nextId($existing_id = 0) {
-    $this->startTransaction();
-    // We can safely use literal queries here instead of the slower query
-    // builder because if a given database breaks here then it can simply
-    // override nextId. However, this is unlikely as we deal with short strings
-    // and integers and no known databases require special handling for those
-    // simple cases. If another transaction wants to write the same row, it will
-    // wait until this transaction commits.
-    $stmt = $this->prepareStatement('UPDATE {sequences} SET [value] = GREATEST([value], :existing_id) + 1', [], TRUE);
-    $args = [':existing_id' => $existing_id];
-    try {
-      $stmt->execute($args);
-    }
-    catch (\Exception $e) {
-      $this->exceptionHandler()->handleExecutionException($e, $stmt, $args, []);
-    }
-    if ($stmt->rowCount() === 0) {
-      $this->query('INSERT INTO {sequences} ([value]) VALUES (:existing_id + 1)', $args);
-    }
-    // The transaction gets committed when the transaction object gets destroyed
-    // because it gets out of scope.
-    return $this->query('SELECT [value] FROM {sequences}')->fetchField();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFullQualifiedTableName($table) {
-    $prefix = $this->tablePrefix($table);
-
-    // Don't include the SQLite database file name as part of the table name.
-    return $prefix . $table;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function createConnectionOptionsFromUrl($url, $root) {
-    $database = parent::createConnectionOptionsFromUrl($url, $root);
-
-    // A SQLite database path with two leading slashes indicates a system path.
-    // Otherwise the path is relative to the Drupal root.
-    $url_components = parse_url($url);
-    if ($url_components['path'][0] === '/') {
-      $url_components['path'] = substr($url_components['path'], 1);
-    }
-    if ($url_components['path'][0] === '/' || $url_components['path'] === ':memory:') {
-      $database['database'] = $url_components['path'];
-    }
-    else {
-      $database['database'] = $root . '/' . $url_components['path'];
-    }
-
-    // User credentials and system port are irrelevant for SQLite.
-    unset(
-      $database['username'],
-      $database['password'],
-      $database['port']
-    );
-
-    return $database;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function createUrlFromConnectionOptions(array $connection_options) {
-    if (!isset($connection_options['driver'], $connection_options['database'])) {
-      throw new \InvalidArgumentException("As a minimum, the connection options array must contain at least the 'driver' and 'database' keys");
-    }
-
-    $db_url = 'sqlite://localhost/' . $connection_options['database'];
-
-    if (isset($connection_options['prefix']) && $connection_options['prefix'] !== '') {
-      $db_url .= '#' . $connection_options['prefix'];
-    }
-
-    return $db_url;
-  }
-
-}
+class Connection extends SqliteConnection {}
diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Insert.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Insert.php
index 4273dd6536fe..1685c8ba02bb 100644
--- a/core/lib/Drupal/Core/Database/Driver/sqlite/Insert.php
+++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Insert.php
@@ -2,51 +2,16 @@
 
 namespace Drupal\Core\Database\Driver\sqlite;
 
-use Drupal\Core\Database\Query\Insert as QueryInsert;
+use Drupal\sqlite\Driver\Database\sqlite\Insert as SqliteInsert;
+
+@trigger_error('\Drupal\Core\Database\Driver\sqlite\Insert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED);
 
 /**
  * SQLite implementation of \Drupal\Core\Database\Query\Insert.
  *
- * We ignore all the default fields and use the clever SQLite syntax:
- *   INSERT INTO table DEFAULT VALUES
- * for degenerated "default only" queries.
+ * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite
+ *   database driver has been moved to the sqlite module.
+ *
+ * @see https://www.drupal.org/node/3129492
  */
-class Insert extends QueryInsert {
-
-  public function execute() {
-    if (!$this->preExecute()) {
-      return NULL;
-    }
-    if (count($this->insertFields) || !empty($this->fromQuery)) {
-      return parent::execute();
-    }
-    else {
-      return $this->connection->query('INSERT INTO {' . $this->table . '} DEFAULT VALUES', [], $this->queryOptions);
-    }
-  }
-
-  public function __toString() {
-    // Create a sanitized comment string to prepend to the query.
-    $comments = $this->connection->makeComment($this->comments);
-
-    // Produce as many generic placeholders as necessary.
-    $placeholders = [];
-    if (!empty($this->insertFields)) {
-      $placeholders = array_fill(0, count($this->insertFields), '?');
-    }
-
-    $insert_fields = array_map(function ($field) {
-      return $this->connection->escapeField($field);
-    }, $this->insertFields);
-
-    // If we're selecting from a SelectQuery, finish building the query and
-    // pass it back, as any remaining options are irrelevant.
-    if (!empty($this->fromQuery)) {
-      $insert_fields_string = $insert_fields ? ' (' . implode(', ', $insert_fields) . ') ' : ' ';
-      return $comments . 'INSERT INTO {' . $this->table . '}' . $insert_fields_string . $this->fromQuery;
-    }
-
-    return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES (' . implode(', ', $placeholders) . ')';
-  }
-
-}
+class Insert extends SqliteInsert {}
diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php
index aecaa6925ba0..22929b619430 100644
--- a/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php
+++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Install/Tasks.php
@@ -2,114 +2,16 @@
 
 namespace Drupal\Core\Database\Driver\sqlite\Install;
 
-use Drupal\Core\Database\Database;
-use Drupal\Core\Database\Driver\sqlite\Connection;
-use Drupal\Core\Database\DatabaseNotFoundException;
-use Drupal\Core\Database\Install\Tasks as InstallTasks;
+use Drupal\sqlite\Driver\Database\sqlite\Install\Tasks as SqliteTasks;
+
+@trigger_error('\Drupal\Core\Database\Driver\sqlite\Install\Tasks is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED);
 
 /**
  * Specifies installation tasks for SQLite databases.
+ *
+ * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite
+ *   database driver has been moved to the sqlite module.
+ *
+ * @see https://www.drupal.org/node/3129492
  */
-class Tasks extends InstallTasks {
-
-  /**
-   * Minimum required SQLite version.
-   *
-   * Use to build sqlite library with json1 option for JSON datatype support.
-   * @see https://www.sqlite.org/json1.html
-   */
-  const SQLITE_MINIMUM_VERSION = '3.26';
-
-  /**
-   * {@inheritdoc}
-   */
-  protected $pdoDriver = 'sqlite';
-
-  /**
-   * {@inheritdoc}
-   */
-  public function name() {
-    return t('SQLite');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function minimumVersion() {
-    return static::SQLITE_MINIMUM_VERSION;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormOptions(array $database) {
-    $form = parent::getFormOptions($database);
-
-    // Remove the options that only apply to client/server style databases.
-    unset($form['username'], $form['password'], $form['advanced_options']['host'], $form['advanced_options']['port']);
-
-    // Make the text more accurate for SQLite.
-    $form['database']['#title'] = t('Database file');
-    $form['database']['#description'] = t('The absolute path to the file where @drupal data will be stored. This must be writable by the web server and should exist outside of the web root.', ['@drupal' => drupal_install_profile_distribution_name()]);
-    $default_database = \Drupal::getContainer()->getParameter('site.path') . '/files/.ht.sqlite';
-    $form['database']['#default_value'] = empty($database['database']) ? $default_database : $database['database'];
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function connect() {
-    try {
-      // This doesn't actually test the connection.
-      Database::setActiveConnection();
-      // Now actually do a check.
-      Database::getConnection();
-      $this->pass('Drupal can CONNECT to the database ok.');
-    }
-    catch (\Exception $e) {
-      // Attempt to create the database if it is not found.
-      if ($e->getCode() == Connection::DATABASE_NOT_FOUND) {
-        // Remove the database string from connection info.
-        $connection_info = Database::getConnectionInfo();
-        $database = $connection_info['default']['database'];
-
-        // We cannot use \Drupal::service('file_system')->getTempDirectory()
-        // here because we haven't yet successfully connected to the database.
-        $connection_info['default']['database'] = \Drupal::service('file_system')->tempnam(sys_get_temp_dir(), 'sqlite');
-
-        // In order to change the Database::$databaseInfo array, need to remove
-        // the active connection, then re-add it with the new info.
-        Database::removeConnection('default');
-        Database::addConnectionInfo('default', 'default', $connection_info['default']);
-
-        try {
-          Database::getConnection()->createDatabase($database);
-          Database::closeConnection();
-
-          // Now, restore the database config.
-          Database::removeConnection('default');
-          $connection_info['default']['database'] = $database;
-          Database::addConnectionInfo('default', 'default', $connection_info['default']);
-
-          // Check the database connection.
-          Database::getConnection();
-          $this->pass('Drupal can CONNECT to the database ok.');
-        }
-        catch (DatabaseNotFoundException $e) {
-          // Still no dice; probably a permission issue. Raise the error to the
-          // installer.
-          $this->fail(t('Failed to open or create database file %database. The database engine reports the following message when attempting to create the database: %error.', ['%database' => $database, '%error' => $e->getMessage()]));
-        }
-      }
-      else {
-        // Database connection failed for some other reason than a non-existent
-        // database.
-        $this->fail(t('Failed to connect to database. The database engine reports the following message: %error.<ul><li>Does the database file exist?</li><li>Does web server have permission to write to the database file?</li>Does the web server have permission to write to the directory the database file should be created in?</li></ul>', ['%error' => $e->getMessage()]));
-        return FALSE;
-      }
-    }
-    return TRUE;
-  }
-
-}
+class Tasks extends SqliteTasks {}
diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php
index 1895522e810a..b880d2c45bdf 100644
--- a/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php
+++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php
@@ -2,836 +2,16 @@
 
 namespace Drupal\Core\Database\Driver\sqlite;
 
-use Drupal\Core\Database\SchemaObjectExistsException;
-use Drupal\Core\Database\SchemaObjectDoesNotExistException;
-use Drupal\Core\Database\Schema as DatabaseSchema;
+use Drupal\sqlite\Driver\Database\sqlite\Schema as SqliteSchema;
 
-/**
- * @ingroup schemaapi
- * @{
- */
+@trigger_error('\Drupal\Core\Database\Driver\sqlite\Schema is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED);
 
 /**
  * SQLite implementation of \Drupal\Core\Database\Schema.
+ *
+ * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite
+ *   database driver has been moved to the sqlite module.
+ *
+ * @see https://www.drupal.org/node/3129492
  */
-class Schema extends DatabaseSchema {
-
-  /**
-   * Override DatabaseSchema::$defaultSchema.
-   *
-   * @var string
-   */
-  protected $defaultSchema = 'main';
-
-  /**
-   * {@inheritdoc}
-   */
-  public function tableExists($table) {
-    $info = $this->getPrefixInfo($table);
-
-    // Don't use {} around sqlite_master table.
-    return (bool) $this->connection->query('SELECT 1 FROM ' . $info['schema'] . '.sqlite_master WHERE type = :type AND name = :name', [':type' => 'table', ':name' => $info['table']])->fetchField();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function fieldExists($table, $column) {
-    $schema = $this->introspectSchema($table);
-    return !empty($schema['fields'][$column]);
-  }
-
-  /**
-   * Generate SQL to create a new table from a Drupal schema definition.
-   *
-   * @param $name
-   *   The name of the table to create.
-   * @param $table
-   *   A Schema API table definition array.
-   *
-   * @return
-   *   An array of SQL statements to create the table.
-   */
-  public function createTableSql($name, $table) {
-    if (!empty($table['primary key']) && is_array($table['primary key'])) {
-      $this->ensureNotNullPrimaryKey($table['primary key'], $table['fields']);
-    }
-
-    $sql = [];
-    $sql[] = "CREATE TABLE {" . $name . "} (\n" . $this->createColumnsSql($name, $table) . "\n)\n";
-    return array_merge($sql, $this->createIndexSql($name, $table));
-  }
-
-  /**
-   * Build the SQL expression for indexes.
-   */
-  protected function createIndexSql($tablename, $schema) {
-    $sql = [];
-    $info = $this->getPrefixInfo($tablename);
-    if (!empty($schema['unique keys'])) {
-      foreach ($schema['unique keys'] as $key => $fields) {
-        $sql[] = 'CREATE UNIQUE INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $key . ' ON ' . $info['table'] . ' (' . $this->createKeySql($fields) . ")\n";
-      }
-    }
-    if (!empty($schema['indexes'])) {
-      foreach ($schema['indexes'] as $key => $fields) {
-        $sql[] = 'CREATE INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $key . ' ON ' . $info['table'] . ' (' . $this->createKeySql($fields) . ")\n";
-      }
-    }
-    return $sql;
-  }
-
-  /**
-   * Build the SQL expression for creating columns.
-   */
-  protected function createColumnsSql($tablename, $schema) {
-    $sql_array = [];
-
-    // Add the SQL statement for each field.
-    foreach ($schema['fields'] as $name => $field) {
-      if (isset($field['type']) && $field['type'] == 'serial') {
-        if (isset($schema['primary key']) && ($key = array_search($name, $schema['primary key'])) !== FALSE) {
-          unset($schema['primary key'][$key]);
-        }
-      }
-      $sql_array[] = $this->createFieldSql($name, $this->processField($field));
-    }
-
-    // Process keys.
-    if (!empty($schema['primary key'])) {
-      $sql_array[] = " PRIMARY KEY (" . $this->createKeySql($schema['primary key']) . ")";
-    }
-
-    return implode(", \n", $sql_array);
-  }
-
-  /**
-   * Build the SQL expression for keys.
-   */
-  protected function createKeySql($fields) {
-    $return = [];
-    foreach ($fields as $field) {
-      if (is_array($field)) {
-        $return[] = $field[0];
-      }
-      else {
-        $return[] = $field;
-      }
-    }
-    return implode(', ', $return);
-  }
-
-  /**
-   * Set database-engine specific properties for a field.
-   *
-   * @param $field
-   *   A field description array, as specified in the schema documentation.
-   */
-  protected function processField($field) {
-    if (!isset($field['size'])) {
-      $field['size'] = 'normal';
-    }
-
-    // Set the correct database-engine specific datatype.
-    // In case one is already provided, force it to uppercase.
-    if (isset($field['sqlite_type'])) {
-      $field['sqlite_type'] = mb_strtoupper($field['sqlite_type']);
-    }
-    else {
-      $map = $this->getFieldTypeMap();
-      $field['sqlite_type'] = $map[$field['type'] . ':' . $field['size']];
-
-      // Numeric fields with a specified scale have to be stored as floats.
-      if ($field['sqlite_type'] === 'NUMERIC' && isset($field['scale'])) {
-        $field['sqlite_type'] = 'FLOAT';
-      }
-    }
-
-    if (isset($field['type']) && $field['type'] == 'serial') {
-      $field['auto_increment'] = TRUE;
-    }
-
-    return $field;
-  }
-
-  /**
-   * Create an SQL string for a field to be used in table creation or alteration.
-   *
-   * Before passing a field out of a schema definition into this function it has
-   * to be processed by self::processField().
-   *
-   * @param $name
-   *   Name of the field.
-   * @param $spec
-   *   The field specification, as per the schema data structure format.
-   */
-  protected function createFieldSql($name, $spec) {
-    $name = $this->connection->escapeField($name);
-    if (!empty($spec['auto_increment'])) {
-      $sql = $name . " INTEGER PRIMARY KEY AUTOINCREMENT";
-      if (!empty($spec['unsigned'])) {
-        $sql .= ' CHECK (' . $name . '>= 0)';
-      }
-    }
-    else {
-      $sql = $name . ' ' . $spec['sqlite_type'];
-
-      if (in_array($spec['sqlite_type'], ['VARCHAR', 'TEXT'])) {
-        if (isset($spec['length'])) {
-          $sql .= '(' . $spec['length'] . ')';
-        }
-
-        if (isset($spec['binary']) && $spec['binary'] === FALSE) {
-          $sql .= ' COLLATE NOCASE_UTF8';
-        }
-      }
-
-      if (isset($spec['not null'])) {
-        if ($spec['not null']) {
-          $sql .= ' NOT NULL';
-        }
-        else {
-          $sql .= ' NULL';
-        }
-      }
-
-      if (!empty($spec['unsigned'])) {
-        $sql .= ' CHECK (' . $name . '>= 0)';
-      }
-
-      if (isset($spec['default'])) {
-        if (is_string($spec['default'])) {
-          $spec['default'] = $this->connection->quote($spec['default']);
-        }
-        $sql .= ' DEFAULT ' . $spec['default'];
-      }
-
-      if (empty($spec['not null']) && !isset($spec['default'])) {
-        $sql .= ' DEFAULT NULL';
-      }
-    }
-    return $sql;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFieldTypeMap() {
-    // Put :normal last so it gets preserved by array_flip. This makes
-    // it much easier for modules (such as schema.module) to map
-    // database types back into schema types.
-    // $map does not use drupal_static as its value never changes.
-    static $map = [
-      'varchar_ascii:normal' => 'VARCHAR',
-
-      'varchar:normal'  => 'VARCHAR',
-      'char:normal'     => 'CHAR',
-
-      'text:tiny'       => 'TEXT',
-      'text:small'      => 'TEXT',
-      'text:medium'     => 'TEXT',
-      'text:big'        => 'TEXT',
-      'text:normal'     => 'TEXT',
-
-      'serial:tiny'     => 'INTEGER',
-      'serial:small'    => 'INTEGER',
-      'serial:medium'   => 'INTEGER',
-      'serial:big'      => 'INTEGER',
-      'serial:normal'   => 'INTEGER',
-
-      'int:tiny'        => 'INTEGER',
-      'int:small'       => 'INTEGER',
-      'int:medium'      => 'INTEGER',
-      'int:big'         => 'INTEGER',
-      'int:normal'      => 'INTEGER',
-
-      'float:tiny'      => 'FLOAT',
-      'float:small'     => 'FLOAT',
-      'float:medium'    => 'FLOAT',
-      'float:big'       => 'FLOAT',
-      'float:normal'    => 'FLOAT',
-
-      'numeric:normal'  => 'NUMERIC',
-
-      'blob:big'        => 'BLOB',
-      'blob:normal'     => 'BLOB',
-    ];
-    return $map;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function renameTable($table, $new_name) {
-    if (!$this->tableExists($table)) {
-      throw new SchemaObjectDoesNotExistException("Cannot rename '$table' to '$new_name': table '$table' doesn't exist.");
-    }
-    if ($this->tableExists($new_name)) {
-      throw new SchemaObjectExistsException("Cannot rename '$table' to '$new_name': table '$new_name' already exists.");
-    }
-
-    $schema = $this->introspectSchema($table);
-
-    // SQLite doesn't allow you to rename tables outside of the current
-    // database. So the syntax '... RENAME TO database.table' would fail.
-    // So we must determine the full table name here rather than surrounding
-    // the table with curly braces in case the db_prefix contains a reference
-    // to a database outside of our existing database.
-    $info = $this->getPrefixInfo($new_name);
-    $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO ' . $info['table']);
-
-    // Drop the indexes, there is no RENAME INDEX command in SQLite.
-    if (!empty($schema['unique keys'])) {
-      foreach ($schema['unique keys'] as $key => $fields) {
-        $this->dropIndex($table, $key);
-      }
-    }
-    if (!empty($schema['indexes'])) {
-      foreach ($schema['indexes'] as $index => $fields) {
-        $this->dropIndex($table, $index);
-      }
-    }
-
-    // Recreate the indexes.
-    $statements = $this->createIndexSql($new_name, $schema);
-    foreach ($statements as $statement) {
-      $this->connection->query($statement);
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function dropTable($table) {
-    if (!$this->tableExists($table)) {
-      return FALSE;
-    }
-    $this->connection->tableDropped = TRUE;
-    $this->connection->query('DROP TABLE {' . $table . '}');
-    return TRUE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function addField($table, $field, $specification, $keys_new = []) {
-    if (!$this->tableExists($table)) {
-      throw new SchemaObjectDoesNotExistException("Cannot add field '$table.$field': table doesn't exist.");
-    }
-    if ($this->fieldExists($table, $field)) {
-      throw new SchemaObjectExistsException("Cannot add field '$table.$field': field already exists.");
-    }
-    if (isset($keys_new['primary key']) && in_array($field, $keys_new['primary key'], TRUE)) {
-      $this->ensureNotNullPrimaryKey($keys_new['primary key'], [$field => $specification]);
-    }
-
-    // SQLite doesn't have a full-featured ALTER TABLE statement. It only
-    // supports adding new fields to a table, in some simple cases. In most
-    // cases, we have to create a new table and copy the data over.
-    if (empty($keys_new) && (empty($specification['not null']) || isset($specification['default']))) {
-      // When we don't have to create new keys and we are not creating a
-      // NOT NULL column without a default value, we can use the quicker version.
-      $query = 'ALTER TABLE {' . $table . '} ADD ' . $this->createFieldSql($field, $this->processField($specification));
-      $this->connection->query($query);
-
-      // Apply the initial value if set.
-      if (isset($specification['initial_from_field'])) {
-        if (isset($specification['initial'])) {
-          $expression = 'COALESCE(' . $specification['initial_from_field'] . ', :default_initial_value)';
-          $arguments = [':default_initial_value' => $specification['initial']];
-        }
-        else {
-          $expression = $specification['initial_from_field'];
-          $arguments = [];
-        }
-        $this->connection->update($table)
-          ->expression($field, $expression, $arguments)
-          ->execute();
-      }
-      elseif (isset($specification['initial'])) {
-        $this->connection->update($table)
-          ->fields([$field => $specification['initial']])
-          ->execute();
-      }
-    }
-    else {
-      // We cannot add the field directly. Use the slower table alteration
-      // method, starting from the old schema.
-      $old_schema = $this->introspectSchema($table);
-      $new_schema = $old_schema;
-
-      // Add the new field.
-      $new_schema['fields'][$field] = $specification;
-
-      // Build the mapping between the old fields and the new fields.
-      $mapping = [];
-      if (isset($specification['initial_from_field'])) {
-        // If we have an initial value, copy it over.
-        if (isset($specification['initial'])) {
-          $expression = 'COALESCE(' . $specification['initial_from_field'] . ', :default_initial_value)';
-          $arguments = [':default_initial_value' => $specification['initial']];
-        }
-        else {
-          $expression = $specification['initial_from_field'];
-          $arguments = [];
-        }
-        $mapping[$field] = [
-          'expression' => $expression,
-          'arguments' => $arguments,
-        ];
-      }
-      elseif (isset($specification['initial'])) {
-        // If we have an initial value, copy it over.
-        $mapping[$field] = [
-          'expression' => ':newfieldinitial',
-          'arguments' => [':newfieldinitial' => $specification['initial']],
-        ];
-      }
-      else {
-        // Else use the default of the field.
-        $mapping[$field] = NULL;
-      }
-
-      // Add the new indexes.
-      $new_schema = array_merge($new_schema, $keys_new);
-
-      $this->alterTable($table, $old_schema, $new_schema, $mapping);
-    }
-  }
-
-  /**
-   * Create a table with a new schema containing the old content.
-   *
-   * As SQLite does not support ALTER TABLE (with a few exceptions) it is
-   * necessary to create a new table and copy over the old content.
-   *
-   * @param $table
-   *   Name of the table to be altered.
-   * @param $old_schema
-   *   The old schema array for the table.
-   * @param $new_schema
-   *   The new schema array for the table.
-   * @param $mapping
-   *   An optional mapping between the fields of the old specification and the
-   *   fields of the new specification. An associative array, whose keys are
-   *   the fields of the new table, and values can take two possible forms:
-   *     - a simple string, which is interpreted as the name of a field of the
-   *       old table,
-   *     - an associative array with two keys 'expression' and 'arguments',
-   *       that will be used as an expression field.
-   */
-  protected function alterTable($table, $old_schema, $new_schema, array $mapping = []) {
-    $i = 0;
-    do {
-      $new_table = $table . '_' . $i++;
-    } while ($this->tableExists($new_table));
-
-    $this->createTable($new_table, $new_schema);
-
-    // Build a SQL query to migrate the data from the old table to the new.
-    $select = $this->connection->select($table);
-
-    // Complete the mapping.
-    $possible_keys = array_keys($new_schema['fields']);
-    $mapping += array_combine($possible_keys, $possible_keys);
-
-    // Now add the fields.
-    foreach ($mapping as $field_alias => $field_source) {
-      // Just ignore this field (ie. use its default value).
-      if (!isset($field_source)) {
-        continue;
-      }
-
-      if (is_array($field_source)) {
-        $select->addExpression($field_source['expression'], $field_alias, $field_source['arguments']);
-      }
-      else {
-        $select->addField($table, $field_source, $field_alias);
-      }
-    }
-
-    // Execute the data migration query.
-    $this->connection->insert($new_table)
-      ->from($select)
-      ->execute();
-
-    $old_count = $this->connection->query('SELECT COUNT(*) FROM {' . $table . '}')->fetchField();
-    $new_count = $this->connection->query('SELECT COUNT(*) FROM {' . $new_table . '}')->fetchField();
-    if ($old_count == $new_count) {
-      $this->dropTable($table);
-      $this->renameTable($new_table, $table);
-    }
-  }
-
-  /**
-   * Find out the schema of a table.
-   *
-   * This function uses introspection methods provided by the database to
-   * create a schema array. This is useful, for example, during update when
-   * the old schema is not available.
-   *
-   * @param $table
-   *   Name of the table.
-   *
-   * @return
-   *   An array representing the schema.
-   *
-   * @throws \Exception
-   *   If a column of the table could not be parsed.
-   */
-  protected function introspectSchema($table) {
-    $mapped_fields = array_flip($this->getFieldTypeMap());
-    $schema = [
-      'fields' => [],
-      'primary key' => [],
-      'unique keys' => [],
-      'indexes' => [],
-    ];
-
-    $info = $this->getPrefixInfo($table);
-    $result = $this->connection->query('PRAGMA ' . $info['schema'] . '.table_info(' . $info['table'] . ')');
-    foreach ($result as $row) {
-      if (preg_match('/^([^(]+)\((.*)\)$/', $row->type, $matches)) {
-        $type = $matches[1];
-        $length = $matches[2];
-      }
-      else {
-        $type = $row->type;
-        $length = NULL;
-      }
-      if (isset($mapped_fields[$type])) {
-        [$type, $size] = explode(':', $mapped_fields[$type]);
-        $schema['fields'][$row->name] = [
-          'type' => $type,
-          'size' => $size,
-          'not null' => !empty($row->notnull) || $row->pk !== "0",
-        ];
-        if ($length) {
-          $schema['fields'][$row->name]['length'] = $length;
-        }
-
-        // Convert the default into a properly typed value.
-        if ($row->dflt_value === 'NULL') {
-          $schema['fields'][$row->name]['default'] = NULL;
-        }
-        elseif (is_string($row->dflt_value) && $row->dflt_value[0] === '\'') {
-          // Remove the wrapping single quotes. And replace duplicate single
-          // quotes with a single quote.
-          $schema['fields'][$row->name]['default'] = str_replace("''", "'", substr($row->dflt_value, 1, -1));
-        }
-        elseif (is_numeric($row->dflt_value)) {
-          // Adding 0 to a string will cause PHP to convert it to a float or
-          // an integer depending on what the string is. For example:
-          // - '1' + 0 = 1
-          // - '1.0' + 0 = 1.0
-          $schema['fields'][$row->name]['default'] = $row->dflt_value + 0;
-        }
-        else {
-          $schema['fields'][$row->name]['default'] = $row->dflt_value;
-        }
-        // $row->pk contains a number that reflects the primary key order. We
-        // use that as the key and sort (by key) below to return the primary key
-        // in the same order that it is stored in.
-        if ($row->pk) {
-          $schema['primary key'][$row->pk] = $row->name;
-        }
-      }
-      else {
-        throw new \Exception("Unable to parse the column type " . $row->type);
-      }
-    }
-    ksort($schema['primary key']);
-    // Re-key the array because $row->pk starts counting at 1.
-    $schema['primary key'] = array_values($schema['primary key']);
-
-    $indexes = [];
-    $result = $this->connection->query('PRAGMA ' . $info['schema'] . '.index_list(' . $info['table'] . ')');
-    foreach ($result as $row) {
-      if (strpos($row->name, 'sqlite_autoindex_') !== 0) {
-        $indexes[] = [
-          'schema_key' => $row->unique ? 'unique keys' : 'indexes',
-          'name' => $row->name,
-        ];
-      }
-    }
-    foreach ($indexes as $index) {
-      $name = $index['name'];
-      // Get index name without prefix.
-      $index_name = substr($name, strlen($info['table']) + 1);
-      $result = $this->connection->query('PRAGMA ' . $info['schema'] . '.index_info(' . $name . ')');
-      foreach ($result as $row) {
-        $schema[$index['schema_key']][$index_name][] = $row->name;
-      }
-    }
-    return $schema;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function dropField($table, $field) {
-    if (!$this->fieldExists($table, $field)) {
-      return FALSE;
-    }
-
-    $old_schema = $this->introspectSchema($table);
-    $new_schema = $old_schema;
-
-    unset($new_schema['fields'][$field]);
-
-    // Drop the primary key if the field to drop is part of it. This is
-    // consistent with the behavior on PostgreSQL.
-    // @see \Drupal\Core\Database\Driver\mysql\Schema::dropField()
-    if (isset($new_schema['primary key']) && in_array($field, $new_schema['primary key'], TRUE)) {
-      unset($new_schema['primary key']);
-    }
-
-    // Handle possible index changes.
-    foreach ($new_schema['indexes'] as $index => $fields) {
-      foreach ($fields as $key => $field_name) {
-        if ($field_name == $field) {
-          unset($new_schema['indexes'][$index][$key]);
-        }
-      }
-      // If this index has no more fields then remove it.
-      if (empty($new_schema['indexes'][$index])) {
-        unset($new_schema['indexes'][$index]);
-      }
-    }
-    $this->alterTable($table, $old_schema, $new_schema);
-    return TRUE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function changeField($table, $field, $field_new, $spec, $keys_new = []) {
-    if (!$this->fieldExists($table, $field)) {
-      throw new SchemaObjectDoesNotExistException("Cannot change the definition of field '$table.$field': field doesn't exist.");
-    }
-    if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
-      throw new SchemaObjectExistsException("Cannot rename field '$table.$field' to '$field_new': target field already exists.");
-    }
-    if (isset($keys_new['primary key']) && in_array($field_new, $keys_new['primary key'], TRUE)) {
-      $this->ensureNotNullPrimaryKey($keys_new['primary key'], [$field_new => $spec]);
-    }
-
-    $old_schema = $this->introspectSchema($table);
-    $new_schema = $old_schema;
-
-    // Map the old field to the new field.
-    if ($field != $field_new) {
-      $mapping[$field_new] = $field;
-    }
-    else {
-      $mapping = [];
-    }
-
-    // Remove the previous definition and swap in the new one.
-    unset($new_schema['fields'][$field]);
-    $new_schema['fields'][$field_new] = $spec;
-
-    // Map the former indexes to the new column name.
-    $new_schema['primary key'] = $this->mapKeyDefinition($new_schema['primary key'], $mapping);
-    foreach (['unique keys', 'indexes'] as $k) {
-      foreach ($new_schema[$k] as &$key_definition) {
-        $key_definition = $this->mapKeyDefinition($key_definition, $mapping);
-      }
-    }
-
-    // Add in the keys from $keys_new.
-    if (isset($keys_new['primary key'])) {
-      $new_schema['primary key'] = $keys_new['primary key'];
-    }
-    foreach (['unique keys', 'indexes'] as $k) {
-      if (!empty($keys_new[$k])) {
-        $new_schema[$k] = $keys_new[$k] + $new_schema[$k];
-      }
-    }
-
-    $this->alterTable($table, $old_schema, $new_schema, $mapping);
-  }
-
-  /**
-   * Utility method: rename columns in an index definition according to a new mapping.
-   *
-   * @param $key_definition
-   *   The key definition.
-   * @param $mapping
-   *   The new mapping.
-   */
-  protected function mapKeyDefinition(array $key_definition, array $mapping) {
-    foreach ($key_definition as &$field) {
-      // The key definition can be an array($field, $length).
-      if (is_array($field)) {
-        $field = &$field[0];
-      }
-
-      $mapped_field = array_search($field, $mapping, TRUE);
-      if ($mapped_field !== FALSE) {
-        $field = $mapped_field;
-      }
-    }
-    return $key_definition;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function addIndex($table, $name, $fields, array $spec) {
-    if (!$this->tableExists($table)) {
-      throw new SchemaObjectDoesNotExistException("Cannot add index '$name' to table '$table': table doesn't exist.");
-    }
-    if ($this->indexExists($table, $name)) {
-      throw new SchemaObjectExistsException("Cannot add index '$name' to table '$table': index already exists.");
-    }
-
-    $schema['indexes'][$name] = $fields;
-    $statements = $this->createIndexSql($table, $schema);
-    foreach ($statements as $statement) {
-      $this->connection->query($statement);
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function indexExists($table, $name) {
-    $info = $this->getPrefixInfo($table);
-
-    return $this->connection->query('PRAGMA ' . $info['schema'] . '.index_info(' . $info['table'] . '_' . $name . ')')->fetchField() != '';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function dropIndex($table, $name) {
-    if (!$this->indexExists($table, $name)) {
-      return FALSE;
-    }
-
-    $info = $this->getPrefixInfo($table);
-
-    $this->connection->query('DROP INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $name);
-    return TRUE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function addUniqueKey($table, $name, $fields) {
-    if (!$this->tableExists($table)) {
-      throw new SchemaObjectDoesNotExistException("Cannot add unique key '$name' to table '$table': table doesn't exist.");
-    }
-    if ($this->indexExists($table, $name)) {
-      throw new SchemaObjectExistsException("Cannot add unique key '$name' to table '$table': unique key already exists.");
-    }
-
-    $schema['unique keys'][$name] = $fields;
-    $statements = $this->createIndexSql($table, $schema);
-    foreach ($statements as $statement) {
-      $this->connection->query($statement);
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function dropUniqueKey($table, $name) {
-    if (!$this->indexExists($table, $name)) {
-      return FALSE;
-    }
-
-    $info = $this->getPrefixInfo($table);
-
-    $this->connection->query('DROP INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $name);
-    return TRUE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function addPrimaryKey($table, $fields) {
-    if (!$this->tableExists($table)) {
-      throw new SchemaObjectDoesNotExistException("Cannot add primary key to table '$table': table doesn't exist.");
-    }
-
-    $old_schema = $this->introspectSchema($table);
-    $new_schema = $old_schema;
-
-    if (!empty($new_schema['primary key'])) {
-      throw new SchemaObjectExistsException("Cannot add primary key to table '$table': primary key already exists.");
-    }
-
-    $new_schema['primary key'] = $fields;
-    $this->ensureNotNullPrimaryKey($new_schema['primary key'], $new_schema['fields']);
-    $this->alterTable($table, $old_schema, $new_schema);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function dropPrimaryKey($table) {
-    $old_schema = $this->introspectSchema($table);
-    $new_schema = $old_schema;
-
-    if (empty($new_schema['primary key'])) {
-      return FALSE;
-    }
-
-    unset($new_schema['primary key']);
-    $this->alterTable($table, $old_schema, $new_schema);
-    return TRUE;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function findPrimaryKeyColumns($table) {
-    if (!$this->tableExists($table)) {
-      return FALSE;
-    }
-    $schema = $this->introspectSchema($table);
-    return $schema['primary key'];
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function introspectIndexSchema($table) {
-    if (!$this->tableExists($table)) {
-      throw new SchemaObjectDoesNotExistException("The table $table doesn't exist.");
-    }
-    $schema = $this->introspectSchema($table);
-    unset($schema['fields']);
-    return $schema;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function findTables($table_expression) {
-    $tables = [];
-
-    // The SQLite implementation doesn't need to use the same filtering strategy
-    // as the parent one because individually prefixed tables live in their own
-    // schema (database), which means that neither the main database nor any
-    // attached one will contain a prefixed table name, so we just need to loop
-    // over all known schemas and filter by the user-supplied table expression.
-    $attached_dbs = $this->connection->getAttachedDatabases();
-    foreach ($attached_dbs as $schema) {
-      // Can't use query placeholders for the schema because the query would
-      // have to be :prefixsqlite_master, which does not work. We also need to
-      // ignore the internal SQLite tables.
-      $result = $this->connection->query("SELECT name FROM " . $schema . ".sqlite_master WHERE type = :type AND name LIKE :table_name AND name NOT LIKE :pattern", [
-        ':type' => 'table',
-        ':table_name' => $table_expression,
-        ':pattern' => 'sqlite_%',
-      ]);
-      $tables += $result->fetchAllKeyed(0, 0);
-    }
-
-    return $tables;
-  }
-
-}
+class Schema extends SqliteSchema {}
diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Select.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Select.php
index 5eaa5157d860..8cfe31cf3f23 100644
--- a/core/lib/Drupal/Core/Database/Driver/sqlite/Select.php
+++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Select.php
@@ -2,16 +2,16 @@
 
 namespace Drupal\Core\Database\Driver\sqlite;
 
-use Drupal\Core\Database\Query\Select as QuerySelect;
+use Drupal\sqlite\Driver\Database\sqlite\Select as SqliteSelect;
+
+@trigger_error('\Drupal\Core\Database\Driver\sqlite\Select is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED);
 
 /**
  * SQLite implementation of \Drupal\Core\Database\Query\Select.
+ *
+ * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite
+ *   database driver has been moved to the sqlite module.
+ *
+ * @see https://www.drupal.org/node/3129492
  */
-class Select extends QuerySelect {
-
-  public function forUpdate($set = TRUE) {
-    // SQLite does not support FOR UPDATE so nothing to do.
-    return $this;
-  }
-
-}
+class Select extends SqliteSelect {}
diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Statement.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Statement.php
index 1e895629ea14..c189fe84943e 100644
--- a/core/lib/Drupal/Core/Database/Driver/sqlite/Statement.php
+++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Statement.php
@@ -2,150 +2,16 @@
 
 namespace Drupal\Core\Database\Driver\sqlite;
 
-use Drupal\Core\Database\StatementPrefetch;
-use Drupal\Core\Database\StatementInterface;
+use Drupal\sqlite\Driver\Database\sqlite\Statement as SqliteStatement;
+
+@trigger_error('\Drupal\Core\Database\Driver\sqlite\Statement is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED);
 
 /**
  * SQLite implementation of \Drupal\Core\Database\Statement.
  *
- * The PDO SQLite driver only closes SELECT statements when the PDOStatement
- * destructor is called and SQLite does not allow data change (INSERT,
- * UPDATE etc) on a table which has open SELECT statements. This is a
- * user-space mock of PDOStatement that buffers all the data and doesn't
- * have those limitations.
+ * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite
+ *   database driver has been moved to the sqlite module.
+ *
+ * @see https://www.drupal.org/node/3129492
  */
-class Statement extends StatementPrefetch implements StatementInterface {
-
-  /**
-   * {@inheritdoc}
-   *
-   * The PDO SQLite layer doesn't replace numeric placeholders in queries
-   * correctly, and this makes numeric expressions (such as COUNT(*) >= :count)
-   * fail. We replace numeric placeholders in the query ourselves to work
-   * around this bug.
-   *
-   * See http://bugs.php.net/bug.php?id=45259 for more details.
-   */
-  protected function getStatement($query, &$args = []) {
-    if (is_array($args) && !empty($args)) {
-      // Check if $args is a simple numeric array.
-      if (range(0, count($args) - 1) === array_keys($args)) {
-        // In that case, we have unnamed placeholders.
-        $count = 0;
-        $new_args = [];
-        foreach ($args as $value) {
-          if (is_float($value) || is_int($value)) {
-            if (is_float($value)) {
-              // Force the conversion to float so as not to loose precision
-              // in the automatic cast.
-              $value = sprintf('%F', $value);
-            }
-            $query = substr_replace($query, $value, strpos($query, '?'), 1);
-          }
-          else {
-            $placeholder = ':db_statement_placeholder_' . $count++;
-            $query = substr_replace($query, $placeholder, strpos($query, '?'), 1);
-            $new_args[$placeholder] = $value;
-          }
-        }
-        $args = $new_args;
-      }
-      else {
-        // Else, this is using named placeholders.
-        foreach ($args as $placeholder => $value) {
-          if (is_float($value) || is_int($value)) {
-            if (is_float($value)) {
-              // Force the conversion to float so as not to loose precision
-              // in the automatic cast.
-              $value = sprintf('%F', $value);
-            }
-
-            // We will remove this placeholder from the query as PDO throws an
-            // exception if the number of placeholders in the query and the
-            // arguments does not match.
-            unset($args[$placeholder]);
-            // PDO allows placeholders to not be prefixed by a colon. See
-            // http://marc.info/?l=php-internals&m=111234321827149&w=2 for
-            // more.
-            if ($placeholder[0] != ':') {
-              $placeholder = ":$placeholder";
-            }
-            // When replacing the placeholders, make sure we search for the
-            // exact placeholder. For example, if searching for
-            // ':db_placeholder_1', do not replace ':db_placeholder_11'.
-            $query = preg_replace('/' . preg_quote($placeholder) . '\b/', $value, $query);
-          }
-        }
-      }
-    }
-
-    return $this->pdoConnection->prepare($query);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function execute($args = [], $options = []) {
-    try {
-      $return = parent::execute($args, $options);
-    }
-    catch (\PDOException $e) {
-      // The database schema might be changed by another process in between the
-      // time that the statement was prepared and the time the statement was run
-      // (e.g. usually happens when running tests). In this case, we need to
-      // re-run the query.
-      // @see http://www.sqlite.org/faq.html#q15
-      // @see http://www.sqlite.org/rescode.html#schema
-      if (!empty($e->errorInfo[1]) && $e->errorInfo[1] === 17) {
-        // The schema has changed. SQLite specifies that we must resend the query.
-        $return = parent::execute($args, $options);
-      }
-      else {
-        // Rethrow the exception.
-        throw $e;
-      }
-    }
-
-    // In some weird cases, SQLite will prefix some column names by the name
-    // of the table. We post-process the data, by renaming the column names
-    // using the same convention as MySQL and PostgreSQL.
-    $rename_columns = [];
-    foreach ($this->columnNames as $k => $column) {
-      // In some SQLite versions, SELECT DISTINCT(field) will return "(field)"
-      // instead of "field".
-      if (preg_match("/^\((.*)\)$/", $column, $matches)) {
-        $rename_columns[$column] = $matches[1];
-        $this->columnNames[$k] = $matches[1];
-        $column = $matches[1];
-      }
-
-      // Remove "table." prefixes.
-      if (preg_match("/^.*\.(.*)$/", $column, $matches)) {
-        $rename_columns[$column] = $matches[1];
-        $this->columnNames[$k] = $matches[1];
-      }
-    }
-    if ($rename_columns) {
-      // DatabaseStatementPrefetch already extracted the first row,
-      // put it back into the result set.
-      if (isset($this->currentRow)) {
-        $this->data[0] = &$this->currentRow;
-      }
-
-      // Then rename all the columns across the result set.
-      foreach ($this->data as $k => $row) {
-        foreach ($rename_columns as $old_column => $new_column) {
-          $this->data[$k][$new_column] = $this->data[$k][$old_column];
-          unset($this->data[$k][$old_column]);
-        }
-      }
-
-      // Finally, extract the first row again.
-      $this->currentRow = $this->data[0];
-      unset($this->data[0]);
-    }
-
-    return $return;
-  }
-
-}
+class Statement extends SqliteStatement {}
diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Truncate.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Truncate.php
index 386912f87801..d9f83cac5b1f 100644
--- a/core/lib/Drupal/Core/Database/Driver/sqlite/Truncate.php
+++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Truncate.php
@@ -2,21 +2,16 @@
 
 namespace Drupal\Core\Database\Driver\sqlite;
 
-use Drupal\Core\Database\Query\Truncate as QueryTruncate;
+use Drupal\sqlite\Driver\Database\sqlite\Truncate as SqliteTruncate;
+
+@trigger_error('\Drupal\Core\Database\Driver\sqlite\Truncate is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED);
 
 /**
  * SQLite implementation of \Drupal\Core\Database\Query\Truncate.
  *
- * SQLite doesn't support TRUNCATE, but a DELETE query with no condition has
- * exactly the effect (it is implemented by DROPing the table).
+ * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite
+ *   database driver has been moved to the sqlite module.
+ *
+ * @see https://www.drupal.org/node/3129492
  */
-class Truncate extends QueryTruncate {
-
-  public function __toString() {
-    // Create a sanitized comment string to prepend to the query.
-    $comments = $this->connection->makeComment($this->comments);
-
-    return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '} ';
-  }
-
-}
+class Truncate extends SqliteTruncate {}
diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php
index f97f6c8d8b4c..9a6ad99d2908 100644
--- a/core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php
+++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php
@@ -2,46 +2,16 @@
 
 namespace Drupal\Core\Database\Driver\sqlite;
 
-use Drupal\Core\Database\Query\Upsert as QueryUpsert;
+use Drupal\sqlite\Driver\Database\sqlite\Upsert as SqliteUpsert;
+
+@trigger_error('\Drupal\Core\Database\Driver\sqlite\Upsert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492', E_USER_DEPRECATED);
 
 /**
  * SQLite implementation of \Drupal\Core\Database\Query\Upsert.
  *
- * @see https://www.sqlite.org/lang_UPSERT.html
+ * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite
+ *   database driver has been moved to the sqlite module.
+ *
+ * @see https://www.drupal.org/node/3129492
  */
-class Upsert extends QueryUpsert {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function __toString() {
-    // Create a sanitized comment string to prepend to the query.
-    $comments = $this->connection->makeComment($this->comments);
-
-    // 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);
-
-    $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
-
-    $values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
-    $query .= implode(', ', $values);
-
-    // Updating the unique / primary key is not necessary.
-    unset($insert_fields[$this->key]);
-
-    $update = [];
-    foreach ($insert_fields as $field) {
-      // The "excluded." prefix causes the field to refer to the value for field
-      // that would have been inserted had there been no conflict.
-      $update[] = "$field = EXCLUDED.$field";
-    }
-
-    $query .= ' ON CONFLICT (' . $this->connection->escapeField($this->key) . ') DO UPDATE SET ' . implode(', ', $update);
-
-    return $query;
-  }
-
-}
+class Upsert extends SqliteUpsert {}
diff --git a/core/lib/Drupal/Core/Database/StatementInterface.php b/core/lib/Drupal/Core/Database/StatementInterface.php
index 8bfb76201329..0bb030d0172e 100644
--- a/core/lib/Drupal/Core/Database/StatementInterface.php
+++ b/core/lib/Drupal/Core/Database/StatementInterface.php
@@ -2,18 +2,20 @@
 
 namespace Drupal\Core\Database;
 
+// cSpell:ignore mydriver
+
 /**
  * Represents a prepared statement.
  *
- * Child implementations should either extend PDOStatement:
+ * Child implementations should either extend StatementWrapper:
  * @code
- * class Drupal\Core\Database\Driver\oracle\Statement extends PDOStatement implements Drupal\Core\Database\StatementInterface {}
+ * class Drupal\mymodule\Driver\Database\mydriver\Statement extends Drupal\Core\Database\StatementWrapper {}
  * @endcode
  * or define their own class. If defining their own class, they will also have
  * to implement either the Iterator or IteratorAggregate interface before
  * Drupal\Core\Database\StatementInterface:
  * @code
- * class Drupal\Core\Database\Driver\oracle\Statement implements Iterator, Drupal\Core\Database\StatementInterface {}
+ * class Drupal\mymodule\Driver\Database\mydriver\Statement implements Iterator, Drupal\Core\Database\StatementInterface {}
  * @endcode
  *
  * @ingroup database
diff --git a/core/lib/Drupal/Core/Database/StatementPrefetch.php b/core/lib/Drupal/Core/Database/StatementPrefetch.php
index ab68dec877b4..267b6a85d55f 100644
--- a/core/lib/Drupal/Core/Database/StatementPrefetch.php
+++ b/core/lib/Drupal/Core/Database/StatementPrefetch.php
@@ -224,7 +224,7 @@ public function execute($args = [], $options = []) {
     // as soon as possible.
     $this->data = $statement->fetchAll(\PDO::FETCH_ASSOC);
     // Destroy the statement as soon as possible. See the documentation of
-    // \Drupal\Core\Database\Driver\sqlite\Statement for an explanation.
+    // \Drupal\sqlite\Driver\Database\sqlite\Statement for an explanation.
     unset($statement);
 
     $this->resultRowCount = count($this->data);
diff --git a/core/lib/Drupal/Core/Site/Settings.php b/core/lib/Drupal/Core/Site/Settings.php
index 31b2b8f0afa5..3998719e8bc9 100644
--- a/core/lib/Drupal/Core/Site/Settings.php
+++ b/core/lib/Drupal/Core/Site/Settings.php
@@ -162,6 +162,43 @@ public static function initialize($app_root, $site_path, &$class_loader) {
     // Initialize databases.
     foreach ($databases as $key => $targets) {
       foreach ($targets as $target => $info) {
+        // Backwards compatibility layer for Drupal 8 style database connection
+        // arrays. Those do not have the 'autoload' key set for core database
+        // drivers.
+        if (empty($info['autoload'])) {
+          switch (strtolower($info['driver'])) {
+            case 'mysql':
+              $info['autoload'] = 'core/modules/mysql/src/Driver/Database/mysql/';
+              break;
+
+            case 'pgsql':
+              $info['autoload'] = 'core/modules/pgsql/src/Driver/Database/pgsql/';
+              break;
+
+            case 'sqlite':
+              $info['autoload'] = 'core/modules/sqlite/src/Driver/Database/sqlite/';
+              break;
+          }
+        }
+        // Backwards compatibility layer for Drupal 8 style database connection
+        // arrays. Those have the wrong 'namespace' key set, or not set at all
+        // for core supported database drivers.
+        if (empty($info['namespace']) || (strpos($info['namespace'], 'Drupal\\Core\\Database\\Driver\\') === 0)) {
+          switch (strtolower($info['driver'])) {
+            case 'mysql':
+              $info['namespace'] = 'Drupal\\mysql\\Driver\\Database\\mysql';
+              break;
+
+            case 'pgsql':
+              $info['namespace'] = 'Drupal\\pgsql\\Driver\\Database\\pgsql';
+              break;
+
+            case 'sqlite':
+              $info['namespace'] = 'Drupal\\sqlite\\Driver\\Database\\sqlite';
+              break;
+          }
+        }
+
         Database::addConnectionInfo($key, $target, $info);
         // If the database driver is provided by a module, then its code may
         // need to be instantiated prior to when the module's root namespace
diff --git a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php
index e1a234c92973..244d0eeb468e 100644
--- a/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php
+++ b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php
@@ -516,6 +516,7 @@ protected function installParameters() {
     $driver = $connection_info['default']['driver'];
     unset($connection_info['default']['driver']);
     unset($connection_info['default']['namespace']);
+    unset($connection_info['default']['autoload']);
     unset($connection_info['default']['pdo']);
     unset($connection_info['default']['init_commands']);
     // Remove database connection info that is not used by SQLite.
diff --git a/core/modules/comment/tests/src/Unit/CommentStatisticsUnitTest.php b/core/modules/comment/tests/src/Unit/CommentStatisticsUnitTest.php
index d8c390dfe876..b0412210e717 100644
--- a/core/modules/comment/tests/src/Unit/CommentStatisticsUnitTest.php
+++ b/core/modules/comment/tests/src/Unit/CommentStatisticsUnitTest.php
@@ -51,7 +51,7 @@ class CommentStatisticsUnitTest extends UnitTestCase {
    * Sets up required mocks and the CommentStatistics service under test.
    */
   protected function setUp(): void {
-    $this->statement = $this->getMockBuilder('Drupal\Core\Database\Driver\sqlite\Statement')
+    $this->statement = $this->getMockBuilder('Drupal\sqlite\Driver\Database\sqlite\Statement')
       ->disableOriginalConstructor()
       ->getMock();
 
diff --git a/core/modules/config/tests/src/Functional/ConfigImportAllTest.php b/core/modules/config/tests/src/Functional/ConfigImportAllTest.php
index f6311e0dab06..e0b1ed891b81 100644
--- a/core/modules/config/tests/src/Functional/ConfigImportAllTest.php
+++ b/core/modules/config/tests/src/Functional/ConfigImportAllTest.php
@@ -87,10 +87,13 @@ public function testInstallUninstall() {
     field_purge_batch(1000);
 
     $all_modules = \Drupal::service('extension.list.module')->getList();
+    $database_module = \Drupal::service('database')->getProvider();
+    $expected_modules = ['path_alias', 'system', 'user', 'standard', $database_module];
 
     // Ensure that only core required modules and the install profile can not be uninstalled.
     $validation_reasons = \Drupal::service('module_installer')->validateUninstall(array_keys($all_modules));
-    $this->assertEquals(['path_alias', 'system', 'user', 'standard'], array_keys($validation_reasons));
+    $validation_modules = array_keys($validation_reasons);
+    $this->assertEqualsCanonicalizing($expected_modules, $validation_modules);
 
     $modules_to_uninstall = array_filter($all_modules, function ($module) use ($validation_reasons) {
       // Filter required and not enabled modules.
@@ -103,6 +106,9 @@ public function testInstallUninstall() {
     // Can not uninstall config and use admin/config/development/configuration!
     unset($modules_to_uninstall['config']);
 
+    // Can not uninstall the database module.
+    unset($modules_to_uninstall[$database_module]);
+
     $this->assertTrue(isset($modules_to_uninstall['comment']), 'The comment module will be disabled');
     $this->assertTrue(isset($modules_to_uninstall['file']), 'The File module will be disabled');
     $this->assertTrue(isset($modules_to_uninstall['editor']), 'The Editor module will be disabled');
diff --git a/core/modules/migrate/tests/src/Kernel/MigrateSqlSourceTestBase.php b/core/modules/migrate/tests/src/Kernel/MigrateSqlSourceTestBase.php
index bbb4168badcb..fcae35cb646b 100644
--- a/core/modules/migrate/tests/src/Kernel/MigrateSqlSourceTestBase.php
+++ b/core/modules/migrate/tests/src/Kernel/MigrateSqlSourceTestBase.php
@@ -3,7 +3,7 @@
 namespace Drupal\Tests\migrate\Kernel;
 
 use Drupal\Core\Cache\MemoryCounterBackendFactory;
-use Drupal\Core\Database\Driver\sqlite\Connection;
+use Drupal\sqlite\Driver\Database\sqlite\Connection;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 
 /**
@@ -26,7 +26,7 @@ public function register(ContainerBuilder $container) {
    *   The source data, keyed by table name. Each table is an array containing
    *   the rows in that table.
    *
-   * @return \Drupal\Core\Database\Driver\sqlite\Connection
+   * @return \Drupal\sqlite\Driver\Database\sqlite\Connection
    *   The SQLite database connection.
    */
   protected function getDatabase(array $source_data) {
diff --git a/core/modules/migrate/tests/src/Kernel/QueryBatchTest.php b/core/modules/migrate/tests/src/Kernel/QueryBatchTest.php
index 7844e4144dc3..a7f6e3db1524 100644
--- a/core/modules/migrate/tests/src/Kernel/QueryBatchTest.php
+++ b/core/modules/migrate/tests/src/Kernel/QueryBatchTest.php
@@ -6,7 +6,7 @@
 use Drupal\migrate\MigrateException;
 use Drupal\migrate\Plugin\MigrateIdMapInterface;
 use Drupal\migrate\Plugin\MigrationInterface;
-use Drupal\Core\Database\Driver\sqlite\Connection;
+use Drupal\sqlite\Driver\Database\sqlite\Connection;
 
 /**
  * Tests query batching.
@@ -224,7 +224,7 @@ protected function getPlugin($configuration) {
    *   The source data, keyed by table name. Each table is an array containing
    *   the rows in that table.
    *
-   * @return \Drupal\Core\Database\Driver\sqlite\Connection
+   * @return \Drupal\sqlite\Driver\Database\sqlite\Connection
    *   The SQLite database connection.
    */
   protected function getDatabase(array $source_data) {
diff --git a/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php b/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php
index 31f456233b91..7908fd912dce 100644
--- a/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php
+++ b/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php
@@ -2,7 +2,7 @@
 
 namespace Drupal\Tests\migrate\Unit;
 
-use Drupal\Core\Database\Driver\sqlite\Connection;
+use Drupal\sqlite\Driver\Database\sqlite\Connection;
 use Drupal\migrate\Plugin\MigrationInterface;
 use Drupal\migrate\MigrateException;
 use Drupal\migrate\Plugin\MigrateIdMapInterface;
@@ -970,7 +970,7 @@ public function testGetQualifiedMapTablePrefix() {
     $qualified_map_table = $this->getIdMap()->getQualifiedMapTableName();
     // The SQLite driver is a special flower. It will prefix tables with
     // PREFIX.TABLE, instead of the standard PREFIXTABLE.
-    // @see \Drupal\Core\Database\Driver\sqlite\Connection::__construct()
+    // @see \Drupal\sqlite\Driver\Database\sqlite\Connection::__construct()
     $this->assertEquals('prefix.migrate_map_sql_idmap_test', $qualified_map_table);
   }
 
diff --git a/core/modules/migrate/tests/src/Unit/MigrateTestCase.php b/core/modules/migrate/tests/src/Unit/MigrateTestCase.php
index aef21d1a720b..6021fab24097 100644
--- a/core/modules/migrate/tests/src/Unit/MigrateTestCase.php
+++ b/core/modules/migrate/tests/src/Unit/MigrateTestCase.php
@@ -2,7 +2,7 @@
 
 namespace Drupal\Tests\migrate\Unit;
 
-use Drupal\Core\Database\Driver\sqlite\Connection;
+use Drupal\sqlite\Driver\Database\sqlite\Connection;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\migrate\Plugin\MigrateIdMapInterface;
 use Drupal\migrate\Plugin\MigrationInterface;
@@ -106,7 +106,7 @@ protected function getMigration($id_map = NULL) {
    *   (optional) Options for the database connection. Defaults to an empty
    *   array.
    *
-   * @return \Drupal\Core\Database\Driver\sqlite\Connection
+   * @return \Drupal\sqlite\Driver\Database\sqlite\Connection
    *   The database connection.
    */
   protected function getDatabase(array $database_contents, $connection_options = []) {
diff --git a/core/modules/mysql/mysql.info.yml b/core/modules/mysql/mysql.info.yml
new file mode 100644
index 000000000000..3a85112110b1
--- /dev/null
+++ b/core/modules/mysql/mysql.info.yml
@@ -0,0 +1,5 @@
+name: MySQL
+type: module
+description: 'Database driver for MySQL.'
+package: Core
+version: VERSION
diff --git a/core/modules/mysql/mysql.module b/core/modules/mysql/mysql.module
new file mode 100644
index 000000000000..a8572bf8650b
--- /dev/null
+++ b/core/modules/mysql/mysql.module
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * @file
+ * The MySQL module provides the connection between Drupal and a MySQL, MariaDB or equivalent database.
+ */
+
+use Drupal\Core\Routing\RouteMatchInterface;
+
+/**
+ * Implements hook_help().
+ */
+function mysql_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    case 'help.page.mysql':
+      $output = '';
+      $output .= '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('The MySQL module provides the connection between Drupal and a MySQL, MariaDB or equivalent database. For more information, see the <a href=":mysql">online documentation for the MySQL module</a>.', [':mysql' => 'https://www.drupal.org/documentation/modules/mysql']) . '</p>';
+      return $output;
+
+  }
+}
diff --git a/core/modules/mysql/src/Driver/Database/mysql/Connection.php b/core/modules/mysql/src/Driver/Database/mysql/Connection.php
new file mode 100644
index 000000000000..4eb7700bf658
--- /dev/null
+++ b/core/modules/mysql/src/Driver/Database/mysql/Connection.php
@@ -0,0 +1,495 @@
+<?php
+
+namespace Drupal\mysql\Driver\Database\mysql;
+
+use Drupal\Core\Database\DatabaseAccessDeniedException;
+use Drupal\Core\Database\IntegrityConstraintViolationException;
+use Drupal\Core\Database\DatabaseExceptionWrapper;
+use Drupal\Core\Database\StatementWrapper;
+use Drupal\Core\Database\Database;
+use Drupal\Core\Database\DatabaseNotFoundException;
+use Drupal\Core\Database\DatabaseException;
+use Drupal\Core\Database\Connection as DatabaseConnection;
+use Drupal\Core\Database\TransactionNoActiveException;
+
+/**
+ * @addtogroup database
+ * @{
+ */
+
+/**
+ * MySQL implementation of \Drupal\Core\Database\Connection.
+ */
+class Connection extends DatabaseConnection {
+
+  /**
+   * Error code for "Unknown database" error.
+   */
+  const DATABASE_NOT_FOUND = 1049;
+
+  /**
+   * Error code for "Access denied" error.
+   */
+  const ACCESS_DENIED = 1045;
+
+  /**
+   * Error code for "Can't initialize character set" error.
+   */
+  const UNSUPPORTED_CHARSET = 2019;
+
+  /**
+   * Driver-specific error code for "Unknown character set" error.
+   */
+  const UNKNOWN_CHARSET = 1115;
+
+  /**
+   * SQLSTATE error code for "Syntax error or access rule violation".
+   */
+  const SQLSTATE_SYNTAX_ERROR = 42000;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $statementClass = NULL;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $statementWrapperClass = StatementWrapper::class;
+
+  /**
+   * Flag to indicate if the cleanup function in __destruct() should run.
+   *
+   * @var bool
+   */
+  protected $needsCleanup = FALSE;
+
+  /**
+   * Stores the server version after it has been retrieved from the database.
+   *
+   * @var string
+   *
+   * @see \Drupal\mysql\Driver\Database\mysql\Connection::version
+   */
+  private $serverVersion;
+
+  /**
+   * The minimal possible value for the max_allowed_packet setting of MySQL.
+   *
+   * @link https://mariadb.com/kb/en/mariadb/server-system-variables/#max_allowed_packet
+   * @link https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_max_allowed_packet
+   *
+   * @var int
+   */
+  const MIN_MAX_ALLOWED_PACKET = 1024;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $identifierQuotes = ['"', '"'];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(\PDO $connection, array $connection_options) {
+    // If the SQL mode doesn't include 'ANSI_QUOTES' (explicitly or via a
+    // combination mode), then MySQL doesn't interpret a double quote as an
+    // identifier quote, in which case use the non-ANSI-standard backtick.
+    //
+    // Because we still support MySQL 5.7, check for the deprecated combination
+    // modes as well.
+    //
+    // @see https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_ansi_quotes
+    $ansi_quotes_modes = ['ANSI_QUOTES', 'ANSI', 'DB2', 'MAXDB', 'MSSQL', 'ORACLE', 'POSTGRESQL'];
+    $is_ansi_quotes_mode = FALSE;
+    foreach ($ansi_quotes_modes as $mode) {
+      // None of the modes in $ansi_quotes_modes are substrings of other modes
+      // that are not in $ansi_quotes_modes, so a simple stripos() does not
+      // return false positives.
+      if (stripos($connection_options['init_commands']['sql_mode'], $mode) !== FALSE) {
+        $is_ansi_quotes_mode = TRUE;
+        break;
+      }
+    }
+    if ($this->identifierQuotes === ['"', '"'] && !$is_ansi_quotes_mode) {
+      $this->identifierQuotes = ['`', '`'];
+    }
+    parent::__construct($connection, $connection_options);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function handleQueryException(\PDOException $e, $query, array $args = [], $options = []) {
+    // In case of attempted INSERT of a record with an undefined column and no
+    // default value indicated in schema, MySql returns a 1364 error code.
+    // Throw an IntegrityConstraintViolationException here like the other
+    // drivers do, to avoid the parent class to throw a generic
+    // DatabaseExceptionWrapper instead.
+    if (!empty($e->errorInfo[1]) && $e->errorInfo[1] === 1364) {
+      @trigger_error('Connection::handleQueryException() is deprecated in drupal:9.2.0 and is removed in drupal:10.0.0. Get a handler through $this->exceptionHandler() instead, and use one of its methods. See https://www.drupal.org/node/3187222', E_USER_DEPRECATED);
+      $query_string = ($query instanceof StatementInterface) ? $query->getQueryString() : $query;
+      $message = $e->getMessage() . ": " . $query_string . "; " . print_r($args, TRUE);
+      throw new IntegrityConstraintViolationException($message, is_int($e->getCode()) ? $e->getCode() : 0, $e);
+    }
+
+    parent::handleQueryException($e, $query, $args, $options);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function open(array &$connection_options = []) {
+    if (isset($connection_options['_dsn_utf8_fallback']) && $connection_options['_dsn_utf8_fallback'] === TRUE) {
+      // Only used during the installer version check, as a fallback from utf8mb4.
+      $charset = 'utf8';
+    }
+    else {
+      $charset = 'utf8mb4';
+    }
+    // The DSN should use either a socket or a host/port.
+    if (isset($connection_options['unix_socket'])) {
+      $dsn = 'mysql:unix_socket=' . $connection_options['unix_socket'];
+    }
+    else {
+      // Default to TCP connection on port 3306.
+      $dsn = 'mysql:host=' . $connection_options['host'] . ';port=' . (empty($connection_options['port']) ? 3306 : $connection_options['port']);
+    }
+    // Character set is added to dsn to ensure PDO uses the proper character
+    // set when escaping. This has security implications. See
+    // https://www.drupal.org/node/1201452 for further discussion.
+    $dsn .= ';charset=' . $charset;
+    if (!empty($connection_options['database'])) {
+      $dsn .= ';dbname=' . $connection_options['database'];
+    }
+    // Allow PDO options to be overridden.
+    $connection_options += [
+      'pdo' => [],
+    ];
+    $connection_options['pdo'] += [
+      \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
+      // So we don't have to mess around with cursors and unbuffered queries by default.
+      \PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => TRUE,
+      // Make sure MySQL returns all matched rows on update queries including
+      // rows that actually didn't have to be updated because the values didn't
+      // change. This matches common behavior among other database systems.
+      \PDO::MYSQL_ATTR_FOUND_ROWS => TRUE,
+      // Because MySQL's prepared statements skip the query cache, because it's dumb.
+      \PDO::ATTR_EMULATE_PREPARES => TRUE,
+      // Limit SQL to a single statement like mysqli.
+      \PDO::MYSQL_ATTR_MULTI_STATEMENTS => FALSE,
+      // Convert numeric values to strings when fetching. In PHP 8.1,
+      // \PDO::ATTR_EMULATE_PREPARES now behaves the same way as non emulated
+      // prepares and returns integers. See https://externals.io/message/113294
+      // for further discussion.
+      \PDO::ATTR_STRINGIFY_FETCHES => TRUE,
+    ];
+
+    try {
+      $pdo = new \PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
+    }
+    catch (\PDOException $e) {
+      if ($e->getCode() == static::DATABASE_NOT_FOUND) {
+        throw new DatabaseNotFoundException($e->getMessage(), $e->getCode(), $e);
+      }
+      if ($e->getCode() == static::ACCESS_DENIED) {
+        throw new DatabaseAccessDeniedException($e->getMessage(), $e->getCode(), $e);
+      }
+      throw $e;
+    }
+
+    // Force MySQL to use the UTF-8 character set. Also set the collation, if a
+    // certain one has been set; otherwise, MySQL defaults to
+    // 'utf8mb4_general_ci' (MySQL 5) or 'utf8mb4_0900_ai_ci' (MySQL 8) for
+    // utf8mb4.
+    if (!empty($connection_options['collation'])) {
+      $pdo->exec('SET NAMES ' . $charset . ' COLLATE ' . $connection_options['collation']);
+    }
+    else {
+      $pdo->exec('SET NAMES ' . $charset);
+    }
+
+    // Set MySQL init_commands if not already defined.  Default Drupal's MySQL
+    // behavior to conform more closely to SQL standards.  This allows Drupal
+    // to run almost seamlessly on many different kinds of database systems.
+    // These settings force MySQL to behave the same as postgresql, or sqlite
+    // in regards to syntax interpretation and invalid data handling.  See
+    // https://www.drupal.org/node/344575 for further discussion. Also, as MySQL
+    // 5.5 changed the meaning of TRADITIONAL we need to spell out the modes one
+    // by one.
+    $connection_options += [
+      'init_commands' => [],
+    ];
+
+    $connection_options['init_commands'] += [
+      'sql_mode' => "SET sql_mode = 'ANSI,TRADITIONAL'",
+    ];
+
+    // Execute initial commands.
+    foreach ($connection_options['init_commands'] as $sql) {
+      $pdo->exec($sql);
+    }
+
+    return $pdo;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __destruct() {
+    if ($this->needsCleanup) {
+      $this->nextIdDelete();
+    }
+    parent::__destruct();
+  }
+
+  public function queryRange($query, $from, $count, array $args = [], array $options = []) {
+    return $this->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function queryTemporary($query, array $args = [], array $options = []) {
+    @trigger_error('Connection::queryTemporary() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. There is no replacement. See https://www.drupal.org/node/3211781', E_USER_DEPRECATED);
+    $tablename = $this->generateTemporaryTableName();
+    $this->query('CREATE TEMPORARY TABLE {' . $tablename . '} Engine=MEMORY ' . $query, $args, $options);
+    return $tablename;
+  }
+
+  public function driver() {
+    return 'mysql';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function version() {
+    if ($this->isMariaDb()) {
+      return $this->getMariaDbVersionMatch();
+    }
+
+    return $this->getServerVersion();
+  }
+
+  /**
+   * Determines whether the MySQL distribution is MariaDB or not.
+   *
+   * @return bool
+   *   Returns TRUE if the distribution is MariaDB, or FALSE if not.
+   */
+  public function isMariaDb(): bool {
+    return (bool) $this->getMariaDbVersionMatch();
+  }
+
+  /**
+   * Gets the MariaDB portion of the server version.
+   *
+   * @return string
+   *   The MariaDB portion of the server version if present, or NULL if not.
+   */
+  protected function getMariaDbVersionMatch(): ?string {
+    // MariaDB may prefix its version string with '5.5.5-', which should be
+    // ignored.
+    // @see https://github.com/MariaDB/server/blob/f6633bf058802ad7da8196d01fd19d75c53f7274/include/mysql_com.h#L42.
+    $regex = '/^(?:5\.5\.5-)?(\d+\.\d+\.\d+.*-mariadb.*)/i';
+
+    preg_match($regex, $this->getServerVersion(), $matches);
+    return (empty($matches[1])) ? NULL : $matches[1];
+  }
+
+  /**
+   * Gets the server version.
+   *
+   * @return string
+   *   The PDO server version.
+   */
+  protected function getServerVersion(): string {
+    if (!$this->serverVersion) {
+      $this->serverVersion = $this->connection->query('SELECT VERSION()')->fetchColumn();
+    }
+    return $this->serverVersion;
+  }
+
+  public function databaseType() {
+    return 'mysql';
+  }
+
+  /**
+   * Overrides \Drupal\Core\Database\Connection::createDatabase().
+   *
+   * @param string $database
+   *   The name of the database to create.
+   *
+   * @throws \Drupal\Core\Database\DatabaseNotFoundException
+   */
+  public function createDatabase($database) {
+    // Escape the database name.
+    $database = Database::getConnection()->escapeDatabase($database);
+
+    try {
+      // Create the database and set it as active.
+      $this->connection->exec("CREATE DATABASE $database");
+      $this->connection->exec("USE $database");
+    }
+    catch (\Exception $e) {
+      throw new DatabaseNotFoundException($e->getMessage());
+    }
+  }
+
+  public function mapConditionOperator($operator) {
+    // We don't want to override any of the defaults.
+    return NULL;
+  }
+
+  public function nextId($existing_id = 0) {
+    $new_id = $this->query('INSERT INTO {sequences} () VALUES ()', [], ['return' => Database::RETURN_INSERT_ID]);
+    // This should only happen after an import or similar event.
+    if ($existing_id >= $new_id) {
+      // If we INSERT a value manually into the sequences table, on the next
+      // INSERT, MySQL will generate a larger value. However, there is no way
+      // of knowing whether this value already exists in the table. MySQL
+      // provides an INSERT IGNORE which would work, but that can mask problems
+      // other than duplicate keys. Instead, we use INSERT ... ON DUPLICATE KEY
+      // UPDATE in such a way that the UPDATE does not do anything. This way,
+      // duplicate keys do not generate errors but everything else does.
+      $this->query('INSERT INTO {sequences} (value) VALUES (:value) ON DUPLICATE KEY UPDATE value = value', [':value' => $existing_id]);
+      $new_id = $this->query('INSERT INTO {sequences} () VALUES ()', [], ['return' => Database::RETURN_INSERT_ID]);
+    }
+    $this->needsCleanup = TRUE;
+    return $new_id;
+  }
+
+  public function nextIdDelete() {
+    // While we want to clean up the table to keep it up from occupying too
+    // much storage and memory, we must keep the highest value in the table
+    // because InnoDB uses an in-memory auto-increment counter as long as the
+    // server runs. When the server is stopped and restarted, InnoDB
+    // reinitializes the counter for each table for the first INSERT to the
+    // table based solely on values from the table so deleting all values would
+    // be a problem in this case. Also, TRUNCATE resets the auto increment
+    // counter.
+    try {
+      $max_id = $this->query('SELECT MAX(value) FROM {sequences}')->fetchField();
+      // We know we are using MySQL here, no need for the slower ::delete().
+      $this->query('DELETE FROM {sequences} WHERE value < :value', [':value' => $max_id]);
+    }
+    // During testing, this function is called from shutdown with the
+    // simpletest prefix stored in $this->connection, and those tables are gone
+    // by the time shutdown is called so we need to ignore the database
+    // errors. There is no problem with completely ignoring errors here: if
+    // these queries fail, the sequence will work just fine, just use a bit
+    // more database storage and memory.
+    catch (DatabaseException $e) {
+    }
+  }
+
+  /**
+   * Overridden to work around issues to MySQL not supporting transactional DDL.
+   */
+  protected function popCommittableTransactions() {
+    // Commit all the committable layers.
+    foreach (array_reverse($this->transactionLayers) as $name => $active) {
+      // Stop once we found an active transaction.
+      if ($active) {
+        break;
+      }
+
+      // If there are no more layers left then we should commit.
+      unset($this->transactionLayers[$name]);
+      if (empty($this->transactionLayers)) {
+        $this->doCommit();
+      }
+      else {
+        // Attempt to release this savepoint in the standard way.
+        try {
+          $this->query('RELEASE SAVEPOINT ' . $name);
+        }
+        catch (DatabaseExceptionWrapper $e) {
+          // However, in MySQL (InnoDB), savepoints are automatically committed
+          // when tables are altered or created (DDL transactions are not
+          // supported). This can cause exceptions due to trying to release
+          // savepoints which no longer exist.
+          //
+          // To avoid exceptions when no actual error has occurred, we silently
+          // succeed for MySQL error code 1305 ("SAVEPOINT does not exist").
+          if ($e->getPrevious()->errorInfo[1] == '1305') {
+            // If one SAVEPOINT was released automatically, then all were.
+            // Therefore, clean the transaction stack.
+            $this->transactionLayers = [];
+            // We also have to explain to PDO that the transaction stack has
+            // been cleaned-up.
+            $this->doCommit();
+          }
+          else {
+            throw $e;
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function rollBack($savepoint_name = 'drupal_transaction') {
+    // MySQL will automatically commit transactions when tables are altered or
+    // created (DDL transactions are not supported). Prevent triggering an
+    // exception to ensure that the error that has caused the rollback is
+    // properly reported.
+    if (!$this->connection->inTransaction()) {
+      // On PHP 7 $this->connection->inTransaction() will return TRUE and
+      // $this->connection->rollback() does not throw an exception; the
+      // following code is unreachable.
+
+      // If \Drupal\Core\Database\Connection::rollBack() would throw an
+      // exception then continue to throw an exception.
+      if (!$this->inTransaction()) {
+        throw new TransactionNoActiveException();
+      }
+      // A previous rollback to an earlier savepoint may mean that the savepoint
+      // in question has already been accidentally committed.
+      if (!isset($this->transactionLayers[$savepoint_name])) {
+        throw new TransactionNoActiveException();
+      }
+
+      trigger_error('Rollback attempted when there is no active transaction. This can cause data integrity issues.', E_USER_WARNING);
+      return;
+    }
+    return parent::rollBack($savepoint_name);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function doCommit() {
+    // MySQL will automatically commit transactions when tables are altered or
+    // created (DDL transactions are not supported). Prevent triggering an
+    // exception in this case as all statements have been committed.
+    if ($this->connection->inTransaction()) {
+      // On PHP 7 $this->connection->inTransaction() will return TRUE and
+      // $this->connection->commit() does not throw an exception.
+      $success = parent::doCommit();
+    }
+    else {
+      // Process the post-root (non-nested) transaction commit callbacks. The
+      // following code is copied from
+      // \Drupal\Core\Database\Connection::doCommit()
+      $success = TRUE;
+      if (!empty($this->rootTransactionEndCallbacks)) {
+        $callbacks = $this->rootTransactionEndCallbacks;
+        $this->rootTransactionEndCallbacks = [];
+        foreach ($callbacks as $callback) {
+          call_user_func($callback, $success);
+        }
+      }
+    }
+    return $success;
+  }
+
+}
+
+
+/**
+ * @} End of "addtogroup database".
+ */
diff --git a/core/modules/mysql/src/Driver/Database/mysql/ExceptionHandler.php b/core/modules/mysql/src/Driver/Database/mysql/ExceptionHandler.php
new file mode 100644
index 000000000000..18ca77ab258e
--- /dev/null
+++ b/core/modules/mysql/src/Driver/Database/mysql/ExceptionHandler.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Drupal\mysql\Driver\Database\mysql;
+
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Database\DatabaseExceptionWrapper;
+use Drupal\Core\Database\ExceptionHandler as BaseExceptionHandler;
+use Drupal\Core\Database\IntegrityConstraintViolationException;
+use Drupal\Core\Database\StatementInterface;
+
+/**
+ * MySql database exception handler class.
+ */
+class ExceptionHandler extends BaseExceptionHandler {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function handleExecutionException(\Exception $exception, StatementInterface $statement, array $arguments = [], array $options = []): void {
+    if (array_key_exists('throw_exception', $options)) {
+      @trigger_error('Passing a \'throw_exception\' option to ' . __METHOD__ . ' is deprecated in drupal:9.2.0 and is removed in drupal:10.0.0. Always catch exceptions. See https://www.drupal.org/node/3201187', E_USER_DEPRECATED);
+      if (!($options['throw_exception'])) {
+        return;
+      }
+    }
+
+    if ($exception instanceof \PDOException) {
+      // Wrap the exception in another exception, because PHP does not allow
+      // overriding Exception::getMessage(). Its message is the extra database
+      // debug information.
+      $code = is_int($exception->getCode()) ? $exception->getCode() : 0;
+
+      // If a max_allowed_packet error occurs the message length is truncated.
+      // This should prevent the error from recurring if the exception is logged
+      // to the database using dblog or the like.
+      if (($exception->errorInfo[1] ?? NULL) === 1153) {
+        $message = Unicode::truncateBytes($exception->getMessage(), Connection::MIN_MAX_ALLOWED_PACKET);
+        throw new DatabaseExceptionWrapper($message, $code, $exception);
+      }
+
+      $message = $exception->getMessage() . ": " . $statement->getQueryString() . "; " . print_r($arguments, TRUE);
+
+      // SQLSTATE 23xxx errors indicate an integrity constraint violation. Also,
+      // in case of attempted INSERT of a record with an undefined column and no
+      // default value indicated in schema, MySql returns a 1364 error code.
+      if (
+        substr($exception->getCode(), -6, -3) == '23' ||
+        ($exception->errorInfo[1] ?? NULL) === 1364
+      ) {
+        throw new IntegrityConstraintViolationException($message, $code, $exception);
+      }
+
+      throw new DatabaseExceptionWrapper($message, 0, $exception);
+    }
+
+    throw $exception;
+  }
+
+}
diff --git a/core/modules/mysql/src/Driver/Database/mysql/Insert.php b/core/modules/mysql/src/Driver/Database/mysql/Insert.php
new file mode 100644
index 000000000000..616985210398
--- /dev/null
+++ b/core/modules/mysql/src/Driver/Database/mysql/Insert.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Drupal\mysql\Driver\Database\mysql;
+
+use Drupal\Core\Database\Query\Insert as QueryInsert;
+
+/**
+ * MySQL implementation of \Drupal\Core\Database\Query\Insert.
+ */
+class Insert extends QueryInsert {
+
+  public function execute() {
+    if (!$this->preExecute()) {
+      return NULL;
+    }
+
+    // If we're selecting from a SelectQuery, finish building the query and
+    // pass it back, as any remaining options are irrelevant.
+    if (empty($this->fromQuery)) {
+      $max_placeholder = 0;
+      $values = [];
+      foreach ($this->insertValues as $insert_values) {
+        foreach ($insert_values as $value) {
+          $values[':db_insert_placeholder_' . $max_placeholder++] = $value;
+        }
+      }
+    }
+    else {
+      $values = $this->fromQuery->getArguments();
+    }
+
+    $last_insert_id = $this->connection->query((string) $this, $values, $this->queryOptions);
+
+    // Re-initialize the values array so that we can re-use this query.
+    $this->insertValues = [];
+
+    return $last_insert_id;
+  }
+
+  public function __toString() {
+    // Create a sanitized comment string to prepend to the query.
+    $comments = $this->connection->makeComment($this->comments);
+
+    // 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);
+
+    // If we're selecting from a SelectQuery, finish building the query and
+    // pass it back, as any remaining options are irrelevant.
+    if (!empty($this->fromQuery)) {
+      $insert_fields_string = $insert_fields ? ' (' . implode(', ', $insert_fields) . ') ' : ' ';
+      return $comments . 'INSERT INTO {' . $this->table . '}' . $insert_fields_string . $this->fromQuery;
+    }
+
+    $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
+
+    $values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
+    $query .= implode(', ', $values);
+
+    return $query;
+  }
+
+}
diff --git a/core/modules/mysql/src/Driver/Database/mysql/Install/Tasks.php b/core/modules/mysql/src/Driver/Database/mysql/Install/Tasks.php
new file mode 100644
index 000000000000..a6ad642425d0
--- /dev/null
+++ b/core/modules/mysql/src/Driver/Database/mysql/Install/Tasks.php
@@ -0,0 +1,214 @@
+<?php
+
+namespace Drupal\mysql\Driver\Database\mysql\Install;
+
+use Drupal\Core\Database\ConnectionNotDefinedException;
+use Drupal\Core\Database\Database;
+use Drupal\Core\Database\Install\Tasks as InstallTasks;
+use Drupal\mysql\Driver\Database\mysql\Connection;
+use Drupal\Core\Database\DatabaseNotFoundException;
+
+/**
+ * Specifies installation tasks for MySQL and equivalent databases.
+ */
+class Tasks extends InstallTasks {
+
+  /**
+   * Minimum required MySQL version.
+   *
+   * 5.7.8 is the minimum version that supports the JSON datatype.
+   * @see https://dev.mysql.com/doc/refman/5.7/en/json.html
+   */
+  const MYSQL_MINIMUM_VERSION = '5.7.8';
+
+  /**
+   * Minimum required MariaDB version.
+   *
+   * 10.3.7 is the first stable (GA) release in the 10.3 series.
+   * @see https://mariadb.com/kb/en/changes-improvements-in-mariadb-103/#list-of-all-mariadb-103-releases
+   */
+  const MARIADB_MINIMUM_VERSION = '10.3.7';
+
+  /**
+   * Minimum required MySQLnd version.
+   */
+  const MYSQLND_MINIMUM_VERSION = '5.0.9';
+
+  /**
+   * Minimum required libmysqlclient version.
+   */
+  const LIBMYSQLCLIENT_MINIMUM_VERSION = '5.5.3';
+
+  /**
+   * The PDO driver name for MySQL and equivalent databases.
+   *
+   * @var string
+   */
+  protected $pdoDriver = 'mysql';
+
+  /**
+   * Constructs a \Drupal\mysql\Driver\Database\mysql\Install\Tasks object.
+   */
+  public function __construct() {
+    $this->tasks[] = [
+      'arguments' => [],
+      'function' => 'ensureInnoDbAvailable',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function name() {
+    try {
+      if (!$this->isConnectionActive() || !$this->getConnection() instanceof Connection) {
+        throw new ConnectionNotDefinedException('The database connection is not active or not a MySql connection');
+      }
+      if ($this->getConnection()->isMariaDb()) {
+        return $this->t('MariaDB');
+      }
+      return $this->t('MySQL, Percona Server, or equivalent');
+    }
+    catch (ConnectionNotDefinedException $e) {
+      return $this->t('MySQL, MariaDB, Percona Server, or equivalent');
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function minimumVersion() {
+    if ($this->getConnection()->isMariaDb()) {
+      return static::MARIADB_MINIMUM_VERSION;
+    }
+    return static::MYSQL_MINIMUM_VERSION;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function connect() {
+    try {
+      // This doesn't actually test the connection.
+      Database::setActiveConnection();
+      // Now actually do a check.
+      try {
+        Database::getConnection();
+      }
+      catch (\Exception $e) {
+        // Detect utf8mb4 incompatibility.
+        if ($e->getCode() == Connection::UNSUPPORTED_CHARSET || ($e->getCode() == Connection::SQLSTATE_SYNTAX_ERROR && $e->errorInfo[1] == Connection::UNKNOWN_CHARSET)) {
+          $this->fail(t('Your MySQL server and PHP MySQL driver must support utf8mb4 character encoding. Make sure to use a database system that supports this (such as MySQL/MariaDB/Percona 5.5.3 and up), and that the utf8mb4 character set is compiled in. See the <a href=":documentation" target="_blank">MySQL documentation</a> for more information.', [':documentation' => 'https://dev.mysql.com/doc/refman/5.0/en/cannot-initialize-character-set.html']));
+          $info = Database::getConnectionInfo();
+          $info_copy = $info;
+          // Set a flag to fall back to utf8. Note: this flag should only be
+          // used here and is for internal use only.
+          $info_copy['default']['_dsn_utf8_fallback'] = TRUE;
+          // In order to change the Database::$databaseInfo array, we need to
+          // remove the active connection, then re-add it with the new info.
+          Database::removeConnection('default');
+          Database::addConnectionInfo('default', 'default', $info_copy['default']);
+          // Connect with the new database info, using the utf8 character set so
+          // that we can run the checkEngineVersion test.
+          Database::getConnection();
+          // Revert to the old settings.
+          Database::removeConnection('default');
+          Database::addConnectionInfo('default', 'default', $info['default']);
+        }
+        else {
+          // Rethrow the exception.
+          throw $e;
+        }
+      }
+      $this->pass('Drupal can CONNECT to the database ok.');
+    }
+    catch (\Exception $e) {
+      // Attempt to create the database if it is not found.
+      if ($e->getCode() == Connection::DATABASE_NOT_FOUND) {
+        // Remove the database string from connection info.
+        $connection_info = Database::getConnectionInfo();
+        $database = $connection_info['default']['database'];
+        unset($connection_info['default']['database']);
+
+        // In order to change the Database::$databaseInfo array, need to remove
+        // the active connection, then re-add it with the new info.
+        Database::removeConnection('default');
+        Database::addConnectionInfo('default', 'default', $connection_info['default']);
+
+        try {
+          // Now, attempt the connection again; if it's successful, attempt to
+          // create the database.
+          Database::getConnection()->createDatabase($database);
+          Database::closeConnection();
+
+          // Now, restore the database config.
+          Database::removeConnection('default');
+          $connection_info['default']['database'] = $database;
+          Database::addConnectionInfo('default', 'default', $connection_info['default']);
+
+          // Check the database connection.
+          Database::getConnection();
+          $this->pass('Drupal can CONNECT to the database ok.');
+        }
+        catch (DatabaseNotFoundException $e) {
+          // Still no dice; probably a permission issue. Raise the error to the
+          // installer.
+          $this->fail(t('Database %database not found. The server reports the following message when attempting to create the database: %error.', ['%database' => $database, '%error' => $e->getMessage()]));
+        }
+      }
+      else {
+        // Database connection failed for some other reason than a non-existent
+        // database.
+        $this->fail(t('Failed to connect to your database server. The server reports the following message: %error.<ul><li>Is the database server running?</li><li>Does the database exist or does the database user have sufficient privileges to create the database?</li><li>Have you entered the correct database name?</li><li>Have you entered the correct username and password?</li><li>Have you entered the correct database hostname and port number?</li></ul>', ['%error' => $e->getMessage()]));
+        return FALSE;
+      }
+    }
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormOptions(array $database) {
+    $form = parent::getFormOptions($database);
+    if (empty($form['advanced_options']['port']['#default_value'])) {
+      $form['advanced_options']['port']['#default_value'] = '3306';
+    }
+
+    return $form;
+  }
+
+  /**
+   * Ensure that InnoDB is available.
+   */
+  public function ensureInnoDbAvailable() {
+    $engines = Database::getConnection()->query('SHOW ENGINES')->fetchAllKeyed();
+    if (isset($engines['MyISAM']) && $engines['MyISAM'] == 'DEFAULT' && !isset($engines['InnoDB'])) {
+      $this->fail(t('The MyISAM storage engine is not supported.'));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkEngineVersion() {
+    parent::checkEngineVersion();
+
+    // Ensure that the MySQL driver supports utf8mb4 encoding.
+    $version = Database::getConnection()->clientVersion();
+    if (FALSE !== strpos($version, 'mysqlnd')) {
+      // The mysqlnd driver supports utf8mb4 starting at version 5.0.9.
+      $version = preg_replace('/^\D+([\d.]+).*/', '$1', $version);
+      if (version_compare($version, self::MYSQLND_MINIMUM_VERSION, '<')) {
+        $this->fail(t("The MySQLnd driver version %version is less than the minimum required version. Upgrade to MySQLnd version %mysqlnd_minimum_version or up, or alternatively switch mysql drivers to libmysqlclient version %libmysqlclient_minimum_version or up.", ['%version' => $version, '%mysqlnd_minimum_version' => self::MYSQLND_MINIMUM_VERSION, '%libmysqlclient_minimum_version' => self::LIBMYSQLCLIENT_MINIMUM_VERSION]));
+      }
+    }
+    else {
+      // The libmysqlclient driver supports utf8mb4 starting at version 5.5.3.
+      if (version_compare($version, self::LIBMYSQLCLIENT_MINIMUM_VERSION, '<')) {
+        $this->fail(t("The libmysqlclient driver version %version is less than the minimum required version. Upgrade to libmysqlclient version %libmysqlclient_minimum_version or up, or alternatively switch mysql drivers to MySQLnd version %mysqlnd_minimum_version or up.", ['%version' => $version, '%libmysqlclient_minimum_version' => self::LIBMYSQLCLIENT_MINIMUM_VERSION, '%mysqlnd_minimum_version' => self::MYSQLND_MINIMUM_VERSION]));
+      }
+    }
+  }
+
+}
diff --git a/core/modules/mysql/src/Driver/Database/mysql/Schema.php b/core/modules/mysql/src/Driver/Database/mysql/Schema.php
new file mode 100644
index 000000000000..2771e9763594
--- /dev/null
+++ b/core/modules/mysql/src/Driver/Database/mysql/Schema.php
@@ -0,0 +1,715 @@
+<?php
+
+namespace Drupal\mysql\Driver\Database\mysql;
+
+use Drupal\Core\Database\SchemaException;
+use Drupal\Core\Database\SchemaObjectExistsException;
+use Drupal\Core\Database\SchemaObjectDoesNotExistException;
+use Drupal\Core\Database\Schema as DatabaseSchema;
+use Drupal\Component\Utility\Unicode;
+
+/**
+ * @addtogroup schemaapi
+ * @{
+ */
+
+/**
+ * MySQL implementation of \Drupal\Core\Database\Schema.
+ */
+class Schema extends DatabaseSchema {
+
+  /**
+   * Maximum length of a table comment in MySQL.
+   */
+  const COMMENT_MAX_TABLE = 60;
+
+  /**
+   * Maximum length of a column comment in MySQL.
+   */
+  const COMMENT_MAX_COLUMN = 255;
+
+  /**
+   * @var array
+   *   List of MySQL string types.
+   */
+  protected $mysqlStringTypes = [
+    'VARCHAR',
+    'CHAR',
+    'TINYTEXT',
+    'MEDIUMTEXT',
+    'LONGTEXT',
+    'TEXT',
+  ];
+
+  /**
+   * Get information about the table and database name from the prefix.
+   *
+   * @return
+   *   A keyed array with information about the database, table name and prefix.
+   */
+  protected function getPrefixInfo($table = 'default', $add_prefix = TRUE) {
+    $info = ['prefix' => $this->connection->tablePrefix($table)];
+    if ($add_prefix) {
+      $table = $info['prefix'] . $table;
+    }
+    if (($pos = strpos($table, '.')) !== FALSE) {
+      $info['database'] = substr($table, 0, $pos);
+      $info['table'] = substr($table, ++$pos);
+    }
+    else {
+      $info['database'] = $this->connection->getConnectionOptions()['database'];
+      $info['table'] = $table;
+    }
+    return $info;
+  }
+
+  /**
+   * Build a condition to match a table name against a standard information_schema.
+   *
+   * MySQL uses databases like schemas rather than catalogs so when we build
+   * a condition to query the information_schema.tables, we set the default
+   * database as the schema unless specified otherwise, and exclude table_catalog
+   * from the condition criteria.
+   */
+  protected function buildTableNameCondition($table_name, $operator = '=', $add_prefix = TRUE) {
+    $table_info = $this->getPrefixInfo($table_name, $add_prefix);
+
+    $condition = $this->connection->condition('AND');
+    $condition->condition('table_schema', $table_info['database']);
+    $condition->condition('table_name', $table_info['table'], $operator);
+    return $condition;
+  }
+
+  /**
+   * Generate SQL to create a new table from a Drupal schema definition.
+   *
+   * @param $name
+   *   The name of the table to create.
+   * @param $table
+   *   A Schema API table definition array.
+   *
+   * @return
+   *   An array of SQL statements to create the table.
+   */
+  protected function createTableSql($name, $table) {
+    $info = $this->connection->getConnectionOptions();
+
+    // Provide defaults if needed.
+    $table += [
+      'mysql_engine' => 'InnoDB',
+      'mysql_character_set' => 'utf8mb4',
+    ];
+
+    $sql = "CREATE TABLE {" . $name . "} (\n";
+
+    // Add the SQL statement for each field.
+    foreach ($table['fields'] as $field_name => $field) {
+      $sql .= $this->createFieldSql($field_name, $this->processField($field)) . ", \n";
+    }
+
+    // Process keys & indexes.
+    if (!empty($table['primary key']) && is_array($table['primary key'])) {
+      $this->ensureNotNullPrimaryKey($table['primary key'], $table['fields']);
+    }
+    $keys = $this->createKeysSql($table);
+    if (count($keys)) {
+      $sql .= implode(", \n", $keys) . ", \n";
+    }
+
+    // Remove the last comma and space.
+    $sql = substr($sql, 0, -3) . "\n) ";
+
+    $sql .= 'ENGINE = ' . $table['mysql_engine'] . ' DEFAULT CHARACTER SET ' . $table['mysql_character_set'];
+    // By default, MySQL uses the default collation for new tables, which is
+    // 'utf8mb4_general_ci' (MySQL 5) or 'utf8mb4_0900_ai_ci' (MySQL 8) for
+    // utf8mb4. If an alternate collation has been set, it needs to be
+    // explicitly specified.
+    // @see \Drupal\mysql\Driver\Database\mysql\Schema
+    if (!empty($info['collation'])) {
+      $sql .= ' COLLATE ' . $info['collation'];
+    }
+
+    // Add table comment.
+    if (!empty($table['description'])) {
+      $sql .= ' COMMENT ' . $this->prepareComment($table['description'], self::COMMENT_MAX_TABLE);
+    }
+
+    return [$sql];
+  }
+
+  /**
+   * Create an SQL string for a field to be used in table creation or alteration.
+   *
+   * @param string $name
+   *   Name of the field.
+   * @param array $spec
+   *   The field specification, as per the schema data structure format.
+   */
+  protected function createFieldSql($name, $spec) {
+    $sql = "`" . $name . "` " . $spec['mysql_type'];
+
+    if (in_array($spec['mysql_type'], $this->mysqlStringTypes)) {
+      if (isset($spec['length'])) {
+        $sql .= '(' . $spec['length'] . ')';
+      }
+      if (isset($spec['type']) && $spec['type'] == 'varchar_ascii') {
+        $sql .= ' CHARACTER SET ascii';
+      }
+      if (!empty($spec['binary'])) {
+        $sql .= ' BINARY';
+      }
+      // Note we check for the "type" key here. "mysql_type" is VARCHAR:
+      elseif (isset($spec['type']) && $spec['type'] == 'varchar_ascii') {
+        $sql .= ' COLLATE ascii_general_ci';
+      }
+    }
+    elseif (isset($spec['precision']) && isset($spec['scale'])) {
+      $sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')';
+    }
+
+    if (!empty($spec['unsigned'])) {
+      $sql .= ' unsigned';
+    }
+
+    if (isset($spec['not null'])) {
+      if ($spec['not null']) {
+        $sql .= ' NOT NULL';
+      }
+      else {
+        $sql .= ' NULL';
+      }
+    }
+
+    if (!empty($spec['auto_increment'])) {
+      $sql .= ' auto_increment';
+    }
+
+    // $spec['default'] can be NULL, so we explicitly check for the key here.
+    if (array_key_exists('default', $spec)) {
+      $sql .= ' DEFAULT ' . $this->escapeDefaultValue($spec['default']);
+    }
+
+    if (empty($spec['not null']) && !isset($spec['default'])) {
+      $sql .= ' DEFAULT NULL';
+    }
+
+    // Add column comment.
+    if (!empty($spec['description'])) {
+      $sql .= ' COMMENT ' . $this->prepareComment($spec['description'], self::COMMENT_MAX_COLUMN);
+    }
+
+    return $sql;
+  }
+
+  /**
+   * Set database-engine specific properties for a field.
+   *
+   * @param $field
+   *   A field description array, as specified in the schema documentation.
+   */
+  protected function processField($field) {
+
+    if (!isset($field['size'])) {
+      $field['size'] = 'normal';
+    }
+
+    // Set the correct database-engine specific datatype.
+    // In case one is already provided, force it to uppercase.
+    if (isset($field['mysql_type'])) {
+      $field['mysql_type'] = mb_strtoupper($field['mysql_type']);
+    }
+    else {
+      $map = $this->getFieldTypeMap();
+      $field['mysql_type'] = $map[$field['type'] . ':' . $field['size']];
+    }
+
+    if (isset($field['type']) && $field['type'] == 'serial') {
+      $field['auto_increment'] = TRUE;
+    }
+
+    return $field;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFieldTypeMap() {
+    // Put :normal last so it gets preserved by array_flip. This makes
+    // it much easier for modules (such as schema.module) to map
+    // database types back into schema types.
+    // $map does not use drupal_static as its value never changes.
+    static $map = [
+      'varchar_ascii:normal' => 'VARCHAR',
+
+      'varchar:normal'  => 'VARCHAR',
+      'char:normal'     => 'CHAR',
+
+      'text:tiny'       => 'TINYTEXT',
+      'text:small'      => 'TINYTEXT',
+      'text:medium'     => 'MEDIUMTEXT',
+      'text:big'        => 'LONGTEXT',
+      'text:normal'     => 'TEXT',
+
+      'serial:tiny'     => 'TINYINT',
+      'serial:small'    => 'SMALLINT',
+      'serial:medium'   => 'MEDIUMINT',
+      'serial:big'      => 'BIGINT',
+      'serial:normal'   => 'INT',
+
+      'int:tiny'        => 'TINYINT',
+      'int:small'       => 'SMALLINT',
+      'int:medium'      => 'MEDIUMINT',
+      'int:big'         => 'BIGINT',
+      'int:normal'      => 'INT',
+
+      'float:tiny'      => 'FLOAT',
+      'float:small'     => 'FLOAT',
+      'float:medium'    => 'FLOAT',
+      'float:big'       => 'DOUBLE',
+      'float:normal'    => 'FLOAT',
+
+      'numeric:normal'  => 'DECIMAL',
+
+      'blob:big'        => 'LONGBLOB',
+      'blob:normal'     => 'BLOB',
+    ];
+    return $map;
+  }
+
+  protected function createKeysSql($spec) {
+    $keys = [];
+
+    if (!empty($spec['primary key'])) {
+      $keys[] = 'PRIMARY KEY (' . $this->createKeySql($spec['primary key']) . ')';
+    }
+    if (!empty($spec['unique keys'])) {
+      foreach ($spec['unique keys'] as $key => $fields) {
+        $keys[] = 'UNIQUE KEY `' . $key . '` (' . $this->createKeySql($fields) . ')';
+      }
+    }
+    if (!empty($spec['indexes'])) {
+      $indexes = $this->getNormalizedIndexes($spec);
+      foreach ($indexes as $index => $fields) {
+        $keys[] = 'INDEX `' . $index . '` (' . $this->createKeySql($fields) . ')';
+      }
+    }
+
+    return $keys;
+  }
+
+  /**
+   * Gets normalized indexes from a table specification.
+   *
+   * Shortens indexes to 191 characters if they apply to utf8mb4-encoded
+   * fields, in order to comply with the InnoDB index limitation of 756 bytes.
+   *
+   * @param array $spec
+   *   The table specification.
+   *
+   * @return array
+   *   List of shortened indexes.
+   *
+   * @throws \Drupal\Core\Database\SchemaException
+   *   Thrown if field specification is missing.
+   */
+  protected function getNormalizedIndexes(array $spec) {
+    $indexes = $spec['indexes'] ?? [];
+    foreach ($indexes as $index_name => $index_fields) {
+      foreach ($index_fields as $index_key => $index_field) {
+        // Get the name of the field from the index specification.
+        $field_name = is_array($index_field) ? $index_field[0] : $index_field;
+        // Check whether the field is defined in the table specification.
+        if (isset($spec['fields'][$field_name])) {
+          // Get the MySQL type from the processed field.
+          $mysql_field = $this->processField($spec['fields'][$field_name]);
+          if (in_array($mysql_field['mysql_type'], $this->mysqlStringTypes)) {
+            // Check whether we need to shorten the index.
+            if ((!isset($mysql_field['type']) || $mysql_field['type'] != 'varchar_ascii') && (!isset($mysql_field['length']) || $mysql_field['length'] > 191)) {
+              // Limit the index length to 191 characters.
+              $this->shortenIndex($indexes[$index_name][$index_key]);
+            }
+          }
+        }
+        else {
+          throw new SchemaException("MySQL needs the '$field_name' field specification in order to normalize the '$index_name' index");
+        }
+      }
+    }
+    return $indexes;
+  }
+
+  /**
+   * Helper function for normalizeIndexes().
+   *
+   * Shortens an index to 191 characters.
+   *
+   * @param array $index
+   *   The index array to be used in createKeySql.
+   *
+   * @see Drupal\mysql\Driver\Database\mysql\Schema::createKeySql()
+   * @see Drupal\mysql\Driver\Database\mysql\Schema::normalizeIndexes()
+   */
+  protected function shortenIndex(&$index) {
+    if (is_array($index)) {
+      if ($index[1] > 191) {
+        $index[1] = 191;
+      }
+    }
+    else {
+      $index = [$index, 191];
+    }
+  }
+
+  protected function createKeySql($fields) {
+    $return = [];
+    foreach ($fields as $field) {
+      if (is_array($field)) {
+        $return[] = '`' . $field[0] . '`(' . $field[1] . ')';
+      }
+      else {
+        $return[] = '`' . $field . '`';
+      }
+    }
+    return implode(', ', $return);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function renameTable($table, $new_name) {
+    if (!$this->tableExists($table)) {
+      throw new SchemaObjectDoesNotExistException("Cannot rename '$table' to '$new_name': table '$table' doesn't exist.");
+    }
+    if ($this->tableExists($new_name)) {
+      throw new SchemaObjectExistsException("Cannot rename '$table' to '$new_name': table '$new_name' already exists.");
+    }
+
+    $info = $this->getPrefixInfo($new_name);
+    $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO `' . $info['table'] . '`');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function dropTable($table) {
+    if (!$this->tableExists($table)) {
+      return FALSE;
+    }
+
+    $this->connection->query('DROP TABLE {' . $table . '}');
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addField($table, $field, $spec, $keys_new = []) {
+    if (!$this->tableExists($table)) {
+      throw new SchemaObjectDoesNotExistException("Cannot add field '$table.$field': table doesn't exist.");
+    }
+    if ($this->fieldExists($table, $field)) {
+      throw new SchemaObjectExistsException("Cannot add field '$table.$field': field already exists.");
+    }
+
+    // Fields that are part of a PRIMARY KEY must be added as NOT NULL.
+    $is_primary_key = isset($keys_new['primary key']) && in_array($field, $keys_new['primary key'], TRUE);
+    if ($is_primary_key) {
+      $this->ensureNotNullPrimaryKey($keys_new['primary key'], [$field => $spec]);
+    }
+
+    $fixnull = FALSE;
+    if (!empty($spec['not null']) && !isset($spec['default']) && !$is_primary_key) {
+      $fixnull = TRUE;
+      $spec['not null'] = FALSE;
+    }
+    $query = 'ALTER TABLE {' . $table . '} ADD ';
+    $query .= $this->createFieldSql($field, $this->processField($spec));
+    if ($keys_sql = $this->createKeysSql($keys_new)) {
+      // Make sure to drop the existing primary key before adding a new one.
+      // This is only needed when adding a field because this method, unlike
+      // changeField(), is supposed to handle primary keys automatically.
+      if (isset($keys_new['primary key']) && $this->indexExists($table, 'PRIMARY')) {
+        $query .= ', DROP PRIMARY KEY';
+      }
+
+      $query .= ', ADD ' . implode(', ADD ', $keys_sql);
+    }
+    $this->connection->query($query);
+    if (isset($spec['initial_from_field'])) {
+      if (isset($spec['initial'])) {
+        $expression = 'COALESCE(' . $spec['initial_from_field'] . ', :default_initial_value)';
+        $arguments = [':default_initial_value' => $spec['initial']];
+      }
+      else {
+        $expression = $spec['initial_from_field'];
+        $arguments = [];
+      }
+      $this->connection->update($table)
+        ->expression($field, $expression, $arguments)
+        ->execute();
+    }
+    elseif (isset($spec['initial'])) {
+      $this->connection->update($table)
+        ->fields([$field => $spec['initial']])
+        ->execute();
+    }
+    if ($fixnull) {
+      $spec['not null'] = TRUE;
+      $this->changeField($table, $field, $field, $spec);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function dropField($table, $field) {
+    if (!$this->fieldExists($table, $field)) {
+      return FALSE;
+    }
+
+    // When dropping a field that is part of a composite primary key MySQL
+    // automatically removes the field from the primary key, which can leave the
+    // table in an invalid state. MariaDB 10.2.8 requires explicitly dropping
+    // the primary key first for this reason. We perform this deletion
+    // explicitly which also makes the behavior on both MySQL and MariaDB
+    // consistent with PostgreSQL.
+    // @see https://mariadb.com/kb/en/library/alter-table
+    $primary_key = $this->findPrimaryKeyColumns($table);
+    if ((count($primary_key) > 1) && in_array($field, $primary_key, TRUE)) {
+      $this->dropPrimaryKey($table);
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} DROP `' . $field . '`');
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function indexExists($table, $name) {
+    // Returns one row for each column in the index. Result is string or FALSE.
+    // Details at http://dev.mysql.com/doc/refman/5.0/en/show-index.html
+    $row = $this->connection->query('SHOW INDEX FROM {' . $table . '} WHERE key_name = ' . $this->connection->quote($name))->fetchAssoc();
+    return isset($row['Key_name']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addPrimaryKey($table, $fields) {
+    if (!$this->tableExists($table)) {
+      throw new SchemaObjectDoesNotExistException("Cannot add primary key to table '$table': table doesn't exist.");
+    }
+    if ($this->indexExists($table, 'PRIMARY')) {
+      throw new SchemaObjectExistsException("Cannot add primary key to table '$table': primary key already exists.");
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} ADD PRIMARY KEY (' . $this->createKeySql($fields) . ')');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function dropPrimaryKey($table) {
+    if (!$this->indexExists($table, 'PRIMARY')) {
+      return FALSE;
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} DROP PRIMARY KEY');
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function findPrimaryKeyColumns($table) {
+    if (!$this->tableExists($table)) {
+      return FALSE;
+    }
+    $result = $this->connection->query("SHOW KEYS FROM {" . $table . "} WHERE Key_name = 'PRIMARY'")->fetchAllAssoc('Column_name');
+    return array_keys($result);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addUniqueKey($table, $name, $fields) {
+    if (!$this->tableExists($table)) {
+      throw new SchemaObjectDoesNotExistException("Cannot add unique key '$name' to table '$table': table doesn't exist.");
+    }
+    if ($this->indexExists($table, $name)) {
+      throw new SchemaObjectExistsException("Cannot add unique key '$name' to table '$table': unique key already exists.");
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} ADD UNIQUE KEY `' . $name . '` (' . $this->createKeySql($fields) . ')');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function dropUniqueKey($table, $name) {
+    if (!$this->indexExists($table, $name)) {
+      return FALSE;
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} DROP KEY `' . $name . '`');
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addIndex($table, $name, $fields, array $spec) {
+    if (!$this->tableExists($table)) {
+      throw new SchemaObjectDoesNotExistException("Cannot add index '$name' to table '$table': table doesn't exist.");
+    }
+    if ($this->indexExists($table, $name)) {
+      throw new SchemaObjectExistsException("Cannot add index '$name' to table '$table': index already exists.");
+    }
+
+    $spec['indexes'][$name] = $fields;
+    $indexes = $this->getNormalizedIndexes($spec);
+
+    $this->connection->query('ALTER TABLE {' . $table . '} ADD INDEX `' . $name . '` (' . $this->createKeySql($indexes[$name]) . ')');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function dropIndex($table, $name) {
+    if (!$this->indexExists($table, $name)) {
+      return FALSE;
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} DROP INDEX `' . $name . '`');
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function introspectIndexSchema($table) {
+    if (!$this->tableExists($table)) {
+      throw new SchemaObjectDoesNotExistException("The table $table doesn't exist.");
+    }
+
+    $index_schema = [
+      'primary key' => [],
+      'unique keys' => [],
+      'indexes' => [],
+    ];
+
+    $result = $this->connection->query('SHOW INDEX FROM {' . $table . '}')->fetchAll();
+    foreach ($result as $row) {
+      if ($row->Key_name === 'PRIMARY') {
+        $index_schema['primary key'][] = $row->Column_name;
+      }
+      elseif ($row->Non_unique == 0) {
+        $index_schema['unique keys'][$row->Key_name][] = $row->Column_name;
+      }
+      else {
+        $index_schema['indexes'][$row->Key_name][] = $row->Column_name;
+      }
+    }
+
+    return $index_schema;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function changeField($table, $field, $field_new, $spec, $keys_new = []) {
+    if (!$this->fieldExists($table, $field)) {
+      throw new SchemaObjectDoesNotExistException("Cannot change the definition of field '$table.$field': field doesn't exist.");
+    }
+    if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
+      throw new SchemaObjectExistsException("Cannot rename field '$table.$field' to '$field_new': target field already exists.");
+    }
+    if (isset($keys_new['primary key']) && in_array($field_new, $keys_new['primary key'], TRUE)) {
+      $this->ensureNotNullPrimaryKey($keys_new['primary key'], [$field_new => $spec]);
+    }
+
+    $sql = 'ALTER TABLE {' . $table . '} CHANGE `' . $field . '` ' . $this->createFieldSql($field_new, $this->processField($spec));
+    if ($keys_sql = $this->createKeysSql($keys_new)) {
+      $sql .= ', ADD ' . implode(', ADD ', $keys_sql);
+    }
+    $this->connection->query($sql);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function prepareComment($comment, $length = NULL) {
+    // Truncate comment to maximum comment length.
+    if (isset($length)) {
+      // Add table prefixes before truncating.
+      $comment = Unicode::truncate($this->connection->prefixTables($comment), $length, TRUE, TRUE);
+    }
+    // Remove semicolons to avoid triggering multi-statement check.
+    $comment = strtr($comment, [';' => '.']);
+    return $this->connection->quote($comment);
+  }
+
+  /**
+   * Retrieve a table or column comment.
+   */
+  public function getComment($table, $column = NULL) {
+    $condition = $this->buildTableNameCondition($table);
+    if (isset($column)) {
+      $condition->condition('column_name', $column);
+      $condition->compile($this->connection, $this);
+      // Don't use {} around information_schema.columns table.
+      return $this->connection->query("SELECT column_comment AS column_comment FROM information_schema.columns WHERE " . (string) $condition, $condition->arguments())->fetchField();
+    }
+    $condition->compile($this->connection, $this);
+    // Don't use {} around information_schema.tables table.
+    $comment = $this->connection->query("SELECT table_comment AS table_comment FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchField();
+    // Work-around for MySQL 5.0 bug http://bugs.mysql.com/bug.php?id=11379
+    return preg_replace('/; InnoDB free:.*$/', '', $comment);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function tableExists($table) {
+    // The information_schema table is very slow to query under MySQL 5.0.
+    // Instead, we try to select from the table in question.  If it fails,
+    // the most likely reason is that it does not exist. That is dramatically
+    // faster than using information_schema.
+    // @link http://bugs.mysql.com/bug.php?id=19588
+    // @todo This override should be removed once we require a version of MySQL
+    //   that has that bug fixed.
+    try {
+      $this->connection->queryRange("SELECT 1 FROM {" . $table . "}", 0, 1);
+      return TRUE;
+    }
+    catch (\Exception $e) {
+      return FALSE;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fieldExists($table, $column) {
+    // The information_schema table is very slow to query under MySQL 5.0.
+    // Instead, we try to select from the table and field in question. If it
+    // fails, the most likely reason is that it does not exist. That is
+    // dramatically faster than using information_schema.
+    // @link http://bugs.mysql.com/bug.php?id=19588
+    // @todo This override should be removed once we require a version of MySQL
+    //   that has that bug fixed.
+    try {
+      $this->connection->queryRange("SELECT $column FROM {" . $table . "}", 0, 1);
+      return TRUE;
+    }
+    catch (\Exception $e) {
+      return FALSE;
+    }
+  }
+
+}
+
+/**
+ * @} End of "addtogroup schemaapi".
+ */
diff --git a/core/modules/mysql/src/Driver/Database/mysql/Upsert.php b/core/modules/mysql/src/Driver/Database/mysql/Upsert.php
new file mode 100644
index 000000000000..0e5f7d3b50e9
--- /dev/null
+++ b/core/modules/mysql/src/Driver/Database/mysql/Upsert.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Drupal\mysql\Driver\Database\mysql;
+
+use Drupal\Core\Database\Query\Upsert as QueryUpsert;
+
+/**
+ * MySQL implementation of \Drupal\Core\Database\Query\Upsert.
+ */
+class Upsert extends QueryUpsert {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __toString() {
+    // Create a sanitized comment string to prepend to the query.
+    $comments = $this->connection->makeComment($this->comments);
+
+    // 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);
+
+    $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
+
+    $values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
+    $query .= implode(', ', $values);
+
+    // Updating the unique / primary key is not necessary.
+    unset($insert_fields[$this->key]);
+
+    $update = [];
+    foreach ($insert_fields as $field) {
+      $update[] = "$field = VALUES($field)";
+    }
+
+    $query .= ' ON DUPLICATE KEY UPDATE ' . implode(', ', $update);
+
+    return $query;
+  }
+
+}
diff --git a/core/modules/pgsql/pgsql.info.yml b/core/modules/pgsql/pgsql.info.yml
new file mode 100644
index 000000000000..93ad5d8e1fa0
--- /dev/null
+++ b/core/modules/pgsql/pgsql.info.yml
@@ -0,0 +1,5 @@
+name: PostgreSQL
+type: module
+description: 'Database driver for PostgreSQL.'
+package: Core
+version: VERSION
diff --git a/core/modules/pgsql/pgsql.module b/core/modules/pgsql/pgsql.module
new file mode 100644
index 000000000000..4d9027bc432f
--- /dev/null
+++ b/core/modules/pgsql/pgsql.module
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * @file
+ * The PostgreSQL module provides the connection between Drupal and a PostgreSQL database.
+ */
+
+use Drupal\Core\Routing\RouteMatchInterface;
+
+/**
+ * Implements hook_help().
+ */
+function pgsql_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    case 'help.page.pgsql':
+      $output = '';
+      $output .= '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('The PostgreSQL module provides the connection between Drupal and a PostgreSQL database. For more information, see the <a href=":pgsql">online documentation for the PostgreSQL module</a>.', [':pgsql' => 'https://www.drupal.org/documentation/modules/pgsql']) . '</p>';
+      return $output;
+
+  }
+}
diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Connection.php b/core/modules/pgsql/src/Driver/Database/pgsql/Connection.php
new file mode 100644
index 000000000000..fc2ab6508f8b
--- /dev/null
+++ b/core/modules/pgsql/src/Driver/Database/pgsql/Connection.php
@@ -0,0 +1,375 @@
+<?php
+
+namespace Drupal\pgsql\Driver\Database\pgsql;
+
+use Drupal\Core\Database\Database;
+use Drupal\Core\Database\Connection as DatabaseConnection;
+use Drupal\Core\Database\DatabaseAccessDeniedException;
+use Drupal\Core\Database\DatabaseNotFoundException;
+use Drupal\Core\Database\StatementInterface;
+use Drupal\Core\Database\StatementWrapper;
+
+// cSpell:ignore ilike nextval
+
+/**
+ * @addtogroup database
+ * @{
+ */
+
+/**
+ * PostgreSQL implementation of \Drupal\Core\Database\Connection.
+ */
+class Connection extends DatabaseConnection {
+
+  /**
+   * The name by which to obtain a lock for retrieve the next insert id.
+   */
+  const POSTGRESQL_NEXTID_LOCK = 1000;
+
+  /**
+   * Error code for "Unknown database" error.
+   */
+  const DATABASE_NOT_FOUND = 7;
+
+  /**
+   * Error code for "Connection failure" errors.
+   *
+   * Technically this is an internal error code that will only be shown in the
+   * PDOException message. It will need to get extracted.
+   */
+  const CONNECTION_FAILURE = '08006';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $statementClass = NULL;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $statementWrapperClass = StatementWrapper::class;
+
+  /**
+   * A map of condition operators to PostgreSQL operators.
+   *
+   * In PostgreSQL, 'LIKE' is case-sensitive. ILIKE should be used for
+   * case-insensitive statements.
+   */
+  protected static $postgresqlConditionOperatorMap = [
+    'LIKE' => ['operator' => 'ILIKE'],
+    'LIKE BINARY' => ['operator' => 'LIKE'],
+    'NOT LIKE' => ['operator' => 'NOT ILIKE'],
+    'REGEXP' => ['operator' => '~*'],
+    'NOT REGEXP' => ['operator' => '!~*'],
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $transactionalDDLSupport = TRUE;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $identifierQuotes = ['"', '"'];
+
+  /**
+   * Constructs a connection object.
+   */
+  public function __construct(\PDO $connection, array $connection_options) {
+    parent::__construct($connection, $connection_options);
+
+    // Force PostgreSQL to use the UTF-8 character set by default.
+    $this->connection->exec("SET NAMES 'UTF8'");
+
+    // Execute PostgreSQL init_commands.
+    if (isset($connection_options['init_commands'])) {
+      $this->connection->exec(implode('; ', $connection_options['init_commands']));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function open(array &$connection_options = []) {
+    // Default to TCP connection on port 5432.
+    if (empty($connection_options['port'])) {
+      $connection_options['port'] = 5432;
+    }
+
+    // PostgreSQL in trust mode doesn't require a password to be supplied.
+    if (empty($connection_options['password'])) {
+      $connection_options['password'] = NULL;
+    }
+    // If the password contains a backslash it is treated as an escape character
+    // http://bugs.php.net/bug.php?id=53217
+    // so backslashes in the password need to be doubled up.
+    // The bug was reported against pdo_pgsql 1.0.2, backslashes in passwords
+    // will break on this doubling up when the bug is fixed, so check the version
+    // elseif (phpversion('pdo_pgsql') < 'version_this_was_fixed_in') {
+    else {
+      $connection_options['password'] = str_replace('\\', '\\\\', $connection_options['password']);
+    }
+
+    $connection_options['database'] = (!empty($connection_options['database']) ? $connection_options['database'] : 'template1');
+    $dsn = 'pgsql:host=' . $connection_options['host'] . ' dbname=' . $connection_options['database'] . ' port=' . $connection_options['port'];
+
+    // Allow PDO options to be overridden.
+    $connection_options += [
+      'pdo' => [],
+    ];
+    $connection_options['pdo'] += [
+      \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
+      // Prepared statements are most effective for performance when queries
+      // are recycled (used several times). However, if they are not re-used,
+      // prepared statements become inefficient. Since most of Drupal's
+      // prepared queries are not re-used, it should be faster to emulate
+      // the preparation than to actually ready statements for re-use. If in
+      // doubt, reset to FALSE and measure performance.
+      \PDO::ATTR_EMULATE_PREPARES => TRUE,
+      // Convert numeric values to strings when fetching.
+      \PDO::ATTR_STRINGIFY_FETCHES => TRUE,
+    ];
+
+    try {
+      $pdo = new \PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
+    }
+    catch (\PDOException $e) {
+      if (static::getSQLState($e) == static::CONNECTION_FAILURE) {
+        if (strpos($e->getMessage(), 'password authentication failed for user') !== FALSE) {
+          throw new DatabaseAccessDeniedException($e->getMessage(), $e->getCode(), $e);
+        }
+        elseif (strpos($e->getMessage(), 'database') !== FALSE && strpos($e->getMessage(), 'does not exist') !== FALSE) {
+          throw new DatabaseNotFoundException($e->getMessage(), $e->getCode(), $e);
+        }
+      }
+      throw $e;
+    }
+
+    return $pdo;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function query($query, array $args = [], $options = []) {
+    $options += $this->defaultOptions();
+
+    // The PDO PostgreSQL driver has a bug which doesn't type cast booleans
+    // correctly when parameters are bound using associative arrays.
+    // @see http://bugs.php.net/bug.php?id=48383
+    foreach ($args as &$value) {
+      if (is_bool($value)) {
+        $value = (int) $value;
+      }
+    }
+
+    // We need to wrap queries with a savepoint if:
+    // - Currently in a transaction.
+    // - A 'mimic_implicit_commit' does not exist already.
+    // - The query is not a savepoint query.
+    $wrap_with_savepoint = $this->inTransaction() &&
+      !isset($this->transactionLayers['mimic_implicit_commit']) &&
+      !(is_string($query) && (
+        stripos($query, 'ROLLBACK TO SAVEPOINT ') === 0 ||
+        stripos($query, 'RELEASE SAVEPOINT ') === 0 ||
+        stripos($query, 'SAVEPOINT ') === 0
+      )
+    );
+    if ($wrap_with_savepoint) {
+      // Create a savepoint so we can rollback a failed query. This is so we can
+      // mimic MySQL and SQLite transactions which don't fail if a single query
+      // fails. This is important for tables that are created on demand. For
+      // example, \Drupal\Core\Cache\DatabaseBackend.
+      $this->addSavepoint();
+      try {
+        $return = parent::query($query, $args, $options);
+        $this->releaseSavepoint();
+      }
+      catch (\Exception $e) {
+        $this->rollbackSavepoint();
+        throw $e;
+      }
+    }
+    else {
+      $return = parent::query($query, $args, $options);
+    }
+
+    return $return;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function prepareStatement(string $query, array $options, bool $allow_row_count = FALSE): StatementInterface {
+    // mapConditionOperator converts some operations (LIKE, REGEXP, etc.) to
+    // PostgreSQL equivalents (ILIKE, ~*, etc.). However PostgreSQL doesn't
+    // automatically cast the fields to the right type for these operators,
+    // so we need to alter the query and add the type-cast.
+    $query = preg_replace('/ ([^ ]+) +(I*LIKE|NOT +I*LIKE|~\*|!~\*) /i', ' ${1}::text ${2} ', $query);
+    return parent::prepareStatement($query, $options, $allow_row_count);
+  }
+
+  public function queryRange($query, $from, $count, array $args = [], array $options = []) {
+    return $this->query($query . ' LIMIT ' . (int) $count . ' OFFSET ' . (int) $from, $args, $options);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function queryTemporary($query, array $args = [], array $options = []) {
+    @trigger_error('Connection::queryTemporary() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. There is no replacement. See https://www.drupal.org/node/3211781', E_USER_DEPRECATED);
+    $tablename = $this->generateTemporaryTableName();
+    $this->query('CREATE TEMPORARY TABLE {' . $tablename . '} AS ' . $query, $args, $options);
+    return $tablename;
+  }
+
+  public function driver() {
+    return 'pgsql';
+  }
+
+  public function databaseType() {
+    return 'pgsql';
+  }
+
+  /**
+   * Overrides \Drupal\Core\Database\Connection::createDatabase().
+   *
+   * @param string $database
+   *   The name of the database to create.
+   *
+   * @throws \Drupal\Core\Database\DatabaseNotFoundException
+   */
+  public function createDatabase($database) {
+    // Escape the database name.
+    $database = Database::getConnection()->escapeDatabase($database);
+
+    // If the PECL intl extension is installed, use it to determine the proper
+    // locale.  Otherwise, fall back to en_US.
+    if (class_exists('Locale')) {
+      $locale = \Locale::getDefault();
+    }
+    else {
+      $locale = 'en_US';
+    }
+
+    try {
+      // Create the database and set it as active.
+      $this->connection->exec("CREATE DATABASE $database WITH TEMPLATE template0 ENCODING='utf8' LC_CTYPE='$locale.utf8' LC_COLLATE='$locale.utf8'");
+    }
+    catch (\Exception $e) {
+      throw new DatabaseNotFoundException($e->getMessage());
+    }
+  }
+
+  public function mapConditionOperator($operator) {
+    return static::$postgresqlConditionOperatorMap[$operator] ?? NULL;
+  }
+
+  /**
+   * Retrieve a the next id in a sequence.
+   *
+   * PostgreSQL has built in sequences. We'll use these instead of inserting
+   * and updating a sequences table.
+   */
+  public function nextId($existing = 0) {
+
+    // Retrieve the name of the sequence. This information cannot be cached
+    // because the prefix may change, for example, like it does in tests.
+    $sequence_name = $this->makeSequenceName('sequences', 'value');
+
+    // When PostgreSQL gets a value too small then it will lock the table,
+    // retry the INSERT and if it's still too small then alter the sequence.
+    $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
+    if ($id > $existing) {
+      return $id;
+    }
+
+    // PostgreSQL advisory locks are simply locks to be used by an
+    // application such as Drupal. This will prevent other Drupal processes
+    // from altering the sequence while we are.
+    $this->query("SELECT pg_advisory_lock(" . self::POSTGRESQL_NEXTID_LOCK . ")");
+
+    // While waiting to obtain the lock, the sequence may have been altered
+    // so lets try again to obtain an adequate value.
+    $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
+    if ($id > $existing) {
+      $this->query("SELECT pg_advisory_unlock(" . self::POSTGRESQL_NEXTID_LOCK . ")");
+      return $id;
+    }
+
+    // Reset the sequence to a higher value than the existing id.
+    $this->query("ALTER SEQUENCE " . $sequence_name . " RESTART WITH " . ($existing + 1));
+
+    // Retrieve the next id. We know this will be as high as we want it.
+    $id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
+
+    $this->query("SELECT pg_advisory_unlock(" . self::POSTGRESQL_NEXTID_LOCK . ")");
+
+    return $id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFullQualifiedTableName($table) {
+    $options = $this->getConnectionOptions();
+    $prefix = $this->tablePrefix($table);
+
+    // The fully qualified table name in PostgreSQL is in the form of
+    // <database>.<schema>.<table>, so we have to include the 'public' schema in
+    // the return value.
+    return $options['database'] . '.public.' . $prefix . $table;
+  }
+
+  /**
+   * Add a new savepoint with a unique name.
+   *
+   * The main use for this method is to mimic InnoDB functionality, which
+   * provides an inherent savepoint before any query in a transaction.
+   *
+   * @param $savepoint_name
+   *   A string representing the savepoint name. By default,
+   *   "mimic_implicit_commit" is used.
+   *
+   * @see Drupal\Core\Database\Connection::pushTransaction()
+   */
+  public function addSavepoint($savepoint_name = 'mimic_implicit_commit') {
+    if ($this->inTransaction()) {
+      $this->pushTransaction($savepoint_name);
+    }
+  }
+
+  /**
+   * Release a savepoint by name.
+   *
+   * @param $savepoint_name
+   *   A string representing the savepoint name. By default,
+   *   "mimic_implicit_commit" is used.
+   *
+   * @see Drupal\Core\Database\Connection::popTransaction()
+   */
+  public function releaseSavepoint($savepoint_name = 'mimic_implicit_commit') {
+    if (isset($this->transactionLayers[$savepoint_name])) {
+      $this->popTransaction($savepoint_name);
+    }
+  }
+
+  /**
+   * Rollback a savepoint by name if it exists.
+   *
+   * @param $savepoint_name
+   *   A string representing the savepoint name. By default,
+   *   "mimic_implicit_commit" is used.
+   */
+  public function rollbackSavepoint($savepoint_name = 'mimic_implicit_commit') {
+    if (isset($this->transactionLayers[$savepoint_name])) {
+      $this->rollBack($savepoint_name);
+    }
+  }
+
+}
+
+/**
+ * @} End of "addtogroup database".
+ */
diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Delete.php b/core/modules/pgsql/src/Driver/Database/pgsql/Delete.php
new file mode 100644
index 000000000000..9585a9c4bc52
--- /dev/null
+++ b/core/modules/pgsql/src/Driver/Database/pgsql/Delete.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Drupal\pgsql\Driver\Database\pgsql;
+
+use Drupal\Core\Database\Query\Delete as QueryDelete;
+
+/**
+ * PostgreSQL implementation of \Drupal\Core\Database\Query\Delete.
+ */
+class Delete extends QueryDelete {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute() {
+    $this->connection->addSavepoint();
+    try {
+      $result = parent::execute();
+    }
+    catch (\Exception $e) {
+      $this->connection->rollbackSavepoint();
+      throw $e;
+    }
+    $this->connection->releaseSavepoint();
+
+    return $result;
+  }
+
+}
diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Insert.php b/core/modules/pgsql/src/Driver/Database/pgsql/Insert.php
new file mode 100644
index 000000000000..1b53274729d8
--- /dev/null
+++ b/core/modules/pgsql/src/Driver/Database/pgsql/Insert.php
@@ -0,0 +1,158 @@
+<?php
+
+namespace Drupal\pgsql\Driver\Database\pgsql;
+
+use Drupal\Core\Database\DatabaseExceptionWrapper;
+use Drupal\Core\Database\IntegrityConstraintViolationException;
+use Drupal\Core\Database\Query\Insert as QueryInsert;
+
+// cSpell:ignore nextval setval
+
+/**
+ * @ingroup database
+ * @{
+ */
+
+/**
+ * PostgreSQL implementation of \Drupal\Core\Database\Query\Insert.
+ */
+class Insert extends QueryInsert {
+
+  public function execute() {
+    if (!$this->preExecute()) {
+      return NULL;
+    }
+
+    $stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions);
+
+    // Fetch the list of blobs and sequences used on that table.
+    $table_information = $this->connection->schema()->queryTableInformation($this->table);
+
+    $max_placeholder = 0;
+    $blobs = [];
+    $blob_count = 0;
+    foreach ($this->insertValues as $insert_values) {
+      foreach ($this->insertFields as $idx => $field) {
+        if (isset($table_information->blob_fields[$field]) && $insert_values[$idx] !== NULL) {
+          $blobs[$blob_count] = fopen('php://memory', 'a');
+          fwrite($blobs[$blob_count], $insert_values[$idx]);
+          rewind($blobs[$blob_count]);
+
+          $stmt->getClientStatement()->bindParam(':db_insert_placeholder_' . $max_placeholder++, $blobs[$blob_count], \PDO::PARAM_LOB);
+
+          // Pre-increment is faster in PHP than increment.
+          ++$blob_count;
+        }
+        else {
+          $stmt->getClientStatement()->bindParam(':db_insert_placeholder_' . $max_placeholder++, $insert_values[$idx]);
+        }
+      }
+      // Check if values for a serial field has been passed.
+      if (!empty($table_information->serial_fields)) {
+        foreach ($table_information->serial_fields as $index => $serial_field) {
+          $serial_key = array_search($serial_field, $this->insertFields);
+          if ($serial_key !== FALSE) {
+            $serial_value = $insert_values[$serial_key];
+
+            // Sequences must be greater than or equal to 1.
+            if ($serial_value === NULL || !$serial_value) {
+              $serial_value = 1;
+            }
+            // Set the sequence to the bigger value of either the passed
+            // value or the max value of the column. It can happen that another
+            // thread calls nextval() which could lead to a serial number being
+            // used twice. However, trying to insert a value into a serial
+            // column should only be done in very rare cases and is not thread
+            // safe by definition.
+            $this->connection->query("SELECT setval('" . $table_information->sequences[$index] . "', GREATEST(MAX(" . $serial_field . "), :serial_value)) FROM {" . $this->table . "}", [':serial_value' => (int) $serial_value]);
+          }
+        }
+      }
+    }
+    if (!empty($this->fromQuery)) {
+      // bindParam stores only a reference to the variable that is followed when
+      // the statement is executed. We pass $arguments[$key] instead of $value
+      // because the second argument to bindParam is passed by reference and
+      // the foreach statement assigns the element to the existing reference.
+      $arguments = $this->fromQuery->getArguments();
+      foreach ($arguments as $key => $value) {
+        $stmt->getClientStatement()->bindParam($key, $arguments[$key]);
+      }
+    }
+
+    // Create a savepoint so we can rollback a failed query. This is so we can
+    // mimic MySQL and SQLite transactions which don't fail if a single query
+    // fails. This is important for tables that are created on demand. For
+    // example, \Drupal\Core\Cache\DatabaseBackend.
+    $this->connection->addSavepoint();
+    try {
+      $stmt->execute(NULL, $this->queryOptions);
+      if (isset($table_information->serial_fields[0])) {
+        $last_insert_id = $stmt->fetchField();
+      }
+      $this->connection->releaseSavepoint();
+    }
+    catch (\PDOException $e) {
+      $this->connection->rollbackSavepoint();
+      $message = $e->getMessage() . ": " . $stmt->getQueryString();
+      // Match all SQLSTATE 23xxx errors.
+      if (substr($e->getCode(), -6, -3) == '23') {
+        throw new IntegrityConstraintViolationException($message, $e->getCode(), $e);
+      }
+      else {
+        throw new DatabaseExceptionWrapper($message, 0, $e->getCode());
+      }
+    }
+    catch (\Exception $e) {
+      $this->connection->rollbackSavepoint();
+      throw $e;
+    }
+
+    // Re-initialize the values array so that we can re-use this query.
+    $this->insertValues = [];
+
+    return $last_insert_id ?? NULL;
+  }
+
+  public function __toString() {
+    // Create a sanitized comment string to prepend to the query.
+    $comments = $this->connection->makeComment($this->comments);
+
+    // Default fields are always placed first for consistency.
+    $insert_fields = array_merge($this->defaultFields, $this->insertFields);
+
+    $insert_fields = array_map(function ($f) {
+      return $this->connection->escapeField($f);
+    }, $insert_fields);
+
+    // If we're selecting from a SelectQuery, finish building the query and
+    // pass it back, as any remaining options are irrelevant.
+    if (!empty($this->fromQuery)) {
+      $insert_fields_string = $insert_fields ? ' (' . implode(', ', $insert_fields) . ') ' : ' ';
+      $query = $comments . 'INSERT INTO {' . $this->table . '}' . $insert_fields_string . $this->fromQuery;
+    }
+    else {
+      $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
+
+      $values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
+      $query .= implode(', ', $values);
+    }
+    try {
+      // Fetch the list of blobs and sequences used on that table.
+      $table_information = $this->connection->schema()->queryTableInformation($this->table);
+      if (isset($table_information->serial_fields[0])) {
+        // Use RETURNING syntax to get the last insert ID in the same INSERT
+        // query, see https://www.postgresql.org/docs/10/dml-returning.html.
+        $query .= ' RETURNING ' . $table_information->serial_fields[0];
+      }
+    }
+    catch (DatabaseExceptionWrapper $e) {
+      // If we fail to get the table information it is probably because the
+      // table does not exist yet so adding the returning statement is pointless
+      // because the query will fail. This happens for tables created on demand,
+      // for example, cache tables.
+    }
+    return $query;
+  }
+
+}
diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Install/Tasks.php b/core/modules/pgsql/src/Driver/Database/pgsql/Install/Tasks.php
new file mode 100644
index 000000000000..a58a0fa62eec
--- /dev/null
+++ b/core/modules/pgsql/src/Driver/Database/pgsql/Install/Tasks.php
@@ -0,0 +1,294 @@
+<?php
+
+namespace Drupal\pgsql\Driver\Database\pgsql\Install;
+
+use Drupal\Core\Database\Database;
+use Drupal\Core\Database\Install\Tasks as InstallTasks;
+use Drupal\Core\Database\DatabaseNotFoundException;
+
+/**
+ * Specifies installation tasks for PostgreSQL databases.
+ */
+class Tasks extends InstallTasks {
+
+  /**
+   * Minimum required PostgreSQL version.
+   *
+   * The contrib extension pg_trgm is supposed to be installed.
+   *
+   * @see https://www.postgresql.org/docs/10/pgtrgm.html
+   */
+  const PGSQL_MINIMUM_VERSION = '10';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $pdoDriver = 'pgsql';
+
+  /**
+   * Constructs a \Drupal\pgsql\Driver\Database\pgsql\Install\Tasks object.
+   */
+  public function __construct() {
+    $this->tasks[] = [
+      'function' => 'checkEncoding',
+      'arguments' => [],
+    ];
+    $this->tasks[] = [
+      'function' => 'checkBinaryOutput',
+      'arguments' => [],
+    ];
+    $this->tasks[] = [
+      'function' => 'checkStandardConformingStrings',
+      'arguments' => [],
+    ];
+    $this->tasks[] = [
+      'function' => 'initializeDatabase',
+      'arguments' => [],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function name() {
+    return t('PostgreSQL');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function minimumVersion() {
+    return static::PGSQL_MINIMUM_VERSION;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function connect() {
+    try {
+      // This doesn't actually test the connection.
+      Database::setActiveConnection();
+      // Now actually do a check.
+      Database::getConnection();
+      $this->pass('Drupal can CONNECT to the database ok.');
+    }
+    catch (\Exception $e) {
+      // Attempt to create the database if it is not found.
+      if ($e instanceof DatabaseNotFoundException) {
+        // Remove the database string from connection info.
+        $connection_info = Database::getConnectionInfo();
+        $database = $connection_info['default']['database'];
+        unset($connection_info['default']['database']);
+
+        // In order to change the Database::$databaseInfo array, need to remove
+        // the active connection, then re-add it with the new info.
+        Database::removeConnection('default');
+        Database::addConnectionInfo('default', 'default', $connection_info['default']);
+
+        try {
+          // Now, attempt the connection again; if it's successful, attempt to
+          // create the database.
+          Database::getConnection()->createDatabase($database);
+          Database::closeConnection();
+
+          // Now, restore the database config.
+          Database::removeConnection('default');
+          $connection_info['default']['database'] = $database;
+          Database::addConnectionInfo('default', 'default', $connection_info['default']);
+
+          // Check the database connection.
+          Database::getConnection();
+          $this->pass('Drupal can CONNECT to the database ok.');
+        }
+        catch (DatabaseNotFoundException $e) {
+          // Still no dice; probably a permission issue. Raise the error to the
+          // installer.
+          $this->fail(t('Database %database not found. The server reports the following message when attempting to create the database: %error.', ['%database' => $database, '%error' => $e->getMessage()]));
+        }
+      }
+      else {
+        // Database connection failed for some other reason than a non-existent
+        // database.
+        $this->fail(t('Failed to connect to your database server. The server reports the following message: %error.<ul><li>Is the database server running?</li><li>Does the database exist, and have you entered the correct database name?</li><li>Have you entered the correct username and password?</li><li>Have you entered the correct database hostname and port number?</li></ul>', ['%error' => $e->getMessage()]));
+        return FALSE;
+      }
+    }
+    return TRUE;
+  }
+
+  /**
+   * Check encoding is UTF8.
+   */
+  protected function checkEncoding() {
+    try {
+      if (Database::getConnection()->query('SHOW server_encoding')->fetchField() == 'UTF8') {
+        $this->pass(t('Database is encoded in UTF-8'));
+      }
+      else {
+        $this->fail(t('The %driver database must use %encoding encoding to work with Drupal. Recreate the database with %encoding encoding. See <a href="INSTALL.pgsql.txt">INSTALL.pgsql.txt</a> for more details.', [
+          '%encoding' => 'UTF8',
+          '%driver' => $this->name(),
+        ]));
+      }
+    }
+    catch (\Exception $e) {
+      $this->fail(t('Drupal could not determine the encoding of the database was set to UTF-8'));
+    }
+  }
+
+  /**
+   * Check Binary Output.
+   *
+   * Unserializing does not work on Postgresql 9 when bytea_output is 'hex'.
+   */
+  public function checkBinaryOutput() {
+    $database_connection = Database::getConnection();
+    if (!$this->checkBinaryOutputSuccess()) {
+      // First try to alter the database. If it fails, raise an error telling
+      // the user to do it themselves.
+      $connection_options = $database_connection->getConnectionOptions();
+      // It is safe to include the database name directly here, because this
+      // code is only called when a connection to the database is already
+      // established, thus the database name is guaranteed to be a correct
+      // value.
+      $query = "ALTER DATABASE \"{$connection_options['database']}\" SET bytea_output = 'escape';";
+      try {
+        $database_connection->query($query);
+      }
+      catch (\Exception $e) {
+        // Ignore possible errors when the user doesn't have the necessary
+        // privileges to ALTER the database.
+      }
+
+      // Close the database connection so that the configuration parameter
+      // is applied to the current connection.
+      Database::closeConnection();
+
+      // Recheck, if it fails, finally just rely on the end user to do the
+      // right thing.
+      if (!$this->checkBinaryOutputSuccess()) {
+        $replacements = [
+          '%setting' => 'bytea_output',
+          '%current_value' => 'hex',
+          '%needed_value' => 'escape',
+          '@query' => $query,
+        ];
+        $this->fail(t("The %setting setting is currently set to '%current_value', but needs to be '%needed_value'. Change this by running the following query: <code>@query</code>", $replacements));
+      }
+    }
+  }
+
+  /**
+   * Verify that a binary data roundtrip returns the original string.
+   */
+  protected function checkBinaryOutputSuccess() {
+    $bytea_output = Database::getConnection()->query("SHOW bytea_output")->fetchField();
+    return ($bytea_output == 'escape');
+  }
+
+  /**
+   * Ensures standard_conforming_strings setting is 'on'.
+   *
+   * When standard_conforming_strings setting is 'on' string literals ('...')
+   * treat backslashes literally, as specified in the SQL standard. This allows
+   * Drupal to convert between bytea, text and varchar columns.
+   */
+  public function checkStandardConformingStrings() {
+    $database_connection = Database::getConnection();
+    if (!$this->checkStandardConformingStringsSuccess()) {
+      // First try to alter the database. If it fails, raise an error telling
+      // the user to do it themselves.
+      $connection_options = $database_connection->getConnectionOptions();
+      // It is safe to include the database name directly here, because this
+      // code is only called when a connection to the database is already
+      // established, thus the database name is guaranteed to be a correct
+      // value.
+      $query = "ALTER DATABASE \"" . $connection_options['database'] . "\" SET standard_conforming_strings = 'on';";
+      try {
+        $database_connection->query($query);
+      }
+      catch (\Exception $e) {
+        // Ignore possible errors when the user doesn't have the necessary
+        // privileges to ALTER the database.
+      }
+
+      // Close the database connection so that the configuration parameter
+      // is applied to the current connection.
+      Database::closeConnection();
+
+      // Recheck, if it fails, finally just rely on the end user to do the
+      // right thing.
+      if (!$this->checkStandardConformingStringsSuccess()) {
+        $replacements = [
+          '%setting' => 'standard_conforming_strings',
+          '%current_value' => 'off',
+          '%needed_value' => 'on',
+          '@query' => $query,
+        ];
+        $this->fail(t("The %setting setting is currently set to '%current_value', but needs to be '%needed_value'. Change this by running the following query: <code>@query</code>", $replacements));
+      }
+    }
+  }
+
+  /**
+   * Verifies the standard_conforming_strings setting.
+   */
+  protected function checkStandardConformingStringsSuccess() {
+    $standard_conforming_strings = Database::getConnection()->query("SHOW standard_conforming_strings")->fetchField();
+    return ($standard_conforming_strings == 'on');
+  }
+
+  /**
+   * Make PostgreSQL Drupal friendly.
+   */
+  public function initializeDatabase() {
+    // We create some functions using global names instead of prefixing them
+    // like we do with table names. This is so that we don't double up if more
+    // than one instance of Drupal is running on a single database. We therefore
+    // avoid trying to create them again in that case.
+    // At the same time checking for the existence of the function fixes
+    // concurrency issues, when both try to update at the same time.
+    try {
+      $connection = Database::getConnection();
+      // When testing, two installs might try to run the CREATE FUNCTION queries
+      // at the same time. Do not let that happen.
+      $connection->query('SELECT pg_advisory_lock(1)');
+      // Don't use {} around pg_proc table.
+      if (!$connection->query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'rand'")->fetchField()) {
+        $connection->query('CREATE OR REPLACE FUNCTION "rand"() RETURNS float AS
+          \'SELECT random();\'
+          LANGUAGE \'sql\'',
+          [],
+          ['allow_delimiter_in_query' => TRUE]
+        );
+      }
+
+      if (!$connection->query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'substring_index'")->fetchField()) {
+        $connection->query('CREATE OR REPLACE FUNCTION "substring_index"(text, text, integer) RETURNS text AS
+          \'SELECT array_to_string((string_to_array($1, $2)) [1:$3], $2);\'
+          LANGUAGE \'sql\'',
+          [],
+          ['allow_delimiter_in_query' => TRUE, 'allow_square_brackets' => TRUE]
+        );
+      }
+      $connection->query('SELECT pg_advisory_unlock(1)');
+
+      $this->pass(t('PostgreSQL has initialized itself.'));
+    }
+    catch (\Exception $e) {
+      $this->fail(t('Drupal could not be correctly setup with the existing database due to the following error: @error.', ['@error' => $e->getMessage()]));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormOptions(array $database) {
+    $form = parent::getFormOptions($database);
+    if (empty($form['advanced_options']['port']['#default_value'])) {
+      $form['advanced_options']['port']['#default_value'] = '5432';
+    }
+    return $form;
+  }
+
+}
diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Schema.php b/core/modules/pgsql/src/Driver/Database/pgsql/Schema.php
new file mode 100644
index 000000000000..ecf514018c9c
--- /dev/null
+++ b/core/modules/pgsql/src/Driver/Database/pgsql/Schema.php
@@ -0,0 +1,1084 @@
+<?php
+
+namespace Drupal\pgsql\Driver\Database\pgsql;
+
+use Drupal\Core\Database\SchemaObjectExistsException;
+use Drupal\Core\Database\SchemaObjectDoesNotExistException;
+use Drupal\Core\Database\Schema as DatabaseSchema;
+
+// cSpell:ignore adbin adnum adrelid adsrc attisdropped attname attnum attrdef
+// cSpell:ignore attrelid atttypid atttypmod bigserial conkey conname conrelid
+// cSpell:ignore contype fillfactor indexname indexrelid indisprimary indkey
+// cSpell:ignore indrelid nextval nspname regclass relkind relname relnamespace
+// cSpell:ignore schemaname setval
+
+/**
+ * @addtogroup schemaapi
+ * @{
+ */
+
+/**
+ * PostgreSQL implementation of \Drupal\Core\Database\Schema.
+ */
+class Schema extends DatabaseSchema {
+
+  /**
+   * A cache of information about blob columns and sequences of tables.
+   *
+   * This is collected by Schema::queryTableInformation(), by introspecting the
+   * database.
+   *
+   * @see \Drupal\pgsql\Driver\Database\pgsql\Schema::queryTableInformation()
+   * @var array
+   */
+  protected $tableInformation = [];
+
+  /**
+   * The maximum allowed length for index, primary key and constraint names.
+   *
+   * Value will usually be set to a 63 chars limit but PostgreSQL allows
+   * to higher this value before compiling, so we need to check for that.
+   *
+   * @var int
+   */
+  protected $maxIdentifierLength;
+
+  /**
+   * PostgreSQL's temporary namespace name.
+   *
+   * @var string
+   */
+  protected $tempNamespaceName;
+
+  /**
+   * Make sure to limit identifiers according to PostgreSQL compiled in length.
+   *
+   * PostgreSQL allows in standard configuration identifiers no longer than 63
+   * chars for table/relation names, indexes, primary keys, and constraints. So
+   * we map all identifiers that are too long to drupal_base64hash_tag, where
+   * tag is one of:
+   *   - idx for indexes
+   *   - key for constraints
+   *   - pkey for primary keys
+   *   - seq for sequences
+   *
+   * @param string $table_identifier_part
+   *   The first argument used to build the identifier string. This usually
+   *   refers to a table/relation name.
+   * @param string $column_identifier_part
+   *   The second argument used to build the identifier string. This usually
+   *   refers to one or more column names.
+   * @param string $tag
+   *   The identifier tag. It can be one of 'idx', 'key', 'pkey' or 'seq'.
+   * @param string $separator
+   *   (optional) The separator used to glue together the aforementioned
+   *   identifier parts. Defaults to '__'.
+   *
+   * @return string
+   *   The index/constraint/pkey identifier.
+   */
+  protected function ensureIdentifiersLength($table_identifier_part, $column_identifier_part, $tag, $separator = '__') {
+    $info = $this->getPrefixInfo($table_identifier_part);
+    $table_identifier_part = $info['table'];
+    $identifierName = implode($separator, [$table_identifier_part, $column_identifier_part, $tag]);
+
+    // Retrieve the max identifier length which is usually 63 characters
+    // but can be altered before PostgreSQL is compiled so we need to check.
+    if (empty($this->maxIdentifierLength)) {
+      $this->maxIdentifierLength = $this->connection->query("SHOW max_identifier_length")->fetchField();
+    }
+
+    if (strlen($identifierName) > $this->maxIdentifierLength) {
+      $saveIdentifier = '"drupal_' . $this->hashBase64($identifierName) . '_' . $tag . '"';
+    }
+    else {
+      $saveIdentifier = $identifierName;
+    }
+    return $saveIdentifier;
+  }
+
+  /**
+   * Fetch the list of blobs and sequences used on a table.
+   *
+   * We introspect the database to collect the information required by insert
+   * and update queries.
+   *
+   * @param string $table
+   *   The non-prefixed name of the table.
+   *
+   * @return mixed|object
+   *   An object with two member variables:
+   *   - 'blob_fields' that lists all the blob fields in the table.
+   *   - 'sequences' that lists the sequences used in that table.
+   *
+   * @throws \Exception
+   *   Exception thrown when the query for the table information fails.
+   */
+  public function queryTableInformation($table) {
+    // Generate a key to reference this table's information on.
+    $key = $this->connection->prefixTables('{' . $table . '}');
+
+    // Take into account that temporary tables are stored in a different schema.
+    // \Drupal\Core\Database\Connection::generateTemporaryTableName() sets the
+    // 'db_temporary_' prefix to all temporary tables.
+    if (strpos($key, '.') === FALSE && strpos($table, 'db_temporary_') === FALSE) {
+      $key = 'public.' . $key;
+    }
+    else {
+      $key = $this->getTempNamespaceName() . '.' . $key;
+    }
+
+    if (!isset($this->tableInformation[$key])) {
+      $table_information = (object) [
+        'blob_fields' => [],
+        'sequences' => [],
+      ];
+      $this->connection->addSavepoint();
+
+      try {
+        // The bytea columns and sequences for a table can be found in
+        // pg_attribute, which is significantly faster than querying the
+        // information_schema. The data type of a field can be found by lookup
+        // of the attribute ID, and the default value must be extracted from the
+        // node tree for the attribute definition instead of the historical
+        // human-readable column, adsrc.
+        $sql = <<<'EOD'
+SELECT pg_attribute.attname AS column_name, format_type(pg_attribute.atttypid, pg_attribute.atttypmod) AS data_type, pg_get_expr(pg_attrdef.adbin, pg_attribute.attrelid) AS column_default
+FROM pg_attribute
+LEFT JOIN pg_attrdef ON pg_attrdef.adrelid = pg_attribute.attrelid AND pg_attrdef.adnum = pg_attribute.attnum
+WHERE pg_attribute.attnum > 0
+AND NOT pg_attribute.attisdropped
+AND pg_attribute.attrelid = :key::regclass
+AND (format_type(pg_attribute.atttypid, pg_attribute.atttypmod) = 'bytea'
+OR pg_get_expr(pg_attrdef.adbin, pg_attribute.attrelid) LIKE 'nextval%')
+EOD;
+        $result = $this->connection->query($sql, [
+          ':key' => $key,
+        ]);
+      }
+      catch (\Exception $e) {
+        $this->connection->rollbackSavepoint();
+        throw $e;
+      }
+      $this->connection->releaseSavepoint();
+
+      // If the table information does not yet exist in the PostgreSQL
+      // metadata, then return the default table information here, so that it
+      // will not be cached.
+      if (empty($result)) {
+        return $table_information;
+      }
+
+      foreach ($result as $column) {
+        if ($column->data_type == 'bytea') {
+          $table_information->blob_fields[$column->column_name] = TRUE;
+        }
+        elseif (preg_match("/nextval\('([^']+)'/", $column->column_default, $matches)) {
+          // We must know of any sequences in the table structure to help us
+          // return the last insert id. If there is more than 1 sequences the
+          // first one (index 0 of the sequences array) will be used.
+          $table_information->sequences[] = $matches[1];
+          $table_information->serial_fields[] = $column->column_name;
+        }
+      }
+      $this->tableInformation[$key] = $table_information;
+    }
+    return $this->tableInformation[$key];
+  }
+
+  /**
+   * Gets PostgreSQL's temporary namespace name.
+   *
+   * @return string
+   *   PostgreSQL's temporary namespace name.
+   */
+  protected function getTempNamespaceName() {
+    if (!isset($this->tempNamespaceName)) {
+      $this->tempNamespaceName = $this->connection->query('SELECT nspname FROM pg_namespace WHERE oid = pg_my_temp_schema()')->fetchField();
+    }
+    return $this->tempNamespaceName;
+  }
+
+  /**
+   * Resets information about table blobs, sequences and serial fields.
+   *
+   * @param $table
+   *   The non-prefixed name of the table.
+   */
+  protected function resetTableInformation($table) {
+    $key = $this->connection->prefixTables('{' . $table . '}');
+    if (strpos($key, '.') === FALSE) {
+      $key = 'public.' . $key;
+    }
+    unset($this->tableInformation[$key]);
+  }
+
+  /**
+   * Fetches the list of constraints used on a field.
+   *
+   * We introspect the database to collect the information required by field
+   * alteration.
+   *
+   * @param string $table
+   *   The non-prefixed name of the table.
+   * @param string $field
+   *   The name of the field.
+   * @param string $constraint_type
+   *   (optional) The type of the constraint. This can be one of the following:
+   *   - c: check constraint;
+   *   - f: foreign key constraint;
+   *   - p: primary key constraint;
+   *   - u: unique constraint;
+   *   - t: constraint trigger;
+   *   - x: exclusion constraint.
+   *   Defaults to 'c' for a CHECK constraint.
+   *   @see https://www.postgresql.org/docs/current/catalog-pg-constraint.html
+   *
+   * @return array
+   *   An array containing all the constraint names for the field.
+   *
+   * @throws \Exception
+   *   Exception thrown when the query for the table information fails.
+   */
+  public function queryFieldInformation($table, $field, $constraint_type = 'c') {
+    assert(in_array($constraint_type, ['c', 'f', 'p', 'u', 't', 'x']));
+    $prefixInfo = $this->getPrefixInfo($table, TRUE);
+
+    // Split the key into schema and table for querying.
+    $schema = $prefixInfo['schema'];
+    $table_name = $prefixInfo['table'];
+
+    $this->connection->addSavepoint();
+
+    try {
+      $checks = $this->connection->query("SELECT conname FROM pg_class cl INNER JOIN pg_constraint co ON co.conrelid = cl.oid INNER JOIN pg_attribute attr ON attr.attrelid = cl.oid AND attr.attnum = ANY (co.conkey) INNER JOIN pg_namespace ns ON cl.relnamespace = ns.oid WHERE co.contype = :constraint_type AND ns.nspname = :schema AND cl.relname = :table AND attr.attname = :column", [
+        ':constraint_type' => $constraint_type,
+        ':schema' => $schema,
+        ':table' => $table_name,
+        ':column' => $field,
+      ]);
+    }
+    catch (\Exception $e) {
+      $this->connection->rollbackSavepoint();
+      throw $e;
+    }
+
+    $this->connection->releaseSavepoint();
+
+    $field_information = $checks->fetchCol();
+
+    return $field_information;
+  }
+
+  /**
+   * Generate SQL to create a new table from a Drupal schema definition.
+   *
+   * @param string $name
+   *   The name of the table to create.
+   * @param array $table
+   *   A Schema API table definition array.
+   *
+   * @return array
+   *   An array of SQL statements to create the table.
+   */
+  protected function createTableSql($name, $table) {
+    $sql_fields = [];
+    foreach ($table['fields'] as $field_name => $field) {
+      $sql_fields[] = $this->createFieldSql($field_name, $this->processField($field));
+    }
+
+    $sql_keys = [];
+    if (!empty($table['primary key']) && is_array($table['primary key'])) {
+      $this->ensureNotNullPrimaryKey($table['primary key'], $table['fields']);
+      $sql_keys[] = 'CONSTRAINT ' . $this->ensureIdentifiersLength($name, '', 'pkey') . ' PRIMARY KEY (' . $this->createPrimaryKeySql($table['primary key']) . ')';
+    }
+    if (isset($table['unique keys']) && is_array($table['unique keys'])) {
+      foreach ($table['unique keys'] as $key_name => $key) {
+        $sql_keys[] = 'CONSTRAINT ' . $this->ensureIdentifiersLength($name, $key_name, 'key') . ' UNIQUE (' . implode(', ', $key) . ')';
+      }
+    }
+
+    $sql = "CREATE TABLE {" . $name . "} (\n\t";
+    $sql .= implode(",\n\t", $sql_fields);
+    if (count($sql_keys) > 0) {
+      $sql .= ",\n\t";
+    }
+    $sql .= implode(",\n\t", $sql_keys);
+    $sql .= "\n)";
+    $statements[] = $sql;
+
+    if (isset($table['indexes']) && is_array($table['indexes'])) {
+      foreach ($table['indexes'] as $key_name => $key) {
+        $statements[] = $this->_createIndexSql($name, $key_name, $key);
+      }
+    }
+
+    // Add table comment.
+    if (!empty($table['description'])) {
+      $statements[] = 'COMMENT ON TABLE {' . $name . '} IS ' . $this->prepareComment($table['description']);
+    }
+
+    // Add column comments.
+    foreach ($table['fields'] as $field_name => $field) {
+      if (!empty($field['description'])) {
+        $statements[] = 'COMMENT ON COLUMN {' . $name . '}.' . $field_name . ' IS ' . $this->prepareComment($field['description']);
+      }
+    }
+
+    return $statements;
+  }
+
+  /**
+   * Create an SQL string for a field to be used in table creation or
+   * alteration.
+   *
+   * @param $name
+   *   Name of the field.
+   * @param $spec
+   *   The field specification, as per the schema data structure format.
+   */
+  protected function createFieldSql($name, $spec) {
+    // The PostgreSQL server converts names into lowercase, unless quoted.
+    $sql = '"' . $name . '" ' . $spec['pgsql_type'];
+
+    if (isset($spec['type']) && $spec['type'] == 'serial') {
+      unset($spec['not null']);
+    }
+
+    if (in_array($spec['pgsql_type'], ['varchar', 'character']) && isset($spec['length'])) {
+      $sql .= '(' . $spec['length'] . ')';
+    }
+    elseif (isset($spec['precision']) && isset($spec['scale'])) {
+      $sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')';
+    }
+
+    if (!empty($spec['unsigned'])) {
+      $sql .= " CHECK ($name >= 0)";
+    }
+
+    if (isset($spec['not null'])) {
+      if ($spec['not null']) {
+        $sql .= ' NOT NULL';
+      }
+      else {
+        $sql .= ' NULL';
+      }
+    }
+    if (array_key_exists('default', $spec)) {
+      $default = $this->escapeDefaultValue($spec['default']);
+      $sql .= " default $default";
+    }
+
+    return $sql;
+  }
+
+  /**
+   * Set database-engine specific properties for a field.
+   *
+   * @param $field
+   *   A field description array, as specified in the schema documentation.
+   */
+  protected function processField($field) {
+    if (!isset($field['size'])) {
+      $field['size'] = 'normal';
+    }
+
+    // Set the correct database-engine specific datatype.
+    // In case one is already provided, force it to lowercase.
+    if (isset($field['pgsql_type'])) {
+      $field['pgsql_type'] = mb_strtolower($field['pgsql_type']);
+    }
+    else {
+      $map = $this->getFieldTypeMap();
+      $field['pgsql_type'] = $map[$field['type'] . ':' . $field['size']];
+    }
+
+    if (!empty($field['unsigned'])) {
+      // Unsigned data types are not supported in PostgreSQL 10. In MySQL,
+      // they are used to ensure a positive number is inserted and it also
+      // doubles the maximum integer size that can be stored in a field.
+      // The PostgreSQL schema in Drupal creates a check constraint
+      // to ensure that a value inserted is >= 0. To provide the extra
+      // integer capacity, here, we bump up the column field size.
+      if (!isset($map)) {
+        $map = $this->getFieldTypeMap();
+      }
+      switch ($field['pgsql_type']) {
+        case 'smallint':
+          $field['pgsql_type'] = $map['int:medium'];
+          break;
+
+        case 'int':
+          $field['pgsql_type'] = $map['int:big'];
+          break;
+      }
+    }
+    if (isset($field['type']) && $field['type'] == 'serial') {
+      unset($field['not null']);
+    }
+    return $field;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFieldTypeMap() {
+    // Put :normal last so it gets preserved by array_flip. This makes
+    // it much easier for modules (such as schema.module) to map
+    // database types back into schema types.
+    // $map does not use drupal_static as its value never changes.
+    static $map = [
+      'varchar_ascii:normal' => 'varchar',
+
+      'varchar:normal' => 'varchar',
+      'char:normal' => 'character',
+
+      'text:tiny' => 'text',
+      'text:small' => 'text',
+      'text:medium' => 'text',
+      'text:big' => 'text',
+      'text:normal' => 'text',
+
+      'int:tiny' => 'smallint',
+      'int:small' => 'smallint',
+      'int:medium' => 'int',
+      'int:big' => 'bigint',
+      'int:normal' => 'int',
+
+      'float:tiny' => 'real',
+      'float:small' => 'real',
+      'float:medium' => 'real',
+      'float:big' => 'double precision',
+      'float:normal' => 'real',
+
+      'numeric:normal' => 'numeric',
+
+      'blob:big' => 'bytea',
+      'blob:normal' => 'bytea',
+
+      'serial:tiny' => 'serial',
+      'serial:small' => 'serial',
+      'serial:medium' => 'serial',
+      'serial:big' => 'bigserial',
+      'serial:normal' => 'serial',
+      ];
+    return $map;
+  }
+
+  protected function _createKeySql($fields) {
+    $return = [];
+    foreach ($fields as $field) {
+      if (is_array($field)) {
+        $return[] = 'substr(' . $field[0] . ', 1, ' . $field[1] . ')';
+      }
+      else {
+        $return[] = '"' . $field . '"';
+      }
+    }
+    return implode(', ', $return);
+  }
+
+  /**
+   * Create the SQL expression for primary keys.
+   *
+   * Postgresql does not support key length. It does support fillfactor, but
+   * that requires a separate database lookup for each column in the key. The
+   * key length defined in the schema is ignored.
+   */
+  protected function createPrimaryKeySql($fields) {
+    $return = [];
+    foreach ($fields as $field) {
+      if (is_array($field)) {
+        $return[] = '"' . $field[0] . '"';
+      }
+      else {
+        $return[] = '"' . $field . '"';
+      }
+    }
+    return implode(', ', $return);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function tableExists($table) {
+    $prefixInfo = $this->getPrefixInfo($table, TRUE);
+
+    return (bool) $this->connection->query("SELECT 1 FROM pg_tables WHERE schemaname = :schema AND tablename = :table", [':schema' => $prefixInfo['schema'], ':table' => $prefixInfo['table']])->fetchField();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function findTables($table_expression) {
+    $individually_prefixed_tables = $this->connection->getUnprefixedTablesMap();
+    $default_prefix = $this->connection->tablePrefix();
+    $default_prefix_length = strlen($default_prefix);
+    $tables = [];
+
+    // Load all the tables up front in order to take into account per-table
+    // prefixes. The actual matching is done at the bottom of the method.
+    $results = $this->connection->query("SELECT tablename FROM pg_tables WHERE schemaname = :schema", [':schema' => $this->defaultSchema]);
+    foreach ($results as $table) {
+      // Take into account tables that have an individual prefix.
+      if (isset($individually_prefixed_tables[$table->tablename])) {
+        $prefix_length = strlen($this->connection->tablePrefix($individually_prefixed_tables[$table->tablename]));
+      }
+      elseif ($default_prefix && substr($table->tablename, 0, $default_prefix_length) !== $default_prefix) {
+        // This table name does not start the default prefix, which means that
+        // it is not managed by Drupal so it should be excluded from the result.
+        continue;
+      }
+      else {
+        $prefix_length = $default_prefix_length;
+      }
+
+      // Remove the prefix from the returned tables.
+      $unprefixed_table_name = substr($table->tablename, $prefix_length);
+
+      // The pattern can match a table which is the same as the prefix. That
+      // will become an empty string when we remove the prefix, which will
+      // probably surprise the caller, besides not being a prefixed table. So
+      // remove it.
+      if (!empty($unprefixed_table_name)) {
+        $tables[$unprefixed_table_name] = $unprefixed_table_name;
+      }
+    }
+
+    // Convert the table expression from its SQL LIKE syntax to a regular
+    // expression and escape the delimiter that will be used for matching.
+    $table_expression = str_replace(['%', '_'], ['.*?', '.'], preg_quote($table_expression, '/'));
+    $tables = preg_grep('/^' . $table_expression . '$/i', $tables);
+
+    return $tables;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function renameTable($table, $new_name) {
+    if (!$this->tableExists($table)) {
+      throw new SchemaObjectDoesNotExistException("Cannot rename '$table' to '$new_name': table '$table' doesn't exist.");
+    }
+    if ($this->tableExists($new_name)) {
+      throw new SchemaObjectExistsException("Cannot rename '$table' to '$new_name': table '$new_name' already exists.");
+    }
+
+    // Get the schema and tablename for the old table.
+    $old_full_name = str_replace('"', '', $this->connection->prefixTables('{' . $table . '}'));
+    [$old_schema, $old_table_name] = strpos($old_full_name, '.') ? explode('.', $old_full_name) : ['public', $old_full_name];
+
+    // Index names and constraint names are global in PostgreSQL, so we need to
+    // rename them when renaming the table.
+    $indexes = $this->connection->query('SELECT indexname FROM pg_indexes WHERE schemaname = :schema AND tablename = :table', [':schema' => $old_schema, ':table' => $old_table_name]);
+
+    foreach ($indexes as $index) {
+      // Get the index type by suffix, e.g. idx/key/pkey
+      $index_type = substr($index->indexname, strrpos($index->indexname, '_') + 1);
+
+      // If the index is already rewritten by ensureIdentifiersLength() to not
+      // exceed the 63 chars limit of PostgreSQL, we need to take care of that.
+      // cSpell:disable-next-line
+      // Example (drupal_Gk7Su_T1jcBHVuvSPeP22_I3Ni4GrVEgTYlIYnBJkro_idx).
+      if (strpos($index->indexname, 'drupal_') !== FALSE) {
+        preg_match('/^drupal_(.*)_' . preg_quote($index_type) . '/', $index->indexname, $matches);
+        $index_name = $matches[1];
+      }
+      else {
+        // Make sure to remove the suffix from index names, because
+        // $this->ensureIdentifiersLength() will add the suffix again and thus
+        // would result in a wrong index name.
+        preg_match('/^' . preg_quote($old_full_name) . '__(.*)__' . preg_quote($index_type) . '/', $index->indexname, $matches);
+        $index_name = $matches[1];
+      }
+      $this->connection->query('ALTER INDEX "' . $index->indexname . '" RENAME TO ' . $this->ensureIdentifiersLength($new_name, $index_name, $index_type) . '');
+    }
+
+    // Ensure the new table name does not include schema syntax.
+    $prefixInfo = $this->getPrefixInfo($new_name);
+
+    // Rename sequences if the table contains serial fields.
+    $info = $this->queryTableInformation($table);
+    if (!empty($info->serial_fields)) {
+      foreach ($info->serial_fields as $field) {
+        // The initial name of the sequence is generated automatically by
+        // PostgreSQL when the table is created, so we need to use
+        // pg_get_serial_sequence() to retrieve it.
+        $old_sequence = $this->connection->query("SELECT pg_get_serial_sequence('" . $old_full_name . "', '" . $field . "')")->fetchField();
+
+        // If the new sequence name exceeds the maximum identifier length limit,
+        // it will not match the pattern that is automatically applied by
+        // PostgreSQL on table creation, but that's ok because
+        // pg_get_serial_sequence() will return our non-standard name on
+        // subsequent table renames.
+        $new_sequence = $this->ensureIdentifiersLength($new_name, $field, 'seq', '_');
+
+        $this->connection->query('ALTER SEQUENCE ' . $old_sequence . ' RENAME TO ' . $new_sequence);
+      }
+    }
+    // Now rename the table.
+    $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO ' . $prefixInfo['table']);
+    $this->resetTableInformation($table);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function dropTable($table) {
+    if (!$this->tableExists($table)) {
+      return FALSE;
+    }
+
+    $this->connection->query('DROP TABLE {' . $table . '}');
+    $this->resetTableInformation($table);
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addField($table, $field, $spec, $new_keys = []) {
+    if (!$this->tableExists($table)) {
+      throw new SchemaObjectDoesNotExistException("Cannot add field '$table.$field': table doesn't exist.");
+    }
+    if ($this->fieldExists($table, $field)) {
+      throw new SchemaObjectExistsException("Cannot add field '$table.$field': field already exists.");
+    }
+
+    // Fields that are part of a PRIMARY KEY must be added as NOT NULL.
+    $is_primary_key = isset($new_keys['primary key']) && in_array($field, $new_keys['primary key'], TRUE);
+    if ($is_primary_key) {
+      $this->ensureNotNullPrimaryKey($new_keys['primary key'], [$field => $spec]);
+    }
+
+    $fixnull = FALSE;
+    if (!empty($spec['not null']) && !isset($spec['default']) && !$is_primary_key) {
+      $fixnull = TRUE;
+      $spec['not null'] = FALSE;
+    }
+    $query = 'ALTER TABLE {' . $table . '} ADD COLUMN ';
+    $query .= $this->createFieldSql($field, $this->processField($spec));
+    $this->connection->query($query);
+    if (isset($spec['initial_from_field'])) {
+      if (isset($spec['initial'])) {
+        $expression = 'COALESCE(' . $spec['initial_from_field'] . ', :default_initial_value)';
+        $arguments = [':default_initial_value' => $spec['initial']];
+      }
+      else {
+        $expression = $spec['initial_from_field'];
+        $arguments = [];
+      }
+      $this->connection->update($table)
+        ->expression($field, $expression, $arguments)
+        ->execute();
+    }
+    elseif (isset($spec['initial'])) {
+      $this->connection->update($table)
+        ->fields([$field => $spec['initial']])
+        ->execute();
+    }
+    if ($fixnull) {
+      $this->connection->query("ALTER TABLE {" . $table . "} ALTER $field SET NOT NULL");
+    }
+    if (isset($new_keys)) {
+      // Make sure to drop the existing primary key before adding a new one.
+      // This is only needed when adding a field because this method, unlike
+      // changeField(), is supposed to handle primary keys automatically.
+      if (isset($new_keys['primary key']) && $this->constraintExists($table, 'pkey')) {
+        $this->dropPrimaryKey($table);
+      }
+      $this->_createKeys($table, $new_keys);
+    }
+    // Add column comment.
+    if (!empty($spec['description'])) {
+      $this->connection->query('COMMENT ON COLUMN {' . $table . '}.' . $field . ' IS ' . $this->prepareComment($spec['description']));
+    }
+    $this->resetTableInformation($table);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function dropField($table, $field) {
+    if (!$this->fieldExists($table, $field)) {
+      return FALSE;
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} DROP COLUMN "' . $field . '"');
+    $this->resetTableInformation($table);
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fieldExists($table, $column) {
+    $prefixInfo = $this->getPrefixInfo($table);
+
+    return (bool) $this->connection->query("SELECT 1 FROM pg_attribute WHERE attrelid = :key::regclass AND attname = :column AND NOT attisdropped AND attnum > 0", [':key' => $prefixInfo['schema'] . '.' . $prefixInfo['table'], ':column' => $column])->fetchField();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function indexExists($table, $name) {
+    // Details https://www.postgresql.org/docs/10/view-pg-indexes.html
+    $index_name = $this->ensureIdentifiersLength($table, $name, 'idx');
+    // Remove leading and trailing quotes because the index name is in a WHERE
+    // clause and not used as an identifier.
+    $index_name = str_replace('"', '', $index_name);
+    return (bool) $this->connection->query("SELECT 1 FROM pg_indexes WHERE indexname = '$index_name'")->fetchField();
+  }
+
+  /**
+   * Helper function: check if a constraint (PK, FK, UK) exists.
+   *
+   * @param string $table
+   *   The name of the table.
+   * @param string $name
+   *   The name of the constraint (typically 'pkey' or '[constraint]__key').
+   *
+   * @return bool
+   *   TRUE if the constraint exists, FALSE otherwise.
+   */
+  public function constraintExists($table, $name) {
+    // ::ensureIdentifiersLength() expects three parameters, although not
+    // explicitly stated in its signature, thus we split our constraint name in
+    // a proper name and a suffix.
+    if ($name == 'pkey') {
+      $suffix = $name;
+      $name = '';
+    }
+    else {
+      $pos = strrpos($name, '__');
+      $suffix = substr($name, $pos + 2);
+      $name = substr($name, 0, $pos);
+    }
+    $constraint_name = $this->ensureIdentifiersLength($table, $name, $suffix);
+    // Remove leading and trailing quotes because the index name is in a WHERE
+    // clause and not used as an identifier.
+    $constraint_name = str_replace('"', '', $constraint_name);
+    return (bool) $this->connection->query("SELECT 1 FROM pg_constraint WHERE conname = '$constraint_name'")->fetchField();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addPrimaryKey($table, $fields) {
+    if (!$this->tableExists($table)) {
+      throw new SchemaObjectDoesNotExistException("Cannot add primary key to table '$table': table doesn't exist.");
+    }
+    if ($this->constraintExists($table, 'pkey')) {
+      throw new SchemaObjectExistsException("Cannot add primary key to table '$table': primary key already exists.");
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} ADD CONSTRAINT ' . $this->ensureIdentifiersLength($table, '', 'pkey') . ' PRIMARY KEY (' . $this->createPrimaryKeySql($fields) . ')');
+    $this->resetTableInformation($table);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function dropPrimaryKey($table) {
+    if (!$this->constraintExists($table, 'pkey')) {
+      return FALSE;
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT ' . $this->ensureIdentifiersLength($table, '', 'pkey'));
+    $this->resetTableInformation($table);
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function findPrimaryKeyColumns($table) {
+    if (!$this->tableExists($table)) {
+      return FALSE;
+    }
+    return $this->connection->query("SELECT array_position(i.indkey, a.attnum) AS position, a.attname FROM pg_index i JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) WHERE i.indrelid = '{" . $table . "}'::regclass AND i.indisprimary ORDER BY position")->fetchAllKeyed();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addUniqueKey($table, $name, $fields) {
+    if (!$this->tableExists($table)) {
+      throw new SchemaObjectDoesNotExistException("Cannot add unique key '$name' to table '$table': table doesn't exist.");
+    }
+    if ($this->constraintExists($table, $name . '__key')) {
+      throw new SchemaObjectExistsException("Cannot add unique key '$name' to table '$table': unique key already exists.");
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} ADD CONSTRAINT ' . $this->ensureIdentifiersLength($table, $name, 'key') . ' UNIQUE (' . implode(',', $fields) . ')');
+    $this->resetTableInformation($table);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function dropUniqueKey($table, $name) {
+    if (!$this->constraintExists($table, $name . '__key')) {
+      return FALSE;
+    }
+
+    $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT ' . $this->ensureIdentifiersLength($table, $name, 'key'));
+    $this->resetTableInformation($table);
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addIndex($table, $name, $fields, array $spec) {
+    if (!$this->tableExists($table)) {
+      throw new SchemaObjectDoesNotExistException("Cannot add index '$name' to table '$table': table doesn't exist.");
+    }
+    if ($this->indexExists($table, $name)) {
+      throw new SchemaObjectExistsException("Cannot add index '$name' to table '$table': index already exists.");
+    }
+
+    $this->connection->query($this->_createIndexSql($table, $name, $fields));
+    $this->resetTableInformation($table);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function dropIndex($table, $name) {
+    if (!$this->indexExists($table, $name)) {
+      return FALSE;
+    }
+
+    $this->connection->query('DROP INDEX ' . $this->ensureIdentifiersLength($table, $name, 'idx'));
+    $this->resetTableInformation($table);
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function introspectIndexSchema($table) {
+    if (!$this->tableExists($table)) {
+      throw new SchemaObjectDoesNotExistException("The table $table doesn't exist.");
+    }
+
+    $index_schema = [
+      'primary key' => [],
+      'unique keys' => [],
+      'indexes' => [],
+    ];
+
+    // Get the schema and tablename for the table without identifier quotes.
+    $full_name = str_replace('"', '', $this->connection->prefixTables('{' . $table . '}'));
+    $result = $this->connection->query("SELECT i.relname AS index_name, a.attname AS column_name FROM pg_class t, pg_class i, pg_index ix, pg_attribute a WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND a.attnum = ANY(ix.indkey) AND t.relkind = 'r' AND t.relname = :table_name ORDER BY index_name ASC, column_name ASC", [
+      ':table_name' => $full_name,
+    ])->fetchAll();
+    foreach ($result as $row) {
+      if (preg_match('/_pkey$/', $row->index_name)) {
+        $index_schema['primary key'][] = $row->column_name;
+      }
+      elseif (preg_match('/_key$/', $row->index_name)) {
+        $index_schema['unique keys'][$row->index_name][] = $row->column_name;
+      }
+      elseif (preg_match('/_idx$/', $row->index_name)) {
+        $index_schema['indexes'][$row->index_name][] = $row->column_name;
+      }
+    }
+
+    return $index_schema;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function changeField($table, $field, $field_new, $spec, $new_keys = []) {
+    if (!$this->fieldExists($table, $field)) {
+      throw new SchemaObjectDoesNotExistException("Cannot change the definition of field '$table.$field': field doesn't exist.");
+    }
+    if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
+      throw new SchemaObjectExistsException("Cannot rename field '$table.$field' to '$field_new': target field already exists.");
+    }
+    if (isset($new_keys['primary key']) && in_array($field_new, $new_keys['primary key'], TRUE)) {
+      $this->ensureNotNullPrimaryKey($new_keys['primary key'], [$field_new => $spec]);
+    }
+
+    $spec = $this->processField($spec);
+
+    // Type 'serial' is known to PostgreSQL, but only during table creation,
+    // not when altering. Because of that, we create it here as an 'int'. After
+    // we create it we manually re-apply the sequence.
+    if (in_array($spec['pgsql_type'], ['serial', 'bigserial'])) {
+      $field_def = 'int';
+    }
+    else {
+      $field_def = $spec['pgsql_type'];
+    }
+
+    if (in_array($spec['pgsql_type'], ['varchar', 'character', 'text']) && isset($spec['length'])) {
+      $field_def .= '(' . $spec['length'] . ')';
+    }
+    elseif (isset($spec['precision']) && isset($spec['scale'])) {
+      $field_def .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')';
+    }
+
+    // Remove old check constraints.
+    $field_info = $this->queryFieldInformation($table, $field);
+
+    foreach ($field_info as $check) {
+      $this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT "' . $check . '"');
+    }
+
+    // Remove old default.
+    $this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN "' . $field . '" DROP DEFAULT');
+
+    // Convert field type.
+    // Usually, we do this via a simple typecast 'USING fieldname::type'. But
+    // the typecast does not work for conversions to bytea.
+    // @see http://www.postgresql.org/docs/current/static/datatype-binary.html
+    $table_information = $this->queryTableInformation($table);
+    $is_bytea = !empty($table_information->blob_fields[$field]);
+    if ($spec['pgsql_type'] != 'bytea') {
+      if ($is_bytea) {
+        $this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $field_def . ' USING convert_from("' . $field . '"' . ", 'UTF8')");
+      }
+      else {
+        $this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $field_def . ' USING "' . $field . '"::' . $field_def);
+      }
+    }
+    else {
+      // Do not attempt to convert a field that is bytea already.
+      if (!$is_bytea) {
+        // Convert to a bytea type by using the SQL replace() function to
+        // convert any single backslashes in the field content to double
+        // backslashes ('\' to '\\').
+        $this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $field_def . ' USING decode(replace("' . $field . '"' . ", E'\\\\', E'\\\\\\\\'), 'escape');");
+      }
+    }
+
+    if (isset($spec['not null'])) {
+      if ($spec['not null']) {
+        $null_action = 'SET NOT NULL';
+      }
+      else {
+        $null_action = 'DROP NOT NULL';
+      }
+      $this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" ' . $null_action);
+    }
+
+    if (in_array($spec['pgsql_type'], ['serial', 'bigserial'])) {
+      // Type "serial" is known to PostgreSQL, but *only* during table creation,
+      // not when altering. Because of that, the sequence needs to be created
+      // and initialized by hand.
+      $seq = $this->connection->makeSequenceName($table, $field_new);
+      $this->connection->query("CREATE SEQUENCE " . $seq);
+      // Set sequence to maximal field value to not conflict with existing
+      // entries.
+      $this->connection->query("SELECT setval('" . $seq . "', MAX(\"" . $field . '")) FROM {' . $table . "}");
+      $this->connection->query('ALTER TABLE {' . $table . '} ALTER ' . $field . ' SET DEFAULT nextval(' . $this->connection->quote($seq) . ')');
+    }
+
+    // Rename the column if necessary.
+    if ($field != $field_new) {
+      $this->connection->query('ALTER TABLE {' . $table . '} RENAME "' . $field . '" TO "' . $field_new . '"');
+    }
+
+    // Add unsigned check if necessary.
+    if (!empty($spec['unsigned'])) {
+      $this->connection->query('ALTER TABLE {' . $table . '} ADD CHECK ("' . $field_new . '" >= 0)');
+    }
+
+    // Add default if necessary.
+    if (isset($spec['default'])) {
+      $this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN "' . $field_new . '" SET DEFAULT ' . $this->escapeDefaultValue($spec['default']));
+    }
+
+    // Change description if necessary.
+    if (!empty($spec['description'])) {
+      $this->connection->query('COMMENT ON COLUMN {' . $table . '}."' . $field_new . '" IS ' . $this->prepareComment($spec['description']));
+    }
+
+    if (isset($new_keys)) {
+      $this->_createKeys($table, $new_keys);
+    }
+    $this->resetTableInformation($table);
+  }
+
+  protected function _createIndexSql($table, $name, $fields) {
+    $query = 'CREATE INDEX ' . $this->ensureIdentifiersLength($table, $name, 'idx') . ' ON {' . $table . '} (';
+    $query .= $this->_createKeySql($fields) . ')';
+    return $query;
+  }
+
+  protected function _createKeys($table, $new_keys) {
+    if (isset($new_keys['primary key'])) {
+      $this->addPrimaryKey($table, $new_keys['primary key']);
+    }
+    if (isset($new_keys['unique keys'])) {
+      foreach ($new_keys['unique keys'] as $name => $fields) {
+        $this->addUniqueKey($table, $name, $fields);
+      }
+    }
+    if (isset($new_keys['indexes'])) {
+      foreach ($new_keys['indexes'] as $name => $fields) {
+        // Even though $new_keys is not a full schema it still has 'indexes' and
+        // so is a partial schema. Technically addIndex() doesn't do anything
+        // with it so passing an empty array would work as well.
+        $this->addIndex($table, $name, $fields, $new_keys);
+      }
+    }
+  }
+
+  /**
+   * Retrieve a table or column comment.
+   */
+  public function getComment($table, $column = NULL) {
+    $info = $this->getPrefixInfo($table);
+    // Don't use {} around pg_class, pg_attribute tables.
+    if (isset($column)) {
+      return $this->connection->query('SELECT col_description(oid, attnum) FROM pg_class, pg_attribute WHERE attrelid = oid AND relname = ? AND attname = ?', [$info['table'], $column])->fetchField();
+    }
+    else {
+      return $this->connection->query('SELECT obj_description(oid, ?) FROM pg_class WHERE relname = ?', ['pg_class', $info['table']])->fetchField();
+    }
+  }
+
+  /**
+   * Calculates a base-64 encoded, PostgreSQL-safe sha-256 hash per PostgreSQL
+   * documentation: 4.1. Lexical Structure.
+   *
+   * @param $data
+   *   String to be hashed.
+   *
+   * @return string
+   *   A base-64 encoded sha-256 hash, with + and / replaced with _ and any =
+   *   padding characters removed.
+   */
+  protected function hashBase64($data) {
+    $hash = base64_encode(hash('sha256', $data, TRUE));
+    // Modify the hash so it's safe to use in PostgreSQL identifiers.
+    return strtr($hash, ['+' => '_', '/' => '_', '=' => '']);
+  }
+
+  /**
+   * Determines whether the PostgreSQL extension is created.
+   *
+   * @param string $name
+   *   The name of the extension.
+   *
+   * @return bool
+   *   Return TRUE when the extension is created, FALSE otherwise.
+   *
+   * @internal
+   */
+  public function extensionExists($name): bool {
+    return (bool) $this->connection->query('SELECT installed_version FROM pg_available_extensions WHERE name = :name', [
+      ':name' => $name,
+    ])->fetchField();
+  }
+
+}
+
+/**
+ * @} End of "addtogroup schemaapi".
+ */
diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Select.php b/core/modules/pgsql/src/Driver/Database/pgsql/Select.php
new file mode 100644
index 000000000000..959b6092d92b
--- /dev/null
+++ b/core/modules/pgsql/src/Driver/Database/pgsql/Select.php
@@ -0,0 +1,160 @@
+<?php
+
+namespace Drupal\pgsql\Driver\Database\pgsql;
+
+use Drupal\Core\Database\Query\Select as QuerySelect;
+
+/**
+ * @addtogroup database
+ * @{
+ */
+
+/**
+ * PostgreSQL implementation of \Drupal\Core\Database\Query\Select.
+ */
+class Select extends QuerySelect {
+
+  public function orderRandom() {
+    $alias = $this->addExpression('RANDOM()', 'random_field');
+    $this->orderBy($alias);
+    return $this;
+  }
+
+  /**
+   * Overrides SelectQuery::orderBy().
+   *
+   * PostgreSQL adheres strictly to the SQL-92 standard and requires that when
+   * using DISTINCT or GROUP BY conditions, fields and expressions that are
+   * ordered on also need to be selected. This is a best effort implementation
+   * to handle the cases that can be automated by adding the field if it is not
+   * yet selected.
+   *
+   * @code
+   *   $query = \Drupal::database()->select('example', 'e');
+   *   $query->join('example_revision', 'er', '[e].[vid] = [er].[vid]');
+   *   $query
+   *     ->distinct()
+   *     ->fields('e')
+   *     ->orderBy('timestamp');
+   * @endcode
+   *
+   * In this query, it is not possible (without relying on the schema) to know
+   * whether timestamp belongs to example_revision and needs to be added or
+   * belongs to node and is already selected. Queries like this will need to be
+   * corrected in the original query by adding an explicit call to
+   * SelectQuery::addField() or SelectQuery::fields().
+   *
+   * Since this has a small performance impact, both by the additional
+   * processing in this function and in the database that needs to return the
+   * additional fields, this is done as an override instead of implementing it
+   * directly in SelectQuery::orderBy().
+   */
+  public function orderBy($field, $direction = 'ASC') {
+    // Only allow ASC and DESC, default to ASC.
+    // Emulate MySQL default behavior to sort NULL values first for ascending,
+    // and last for descending.
+    // @see http://www.postgresql.org/docs/9.3/static/queries-order.html
+    $direction = strtoupper($direction) == 'DESC' ? 'DESC NULLS LAST' : 'ASC NULLS FIRST';
+    $this->order[$field] = $direction;
+
+    if ($this->hasTag('entity_query')) {
+      return $this;
+    }
+
+    // If there is a table alias specified, split it up.
+    if (strpos($field, '.') !== FALSE) {
+      [$table, $table_field] = explode('.', $field);
+    }
+    // Figure out if the field has already been added.
+    foreach ($this->fields as $existing_field) {
+      if (!empty($table)) {
+        // If table alias is given, check if field and table exists.
+        if ($existing_field['table'] == $table && $existing_field['field'] == $table_field) {
+          return $this;
+        }
+      }
+      else {
+        // If there is no table, simply check if the field exists as a field or
+        // an aliased field.
+        if ($existing_field['alias'] == $field) {
+          return $this;
+        }
+      }
+    }
+
+    // Also check expression aliases.
+    foreach ($this->expressions as $expression) {
+      if ($expression['alias'] == $this->connection->escapeAlias($field)) {
+        return $this;
+      }
+    }
+
+    // If a table loads all fields, it can not be added again. It would
+    // result in an ambiguous alias error because that field would be loaded
+    // twice: Once through table_alias.* and once directly. If the field
+    // actually belongs to a different table, it must be added manually.
+    foreach ($this->tables as $table) {
+      if (!empty($table['all_fields'])) {
+        return $this;
+      }
+    }
+
+    // If $field contains characters which are not allowed in a field name
+    // it is considered an expression, these can't be handled automatically
+    // either.
+    if ($this->connection->escapeField($field) != $field) {
+      return $this;
+    }
+
+    // This is a case that can be handled automatically, add the field.
+    $this->addField(NULL, $field);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addExpression($expression, $alias = NULL, $arguments = []) {
+    if (empty($alias)) {
+      $alias = 'expression';
+    }
+
+    // This implements counting in the same manner as the parent method.
+    $alias_candidate = $alias;
+    $count = 2;
+    while (!empty($this->expressions[$alias_candidate])) {
+      $alias_candidate = $alias . '_' . $count++;
+    }
+    $alias = $alias_candidate;
+
+    $this->expressions[$alias] = [
+      'expression' => $expression,
+      'alias' => $this->connection->escapeAlias($alias_candidate),
+      'arguments' => $arguments,
+    ];
+
+    return $alias;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute() {
+    $this->connection->addSavepoint();
+    try {
+      $result = parent::execute();
+    }
+    catch (\Exception $e) {
+      $this->connection->rollbackSavepoint();
+      throw $e;
+    }
+    $this->connection->releaseSavepoint();
+
+    return $result;
+  }
+
+}
+
+/**
+ * @} End of "addtogroup database".
+ */
diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Truncate.php b/core/modules/pgsql/src/Driver/Database/pgsql/Truncate.php
new file mode 100644
index 000000000000..18115e0a7d05
--- /dev/null
+++ b/core/modules/pgsql/src/Driver/Database/pgsql/Truncate.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Drupal\pgsql\Driver\Database\pgsql;
+
+use Drupal\Core\Database\Query\Truncate as QueryTruncate;
+
+/**
+ * PostgreSQL implementation of \Drupal\Core\Database\Query\Truncate.
+ */
+class Truncate extends QueryTruncate {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute() {
+    $this->connection->addSavepoint();
+    try {
+      $result = parent::execute();
+    }
+    catch (\Exception $e) {
+      $this->connection->rollbackSavepoint();
+      throw $e;
+    }
+    $this->connection->releaseSavepoint();
+
+    return $result;
+  }
+
+}
diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Update.php b/core/modules/pgsql/src/Driver/Database/pgsql/Update.php
new file mode 100644
index 000000000000..d3f2ebf6431d
--- /dev/null
+++ b/core/modules/pgsql/src/Driver/Database/pgsql/Update.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace Drupal\pgsql\Driver\Database\pgsql;
+
+use Drupal\Core\Database\Query\Update as QueryUpdate;
+use Drupal\Core\Database\Query\SelectInterface;
+
+/**
+ * PostgreSQL implementation of \Drupal\Core\Database\Query\Update.
+ */
+class Update extends QueryUpdate {
+
+  public function execute() {
+    $max_placeholder = 0;
+    $blobs = [];
+    $blob_count = 0;
+
+    // Because we filter $fields the same way here and in __toString(), the
+    // placeholders will all match up properly.
+    $stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions, TRUE);
+
+    // Fetch the list of blobs and sequences used on that table.
+    $table_information = $this->connection->schema()->queryTableInformation($this->table);
+
+    // Expressions take priority over literal fields, so we process those first
+    // and remove any literal fields that conflict.
+    $fields = $this->fields;
+    foreach ($this->expressionFields as $field => $data) {
+      if (!empty($data['arguments'])) {
+        foreach ($data['arguments'] as $placeholder => $argument) {
+          // We assume that an expression will never happen on a BLOB field,
+          // which is a fairly safe assumption to make since in most cases
+          // it would be an invalid query anyway.
+          $stmt->getClientStatement()->bindParam($placeholder, $data['arguments'][$placeholder]);
+        }
+      }
+      if ($data['expression'] instanceof SelectInterface) {
+        $data['expression']->compile($this->connection, $this);
+        $select_query_arguments = $data['expression']->arguments();
+        foreach ($select_query_arguments as $placeholder => $argument) {
+          $stmt->getClientStatement()->bindParam($placeholder, $select_query_arguments[$placeholder]);
+        }
+      }
+      unset($fields[$field]);
+    }
+
+    foreach ($fields as $field => $value) {
+      $placeholder = ':db_update_placeholder_' . ($max_placeholder++);
+
+      if (isset($table_information->blob_fields[$field]) && $value !== NULL) {
+        $blobs[$blob_count] = fopen('php://memory', 'a');
+        fwrite($blobs[$blob_count], $value);
+        rewind($blobs[$blob_count]);
+        $stmt->getClientStatement()->bindParam($placeholder, $blobs[$blob_count], \PDO::PARAM_LOB);
+        ++$blob_count;
+      }
+      else {
+        $stmt->getClientStatement()->bindParam($placeholder, $fields[$field]);
+      }
+    }
+
+    if (count($this->condition)) {
+      $this->condition->compile($this->connection, $this);
+
+      $arguments = $this->condition->arguments();
+      foreach ($arguments as $placeholder => $value) {
+        $stmt->getClientStatement()->bindParam($placeholder, $arguments[$placeholder]);
+      }
+    }
+
+    $this->connection->addSavepoint();
+    try {
+      $stmt->execute(NULL, $this->queryOptions);
+      $this->connection->releaseSavepoint();
+      return $stmt->rowCount();
+    }
+    catch (\Exception $e) {
+      $this->connection->rollbackSavepoint();
+      $this->connection->exceptionHandler()->handleExecutionException($e, $stmt, [], $this->queryOptions);
+    }
+  }
+
+}
diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Upsert.php b/core/modules/pgsql/src/Driver/Database/pgsql/Upsert.php
new file mode 100644
index 000000000000..35823a270b1c
--- /dev/null
+++ b/core/modules/pgsql/src/Driver/Database/pgsql/Upsert.php
@@ -0,0 +1,126 @@
+<?php
+
+namespace Drupal\pgsql\Driver\Database\pgsql;
+
+use Drupal\Core\Database\Query\Upsert as QueryUpsert;
+
+// cSpell:ignore nextval setval
+
+/**
+ * PostgreSQL implementation of \Drupal\Core\Database\Query\Upsert.
+ */
+class Upsert extends QueryUpsert {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute() {
+    if (!$this->preExecute()) {
+      return NULL;
+    }
+
+    $stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions, TRUE);
+
+    // Fetch the list of blobs and sequences used on that table.
+    $table_information = $this->connection->schema()->queryTableInformation($this->table);
+
+    $max_placeholder = 0;
+    $blobs = [];
+    $blob_count = 0;
+    foreach ($this->insertValues as $insert_values) {
+      foreach ($this->insertFields as $idx => $field) {
+        if (isset($table_information->blob_fields[$field]) && $insert_values[$idx] !== NULL) {
+          $blobs[$blob_count] = fopen('php://memory', 'a');
+          fwrite($blobs[$blob_count], $insert_values[$idx]);
+          rewind($blobs[$blob_count]);
+
+          $stmt->getClientStatement()->bindParam(':db_insert_placeholder_' . $max_placeholder++, $blobs[$blob_count], \PDO::PARAM_LOB);
+
+          // Pre-increment is faster in PHP than increment.
+          ++$blob_count;
+        }
+        else {
+          $stmt->getClientStatement()->bindParam(':db_insert_placeholder_' . $max_placeholder++, $insert_values[$idx]);
+        }
+      }
+      // Check if values for a serial field has been passed.
+      if (!empty($table_information->serial_fields)) {
+        foreach ($table_information->serial_fields as $index => $serial_field) {
+          $serial_key = array_search($serial_field, $this->insertFields);
+          if ($serial_key !== FALSE) {
+            $serial_value = $insert_values[$serial_key];
+
+            // Sequences must be greater than or equal to 1.
+            if ($serial_value === NULL || !$serial_value) {
+              $serial_value = 1;
+            }
+            // Set the sequence to the bigger value of either the passed
+            // value or the max value of the column. It can happen that another
+            // thread calls nextval() which could lead to a serial number being
+            // used twice. However, trying to insert a value into a serial
+            // column should only be done in very rare cases and is not thread
+            // safe by definition.
+            $this->connection->query("SELECT setval('" . $table_information->sequences[$index] . "', GREATEST(MAX(" . $serial_field . "), :serial_value)) FROM {" . $this->table . "}", [':serial_value' => (int) $serial_value]);
+          }
+        }
+      }
+    }
+
+    $options = $this->queryOptions;
+    if (!empty($table_information->sequences)) {
+      $options['sequence_name'] = $table_information->sequences[0];
+    }
+
+    // Re-initialize the values array so that we can re-use this query.
+    $this->insertValues = [];
+
+    // Create a savepoint so we can rollback a failed query. This is so we can
+    // mimic MySQL and SQLite transactions which don't fail if a single query
+    // fails. This is important for tables that are created on demand. For
+    // example, \Drupal\Core\Cache\DatabaseBackend.
+    $this->connection->addSavepoint();
+    try {
+      $stmt->execute(NULL, $options);
+      $this->connection->releaseSavepoint();
+      return $stmt->rowCount();
+    }
+    catch (\Exception $e) {
+      $this->connection->rollbackSavepoint();
+      $this->connection->exceptionHandler()->handleExecutionException($e, $stmt, [], $options);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __toString() {
+    // Create a sanitized comment string to prepend to the query.
+    $comments = $this->connection->makeComment($this->comments);
+
+    // 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);
+
+    $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
+
+    $values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
+    $query .= implode(', ', $values);
+
+    // Updating the unique / primary key is not necessary.
+    unset($insert_fields[$this->key]);
+
+    $update = [];
+    foreach ($insert_fields as $field) {
+      // The "excluded." prefix causes the field to refer to the value for field
+      // that would have been inserted had there been no conflict.
+      $update[] = "$field = EXCLUDED.$field";
+    }
+
+    $query .= ' ON CONFLICT (' . $this->connection->escapeField($this->key) . ') DO UPDATE SET ' . implode(', ', $update);
+
+    return $query;
+  }
+
+}
diff --git a/core/modules/sqlite/sqlite.info.yml b/core/modules/sqlite/sqlite.info.yml
new file mode 100644
index 000000000000..a5f55f4e5318
--- /dev/null
+++ b/core/modules/sqlite/sqlite.info.yml
@@ -0,0 +1,5 @@
+name: SQLite
+type: module
+description: 'Database driver for SQLite.'
+package: Core
+version: VERSION
diff --git a/core/modules/sqlite/sqlite.module b/core/modules/sqlite/sqlite.module
new file mode 100644
index 000000000000..4cfb9923aac9
--- /dev/null
+++ b/core/modules/sqlite/sqlite.module
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * @file
+ * The SQLite module provides the connection between Drupal and a SQLite database.
+ */
+
+use Drupal\Core\Routing\RouteMatchInterface;
+
+/**
+ * Implements hook_help().
+ */
+function sqlite_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    case 'help.page.sqlite':
+      $output = '';
+      $output .= '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('The SQLite module provides the connection between Drupal and a SQLite database. For more information, see the <a href=":sqlite">online documentation for the SQLite module</a>.', [':sqlite' => 'https://www.drupal.org/documentation/modules/sqlite']) . '</p>';
+      return $output;
+
+  }
+}
diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php b/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php
new file mode 100644
index 000000000000..5a1ab3b9d42b
--- /dev/null
+++ b/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php
@@ -0,0 +1,528 @@
+<?php
+
+namespace Drupal\sqlite\Driver\Database\sqlite;
+
+use Drupal\Core\Database\DatabaseNotFoundException;
+use Drupal\Core\Database\Connection as DatabaseConnection;
+use Drupal\Core\Database\StatementInterface;
+
+/**
+ * SQLite implementation of \Drupal\Core\Database\Connection.
+ */
+class Connection extends DatabaseConnection {
+
+  /**
+   * Error code for "Unable to open database file" error.
+   */
+  const DATABASE_NOT_FOUND = 14;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $statementClass = NULL;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $statementWrapperClass = NULL;
+
+  /**
+   * Whether or not the active transaction (if any) will be rolled back.
+   *
+   * @var bool
+   */
+  protected $willRollback;
+
+  /**
+   * A map of condition operators to SQLite operators.
+   *
+   * We don't want to override any of the defaults.
+   */
+  protected static $sqliteConditionOperatorMap = [
+    'LIKE' => ['postfix' => " ESCAPE '\\'"],
+    'NOT LIKE' => ['postfix' => " ESCAPE '\\'"],
+    'LIKE BINARY' => ['postfix' => " ESCAPE '\\'", 'operator' => 'GLOB'],
+    'NOT LIKE BINARY' => ['postfix' => " ESCAPE '\\'", 'operator' => 'NOT GLOB'],
+  ];
+
+  /**
+   * All databases attached to the current database.
+   *
+   * This is used to allow prefixes to be safely handled without locking the
+   * table.
+   *
+   * @var array
+   */
+  protected $attachedDatabases = [];
+
+  /**
+   * Whether or not a table has been dropped this request.
+   *
+   * The destructor will only try to get rid of unnecessary databases if there
+   * is potential of them being empty.
+   *
+   * This variable is set to public because Schema needs to
+   * access it. However, it should not be manually set.
+   *
+   * @var bool
+   */
+  public $tableDropped = FALSE;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $transactionalDDLSupport = TRUE;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $identifierQuotes = ['"', '"'];
+
+  /**
+   * Constructs a \Drupal\sqlite\Driver\Database\sqlite\Connection object.
+   */
+  public function __construct(\PDO $connection, array $connection_options) {
+    parent::__construct($connection, $connection_options);
+
+    // Attach one database for each registered prefix.
+    $prefixes = $this->prefixes;
+    foreach ($prefixes as &$prefix) {
+      // Empty prefix means query the main database -- no need to attach
+      // anything.
+      if ($prefix !== '') {
+        $this->attachDatabase($prefix);
+        // Add a ., so queries become prefix.table, which is proper syntax for
+        // querying an attached database.
+        $prefix .= '.';
+      }
+    }
+
+    // Regenerate the prefixes replacement table.
+    $this->setPrefix($prefixes);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function open(array &$connection_options = []) {
+    // Allow PDO options to be overridden.
+    $connection_options += [
+      'pdo' => [],
+    ];
+    $connection_options['pdo'] += [
+      \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
+      // Convert numeric values to strings when fetching.
+      \PDO::ATTR_STRINGIFY_FETCHES => TRUE,
+    ];
+
+    try {
+      $pdo = new \PDO('sqlite:' . $connection_options['database'], '', '', $connection_options['pdo']);
+    }
+    catch (\PDOException $e) {
+      if ($e->getCode() == static::DATABASE_NOT_FOUND) {
+        throw new DatabaseNotFoundException($e->getMessage(), $e->getCode(), $e);
+      }
+      // SQLite doesn't have a distinct error code for access denied, so don't
+      // deal with that case.
+      throw $e;
+    }
+
+    // Create functions needed by SQLite.
+    $pdo->sqliteCreateFunction('if', [__CLASS__, 'sqlFunctionIf']);
+    $pdo->sqliteCreateFunction('greatest', [__CLASS__, 'sqlFunctionGreatest']);
+    $pdo->sqliteCreateFunction('least', [__CLASS__, 'sqlFunctionLeast']);
+    $pdo->sqliteCreateFunction('pow', 'pow', 2);
+    $pdo->sqliteCreateFunction('exp', 'exp', 1);
+    $pdo->sqliteCreateFunction('length', 'strlen', 1);
+    $pdo->sqliteCreateFunction('md5', 'md5', 1);
+    $pdo->sqliteCreateFunction('concat', [__CLASS__, 'sqlFunctionConcat']);
+    $pdo->sqliteCreateFunction('concat_ws', [__CLASS__, 'sqlFunctionConcatWs']);
+    $pdo->sqliteCreateFunction('substring', [__CLASS__, 'sqlFunctionSubstring'], 3);
+    $pdo->sqliteCreateFunction('substring_index', [__CLASS__, 'sqlFunctionSubstringIndex'], 3);
+    $pdo->sqliteCreateFunction('rand', [__CLASS__, 'sqlFunctionRand']);
+    $pdo->sqliteCreateFunction('regexp', [__CLASS__, 'sqlFunctionRegexp']);
+
+    // SQLite does not support the LIKE BINARY operator, so we overload the
+    // non-standard GLOB operator for case-sensitive matching. Another option
+    // would have been to override another non-standard operator, MATCH, but
+    // that does not support the NOT keyword prefix.
+    $pdo->sqliteCreateFunction('glob', [__CLASS__, 'sqlFunctionLikeBinary']);
+
+    // Create a user-space case-insensitive collation with UTF-8 support.
+    $pdo->sqliteCreateCollation('NOCASE_UTF8', ['Drupal\Component\Utility\Unicode', 'strcasecmp']);
+
+    // Set SQLite init_commands if not already defined. Enable the Write-Ahead
+    // Logging (WAL) for SQLite. See https://www.drupal.org/node/2348137 and
+    // https://www.sqlite.org/wal.html.
+    $connection_options += [
+      'init_commands' => [],
+    ];
+    $connection_options['init_commands'] += [
+      'wal' => "PRAGMA journal_mode=WAL",
+    ];
+
+    // Execute sqlite init_commands.
+    if (isset($connection_options['init_commands'])) {
+      $pdo->exec(implode('; ', $connection_options['init_commands']));
+    }
+
+    return $pdo;
+  }
+
+  /**
+   * Destructor for the SQLite connection.
+   *
+   * We prune empty databases on destruct, but only if tables have been
+   * dropped. This is especially needed when running the test suite, which
+   * creates and destroy databases several times in a row.
+   */
+  public function __destruct() {
+    if ($this->tableDropped && !empty($this->attachedDatabases)) {
+      foreach ($this->attachedDatabases as $prefix) {
+        // Check if the database is now empty, ignore the internal SQLite tables.
+        try {
+          $count = $this->query('SELECT COUNT(*) FROM ' . $prefix . '.sqlite_master WHERE type = :type AND name NOT LIKE :pattern', [':type' => 'table', ':pattern' => 'sqlite_%'])->fetchField();
+
+          // We can prune the database file if it doesn't have any tables.
+          if ($count == 0 && $this->connectionOptions['database'] != ':memory:' && file_exists($this->connectionOptions['database'] . '-' . $prefix)) {
+            // Detach the database.
+            $this->query('DETACH DATABASE :schema', [':schema' => $prefix]);
+            // Destroy the database file.
+            unlink($this->connectionOptions['database'] . '-' . $prefix);
+          }
+        }
+        catch (\Exception $e) {
+          // Ignore the exception and continue. There is nothing we can do here
+          // to report the error or fail safe.
+        }
+      }
+    }
+    parent::__destruct();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function attachDatabase(string $database): void {
+    // Only attach the database once.
+    if (!isset($this->attachedDatabases[$database])) {
+      // In memory database use ':memory:' as database name. According to
+      // http://www.sqlite.org/inmemorydb.html it will open a unique database so
+      // attaching it twice is not a problem.
+      $database_file = $this->connectionOptions['database'] !== ':memory:' ? $this->connectionOptions['database'] . '-' . $database : $this->connectionOptions['database'];
+      $this->query('ATTACH DATABASE :database_file AS :database', [':database_file' => $database_file, ':database' => $database]);
+      $this->attachedDatabases[$database] = $database;
+    }
+  }
+
+  /**
+   * Gets all the attached databases.
+   *
+   * @return array
+   *   An array of attached database names.
+   *
+   * @see \Drupal\sqlite\Driver\Database\sqlite\Connection::__construct()
+   */
+  public function getAttachedDatabases() {
+    return $this->attachedDatabases;
+  }
+
+  /**
+   * SQLite compatibility implementation for the IF() SQL function.
+   */
+  public static function sqlFunctionIf($condition, $expr1, $expr2 = NULL) {
+    return $condition ? $expr1 : $expr2;
+  }
+
+  /**
+   * SQLite compatibility implementation for the GREATEST() SQL function.
+   */
+  public static function sqlFunctionGreatest() {
+    $args = func_get_args();
+    foreach ($args as $v) {
+      if (!isset($v)) {
+        unset($args);
+      }
+    }
+    if (count($args)) {
+      return max($args);
+    }
+    else {
+      return NULL;
+    }
+  }
+
+  /**
+   * SQLite compatibility implementation for the LEAST() SQL function.
+   */
+  public static function sqlFunctionLeast() {
+    // Remove all NULL, FALSE and empty strings values but leaves 0 (zero) values.
+    $values = array_filter(func_get_args(), 'strlen');
+
+    return count($values) < 1 ? NULL : min($values);
+  }
+
+  /**
+   * SQLite compatibility implementation for the CONCAT() SQL function.
+   */
+  public static function sqlFunctionConcat() {
+    $args = func_get_args();
+    return implode('', $args);
+  }
+
+  /**
+   * SQLite compatibility implementation for the CONCAT_WS() SQL function.
+   *
+   * @see http://dev.mysql.com/doc/refman/5.6/en/string-functions.html#function_concat-ws
+   */
+  public static function sqlFunctionConcatWs() {
+    $args = func_get_args();
+    $separator = array_shift($args);
+    // If the separator is NULL, the result is NULL.
+    if ($separator === FALSE || is_null($separator)) {
+      return NULL;
+    }
+    // Skip any NULL values after the separator argument.
+    $args = array_filter($args, function ($value) {
+      return !is_null($value);
+    });
+    return implode($separator, $args);
+  }
+
+  /**
+   * SQLite compatibility implementation for the SUBSTRING() SQL function.
+   */
+  public static function sqlFunctionSubstring($string, $from, $length) {
+    return substr($string, $from - 1, $length);
+  }
+
+  /**
+   * SQLite compatibility implementation for the SUBSTRING_INDEX() SQL function.
+   */
+  public static function sqlFunctionSubstringIndex($string, $delimiter, $count) {
+    // If string is empty, simply return an empty string.
+    if (empty($string)) {
+      return '';
+    }
+    $end = 0;
+    for ($i = 0; $i < $count; $i++) {
+      $end = strpos($string, $delimiter, $end + 1);
+      if ($end === FALSE) {
+        $end = strlen($string);
+      }
+    }
+    return substr($string, 0, $end);
+  }
+
+  /**
+   * SQLite compatibility implementation for the RAND() SQL function.
+   */
+  public static function sqlFunctionRand($seed = NULL) {
+    if (isset($seed)) {
+      mt_srand($seed);
+    }
+    return mt_rand() / mt_getrandmax();
+  }
+
+  /**
+   * SQLite compatibility implementation for the REGEXP SQL operator.
+   *
+   * The REGEXP operator is natively known, but not implemented by default.
+   *
+   * @see http://www.sqlite.org/lang_expr.html#regexp
+   */
+  public static function sqlFunctionRegexp($pattern, $subject) {
+    // preg_quote() cannot be used here, since $pattern may contain reserved
+    // regular expression characters already (such as ^, $, etc). Therefore,
+    // use a rare character as PCRE delimiter.
+    $pattern = '#' . addcslashes($pattern, '#') . '#i';
+    return preg_match($pattern, $subject);
+  }
+
+  /**
+   * SQLite compatibility implementation for the LIKE BINARY SQL operator.
+   *
+   * SQLite supports case-sensitive LIKE operations through the
+   * 'case_sensitive_like' PRAGMA statement, but only for ASCII characters, so
+   * we have to provide our own implementation with UTF-8 support.
+   *
+   * @see https://sqlite.org/pragma.html#pragma_case_sensitive_like
+   * @see https://sqlite.org/lang_expr.html#like
+   */
+  public static function sqlFunctionLikeBinary($pattern, $subject) {
+    // Replace the SQL LIKE wildcard meta-characters with the equivalent regular
+    // expression meta-characters and escape the delimiter that will be used for
+    // matching.
+    $pattern = str_replace(['%', '_'], ['.*?', '.'], preg_quote($pattern, '/'));
+    return preg_match('/^' . $pattern . '$/', $subject);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function prepare($statement, array $driver_options = []) {
+    @trigger_error('Connection::prepare() is deprecated in drupal:9.1.0 and is removed from drupal:10.0.0. Database drivers should instantiate \PDOStatement objects by calling \PDO::prepare in their Connection::prepareStatement method instead. \PDO::prepare should not be called outside of driver code. See https://www.drupal.org/node/3137786', E_USER_DEPRECATED);
+    return new Statement($this->connection, $this, $statement, $driver_options);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function handleQueryException(\PDOException $e, $query, array $args = [], $options = []) {
+    // The database schema might be changed by another process in between the
+    // time that the statement was prepared and the time the statement was run
+    // (e.g. usually happens when running tests). In this case, we need to
+    // re-run the query.
+    // @see http://www.sqlite.org/faq.html#q15
+    // @see http://www.sqlite.org/rescode.html#schema
+    if (!empty($e->errorInfo[1]) && $e->errorInfo[1] === 17) {
+      @trigger_error('Connection::handleQueryException() is deprecated in drupal:9.2.0 and is removed in drupal:10.0.0. Get a handler through $this->exceptionHandler() instead, and use one of its methods. See https://www.drupal.org/node/3187222', E_USER_DEPRECATED);
+      return $this->query($query, $args, $options);
+    }
+
+    parent::handleQueryException($e, $query, $args, $options);
+  }
+
+  public function queryRange($query, $from, $count, array $args = [], array $options = []) {
+    return $this->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function queryTemporary($query, array $args = [], array $options = []) {
+    @trigger_error('Connection::queryTemporary() is deprecated in drupal:9.3.0 and is removed from drupal:10.0.0. There is no replacement. See https://www.drupal.org/node/3211781', E_USER_DEPRECATED);
+    // Generate a new temporary table name and protect it from prefixing.
+    // SQLite requires that temporary tables to be non-qualified.
+    $tablename = $this->generateTemporaryTableName();
+    $prefixes = $this->prefixes;
+    $prefixes[$tablename] = '';
+    $this->setPrefix($prefixes);
+
+    $this->query('CREATE TEMPORARY TABLE ' . $tablename . ' AS ' . $query, $args, $options);
+    return $tablename;
+  }
+
+  public function driver() {
+    return 'sqlite';
+  }
+
+  public function databaseType() {
+    return 'sqlite';
+  }
+
+  /**
+   * Overrides \Drupal\Core\Database\Connection::createDatabase().
+   *
+   * @param string $database
+   *   The name of the database to create.
+   *
+   * @throws \Drupal\Core\Database\DatabaseNotFoundException
+   */
+  public function createDatabase($database) {
+    // Verify the database is writable.
+    $db_directory = new \SplFileInfo(dirname($database));
+    if (!$db_directory->isDir() && !\Drupal::service('file_system')->mkdir($db_directory->getPathName(), 0755, TRUE)) {
+      throw new DatabaseNotFoundException('Unable to create database directory ' . $db_directory->getPathName());
+    }
+  }
+
+  public function mapConditionOperator($operator) {
+    return static::$sqliteConditionOperatorMap[$operator] ?? NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function prepareStatement(string $query, array $options, bool $allow_row_count = FALSE): StatementInterface {
+    try {
+      $query = $this->preprocessStatement($query, $options);
+      $statement = new Statement($this->connection, $this, $query, $options['pdo'] ?? [], $allow_row_count);
+    }
+    catch (\Exception $e) {
+      $this->exceptionHandler()->handleStatementException($e, $query, $options);
+    }
+    return $statement;
+  }
+
+  public function nextId($existing_id = 0) {
+    $this->startTransaction();
+    // We can safely use literal queries here instead of the slower query
+    // builder because if a given database breaks here then it can simply
+    // override nextId. However, this is unlikely as we deal with short strings
+    // and integers and no known databases require special handling for those
+    // simple cases. If another transaction wants to write the same row, it will
+    // wait until this transaction commits.
+    $stmt = $this->prepareStatement('UPDATE {sequences} SET [value] = GREATEST([value], :existing_id) + 1', [], TRUE);
+    $args = [':existing_id' => $existing_id];
+    try {
+      $stmt->execute($args);
+    }
+    catch (\Exception $e) {
+      $this->exceptionHandler()->handleExecutionException($e, $stmt, $args, []);
+    }
+    if ($stmt->rowCount() === 0) {
+      $this->query('INSERT INTO {sequences} ([value]) VALUES (:existing_id + 1)', $args);
+    }
+    // The transaction gets committed when the transaction object gets destroyed
+    // because it gets out of scope.
+    return $this->query('SELECT [value] FROM {sequences}')->fetchField();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFullQualifiedTableName($table) {
+    $prefix = $this->tablePrefix($table);
+
+    // Don't include the SQLite database file name as part of the table name.
+    return $prefix . $table;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createConnectionOptionsFromUrl($url, $root) {
+    $database = parent::createConnectionOptionsFromUrl($url, $root);
+
+    // A SQLite database path with two leading slashes indicates a system path.
+    // Otherwise the path is relative to the Drupal root.
+    $url_components = parse_url($url);
+    if ($url_components['path'][0] === '/') {
+      $url_components['path'] = substr($url_components['path'], 1);
+    }
+    if ($url_components['path'][0] === '/' || $url_components['path'] === ':memory:') {
+      $database['database'] = $url_components['path'];
+    }
+    else {
+      $database['database'] = $root . '/' . $url_components['path'];
+    }
+
+    // User credentials and system port are irrelevant for SQLite.
+    unset(
+      $database['username'],
+      $database['password'],
+      $database['port']
+    );
+
+    return $database;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createUrlFromConnectionOptions(array $connection_options) {
+    if (!isset($connection_options['driver'], $connection_options['database'])) {
+      throw new \InvalidArgumentException("As a minimum, the connection options array must contain at least the 'driver' and 'database' keys");
+    }
+
+    $db_url = 'sqlite://localhost/' . $connection_options['database'] . '?module=sqlite';
+
+    if (isset($connection_options['prefix']) && $connection_options['prefix'] !== '') {
+      $db_url .= '#' . $connection_options['prefix'];
+    }
+
+    return $db_url;
+  }
+
+}
diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Insert.php b/core/modules/sqlite/src/Driver/Database/sqlite/Insert.php
new file mode 100644
index 000000000000..d1cc245e273e
--- /dev/null
+++ b/core/modules/sqlite/src/Driver/Database/sqlite/Insert.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace Drupal\sqlite\Driver\Database\sqlite;
+
+use Drupal\Core\Database\Query\Insert as QueryInsert;
+
+/**
+ * SQLite implementation of \Drupal\Core\Database\Query\Insert.
+ *
+ * We ignore all the default fields and use the clever SQLite syntax:
+ *   INSERT INTO table DEFAULT VALUES
+ * for degenerated "default only" queries.
+ */
+class Insert extends QueryInsert {
+
+  public function execute() {
+    if (!$this->preExecute()) {
+      return NULL;
+    }
+    if (count($this->insertFields) || !empty($this->fromQuery)) {
+      return parent::execute();
+    }
+    else {
+      return $this->connection->query('INSERT INTO {' . $this->table . '} DEFAULT VALUES', [], $this->queryOptions);
+    }
+  }
+
+  public function __toString() {
+    // Create a sanitized comment string to prepend to the query.
+    $comments = $this->connection->makeComment($this->comments);
+
+    // Produce as many generic placeholders as necessary.
+    $placeholders = [];
+    if (!empty($this->insertFields)) {
+      $placeholders = array_fill(0, count($this->insertFields), '?');
+    }
+
+    $insert_fields = array_map(function ($field) {
+      return $this->connection->escapeField($field);
+    }, $this->insertFields);
+
+    // If we're selecting from a SelectQuery, finish building the query and
+    // pass it back, as any remaining options are irrelevant.
+    if (!empty($this->fromQuery)) {
+      $insert_fields_string = $insert_fields ? ' (' . implode(', ', $insert_fields) . ') ' : ' ';
+      return $comments . 'INSERT INTO {' . $this->table . '}' . $insert_fields_string . $this->fromQuery;
+    }
+
+    return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES (' . implode(', ', $placeholders) . ')';
+  }
+
+}
diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Install/Tasks.php b/core/modules/sqlite/src/Driver/Database/sqlite/Install/Tasks.php
new file mode 100644
index 000000000000..4d262a5ac4a0
--- /dev/null
+++ b/core/modules/sqlite/src/Driver/Database/sqlite/Install/Tasks.php
@@ -0,0 +1,115 @@
+<?php
+
+namespace Drupal\sqlite\Driver\Database\sqlite\Install;
+
+use Drupal\Core\Database\Database;
+use Drupal\sqlite\Driver\Database\sqlite\Connection;
+use Drupal\Core\Database\DatabaseNotFoundException;
+use Drupal\Core\Database\Install\Tasks as InstallTasks;
+
+/**
+ * Specifies installation tasks for SQLite databases.
+ */
+class Tasks extends InstallTasks {
+
+  /**
+   * Minimum required SQLite version.
+   *
+   * Use to build sqlite library with json1 option for JSON datatype support.
+   * @see https://www.sqlite.org/json1.html
+   */
+  const SQLITE_MINIMUM_VERSION = '3.26';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $pdoDriver = 'sqlite';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function name() {
+    return t('SQLite');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function minimumVersion() {
+    return static::SQLITE_MINIMUM_VERSION;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormOptions(array $database) {
+    $form = parent::getFormOptions($database);
+
+    // Remove the options that only apply to client/server style databases.
+    unset($form['username'], $form['password'], $form['advanced_options']['host'], $form['advanced_options']['port']);
+
+    // Make the text more accurate for SQLite.
+    $form['database']['#title'] = t('Database file');
+    $form['database']['#description'] = t('The absolute path to the file where @drupal data will be stored. This must be writable by the web server and should exist outside of the web root.', ['@drupal' => drupal_install_profile_distribution_name()]);
+    $default_database = \Drupal::getContainer()->getParameter('site.path') . '/files/.ht.sqlite';
+    $form['database']['#default_value'] = empty($database['database']) ? $default_database : $database['database'];
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function connect() {
+    try {
+      // This doesn't actually test the connection.
+      Database::setActiveConnection();
+      // Now actually do a check.
+      Database::getConnection();
+      $this->pass('Drupal can CONNECT to the database ok.');
+    }
+    catch (\Exception $e) {
+      // Attempt to create the database if it is not found.
+      if ($e->getCode() == Connection::DATABASE_NOT_FOUND) {
+        // Remove the database string from connection info.
+        $connection_info = Database::getConnectionInfo();
+        $database = $connection_info['default']['database'];
+
+        // We cannot use \Drupal::service('file_system')->getTempDirectory()
+        // here because we haven't yet successfully connected to the database.
+        $connection_info['default']['database'] = \Drupal::service('file_system')->tempnam(sys_get_temp_dir(), 'sqlite');
+
+        // In order to change the Database::$databaseInfo array, need to remove
+        // the active connection, then re-add it with the new info.
+        Database::removeConnection('default');
+        Database::addConnectionInfo('default', 'default', $connection_info['default']);
+
+        try {
+          Database::getConnection()->createDatabase($database);
+          Database::closeConnection();
+
+          // Now, restore the database config.
+          Database::removeConnection('default');
+          $connection_info['default']['database'] = $database;
+          Database::addConnectionInfo('default', 'default', $connection_info['default']);
+
+          // Check the database connection.
+          Database::getConnection();
+          $this->pass('Drupal can CONNECT to the database ok.');
+        }
+        catch (DatabaseNotFoundException $e) {
+          // Still no dice; probably a permission issue. Raise the error to the
+          // installer.
+          $this->fail(t('Failed to open or create database file %database. The database engine reports the following message when attempting to create the database: %error.', ['%database' => $database, '%error' => $e->getMessage()]));
+        }
+      }
+      else {
+        // Database connection failed for some other reason than a non-existent
+        // database.
+        $this->fail(t('Failed to connect to database. The database engine reports the following message: %error.<ul><li>Does the database file exist?</li><li>Does web server have permission to write to the database file?</li>Does the web server have permission to write to the directory the database file should be created in?</li></ul>', ['%error' => $e->getMessage()]));
+        return FALSE;
+      }
+    }
+    return TRUE;
+  }
+
+}
diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Schema.php b/core/modules/sqlite/src/Driver/Database/sqlite/Schema.php
new file mode 100644
index 000000000000..dd88410715f3
--- /dev/null
+++ b/core/modules/sqlite/src/Driver/Database/sqlite/Schema.php
@@ -0,0 +1,837 @@
+<?php
+
+namespace Drupal\sqlite\Driver\Database\sqlite;
+
+use Drupal\Core\Database\SchemaObjectExistsException;
+use Drupal\Core\Database\SchemaObjectDoesNotExistException;
+use Drupal\Core\Database\Schema as DatabaseSchema;
+
+/**
+ * @ingroup schemaapi
+ * @{
+ */
+
+/**
+ * SQLite implementation of \Drupal\Core\Database\Schema.
+ */
+class Schema extends DatabaseSchema {
+
+  /**
+   * Override DatabaseSchema::$defaultSchema.
+   *
+   * @var string
+   */
+  protected $defaultSchema = 'main';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function tableExists($table) {
+    $info = $this->getPrefixInfo($table);
+
+    // Don't use {} around sqlite_master table.
+    return (bool) $this->connection->query('SELECT 1 FROM ' . $info['schema'] . '.sqlite_master WHERE type = :type AND name = :name', [':type' => 'table', ':name' => $info['table']])->fetchField();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fieldExists($table, $column) {
+    $schema = $this->introspectSchema($table);
+    return !empty($schema['fields'][$column]);
+  }
+
+  /**
+   * Generate SQL to create a new table from a Drupal schema definition.
+   *
+   * @param $name
+   *   The name of the table to create.
+   * @param $table
+   *   A Schema API table definition array.
+   *
+   * @return
+   *   An array of SQL statements to create the table.
+   */
+  public function createTableSql($name, $table) {
+    if (!empty($table['primary key']) && is_array($table['primary key'])) {
+      $this->ensureNotNullPrimaryKey($table['primary key'], $table['fields']);
+    }
+
+    $sql = [];
+    $sql[] = "CREATE TABLE {" . $name . "} (\n" . $this->createColumnsSql($name, $table) . "\n)\n";
+    return array_merge($sql, $this->createIndexSql($name, $table));
+  }
+
+  /**
+   * Build the SQL expression for indexes.
+   */
+  protected function createIndexSql($tablename, $schema) {
+    $sql = [];
+    $info = $this->getPrefixInfo($tablename);
+    if (!empty($schema['unique keys'])) {
+      foreach ($schema['unique keys'] as $key => $fields) {
+        $sql[] = 'CREATE UNIQUE INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $key . ' ON ' . $info['table'] . ' (' . $this->createKeySql($fields) . ")\n";
+      }
+    }
+    if (!empty($schema['indexes'])) {
+      foreach ($schema['indexes'] as $key => $fields) {
+        $sql[] = 'CREATE INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $key . ' ON ' . $info['table'] . ' (' . $this->createKeySql($fields) . ")\n";
+      }
+    }
+    return $sql;
+  }
+
+  /**
+   * Build the SQL expression for creating columns.
+   */
+  protected function createColumnsSql($tablename, $schema) {
+    $sql_array = [];
+
+    // Add the SQL statement for each field.
+    foreach ($schema['fields'] as $name => $field) {
+      if (isset($field['type']) && $field['type'] == 'serial') {
+        if (isset($schema['primary key']) && ($key = array_search($name, $schema['primary key'])) !== FALSE) {
+          unset($schema['primary key'][$key]);
+        }
+      }
+      $sql_array[] = $this->createFieldSql($name, $this->processField($field));
+    }
+
+    // Process keys.
+    if (!empty($schema['primary key'])) {
+      $sql_array[] = " PRIMARY KEY (" . $this->createKeySql($schema['primary key']) . ")";
+    }
+
+    return implode(", \n", $sql_array);
+  }
+
+  /**
+   * Build the SQL expression for keys.
+   */
+  protected function createKeySql($fields) {
+    $return = [];
+    foreach ($fields as $field) {
+      if (is_array($field)) {
+        $return[] = $field[0];
+      }
+      else {
+        $return[] = $field;
+      }
+    }
+    return implode(', ', $return);
+  }
+
+  /**
+   * Set database-engine specific properties for a field.
+   *
+   * @param $field
+   *   A field description array, as specified in the schema documentation.
+   */
+  protected function processField($field) {
+    if (!isset($field['size'])) {
+      $field['size'] = 'normal';
+    }
+
+    // Set the correct database-engine specific datatype.
+    // In case one is already provided, force it to uppercase.
+    if (isset($field['sqlite_type'])) {
+      $field['sqlite_type'] = mb_strtoupper($field['sqlite_type']);
+    }
+    else {
+      $map = $this->getFieldTypeMap();
+      $field['sqlite_type'] = $map[$field['type'] . ':' . $field['size']];
+
+      // Numeric fields with a specified scale have to be stored as floats.
+      if ($field['sqlite_type'] === 'NUMERIC' && isset($field['scale'])) {
+        $field['sqlite_type'] = 'FLOAT';
+      }
+    }
+
+    if (isset($field['type']) && $field['type'] == 'serial') {
+      $field['auto_increment'] = TRUE;
+    }
+
+    return $field;
+  }
+
+  /**
+   * Create an SQL string for a field to be used in table creation or alteration.
+   *
+   * Before passing a field out of a schema definition into this function it has
+   * to be processed by self::processField().
+   *
+   * @param $name
+   *   Name of the field.
+   * @param $spec
+   *   The field specification, as per the schema data structure format.
+   */
+  protected function createFieldSql($name, $spec) {
+    $name = $this->connection->escapeField($name);
+    if (!empty($spec['auto_increment'])) {
+      $sql = $name . " INTEGER PRIMARY KEY AUTOINCREMENT";
+      if (!empty($spec['unsigned'])) {
+        $sql .= ' CHECK (' . $name . '>= 0)';
+      }
+    }
+    else {
+      $sql = $name . ' ' . $spec['sqlite_type'];
+
+      if (in_array($spec['sqlite_type'], ['VARCHAR', 'TEXT'])) {
+        if (isset($spec['length'])) {
+          $sql .= '(' . $spec['length'] . ')';
+        }
+
+        if (isset($spec['binary']) && $spec['binary'] === FALSE) {
+          $sql .= ' COLLATE NOCASE_UTF8';
+        }
+      }
+
+      if (isset($spec['not null'])) {
+        if ($spec['not null']) {
+          $sql .= ' NOT NULL';
+        }
+        else {
+          $sql .= ' NULL';
+        }
+      }
+
+      if (!empty($spec['unsigned'])) {
+        $sql .= ' CHECK (' . $name . '>= 0)';
+      }
+
+      if (isset($spec['default'])) {
+        if (is_string($spec['default'])) {
+          $spec['default'] = $this->connection->quote($spec['default']);
+        }
+        $sql .= ' DEFAULT ' . $spec['default'];
+      }
+
+      if (empty($spec['not null']) && !isset($spec['default'])) {
+        $sql .= ' DEFAULT NULL';
+      }
+    }
+    return $sql;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFieldTypeMap() {
+    // Put :normal last so it gets preserved by array_flip. This makes
+    // it much easier for modules (such as schema.module) to map
+    // database types back into schema types.
+    // $map does not use drupal_static as its value never changes.
+    static $map = [
+      'varchar_ascii:normal' => 'VARCHAR',
+
+      'varchar:normal'  => 'VARCHAR',
+      'char:normal'     => 'CHAR',
+
+      'text:tiny'       => 'TEXT',
+      'text:small'      => 'TEXT',
+      'text:medium'     => 'TEXT',
+      'text:big'        => 'TEXT',
+      'text:normal'     => 'TEXT',
+
+      'serial:tiny'     => 'INTEGER',
+      'serial:small'    => 'INTEGER',
+      'serial:medium'   => 'INTEGER',
+      'serial:big'      => 'INTEGER',
+      'serial:normal'   => 'INTEGER',
+
+      'int:tiny'        => 'INTEGER',
+      'int:small'       => 'INTEGER',
+      'int:medium'      => 'INTEGER',
+      'int:big'         => 'INTEGER',
+      'int:normal'      => 'INTEGER',
+
+      'float:tiny'      => 'FLOAT',
+      'float:small'     => 'FLOAT',
+      'float:medium'    => 'FLOAT',
+      'float:big'       => 'FLOAT',
+      'float:normal'    => 'FLOAT',
+
+      'numeric:normal'  => 'NUMERIC',
+
+      'blob:big'        => 'BLOB',
+      'blob:normal'     => 'BLOB',
+    ];
+    return $map;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function renameTable($table, $new_name) {
+    if (!$this->tableExists($table)) {
+      throw new SchemaObjectDoesNotExistException("Cannot rename '$table' to '$new_name': table '$table' doesn't exist.");
+    }
+    if ($this->tableExists($new_name)) {
+      throw new SchemaObjectExistsException("Cannot rename '$table' to '$new_name': table '$new_name' already exists.");
+    }
+
+    $schema = $this->introspectSchema($table);
+
+    // SQLite doesn't allow you to rename tables outside of the current
+    // database. So the syntax '... RENAME TO database.table' would fail.
+    // So we must determine the full table name here rather than surrounding
+    // the table with curly braces in case the db_prefix contains a reference
+    // to a database outside of our existing database.
+    $info = $this->getPrefixInfo($new_name);
+    $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO ' . $info['table']);
+
+    // Drop the indexes, there is no RENAME INDEX command in SQLite.
+    if (!empty($schema['unique keys'])) {
+      foreach ($schema['unique keys'] as $key => $fields) {
+        $this->dropIndex($table, $key);
+      }
+    }
+    if (!empty($schema['indexes'])) {
+      foreach ($schema['indexes'] as $index => $fields) {
+        $this->dropIndex($table, $index);
+      }
+    }
+
+    // Recreate the indexes.
+    $statements = $this->createIndexSql($new_name, $schema);
+    foreach ($statements as $statement) {
+      $this->connection->query($statement);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function dropTable($table) {
+    if (!$this->tableExists($table)) {
+      return FALSE;
+    }
+    $this->connection->tableDropped = TRUE;
+    $this->connection->query('DROP TABLE {' . $table . '}');
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addField($table, $field, $specification, $keys_new = []) {
+    if (!$this->tableExists($table)) {
+      throw new SchemaObjectDoesNotExistException("Cannot add field '$table.$field': table doesn't exist.");
+    }
+    if ($this->fieldExists($table, $field)) {
+      throw new SchemaObjectExistsException("Cannot add field '$table.$field': field already exists.");
+    }
+    if (isset($keys_new['primary key']) && in_array($field, $keys_new['primary key'], TRUE)) {
+      $this->ensureNotNullPrimaryKey($keys_new['primary key'], [$field => $specification]);
+    }
+
+    // SQLite doesn't have a full-featured ALTER TABLE statement. It only
+    // supports adding new fields to a table, in some simple cases. In most
+    // cases, we have to create a new table and copy the data over.
+    if (empty($keys_new) && (empty($specification['not null']) || isset($specification['default']))) {
+      // When we don't have to create new keys and we are not creating a
+      // NOT NULL column without a default value, we can use the quicker version.
+      $query = 'ALTER TABLE {' . $table . '} ADD ' . $this->createFieldSql($field, $this->processField($specification));
+      $this->connection->query($query);
+
+      // Apply the initial value if set.
+      if (isset($specification['initial_from_field'])) {
+        if (isset($specification['initial'])) {
+          $expression = 'COALESCE(' . $specification['initial_from_field'] . ', :default_initial_value)';
+          $arguments = [':default_initial_value' => $specification['initial']];
+        }
+        else {
+          $expression = $specification['initial_from_field'];
+          $arguments = [];
+        }
+        $this->connection->update($table)
+          ->expression($field, $expression, $arguments)
+          ->execute();
+      }
+      elseif (isset($specification['initial'])) {
+        $this->connection->update($table)
+          ->fields([$field => $specification['initial']])
+          ->execute();
+      }
+    }
+    else {
+      // We cannot add the field directly. Use the slower table alteration
+      // method, starting from the old schema.
+      $old_schema = $this->introspectSchema($table);
+      $new_schema = $old_schema;
+
+      // Add the new field.
+      $new_schema['fields'][$field] = $specification;
+
+      // Build the mapping between the old fields and the new fields.
+      $mapping = [];
+      if (isset($specification['initial_from_field'])) {
+        // If we have an initial value, copy it over.
+        if (isset($specification['initial'])) {
+          $expression = 'COALESCE(' . $specification['initial_from_field'] . ', :default_initial_value)';
+          $arguments = [':default_initial_value' => $specification['initial']];
+        }
+        else {
+          $expression = $specification['initial_from_field'];
+          $arguments = [];
+        }
+        $mapping[$field] = [
+          'expression' => $expression,
+          'arguments' => $arguments,
+        ];
+      }
+      elseif (isset($specification['initial'])) {
+        // If we have an initial value, copy it over.
+        $mapping[$field] = [
+          'expression' => ':newfieldinitial',
+          'arguments' => [':newfieldinitial' => $specification['initial']],
+        ];
+      }
+      else {
+        // Else use the default of the field.
+        $mapping[$field] = NULL;
+      }
+
+      // Add the new indexes.
+      $new_schema = array_merge($new_schema, $keys_new);
+
+      $this->alterTable($table, $old_schema, $new_schema, $mapping);
+    }
+  }
+
+  /**
+   * Create a table with a new schema containing the old content.
+   *
+   * As SQLite does not support ALTER TABLE (with a few exceptions) it is
+   * necessary to create a new table and copy over the old content.
+   *
+   * @param $table
+   *   Name of the table to be altered.
+   * @param $old_schema
+   *   The old schema array for the table.
+   * @param $new_schema
+   *   The new schema array for the table.
+   * @param $mapping
+   *   An optional mapping between the fields of the old specification and the
+   *   fields of the new specification. An associative array, whose keys are
+   *   the fields of the new table, and values can take two possible forms:
+   *     - a simple string, which is interpreted as the name of a field of the
+   *       old table,
+   *     - an associative array with two keys 'expression' and 'arguments',
+   *       that will be used as an expression field.
+   */
+  protected function alterTable($table, $old_schema, $new_schema, array $mapping = []) {
+    $i = 0;
+    do {
+      $new_table = $table . '_' . $i++;
+    } while ($this->tableExists($new_table));
+
+    $this->createTable($new_table, $new_schema);
+
+    // Build a SQL query to migrate the data from the old table to the new.
+    $select = $this->connection->select($table);
+
+    // Complete the mapping.
+    $possible_keys = array_keys($new_schema['fields']);
+    $mapping += array_combine($possible_keys, $possible_keys);
+
+    // Now add the fields.
+    foreach ($mapping as $field_alias => $field_source) {
+      // Just ignore this field (ie. use its default value).
+      if (!isset($field_source)) {
+        continue;
+      }
+
+      if (is_array($field_source)) {
+        $select->addExpression($field_source['expression'], $field_alias, $field_source['arguments']);
+      }
+      else {
+        $select->addField($table, $field_source, $field_alias);
+      }
+    }
+
+    // Execute the data migration query.
+    $this->connection->insert($new_table)
+      ->from($select)
+      ->execute();
+
+    $old_count = $this->connection->query('SELECT COUNT(*) FROM {' . $table . '}')->fetchField();
+    $new_count = $this->connection->query('SELECT COUNT(*) FROM {' . $new_table . '}')->fetchField();
+    if ($old_count == $new_count) {
+      $this->dropTable($table);
+      $this->renameTable($new_table, $table);
+    }
+  }
+
+  /**
+   * Find out the schema of a table.
+   *
+   * This function uses introspection methods provided by the database to
+   * create a schema array. This is useful, for example, during update when
+   * the old schema is not available.
+   *
+   * @param $table
+   *   Name of the table.
+   *
+   * @return
+   *   An array representing the schema.
+   *
+   * @throws \Exception
+   *   If a column of the table could not be parsed.
+   */
+  protected function introspectSchema($table) {
+    $mapped_fields = array_flip($this->getFieldTypeMap());
+    $schema = [
+      'fields' => [],
+      'primary key' => [],
+      'unique keys' => [],
+      'indexes' => [],
+    ];
+
+    $info = $this->getPrefixInfo($table);
+    $result = $this->connection->query('PRAGMA ' . $info['schema'] . '.table_info(' . $info['table'] . ')');
+    foreach ($result as $row) {
+      if (preg_match('/^([^(]+)\((.*)\)$/', $row->type, $matches)) {
+        $type = $matches[1];
+        $length = $matches[2];
+      }
+      else {
+        $type = $row->type;
+        $length = NULL;
+      }
+      if (isset($mapped_fields[$type])) {
+        [$type, $size] = explode(':', $mapped_fields[$type]);
+        $schema['fields'][$row->name] = [
+          'type' => $type,
+          'size' => $size,
+          'not null' => !empty($row->notnull) || $row->pk !== "0",
+        ];
+        if ($length) {
+          $schema['fields'][$row->name]['length'] = $length;
+        }
+
+        // Convert the default into a properly typed value.
+        if ($row->dflt_value === 'NULL') {
+          $schema['fields'][$row->name]['default'] = NULL;
+        }
+        elseif (is_string($row->dflt_value) && $row->dflt_value[0] === '\'') {
+          // Remove the wrapping single quotes. And replace duplicate single
+          // quotes with a single quote.
+          $schema['fields'][$row->name]['default'] = str_replace("''", "'", substr($row->dflt_value, 1, -1));
+        }
+        elseif (is_numeric($row->dflt_value)) {
+          // Adding 0 to a string will cause PHP to convert it to a float or
+          // an integer depending on what the string is. For example:
+          // - '1' + 0 = 1
+          // - '1.0' + 0 = 1.0
+          $schema['fields'][$row->name]['default'] = $row->dflt_value + 0;
+        }
+        else {
+          $schema['fields'][$row->name]['default'] = $row->dflt_value;
+        }
+        // $row->pk contains a number that reflects the primary key order. We
+        // use that as the key and sort (by key) below to return the primary key
+        // in the same order that it is stored in.
+        if ($row->pk) {
+          $schema['primary key'][$row->pk] = $row->name;
+        }
+      }
+      else {
+        throw new \Exception("Unable to parse the column type " . $row->type);
+      }
+    }
+    ksort($schema['primary key']);
+    // Re-key the array because $row->pk starts counting at 1.
+    $schema['primary key'] = array_values($schema['primary key']);
+
+    $indexes = [];
+    $result = $this->connection->query('PRAGMA ' . $info['schema'] . '.index_list(' . $info['table'] . ')');
+    foreach ($result as $row) {
+      if (strpos($row->name, 'sqlite_autoindex_') !== 0) {
+        $indexes[] = [
+          'schema_key' => $row->unique ? 'unique keys' : 'indexes',
+          'name' => $row->name,
+        ];
+      }
+    }
+    foreach ($indexes as $index) {
+      $name = $index['name'];
+      // Get index name without prefix.
+      $index_name = substr($name, strlen($info['table']) + 1);
+      $result = $this->connection->query('PRAGMA ' . $info['schema'] . '.index_info(' . $name . ')');
+      foreach ($result as $row) {
+        $schema[$index['schema_key']][$index_name][] = $row->name;
+      }
+    }
+    return $schema;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function dropField($table, $field) {
+    if (!$this->fieldExists($table, $field)) {
+      return FALSE;
+    }
+
+    $old_schema = $this->introspectSchema($table);
+    $new_schema = $old_schema;
+
+    unset($new_schema['fields'][$field]);
+
+    // Drop the primary key if the field to drop is part of it. This is
+    // consistent with the behavior on PostgreSQL.
+    // @see \Drupal\mysql\Driver\Database\mysql\Schema::dropField()
+    if (isset($new_schema['primary key']) && in_array($field, $new_schema['primary key'], TRUE)) {
+      unset($new_schema['primary key']);
+    }
+
+    // Handle possible index changes.
+    foreach ($new_schema['indexes'] as $index => $fields) {
+      foreach ($fields as $key => $field_name) {
+        if ($field_name == $field) {
+          unset($new_schema['indexes'][$index][$key]);
+        }
+      }
+      // If this index has no more fields then remove it.
+      if (empty($new_schema['indexes'][$index])) {
+        unset($new_schema['indexes'][$index]);
+      }
+    }
+    $this->alterTable($table, $old_schema, $new_schema);
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function changeField($table, $field, $field_new, $spec, $keys_new = []) {
+    if (!$this->fieldExists($table, $field)) {
+      throw new SchemaObjectDoesNotExistException("Cannot change the definition of field '$table.$field': field doesn't exist.");
+    }
+    if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
+      throw new SchemaObjectExistsException("Cannot rename field '$table.$field' to '$field_new': target field already exists.");
+    }
+    if (isset($keys_new['primary key']) && in_array($field_new, $keys_new['primary key'], TRUE)) {
+      $this->ensureNotNullPrimaryKey($keys_new['primary key'], [$field_new => $spec]);
+    }
+
+    $old_schema = $this->introspectSchema($table);
+    $new_schema = $old_schema;
+
+    // Map the old field to the new field.
+    if ($field != $field_new) {
+      $mapping[$field_new] = $field;
+    }
+    else {
+      $mapping = [];
+    }
+
+    // Remove the previous definition and swap in the new one.
+    unset($new_schema['fields'][$field]);
+    $new_schema['fields'][$field_new] = $spec;
+
+    // Map the former indexes to the new column name.
+    $new_schema['primary key'] = $this->mapKeyDefinition($new_schema['primary key'], $mapping);
+    foreach (['unique keys', 'indexes'] as $k) {
+      foreach ($new_schema[$k] as &$key_definition) {
+        $key_definition = $this->mapKeyDefinition($key_definition, $mapping);
+      }
+    }
+
+    // Add in the keys from $keys_new.
+    if (isset($keys_new['primary key'])) {
+      $new_schema['primary key'] = $keys_new['primary key'];
+    }
+    foreach (['unique keys', 'indexes'] as $k) {
+      if (!empty($keys_new[$k])) {
+        $new_schema[$k] = $keys_new[$k] + $new_schema[$k];
+      }
+    }
+
+    $this->alterTable($table, $old_schema, $new_schema, $mapping);
+  }
+
+  /**
+   * Utility method: rename columns in an index definition according to a new mapping.
+   *
+   * @param $key_definition
+   *   The key definition.
+   * @param $mapping
+   *   The new mapping.
+   */
+  protected function mapKeyDefinition(array $key_definition, array $mapping) {
+    foreach ($key_definition as &$field) {
+      // The key definition can be an array($field, $length).
+      if (is_array($field)) {
+        $field = &$field[0];
+      }
+
+      $mapped_field = array_search($field, $mapping, TRUE);
+      if ($mapped_field !== FALSE) {
+        $field = $mapped_field;
+      }
+    }
+    return $key_definition;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addIndex($table, $name, $fields, array $spec) {
+    if (!$this->tableExists($table)) {
+      throw new SchemaObjectDoesNotExistException("Cannot add index '$name' to table '$table': table doesn't exist.");
+    }
+    if ($this->indexExists($table, $name)) {
+      throw new SchemaObjectExistsException("Cannot add index '$name' to table '$table': index already exists.");
+    }
+
+    $schema['indexes'][$name] = $fields;
+    $statements = $this->createIndexSql($table, $schema);
+    foreach ($statements as $statement) {
+      $this->connection->query($statement);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function indexExists($table, $name) {
+    $info = $this->getPrefixInfo($table);
+
+    return $this->connection->query('PRAGMA ' . $info['schema'] . '.index_info(' . $info['table'] . '_' . $name . ')')->fetchField() != '';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function dropIndex($table, $name) {
+    if (!$this->indexExists($table, $name)) {
+      return FALSE;
+    }
+
+    $info = $this->getPrefixInfo($table);
+
+    $this->connection->query('DROP INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $name);
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addUniqueKey($table, $name, $fields) {
+    if (!$this->tableExists($table)) {
+      throw new SchemaObjectDoesNotExistException("Cannot add unique key '$name' to table '$table': table doesn't exist.");
+    }
+    if ($this->indexExists($table, $name)) {
+      throw new SchemaObjectExistsException("Cannot add unique key '$name' to table '$table': unique key already exists.");
+    }
+
+    $schema['unique keys'][$name] = $fields;
+    $statements = $this->createIndexSql($table, $schema);
+    foreach ($statements as $statement) {
+      $this->connection->query($statement);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function dropUniqueKey($table, $name) {
+    if (!$this->indexExists($table, $name)) {
+      return FALSE;
+    }
+
+    $info = $this->getPrefixInfo($table);
+
+    $this->connection->query('DROP INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $name);
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addPrimaryKey($table, $fields) {
+    if (!$this->tableExists($table)) {
+      throw new SchemaObjectDoesNotExistException("Cannot add primary key to table '$table': table doesn't exist.");
+    }
+
+    $old_schema = $this->introspectSchema($table);
+    $new_schema = $old_schema;
+
+    if (!empty($new_schema['primary key'])) {
+      throw new SchemaObjectExistsException("Cannot add primary key to table '$table': primary key already exists.");
+    }
+
+    $new_schema['primary key'] = $fields;
+    $this->ensureNotNullPrimaryKey($new_schema['primary key'], $new_schema['fields']);
+    $this->alterTable($table, $old_schema, $new_schema);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function dropPrimaryKey($table) {
+    $old_schema = $this->introspectSchema($table);
+    $new_schema = $old_schema;
+
+    if (empty($new_schema['primary key'])) {
+      return FALSE;
+    }
+
+    unset($new_schema['primary key']);
+    $this->alterTable($table, $old_schema, $new_schema);
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function findPrimaryKeyColumns($table) {
+    if (!$this->tableExists($table)) {
+      return FALSE;
+    }
+    $schema = $this->introspectSchema($table);
+    return $schema['primary key'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function introspectIndexSchema($table) {
+    if (!$this->tableExists($table)) {
+      throw new SchemaObjectDoesNotExistException("The table $table doesn't exist.");
+    }
+    $schema = $this->introspectSchema($table);
+    unset($schema['fields']);
+    return $schema;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function findTables($table_expression) {
+    $tables = [];
+
+    // The SQLite implementation doesn't need to use the same filtering strategy
+    // as the parent one because individually prefixed tables live in their own
+    // schema (database), which means that neither the main database nor any
+    // attached one will contain a prefixed table name, so we just need to loop
+    // over all known schemas and filter by the user-supplied table expression.
+    $attached_dbs = $this->connection->getAttachedDatabases();
+    foreach ($attached_dbs as $schema) {
+      // Can't use query placeholders for the schema because the query would
+      // have to be :prefixsqlite_master, which does not work. We also need to
+      // ignore the internal SQLite tables.
+      $result = $this->connection->query("SELECT name FROM " . $schema . ".sqlite_master WHERE type = :type AND name LIKE :table_name AND name NOT LIKE :pattern", [
+        ':type' => 'table',
+        ':table_name' => $table_expression,
+        ':pattern' => 'sqlite_%',
+      ]);
+      $tables += $result->fetchAllKeyed(0, 0);
+    }
+
+    return $tables;
+  }
+
+}
diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Select.php b/core/modules/sqlite/src/Driver/Database/sqlite/Select.php
new file mode 100644
index 000000000000..5ee521af8b22
--- /dev/null
+++ b/core/modules/sqlite/src/Driver/Database/sqlite/Select.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Drupal\sqlite\Driver\Database\sqlite;
+
+use Drupal\Core\Database\Query\Select as QuerySelect;
+
+/**
+ * SQLite implementation of \Drupal\Core\Database\Query\Select.
+ */
+class Select extends QuerySelect {
+
+  public function forUpdate($set = TRUE) {
+    // SQLite does not support FOR UPDATE so nothing to do.
+    return $this;
+  }
+
+}
diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Statement.php b/core/modules/sqlite/src/Driver/Database/sqlite/Statement.php
new file mode 100644
index 000000000000..5de75a5decf8
--- /dev/null
+++ b/core/modules/sqlite/src/Driver/Database/sqlite/Statement.php
@@ -0,0 +1,151 @@
+<?php
+
+namespace Drupal\sqlite\Driver\Database\sqlite;
+
+use Drupal\Core\Database\StatementPrefetch;
+use Drupal\Core\Database\StatementInterface;
+
+/**
+ * SQLite implementation of \Drupal\Core\Database\Statement.
+ *
+ * The PDO SQLite driver only closes SELECT statements when the PDOStatement
+ * destructor is called and SQLite does not allow data change (INSERT,
+ * UPDATE etc) on a table which has open SELECT statements. This is a
+ * user-space mock of PDOStatement that buffers all the data and doesn't
+ * have those limitations.
+ */
+class Statement extends StatementPrefetch implements StatementInterface {
+
+  /**
+   * {@inheritdoc}
+   *
+   * The PDO SQLite layer doesn't replace numeric placeholders in queries
+   * correctly, and this makes numeric expressions (such as COUNT(*) >= :count)
+   * fail. We replace numeric placeholders in the query ourselves to work
+   * around this bug.
+   *
+   * See http://bugs.php.net/bug.php?id=45259 for more details.
+   */
+  protected function getStatement($query, &$args = []) {
+    if (is_array($args) && !empty($args)) {
+      // Check if $args is a simple numeric array.
+      if (range(0, count($args) - 1) === array_keys($args)) {
+        // In that case, we have unnamed placeholders.
+        $count = 0;
+        $new_args = [];
+        foreach ($args as $value) {
+          if (is_float($value) || is_int($value)) {
+            if (is_float($value)) {
+              // Force the conversion to float so as not to loose precision
+              // in the automatic cast.
+              $value = sprintf('%F', $value);
+            }
+            $query = substr_replace($query, $value, strpos($query, '?'), 1);
+          }
+          else {
+            $placeholder = ':db_statement_placeholder_' . $count++;
+            $query = substr_replace($query, $placeholder, strpos($query, '?'), 1);
+            $new_args[$placeholder] = $value;
+          }
+        }
+        $args = $new_args;
+      }
+      else {
+        // Else, this is using named placeholders.
+        foreach ($args as $placeholder => $value) {
+          if (is_float($value) || is_int($value)) {
+            if (is_float($value)) {
+              // Force the conversion to float so as not to loose precision
+              // in the automatic cast.
+              $value = sprintf('%F', $value);
+            }
+
+            // We will remove this placeholder from the query as PDO throws an
+            // exception if the number of placeholders in the query and the
+            // arguments does not match.
+            unset($args[$placeholder]);
+            // PDO allows placeholders to not be prefixed by a colon. See
+            // http://marc.info/?l=php-internals&m=111234321827149&w=2 for
+            // more.
+            if ($placeholder[0] != ':') {
+              $placeholder = ":$placeholder";
+            }
+            // When replacing the placeholders, make sure we search for the
+            // exact placeholder. For example, if searching for
+            // ':db_placeholder_1', do not replace ':db_placeholder_11'.
+            $query = preg_replace('/' . preg_quote($placeholder) . '\b/', $value, $query);
+          }
+        }
+      }
+    }
+
+    return $this->pdoConnection->prepare($query);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute($args = [], $options = []) {
+    try {
+      $return = parent::execute($args, $options);
+    }
+    catch (\PDOException $e) {
+      // The database schema might be changed by another process in between the
+      // time that the statement was prepared and the time the statement was run
+      // (e.g. usually happens when running tests). In this case, we need to
+      // re-run the query.
+      // @see http://www.sqlite.org/faq.html#q15
+      // @see http://www.sqlite.org/rescode.html#schema
+      if (!empty($e->errorInfo[1]) && $e->errorInfo[1] === 17) {
+        // The schema has changed. SQLite specifies that we must resend the query.
+        $return = parent::execute($args, $options);
+      }
+      else {
+        // Rethrow the exception.
+        throw $e;
+      }
+    }
+
+    // In some weird cases, SQLite will prefix some column names by the name
+    // of the table. We post-process the data, by renaming the column names
+    // using the same convention as MySQL and PostgreSQL.
+    $rename_columns = [];
+    foreach ($this->columnNames as $k => $column) {
+      // In some SQLite versions, SELECT DISTINCT(field) will return "(field)"
+      // instead of "field".
+      if (preg_match("/^\((.*)\)$/", $column, $matches)) {
+        $rename_columns[$column] = $matches[1];
+        $this->columnNames[$k] = $matches[1];
+        $column = $matches[1];
+      }
+
+      // Remove "table." prefixes.
+      if (preg_match("/^.*\.(.*)$/", $column, $matches)) {
+        $rename_columns[$column] = $matches[1];
+        $this->columnNames[$k] = $matches[1];
+      }
+    }
+    if ($rename_columns) {
+      // DatabaseStatementPrefetch already extracted the first row,
+      // put it back into the result set.
+      if (isset($this->currentRow)) {
+        $this->data[0] = &$this->currentRow;
+      }
+
+      // Then rename all the columns across the result set.
+      foreach ($this->data as $k => $row) {
+        foreach ($rename_columns as $old_column => $new_column) {
+          $this->data[$k][$new_column] = $this->data[$k][$old_column];
+          unset($this->data[$k][$old_column]);
+        }
+      }
+
+      // Finally, extract the first row again.
+      $this->currentRow = $this->data[0];
+      unset($this->data[0]);
+    }
+
+    return $return;
+  }
+
+}
diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Truncate.php b/core/modules/sqlite/src/Driver/Database/sqlite/Truncate.php
new file mode 100644
index 000000000000..f1535fb0196d
--- /dev/null
+++ b/core/modules/sqlite/src/Driver/Database/sqlite/Truncate.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Drupal\sqlite\Driver\Database\sqlite;
+
+use Drupal\Core\Database\Query\Truncate as QueryTruncate;
+
+/**
+ * SQLite implementation of \Drupal\Core\Database\Query\Truncate.
+ *
+ * SQLite doesn't support TRUNCATE, but a DELETE query with no condition has
+ * exactly the effect (it is implemented by DROPing the table).
+ */
+class Truncate extends QueryTruncate {
+
+  public function __toString() {
+    // Create a sanitized comment string to prepend to the query.
+    $comments = $this->connection->makeComment($this->comments);
+
+    return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '} ';
+  }
+
+}
diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Upsert.php b/core/modules/sqlite/src/Driver/Database/sqlite/Upsert.php
new file mode 100644
index 000000000000..599742725310
--- /dev/null
+++ b/core/modules/sqlite/src/Driver/Database/sqlite/Upsert.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Drupal\sqlite\Driver\Database\sqlite;
+
+use Drupal\Core\Database\Query\Upsert as QueryUpsert;
+
+/**
+ * SQLite implementation of \Drupal\Core\Database\Query\Upsert.
+ *
+ * @see https://www.sqlite.org/lang_UPSERT.html
+ */
+class Upsert extends QueryUpsert {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __toString() {
+    // Create a sanitized comment string to prepend to the query.
+    $comments = $this->connection->makeComment($this->comments);
+
+    // 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);
+
+    $query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
+
+    $values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
+    $query .= implode(', ', $values);
+
+    // Updating the unique / primary key is not necessary.
+    unset($insert_fields[$this->key]);
+
+    $update = [];
+    foreach ($insert_fields as $field) {
+      // The "excluded." prefix causes the field to refer to the value for field
+      // that would have been inserted had there been no conflict.
+      $update[] = "$field = EXCLUDED.$field";
+    }
+
+    $query .= ' ON CONFLICT (' . $this->connection->escapeField($this->key) . ') DO UPDATE SET ' . implode(', ', $update);
+
+    return $query;
+  }
+
+}
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index f07133bcd427..3a8a5b2dcb62 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -1167,12 +1167,18 @@ function system_requirements($phase) {
     if ($provider !== 'core' && !\Drupal::moduleHandler()->moduleExists($provider)) {
       $autoload = $connection->getConnectionOptions()['autoload'] ?? '';
       if (($pos = strpos($autoload, 'src/Driver/Database/')) !== FALSE) {
-        $requirements['database_driver_provided_by_module'] = [
-          'title' => t('Database driver provided by module'),
-          'value' => t('Not enabled'),
-          'description' => t('The current database driver is provided by the module: %module. The module is currently not enabled. You should immediately <a href=":enable">enable</a> the module.', ['%module' => $provider, ':enable' => Url::fromRoute('system.modules_list')->toString()]),
-          'severity' => REQUIREMENT_ERROR,
-        ];
+        $post_update_registry = \Drupal::service('update.post_update_registry');
+        $pending_updates = $post_update_registry->getPendingUpdateInformation();
+        if (!in_array('enable_provider_database_driver', array_keys($pending_updates['system']['pending'] ?? []), TRUE)) {
+          // Only show the warning when the post update function has run and
+          // the module that is providing the database driver is not enabled.
+          $requirements['database_driver_provided_by_module'] = [
+            'title' => t('Database driver provided by module'),
+            'value' => t('Not enabled'),
+            'description' => t('The current database driver is provided by the module: %module. The module is currently not enabled. You should immediately <a href=":enable">enable</a> the module.', ['%module' => $provider, ':enable' => Url::fromRoute('system.modules_list')->toString()]),
+            'severity' => REQUIREMENT_ERROR,
+          ];
+        }
       }
     }
   }
diff --git a/core/modules/system/system.post_update.php b/core/modules/system/system.post_update.php
index 7ddcda6476b2..a7230ecd2939 100644
--- a/core/modules/system/system.post_update.php
+++ b/core/modules/system/system.post_update.php
@@ -7,6 +7,7 @@
 
 use Drupal\Core\Site\Settings;
 use Drupal\Core\Config\Entity\ConfigEntityUpdater;
+use Drupal\Core\Database\Database;
 use Drupal\Core\Entity\Display\EntityDisplayInterface;
 use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
 use Drupal\Core\Entity\ContentEntityType;
@@ -233,3 +234,29 @@ function system_post_update_sort_all_config(&$sandbox) {
     $sandbox['#finished'] = 1;
   }
 }
+
+/**
+ * Enable the modules that are providing the listed database drivers.
+ */
+function system_post_update_enable_provider_database_driver() {
+  $modules_to_install = [];
+  foreach (Database::getAllConnectionInfo() as $targets) {
+    foreach ($targets as $target) {
+      // Provider determination taken from Connection::getProvider().
+      [$first, $second] = explode('\\', $target['namespace'] ?? '', 3);
+      $provider = ($first === 'Drupal' && strtolower($second) === $second) ? $second : 'core';
+      if ($provider !== 'core' && !\Drupal::moduleHandler()->moduleExists($provider)) {
+        $autoload = $target['autoload'] ?? '';
+        // We are only enabling the module for database drivers that are
+        // provided by a module.
+        if (str_contains($autoload, 'src/Driver/Database/')) {
+          $modules_to_install[$provider] = TRUE;
+        }
+      }
+    }
+  }
+
+  if ($modules_to_install !== []) {
+    \Drupal::service('module_installer')->install(array_keys($modules_to_install));
+  }
+}
diff --git a/core/modules/system/tests/modules/database_statement_monitoring_test/src/mysql/Connection.php b/core/modules/system/tests/modules/database_statement_monitoring_test/src/mysql/Connection.php
index dc5dab25ac2f..caa96940090d 100644
--- a/core/modules/system/tests/modules/database_statement_monitoring_test/src/mysql/Connection.php
+++ b/core/modules/system/tests/modules/database_statement_monitoring_test/src/mysql/Connection.php
@@ -2,7 +2,7 @@
 
 namespace Drupal\database_statement_monitoring_test\mysql;
 
-use Drupal\Core\Database\Driver\mysql\Connection as BaseConnection;
+use Drupal\mysql\Driver\Database\mysql\Connection as BaseConnection;
 use Drupal\database_statement_monitoring_test\LoggedStatementsTrait;
 
 /**
diff --git a/core/modules/system/tests/modules/database_statement_monitoring_test/src/mysql/Install/Tasks.php b/core/modules/system/tests/modules/database_statement_monitoring_test/src/mysql/Install/Tasks.php
index 443072d47453..338e136cd5c0 100644
--- a/core/modules/system/tests/modules/database_statement_monitoring_test/src/mysql/Install/Tasks.php
+++ b/core/modules/system/tests/modules/database_statement_monitoring_test/src/mysql/Install/Tasks.php
@@ -2,7 +2,7 @@
 
 namespace Drupal\database_statement_monitoring_test\mysql\Install;
 
-use Drupal\Core\Database\Driver\mysql\Install\Tasks as BaseTasks;
+use Drupal\mysql\Driver\Database\mysql\Install\Tasks as BaseTasks;
 
 class Tasks extends BaseTasks {
 }
diff --git a/core/modules/system/tests/modules/database_statement_monitoring_test/src/pgsql/Connection.php b/core/modules/system/tests/modules/database_statement_monitoring_test/src/pgsql/Connection.php
index 86f004e916cc..43995d325a84 100644
--- a/core/modules/system/tests/modules/database_statement_monitoring_test/src/pgsql/Connection.php
+++ b/core/modules/system/tests/modules/database_statement_monitoring_test/src/pgsql/Connection.php
@@ -2,7 +2,7 @@
 
 namespace Drupal\database_statement_monitoring_test\pgsql;
 
-use Drupal\Core\Database\Driver\pgsql\Connection as BaseConnection;
+use Drupal\pgsql\Driver\Database\pgsql\Connection as BaseConnection;
 use Drupal\database_statement_monitoring_test\LoggedStatementsTrait;
 
 /**
diff --git a/core/modules/system/tests/modules/database_statement_monitoring_test/src/pgsql/Install/Tasks.php b/core/modules/system/tests/modules/database_statement_monitoring_test/src/pgsql/Install/Tasks.php
index c51bb2541a9b..0b95ddf53d4f 100644
--- a/core/modules/system/tests/modules/database_statement_monitoring_test/src/pgsql/Install/Tasks.php
+++ b/core/modules/system/tests/modules/database_statement_monitoring_test/src/pgsql/Install/Tasks.php
@@ -2,7 +2,7 @@
 
 namespace Drupal\database_statement_monitoring_test\pgsql\Install;
 
-use Drupal\Core\Database\Driver\pgsql\Install\Tasks as BaseTasks;
+use Drupal\pgsql\Driver\Database\pgsql\Install\Tasks as BaseTasks;
 
 class Tasks extends BaseTasks {
 }
diff --git a/core/modules/system/tests/modules/database_statement_monitoring_test/src/sqlite/Connection.php b/core/modules/system/tests/modules/database_statement_monitoring_test/src/sqlite/Connection.php
index fea32d6798ab..2b4201a9763a 100644
--- a/core/modules/system/tests/modules/database_statement_monitoring_test/src/sqlite/Connection.php
+++ b/core/modules/system/tests/modules/database_statement_monitoring_test/src/sqlite/Connection.php
@@ -2,7 +2,7 @@
 
 namespace Drupal\database_statement_monitoring_test\sqlite;
 
-use Drupal\Core\Database\Driver\sqlite\Connection as BaseConnection;
+use Drupal\sqlite\Driver\Database\sqlite\Connection as BaseConnection;
 use Drupal\database_statement_monitoring_test\LoggedStatementsTrait;
 
 /**
diff --git a/core/modules/system/tests/modules/database_statement_monitoring_test/src/sqlite/Install/Tasks.php b/core/modules/system/tests/modules/database_statement_monitoring_test/src/sqlite/Install/Tasks.php
index 41d5962fefa3..4827f8c18b92 100644
--- a/core/modules/system/tests/modules/database_statement_monitoring_test/src/sqlite/Install/Tasks.php
+++ b/core/modules/system/tests/modules/database_statement_monitoring_test/src/sqlite/Install/Tasks.php
@@ -2,7 +2,7 @@
 
 namespace Drupal\database_statement_monitoring_test\sqlite\Install;
 
-use Drupal\Core\Database\Driver\sqlite\Install\Tasks as BaseTasks;
+use Drupal\sqlite\Driver\Database\sqlite\Install\Tasks as BaseTasks;
 
 class Tasks extends BaseTasks {
 }
diff --git a/core/modules/system/tests/modules/database_test/database_test.install b/core/modules/system/tests/modules/database_test/database_test.install
index 47ed774a590a..855a518031a8 100644
--- a/core/modules/system/tests/modules/database_test/database_test.install
+++ b/core/modules/system/tests/modules/database_test/database_test.install
@@ -321,7 +321,7 @@ function database_test_schema() {
       'id' => [
         'description' => 'Simple unique ID.',
         // Using a serial as an ID properly tests
-        // \Drupal\Core\Database\Driver\pgsql\Upsert.
+        // \Drupal\pgsql\Driver\Database\pgsql\Upsert.
         'type' => 'serial',
         'not null' => TRUE,
       ],
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Connection.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Connection.php
index 9138084acf18..a87a0d386951 100644
--- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Connection.php
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Connection.php
@@ -2,7 +2,9 @@
 
 namespace Drupal\driver_test\Driver\Database\DrivertestMysql;
 
-use Drupal\Core\Database\Driver\mysql\Connection as CoreConnection;
+include_once dirname(__DIR__, 8) . '/mysql/src/Driver/Database/mysql/Connection.php';
+
+use Drupal\mysql\Driver\Database\mysql\Connection as CoreConnection;
 
 /**
  * MySQL test implementation of \Drupal\Core\Database\Connection.
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Insert.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Insert.php
index a9545d77c0f5..8c15b608fe46 100644
--- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Insert.php
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Insert.php
@@ -2,7 +2,9 @@
 
 namespace Drupal\driver_test\Driver\Database\DrivertestMysql;
 
-use Drupal\Core\Database\Driver\mysql\Insert as CoreInsert;
+include_once dirname(__DIR__, 8) . '/mysql/src/Driver/Database/mysql/Insert.php';
+
+use Drupal\mysql\Driver\Database\mysql\Insert as CoreInsert;
 
 /**
  * MySQL test implementation of \Drupal\Core\Database\Query\Insert.
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Install/Tasks.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Install/Tasks.php
index 5c10c2c6be1c..2b20ba147f96 100644
--- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Install/Tasks.php
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Install/Tasks.php
@@ -2,7 +2,9 @@
 
 namespace Drupal\driver_test\Driver\Database\DrivertestMysql\Install;
 
-use Drupal\Core\Database\Driver\mysql\Install\Tasks as CoreTasks;
+include_once dirname(__DIR__, 9) . '/mysql/src/Driver/Database/mysql/Install/Tasks.php';
+
+use Drupal\mysql\Driver\Database\mysql\Install\Tasks as CoreTasks;
 
 /**
  * Specifies installation tasks for MySQL test databases.
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Schema.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Schema.php
index 69277a13914f..b3a2dc01fef5 100644
--- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Schema.php
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Schema.php
@@ -2,7 +2,9 @@
 
 namespace Drupal\driver_test\Driver\Database\DrivertestMysql;
 
-use Drupal\Core\Database\Driver\mysql\Schema as CoreSchema;
+include_once dirname(__DIR__, 8) . '/mysql/src/Driver/Database/mysql/Schema.php';
+
+use Drupal\mysql\Driver\Database\mysql\Schema as CoreSchema;
 
 /**
  * MySQL test implementation of \Drupal\Core\Database\Schema.
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Upsert.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Upsert.php
index 8b4bb482588e..dd2d71af8c77 100644
--- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Upsert.php
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Upsert.php
@@ -2,7 +2,9 @@
 
 namespace Drupal\driver_test\Driver\Database\DrivertestMysql;
 
-use Drupal\Core\Database\Driver\mysql\Upsert as CoreUpsert;
+include_once dirname(__DIR__, 8) . '/mysql/src/Driver/Database/mysql/Upsert.php';
+
+use Drupal\mysql\Driver\Database\mysql\Upsert as CoreUpsert;
 
 /**
  * MySQL test implementation of \Drupal\Core\Database\Query\Upsert.
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Connection.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Connection.php
index c7ec0fed72a8..6ef463cb1577 100644
--- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Connection.php
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Connection.php
@@ -2,7 +2,9 @@
 
 namespace Drupal\driver_test\Driver\Database\DrivertestMysqlDeprecatedVersion;
 
-use Drupal\Core\Database\Driver\mysql\Connection as CoreConnection;
+include_once dirname(__DIR__, 8) . '/mysql/src/Driver/Database/mysql/Connection.php';
+
+use Drupal\mysql\Driver\Database\mysql\Connection as CoreConnection;
 
 /**
  * MySQL test implementation of \Drupal\Core\Database\Connection.
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Insert.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Insert.php
index 86affc1a349c..f1a54e3cacc4 100644
--- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Insert.php
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Insert.php
@@ -2,7 +2,9 @@
 
 namespace Drupal\driver_test\Driver\Database\DrivertestMysqlDeprecatedVersion;
 
-use Drupal\Core\Database\Driver\mysql\Insert as CoreInsert;
+include_once dirname(__DIR__, 8) . '/mysql/src/Driver/Database/mysql/Insert.php';
+
+use Drupal\mysql\Driver\Database\mysql\Insert as CoreInsert;
 
 /**
  * MySQL test implementation of \Drupal\Core\Database\Query\Insert.
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Install/Tasks.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Install/Tasks.php
index c768de5721ef..647268c7b81b 100644
--- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Install/Tasks.php
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Install/Tasks.php
@@ -2,7 +2,9 @@
 
 namespace Drupal\driver_test\Driver\Database\DrivertestMysqlDeprecatedVersion\Install;
 
-use Drupal\Core\Database\Driver\mysql\Install\Tasks as CoreTasks;
+include_once dirname(__DIR__, 9) . '/mysql/src/Driver/Database/mysql/Install/Tasks.php';
+
+use Drupal\mysql\Driver\Database\mysql\Install\Tasks as CoreTasks;
 
 /**
  * Specifies installation tasks for MySQL test databases.
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Schema.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Schema.php
index fef11ed4de0b..4e739215848e 100644
--- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Schema.php
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Schema.php
@@ -2,7 +2,9 @@
 
 namespace Drupal\driver_test\Driver\Database\DrivertestMysqlDeprecatedVersion;
 
-use Drupal\Core\Database\Driver\mysql\Schema as CoreSchema;
+include_once dirname(__DIR__, 8) . '/mysql/src/Driver/Database/mysql/Schema.php';
+
+use Drupal\mysql\Driver\Database\mysql\Schema as CoreSchema;
 
 /**
  * MySQL test implementation of \Drupal\Core\Database\Schema.
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Upsert.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Upsert.php
index 78ee82d34db4..2513c7357b40 100644
--- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Upsert.php
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Upsert.php
@@ -2,7 +2,9 @@
 
 namespace Drupal\driver_test\Driver\Database\DrivertestMysqlDeprecatedVersion;
 
-use Drupal\Core\Database\Driver\mysql\Upsert as CoreUpsert;
+include_once dirname(__DIR__, 8) . '/mysql/src/Driver/Database/mysql/Upsert.php';
+
+use Drupal\mysql\Driver\Database\mysql\Upsert as CoreUpsert;
 
 /**
  * MySQL test implementation of \Drupal\Core\Database\Query\Upsert.
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Connection.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Connection.php
index 87dcf7dd4a67..e45d48ade944 100644
--- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Connection.php
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Connection.php
@@ -2,7 +2,9 @@
 
 namespace Drupal\driver_test\Driver\Database\DrivertestPgsql;
 
-use Drupal\Core\Database\Driver\pgsql\Connection as CoreConnection;
+include_once dirname(__DIR__, 8) . '/pgsql/src/Driver/Database/pgsql/Connection.php';
+
+use Drupal\pgsql\Driver\Database\pgsql\Connection as CoreConnection;
 
 /**
  * PostgreSQL implementation of \Drupal\Core\Database\Connection.
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Delete.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Delete.php
index 5340c8afb026..92081533cbaa 100644
--- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Delete.php
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Delete.php
@@ -2,7 +2,9 @@
 
 namespace Drupal\driver_test\Driver\Database\DrivertestPgsql;
 
-use Drupal\Core\Database\Driver\pgsql\Delete as CoreDelete;
+include_once dirname(__DIR__, 8) . '/pgsql/src/Driver/Database/pgsql/Delete.php';
+
+use Drupal\pgsql\Driver\Database\pgsql\Delete as CoreDelete;
 
 /**
  * PostgreSQL implementation of \Drupal\Core\Database\Query\Delete.
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Insert.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Insert.php
index a2f6f0791afc..957373ffc38f 100644
--- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Insert.php
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Insert.php
@@ -2,7 +2,9 @@
 
 namespace Drupal\driver_test\Driver\Database\DrivertestPgsql;
 
-use Drupal\Core\Database\Driver\pgsql\Insert as CoreInsert;
+include_once dirname(__DIR__, 8) . '/pgsql/src/Driver/Database/pgsql/Insert.php';
+
+use Drupal\pgsql\Driver\Database\pgsql\Insert as CoreInsert;
 
 /**
  * PostgreSQL implementation of \Drupal\Core\Database\Query\Insert.
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Install/Tasks.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Install/Tasks.php
index 55705d832a01..a8c096438f23 100644
--- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Install/Tasks.php
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Install/Tasks.php
@@ -2,7 +2,9 @@
 
 namespace Drupal\driver_test\Driver\Database\DrivertestPgsql\Install;
 
-use Drupal\Core\Database\Driver\pgsql\Install\Tasks as CoreTasks;
+include_once dirname(__DIR__, 9) . '/pgsql/src/Driver/Database/pgsql/Install/Tasks.php';
+
+use Drupal\pgsql\Driver\Database\pgsql\Install\Tasks as CoreTasks;
 
 /**
  * Specifies installation tasks for PostgreSQL databases.
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Schema.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Schema.php
index 8cfa96912699..df0c45c22200 100644
--- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Schema.php
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Schema.php
@@ -2,7 +2,9 @@
 
 namespace Drupal\driver_test\Driver\Database\DrivertestPgsql;
 
-use Drupal\Core\Database\Driver\pgsql\Schema as CoreSchema;
+include_once dirname(__DIR__, 8) . '/pgsql/src/Driver/Database/pgsql/Schema.php';
+
+use Drupal\pgsql\Driver\Database\pgsql\Schema as CoreSchema;
 
 /**
  * PostgreSQL implementation of \Drupal\Core\Database\Schema.
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Select.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Select.php
index f0faf65ca65b..b11fbefe1148 100644
--- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Select.php
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Select.php
@@ -2,7 +2,9 @@
 
 namespace Drupal\driver_test\Driver\Database\DrivertestPgsql;
 
-use Drupal\Core\Database\Driver\pgsql\Select as CoreSelect;
+include_once dirname(__DIR__, 8) . '/pgsql/src/Driver/Database/pgsql/Select.php';
+
+use Drupal\pgsql\Driver\Database\pgsql\Select as CoreSelect;
 
 /**
  * PostgreSQL implementation of \Drupal\Core\Database\Query\Select.
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Truncate.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Truncate.php
index c1b4322d5b79..61b58711ff97 100644
--- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Truncate.php
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Truncate.php
@@ -2,7 +2,9 @@
 
 namespace Drupal\driver_test\Driver\Database\DrivertestPgsql;
 
-use Drupal\Core\Database\Driver\pgsql\Truncate as CoreTruncate;
+include_once dirname(__DIR__, 8) . '/pgsql/src/Driver/Database/pgsql/Truncate.php';
+
+use Drupal\pgsql\Driver\Database\pgsql\Truncate as CoreTruncate;
 
 /**
  * PostgreSQL implementation of \Drupal\Core\Database\Query\Truncate.
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Update.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Update.php
index d5ed1ed5f08f..e30ace4bc8ec 100644
--- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Update.php
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Update.php
@@ -2,7 +2,9 @@
 
 namespace Drupal\driver_test\Driver\Database\DrivertestPgsql;
 
-use Drupal\Core\Database\Driver\pgsql\Update as CoreUpdate;
+include_once dirname(__DIR__, 8) . '/pgsql/src/Driver/Database/pgsql/Update.php';
+
+use Drupal\pgsql\Driver\Database\pgsql\Update as CoreUpdate;
 
 /**
  * PostgreSQL implementation of \Drupal\Core\Database\Query\Update.
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Upsert.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Upsert.php
index 2237a755cf44..b36b039bf9ba 100644
--- a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Upsert.php
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestPgsql/Upsert.php
@@ -2,7 +2,9 @@
 
 namespace Drupal\driver_test\Driver\Database\DrivertestPgsql;
 
-use Drupal\Core\Database\Driver\pgsql\Upsert as CoreUpsert;
+include_once dirname(__DIR__, 8) . '/pgsql/src/Driver/Database/pgsql/Upsert.php';
+
+use Drupal\pgsql\Driver\Database\pgsql\Upsert as CoreUpsert;
 
 /**
  * PostgreSQL implementation of \Drupal\Core\Database\Query\Upsert.
diff --git a/core/modules/system/tests/src/Functional/Update/UpdateEnableProviderDatabaseDriverTest.php b/core/modules/system/tests/src/Functional/Update/UpdateEnableProviderDatabaseDriverTest.php
new file mode 100644
index 000000000000..563d5f4072ec
--- /dev/null
+++ b/core/modules/system/tests/src/Functional/Update/UpdateEnableProviderDatabaseDriverTest.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Drupal\Tests\system\Functional\Update;
+
+use Drupal\Core\Database\Database;
+use Drupal\FunctionalTests\Update\UpdatePathTestBase;
+
+/**
+ * Tests that update hooks are enabling the database driver providing module.
+ *
+ * @group Update
+ */
+class UpdateEnableProviderDatabaseDriverTest extends UpdatePathTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setDatabaseDumpFiles() {
+    $this->databaseDumpFiles = [
+      __DIR__ . '/../../../fixtures/update/drupal-8.8.0.bare.standard.php.gz',
+    ];
+  }
+
+  /**
+   * Tests that post update hooks are properly run.
+   */
+  public function testPostUpdateEnableProviderDatabaseDriverHook() {
+    $connection = Database::getConnection();
+    $provider = $connection->getProvider();
+
+    $this->assertFalse(\Drupal::moduleHandler()->moduleExists($provider));
+
+    // Running the updates enables the module that is providing the database
+    // driver.
+    $this->runUpdates();
+
+    $this->assertTrue(\Drupal::moduleHandler()->moduleExists($provider));
+  }
+
+}
diff --git a/core/modules/views/src/Plugin/views/argument/StringArgument.php b/core/modules/views/src/Plugin/views/argument/StringArgument.php
index be47b6959256..ef85b80e6386 100644
--- a/core/modules/views/src/Plugin/views/argument/StringArgument.php
+++ b/core/modules/views/src/Plugin/views/argument/StringArgument.php
@@ -178,7 +178,7 @@ public function getFormula() {
     if ($this->options['case'] != 'none') {
       // Support case-insensitive substring comparisons for SQLite by using the
       // 'NOCASE_UTF8' collation.
-      // @see Drupal\Core\Database\Driver\sqlite\Connection::open()
+      // @see Drupal\sqlite\Driver\Database\sqlite\Connection::open()
       if (Database::getConnection()->databaseType() == 'sqlite') {
         $formula .= ' COLLATE NOCASE_UTF8';
       }
diff --git a/core/modules/views/src/Plugin/views/query/SqliteDateSql.php b/core/modules/views/src/Plugin/views/query/SqliteDateSql.php
index d6006ac71f97..5ee488689c03 100644
--- a/core/modules/views/src/Plugin/views/query/SqliteDateSql.php
+++ b/core/modules/views/src/Plugin/views/query/SqliteDateSql.php
@@ -97,7 +97,7 @@ public function getDateFormat($field, $format) {
     // case the comparison value is a float, integer, or numeric. All of the
     // above SQLite format tokens only produce integers. However, the given
     // $format may contain 'Y-m-d', which results in a string.
-    // @see \Drupal\Core\Database\Driver\sqlite\Connection::expandArguments()
+    // @see \Drupal\sqlite\Driver\Database\sqlite\Connection::expandArguments()
     // @see http://www.sqlite.org/lang_datefunc.html
     // @see http://www.sqlite.org/lang_expr.html#castexpr
     if (preg_match('/^(?:%\w)+$/', $format)) {
diff --git a/core/tests/Drupal/BuildTests/Framework/Tests/HtRouterTest.php b/core/tests/Drupal/BuildTests/Framework/Tests/HtRouterTest.php
index c96a0f194744..721766587f36 100644
--- a/core/tests/Drupal/BuildTests/Framework/Tests/HtRouterTest.php
+++ b/core/tests/Drupal/BuildTests/Framework/Tests/HtRouterTest.php
@@ -3,7 +3,7 @@
 namespace Drupal\BuildTests\Framework\Tests;
 
 use Drupal\BuildTests\QuickStart\QuickStartTestBase;
-use Drupal\Core\Database\Driver\sqlite\Install\Tasks;
+use Drupal\sqlite\Driver\Database\sqlite\Install\Tasks;
 
 /**
  * @coversDefaultClass \Drupal\BuildTests\Framework\BuildTestBase
diff --git a/core/tests/Drupal/FunctionalTests/ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest.php b/core/tests/Drupal/FunctionalTests/ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest.php
new file mode 100644
index 000000000000..855387f695d5
--- /dev/null
+++ b/core/tests/Drupal/FunctionalTests/ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Drupal\FunctionalTests;
+
+use Drupal\Core\Database\Database;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * @group Database
+ */
+class ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $driver = Database::getConnection()->driver();
+    if (!in_array($driver, ['mysql', 'pgsql', 'sqlite'])) {
+      $this->markTestSkipped("This test does not support the {$driver} database driver.");
+    }
+
+    $filename = $this->siteDirectory . '/settings.php';
+    chmod($filename, 0777);
+    $contents = file_get_contents($filename);
+
+    $autoload = "'autoload' => 'core/modules/$driver/src/Driver/Database/$driver/',";
+    $contents = str_replace($autoload, '', $contents);
+    $namespace_search = "'namespace' => 'Drupal\\\\$driver\\\\Driver\\\\Database\\\\$driver',";
+    $namespace_replace = "'namespace' => 'Drupal\\\\Core\\\\Database\\\\Driver\\\\$driver',";
+    $contents = str_replace($namespace_search, $namespace_replace, $contents);
+    file_put_contents($filename, $contents);
+  }
+
+  /**
+   * Confirms that the site works with Drupal 8 style database connection array.
+   */
+  public function testExistingDrupal8StyleDatabaseConnectionInSettingsPhp() {
+    $this->drupalLogin($this->drupalCreateUser());
+    $this->assertSession()->addressEquals('user/2');
+    $this->assertSession()->statusCodeEquals(200);
+
+    // Make sure that we are have tested with the Drupal 8 style database
+    // connection array.
+    $filename = $this->siteDirectory . '/settings.php';
+    $contents = file_get_contents($filename);
+    $driver = Database::getConnection()->driver();
+    $this->assertStringContainsString("'namespace' => 'Drupal\\\\Core\\\\Database\\\\Driver\\\\$driver',", $contents);
+    $this->assertStringContainsString("'driver' => '$driver',", $contents);
+    $this->assertStringNotContainsString("'autoload' => 'core/modules/$driver/src/Driver/Database/$driver/", $contents);
+  }
+
+}
diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerTest.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerTest.php
index 4b3f1c605e01..118a77124577 100644
--- a/core/tests/Drupal/FunctionalTests/Installer/InstallerTest.php
+++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerTest.php
@@ -2,8 +2,10 @@
 
 namespace Drupal\FunctionalTests\Installer;
 
+use Drupal\Core\Database\Database;
 use Drupal\Core\Routing\RoutingEvents;
 use Drupal\Core\Test\PerformanceTestRecorder;
+use Drupal\Core\Extension\ModuleUninstallValidatorException;
 
 /**
  * Tests the interactive installer.
@@ -118,4 +120,31 @@ protected function visitInstaller() {
     $this->assertSession()->titleEquals('Choose language | Drupal');
   }
 
+  /**
+   * Confirms that the installation succeeded.
+   */
+  public function testInstalled() {
+    $this->assertSession()->addressEquals('user/1');
+    $this->assertSession()->statusCodeEquals(200);
+
+    $database = Database::getConnection();
+    $module = $database->getProvider();
+    $module_handler = \Drupal::service('module_handler');
+
+    // Assert that the module that is providing the database driver has been
+    // installed.
+    $this->assertTrue($module_handler->moduleExists($module));
+
+    // The module that is providing the database driver should be uninstallable.
+    try {
+      $this->container->get('module_installer')->uninstall([$module]);
+      $this->fail("Uninstalled $module module.");
+    }
+    catch (ModuleUninstallValidatorException $e) {
+      $module_name = $module_handler->getName($module);
+      $driver = $database->driver();
+      $this->assertStringContainsString("The module '$module_name' is providing the database driver '$driver'.", $e->getMessage());
+    }
+  }
+
 }
diff --git a/core/tests/Drupal/KernelTests/Core/Database/MysqlDriverLegacyTest.php b/core/tests/Drupal/KernelTests/Core/Database/MysqlDriverLegacyTest.php
new file mode 100644
index 000000000000..549c1986c0a4
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Database/MysqlDriverLegacyTest.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Database;
+
+use Drupal\Core\Database\Driver\mysql\Connection;
+use Drupal\Core\Database\Driver\mysql\ExceptionHandler;
+use Drupal\Core\Database\Driver\mysql\Install\Tasks;
+use Drupal\Core\Database\Driver\mysql\Insert;
+use Drupal\Core\Database\Driver\mysql\Schema;
+use Drupal\Core\Database\Driver\mysql\Upsert;
+use Drupal\Tests\Core\Database\Stub\StubPDO;
+
+/**
+ * Tests the deprecations of the MySQL database driver classes in Core.
+ *
+ * @group legacy
+ * @group Database
+ */
+class MysqlDriverLegacyTest extends DatabaseTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    if ($this->connection->driver() !== 'mysql') {
+      $this->markTestSkipped('Only test the deprecation message for the MySQL database driver classes in Core.');
+    }
+  }
+
+  /**
+   * @covers Drupal\Core\Database\Driver\mysql\Install\Tasks
+   */
+  public function testDeprecationInstallTasks() {
+    $this->expectDeprecation('\Drupal\Core\Database\Driver\mysql\Install\Tasks is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492');
+    $tasks = new Tasks();
+    $this->assertInstanceOf(Tasks::class, $tasks);
+  }
+
+  /**
+   * @covers Drupal\Core\Database\Driver\mysql\Connection
+   */
+  public function testDeprecationConnection() {
+    $this->expectDeprecation('\Drupal\Core\Database\Driver\mysql\Connection is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492');
+    // @todo https://www.drupal.org/project/drupal/issues/3251084 Remove setting
+    // the $options parameter.
+    $options['init_commands']['sql_mode'] = '';
+    $connection = new Connection($this->createMock(StubPDO::class), $options);
+    $this->assertInstanceOf(Connection::class, $connection);
+  }
+
+  /**
+   * @covers Drupal\Core\Database\Driver\mysql\ExceptionHandler
+   */
+  public function testDeprecationExceptionHandler() {
+    $this->expectDeprecation('\Drupal\Core\Database\Driver\mysql\ExceptionHandler is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492');
+    $handler = new ExceptionHandler();
+    $this->assertInstanceOf(ExceptionHandler::class, $handler);
+  }
+
+  /**
+   * @covers Drupal\Core\Database\Driver\mysql\Insert
+   */
+  public function testDeprecationInsert() {
+    $this->expectDeprecation('\Drupal\Core\Database\Driver\mysql\Insert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492');
+    $insert = new Insert($this->connection, 'test');
+    $this->assertInstanceOf(Insert::class, $insert);
+  }
+
+  /**
+   * @covers Drupal\Core\Database\Driver\mysql\Schema
+   */
+  public function testDeprecationSchema() {
+    $this->expectDeprecation('\Drupal\Core\Database\Driver\mysql\Schema is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492');
+    $schema = new Schema($this->connection);
+    $this->assertInstanceOf(Schema::class, $schema);
+  }
+
+  /**
+   * @covers Drupal\Core\Database\Driver\mysql\Upsert
+   */
+  public function testDeprecationUpsert() {
+    $this->expectDeprecation('\Drupal\Core\Database\Driver\mysql\Upsert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The MySQL database driver has been moved to the mysql module. See https://www.drupal.org/node/3129492');
+    $upsert = new Upsert($this->connection, 'test');
+    $this->assertInstanceOf(Upsert::class, $upsert);
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Database/NextIdTest.php b/core/tests/Drupal/KernelTests/Core/Database/NextIdTest.php
index 3655be917bee..3dfe5866bad8 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/NextIdTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/NextIdTest.php
@@ -43,7 +43,7 @@ public function testDbNextId() {
   /**
    * Tests that sequences table clear up works when a connection is closed.
    *
-   * @see \Drupal\Core\Database\Driver\mysql\Connection::__destruct()
+   * @see \Drupal\mysql\Driver\Database\mysql\Connection::__destruct()
    */
   public function testDbNextIdClosedConnection() {
     // Only run this test for the 'mysql' driver.
@@ -67,7 +67,7 @@ public function testDbNextIdClosedConnection() {
     // Close the connection.
     Database::closeConnection('next_id');
 
-    // Test that \Drupal\Core\Database\Driver\mysql\Connection::__destruct()
+    // Test that \Drupal\mysql\Driver\Database\mysql\Connection::__destruct()
     // successfully trims the sequences table if the connection is closed.
     $count = $this->connection->select('sequences')->countQuery()->execute()->fetchField();
     $this->assertEquals(1, $count);
diff --git a/core/tests/Drupal/KernelTests/Core/Database/PgsqlDriverLegacyTest.php b/core/tests/Drupal/KernelTests/Core/Database/PgsqlDriverLegacyTest.php
new file mode 100644
index 000000000000..273e1f960709
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Database/PgsqlDriverLegacyTest.php
@@ -0,0 +1,115 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Database;
+
+use Drupal\Core\Database\Driver\pgsql\Connection;
+use Drupal\Core\Database\Driver\pgsql\Delete;
+use Drupal\Core\Database\Driver\pgsql\Install\Tasks;
+use Drupal\Core\Database\Driver\pgsql\Insert;
+use Drupal\Core\Database\Driver\pgsql\Schema;
+use Drupal\Core\Database\Driver\pgsql\Select;
+use Drupal\Core\Database\Driver\pgsql\Truncate;
+use Drupal\Core\Database\Driver\pgsql\Update;
+use Drupal\Core\Database\Driver\pgsql\Upsert;
+use Drupal\Tests\Core\Database\Stub\StubPDO;
+
+/**
+ * Tests the deprecations of the PostgreSQL database driver classes in Core.
+ *
+ * @group legacy
+ * @group Database
+ */
+class PgsqlDriverLegacyTest extends DatabaseTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    if ($this->connection->driver() !== 'pgsql') {
+      $this->markTestSkipped('Only test the deprecation message for the PostgreSQL database driver classes in Core.');
+    }
+  }
+
+  /**
+   * @covers Drupal\Core\Database\Driver\pgsql\Install\Tasks
+   */
+  public function testDeprecationInstallTasks() {
+    $this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Install\Tasks is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492');
+    $tasks = new Tasks();
+    $this->assertInstanceOf(Tasks::class, $tasks);
+  }
+
+  /**
+   * @covers Drupal\Core\Database\Driver\pgsql\Connection
+   */
+  public function testDeprecationConnection() {
+    $this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Connection is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492');
+    $connection = new Connection($this->createMock(StubPDO::class), []);
+    $this->assertInstanceOf(Connection::class, $connection);
+  }
+
+  /**
+   * @covers Drupal\Core\Database\Driver\pgsql\Delete
+   */
+  public function testDeprecationDelete() {
+    $this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Delete is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492');
+    $delete = new Delete($this->connection, 'test');
+    $this->assertInstanceOf(Delete::class, $delete);
+  }
+
+  /**
+   * @covers Drupal\Core\Database\Driver\pgsql\Insert
+   */
+  public function testDeprecationInsert() {
+    $this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Insert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492');
+    $insert = new Insert($this->connection, 'test');
+    $this->assertInstanceOf(Insert::class, $insert);
+  }
+
+  /**
+   * @covers Drupal\Core\Database\Driver\pgsql\Schema
+   */
+  public function testDeprecationSchema() {
+    $this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Schema is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492');
+    $schema = new Schema($this->connection);
+    $this->assertInstanceOf(Schema::class, $schema);
+  }
+
+  /**
+   * @covers Drupal\Core\Database\Driver\pgsql\Select
+   */
+  public function testDeprecationSelect() {
+    $this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Select is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492');
+    $select = new Select($this->connection, 'test');
+    $this->assertInstanceOf(Select::class, $select);
+  }
+
+  /**
+   * @covers Drupal\Core\Database\Driver\pgsql\Truncate
+   */
+  public function testDeprecationTruncate() {
+    $this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Truncate is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492');
+    $truncate = new Truncate($this->connection, 'test');
+    $this->assertInstanceOf(Truncate::class, $truncate);
+  }
+
+  /**
+   * @covers Drupal\Core\Database\Driver\pgsql\Update
+   */
+  public function testDeprecationUpdate() {
+    $this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Update is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492');
+    $update = new Update($this->connection, 'test');
+    $this->assertInstanceOf(Update::class, $update);
+  }
+
+  /**
+   * @covers Drupal\Core\Database\Driver\pgsql\Upsert
+   */
+  public function testDeprecationUpsert() {
+    $this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Upsert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492');
+    $upsert = new Upsert($this->connection, 'test');
+    $this->assertInstanceOf(Upsert::class, $upsert);
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Database/PrefixInfoTest.php b/core/tests/Drupal/KernelTests/Core/Database/PrefixInfoTest.php
index efa0c53ed8db..2afec6de82bf 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/PrefixInfoTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/PrefixInfoTest.php
@@ -15,7 +15,7 @@ class PrefixInfoTest extends DatabaseTestBase {
    * Tests that DatabaseSchema::getPrefixInfo() returns the right database.
    *
    * We are testing if the return array of the method
-   * \Drupal\Core\Database\Driver\mysql\Schema::getPrefixInfo(). This return
+   * \Drupal\mysql\Driver\Database\mysql\Schema::getPrefixInfo(). This return
    * array is a keyed array with info about amongst other things the database.
    * The other two by Drupal core supported databases do not have this variable
    * set in the return array.
diff --git a/core/tests/Drupal/KernelTests/Core/Database/QueryTest.php b/core/tests/Drupal/KernelTests/Core/Database/QueryTest.php
index 1172a0f0503c..32f3a78d0b4a 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/QueryTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/QueryTest.php
@@ -138,7 +138,7 @@ public function testConditionOperatorArgumentsSQLInjection() {
   /**
    * Tests numeric query parameter expansion in expressions.
    *
-   * @see \Drupal\Core\Database\Driver\sqlite\Statement::getStatement()
+   * @see \Drupal\sqlite\Driver\Database\sqlite\Statement::getStatement()
    * @see http://bugs.php.net/bug.php?id=45259
    */
   public function testNumericExpressionSubstitution() {
diff --git a/core/tests/Drupal/KernelTests/Core/Database/SchemaTest.php b/core/tests/Drupal/KernelTests/Core/Database/SchemaTest.php
index bf73ba4609c8..678e0eee91ad 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/SchemaTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/SchemaTest.php
@@ -276,9 +276,9 @@ public function testSchema() {
   }
 
   /**
-   * @covers \Drupal\Core\Database\Driver\mysql\Schema::introspectIndexSchema
-   * @covers \Drupal\Core\Database\Driver\pgsql\Schema::introspectIndexSchema
-   * @covers \Drupal\Core\Database\Driver\sqlite\Schema::introspectIndexSchema
+   * @covers \Drupal\mysql\Driver\Database\mysql\Schema::introspectIndexSchema
+   * @covers \Drupal\pgsql\Driver\Database\pgsql\Schema::introspectIndexSchema
+   * @covers \Drupal\sqlite\Driver\Database\sqlite\Schema::introspectIndexSchema
    */
   public function testIntrospectIndexSchema() {
     $table_specification = [
@@ -355,7 +355,7 @@ public function testIntrospectIndexSchema() {
   /**
    * Tests that indexes on string fields are limited to 191 characters on MySQL.
    *
-   * @see \Drupal\Core\Database\Driver\mysql\Schema::getNormalizedIndexes()
+   * @see \Drupal\mysql\Driver\Database\mysql\Schema::getNormalizedIndexes()
    */
   public function testIndexLength() {
     if ($this->connection->databaseType() !== 'mysql') {
diff --git a/core/tests/Drupal/KernelTests/Core/Database/SelectComplexTest.php b/core/tests/Drupal/KernelTests/Core/Database/SelectComplexTest.php
index 44ef6ccb4795..281f3d4d57c9 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/SelectComplexTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/SelectComplexTest.php
@@ -238,7 +238,7 @@ public function testCountQueryRemovals() {
     // Check that the ordering clause is handled properly.
     $orderby = $query->getOrderBy();
     // The orderby string is different for PostgreSQL.
-    // @see Drupal\Core\Database\Driver\pgsql\Select::orderBy()
+    // @see Drupal\pgsql\Driver\Database\pgsql\Select::orderBy()
     $db_type = Database::getConnection()->databaseType();
     $this->assertEquals($db_type == 'pgsql' ? 'ASC NULLS FIRST' : 'ASC', $orderby['name'], 'Query correctly sets ordering clause.');
     $orderby = $count->getOrderBy();
diff --git a/core/tests/Drupal/KernelTests/Core/Database/SqliteDriverLegacyTest.php b/core/tests/Drupal/KernelTests/Core/Database/SqliteDriverLegacyTest.php
new file mode 100644
index 000000000000..f3a34c7389fd
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Database/SqliteDriverLegacyTest.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Database;
+
+use Drupal\Core\Database\Driver\sqlite\Connection;
+use Drupal\Core\Database\Driver\sqlite\Install\Tasks;
+use Drupal\Core\Database\Driver\sqlite\Insert;
+use Drupal\Core\Database\Driver\sqlite\Schema;
+use Drupal\Core\Database\Driver\sqlite\Select;
+use Drupal\Core\Database\Driver\sqlite\Statement;
+use Drupal\Core\Database\Driver\sqlite\Truncate;
+use Drupal\Core\Database\Driver\sqlite\Upsert;
+use Drupal\Tests\Core\Database\Stub\StubPDO;
+
+/**
+ * Tests the deprecations of the SQLite database driver classes in Core.
+ *
+ * @group legacy
+ * @group Database
+ */
+class SqliteDriverLegacyTest extends DatabaseTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    if ($this->connection->driver() !== 'sqlite') {
+      $this->markTestSkipped('Only test the deprecation message for the SQLite database driver classes in Core.');
+    }
+  }
+
+  /**
+   * @covers Drupal\Core\Database\Driver\sqlite\Install\Tasks
+   */
+  public function testDeprecationInstallTasks() {
+    $this->expectDeprecation('\Drupal\Core\Database\Driver\sqlite\Install\Tasks is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492');
+    $tasks = new Tasks();
+    $this->assertInstanceOf(Tasks::class, $tasks);
+  }
+
+  /**
+   * @covers Drupal\Core\Database\Driver\sqlite\Connection
+   */
+  public function testDeprecationConnection() {
+    $this->expectDeprecation('\Drupal\Core\Database\Driver\sqlite\Connection is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492');
+    $connection = new Connection($this->createMock(StubPDO::class), []);
+    $this->assertInstanceOf(Connection::class, $connection);
+  }
+
+  /**
+   * @covers Drupal\Core\Database\Driver\sqlite\Insert
+   */
+  public function testDeprecationInsert() {
+    $this->expectDeprecation('\Drupal\Core\Database\Driver\sqlite\Insert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492');
+    $insert = new Insert($this->connection, 'test');
+    $this->assertInstanceOf(Insert::class, $insert);
+  }
+
+  /**
+   * @covers Drupal\Core\Database\Driver\sqlite\Schema
+   */
+  public function testDeprecationSchema() {
+    $this->expectDeprecation('\Drupal\Core\Database\Driver\sqlite\Schema is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492');
+    $schema = new Schema($this->connection);
+    $this->assertInstanceOf(Schema::class, $schema);
+  }
+
+  /**
+   * @covers Drupal\Core\Database\Driver\sqlite\Select
+   */
+  public function testDeprecationSelect() {
+    $this->expectDeprecation('\Drupal\Core\Database\Driver\sqlite\Select is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492');
+    $select = new Select($this->connection, 'test');
+    $this->assertInstanceOf(Select::class, $select);
+  }
+
+  /**
+   * @covers Drupal\Core\Database\Driver\sqlite\Statement
+   */
+  public function testDeprecationStatement() {
+    $this->expectDeprecation('\Drupal\Core\Database\Driver\sqlite\Statement is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492');
+    $statement = new Statement($this->createMock(StubPDO::class), $this->connection, '', []);
+    $this->assertInstanceOf(Statement::class, $statement);
+  }
+
+  /**
+   * @covers Drupal\Core\Database\Driver\sqlite\Truncate
+   */
+  public function testDeprecationTruncate() {
+    $this->expectDeprecation('\Drupal\Core\Database\Driver\sqlite\Truncate is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492');
+    $truncate = new Truncate($this->connection, 'test');
+    $this->assertInstanceOf(Truncate::class, $truncate);
+  }
+
+  /**
+   * @covers Drupal\Core\Database\Driver\sqlite\Upsert
+   */
+  public function testDeprecationUpsert() {
+    $this->expectDeprecation('\Drupal\Core\Database\Driver\sqlite\Upsert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The SQLite database driver has been moved to the sqlite module. See https://www.drupal.org/node/3129492');
+    $upsert = new Upsert($this->connection, 'test');
+    $this->assertInstanceOf(Upsert::class, $upsert);
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php b/core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php
index 7aa37c70ef8b..8adc5840631e 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php
@@ -263,7 +263,7 @@ public function testTransactionWithDdlStatement() {
       try {
         // Rollback the outer transaction.
         $transaction->rollBack();
-        // @see \Drupal\Core\Database\Driver\mysql\Connection::rollBack()
+        // @see \Drupal\mysql\Driver\Database\mysql\Connection::rollBack()
         $this->fail('Rolling back a transaction containing DDL should produce a warning.');
       }
       catch (Warning $warning) {
diff --git a/core/tests/Drupal/KernelTests/KernelTestBaseTest.php b/core/tests/Drupal/KernelTests/KernelTestBaseTest.php
index 7ca986ef3509..378576dbcd58 100644
--- a/core/tests/Drupal/KernelTests/KernelTestBaseTest.php
+++ b/core/tests/Drupal/KernelTests/KernelTestBaseTest.php
@@ -441,4 +441,14 @@ public function testVarDump() {
     $this->assertStringContainsString('test_role', StreamCapturer::$cache);
   }
 
+  /**
+   * @covers ::bootEnvironment
+   */
+  public function testDatabaseDriverModuleEnabled() {
+    $module = Database::getConnection()->getProvider();
+
+    // Test that the module that is providing the database driver is enabled.
+    $this->assertSame(1, \Drupal::service('extension.list.module')->get($module)->status);
+  }
+
 }
diff --git a/core/tests/Drupal/Tests/Core/Command/GenerateThemeTest.php b/core/tests/Drupal/Tests/Core/Command/GenerateThemeTest.php
index c203cae17d4b..76f6a00eb0f6 100644
--- a/core/tests/Drupal/Tests/Core/Command/GenerateThemeTest.php
+++ b/core/tests/Drupal/Tests/Core/Command/GenerateThemeTest.php
@@ -3,7 +3,7 @@
 namespace Drupal\Tests\Core\Command;
 
 use Drupal\BuildTests\QuickStart\QuickStartTestBase;
-use Drupal\Core\Database\Driver\sqlite\Install\Tasks;
+use Drupal\sqlite\Driver\Database\sqlite\Install\Tasks;
 use Symfony\Component\Process\PhpExecutableFinder;
 use Symfony\Component\Process\Process;
 
diff --git a/core/tests/Drupal/Tests/Core/Command/QuickStartTest.php b/core/tests/Drupal/Tests/Core/Command/QuickStartTest.php
index 917179c83bd9..081ab4950d95 100644
--- a/core/tests/Drupal/Tests/Core/Command/QuickStartTest.php
+++ b/core/tests/Drupal/Tests/Core/Command/QuickStartTest.php
@@ -2,7 +2,7 @@
 
 namespace Drupal\Tests\Core\Command;
 
-use Drupal\Core\Database\Driver\sqlite\Install\Tasks;
+use Drupal\sqlite\Driver\Database\sqlite\Install\Tasks;
 use Drupal\Core\Test\TestDatabase;
 use Drupal\Tests\BrowserTestBase;
 use GuzzleHttp\Client;
diff --git a/core/tests/Drupal/Tests/Core/Database/DatabaseTest.php b/core/tests/Drupal/Tests/Core/Database/DatabaseTest.php
index 7c7dcd25752f..1eb469399fd0 100644
--- a/core/tests/Drupal/Tests/Core/Database/DatabaseTest.php
+++ b/core/tests/Drupal/Tests/Core/Database/DatabaseTest.php
@@ -70,7 +70,7 @@ public function testFindDriverAutoloadDirectory($expected, $namespace) {
    */
   public function providerFindDriverAutoloadDirectory() {
     return [
-      'core mysql' => [FALSE, 'Drupal\Core\Database\Driver\mysql'],
+      'core mysql' => ['core/modules/mysql/src/Driver/Database/mysql/', 'Drupal\mysql\Driver\Database\mysql'],
       'D8 custom fake' => [FALSE, 'Drupal\Driver\Database\corefake'],
       'module mysql' => ['core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/', 'Drupal\driver_test\Driver\Database\DrivertestMysql'],
     ];
diff --git a/core/tests/Drupal/Tests/Core/Database/Driver/mysql/ConnectionTest.php b/core/tests/Drupal/Tests/Core/Database/Driver/mysql/ConnectionTest.php
index d6913c8a7056..e8d637022522 100644
--- a/core/tests/Drupal/Tests/Core/Database/Driver/mysql/ConnectionTest.php
+++ b/core/tests/Drupal/Tests/Core/Database/Driver/mysql/ConnectionTest.php
@@ -2,13 +2,13 @@
 
 namespace Drupal\Tests\Core\Database\Driver\mysql;
 
-use Drupal\Core\Database\Driver\mysql\Connection;
+use Drupal\mysql\Driver\Database\mysql\Connection;
 use Drupal\Tests\UnitTestCase;
 
 /**
  * Tests MySQL database connections.
  *
- * @coversDefaultClass \Drupal\Core\Database\Driver\mysql\Connection
+ * @coversDefaultClass \Drupal\mysql\Driver\Database\mysql\Connection
  * @group Database
  */
 class ConnectionTest extends UnitTestCase {
@@ -38,7 +38,7 @@ protected function setUp(): void {
   /**
    * Creates a Connection object for testing.
    *
-   * @return \Drupal\Core\Database\Driver\mysql\Connection
+   * @return \Drupal\mysql\Driver\Database\mysql\Connection
    */
   private function createConnection(): Connection {
     /** @var \PDO $pdo_connection */
diff --git a/core/tests/Drupal/Tests/Core/Database/Driver/mysql/install/TasksTest.php b/core/tests/Drupal/Tests/Core/Database/Driver/mysql/install/TasksTest.php
index d6ef38a27f4a..b8b6915aea2d 100644
--- a/core/tests/Drupal/Tests/Core/Database/Driver/mysql/install/TasksTest.php
+++ b/core/tests/Drupal/Tests/Core/Database/Driver/mysql/install/TasksTest.php
@@ -2,14 +2,14 @@
 
 namespace Drupal\Tests\Core\Database\Driver\mysql\install;
 
-use Drupal\Core\Database\Driver\mysql\Connection;
-use Drupal\Core\Database\Driver\mysql\Install\Tasks;
+use Drupal\mysql\Driver\Database\mysql\Connection;
+use Drupal\mysql\Driver\Database\mysql\Install\Tasks;
 use Drupal\Tests\UnitTestCase;
 
 /**
  * Tests the MySQL install tasks.
  *
- * @coversDefaultClass \Drupal\Core\Database\Driver\mysql\Install\Tasks
+ * @coversDefaultClass \Drupal\mysql\Driver\Database\mysql\Install\Tasks
  * @group Database
  */
 class TasksTest extends UnitTestCase {
@@ -17,7 +17,7 @@ class TasksTest extends UnitTestCase {
   /**
    * A connection object prophecy.
    *
-   * @var \Drupal\Core\Database\Driver\mysql\Connection|\Prophecy\Prophecy\ObjectProphecy
+   * @var \Drupal\mysql\Driver\Database\mysql\Connection|\Prophecy\Prophecy\ObjectProphecy
    */
   private $connection;
 
@@ -31,10 +31,10 @@ protected function setUp(): void {
   /**
    * Creates a Tasks object for testing.
    *
-   * @return \Drupal\Core\Database\Driver\mysql\Install\Tasks
+   * @return \Drupal\mysql\Driver\Database\mysql\Install\Tasks
    */
   private function createTasks(): Tasks {
-    /** @var \Drupal\Core\Database\Driver\mysql\Connection $connection */
+    /** @var \Drupal\mysql\Driver\Database\mysql\Connection $connection */
     $connection = $this->connection->reveal();
 
     return new class($connection) extends Tasks {
@@ -63,7 +63,7 @@ protected function t($string, array $args = [], array $options = []) {
   /**
    * Creates a Tasks object for testing, without connection.
    *
-   * @return \Drupal\Core\Database\Driver\mysql\Install\Tasks
+   * @return \Drupal\mysql\Driver\Database\mysql\Install\Tasks
    */
   private function createTasksNoConnection(): Tasks {
     return new class() extends Tasks {
diff --git a/core/tests/Drupal/Tests/Core/Database/Driver/pgsql/PostgresqlSchemaTest.php b/core/tests/Drupal/Tests/Core/Database/Driver/pgsql/PostgresqlSchemaTest.php
index df2a05f116dc..0e50eb18408b 100644
--- a/core/tests/Drupal/Tests/Core/Database/Driver/pgsql/PostgresqlSchemaTest.php
+++ b/core/tests/Drupal/Tests/Core/Database/Driver/pgsql/PostgresqlSchemaTest.php
@@ -2,13 +2,13 @@
 
 namespace Drupal\Tests\Core\Database\Driver\pgsql;
 
-use Drupal\Core\Database\Driver\pgsql\Schema;
+use Drupal\pgsql\Driver\Database\pgsql\Schema;
 use Drupal\Tests\UnitTestCase;
 
 // cSpell:ignore conname
 
 /**
- * @coversDefaultClass \Drupal\Core\Database\Driver\pgsql\Schema
+ * @coversDefaultClass \Drupal\pgsql\Driver\Database\pgsql\Schema
  * @group Database
  */
 class PostgresqlSchemaTest extends UnitTestCase {
@@ -16,7 +16,7 @@ class PostgresqlSchemaTest extends UnitTestCase {
   /**
    * The PostgreSql DB connection.
    *
-   * @var \PHPUnit\Framework\MockObject\MockObject|\Drupal\Core\Database\Driver\pgsql\Connection
+   * @var \PHPUnit\Framework\MockObject\MockObject|\Drupal\pgsql\Driver\Database\pgsql\Connection
    */
   protected $connection;
 
@@ -26,7 +26,7 @@ class PostgresqlSchemaTest extends UnitTestCase {
   protected function setUp(): void {
     parent::setUp();
 
-    $this->connection = $this->getMockBuilder('\Drupal\Core\Database\Driver\pgsql\Connection')
+    $this->connection = $this->getMockBuilder('\Drupal\pgsql\Driver\Database\pgsql\Connection')
       ->disableOriginalConstructor()
       ->getMock();
   }
diff --git a/core/tests/Drupal/Tests/Core/Database/Driver/sqlite/ConnectionTest.php b/core/tests/Drupal/Tests/Core/Database/Driver/sqlite/ConnectionTest.php
index 3d872ceb779d..3e52d6aecca8 100644
--- a/core/tests/Drupal/Tests/Core/Database/Driver/sqlite/ConnectionTest.php
+++ b/core/tests/Drupal/Tests/Core/Database/Driver/sqlite/ConnectionTest.php
@@ -2,12 +2,12 @@
 
 namespace Drupal\Tests\Core\Database\Driver\sqlite;
 
-use Drupal\Core\Database\Driver\sqlite\Connection;
+use Drupal\sqlite\Driver\Database\sqlite\Connection;
 use Drupal\Tests\Core\Database\Stub\StubPDO;
 use Drupal\Tests\UnitTestCase;
 
 /**
- * @coversDefaultClass \Drupal\Core\Database\Driver\sqlite\Connection
+ * @coversDefaultClass \Drupal\sqlite\Driver\Database\sqlite\Connection
  * @group Database
  */
 class ConnectionTest extends UnitTestCase {
diff --git a/core/tests/Drupal/Tests/Core/Database/InstallerObjectTest.php b/core/tests/Drupal/Tests/Core/Database/InstallerObjectTest.php
index 15f823a688fd..278ace0becd4 100644
--- a/core/tests/Drupal/Tests/Core/Database/InstallerObjectTest.php
+++ b/core/tests/Drupal/Tests/Core/Database/InstallerObjectTest.php
@@ -3,7 +3,7 @@
 namespace Drupal\Tests\Core\Database;
 
 use Composer\Autoload\ClassLoader;
-use Drupal\Core\Database\Driver\mysql\Install\Tasks as MysqlInstallTasks;
+use Drupal\mysql\Driver\Database\mysql\Install\Tasks as MysqlInstallTasks;
 use Drupal\Driver\Database\fake\Install\Tasks as FakeInstallTasks;
 use Drupal\Driver\Database\corefake\Install\Tasks as CustomCoreFakeInstallTasks;
 use Drupal\driver_test\Driver\Database\DrivertestMysql\Install\Tasks as DriverTestMysqlInstallTasks;
@@ -58,7 +58,7 @@ public function testDbInstallerObject($driver, $namespace, $expected_class_name)
   public function providerDbInstallerObject() {
     return [
       // A driver only in the core namespace.
-      ['mysql', NULL, MysqlInstallTasks::class],
+      ['mysql', "Drupal\\mysql\\Driver\\Database\\mysql", MysqlInstallTasks::class],
 
       // A driver only in the custom namespace.
       ['fake', "Drupal\\Driver\\Database\\fake", FakeInstallTasks::class],
diff --git a/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php b/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php
index 2dd58125f7fd..fb1c9dde8df2 100644
--- a/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php
+++ b/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php
@@ -48,8 +48,8 @@ protected function setUp(): void {
    *
    * @dataProvider providerConvertDbUrlToConnectionInfo
    */
-  public function testDbUrlToConnectionConversion($root, $url, $database_array) {
-    $result = Database::convertDbUrlToConnectionInfo($url, $root ?: $this->root);
+  public function testDbUrlToConnectionConversion($url, $database_array) {
+    $result = Database::convertDbUrlToConnectionInfo($url, $this->root);
     $this->assertEquals($database_array, $result);
   }
 
@@ -58,14 +58,13 @@ public function testDbUrlToConnectionConversion($root, $url, $database_array) {
    *
    * @return array
    *   Array of arrays with the following elements:
-   *   - root: The baseroot string, only used with sqlite drivers.
    *   - url: The full URL string to be tested.
    *   - database_array: An array containing the expected results.
    */
   public function providerConvertDbUrlToConnectionInfo() {
+    $root = dirname(__FILE__, 7);
     return [
       'MySql without prefix' => [
-        '',
         'mysql://test_user:test_pass@test_host:3306/test_database',
         [
           'driver' => 'mysql',
@@ -74,21 +73,21 @@ public function providerConvertDbUrlToConnectionInfo() {
           'host' => 'test_host',
           'database' => 'test_database',
           'port' => 3306,
-          'namespace' => 'Drupal\Core\Database\Driver\mysql',
+          'namespace' => 'Drupal\mysql\Driver\Database\mysql',
+          'autoload' => 'core/modules/mysql/src/Driver/Database/mysql/',
         ],
       ],
       'SQLite, relative to root, without prefix' => [
-        '/var/www/d8',
         'sqlite://localhost/test_database',
         [
           'driver' => 'sqlite',
           'host' => 'localhost',
-          'database' => '/var/www/d8/test_database',
-          'namespace' => 'Drupal\Core\Database\Driver\sqlite',
+          'database' => $root . '/test_database',
+          'namespace' => 'Drupal\sqlite\Driver\Database\sqlite',
+          'autoload' => 'core/modules/sqlite/src/Driver/Database/sqlite/',
         ],
       ],
       'MySql with prefix' => [
-        '',
         'mysql://test_user:test_pass@test_host:3306/test_database#bar',
         [
           'driver' => 'mysql',
@@ -98,32 +97,32 @@ public function providerConvertDbUrlToConnectionInfo() {
           'database' => 'test_database',
           'prefix' => 'bar',
           'port' => 3306,
-          'namespace' => 'Drupal\Core\Database\Driver\mysql',
+          'namespace' => 'Drupal\mysql\Driver\Database\mysql',
+          'autoload' => 'core/modules/mysql/src/Driver/Database/mysql/',
         ],
       ],
       'SQLite, relative to root, with prefix' => [
-        '/var/www/d8',
         'sqlite://localhost/test_database#foo',
         [
           'driver' => 'sqlite',
           'host' => 'localhost',
-          'database' => '/var/www/d8/test_database',
+          'database' => $root . '/test_database',
           'prefix' => 'foo',
-          'namespace' => 'Drupal\Core\Database\Driver\sqlite',
+          'namespace' => 'Drupal\sqlite\Driver\Database\sqlite',
+          'autoload' => 'core/modules/sqlite/src/Driver/Database/sqlite/',
         ],
       ],
       'SQLite, absolute path, without prefix' => [
-        '/var/www/d8',
         'sqlite://localhost//baz/test_database',
         [
           'driver' => 'sqlite',
           'host' => 'localhost',
           'database' => '/baz/test_database',
-          'namespace' => 'Drupal\Core\Database\Driver\sqlite',
+          'namespace' => 'Drupal\sqlite\Driver\Database\sqlite',
+          'autoload' => 'core/modules/sqlite/src/Driver/Database/sqlite/',
         ],
       ],
       'MySQL contrib test driver without prefix' => [
-        '',
         'DrivertestMysql://test_user:test_pass@test_host:3306/test_database?module=driver_test',
         [
           'driver' => 'DrivertestMysql',
@@ -137,7 +136,6 @@ public function providerConvertDbUrlToConnectionInfo() {
         ],
       ],
       'MySQL contrib test driver with prefix' => [
-        '',
         'DrivertestMysql://test_user:test_pass@test_host:3306/test_database?module=driver_test#bar',
         [
           'driver' => 'DrivertestMysql',
@@ -152,7 +150,6 @@ public function providerConvertDbUrlToConnectionInfo() {
         ],
       ],
       'PostgreSQL contrib test driver without prefix' => [
-        '',
         'DrivertestPgsql://test_user:test_pass@test_host:5432/test_database?module=driver_test',
         [
           'driver' => 'DrivertestPgsql',
@@ -166,7 +163,6 @@ public function providerConvertDbUrlToConnectionInfo() {
         ],
       ],
       'PostgreSQL contrib test driver with prefix' => [
-        '',
         'DrivertestPgsql://test_user:test_pass@test_host:5432/test_database?module=driver_test#bar',
         [
           'driver' => 'DrivertestPgsql',
@@ -181,7 +177,6 @@ public function providerConvertDbUrlToConnectionInfo() {
         ],
       ],
       'MySql with a custom query parameter' => [
-        '',
         'mysql://test_user:test_pass@test_host:3306/test_database?extra=value',
         [
           'driver' => 'mysql',
@@ -190,7 +185,55 @@ public function providerConvertDbUrlToConnectionInfo() {
           'host' => 'test_host',
           'database' => 'test_database',
           'port' => 3306,
-          'namespace' => 'Drupal\Core\Database\Driver\mysql',
+          'namespace' => 'Drupal\mysql\Driver\Database\mysql',
+          'autoload' => 'core/modules/mysql/src/Driver/Database/mysql/',
+        ],
+      ],
+      'MySql with the module name mysql' => [
+        'mysql://test_user:test_pass@test_host:3306/test_database?module=mysql',
+        [
+          'driver' => 'mysql',
+          'username' => 'test_user',
+          'password' => 'test_pass',
+          'host' => 'test_host',
+          'database' => 'test_database',
+          'port' => 3306,
+          'namespace' => 'Drupal\mysql\Driver\Database\mysql',
+          'autoload' => 'core/modules/mysql/src/Driver/Database/mysql/',
+        ],
+      ],
+      'PostgreSql without the module name set' => [
+        'pgsql://test_user:test_pass@test_host/test_database',
+        [
+          'driver' => 'pgsql',
+          'username' => 'test_user',
+          'password' => 'test_pass',
+          'host' => 'test_host',
+          'database' => 'test_database',
+          'namespace' => 'Drupal\pgsql\Driver\Database\pgsql',
+          'autoload' => 'core/modules/pgsql/src/Driver/Database/pgsql/',
+        ],
+      ],
+      'PostgreSql with the module name pgsql' => [
+        'pgsql://test_user:test_pass@test_host/test_database?module=pgsql',
+        [
+          'driver' => 'pgsql',
+          'username' => 'test_user',
+          'password' => 'test_pass',
+          'host' => 'test_host',
+          'database' => 'test_database',
+          'namespace' => 'Drupal\pgsql\Driver\Database\pgsql',
+          'autoload' => 'core/modules/pgsql/src/Driver/Database/pgsql/',
+        ],
+      ],
+      'SQLite, relative to root, without prefix and with the module name sqlite' => [
+        'sqlite://localhost/test_database?module=sqlite',
+        [
+          'driver' => 'sqlite',
+          'host' => 'localhost',
+          'database' => $root . '/test_database',
+          'namespace' => 'Drupal\sqlite\Driver\Database\sqlite',
+          'autoload' => 'core/modules/sqlite/src/Driver/Database/sqlite/',
         ],
       ],
     ];
@@ -258,7 +301,7 @@ public function providerGetConnectionInfoAsUrl() {
       'port' => '3306',
       'driver' => 'mysql',
     ];
-    $expected_url1 = 'mysql://test_user:test_pass@test_host:3306/test_database';
+    $expected_url1 = 'mysql://test_user:test_pass@test_host:3306/test_database?module=mysql';
 
     $info2 = [
       'database' => 'test_database',
@@ -269,20 +312,20 @@ public function providerGetConnectionInfoAsUrl() {
       'port' => '3306',
       'driver' => 'mysql',
     ];
-    $expected_url2 = 'mysql://test_user:test_pass@test_host:3306/test_database#pre';
+    $expected_url2 = 'mysql://test_user:test_pass@test_host:3306/test_database?module=mysql#pre';
 
     $info3 = [
       'database' => 'test_database',
       'driver' => 'sqlite',
     ];
-    $expected_url3 = 'sqlite://localhost/test_database';
+    $expected_url3 = 'sqlite://localhost/test_database?module=sqlite';
 
     $info4 = [
       'database' => 'test_database',
       'driver' => 'sqlite',
       'prefix' => 'pre',
     ];
-    $expected_url4 = 'sqlite://localhost/test_database#pre';
+    $expected_url4 = 'sqlite://localhost/test_database?module=sqlite#pre';
 
     $info5 = [
       'database' => 'test_database',
@@ -382,7 +425,7 @@ public function providerInvalidArgumentGetConnectionInfoAsUrl() {
         [
           'driver' => 'sqlite',
           'host' => 'localhost',
-          'namespace' => 'Drupal\Core\Database\Driver\sqlite',
+          'namespace' => 'Drupal\sqlite\Driver\Database\sqlite',
         ],
         "As a minimum, the connection options array must contain at least the 'driver' and 'database' keys",
       ],
diff --git a/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/BackendCompilerPassTest.php b/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/BackendCompilerPassTest.php
index fa9a371d001b..2d7b5505db00 100644
--- a/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/BackendCompilerPassTest.php
+++ b/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/BackendCompilerPassTest.php
@@ -124,7 +124,7 @@ protected function getSqliteContainer($service) {
     $container = new ContainerBuilder();
     $container->setDefinition('service', $service);
     $container->setDefinition('sqlite.service', new Definition(__NAMESPACE__ . '\\ServiceClassSqlite'));
-    $mock = $this->getMockBuilder('Drupal\Core\Database\Driver\sqlite\Connection')->onlyMethods([])->disableOriginalConstructor()->getMock();
+    $mock = $this->getMockBuilder('Drupal\sqlite\Driver\Database\sqlite\Connection')->onlyMethods([])->disableOriginalConstructor()->getMock();
     $container->set('database', $mock);
     return $container;
   }
diff --git a/core/tests/Drupal/Tests/Core/Test/TestSetupTraitTest.php b/core/tests/Drupal/Tests/Core/Test/TestSetupTraitTest.php
index d0b828356661..7b88af74511c 100644
--- a/core/tests/Drupal/Tests/Core/Test/TestSetupTraitTest.php
+++ b/core/tests/Drupal/Tests/Core/Test/TestSetupTraitTest.php
@@ -25,8 +25,9 @@ class TestSetupTraitTest extends UnitTestCase {
    * @covers ::changeDatabasePrefix
    */
   public function testChangeDatabasePrefix() {
+    $root = dirname(__FILE__, 7);
     putenv('SIMPLETEST_DB=pgsql://user:pass@127.0.0.1/db');
-    $connection_info = Database::convertDbUrlToConnectionInfo('mysql://user:pass@localhost/db', '');
+    $connection_info = Database::convertDbUrlToConnectionInfo('mysql://user:pass@localhost/db', $root);
     Database::addConnectionInfo('default', 'default', $connection_info);
     $this->assertEquals('mysql', Database::getConnectionInfo()['default']['driver']);
     $this->assertEquals('localhost', Database::getConnectionInfo()['default']['host']);
@@ -35,7 +36,7 @@ public function testChangeDatabasePrefix() {
     // used to avoid unnecessary set up.
     $test_setup = $this->getMockForTrait(TestSetupTrait::class);
     $test_setup->databasePrefix = 'testDbPrefix';
-    $test_setup->root = '';
+    $test_setup->root = $root;
 
     $method = new \ReflectionMethod(get_class($test_setup), 'changeDatabasePrefix');
     $method->setAccessible(TRUE);
diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php
index 718890224b48..4768843bcf07 100644
--- a/sites/default/default.settings.php
+++ b/sites/default/default.settings.php
@@ -170,9 +170,9 @@
  * information on these defaults and the potential issues.
  *
  * More details can be found in the constructor methods for each driver:
- * - \Drupal\Core\Database\Driver\mysql\Connection::__construct()
- * - \Drupal\Core\Database\Driver\pgsql\Connection::__construct()
- * - \Drupal\Core\Database\Driver\sqlite\Connection::__construct()
+ * - \Drupal\mysql\Driver\Database\mysql\Connection::__construct()
+ * - \Drupal\pgsql\Driver\Database\pgsql\Connection::__construct()
+ * - \Drupal\sqlite\Driver\Database\sqlite\Connection::__construct()
  *
  * Sample Database configuration format for PostgreSQL (pgsql):
  * @code
-- 
GitLab