diff --git a/core/includes/install.inc b/core/includes/install.inc
index da4a89049f5789693e420ffc70aa7e3c1a4a24a0..f85499042f8e6045b0e2d1db5ecfde027c9dbe41 100644
--- a/core/includes/install.inc
+++ b/core/includes/install.inc
@@ -626,10 +626,15 @@ function drupal_install_system($install_state) {
   // When the database driver is provided by a module, then install that module.
   // This module must be installed before any other module, as it must be able
   // to override any call to hook_schema() or any "backend_overridable" service.
+  // In edge cases, a driver module may extend from another driver module (for
+  // instance, a module to provide backward compatibility with a database
+  // version no longer supported by core). In order for the extended classes to
+  // be autoloadable, the extending module should list the extended module in
+  // its dependencies, and here the dependencies will be installed as well.
   if ($provider !== 'core') {
     $autoload = $connection->getConnectionOptions()['autoload'] ?? '';
     if (($pos = strpos($autoload, 'src/Driver/Database/')) !== FALSE) {
-      $kernel->getContainer()->get('module_installer')->install([$provider], FALSE);
+      $kernel->getContainer()->get('module_installer')->install([$provider], TRUE);
     }
   }
 
diff --git a/core/lib/Drupal/Core/Config/DatabaseStorage.php b/core/lib/Drupal/Core/Config/DatabaseStorage.php
index 607fdde5fb7096a17f00983d0a778f759b08e6f7..14290691e1bc45283cc5b8eef8faf1169cde16ad 100644
--- a/core/lib/Drupal/Core/Config/DatabaseStorage.php
+++ b/core/lib/Drupal/Core/Config/DatabaseStorage.php
@@ -143,6 +143,8 @@ public function write($name, array $data) {
    * @return bool
    */
   protected function doWrite($name, $data) {
+    // @todo Remove the 'return' option in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
     $options = ['return' => Database::RETURN_AFFECTED] + $this->options;
     return (bool) $this->connection->merge($this->table, $options)
       ->keys(['collection', 'name'], [$this->collection, $name])
@@ -218,6 +220,8 @@ protected static function schemaDefinition() {
    * @todo Ignore replica targets for data manipulation operations.
    */
   public function delete($name) {
+    // @todo Remove the 'return' option in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
     $options = ['return' => Database::RETURN_AFFECTED] + $this->options;
     return (bool) $this->connection->delete($this->table, $options)
       ->condition('collection', $this->collection)
@@ -231,6 +235,8 @@ public function delete($name) {
    * @throws PDOException
    */
   public function rename($name, $new_name) {
+    // @todo Remove the 'return' option in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
     $options = ['return' => Database::RETURN_AFFECTED] + $this->options;
     return (bool) $this->connection->update($this->table, $options)
       ->fields(['name' => $new_name])
@@ -280,6 +286,8 @@ public function listAll($prefix = '') {
    */
   public function deleteAll($prefix = '') {
     try {
+      // @todo Remove the 'return' option in Drupal 11.
+      // @see https://www.drupal.org/project/drupal/issues/3256524
       $options = ['return' => Database::RETURN_AFFECTED] + $this->options;
       return (bool) $this->connection->delete($this->table, $options)
         ->condition('name', $prefix . '%', 'LIKE')
diff --git a/core/lib/Drupal/Core/Database/Connection.php b/core/lib/Drupal/Core/Database/Connection.php
index 67c601d3db0e463881515d35ece9f88c63689848..cea7fc4028eb1a3fc362ef6cacce8274788417e5 100644
--- a/core/lib/Drupal/Core/Database/Connection.php
+++ b/core/lib/Drupal/Core/Database/Connection.php
@@ -367,12 +367,12 @@ public function __destruct() {
    *   class. If a string is specified, each record will be fetched into a new
    *   object of that class. The behavior of all other values is defined by PDO.
    *   See http://php.net/manual/pdostatement.fetch.php
-   * - return: Depending on the type of query, different return values may be
-   *   meaningful. This directive instructs the system which type of return
-   *   value is desired. The system will generally set the correct value
-   *   automatically, so it is extremely rare that a module developer will ever
-   *   need to specify this value. Setting it incorrectly will likely lead to
-   *   unpredictable results or fatal errors. Legal values include:
+   * - return: (deprecated) Depending on the type of query, different return
+   *   values may be meaningful. This directive instructs the system which type
+   *   of return value is desired. The system will generally set the correct
+   *   value automatically, so it is extremely rare that a module developer will
+   *   ever need to specify this value. Setting it incorrectly will likely lead
+   *   to unpredictable results or fatal errors. Legal values include:
    *   - Database::RETURN_STATEMENT: Return the prepared statement object for
    *     the query. This is usually only meaningful for SELECT queries, where
    *     the statement object is how one accesses the result set returned by the
@@ -414,7 +414,6 @@ public function __destruct() {
   protected function defaultOptions() {
     return [
       'fetch' => \PDO::FETCH_OBJ,
-      'return' => Database::RETURN_STATEMENT,
       'allow_delimiter_in_query' => FALSE,
       'allow_square_brackets' => FALSE,
       'pdo' => [],
@@ -616,6 +615,10 @@ public function getFullQualifiedTableName($table) {
    * @throws \Drupal\Core\Database\DatabaseExceptionWrapper
    */
   public function prepareStatement(string $query, array $options, bool $allow_row_count = FALSE): StatementInterface {
+    if (isset($options['return'])) {
+      @trigger_error('Passing "return" option to ' . __METHOD__ . '() is deprecated in drupal:9.4.0 and is removed in drupal:11.0.0. For data manipulation operations, use dynamic queries instead. See https://www.drupal.org/node/3185520', E_USER_DEPRECATED);
+    }
+
     try {
       $query = $this->preprocessStatement($query, $options);
 
@@ -914,6 +917,11 @@ protected function filterComment($comment = '') {
   public function query($query, array $args = [], $options = []) {
     // Use default values if not already set.
     $options += $this->defaultOptions();
+
+    if (isset($options['return'])) {
+      @trigger_error('Passing "return" option to ' . __METHOD__ . '() is deprecated in drupal:9.4.0 and is removed in drupal:11.0.0. For data manipulation operations, use dynamic queries instead. See https://www.drupal.org/node/3185520', E_USER_DEPRECATED);
+    }
+
     assert(!isset($options['target']), 'Passing "target" option to query() has no effect. See https://www.drupal.org/node/2993033');
 
     // We allow either a pre-bound statement object (deprecated) or a literal
@@ -946,7 +954,7 @@ public function query($query, array $args = [], $options = []) {
       // Depending on the type of query we may need to return a different value.
       // See DatabaseConnection::defaultOptions() for a description of each
       // value.
-      switch ($options['return']) {
+      switch ($options['return'] ?? Database::RETURN_STATEMENT) {
         case Database::RETURN_STATEMENT:
           return $stmt;
 
@@ -1234,6 +1242,40 @@ public function insert($table, array $options = []) {
     return new $class($this, $table, $options);
   }
 
+  /**
+   * Returns the ID of the last inserted row or sequence value.
+   *
+   * This method should normally be used only within database driver code.
+   *
+   * This is a proxy to invoke lastInsertId() from the wrapped connection.
+   * If a sequence name is not specified for the name parameter, this returns a
+   * string representing the row ID of the last row that was inserted into the
+   * database.
+   * If a sequence name is specified for the name parameter, this returns a
+   * string representing the last value retrieved from the specified sequence
+   * object.
+   *
+   * @param string|null $name
+   *   (Optional) Name of the sequence object from which the ID should be
+   *   returned.
+   *
+   * @return string
+   *   The value returned by the wrapped connection.
+   *
+   * @throws \Drupal\Core\Database\DatabaseExceptionWrapper
+   *   In case of failure.
+   *
+   * @see \PDO::lastInsertId
+   *
+   * @internal
+   */
+  public function lastInsertId(?string $name = NULL): string {
+    if (($last_insert_id = $this->connection->lastInsertId($name)) === FALSE) {
+      throw new DatabaseExceptionWrapper("Could not determine last insert id" . $name === NULL ? '' : " for sequence $name");
+    }
+    return $last_insert_id;
+  }
+
   /**
    * Prepares and returns a MERGE query object.
    *
diff --git a/core/lib/Drupal/Core/Database/Database.php b/core/lib/Drupal/Core/Database/Database.php
index 2394c5eff6effeb095afb977e326c2ea6060b35f..79b681dc883a0f168ab4d7f39e4f5e1be05c59d4 100644
--- a/core/lib/Drupal/Core/Database/Database.php
+++ b/core/lib/Drupal/Core/Database/Database.php
@@ -19,21 +19,41 @@ abstract class Database {
    *
    * This is used for queries that have no reasonable return value anyway, such
    * as INSERT statements to a table without a serial primary key.
+   *
+   * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. There is no
+   *   replacement.
+   *
+   * @see https://www.drupal.org/node/3185520
    */
   const RETURN_NULL = 0;
 
   /**
    * Flag to indicate a query call should return the prepared statement.
+   *
+   * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. There is no
+   *   replacement.
+   *
+   * @see https://www.drupal.org/node/3185520
    */
   const RETURN_STATEMENT = 1;
 
   /**
    * Flag to indicate a query call should return the number of affected rows.
+   *
+   * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. There is no
+   *   replacement.
+   *
+   * @see https://www.drupal.org/node/3185520
    */
   const RETURN_AFFECTED = 2;
 
   /**
    * Flag to indicate a query call should return the "last insert id".
+   *
+   * @deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. There is no
+   *   replacement.
+   *
+   * @see https://www.drupal.org/node/3185520
    */
   const RETURN_INSERT_ID = 3;
 
diff --git a/core/lib/Drupal/Core/Database/Query/Delete.php b/core/lib/Drupal/Core/Database/Query/Delete.php
index 2fb0b91449d859dfc070b7e1dd944572ef52eb5d..864d0648dd2295f7548c41516c75190e62ac1def 100644
--- a/core/lib/Drupal/Core/Database/Query/Delete.php
+++ b/core/lib/Drupal/Core/Database/Query/Delete.php
@@ -32,6 +32,8 @@ class Delete extends Query implements ConditionInterface {
    *   Array of database options.
    */
   public function __construct(Connection $connection, $table, array $options = []) {
+    // @todo Remove $options['return'] in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
     $options['return'] = Database::RETURN_AFFECTED;
     parent::__construct($connection, $options);
     $this->table = $table;
diff --git a/core/lib/Drupal/Core/Database/Query/Insert.php b/core/lib/Drupal/Core/Database/Query/Insert.php
index 011d4423d1e53cc2c6c2ec2a405f8f696f37c4b7..af4e32820f0637acac66eac529ec3beec4cc6290 100644
--- a/core/lib/Drupal/Core/Database/Query/Insert.php
+++ b/core/lib/Drupal/Core/Database/Query/Insert.php
@@ -31,6 +31,8 @@ class Insert extends Query implements \Countable {
    *   Array of database options.
    */
   public function __construct($connection, $table, array $options = []) {
+    // @todo Remove $options['return'] in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
     if (!isset($options['return'])) {
       $options['return'] = Database::RETURN_INSERT_ID;
     }
@@ -82,11 +84,12 @@ public function execute() {
     // we wrap it in a transaction so that it is atomic where possible. On many
     // databases, such as SQLite, this is also a notable performance boost.
     $transaction = $this->connection->startTransaction();
+    $stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions);
 
     try {
-      $sql = (string) $this;
       foreach ($this->insertValues as $insert_values) {
-        $last_insert_id = $this->connection->query($sql, $insert_values, $this->queryOptions);
+        $stmt->execute($insert_values, $this->queryOptions);
+        $last_insert_id = $this->connection->lastInsertId();
       }
     }
     catch (\Exception $e) {
diff --git a/core/lib/Drupal/Core/Database/Query/Merge.php b/core/lib/Drupal/Core/Database/Query/Merge.php
index 90a9abab1e110b7b8bb6205915b57bf146ce5035..fffca6ce9f74d8befb5a9611d5c346fba1683944 100644
--- a/core/lib/Drupal/Core/Database/Query/Merge.php
+++ b/core/lib/Drupal/Core/Database/Query/Merge.php
@@ -134,6 +134,8 @@ class Merge extends Query implements ConditionInterface {
    *   Array of database options.
    */
   public function __construct(Connection $connection, $table, array $options = []) {
+    // @todo Remove $options['return'] in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
     $options['return'] = Database::RETURN_AFFECTED;
     parent::__construct($connection, $options);
     $this->table = $table;
diff --git a/core/lib/Drupal/Core/Database/Query/Select.php b/core/lib/Drupal/Core/Database/Query/Select.php
index 5da38481662c73017ce473a000649f670b41edcd..ac7bdd4e6f932717865ba41c1012aaa6e6694bac 100644
--- a/core/lib/Drupal/Core/Database/Query/Select.php
+++ b/core/lib/Drupal/Core/Database/Query/Select.php
@@ -131,6 +131,8 @@ class Select extends Query implements SelectInterface {
    *   Array of query options.
    */
   public function __construct(Connection $connection, $table, $alias = NULL, $options = []) {
+    // @todo Remove $options['return'] in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
     $options['return'] = Database::RETURN_STATEMENT;
     parent::__construct($connection, $options);
     $conjunction = $options['conjunction'] ?? 'AND';
diff --git a/core/lib/Drupal/Core/Database/Query/Truncate.php b/core/lib/Drupal/Core/Database/Query/Truncate.php
index 2c0adcb310125ebd89a89ed7c3fe9e7e5555c073..66b39644b502bf3fb112e50c2bda227ed39a5057 100644
--- a/core/lib/Drupal/Core/Database/Query/Truncate.php
+++ b/core/lib/Drupal/Core/Database/Query/Truncate.php
@@ -28,6 +28,8 @@ class Truncate extends Query {
    *   Array of database options.
    */
   public function __construct(Connection $connection, $table, array $options = []) {
+    // @todo Remove $options['return'] in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
     $options['return'] = Database::RETURN_AFFECTED;
     parent::__construct($connection, $options);
     $this->table = $table;
diff --git a/core/lib/Drupal/Core/Database/Query/Update.php b/core/lib/Drupal/Core/Database/Query/Update.php
index 88af918647e0c611ce5b0600efa192b39d69ffed..1311083e102f267da17d7894baebd3d81dd31a2f 100644
--- a/core/lib/Drupal/Core/Database/Query/Update.php
+++ b/core/lib/Drupal/Core/Database/Query/Update.php
@@ -61,6 +61,8 @@ class Update extends Query implements ConditionInterface {
    *   Array of database options.
    */
   public function __construct(Connection $connection, $table, array $options = []) {
+    // @todo Remove $options['return'] in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
     $options['return'] = Database::RETURN_AFFECTED;
     parent::__construct($connection, $options);
     $this->table = $table;
diff --git a/core/lib/Drupal/Core/Database/Query/Upsert.php b/core/lib/Drupal/Core/Database/Query/Upsert.php
index 524b145b55abf777cdd626fbe732e485a007b441..00c26cddbb60d3bc7bac29a4d92b64ba626c8312 100644
--- a/core/lib/Drupal/Core/Database/Query/Upsert.php
+++ b/core/lib/Drupal/Core/Database/Query/Upsert.php
@@ -35,6 +35,8 @@ abstract class Upsert extends Query implements \Countable {
    *   (optional) An array of database options.
    */
   public function __construct(Connection $connection, $table, array $options = []) {
+    // @todo Remove $options['return'] in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
     $options['return'] = Database::RETURN_AFFECTED;
     parent::__construct($connection, $options);
     $this->table = $table;
diff --git a/core/lib/Drupal/Core/Database/RowCountException.php b/core/lib/Drupal/Core/Database/RowCountException.php
index cd4131045fc610e0b7e6d43d0cca9d19239cab04..83b6d65e721e1d2ded544153bcda40742abb1d3f 100644
--- a/core/lib/Drupal/Core/Database/RowCountException.php
+++ b/core/lib/Drupal/Core/Database/RowCountException.php
@@ -9,7 +9,7 @@ class RowCountException extends \RuntimeException implements DatabaseException {
 
   public function __construct($message = '', $code = 0, \Exception $previous = NULL) {
     if (empty($message)) {
-      $message = "rowCount() is supported for DELETE, INSERT, or UPDATE statements performed with structured query builders only, since they would not be portable across database engines otherwise. If the query builders are not sufficient, set the 'return' option to Database::RETURN_AFFECTED to get the number of affected rows.";
+      $message = "rowCount() is supported for DELETE, INSERT, or UPDATE statements performed with structured query builders only, since they would not be portable across database engines otherwise. If the query builders are not sufficient, use a prepareStatement() with an \$allow_row_count argument set to TRUE, execute() the Statement and get the number of affected rows via rowCount().";
     }
     parent::__construct($message, $code, $previous);
   }
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
index a21f2646987d8bc25b0a5f0fb7938b4b6084ba33..289d2a993457cd1c581c14ddfe9c6d16f2af2ec7 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
@@ -932,6 +932,8 @@ protected function doSaveFieldItems(ContentEntityInterface $entity, array $names
         }
       }
       else {
+        // @todo Remove the 'return' option in Drupal 11.
+        // @see https://www.drupal.org/project/drupal/issues/3256524
         $insert_id = $this->database
           ->insert($this->baseTable, ['return' => Database::RETURN_INSERT_ID])
           ->fields((array) $record)
@@ -1135,6 +1137,8 @@ protected function saveRevision(ContentEntityInterface $entity) {
     $entity->preSaveRevision($this, $record);
 
     if ($entity->isNewRevision()) {
+      // @todo Remove the 'return' option in Drupal 11.
+      // @see https://www.drupal.org/project/drupal/issues/3256524
       $insert_id = $this->database
         ->insert($this->revisionTable, ['return' => Database::RETURN_INSERT_ID])
         ->fields((array) $record)
diff --git a/core/lib/Drupal/Core/Menu/MenuTreeStorage.php b/core/lib/Drupal/Core/Menu/MenuTreeStorage.php
index b343a2f36fed70153d11ade2fff37913d99e4979..b0d8787b4afd20d48409e80c73ef623d775bf966 100644
--- a/core/lib/Drupal/Core/Menu/MenuTreeStorage.php
+++ b/core/lib/Drupal/Core/Menu/MenuTreeStorage.php
@@ -314,6 +314,8 @@ protected function doSave(array $link) {
     try {
       if (!$original) {
         // Generate a new mlid.
+        // @todo Remove the 'return' option in Drupal 11.
+        // @see https://www.drupal.org/project/drupal/issues/3256524
         $options = ['return' => Database::RETURN_INSERT_ID] + $this->options;
         $link['mlid'] = $this->connection->insert($this->table, $options)
           ->fields(['id' => $link['id'], 'menu_name' => $link['menu_name']])
diff --git a/core/modules/mysql/src/Driver/Database/mysql/Connection.php b/core/modules/mysql/src/Driver/Database/mysql/Connection.php
index 4eb7700bf658e8f2c81206b75b1680cf4c4eab8a..b2de80be4f4a18d44c6d49b1bbf80d0074e07a84 100644
--- a/core/modules/mysql/src/Driver/Database/mysql/Connection.php
+++ b/core/modules/mysql/src/Driver/Database/mysql/Connection.php
@@ -343,7 +343,8 @@ public function mapConditionOperator($operator) {
   }
 
   public function nextId($existing_id = 0) {
-    $new_id = $this->query('INSERT INTO {sequences} () VALUES ()', [], ['return' => Database::RETURN_INSERT_ID]);
+    $this->query('INSERT INTO {sequences} () VALUES ()');
+    $new_id = $this->lastInsertId();
     // 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
@@ -354,7 +355,8 @@ public function nextId($existing_id = 0) {
       // 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->query('INSERT INTO {sequences} () VALUES ()');
+      $new_id = $this->lastInsertId();
     }
     $this->needsCleanup = TRUE;
     return $new_id;
diff --git a/core/modules/mysql/src/Driver/Database/mysql/Delete.php b/core/modules/mysql/src/Driver/Database/mysql/Delete.php
new file mode 100644
index 0000000000000000000000000000000000000000..764a99b133c224266fe76ac146229e6e06e14790
--- /dev/null
+++ b/core/modules/mysql/src/Driver/Database/mysql/Delete.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Drupal\mysql\Driver\Database\mysql;
+
+use Drupal\Core\Database\Query\Delete as QueryDelete;
+
+/**
+ * MySQL implementation of \Drupal\Core\Database\Query\Delete.
+ */
+class Delete extends QueryDelete {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(Connection $connection, string $table, array $options = []) {
+    // @todo Remove the __construct in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
+    parent::__construct($connection, $table, $options);
+    unset($this->queryOptions['return']);
+  }
+
+}
diff --git a/core/modules/mysql/src/Driver/Database/mysql/Insert.php b/core/modules/mysql/src/Driver/Database/mysql/Insert.php
index 616985210398294b360859b0574ebbc43a2ed08c..e14e6d0d2802d0d499031d8824389a5d6d7fe668 100644
--- a/core/modules/mysql/src/Driver/Database/mysql/Insert.php
+++ b/core/modules/mysql/src/Driver/Database/mysql/Insert.php
@@ -9,6 +9,16 @@
  */
 class Insert extends QueryInsert {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(Connection $connection, string $table, array $options = []) {
+    // @todo Remove the __construct in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
+    parent::__construct($connection, $table, $options);
+    unset($this->queryOptions['return']);
+  }
+
   public function execute() {
     if (!$this->preExecute()) {
       return NULL;
@@ -29,7 +39,14 @@ public function execute() {
       $values = $this->fromQuery->getArguments();
     }
 
-    $last_insert_id = $this->connection->query((string) $this, $values, $this->queryOptions);
+    $stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions);
+    try {
+      $stmt->execute($values, $this->queryOptions);
+      $last_insert_id = $this->connection->lastInsertId();
+    }
+    catch (\Exception $e) {
+      $this->connection->exceptionHandler()->handleExecutionException($e, $stmt, $values, $this->queryOptions);
+    }
 
     // Re-initialize the values array so that we can re-use this query.
     $this->insertValues = [];
diff --git a/core/modules/mysql/src/Driver/Database/mysql/Merge.php b/core/modules/mysql/src/Driver/Database/mysql/Merge.php
new file mode 100644
index 0000000000000000000000000000000000000000..f26090032ba2e2d88729c6b1fe26c9e01581acdc
--- /dev/null
+++ b/core/modules/mysql/src/Driver/Database/mysql/Merge.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Drupal\mysql\Driver\Database\mysql;
+
+use Drupal\Core\Database\Query\Merge as QueryMerge;
+
+/**
+ * MySQL implementation of \Drupal\Core\Database\Query\Merge.
+ */
+class Merge extends QueryMerge {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(Connection $connection, string $table, array $options = []) {
+    // @todo Remove the __construct in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
+    parent::__construct($connection, $table, $options);
+    unset($this->queryOptions['return']);
+  }
+
+}
diff --git a/core/modules/mysql/src/Driver/Database/mysql/Select.php b/core/modules/mysql/src/Driver/Database/mysql/Select.php
new file mode 100644
index 0000000000000000000000000000000000000000..534cf38ac24955f4f7ec5db9721d85fa1efc662c
--- /dev/null
+++ b/core/modules/mysql/src/Driver/Database/mysql/Select.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Drupal\mysql\Driver\Database\mysql;
+
+use Drupal\Core\Database\Query\Select as QuerySelect;
+
+/**
+ * MySQL implementation of \Drupal\Core\Database\Query\Select.
+ */
+class Select extends QuerySelect {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(Connection $connection, $table, $alias = NULL, array $options = []) {
+    // @todo Remove the __construct in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
+    parent::__construct($connection, $table, $alias, $options);
+    unset($this->queryOptions['return']);
+  }
+
+}
diff --git a/core/modules/mysql/src/Driver/Database/mysql/Truncate.php b/core/modules/mysql/src/Driver/Database/mysql/Truncate.php
new file mode 100644
index 0000000000000000000000000000000000000000..6682318f85e7bf458fab8b77ed040e9b9cadc82d
--- /dev/null
+++ b/core/modules/mysql/src/Driver/Database/mysql/Truncate.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Drupal\mysql\Driver\Database\mysql;
+
+use Drupal\Core\Database\Query\Truncate as QueryTruncate;
+
+/**
+ * MySQL implementation of \Drupal\Core\Database\Query\Truncate.
+ */
+class Truncate extends QueryTruncate {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(Connection $connection, string $table, array $options = []) {
+    // @todo Remove the __construct in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
+    parent::__construct($connection, $table, $options);
+    unset($this->queryOptions['return']);
+  }
+
+}
diff --git a/core/modules/mysql/src/Driver/Database/mysql/Update.php b/core/modules/mysql/src/Driver/Database/mysql/Update.php
new file mode 100644
index 0000000000000000000000000000000000000000..e0995d9c881c182467d6b9c5a25a5ff19955c3e1
--- /dev/null
+++ b/core/modules/mysql/src/Driver/Database/mysql/Update.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Drupal\mysql\Driver\Database\mysql;
+
+use Drupal\Core\Database\Query\Update as QueryUpdate;
+
+/**
+ * MySQL implementation of \Drupal\Core\Database\Query\Update.
+ */
+class Update extends QueryUpdate {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(Connection $connection, string $table, array $options = []) {
+    // @todo Remove the __construct in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
+    parent::__construct($connection, $table, $options);
+    unset($this->queryOptions['return']);
+  }
+
+}
diff --git a/core/modules/mysql/src/Driver/Database/mysql/Upsert.php b/core/modules/mysql/src/Driver/Database/mysql/Upsert.php
index 0e5f7d3b50e91e9558126917c32fad8c73ab7770..5b2a5929d09b0c6508b5087dd2b143aac1809666 100644
--- a/core/modules/mysql/src/Driver/Database/mysql/Upsert.php
+++ b/core/modules/mysql/src/Driver/Database/mysql/Upsert.php
@@ -9,6 +9,16 @@
  */
 class Upsert extends QueryUpsert {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(Connection $connection, string $table, array $options = []) {
+    // @todo Remove the __construct in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
+    parent::__construct($connection, $table, $options);
+    unset($this->queryOptions['return']);
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Delete.php b/core/modules/pgsql/src/Driver/Database/pgsql/Delete.php
index 9585a9c4bc52ffe3a31d6a602cb36f74f03adadf..78f7908956a3ac0abc42ce78e7a0b14a0fa39ee3 100644
--- a/core/modules/pgsql/src/Driver/Database/pgsql/Delete.php
+++ b/core/modules/pgsql/src/Driver/Database/pgsql/Delete.php
@@ -9,6 +9,16 @@
  */
 class Delete extends QueryDelete {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(Connection $connection, string $table, array $options = []) {
+    // @todo Remove the __construct in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
+    parent::__construct($connection, $table, $options);
+    unset($this->queryOptions['return']);
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Insert.php b/core/modules/pgsql/src/Driver/Database/pgsql/Insert.php
index 1b53274729d80376319945f41fac56cf112f5eae..3caea783d8f5209c4a8e7a70e42056d960c46901 100644
--- a/core/modules/pgsql/src/Driver/Database/pgsql/Insert.php
+++ b/core/modules/pgsql/src/Driver/Database/pgsql/Insert.php
@@ -3,7 +3,6 @@
 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
@@ -18,6 +17,16 @@
  */
 class Insert extends QueryInsert {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(Connection $connection, string $table, array $options = []) {
+    // @todo Remove the __construct in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
+    parent::__construct($connection, $table, $options);
+    unset($this->queryOptions['return']);
+  }
+
   public function execute() {
     if (!$this->preExecute()) {
       return NULL;
@@ -92,20 +101,9 @@ public function execute() {
       }
       $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;
+      $this->connection->exceptionHandler()->handleExecutionException($e, $stmt, [], $this->queryOptions);
     }
 
     // Re-initialize the values array so that we can re-use this query.
diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Merge.php b/core/modules/pgsql/src/Driver/Database/pgsql/Merge.php
new file mode 100644
index 0000000000000000000000000000000000000000..11fa63868669951f678617bc68676e6700609698
--- /dev/null
+++ b/core/modules/pgsql/src/Driver/Database/pgsql/Merge.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Drupal\pgsql\Driver\Database\pgsql;
+
+use Drupal\Core\Database\Query\Merge as QueryMerge;
+
+/**
+ * PostgreSQL implementation of \Drupal\Core\Database\Query\Merge.
+ */
+class Merge extends QueryMerge {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(Connection $connection, string $table, array $options = []) {
+    // @todo Remove the __construct in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
+    parent::__construct($connection, $table, $options);
+    unset($this->queryOptions['return']);
+  }
+
+}
diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Select.php b/core/modules/pgsql/src/Driver/Database/pgsql/Select.php
index 959b6092d92bdf558bc94523e58795464e51b5a5..dbd787027b81c9f35e02ab1609b11d2d13a83988 100644
--- a/core/modules/pgsql/src/Driver/Database/pgsql/Select.php
+++ b/core/modules/pgsql/src/Driver/Database/pgsql/Select.php
@@ -14,6 +14,16 @@
  */
 class Select extends QuerySelect {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(Connection $connection, $table, $alias = NULL, array $options = []) {
+    // @todo Remove the __construct in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
+    parent::__construct($connection, $table, $alias, $options);
+    unset($this->queryOptions['return']);
+  }
+
   public function orderRandom() {
     $alias = $this->addExpression('RANDOM()', 'random_field');
     $this->orderBy($alias);
diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Truncate.php b/core/modules/pgsql/src/Driver/Database/pgsql/Truncate.php
index 18115e0a7d0517564c5ad6a6b08040037570a5ab..102cceae4b51c760aa1376515285fc6ffdca0247 100644
--- a/core/modules/pgsql/src/Driver/Database/pgsql/Truncate.php
+++ b/core/modules/pgsql/src/Driver/Database/pgsql/Truncate.php
@@ -9,6 +9,16 @@
  */
 class Truncate extends QueryTruncate {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(Connection $connection, string $table, array $options = []) {
+    // @todo Remove the __construct in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
+    parent::__construct($connection, $table, $options);
+    unset($this->queryOptions['return']);
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Update.php b/core/modules/pgsql/src/Driver/Database/pgsql/Update.php
index d3f2ebf6431d7b844863701efed9190d2ac6b69e..c680097284ab0ae0c270166e3a704cbe749c8b25 100644
--- a/core/modules/pgsql/src/Driver/Database/pgsql/Update.php
+++ b/core/modules/pgsql/src/Driver/Database/pgsql/Update.php
@@ -10,6 +10,16 @@
  */
 class Update extends QueryUpdate {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(Connection $connection, string $table, array $options = []) {
+    // @todo Remove the __construct in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
+    parent::__construct($connection, $table, $options);
+    unset($this->queryOptions['return']);
+  }
+
   public function execute() {
     $max_placeholder = 0;
     $blobs = [];
diff --git a/core/modules/pgsql/src/Driver/Database/pgsql/Upsert.php b/core/modules/pgsql/src/Driver/Database/pgsql/Upsert.php
index 35823a270b1c8bd89f2cddba9482cff36da7a8f6..e738bf25a0a327283e95b8fe80c49b7aefa50431 100644
--- a/core/modules/pgsql/src/Driver/Database/pgsql/Upsert.php
+++ b/core/modules/pgsql/src/Driver/Database/pgsql/Upsert.php
@@ -11,6 +11,16 @@
  */
 class Upsert extends QueryUpsert {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(Connection $connection, string $table, array $options = []) {
+    // @todo Remove the __construct in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
+    parent::__construct($connection, $table, $options);
+    unset($this->queryOptions['return']);
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php b/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php
index 5a1ab3b9d42b6ea002e49acc6e2e3448c24d6e24..17fd2567d574f641076897ff8ce87f8ac31027a5 100644
--- a/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php
+++ b/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php
@@ -435,6 +435,10 @@ public function mapConditionOperator($operator) {
    * {@inheritdoc}
    */
   public function prepareStatement(string $query, array $options, bool $allow_row_count = FALSE): StatementInterface {
+    if (isset($options['return'])) {
+      @trigger_error('Passing "return" option to ' . __METHOD__ . '() is deprecated in drupal:9.4.0 and is removed in drupal:11.0.0. For data manipulation operations, use dynamic queries instead. See https://www.drupal.org/node/3185520', E_USER_DEPRECATED);
+    }
+
     try {
       $query = $this->preprocessStatement($query, $options);
       $statement = new Statement($this->connection, $this, $query, $options['pdo'] ?? [], $allow_row_count);
diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Delete.php b/core/modules/sqlite/src/Driver/Database/sqlite/Delete.php
new file mode 100644
index 0000000000000000000000000000000000000000..bdbe0138e7dfbdf5a109c118d56e1ea89bf3d641
--- /dev/null
+++ b/core/modules/sqlite/src/Driver/Database/sqlite/Delete.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Drupal\sqlite\Driver\Database\sqlite;
+
+use Drupal\Core\Database\Query\Delete as QueryDelete;
+
+/**
+ * SQLite implementation of \Drupal\Core\Database\Query\Delete.
+ */
+class Delete extends QueryDelete {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(Connection $connection, string $table, array $options = []) {
+    // @todo Remove the __construct in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
+    parent::__construct($connection, $table, $options);
+    unset($this->queryOptions['return']);
+  }
+
+}
diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Insert.php b/core/modules/sqlite/src/Driver/Database/sqlite/Insert.php
index d1cc245e273e5ff3bf81a1e803750a4651291e48..229fd03656a1d88f7992ebfcd624d80575694ee1 100644
--- a/core/modules/sqlite/src/Driver/Database/sqlite/Insert.php
+++ b/core/modules/sqlite/src/Driver/Database/sqlite/Insert.php
@@ -13,16 +13,61 @@
  */
 class Insert extends QueryInsert {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(Connection $connection, string $table, array $options = []) {
+    // @todo Remove the __construct in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
+    parent::__construct($connection, $table, $options);
+    unset($this->queryOptions['return']);
+  }
+
   public function execute() {
     if (!$this->preExecute()) {
       return NULL;
     }
-    if (count($this->insertFields) || !empty($this->fromQuery)) {
-      return parent::execute();
+
+    // 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)) {
+      // The SelectQuery may contain arguments, load and pass them through.
+      return $this->connection->query((string) $this, $this->fromQuery->getArguments(), $this->queryOptions);
+    }
+
+    // We wrap the insert in a transaction so that it is atomic where possible.
+    // In SQLite, this is also a notable performance boost.
+    $transaction = $this->connection->startTransaction();
+
+    if (count($this->insertFields)) {
+      // Each insert happens in its own query.
+      $stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions);
+      foreach ($this->insertValues as $insert_values) {
+        try {
+          $stmt->execute($insert_values, $this->queryOptions);
+        }
+        catch (\Exception $e) {
+          // One of the INSERTs failed, rollback the whole batch.
+          $transaction->rollBack();
+          $this->connection->exceptionHandler()->handleExecutionException($e, $stmt, $insert_values, $this->queryOptions);
+        }
+      }
+      // Re-initialize the values array so that we can re-use this query.
+      $this->insertValues = [];
     }
     else {
-      return $this->connection->query('INSERT INTO {' . $this->table . '} DEFAULT VALUES', [], $this->queryOptions);
+      $stmt = $this->connection->prepareStatement("INSERT INTO {{$this->table}} DEFAULT VALUES", $this->queryOptions);
+      try {
+        $stmt->execute(NULL, $this->queryOptions);
+      }
+      catch (\Exception $e) {
+        $transaction->rollBack();
+        $this->connection->exceptionHandler()->handleExecutionException($e, $stmt, [], $this->queryOptions);
+      }
     }
+
+    // Transaction commits here when $transaction looses scope.
+    return $this->connection->lastInsertId();
   }
 
   public function __toString() {
diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Merge.php b/core/modules/sqlite/src/Driver/Database/sqlite/Merge.php
new file mode 100644
index 0000000000000000000000000000000000000000..377720784f136b20fbcd6b4a391a65ffc8ee477e
--- /dev/null
+++ b/core/modules/sqlite/src/Driver/Database/sqlite/Merge.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Drupal\sqlite\Driver\Database\sqlite;
+
+use Drupal\Core\Database\Query\Merge as QueryMerge;
+
+/**
+ * SQLite implementation of \Drupal\Core\Database\Query\Merge.
+ */
+class Merge extends QueryMerge {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(Connection $connection, string $table, array $options = []) {
+    // @todo Remove the __construct in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
+    parent::__construct($connection, $table, $options);
+    unset($this->queryOptions['return']);
+  }
+
+}
diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Select.php b/core/modules/sqlite/src/Driver/Database/sqlite/Select.php
index 5ee521af8b22e24b27327b44245449d1b80654a9..fbd928065cf6bcebf60c05d96b7d6a7c7a89ba21 100644
--- a/core/modules/sqlite/src/Driver/Database/sqlite/Select.php
+++ b/core/modules/sqlite/src/Driver/Database/sqlite/Select.php
@@ -9,6 +9,16 @@
  */
 class Select extends QuerySelect {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(Connection $connection, $table, $alias = NULL, array $options = []) {
+    // @todo Remove the __construct in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
+    parent::__construct($connection, $table, $alias, $options);
+    unset($this->queryOptions['return']);
+  }
+
   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/Truncate.php b/core/modules/sqlite/src/Driver/Database/sqlite/Truncate.php
index f1535fb0196d8e5894b607c581d697d4a5f0518e..137e395b03fb6ff0670476d908b991c74b2603b3 100644
--- a/core/modules/sqlite/src/Driver/Database/sqlite/Truncate.php
+++ b/core/modules/sqlite/src/Driver/Database/sqlite/Truncate.php
@@ -12,6 +12,16 @@
  */
 class Truncate extends QueryTruncate {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(Connection $connection, string $table, array $options = []) {
+    // @todo Remove the __construct in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
+    parent::__construct($connection, $table, $options);
+    unset($this->queryOptions['return']);
+  }
+
   public function __toString() {
     // Create a sanitized comment string to prepend to the query.
     $comments = $this->connection->makeComment($this->comments);
diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Update.php b/core/modules/sqlite/src/Driver/Database/sqlite/Update.php
new file mode 100644
index 0000000000000000000000000000000000000000..d101ed384f75809cdd31543ea1557814fac04b7c
--- /dev/null
+++ b/core/modules/sqlite/src/Driver/Database/sqlite/Update.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Drupal\sqlite\Driver\Database\sqlite;
+
+use Drupal\Core\Database\Query\Update as QueryUpdate;
+
+/**
+ * SQLite implementation of \Drupal\Core\Database\Query\Update.
+ */
+class Update extends QueryUpdate {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(Connection $connection, string $table, array $options = []) {
+    // @todo Remove the __construct in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
+    parent::__construct($connection, $table, $options);
+    unset($this->queryOptions['return']);
+  }
+
+}
diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Upsert.php b/core/modules/sqlite/src/Driver/Database/sqlite/Upsert.php
index 59974272531008486cb2dcb35e0893954737922c..9e42fcf4baa064aa21eb6fa0371d3321dae06bd7 100644
--- a/core/modules/sqlite/src/Driver/Database/sqlite/Upsert.php
+++ b/core/modules/sqlite/src/Driver/Database/sqlite/Upsert.php
@@ -11,6 +11,16 @@
  */
 class Upsert extends QueryUpsert {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(Connection $connection, string $table, array $options = []) {
+    // @todo Remove the __construct in Drupal 11.
+    // @see https://www.drupal.org/project/drupal/issues/3256524
+    parent::__construct($connection, $table, $options);
+    unset($this->queryOptions['return']);
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/system/tests/modules/driver_test/driver_test.info.yml b/core/modules/system/tests/modules/driver_test/driver_test.info.yml
index c36161d27046414646a1a4ce35db99deb3c085b0..ad967305b36a321d0e647f9626ded5211fc912ee 100644
--- a/core/modules/system/tests/modules/driver_test/driver_test.info.yml
+++ b/core/modules/system/tests/modules/driver_test/driver_test.info.yml
@@ -3,3 +3,6 @@ type: module
 description: 'Support database contrib driver testing.'
 package: Testing
 version: VERSION
+dependencies:
+  - drupal:mysql
+  - drupal:pgsql
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Delete.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Delete.php
new file mode 100644
index 0000000000000000000000000000000000000000..9e0ca191345b19255121ae7377be37942c17161c
--- /dev/null
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Delete.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Drupal\driver_test\Driver\Database\DrivertestMysql;
+
+use Drupal\mysql\Driver\Database\mysql\Delete as CoreDelete;
+
+/**
+ * MySQL test implementation of \Drupal\Core\Database\Query\Delete.
+ */
+class Delete extends CoreDelete {}
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Merge.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Merge.php
new file mode 100644
index 0000000000000000000000000000000000000000..832d778567a9aadd5122d63327d6cc6b3620215a
--- /dev/null
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Merge.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Drupal\driver_test\Driver\Database\DrivertestMysql;
+
+use Drupal\mysql\Driver\Database\mysql\Merge as CoreMerge;
+
+/**
+ * MySQL test implementation of \Drupal\Core\Database\Query\Merge.
+ */
+class Merge extends CoreMerge {}
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Select.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Select.php
new file mode 100644
index 0000000000000000000000000000000000000000..b87333103e2112a5c1dbdafd36099ee7ee4b3235
--- /dev/null
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Select.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Drupal\driver_test\Driver\Database\DrivertestMysql;
+
+use Drupal\mysql\Driver\Database\mysql\Select as CoreSelect;
+
+/**
+ * MySQL test implementation of \Drupal\Core\Database\Query\Select.
+ */
+class Select extends CoreSelect {}
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Truncate.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Truncate.php
new file mode 100644
index 0000000000000000000000000000000000000000..24ac394cc7f7f6260407cf2f2fd650e7b57a3737
--- /dev/null
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Truncate.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Drupal\driver_test\Driver\Database\DrivertestMysql;
+
+use Drupal\mysql\Driver\Database\mysql\Truncate as CoreTruncate;
+
+/**
+ * MySQL test implementation of \Drupal\Core\Database\Query\Truncate.
+ */
+class Truncate extends CoreTruncate {}
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Update.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Update.php
new file mode 100644
index 0000000000000000000000000000000000000000..3f36bab73842380b978cb537f59a8151a4e13747
--- /dev/null
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysql/Update.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Drupal\driver_test\Driver\Database\DrivertestMysql;
+
+use Drupal\mysql\Driver\Database\mysql\Update as CoreUpdate;
+
+/**
+ * MySQL test implementation of \Drupal\Core\Database\Query\Update.
+ */
+class Update extends CoreUpdate {}
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Delete.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Delete.php
new file mode 100644
index 0000000000000000000000000000000000000000..25908104426f9d55404681c81d78d5d6adfb997e
--- /dev/null
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Delete.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Drupal\driver_test\Driver\Database\DrivertestMysqlDeprecatedVersion;
+
+use Drupal\mysql\Driver\Database\mysql\Delete as CoreDelete;
+
+/**
+ * MySQL test implementation of \Drupal\Core\Database\Query\Delete.
+ */
+class Delete extends CoreDelete {}
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Merge.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Merge.php
new file mode 100644
index 0000000000000000000000000000000000000000..d4f0fd8cd2825d0cea890e2626c4d57ed1f1b766
--- /dev/null
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Merge.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Drupal\driver_test\Driver\Database\DrivertestMysqlDeprecatedVersion;
+
+use Drupal\mysql\Driver\Database\mysql\Merge as CoreMerge;
+
+/**
+ * MySQL test implementation of \Drupal\Core\Database\Query\Merge.
+ */
+class Merge extends CoreMerge {}
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Select.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Select.php
new file mode 100644
index 0000000000000000000000000000000000000000..8b5867700593ac4394a8b34738f5bbffd7745885
--- /dev/null
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Select.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Drupal\driver_test\Driver\Database\DrivertestMysqlDeprecatedVersion;
+
+use Drupal\mysql\Driver\Database\mysql\Select as CoreSelect;
+
+/**
+ * MySQL test implementation of \Drupal\Core\Database\Query\Select.
+ */
+class Select extends CoreSelect {}
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Truncate.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Truncate.php
new file mode 100644
index 0000000000000000000000000000000000000000..fbdbb05b79fa15c52a9a51122a3a3cb7847aa4ac
--- /dev/null
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Truncate.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Drupal\driver_test\Driver\Database\DrivertestMysqlDeprecatedVersion;
+
+use Drupal\mysql\Driver\Database\mysql\Truncate as CoreTruncate;
+
+/**
+ * MySQL test implementation of \Drupal\Core\Database\Query\Truncate.
+ */
+class Truncate extends CoreTruncate {}
diff --git a/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Update.php b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Update.php
new file mode 100644
index 0000000000000000000000000000000000000000..9d8c63905e0a5d77a3f345df4241f16c71dc1637
--- /dev/null
+++ b/core/modules/system/tests/modules/driver_test/src/Driver/Database/DrivertestMysqlDeprecatedVersion/Update.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Drupal\driver_test\Driver\Database\DrivertestMysqlDeprecatedVersion;
+
+use Drupal\mysql\Driver\Database\mysql\Update as CoreUpdate;
+
+/**
+ * MySQL test implementation of \Drupal\Core\Database\Query\Update.
+ */
+class Update extends CoreUpdate {}
diff --git a/core/tests/Drupal/KernelTests/Core/Database/QueryTest.php b/core/tests/Drupal/KernelTests/Core/Database/QueryTest.php
index 32f3a78d0b4a37c71ba815a6aa4df8a24d824bea..d8e2188095b62dcab940e6cc860ab225203db258 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/QueryTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/QueryTest.php
@@ -2,6 +2,10 @@
 
 namespace Drupal\KernelTests\Core\Database;
 
+use Drupal\Core\Database\Database;
+
+// cSpell:ignore aquery aprepare
+
 /**
  * Tests Drupal's extended prepared statement syntax..
  *
@@ -160,4 +164,22 @@ public function testQuotingIdentifiers() {
     $this->assertEquals('Update value 1', $result->update);
   }
 
+  /**
+   * Tests deprecation of the 'return' query option.
+   *
+   * @covers ::query
+   * @covers ::prepareStatement
+   *
+   * @group legacy
+   */
+  public function testReturnOptionDeprecation() {
+    $this->expectDeprecation('Passing "return" option to %Aquery() is deprecated in drupal:9.4.0 and is removed in drupal:11.0.0. For data manipulation operations, use dynamic queries instead. See https://www.drupal.org/node/3185520');
+    $this->expectDeprecation('Passing "return" option to %AprepareStatement() is deprecated in drupal:9.4.0 and is removed in drupal:11.0.0. For data manipulation operations, use dynamic queries instead. See https://www.drupal.org/node/3185520');
+    $this->assertIsInt((int) $this->connection->query('INSERT INTO {test} ([name], [age], [job]) VALUES (:name, :age, :job)', [
+      ':name' => 'Magoo',
+      ':age' => 56,
+      ':job' => 'Driver',
+    ], ['return' => Database::RETURN_INSERT_ID]));
+  }
+
 }
diff --git a/core/tests/Drupal/KernelTests/Core/Database/StatementTest.php b/core/tests/Drupal/KernelTests/Core/Database/StatementTest.php
index b0071a4bf07fb7487b876fe0c8a51e516bed89f8..f1c0cc806023c319641b54b0e01ae98fa1b3ae96 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/StatementTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/StatementTest.php
@@ -2,7 +2,6 @@
 
 namespace Drupal\KernelTests\Core\Database;
 
-use Drupal\Core\Database\Database;
 use Drupal\Core\Database\StatementInterface;
 
 /**
@@ -24,7 +23,6 @@ public function testRepeatedInsertStatementReuse() {
       ':age' => '30',
     ];
     $options = [
-      'return' => Database::RETURN_STATEMENT,
       'allow_square_brackets' => FALSE,
     ];
 
diff --git a/core/tests/Drupal/Tests/Core/Database/RowCountExceptionTest.php b/core/tests/Drupal/Tests/Core/Database/RowCountExceptionTest.php
index 10bfc0b726719486c644c2639785f9e4e6020e20..593054d19ca37712609a8b6c39162c3f4e035b07 100644
--- a/core/tests/Drupal/Tests/Core/Database/RowCountExceptionTest.php
+++ b/core/tests/Drupal/Tests/Core/Database/RowCountExceptionTest.php
@@ -15,7 +15,7 @@ class RowCountExceptionTest extends UnitTestCase {
   /**
    * The default exception message.
    */
-  private const DEFAULT_EXCEPTION_MESSAGE = "rowCount() is supported for DELETE, INSERT, or UPDATE statements performed with structured query builders only, since they would not be portable across database engines otherwise. If the query builders are not sufficient, set the 'return' option to Database::RETURN_AFFECTED to get the number of affected rows.";
+  private const DEFAULT_EXCEPTION_MESSAGE = "rowCount() is supported for DELETE, INSERT, or UPDATE statements performed with structured query builders only, since they would not be portable across database engines otherwise. If the query builders are not sufficient, use a prepareStatement() with an \$allow_row_count argument set to TRUE, execute() the Statement and get the number of affected rows via rowCount().";
 
   /**
    * Data provider for ::testExceptionMessage()