diff --git a/core/lib/Drupal/Core/Database/Connection.php b/core/lib/Drupal/Core/Database/Connection.php
index f28f60a521026bce85946f3d866c7de6fb1861d7..60991cbbdb6df32f1d7203b88e11a6b6663df29d 100644
--- a/core/lib/Drupal/Core/Database/Connection.php
+++ b/core/lib/Drupal/Core/Database/Connection.php
@@ -12,6 +12,7 @@
 use Drupal\Core\Database\Query\Select;
 use Drupal\Core\Database\Query\Truncate;
 use Drupal\Core\Database\Query\Update;
+use Drupal\Core\Database\Statement\FetchAs;
 use Drupal\Core\Database\Transaction\TransactionManagerInterface;
 use Drupal\Core\Pager\PagerManagerInterface;
 
@@ -250,11 +251,10 @@ public function getClientConnection(): object {
    * A given query can be customized with a number of option flags in an
    * associative array:
    * - fetch: This element controls how rows from a result set will be
-   *   returned. Legal values include \PDO::FETCH_ASSOC, \PDO::FETCH_BOTH,
-   *   \PDO::FETCH_OBJ, \PDO::FETCH_NUM, or a string representing the name of a
-   *   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
+   *   returned. Legal values include one of the enumeration cases of FetchAs or
+   *   a string representing the name of a 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 described in the FetchAs enum.
    * - allow_delimiter_in_query: By default, queries which have the ; delimiter
    *   any place in them will cause an exception. This reduces the chance of SQL
    *   injection attacks that terminate the original query and add one or more
@@ -277,7 +277,7 @@ public function getClientConnection(): object {
    */
   protected function defaultOptions() {
     return [
-      'fetch' => \PDO::FETCH_OBJ,
+      'fetch' => FetchAs::Object,
       'allow_delimiter_in_query' => FALSE,
       'allow_square_brackets' => FALSE,
       'pdo' => [],
@@ -432,6 +432,9 @@ public function getFullQualifiedTableName($table) {
    */
   public function prepareStatement(string $query, array $options, bool $allow_row_count = FALSE): StatementInterface {
     assert(!isset($options['return']), 'Passing "return" option to prepareStatement() has no effect. See https://www.drupal.org/node/3185520');
+    if (isset($options['fetch']) && is_int($options['fetch'])) {
+      @trigger_error("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED);
+    }
 
     try {
       $query = $this->preprocessStatement($query, $options);
@@ -650,6 +653,9 @@ public function query($query, array $args = [], $options = []) {
     assert(is_string($query), 'The \'$query\' argument to ' . __METHOD__ . '() must be a string');
     assert(!isset($options['return']), 'Passing "return" option to query() has no effect. See https://www.drupal.org/node/3185520');
     assert(!isset($options['target']), 'Passing "target" option to query() has no effect. See https://www.drupal.org/node/2993033');
+    if (isset($options['fetch']) && is_int($options['fetch'])) {
+      @trigger_error("Passing the 'fetch' key as an integer to \$options in query() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED);
+    }
 
     // Use default values if not already set.
     $options += $this->defaultOptions();
diff --git a/core/lib/Drupal/Core/Database/Statement/FetchAs.php b/core/lib/Drupal/Core/Database/Statement/FetchAs.php
new file mode 100644
index 0000000000000000000000000000000000000000..051ed412a2b1c1bae4382c7a75ef3d6d018b6235
--- /dev/null
+++ b/core/lib/Drupal/Core/Database/Statement/FetchAs.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Core\Database\Statement;
+
+/**
+ * Enumeration of the fetch modes for result sets.
+ */
+enum FetchAs {
+
+  // Returns an anonymous object with property names that correspond to the
+  // column names returned in the result set. This is the default fetch mode
+  // for Drupal.
+  case Object;
+
+  // Returns a new instance of a requested class, mapping the columns of the
+  // result set to named properties in the class.
+  case ClassObject;
+
+  // Returns an array indexed by column name as returned in the result set.
+  case Associative;
+
+  // Returns an array indexed by column number as returned in the result set,
+  // starting at column 0.
+  case List;
+
+  // Returns a single column from the next row of a result set.
+  case Column;
+
+}
diff --git a/core/lib/Drupal/Core/Database/Statement/PdoTrait.php b/core/lib/Drupal/Core/Database/Statement/PdoTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..064c882b7f11c7f665148874f4c0fe46358d9369
--- /dev/null
+++ b/core/lib/Drupal/Core/Database/Statement/PdoTrait.php
@@ -0,0 +1,229 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Core\Database\Statement;
+
+/**
+ * A trait for calling \PDOStatement methods.
+ */
+trait PdoTrait {
+
+  /**
+   * Converts a FetchAs mode to a \PDO::FETCH_* constant value.
+   *
+   * @param \Drupal\Core\Database\FetchAs $mode
+   *   The FetchAs mode.
+   *
+   * @return int
+   *   A \PDO::FETCH_* constant value.
+   */
+  protected function fetchAsToPdo(FetchAs $mode): int {
+    return match ($mode) {
+      FetchAs::Associative => \PDO::FETCH_ASSOC,
+      FetchAs::ClassObject => \PDO::FETCH_CLASS | \PDO::FETCH_PROPS_LATE,
+      FetchAs::Column => \PDO::FETCH_COLUMN,
+      FetchAs::List => \PDO::FETCH_NUM,
+      FetchAs::Object => \PDO::FETCH_OBJ,
+    };
+  }
+
+  /**
+   * Converts a \PDO::FETCH_* constant value to a FetchAs mode.
+   *
+   * @param int $mode
+   *   The \PDO::FETCH_* constant value.
+   *
+   * @return \Drupal\Core\Database\FetchAs
+   *   A FetchAs mode.
+   */
+  protected function pdoToFetchAs(int $mode): FetchAs {
+    return match ($mode) {
+      \PDO::FETCH_ASSOC => FetchAs::Associative,
+      \PDO::FETCH_CLASS, \PDO::FETCH_CLASS | \PDO::FETCH_PROPS_LATE => FetchAs::ClassObject,
+      \PDO::FETCH_COLUMN => FetchAs::Column,
+      \PDO::FETCH_NUM => FetchAs::List,
+      \PDO::FETCH_OBJ => FetchAs::Object,
+      default => throw new \RuntimeException('Fetch mode ' . ($this->fetchModeLiterals[$mode] ?? $mode) . ' is not supported. Use supported modes only.'),
+    };
+  }
+
+  /**
+   * Returns the client-level database PDO statement object.
+   *
+   * This method should normally be used only within database driver code.
+   *
+   * @return \PDOStatement
+   *   The client-level database PDO statement.
+   *
+   * @throws \RuntimeException
+   *   If the client-level statement is not set.
+   */
+  public function getClientStatement(): \PDOStatement {
+    if (isset($this->clientStatement)) {
+      assert($this->clientStatement instanceof \PDOStatement);
+      return $this->clientStatement;
+    }
+    throw new \LogicException('\\PDOStatement not initialized');
+  }
+
+  /**
+   * Sets the default fetch mode for the PDO statement.
+   *
+   * @param \Drupal\Core\Database\FetchAs $mode
+   *   One of the cases of the FetchAs enum.
+   * @param int|class-string|null $columnOrClass
+   *   If $mode is FetchAs::Column, the index of the column to fetch.
+   *   If $mode is FetchAs::ClassObject, the FQCN of the object.
+   * @param list<mixed>|null $constructorArguments
+   *   If $mode is FetchAs::ClassObject, the arguments to pass to the
+   *   constructor.
+   *
+   * @return bool
+   *   Returns true on success or false on failure.
+   */
+  protected function clientSetFetchMode(FetchAs $mode, int|string|null $columnOrClass = NULL, array|null $constructorArguments = NULL): bool {
+    return match ($mode) {
+      FetchAs::Column => $this->getClientStatement()->setFetchMode(
+        \PDO::FETCH_COLUMN,
+        $columnOrClass ?? $this->fetchOptions['column'],
+      ),
+      FetchAs::ClassObject => $this->getClientStatement()->setFetchMode(
+        \PDO::FETCH_CLASS,
+        $columnOrClass ?? $this->fetchOptions['class'],
+        $constructorArguments ?? $this->fetchOptions['constructor_args'],
+      ),
+      default => $this->getClientStatement()->setFetchMode(
+        $this->fetchAsToPdo($mode),
+      ),
+    };
+  }
+
+  /**
+   * Executes the prepared PDO statement.
+   *
+   * @param array|null $arguments
+   *   An array of values with as many elements as there are bound parameters in
+   *   the SQL statement being executed. This can be NULL.
+   * @param array $options
+   *   An array of options for this query.
+   *
+   * @return bool
+   *   TRUE on success, or FALSE on failure.
+   */
+  protected function clientExecute(?array $arguments = [], array $options = []): bool {
+    return $this->getClientStatement()->execute($arguments);
+  }
+
+  /**
+   * Fetches the next row from the PDO statement.
+   *
+   * @param \Drupal\Core\Database\FetchAs|null $mode
+   *   (Optional) one of the cases of the FetchAs enum. If not specified,
+   *   defaults to what is specified by setFetchMode().
+   * @param int|null $cursorOrientation
+   *   Not implemented in all database drivers, don't use.
+   * @param int|null $cursorOffset
+   *   Not implemented in all database drivers, don't use.
+   *
+   * @return array<scalar|null>|object|scalar|null|false
+   *   A result, formatted according to $mode, or FALSE on failure.
+   */
+  protected function clientFetch(?FetchAs $mode = NULL, ?int $cursorOrientation = NULL, ?int $cursorOffset = NULL): array|object|int|float|string|bool|NULL {
+    return match(func_num_args()) {
+      0 => $this->getClientStatement()->fetch(),
+      1 => $this->getClientStatement()->fetch($this->fetchAsToPdo($mode)),
+      2 => $this->getClientStatement()->fetch($this->fetchAsToPdo($mode), $cursorOrientation),
+      default => $this->getClientStatement()->fetch($this->fetchAsToPdo($mode), $cursorOrientation, $cursorOffset),
+    };
+  }
+
+  /**
+   * Returns a single column from the next row of a result set.
+   *
+   * @param int $column
+   *   0-indexed number of the column to retrieve from the row. If no value is
+   *   supplied, the first column is fetched.
+   *
+   * @return scalar|null|false
+   *   A single column from the next row of a result set or false if there are
+   *   no more rows.
+   */
+  protected function clientFetchColumn(int $column = 0): int|float|string|bool|NULL {
+    return $this->getClientStatement()->fetchColumn($column);
+  }
+
+  /**
+   * Fetches the next row and returns it as an object.
+   *
+   * @param class-string|null $class
+   *   FQCN of the class to be instantiated.
+   * @param list<mixed>|null $constructorArguments
+   *   The arguments to be passed to the constructor.
+   *
+   * @return object|false
+   *   An instance of the required class with property names that correspond
+   *   to the column names, or FALSE on failure.
+   */
+  protected function clientFetchObject(?string $class = NULL, array $constructorArguments = []): object|FALSE {
+    if ($class) {
+      return $this->getClientStatement()->fetchObject($class, $constructorArguments);
+    }
+    return $this->getClientStatement()->fetchObject();
+  }
+
+  /**
+   * Returns an array containing all of the result set rows.
+   *
+   * @param \Drupal\Core\Database\FetchAs|null $mode
+   *   (Optional) one of the cases of the FetchAs enum. If not specified,
+   *   defaults to what is specified by setFetchMode().
+   * @param int|class-string|null $columnOrClass
+   *   If $mode is FetchAs::Column, the index of the column to fetch.
+   *   If $mode is FetchAs::ClassObject, the FQCN of the object.
+   * @param list<mixed>|null $constructorArguments
+   *   If $mode is FetchAs::ClassObject, the arguments to pass to the
+   *   constructor.
+   *
+   * @return array<array<scalar|null>|object|scalar|null>
+   *   An array of results.
+   */
+  // phpcs:ignore Drupal.Commenting.FunctionComment.InvalidReturn, Drupal.Commenting.FunctionComment.Missing
+  protected function clientFetchAll(?FetchAs $mode = NULL, int|string|null $columnOrClass = NULL, array|null $constructorArguments = NULL): array {
+    return match ($mode) {
+      FetchAs::Column => $this->getClientStatement()->fetchAll(
+        \PDO::FETCH_COLUMN,
+        $columnOrClass ?? $this->fetchOptions['column'],
+      ),
+      FetchAs::ClassObject => $this->getClientStatement()->fetchAll(
+        \PDO::FETCH_CLASS,
+        $columnOrClass ?? $this->fetchOptions['class'],
+        $constructorArguments ?? $this->fetchOptions['constructor_args'],
+      ),
+      default => $this->getClientStatement()->fetchAll(
+        $this->fetchAsToPdo($mode ?? $this->defaultFetchMode),
+      ),
+    };
+  }
+
+  /**
+   * Returns the number of rows affected by the last SQL statement.
+   *
+   * @return int
+   *   The number of rows.
+   */
+  protected function clientRowCount(): int {
+    return $this->getClientStatement()->rowCount();
+  }
+
+  /**
+   * Returns the query string used to prepare the statement.
+   *
+   * @return string
+   *   The query string.
+   */
+  protected function clientQueryString(): string {
+    return $this->getClientStatement()->queryString;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Database/StatementInterface.php b/core/lib/Drupal/Core/Database/StatementInterface.php
index c7d19201ffa685de4abe3ce511b210645a6abe0f..afef8ea9932dd4a5bafa7825af9d42851a59c2a1 100644
--- a/core/lib/Drupal/Core/Database/StatementInterface.php
+++ b/core/lib/Drupal/Core/Database/StatementInterface.php
@@ -65,18 +65,15 @@ public function rowCount();
   /**
    * Sets the default fetch mode for this statement.
    *
-   * See http://php.net/manual/pdo.constants.php for the definition of the
-   * constants used.
-   *
-   * @param int $mode
-   *   One of the \PDO::FETCH_* constants.
-   * @param int|null $a1
+   * @param \Drupal\Core\Database\FetchAs|int $mode
+   *   One of the cases of the FetchAs enum, or (deprecated) a \PDO::FETCH_*
+   *   constant.
+   * @param string|int|null $a1
    *   An option depending of the fetch mode specified by $mode:
-   *   - for \PDO::FETCH_COLUMN, the index of the column to fetch
-   *   - for \PDO::FETCH_CLASS, the name of the class to create
-   *   - for \PDO::FETCH_INTO, the object to add the data to
-   * @param array $a2
-   *   If $mode is \PDO::FETCH_CLASS, the optional arguments to pass to the
+   *   - for FetchAs::Column, the index of the column to fetch;
+   *   - for FetchAs::ClassObject, the name of the class to create.
+   * @param list<mixed> $a2
+   *   If $mode is FetchAs::ClassObject, the optional arguments to pass to the
    *   constructor.
    */
   public function setFetchMode($mode, $a1 = NULL, $a2 = []);
@@ -84,12 +81,10 @@ public function setFetchMode($mode, $a1 = NULL, $a2 = []);
   /**
    * Fetches the next row from a result set.
    *
-   * See http://php.net/manual/pdo.constants.php for the definition of the
-   * constants used.
-   *
-   * @param int $mode
-   *   One of the \PDO::FETCH_* constants.
-   *   Default to what was specified by setFetchMode().
+   * @param \Drupal\Core\Database\FetchAs|int|null $mode
+   *   (Optional) one of the cases of the FetchAs enum, or (deprecated) a
+   *   \PDO::FETCH_* constant. If not specified, defaults to what is specified
+   *   by setFetchMode().
    * @param int|null $cursor_orientation
    *   Not implemented in all database drivers, don't use.
    * @param int|null $cursor_offset
@@ -146,12 +141,14 @@ public function fetchAssoc();
   /**
    * Returns an array containing all of the result set rows.
    *
-   * @param int|null $mode
-   *   One of the \PDO::FETCH_* constants.
+   * @param \Drupal\Core\Database\FetchAs|int|null $mode
+   *   (Optional) one of the cases of the FetchAs enum, or (deprecated) a
+   *   \PDO::FETCH_* constant. If not specified, defaults to what is specified
+   *   by setFetchMode().
    * @param int|null $column_index
-   *   If $mode is \PDO::FETCH_COLUMN, the index of the column to fetch.
+   *   If $mode is FetchAs::Column, the index of the column to fetch.
    * @param array $constructor_arguments
-   *   If $mode is \PDO::FETCH_CLASS, the arguments to pass to the constructor.
+   *   If $mode is FetchAs::ClassObject, the arguments to pass to the constructor.
    *
    * @return array
    *   An array of results.
@@ -202,11 +199,12 @@ public function fetchAllKeyed($key_index = 0, $value_index = 1);
    *
    * @param string $key
    *   The name of the field on which to index the array.
-   * @param int|null $fetch
-   *   The fetch mode to use. If set to \PDO::FETCH_ASSOC, \PDO::FETCH_NUM, or
-   *   \PDO::FETCH_BOTH the returned value with be an array of arrays. For any
-   *   other value it will be an array of objects. By default, the fetch mode
-   *   set for the query will be used.
+   * @param \Drupal\Core\Database\FetchAs|int|string|null $fetch
+   *   (Optional) the fetch mode to use. One of the cases of the FetchAs enum,
+   *   or (deprecated) a \PDO::FETCH_* constant. If set to FetchAs::Associative
+   *   or FetchAs::List the returned value with be an array of arrays. For any
+   *   other value it will be an array of objects. If not specified, defaults to
+   *   what is specified by setFetchMode().
    *
    * @return array
    *   An associative array, or an empty array if there is no result set.
diff --git a/core/lib/Drupal/Core/Database/StatementPrefetchIterator.php b/core/lib/Drupal/Core/Database/StatementPrefetchIterator.php
index 8d6a1b3906dc8ddd89914bba35a58f9274df3b5f..70209b2d12e06a48db2db1361d0060a10add065e 100644
--- a/core/lib/Drupal/Core/Database/StatementPrefetchIterator.php
+++ b/core/lib/Drupal/Core/Database/StatementPrefetchIterator.php
@@ -5,6 +5,8 @@
 use Drupal\Core\Database\Event\StatementExecutionEndEvent;
 use Drupal\Core\Database\Event\StatementExecutionFailureEvent;
 use Drupal\Core\Database\Event\StatementExecutionStartEvent;
+use Drupal\Core\Database\Statement\FetchAs;
+use Drupal\Core\Database\Statement\PdoTrait;
 
 /**
  * An implementation of StatementInterface that prefetches all data.
@@ -15,13 +17,21 @@
  */
 class StatementPrefetchIterator implements \Iterator, StatementInterface {
 
-  use StatementIteratorTrait;
   use FetchModeTrait;
+  use PdoTrait;
+  use StatementIteratorTrait;
+
+  /**
+   * The client database Statement object.
+   *
+   * For a \PDO client connection, this will be a \PDOStatement object.
+   */
+  protected ?object $clientStatement;
 
   /**
    * Main data store.
    *
-   * The resultset is stored as a \PDO::FETCH_ASSOC array.
+   * The resultset is stored as a FetchAs::Associative array.
    */
   protected array $data = [];
 
@@ -39,18 +49,27 @@ class StatementPrefetchIterator implements \Iterator, StatementInterface {
 
   /**
    * Holds the default fetch style.
+   *
+   * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use
+   * $defaultFetchMode instead.
+   *
+   * @see https://www.drupal.org/node/3488338
    */
   protected int $defaultFetchStyle = \PDO::FETCH_OBJ;
 
+  /**
+   * Holds the default fetch mode.
+   */
+  protected FetchAs $defaultFetchMode = FetchAs::Object;
+
   /**
    * Holds fetch options.
    *
-   * @var string[]
+   * @var array{'class': class-string, 'constructor_args': array<mixed>, 'column': int}
    */
   protected array $fetchOptions = [
     'class' => 'stdClass',
     'constructor_args' => [],
-    'object' => NULL,
     'column' => 0,
   ];
 
@@ -88,12 +107,16 @@ public function getConnectionTarget(): string {
    * {@inheritdoc}
    */
   public function execute($args = [], $options = []) {
+    if (isset($options['fetch']) && is_int($options['fetch'])) {
+      @trigger_error("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED);
+    }
+
     if (isset($options['fetch'])) {
       if (is_string($options['fetch'])) {
         // Default to an object. Note: db fields will be added to the object
         // before the constructor is run. If you need to assign fields after
         // the constructor is run. See https://www.drupal.org/node/315092.
-        $this->setFetchMode(\PDO::FETCH_CLASS, $options['fetch']);
+        $this->setFetchMode(FetchAs::ClassObject, $options['fetch']);
       }
       else {
         $this->setFetchMode($options['fetch']);
@@ -114,8 +137,8 @@ public function execute($args = [], $options = []) {
 
     // Prepare and execute the statement.
     try {
-      $statement = $this->getStatement($this->queryString, $args);
-      $return = $statement->execute($args);
+      $this->clientStatement = $this->getStatement($this->queryString, $args);
+      $return = $this->clientExecute($args, $options);
     }
     catch (\Exception $e) {
       if (isset($startEvent) && $this->connection->isEventEnabled(StatementExecutionFailureEvent::class)) {
@@ -132,16 +155,17 @@ public function execute($args = [], $options = []) {
           $e->getMessage(),
         ));
       }
+      unset($this->clientStatement);
       throw $e;
     }
 
     // Fetch all the data from the reply, in order to release any lock as soon
     // as possible.
-    $this->data = $statement->fetchAll(\PDO::FETCH_ASSOC);
-    $this->rowCount = $this->rowCountEnabled ? $statement->rowCount() : NULL;
+    $this->data = $this->clientFetchAll(FetchAs::Associative);
+    $this->rowCount = $this->rowCountEnabled ? $this->clientRowCount() : NULL;
     // Destroy the statement as soon as possible. See the documentation of
     // \Drupal\sqlite\Driver\Database\sqlite\Statement for an explanation.
-    unset($statement);
+    unset($this->clientStatement);
     $this->markResultsetIterable($return);
 
     $this->columnNames = count($this->data) > 0 ? array_keys($this->data[0]) : [];
@@ -190,24 +214,27 @@ public function getQueryString() {
    * {@inheritdoc}
    */
   public function setFetchMode($mode, $a1 = NULL, $a2 = []) {
-    assert(in_array($mode, $this->supportedFetchModes), 'Fetch mode ' . ($this->fetchModeLiterals[$mode] ?? $mode) . ' is not supported. Use supported modes only.');
+    if (is_int($mode)) {
+      @trigger_error("Passing the \$mode argument as an integer to setFetchMode() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED);
+      $mode = $this->pdoToFetchAs($mode);
+    }
 
-    $this->defaultFetchStyle = $mode;
+    $this->defaultFetchMode = $mode;
+    // @todo Remove backwards compatibility statement below in drupal:12.0.0.
+    // @phpstan-ignore property.deprecated
+    $this->defaultFetchStyle = $this->fetchAsToPdo($mode);
     switch ($mode) {
-      case \PDO::FETCH_CLASS:
+      case FetchAs::ClassObject:
         $this->fetchOptions['class'] = $a1;
         if ($a2) {
           $this->fetchOptions['constructor_args'] = $a2;
         }
         break;
 
-      case \PDO::FETCH_COLUMN:
+      case FetchAs::Column:
         $this->fetchOptions['column'] = $a1;
         break;
 
-      case \PDO::FETCH_INTO:
-        $this->fetchOptions['object'] = $a1;
-        break;
     }
   }
 
@@ -228,6 +255,11 @@ public function rowCount() {
    * {@inheritdoc}
    */
   public function fetch($fetch_style = NULL, $cursor_orientation = \PDO::FETCH_ORI_NEXT, $cursor_offset = NULL) {
+    if (is_int($fetch_style)) {
+      @trigger_error("Passing the \$fetch_style argument as an integer to fetch() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED);
+      $fetch_style = $this->pdoToFetchAs($fetch_style);
+    }
+
     $currentKey = $this->getResultsetCurrentRowIndex();
 
     // We can remove the current record from the prefetched data, before
@@ -242,14 +274,13 @@ public function fetch($fetch_style = NULL, $cursor_orientation = \PDO::FETCH_ORI
     // Now, format the next prefetched record according to the required fetch
     // style.
     $rowAssoc = $this->data[$currentKey];
-    $mode = $fetch_style ?? $this->defaultFetchStyle;
+    $mode = $fetch_style ?? $this->defaultFetchMode;
     $row = match($mode) {
-      \PDO::FETCH_ASSOC => $rowAssoc,
-      \PDO::FETCH_CLASS, \PDO::FETCH_CLASS | \PDO::FETCH_PROPS_LATE => $this->assocToClass($rowAssoc, $this->fetchOptions['class'], $this->fetchOptions['constructor_args']),
-      \PDO::FETCH_COLUMN => $this->assocToColumn($rowAssoc, $this->columnNames, $this->fetchOptions['column']),
-      \PDO::FETCH_NUM => $this->assocToNum($rowAssoc),
-      \PDO::FETCH_OBJ => $this->assocToObj($rowAssoc),
-      default => throw new DatabaseExceptionWrapper('Fetch mode ' . ($this->fetchModeLiterals[$mode] ?? $mode) . ' is not supported. Use supported modes only.'),
+      FetchAs::Associative => $rowAssoc,
+      FetchAs::ClassObject => $this->assocToClass($rowAssoc, $this->fetchOptions['class'], $this->fetchOptions['constructor_args']),
+      FetchAs::Column => $this->assocToColumn($rowAssoc, $this->columnNames, $this->fetchOptions['column']),
+      FetchAs::List => $this->assocToNum($rowAssoc),
+      FetchAs::Object => $this->assocToObj($rowAssoc),
     };
     $this->setResultsetCurrentRow($row);
     return $row;
@@ -270,7 +301,7 @@ public function fetchColumn($index = 0) {
    * {@inheritdoc}
    */
   public function fetchField($index = 0) {
-    if ($row = $this->fetch(\PDO::FETCH_ASSOC)) {
+    if ($row = $this->fetch(FetchAs::Associative)) {
       return $this->assocToColumn($row, $this->columnNames, $index);
     }
     return FALSE;
@@ -281,30 +312,32 @@ public function fetchField($index = 0) {
    */
   public function fetchObject(?string $class_name = NULL, array $constructor_arguments = []) {
     if (!isset($class_name)) {
-      return $this->fetch(\PDO::FETCH_OBJ);
+      return $this->fetch(FetchAs::Object);
     }
     $this->fetchOptions = [
       'class' => $class_name,
       'constructor_args' => $constructor_arguments,
     ];
-    return $this->fetch(\PDO::FETCH_CLASS);
+    return $this->fetch(FetchAs::ClassObject);
   }
 
   /**
    * {@inheritdoc}
    */
   public function fetchAssoc() {
-    return $this->fetch(\PDO::FETCH_ASSOC);
+    return $this->fetch(FetchAs::Associative);
   }
 
   /**
    * {@inheritdoc}
    */
   public function fetchAll($mode = NULL, $column_index = NULL, $constructor_arguments = NULL) {
-    $fetchStyle = $mode ?? $this->defaultFetchStyle;
-
-    assert(in_array($fetchStyle, $this->supportedFetchModes), 'Fetch mode ' . ($this->fetchModeLiterals[$fetchStyle] ?? $fetchStyle) . ' is not supported. Use supported modes only.');
+    if (is_int($mode)) {
+      @trigger_error("Passing the \$mode argument as an integer to fetchAll() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED);
+      $mode = $this->pdoToFetchAs($mode);
+    }
 
+    $fetchStyle = $mode ?? $this->defaultFetchMode;
     if (isset($column_index)) {
       $this->fetchOptions['column'] = $column_index;
     }
@@ -342,7 +375,7 @@ public function fetchAllKeyed($key_index = 0, $value_index = 1) {
     $value = $this->columnNames[$value_index];
 
     $result = [];
-    while ($row = $this->fetch(\PDO::FETCH_ASSOC)) {
+    while ($row = $this->fetch(FetchAs::Associative)) {
       $result[$row[$key]] = $row[$value];
     }
     return $result;
@@ -351,11 +384,14 @@ public function fetchAllKeyed($key_index = 0, $value_index = 1) {
   /**
    * {@inheritdoc}
    */
-  public function fetchAllAssoc($key, $fetch_style = NULL) {
-    $fetchStyle = $fetch_style ?? $this->defaultFetchStyle;
+  public function fetchAllAssoc($key, $fetch = NULL) {
+    if (is_int($fetch)) {
+      @trigger_error("Passing the \$fetch argument as an integer to fetchAllAssoc() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED);
+      $fetch = $this->pdoToFetchAs($fetch);
+    }
 
     $result = [];
-    while ($row = $this->fetch($fetchStyle)) {
+    while ($row = $this->fetch($fetch ?? $this->defaultFetchMode)) {
       $result[$this->data[$this->getResultsetCurrentRowIndex()][$key]] = $row;
     }
     return $result;
diff --git a/core/lib/Drupal/Core/Database/StatementWrapperIterator.php b/core/lib/Drupal/Core/Database/StatementWrapperIterator.php
index 980ef74ed2e103575fd361ce00c92baa8f3dc0b5..364cbe05b4159f58b8f191a7b99f9030f40f9cfd 100644
--- a/core/lib/Drupal/Core/Database/StatementWrapperIterator.php
+++ b/core/lib/Drupal/Core/Database/StatementWrapperIterator.php
@@ -5,6 +5,8 @@
 use Drupal\Core\Database\Event\StatementExecutionEndEvent;
 use Drupal\Core\Database\Event\StatementExecutionFailureEvent;
 use Drupal\Core\Database\Event\StatementExecutionStartEvent;
+use Drupal\Core\Database\Statement\FetchAs;
+use Drupal\Core\Database\Statement\PdoTrait;
 
 // cSpell:ignore maxlen driverdata INOUT
 
@@ -28,8 +30,9 @@
  */
 class StatementWrapperIterator implements \Iterator, StatementInterface {
 
-  use StatementIteratorTrait;
   use FetchModeTrait;
+  use PdoTrait;
+  use StatementIteratorTrait;
 
   /**
    * The client database Statement object.
@@ -38,6 +41,22 @@ class StatementWrapperIterator implements \Iterator, StatementInterface {
    */
   protected object $clientStatement;
 
+  /**
+   * Holds the default fetch mode.
+   */
+  protected FetchAs $defaultFetchMode = FetchAs::Object;
+
+  /**
+   * Holds fetch options.
+   *
+   * @var array{'class': class-string, 'constructor_args': array<mixed>, 'column': int}
+   */
+  protected array $fetchOptions = [
+    'class' => 'stdClass',
+    'constructor_args' => [],
+    'column' => 0,
+  ];
+
   /**
    * Constructs a StatementWrapperIterator object.
    *
@@ -60,19 +79,7 @@ public function __construct(
     protected readonly bool $rowCountEnabled = FALSE,
   ) {
     $this->clientStatement = $clientConnection->prepare($query, $options);
-    $this->setFetchMode(\PDO::FETCH_OBJ);
-  }
-
-  /**
-   * Returns the client-level database statement object.
-   *
-   * This method should normally be used only within database driver code.
-   *
-   * @return object
-   *   The client-level database statement, for example \PDOStatement.
-   */
-  public function getClientStatement(): object {
-    return $this->clientStatement;
+    $this->setFetchMode(FetchAs::Object);
   }
 
   /**
@@ -86,11 +93,13 @@ public function getConnectionTarget(): string {
    * {@inheritdoc}
    */
   public function execute($args = [], $options = []) {
+    if (isset($options['fetch']) && is_int($options['fetch'])) {
+      @trigger_error("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED);
+    }
+
     if (isset($options['fetch'])) {
       if (is_string($options['fetch'])) {
-        // \PDO::FETCH_PROPS_LATE tells __construct() to run before properties
-        // are added to the object.
-        $this->setFetchMode(\PDO::FETCH_CLASS | \PDO::FETCH_PROPS_LATE, $options['fetch']);
+        $this->setFetchMode(FetchAs::ClassObject, $options['fetch']);
       }
       else {
         $this->setFetchMode($options['fetch']);
@@ -110,7 +119,7 @@ public function execute($args = [], $options = []) {
     }
 
     try {
-      $return = $this->clientStatement->execute($args);
+      $return = $this->clientExecute($args, $options);
       $this->markResultsetIterable($return);
     }
     catch (\Exception $e) {
@@ -150,23 +159,28 @@ public function execute($args = [], $options = []) {
    * {@inheritdoc}
    */
   public function getQueryString() {
-    return $this->clientStatement->queryString;
+    return $this->clientQueryString();
   }
 
   /**
    * {@inheritdoc}
    */
   public function fetchCol($index = 0) {
-    return $this->fetchAll(\PDO::FETCH_COLUMN, $index);
+    return $this->fetchAll(FetchAs::Column, $index);
   }
 
   /**
    * {@inheritdoc}
    */
   public function fetchAllAssoc($key, $fetch = NULL) {
+    if (is_int($fetch)) {
+      @trigger_error("Passing the \$fetch argument as an integer to fetchAllAssoc() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED);
+      $fetch = $this->pdoToFetchAs($fetch);
+    }
+
     if (isset($fetch)) {
       if (is_string($fetch)) {
-        $this->setFetchMode(\PDO::FETCH_CLASS, $fetch);
+        $this->setFetchMode(FetchAs::ClassObject, $fetch);
       }
       else {
         $this->setFetchMode($fetch);
@@ -193,7 +207,7 @@ public function fetchAllAssoc($key, $fetch = NULL) {
    * {@inheritdoc}
    */
   public function fetchAllKeyed($key_index = 0, $value_index = 1) {
-    $this->setFetchMode(\PDO::FETCH_NUM);
+    $this->setFetchMode(FetchAs::List);
 
     // Return early if the statement was already fully traversed.
     if (!$this->isResultsetIterable) {
@@ -213,8 +227,7 @@ public function fetchAllKeyed($key_index = 0, $value_index = 1) {
    * {@inheritdoc}
    */
   public function fetchField($index = 0) {
-    // Call \PDOStatement::fetchColumn to fetch the field.
-    $column = $this->clientStatement->fetchColumn($index);
+    $column = $this->clientFetchColumn($index);
 
     if ($column === FALSE) {
       $this->markResultsetFetchingComplete();
@@ -229,19 +242,14 @@ public function fetchField($index = 0) {
    * {@inheritdoc}
    */
   public function fetchAssoc() {
-    return $this->fetch(\PDO::FETCH_ASSOC);
+    return $this->fetch(FetchAs::Associative);
   }
 
   /**
    * {@inheritdoc}
    */
   public function fetchObject(?string $class_name = NULL, array $constructor_arguments = []) {
-    if ($class_name) {
-      $row = $this->clientStatement->fetchObject($class_name, $constructor_arguments);
-    }
-    else {
-      $row = $this->clientStatement->fetchObject();
-    }
+    $row = $this->clientFetchObject($class_name, $constructor_arguments);
 
     if ($row === FALSE) {
       $this->markResultsetFetchingComplete();
@@ -258,7 +266,7 @@ public function fetchObject(?string $class_name = NULL, array $constructor_argum
   public function rowCount() {
     // SELECT query should not use the method.
     if ($this->rowCountEnabled) {
-      return $this->clientStatement->rowCount();
+      return $this->clientRowCount();
     }
     else {
       throw new RowCountException();
@@ -269,32 +277,43 @@ public function rowCount() {
    * {@inheritdoc}
    */
   public function setFetchMode($mode, $a1 = NULL, $a2 = []) {
-    assert(in_array($mode, $this->supportedFetchModes), 'Fetch mode ' . ($this->fetchModeLiterals[$mode] ?? $mode) . ' is not supported. Use supported modes only.');
-
-    // Call \PDOStatement::setFetchMode to set fetch mode.
-    // \PDOStatement is picky about the number of arguments in some cases so we
-    // need to be pass the exact number of arguments we where given.
-    return match(func_num_args()) {
-      1 => $this->clientStatement->setFetchMode($mode),
-      2 => $this->clientStatement->setFetchMode($mode, $a1),
-      default => $this->clientStatement->setFetchMode($mode, $a1, $a2),
-    };
+    if (is_int($mode)) {
+      @trigger_error("Passing the \$mode argument as an integer to setFetchMode() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED);
+      $mode = $this->pdoToFetchAs($mode);
+    }
+
+    $this->defaultFetchMode = $mode;
+    switch ($mode) {
+      case FetchAs::ClassObject:
+        $this->fetchOptions['class'] = $a1;
+        if ($a2) {
+          $this->fetchOptions['constructor_args'] = $a2;
+        }
+        break;
+
+      case FetchAs::Column:
+        $this->fetchOptions['column'] = $a1;
+        break;
+
+    }
+
+    return $this->clientSetFetchMode($mode, $a1, $a2);
   }
 
   /**
    * {@inheritdoc}
    */
   public function fetch($mode = NULL, $cursor_orientation = NULL, $cursor_offset = NULL) {
-    assert(!isset($mode) || in_array($mode, $this->supportedFetchModes), 'Fetch mode ' . ($this->fetchModeLiterals[$mode] ?? $mode) . ' is not supported. Use supported modes only.');
+    if (is_int($mode)) {
+      @trigger_error("Passing the \$mode argument as an integer to fetch() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED);
+      $mode = $this->pdoToFetchAs($mode);
+    }
 
-    // Call \PDOStatement::fetchAll to fetch all rows.
-    // \PDOStatement is picky about the number of arguments in some cases so we
-    // need to pass the exact number of arguments we were given.
     $row = match(func_num_args()) {
-      0 => $this->clientStatement->fetch(),
-      1 => $this->clientStatement->fetch($mode),
-      2 => $this->clientStatement->fetch($mode, $cursor_orientation),
-      default => $this->clientStatement->fetch($mode, $cursor_orientation, $cursor_offset),
+      0 => $this->clientFetch(),
+      1 => $this->clientFetch($mode),
+      2 => $this->clientFetch($mode, $cursor_orientation),
+      default => $this->clientFetch($mode, $cursor_orientation, $cursor_offset),
     };
 
     if ($row === FALSE) {
@@ -310,17 +329,20 @@ public function fetch($mode = NULL, $cursor_orientation = NULL, $cursor_offset =
    * {@inheritdoc}
    */
   public function fetchAll($mode = NULL, $column_index = NULL, $constructor_arguments = NULL) {
-    assert(!isset($mode) || in_array($mode, $this->supportedFetchModes), 'Fetch mode ' . ($this->fetchModeLiterals[$mode] ?? $mode) . ' is not supported. Use supported modes only.');
-
-    // Call \PDOStatement::fetchAll to fetch all rows.
-    // \PDOStatement is picky about the number of arguments in some cases so we
-    // need to be pass the exact number of arguments we where given.
-    $return = match(func_num_args()) {
-      0 => $this->clientStatement->fetchAll(),
-      1 => $this->clientStatement->fetchAll($mode),
-      2 => $this->clientStatement->fetchAll($mode, $column_index),
-      default => $this->clientStatement->fetchAll($mode, $column_index, $constructor_arguments),
-    };
+    if (is_int($mode)) {
+      @trigger_error("Passing the \$mode argument as an integer to fetchAll() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED);
+      $mode = $this->pdoToFetchAs($mode);
+    }
+
+    $fetchMode = $mode ?? $this->defaultFetchMode;
+    if (isset($column_index)) {
+      $this->fetchOptions['column'] = $column_index;
+    }
+    if (isset($constructor_arguments)) {
+      $this->fetchOptions['constructor_args'] = $constructor_arguments;
+    }
+
+    $return = $this->clientFetchAll($fetchMode, $column_index, $constructor_arguments);
 
     $this->markResultsetFetchingComplete();
 
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
index eeb714f6ad48434d5e2e25043a177c4ab103b8d3..78d6061100f66644fa3da068dad6b566f4bfb79f 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
@@ -7,6 +7,7 @@
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Database\DatabaseExceptionWrapper;
 use Drupal\Core\Database\SchemaException;
+use Drupal\Core\Database\Statement\FetchAs;
 use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Entity\ContentEntityStorageBase;
 use Drupal\Core\Entity\ContentEntityTypeInterface;
@@ -530,7 +531,7 @@ protected function loadFromSharedTables(array &$values, array &$translations, $l
       // latest revision. Otherwise we fall back to the data table.
       $table = $this->revisionDataTable ?: $this->dataTable;
       $alias = $this->revisionDataTable ? 'revision' : 'data';
-      $query = $this->database->select($table, $alias, ['fetch' => \PDO::FETCH_ASSOC])
+      $query = $this->database->select($table, $alias, ['fetch' => FetchAs::Associative])
         ->fields($alias)
         ->condition($alias . '.' . $record_key, array_keys($values), 'IN')
         ->orderBy($alias . '.' . $record_key);
@@ -1641,7 +1642,7 @@ protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definit
     $table_name = $table_mapping->getDedicatedDataTableName($storage_definition, $storage_definition->isDeleted());
 
     // Get the entities which we want to purge first.
-    $entity_query = $this->database->select($table_name, 't', ['fetch' => \PDO::FETCH_ASSOC]);
+    $entity_query = $this->database->select($table_name, 't', ['fetch' => FetchAs::Associative]);
     $or = $entity_query->orConditionGroup();
     foreach ($storage_definition->getColumns() as $column_name => $data) {
       $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name));
@@ -1661,7 +1662,7 @@ protected function readFieldItemsToPurge(FieldDefinitionInterface $field_definit
     $entities = [];
     $items_by_entity = [];
     foreach ($entity_query->execute() as $row) {
-      $item_query = $this->database->select($table_name, 't', ['fetch' => \PDO::FETCH_ASSOC])
+      $item_query = $this->database->select($table_name, 't', ['fetch' => FetchAs::Associative])
         ->fields('t')
         ->condition('entity_id', $row['entity_id'])
         ->condition('deleted', 1)
diff --git a/core/lib/Drupal/Core/Menu/MenuTreeStorage.php b/core/lib/Drupal/Core/Menu/MenuTreeStorage.php
index 2dc2fd53c6f1def085a5a528d97f0c94c2cc2783..fb5d75a9c2dd46bcaee7f349babd786f2143bd6b 100644
--- a/core/lib/Drupal/Core/Menu/MenuTreeStorage.php
+++ b/core/lib/Drupal/Core/Menu/MenuTreeStorage.php
@@ -10,6 +10,7 @@
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Database\DatabaseException;
 use Drupal\Core\Database\Query\SelectInterface;
+use Drupal\Core\Database\Statement\FetchAs;
 
 // cspell:ignore mlid
 
@@ -642,7 +643,7 @@ public function loadByProperties(array $properties) {
       }
       $query->condition($name, $value);
     }
-    $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC);
+    $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', FetchAs::Associative);
     foreach ($loaded as $id => $link) {
       $loaded[$id] = $this->prepareLink($link);
     }
@@ -671,7 +672,7 @@ public function loadByRoute($route_name, array $route_parameters = [], $menu_nam
     $query->orderBy('depth');
     $query->orderBy('weight');
     $query->orderBy('id');
-    $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC);
+    $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', FetchAs::Associative);
     foreach ($loaded as $id => $link) {
       $loaded[$id] = $this->prepareLink($link);
     }
@@ -688,7 +689,7 @@ public function loadMultiple(array $ids) {
       $query = $this->connection->select($this->table, NULL, $this->options);
       $query->fields($this->table, $this->definitionFields());
       $query->condition('id', $missing_ids, 'IN');
-      $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC);
+      $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', FetchAs::Associative);
       foreach ($loaded as $id => $link) {
         $this->definitions[$id] = $this->prepareLink($link);
       }
@@ -734,7 +735,7 @@ protected function loadFullMultiple(array $ids) {
     $query = $this->connection->select($this->table, NULL, $this->options);
     $query->fields($this->table);
     $query->condition('id', $ids, 'IN');
-    $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC);
+    $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', FetchAs::Associative);
     foreach ($loaded as &$link) {
       foreach ($this->serializedFields() as $name) {
         if (isset($link[$name])) {
@@ -755,7 +756,7 @@ public function getRootPathIds($id) {
     //   https://www.drupal.org/node/2302043
     $subquery->fields($this->table, ['p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', 'p8', 'p9']);
     $subquery->condition('id', $id);
-    $result = current($subquery->execute()->fetchAll(\PDO::FETCH_ASSOC));
+    $result = current($subquery->execute()->fetchAll(FetchAs::Associative));
     $ids = array_filter($result);
     if ($ids) {
       $query = $this->connection->select($this->table, NULL, $this->options);
@@ -942,7 +943,7 @@ protected function loadLinks($menu_name, MenuTreeParameters $parameters) {
       }
     }
 
-    $links = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC);
+    $links = $this->safeExecuteSelect($query)->fetchAllAssoc('id', FetchAs::Associative);
 
     return $links;
   }
diff --git a/core/lib/Drupal/Core/Routing/RouteProvider.php b/core/lib/Drupal/Core/Routing/RouteProvider.php
index ef456eca5195824c211126330af9c4d2efe7a620..3330303114ce2037735755a01b0b43085f668e4d 100644
--- a/core/lib/Drupal/Core/Routing/RouteProvider.php
+++ b/core/lib/Drupal/Core/Routing/RouteProvider.php
@@ -5,6 +5,7 @@
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
+use Drupal\Core\Database\Statement\FetchAs;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
 use Drupal\Core\Path\CurrentPathStack;
@@ -383,7 +384,7 @@ protected function getRoutesByPath($path) {
         ':patterns[]' => $ancestors,
         ':count_parts' => count($parts),
       ])
-        ->fetchAll(\PDO::FETCH_ASSOC);
+        ->fetchAll(FetchAs::Associative);
     }
     catch (\Exception) {
       $routes = [];
diff --git a/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php b/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php
index 46775c5de0f5b803c1388fcc30ff2ac0646ed4cf..99710776cdd68e43cc0f679f1d648f4768b72499 100644
--- a/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php
+++ b/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php
@@ -6,6 +6,7 @@
 use Drupal\Core\Database\DatabaseException;
 use Drupal\Core\Database\DatabaseExceptionWrapper;
 use Drupal\Core\Database\Exception\SchemaTableKeyTooLargeException;
+use Drupal\Core\Database\Statement\FetchAs;
 use Drupal\Core\Field\BaseFieldDefinition;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Plugin\PluginBase;
@@ -655,7 +656,7 @@ public function lookupDestinationIds(array $source_id_values) {
     }
 
     try {
-      return $query->execute()->fetchAll(\PDO::FETCH_NUM);
+      return $query->execute()->fetchAll(FetchAs::List);
     }
     catch (DatabaseExceptionWrapper) {
       // It's possible that the query will cause an exception to be thrown. For
diff --git a/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php b/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php
index 05123fe9b780502620401a075d3210aa231fe6bc..40dea1c7a2b362c597f00070659baab189836734 100644
--- a/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php
+++ b/core/modules/migrate/src/Plugin/migrate/source/SqlBase.php
@@ -5,6 +5,7 @@
 use Drupal\Core\Database\ConnectionNotDefinedException;
 use Drupal\Core\Database\Database;
 use Drupal\Core\Database\DatabaseException;
+use Drupal\Core\Database\Statement\FetchAs;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\State\StateInterface;
 use Drupal\migrate\Exception\RequirementsException;
@@ -224,7 +225,7 @@ public function checkRequirements() {
    * Wrapper for database select.
    */
   protected function select($table, $alias = NULL, array $options = []) {
-    $options['fetch'] = \PDO::FETCH_ASSOC;
+    $options['fetch'] = FetchAs::Associative;
     return $this->getDatabase()->select($table, $alias, $options);
   }
 
@@ -347,7 +348,7 @@ protected function initializeIterator() {
       $this->query->range($this->batch * $this->batchSize, $this->batchSize);
     }
     $statement = $this->query->execute();
-    $statement->setFetchMode(\PDO::FETCH_ASSOC);
+    $statement->setFetchMode(FetchAs::Associative);
     return new \IteratorIterator($statement);
   }
 
diff --git a/core/modules/migrate/tests/src/Kernel/SqlBaseTest.php b/core/modules/migrate/tests/src/Kernel/SqlBaseTest.php
index 20477876e9988c5c1abbb4ef64698a51be6e9114..2f5c2a2ceec76a49f482d633855086d4d6ab3022 100644
--- a/core/modules/migrate/tests/src/Kernel/SqlBaseTest.php
+++ b/core/modules/migrate/tests/src/Kernel/SqlBaseTest.php
@@ -6,6 +6,7 @@
 
 use Drupal\Core\Database\Query\ConditionInterface;
 use Drupal\Core\Database\Query\SelectInterface;
+use Drupal\Core\Database\Statement\FetchAs;
 use Drupal\Core\Database\StatementInterface;
 use Drupal\migrate\Exception\RequirementsException;
 use Drupal\Core\Database\Database;
@@ -175,7 +176,7 @@ public function testHighWater($high_water = NULL, array $query_result = []): voi
     }
 
     $statement = $this->createMock(StatementInterface::class);
-    $statement->expects($this->atLeastOnce())->method('setFetchMode')->with(\PDO::FETCH_ASSOC);
+    $statement->expects($this->atLeastOnce())->method('setFetchMode')->with(FetchAs::Associative);
     $query = $this->createMock(SelectInterface::class);
     $query->method('execute')->willReturn($statement);
     $query->expects($this->atLeastOnce())->method('orderBy')->with('order', 'ASC');
diff --git a/core/modules/migrate_drupal/src/MigrationConfigurationTrait.php b/core/modules/migrate_drupal/src/MigrationConfigurationTrait.php
index ce25ea4d21f20f30f4354465934a5383b94f7737..cd64f935ca1e6e6ec9309a3ebba006cd569f651d 100644
--- a/core/modules/migrate_drupal/src/MigrationConfigurationTrait.php
+++ b/core/modules/migrate_drupal/src/MigrationConfigurationTrait.php
@@ -5,6 +5,7 @@
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Database\Database;
 use Drupal\Core\Database\DatabaseExceptionWrapper;
+use Drupal\Core\Database\Statement\FetchAs;
 use Drupal\migrate\Exception\RequirementsException;
 use Drupal\migrate\Plugin\RequirementsInterface;
 
@@ -70,7 +71,7 @@ protected function getSystemData(Connection $connection) {
     $system_data = [];
     try {
       $results = $connection->select('system', 's', [
-        'fetch' => \PDO::FETCH_ASSOC,
+        'fetch' => FetchAs::Associative,
       ])
         ->fields('s')
         ->execute();
diff --git a/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeCompleteTest.php b/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeCompleteTest.php
index 90dcb8c92a4a1361fbee4b0cec3c36cec22d9565..8f30abbc564208b886f9a86de1e33b7d9ce8f4f5 100644
--- a/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeCompleteTest.php
+++ b/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeCompleteTest.php
@@ -4,6 +4,7 @@
 
 namespace Drupal\Tests\node\Kernel\Migrate\d6;
 
+use Drupal\Core\Database\Statement\FetchAs;
 use Drupal\node\NodeInterface;
 use Drupal\Tests\file\Kernel\Migrate\d6\FileMigrationTestTrait;
 use Drupal\Tests\migrate_drupal\Traits\CreateTestContentEntitiesTrait;
@@ -65,14 +66,14 @@ public function testNodeCompleteMigration(): void {
       ->orderBy('vid')
       ->orderBy('langcode')
       ->execute()
-      ->fetchAll(\PDO::FETCH_ASSOC));
+      ->fetchAll(FetchAs::Associative));
     $this->assertEquals($this->expectedNodeFieldDataTable(), $db->select('node_field_data', 'nr')
       ->fields('nr')
       ->orderBy('nid')
       ->orderBy('vid')
       ->orderBy('langcode')
       ->execute()
-      ->fetchAll(\PDO::FETCH_ASSOC));
+      ->fetchAll(FetchAs::Associative));
 
     // Now load and test each revision, including the field 'field_text_plain'
     // which has text reflecting the revision.
diff --git a/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeCompleteTest.php b/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeCompleteTest.php
index 304747081ad89ef1399ba50a37e1ffaefa7aa601..cbe9b346623e49bd496b8264c3fffa3f82b94320 100644
--- a/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeCompleteTest.php
+++ b/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeCompleteTest.php
@@ -4,6 +4,7 @@
 
 namespace Drupal\Tests\node\Kernel\Migrate\d7;
 
+use Drupal\Core\Database\Statement\FetchAs;
 use Drupal\migrate\MigrateExecutable;
 use Drupal\migrate_drupal\NodeMigrateType;
 use Drupal\node\Entity\Node;
@@ -112,14 +113,14 @@ public function testNodeCompleteMigration(): void {
       ->orderBy('vid')
       ->orderBy('langcode')
       ->execute()
-      ->fetchAll(\PDO::FETCH_ASSOC));
+      ->fetchAll(FetchAs::Associative));
     $this->assertEquals($this->expectedNodeFieldDataTable(), $db->select('node_field_data', 'nr')
       ->fields('nr')
       ->orderBy('nid')
       ->orderBy('vid')
       ->orderBy('langcode')
       ->execute()
-      ->fetchAll(\PDO::FETCH_ASSOC));
+      ->fetchAll(FetchAs::Associative));
 
     // Load and test each revision.
     $data = $this->expectedRevisionEntityData()[0];
diff --git a/core/modules/path_alias/src/AliasRepository.php b/core/modules/path_alias/src/AliasRepository.php
index 21eb3daef0d65bc2a85a14fd9285cea81c827c29..0172b8da57b6ed6d0bbbd4a381b094823f08ece7 100644
--- a/core/modules/path_alias/src/AliasRepository.php
+++ b/core/modules/path_alias/src/AliasRepository.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\Database\Connection;
 use Drupal\Core\Database\Query\SelectInterface;
+use Drupal\Core\Database\Statement\FetchAs;
 use Drupal\Core\Language\LanguageInterface;
 
 /**
@@ -53,7 +54,7 @@ public function preloadPathAlias($preloaded, $langcode) {
     // 'base_table.id' column, as that would not guarantee other conditions
     // added to the query, such as those in ::addLanguageFallback, would be
     // reversed.
-    $results = $select->execute()->fetchAll(\PDO::FETCH_ASSOC);
+    $results = $select->execute()->fetchAll(FetchAs::Associative);
     $aliases = [];
     foreach (array_reverse($results) as $result) {
       $aliases[$result['path']] = $result['alias'];
diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php b/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php
index e949cdd7652a01c0657972b83178f4b8e5443664..0a353dceed05ce51d151de60f99ed24ac0e7f1ac 100644
--- a/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php
+++ b/core/modules/sqlite/src/Driver/Database/sqlite/Connection.php
@@ -414,6 +414,9 @@ public function mapConditionOperator($operator) {
    */
   public function prepareStatement(string $query, array $options, bool $allow_row_count = FALSE): StatementInterface {
     assert(!isset($options['return']), 'Passing "return" option to prepareStatement() has no effect. See https://www.drupal.org/node/3185520');
+    if (isset($options['fetch']) && is_int($options['fetch'])) {
+      @trigger_error("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED);
+    }
 
     try {
       $query = $this->preprocessStatement($query, $options);
diff --git a/core/modules/sqlite/src/Driver/Database/sqlite/Statement.php b/core/modules/sqlite/src/Driver/Database/sqlite/Statement.php
index 5a2d6b826841ce5c0145d8e79d7c334a609e0d9d..1c7378a01737a61b9bab2a7db1b3b16f3d3f1834 100644
--- a/core/modules/sqlite/src/Driver/Database/sqlite/Statement.php
+++ b/core/modules/sqlite/src/Driver/Database/sqlite/Statement.php
@@ -86,6 +86,10 @@ protected function getStatement(string $query, ?array &$args = []): object {
    * {@inheritdoc}
    */
   public function execute($args = [], $options = []) {
+    if (isset($options['fetch']) && is_int($options['fetch'])) {
+      @trigger_error("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED);
+    }
+
     try {
       $return = parent::execute($args, $options);
     }
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 6f4d3b7a4f8006b71369fd2c2acc635821559e27..f152becb6cd32beaa9cc09f24efe8821e8f46ae3 100644
--- a/core/modules/system/tests/modules/database_test/database_test.install
+++ b/core/modules/system/tests/modules/database_test/database_test.install
@@ -57,7 +57,7 @@ function database_test_schema(): array {
   ];
 
   $schema['test_classtype'] = [
-    'description' => 'A duplicate version of the test table, used for fetch_style PDO::FETCH_CLASSTYPE tests.',
+    'description' => 'A duplicate version of the test table, used for obsolete fetch_style PDO::FETCH_CLASSTYPE tests.',
     'fields' => [
       'classname' => [
         'description' => "A custom class name",
diff --git a/core/modules/views/src/Plugin/views/query/Sql.php b/core/modules/views/src/Plugin/views/query/Sql.php
index 20e19439ffc227583dd6ce9b39209e5c9d3954fb..05b425557d15563263a3fd65d787359ec4ee2fa9 100644
--- a/core/modules/views/src/Plugin/views/query/Sql.php
+++ b/core/modules/views/src/Plugin/views/query/Sql.php
@@ -5,6 +5,7 @@
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Database\Database;
+use Drupal\Core\Database\Statement\FetchAs;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Messenger\MessengerInterface;
@@ -1571,7 +1572,7 @@ public function execute(ViewExecutable $view) {
         }
 
         $result = $query->execute();
-        $result->setFetchMode(\PDO::FETCH_CLASS, 'Drupal\views\ResultRow');
+        $result->setFetchMode(FetchAs::ClassObject, 'Drupal\views\ResultRow');
 
         // Setup the result row objects.
         $view->result = iterator_to_array($result);
diff --git a/core/tests/Drupal/KernelTests/Core/Database/FetchLegacyTest.php b/core/tests/Drupal/KernelTests/Core/Database/FetchLegacyTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..6e28787f764c2f3179ed1bd92fd80743704fa0c3
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Database/FetchLegacyTest.php
@@ -0,0 +1,147 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Core\Database;
+
+use PHPUnit\Framework\Attributes\IgnoreDeprecations;
+
+/**
+ * Tests the Database system's various fetch capabilities.
+ *
+ * We get timeout errors if we try to run too many tests at once.
+ *
+ * @group Database
+ */
+class FetchLegacyTest extends DatabaseTestBase {
+
+  /**
+   * Confirms that we can fetch a record to an object explicitly.
+   */
+  #[IgnoreDeprecations]
+  public function testQueryFetchObject(): void {
+    $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in query() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+    $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+    $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+    $records = [];
+    $result = $this->connection->query('SELECT [name] FROM {test} WHERE [age] = :age', [':age' => 25], ['fetch' => \PDO::FETCH_OBJ]);
+    foreach ($result as $record) {
+      $records[] = $record;
+      $this->assertIsObject($record);
+      $this->assertSame('John', $record->name);
+    }
+
+    $this->assertCount(1, $records, 'There is only one record.');
+  }
+
+  /**
+   * Confirms that we can fetch a record to an associative array explicitly.
+   */
+  #[IgnoreDeprecations]
+  public function testQueryFetchArray(): void {
+    $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in query() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+    $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+    $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+    $records = [];
+    $result = $this->connection->query('SELECT [name] FROM {test} WHERE [age] = :age', [':age' => 25], ['fetch' => \PDO::FETCH_ASSOC]);
+    foreach ($result as $record) {
+      $records[] = $record;
+      $this->assertIsArray($record);
+      $this->assertArrayHasKey('name', $record);
+      $this->assertSame('John', $record['name']);
+    }
+
+    $this->assertCount(1, $records, 'There is only one record.');
+  }
+
+  /**
+   * Confirms that we can fetch a record into an indexed array explicitly.
+   */
+  #[IgnoreDeprecations]
+  public function testQueryFetchNum(): void {
+    $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in query() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+    $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+    $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+    $records = [];
+    $result = $this->connection->query('SELECT [name] FROM {test} WHERE [age] = :age', [':age' => 25], ['fetch' => \PDO::FETCH_NUM]);
+    foreach ($result as $record) {
+      $records[] = $record;
+      $this->assertIsArray($record);
+      $this->assertArrayHasKey(0, $record);
+      $this->assertSame('John', $record[0]);
+    }
+
+    $this->assertCount(1, $records, 'There is only one record');
+  }
+
+  /**
+   * Confirms that we can fetch all records into an array explicitly.
+   */
+  #[IgnoreDeprecations]
+  public function testQueryFetchAllColumn(): void {
+    $this->expectDeprecation("Passing the \$mode argument as an integer to fetchAll() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+    $query = $this->connection->select('test');
+    $query->addField('test', 'name');
+    $query->orderBy('name');
+    $query_result = $query->execute()->fetchAll(\PDO::FETCH_COLUMN);
+
+    $expected_result = ['George', 'John', 'Paul', 'Ringo'];
+    $this->assertEquals($expected_result, $query_result, 'Returned the correct result.');
+  }
+
+  /**
+   * Tests ::fetchAllAssoc().
+   */
+  #[IgnoreDeprecations]
+  public function testQueryFetchAllAssoc(): void {
+    $this->expectDeprecation("Passing the \$fetch argument as an integer to fetchAllAssoc() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+    $expected_result = [
+      "Singer" => [
+        "id" => "2",
+        "name" => "George",
+        "age" => "27",
+        "job" => "Singer",
+      ],
+      "Drummer" => [
+        "id" => "3",
+        "name" => "Ringo",
+        "age" => "28",
+        "job" => "Drummer",
+      ],
+    ];
+
+    $statement = $this->connection->query('SELECT * FROM {test} WHERE [age] > :age', [':age' => 26]);
+    $result = $statement->fetchAllAssoc('job', \PDO::FETCH_ASSOC);
+    $this->assertSame($expected_result, $result);
+
+    $statement = $this->connection->query('SELECT * FROM {test} WHERE [age] > :age', [':age' => 26]);
+    $result = $statement->fetchAllAssoc('job', \PDO::FETCH_OBJ);
+    $this->assertEquals((object) $expected_result['Singer'], $result['Singer']);
+    $this->assertEquals((object) $expected_result['Drummer'], $result['Drummer']);
+  }
+
+  /**
+   * Confirms that we can fetch a single column value.
+   */
+  #[IgnoreDeprecations]
+  public function testQueryFetchColumn(): void {
+    $statement = $this->connection
+      ->query('SELECT [name] FROM {test} WHERE [age] = :age', [':age' => 25]);
+    $statement->setFetchMode(\PDO::FETCH_COLUMN, 0);
+    $this->assertSame('John', $statement->fetch());
+  }
+
+  /**
+   * Confirms that an out of range index throws an error.
+   */
+  #[IgnoreDeprecations]
+  public function testQueryFetchColumnOutOfRange(): void {
+    $this->expectException(\ValueError::class);
+    $this->expectExceptionMessage('Invalid column index');
+    $statement = $this->connection
+      ->query('SELECT [name] FROM {test} WHERE [age] = :age', [':age' => 25]);
+    $statement->setFetchMode(\PDO::FETCH_COLUMN, 200);
+    $statement->fetch();
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Database/FetchTest.php b/core/tests/Drupal/KernelTests/Core/Database/FetchTest.php
index 62c87e32d130635a87dd414b9b96c56c585137ca..47fbd33f31a8735e8128383463bdd75c46a8e739 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/FetchTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/FetchTest.php
@@ -5,6 +5,7 @@
 namespace Drupal\KernelTests\Core\Database;
 
 use Drupal\Core\Database\RowCountException;
+use Drupal\Core\Database\Statement\FetchAs;
 use Drupal\Core\Database\StatementInterface;
 use Drupal\Core\Database\StatementPrefetchIterator;
 use Drupal\Tests\system\Functional\Database\FakeRecord;
@@ -41,7 +42,7 @@ public function testQueryFetchDefault(): void {
   public function testQueryFetchColumn(): void {
     $statement = $this->connection
       ->query('SELECT [name] FROM {test} WHERE [age] = :age', [':age' => 25]);
-    $statement->setFetchMode(\PDO::FETCH_COLUMN, 0);
+    $statement->setFetchMode(FetchAs::Column, 0);
     $this->assertSame('John', $statement->fetch());
   }
 
@@ -53,7 +54,7 @@ public function testQueryFetchColumnOutOfRange(): void {
     $this->expectExceptionMessage('Invalid column index');
     $statement = $this->connection
       ->query('SELECT [name] FROM {test} WHERE [age] = :age', [':age' => 25]);
-    $statement->setFetchMode(\PDO::FETCH_COLUMN, 200);
+    $statement->setFetchMode(FetchAs::Column, 200);
     $statement->fetch();
   }
 
@@ -62,7 +63,7 @@ public function testQueryFetchColumnOutOfRange(): void {
    */
   public function testQueryFetchObject(): void {
     $records = [];
-    $result = $this->connection->query('SELECT [name] FROM {test} WHERE [age] = :age', [':age' => 25], ['fetch' => \PDO::FETCH_OBJ]);
+    $result = $this->connection->query('SELECT [name] FROM {test} WHERE [age] = :age', [':age' => 25], ['fetch' => FetchAs::Object]);
     foreach ($result as $record) {
       $records[] = $record;
       $this->assertIsObject($record);
@@ -77,7 +78,7 @@ public function testQueryFetchObject(): void {
    */
   public function testQueryFetchArray(): void {
     $records = [];
-    $result = $this->connection->query('SELECT [name] FROM {test} WHERE [age] = :age', [':age' => 25], ['fetch' => \PDO::FETCH_ASSOC]);
+    $result = $this->connection->query('SELECT [name] FROM {test} WHERE [age] = :age', [':age' => 25], ['fetch' => FetchAs::Associative]);
     foreach ($result as $record) {
       $records[] = $record;
       $this->assertIsArray($record);
@@ -146,7 +147,7 @@ public function testQueryFetchObjectClassNoConstructorArgs(): void {
    */
   public function testQueryFetchNum(): void {
     $records = [];
-    $result = $this->connection->query('SELECT [name] FROM {test} WHERE [age] = :age', [':age' => 25], ['fetch' => \PDO::FETCH_NUM]);
+    $result = $this->connection->query('SELECT [name] FROM {test} WHERE [age] = :age', [':age' => 25], ['fetch' => FetchAs::List]);
     foreach ($result as $record) {
       $records[] = $record;
       $this->assertIsArray($record);
@@ -164,7 +165,7 @@ public function testQueryFetchAllColumn(): void {
     $query = $this->connection->select('test');
     $query->addField('test', 'name');
     $query->orderBy('name');
-    $query_result = $query->execute()->fetchAll(\PDO::FETCH_COLUMN);
+    $query_result = $query->execute()->fetchAll(FetchAs::Column);
 
     $expected_result = ['George', 'John', 'Paul', 'Ringo'];
     $this->assertEquals($expected_result, $query_result, 'Returned the correct result.');
@@ -261,11 +262,11 @@ public function testQueryFetchAllAssoc(): void {
     ];
 
     $statement = $this->connection->query('SELECT * FROM {test} WHERE [age] > :age', [':age' => 26]);
-    $result = $statement->fetchAllAssoc('job', \PDO::FETCH_ASSOC);
+    $result = $statement->fetchAllAssoc('job', FetchAs::Associative);
     $this->assertSame($expected_result, $result);
 
     $statement = $this->connection->query('SELECT * FROM {test} WHERE [age] > :age', [':age' => 26]);
-    $result = $statement->fetchAllAssoc('job', \PDO::FETCH_OBJ);
+    $result = $statement->fetchAllAssoc('job', FetchAs::Object);
     $this->assertEquals((object) $expected_result['Singer'], $result['Singer']);
     $this->assertEquals((object) $expected_result['Drummer'], $result['Drummer']);
   }
diff --git a/core/tests/Drupal/KernelTests/Core/Database/SelectComplexTest.php b/core/tests/Drupal/KernelTests/Core/Database/SelectComplexTest.php
index b20789bd9e3cd32e242d20a2bdf87d91195281ac..7b5f567cdae923192bddcb5247a5dfdea432676f 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/SelectComplexTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/SelectComplexTest.php
@@ -7,6 +7,7 @@
 use Drupal\Core\Database\Database;
 use Drupal\Core\Database\Query\PagerSelectExtender;
 use Drupal\Core\Database\RowCountException;
+use Drupal\Core\Database\Statement\FetchAs;
 use Drupal\user\Entity\User;
 
 /**
@@ -185,7 +186,7 @@ public function testDistinct(): void {
     $query->addField('test_task', 'task');
     $query->orderBy('task');
     $query->distinct();
-    $query_result = $query->execute()->fetchAll(\PDO::FETCH_COLUMN);
+    $query_result = $query->execute()->fetchAll(FetchAs::Column);
 
     $expected_result = ['code', 'eat', 'found new band', 'perform at superbowl', 'sing', 'sleep'];
     $this->assertEquals($query_result, $expected_result, 'Returned the correct result.');
diff --git a/core/tests/Drupal/KernelTests/Core/Database/SelectOrderedTest.php b/core/tests/Drupal/KernelTests/Core/Database/SelectOrderedTest.php
index b3cc7c2d2ced9a464f3489f404869fc79983f710..8a319448790f9863df212382c677ee96e61ddc76 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/SelectOrderedTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/SelectOrderedTest.php
@@ -4,6 +4,8 @@
 
 namespace Drupal\KernelTests\Core\Database;
 
+use Drupal\Core\Database\Statement\FetchAs;
+
 /**
  * Tests the Select query builder.
  *
@@ -52,7 +54,7 @@ public function testSimpleSelectMultiOrdered(): void {
       ['George', 27, 'Singer'],
       ['Paul', 26, 'Songwriter'],
     ];
-    $results = $result->fetchAll(\PDO::FETCH_NUM);
+    $results = $result->fetchAll(FetchAs::List);
     foreach ($expected as $k => $record) {
       $num_records++;
       foreach ($record as $kk => $col) {
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateProviderTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateProviderTest.php
index 810915aff6e93525239b3bb4d05c0b4d66a64364..f8c62110eb1b96f4d7657b2eddaa9cc5794b6937 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateProviderTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateProviderTest.php
@@ -4,6 +4,7 @@
 
 namespace Drupal\KernelTests\Core\Entity;
 
+use Drupal\Core\Database\Statement\FetchAs;
 use Drupal\Core\Field\BaseFieldDefinition;
 use Drupal\language\Entity\ConfigurableLanguage;
 use Drupal\Tests\system\Functional\Entity\Traits\EntityDefinitionTestTrait;
@@ -152,7 +153,7 @@ public function testBaseFieldDeleteWithExistingData($entity_type_id, $create_ent
       ->orderBy('revision_id', 'ASC')
       ->orderBy('langcode', 'ASC')
       ->execute()
-      ->fetchAll(\PDO::FETCH_ASSOC);
+      ->fetchAll(FetchAs::Associative);
     $this->assertSameSize($expected, $result);
 
     // Use assertEquals and not assertSame here to prevent that a different
@@ -192,7 +193,7 @@ public function testBaseFieldDeleteWithExistingData($entity_type_id, $create_ent
         ->orderBy('revision_id', 'ASC')
         ->orderBy('langcode', 'ASC')
         ->execute()
-        ->fetchAll(\PDO::FETCH_ASSOC);
+        ->fetchAll(FetchAs::Associative);
       $this->assertSameSize($expected, $result);
 
       // Use assertEquals and not assertSame here to prevent that a different
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/FieldSqlStorageTest.php b/core/tests/Drupal/KernelTests/Core/Entity/FieldSqlStorageTest.php
index db858e1d516763e352354b925acec065eab37489..8b6d9ed52eb693069a0b5e56b5f7629fbb02f847 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/FieldSqlStorageTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/FieldSqlStorageTest.php
@@ -5,6 +5,7 @@
 namespace Drupal\KernelTests\Core\Entity;
 
 use Drupal\Core\Database\Database;
+use Drupal\Core\Database\Statement\FetchAs;
 use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
@@ -201,7 +202,7 @@ public function testFieldWrite(): void {
 
     $connection = Database::getConnection();
     // Read the tables and check the correct values have been stored.
-    $rows = $connection->select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', \PDO::FETCH_ASSOC);
+    $rows = $connection->select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', FetchAs::Associative);
     $this->assertCount($this->fieldCardinality, $rows);
     foreach ($rows as $delta => $row) {
       $expected = [
@@ -225,7 +226,7 @@ public function testFieldWrite(): void {
     $values_count = count($values);
     $entity->{$this->fieldName} = $values;
     $entity->save();
-    $rows = $connection->select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', \PDO::FETCH_ASSOC);
+    $rows = $connection->select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', FetchAs::Associative);
     $this->assertCount($values_count, $rows);
     foreach ($rows as $delta => $row) {
       $expected = [
@@ -253,7 +254,7 @@ public function testFieldWrite(): void {
 
     // Check that data for both revisions are in the revision table.
     foreach ($revision_values as $revision_id => $values) {
-      $rows = $connection->select($this->revisionTable, 't')->fields('t')->condition('revision_id', $revision_id)->execute()->fetchAllAssoc('delta', \PDO::FETCH_ASSOC);
+      $rows = $connection->select($this->revisionTable, 't')->fields('t')->condition('revision_id', $revision_id)->execute()->fetchAllAssoc('delta', FetchAs::Associative);
       $this->assertCount(min(count($values), $this->fieldCardinality), $rows);
       foreach ($rows as $delta => $row) {
         $expected = [
@@ -272,7 +273,7 @@ public function testFieldWrite(): void {
     // Test emptying the field.
     $entity->{$this->fieldName} = NULL;
     $entity->save();
-    $rows = $connection->select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', \PDO::FETCH_ASSOC);
+    $rows = $connection->select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', FetchAs::Associative);
     $this->assertCount(0, $rows);
   }
 
diff --git a/core/tests/Drupal/KernelTests/Core/Menu/MenuTreeStorageTest.php b/core/tests/Drupal/KernelTests/Core/Menu/MenuTreeStorageTest.php
index baa01d730b200896261686e3ca0018ee0bb1bff7..cf26cb9b3c17f2ddafc63a986fea5ab7f3ded90e 100644
--- a/core/tests/Drupal/KernelTests/Core/Menu/MenuTreeStorageTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Menu/MenuTreeStorageTest.php
@@ -5,6 +5,7 @@
 namespace Drupal\KernelTests\Core\Menu;
 
 use Drupal\Component\Plugin\Exception\PluginException;
+use Drupal\Core\Database\Statement\FetchAs;
 use Drupal\Core\Menu\MenuTreeParameters;
 use Drupal\Core\Menu\MenuTreeStorage;
 use Drupal\KernelTests\KernelTestBase;
@@ -426,7 +427,7 @@ protected function assertMenuLink(string $id, array $expected_properties, array
     foreach ($expected_properties as $field => $value) {
       $query->condition($field, $value);
     }
-    $all = $query->execute()->fetchAll(\PDO::FETCH_ASSOC);
+    $all = $query->execute()->fetchAll(FetchAs::Associative);
     $this->assertCount(1, $all, "Found link $id matching all the expected properties");
     $raw = reset($all);
 
diff --git a/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php b/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php
index 15e4cf28aa1a7f621b8e240d1090428f4b46ecb2..f9b25e8e70723b7dac051c6fd68526fbc23231b0 100644
--- a/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php
+++ b/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php
@@ -6,10 +6,13 @@
 
 use Composer\Autoload\ClassLoader;
 use Drupal\Core\Database\Database;
+use Drupal\Core\Database\Statement\FetchAs;
 use Drupal\Core\Database\StatementPrefetchIterator;
 use Drupal\Tests\Core\Database\Stub\StubConnection;
 use Drupal\Tests\Core\Database\Stub\StubPDO;
 use Drupal\Tests\UnitTestCase;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\IgnoreDeprecations;
 
 /**
  * Tests the Connection class.
@@ -890,7 +893,7 @@ public static function providerMockedBacktrace(): array {
    *   elements:
    *   - a PDO fetch mode.
    */
-  public static function providerSupportedFetchModes(): array {
+  public static function providerSupportedLegacyFetchModes(): array {
     return [
       'FETCH_ASSOC' => [\PDO::FETCH_ASSOC],
       'FETCH_CLASS' => [\PDO::FETCH_CLASS],
@@ -901,12 +904,42 @@ public static function providerSupportedFetchModes(): array {
     ];
   }
 
+  /**
+   * Tests supported fetch modes.
+   */
+  #[IgnoreDeprecations]
+  #[DataProvider('providerSupportedLegacyFetchModes')]
+  public function testSupportedLegacyFetchModes(int $mode): void {
+    $this->expectDeprecation("Passing the \$mode argument as an integer to setFetchMode() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+    $mockPdo = $this->createMock(StubPDO::class);
+    $mockConnection = new StubConnection($mockPdo, []);
+    $statement = new StatementPrefetchIterator($mockPdo, $mockConnection, '');
+    $this->assertInstanceOf(StatementPrefetchIterator::class, $statement);
+    $statement->setFetchMode($mode);
+  }
+
+  /**
+   * Provides data for testSupportedFetchModes.
+   *
+   * @return array<string,array<\Drupal\Core\Database\FetchAs>>
+   *   The FetchAs cases.
+   */
+  public static function providerSupportedFetchModes(): array {
+    return [
+      'Associative array' => [FetchAs::Associative],
+      'Classed object' => [FetchAs::ClassObject],
+      'Single column' => [FetchAs::Column],
+      'Simple array' => [FetchAs::List],
+      'Standard object' => [FetchAs::Object],
+    ];
+  }
+
   /**
    * Tests supported fetch modes.
    *
    * @dataProvider providerSupportedFetchModes
    */
-  public function testSupportedFetchModes(int $mode): void {
+  public function testSupportedFetchModes(FetchAs $mode): void {
     $mockPdo = $this->createMock(StubPDO::class);
     $mockConnection = new StubConnection($mockPdo, []);
     $statement = new StatementPrefetchIterator($mockPdo, $mockConnection, '');
@@ -937,12 +970,13 @@ public static function providerUnsupportedFetchModes(): array {
   }
 
   /**
-   * Tests unsupported fetch modes.
-   *
-   * @dataProvider providerUnsupportedFetchModes
+   * Tests unsupported legacy fetch modes.
    */
+  #[IgnoreDeprecations]
+  #[DataProvider('providerUnsupportedFetchModes')]
   public function testUnsupportedFetchModes(int $mode): void {
-    $this->expectException(\AssertionError::class);
+    $this->expectDeprecation("Passing the \$mode argument as an integer to setFetchMode() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+    $this->expectException(\RuntimeException::class);
     $this->expectExceptionMessageMatches("/^Fetch mode FETCH_.* is not supported\\. Use supported modes only/");
     $mockPdo = $this->createMock(StubPDO::class);
     $mockConnection = new StubConnection($mockPdo, []);