diff --git a/composer.json b/composer.json
index aafa9d73dc0b5eb2e9c295af1622bb88e09e5b2c..e2f61233d64c775e5e517ce81d4986df04bb67c7 100644
--- a/composer.json
+++ b/composer.json
@@ -43,7 +43,7 @@
 		"bacon/bacon-qr-code": "^2.0.4",
 		"christian-riesen/base32": "^1.6",
 		"doctrine/annotations": "^1.11",
-		"doctrine/dbal": "^2.13.5",
+		"doctrine/dbal": "^3.2",
 		"doctrine/event-manager": "^1.0.0",
 		"doctrine/lexer": "^1.2.1",
 		"egulias/email-validator": "^3.1",
diff --git a/composer.lock b/composer.lock
index 29bcf845df6cdf907bb7e20f9c36adddf2bb1136..fca81a469de37f3467b21a02585ac6f2a9102491 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "2c78ea109eaae98bc8d0db996ec4e61e",
+    "content-hash": "c0bbf0d3a34fb321304fd8b0ce3434fc",
     "packages": [
         {
             "name": "bacon/bacon-qr-code",
@@ -118,6 +118,79 @@
             },
             "time": "2021-02-26T10:19:33+00:00"
         },
+        {
+            "name": "composer/package-versions-deprecated",
+            "version": "1.11.99.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/composer/package-versions-deprecated.git",
+                "reference": "b174585d1fe49ceed21928a945138948cb394600"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/b174585d1fe49ceed21928a945138948cb394600",
+                "reference": "b174585d1fe49ceed21928a945138948cb394600",
+                "shasum": ""
+            },
+            "require": {
+                "composer-plugin-api": "^1.1.0 || ^2.0",
+                "php": "^7 || ^8"
+            },
+            "replace": {
+                "ocramius/package-versions": "1.11.99"
+            },
+            "require-dev": {
+                "composer/composer": "^1.9.3 || ^2.0@dev",
+                "ext-zip": "^1.13",
+                "phpunit/phpunit": "^6.5 || ^7"
+            },
+            "type": "composer-plugin",
+            "extra": {
+                "class": "PackageVersions\\Installer",
+                "branch-alias": {
+                    "dev-master": "1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "PackageVersions\\": "src/PackageVersions"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Marco Pivetta",
+                    "email": "ocramius@gmail.com"
+                },
+                {
+                    "name": "Jordi Boggiano",
+                    "email": "j.boggiano@seld.be"
+                }
+            ],
+            "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)",
+            "support": {
+                "issues": "https://github.com/composer/package-versions-deprecated/issues",
+                "source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.4"
+            },
+            "funding": [
+                {
+                    "url": "https://packagist.com",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/composer",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-09-13T08:41:34+00:00"
+        },
         {
             "name": "dasprid/enum",
             "version": "1.0.3",
@@ -239,40 +312,39 @@
         },
         {
             "name": "doctrine/cache",
-            "version": "v1.8.1",
+            "version": "2.1.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/cache.git",
-                "reference": "d4374ae95b36062d02ef310100ed33d78738d76c"
+                "reference": "331b4d5dbaeab3827976273e9356b3b453c300ce"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/cache/zipball/d4374ae95b36062d02ef310100ed33d78738d76c",
-                "reference": "d4374ae95b36062d02ef310100ed33d78738d76c",
+                "url": "https://api.github.com/repos/doctrine/cache/zipball/331b4d5dbaeab3827976273e9356b3b453c300ce",
+                "reference": "331b4d5dbaeab3827976273e9356b3b453c300ce",
                 "shasum": ""
             },
             "require": {
-                "php": "~7.1"
+                "php": "~7.1 || ^8.0"
             },
             "conflict": {
                 "doctrine/common": ">2.2,<2.4"
             },
             "require-dev": {
                 "alcaeus/mongo-php-adapter": "^1.1",
-                "doctrine/coding-standard": "^4.0",
+                "cache/integration-tests": "dev-master",
+                "doctrine/coding-standard": "^8.0",
                 "mongodb/mongodb": "^1.1",
-                "phpunit/phpunit": "^7.0",
-                "predis/predis": "~1.0"
+                "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
+                "predis/predis": "~1.0",
+                "psr/cache": "^1.0 || ^2.0 || ^3.0",
+                "symfony/cache": "^4.4 || ^5.2 || ^6.0@dev",
+                "symfony/var-exporter": "^4.4 || ^5.2 || ^6.0@dev"
             },
             "suggest": {
                 "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver"
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "1.8.x-dev"
-                }
-            },
             "autoload": {
                 "psr-4": {
                     "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache"
@@ -304,49 +376,73 @@
                     "email": "schmittjoh@gmail.com"
                 }
             ],
-            "description": "Caching library offering an object-oriented API for many cache backends",
-            "homepage": "https://www.doctrine-project.org",
+            "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.",
+            "homepage": "https://www.doctrine-project.org/projects/cache.html",
             "keywords": [
+                "abstraction",
+                "apcu",
                 "cache",
-                "caching"
+                "caching",
+                "couchdb",
+                "memcached",
+                "php",
+                "redis",
+                "xcache"
             ],
             "support": {
                 "issues": "https://github.com/doctrine/cache/issues",
-                "source": "https://github.com/doctrine/cache/tree/v1.8.1"
+                "source": "https://github.com/doctrine/cache/tree/2.1.1"
             },
-            "time": "2019-10-28T09:31:32+00:00"
+            "funding": [
+                {
+                    "url": "https://www.doctrine-project.org/sponsorship.html",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://www.patreon.com/phpdoctrine",
+                    "type": "patreon"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-07-17T14:49:29+00:00"
         },
         {
             "name": "doctrine/dbal",
-            "version": "2.13.5",
+            "version": "3.2.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/dbal.git",
-                "reference": "d92ddb260547c2a7b650ff140f744eb6f395d8fc"
+                "reference": "5d54f63541d7bed1156cb5c9b79274ced61890e4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/dbal/zipball/d92ddb260547c2a7b650ff140f744eb6f395d8fc",
-                "reference": "d92ddb260547c2a7b650ff140f744eb6f395d8fc",
+                "url": "https://api.github.com/repos/doctrine/dbal/zipball/5d54f63541d7bed1156cb5c9b79274ced61890e4",
+                "reference": "5d54f63541d7bed1156cb5c9b79274ced61890e4",
                 "shasum": ""
             },
             "require": {
-                "doctrine/cache": "^1.0|^2.0",
+                "composer/package-versions-deprecated": "^1.11.99",
+                "doctrine/cache": "^1.11|^2.0",
                 "doctrine/deprecations": "^0.5.3",
                 "doctrine/event-manager": "^1.0",
-                "ext-pdo": "*",
-                "php": "^7.1 || ^8"
+                "php": "^7.3 || ^8.0",
+                "psr/cache": "^1|^2|^3",
+                "psr/log": "^1|^2|^3"
             },
             "require-dev": {
                 "doctrine/coding-standard": "9.0.0",
                 "jetbrains/phpstorm-stubs": "2021.1",
-                "phpstan/phpstan": "1.1.1",
-                "phpunit/phpunit": "^7.5.20|^8.5|9.5.10",
+                "phpstan/phpstan": "1.2.0",
+                "phpstan/phpstan-strict-rules": "^1.1",
+                "phpunit/phpunit": "9.5.10",
                 "psalm/plugin-phpunit": "0.16.1",
                 "squizlabs/php_codesniffer": "3.6.1",
-                "symfony/cache": "^4.4",
-                "symfony/console": "^2.0.5|^3.0|^4.0|^5.0",
-                "vimeo/psalm": "4.12.0"
+                "symfony/cache": "^5.2|^6.0",
+                "symfony/console": "^2.0.5|^3.0|^4.0|^5.0|^6.0",
+                "vimeo/psalm": "4.13.0"
             },
             "suggest": {
                 "symfony/console": "For helpful console commands such as SQL execution and import of files."
@@ -357,7 +453,7 @@
             "type": "library",
             "autoload": {
                 "psr-4": {
-                    "Doctrine\\DBAL\\": "lib/Doctrine/DBAL"
+                    "Doctrine\\DBAL\\": "src"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
@@ -400,14 +496,13 @@
                 "queryobject",
                 "sasql",
                 "sql",
-                "sqlanywhere",
                 "sqlite",
                 "sqlserver",
                 "sqlsrv"
             ],
             "support": {
                 "issues": "https://github.com/doctrine/dbal/issues",
-                "source": "https://github.com/doctrine/dbal/tree/2.13.5"
+                "source": "https://github.com/doctrine/dbal/tree/3.2.0"
             },
             "funding": [
                 {
@@ -423,7 +518,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2021-11-11T16:27:36+00:00"
+            "time": "2021-11-26T21:00:12+00:00"
         },
         {
             "name": "doctrine/deprecations",
@@ -5572,79 +5667,6 @@
             },
             "time": "2020-07-03T15:54:43+00:00"
         },
-        {
-            "name": "composer/package-versions-deprecated",
-            "version": "1.11.99.4",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/composer/package-versions-deprecated.git",
-                "reference": "b174585d1fe49ceed21928a945138948cb394600"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/b174585d1fe49ceed21928a945138948cb394600",
-                "reference": "b174585d1fe49ceed21928a945138948cb394600",
-                "shasum": ""
-            },
-            "require": {
-                "composer-plugin-api": "^1.1.0 || ^2.0",
-                "php": "^7 || ^8"
-            },
-            "replace": {
-                "ocramius/package-versions": "1.11.99"
-            },
-            "require-dev": {
-                "composer/composer": "^1.9.3 || ^2.0@dev",
-                "ext-zip": "^1.13",
-                "phpunit/phpunit": "^6.5 || ^7"
-            },
-            "type": "composer-plugin",
-            "extra": {
-                "class": "PackageVersions\\Installer",
-                "branch-alias": {
-                    "dev-master": "1.x-dev"
-                }
-            },
-            "autoload": {
-                "psr-4": {
-                    "PackageVersions\\": "src/PackageVersions"
-                }
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Marco Pivetta",
-                    "email": "ocramius@gmail.com"
-                },
-                {
-                    "name": "Jordi Boggiano",
-                    "email": "j.boggiano@seld.be"
-                }
-            ],
-            "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)",
-            "support": {
-                "issues": "https://github.com/composer/package-versions-deprecated/issues",
-                "source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.4"
-            },
-            "funding": [
-                {
-                    "url": "https://packagist.com",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/composer",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/composer/composer",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2021-09-13T08:41:34+00:00"
-        },
         {
             "name": "composer/semver",
             "version": "3.2.5",
diff --git a/typo3/sysext/backend/Classes/Authentication/PasswordReset.php b/typo3/sysext/backend/Classes/Authentication/PasswordReset.php
index 4a95c85010b50f44559fedb68705370d4de2c01b..4d765aebdb22eaeeb282d478e95cebfdd3cfc40f 100644
--- a/typo3/sysext/backend/Classes/Authentication/PasswordReset.php
+++ b/typo3/sysext/backend/Classes/Authentication/PasswordReset.php
@@ -17,7 +17,7 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Backend\Authentication;
 
-use Doctrine\DBAL\Platforms\MySqlPlatform;
+use Doctrine\DBAL\Platforms\MySQLPlatform;
 use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Message\UriInterface;
 use Psr\Log\LoggerAwareInterface;
@@ -285,7 +285,7 @@ class PasswordReset implements LoggerAwareInterface
         $queryBuilder
             ->select('uid', 'email', 'password_reset_token')
             ->from('be_users');
-        if ($queryBuilder->getConnection()->getDatabasePlatform() instanceof MySqlPlatform) {
+        if ($queryBuilder->getConnection()->getDatabasePlatform() instanceof MySQLPlatform) {
             $queryBuilder->andWhere(
                 $queryBuilder->expr()->comparison('SHA1(CONCAT(' . $queryBuilder->quoteIdentifier('email') . ', ' . $queryBuilder->quoteIdentifier('uid') . '))', $queryBuilder->expr()::EQ, $queryBuilder->createNamedParameter($identity))
             );
diff --git a/typo3/sysext/backend/Classes/Utility/BackendUtility.php b/typo3/sysext/backend/Classes/Utility/BackendUtility.php
index a0cc5baca367a074129f87a1836ed34ad5384089..3ecffca875246baa49d78bbc1b78adbce4d69305 100644
--- a/typo3/sysext/backend/Classes/Utility/BackendUtility.php
+++ b/typo3/sysext/backend/Classes/Utility/BackendUtility.php
@@ -429,8 +429,8 @@ class BackendUtility
         if (is_array($pageForRootlineCache[$ident] ?? false)) {
             $row = $pageForRootlineCache[$ident];
         } else {
-            $statement = $runtimeCache->get('getPageForRootlineStatement-' . $statementCacheIdent);
-            if (!$statement) {
+            $queryBuilder = $runtimeCache->get('getPageForRootlineStatement-' . $statementCacheIdent);
+            if (!$queryBuilder) {
                 $queryBuilder = static::getQueryBuilderForTable('pages');
                 $queryBuilder->getRestrictions()
                              ->removeAll()
@@ -466,17 +466,11 @@ class BackendUtility
                         $queryBuilder->expr()->eq('uid', $queryBuilder->createPositionalParameter($uid, \PDO::PARAM_INT)),
                         QueryHelper::stripLogicalOperatorPrefix($clause)
                     );
-                $statement = $queryBuilder->execute();
-                if (class_exists(\Doctrine\DBAL\ForwardCompatibility\Result::class) && $statement instanceof \Doctrine\DBAL\ForwardCompatibility\Result) {
-                    $statement = $statement->getIterator();
-                }
-                $runtimeCache->set('getPageForRootlineStatement-' . $statementCacheIdent, $statement);
+                $runtimeCache->set('getPageForRootlineStatement-' . $statementCacheIdent, $queryBuilder);
             } else {
-                $statement->bindValue(1, (int)$uid);
-                $statement->execute();
+                $queryBuilder->setParameter(0, (int)$uid);
             }
-            $row = $statement->fetchAssociative();
-            $statement->free();
+            $row = $queryBuilder->executeQuery()->fetchAssociative();
 
             if ($row) {
                 if ($workspaceOL) {
diff --git a/typo3/sysext/core/Classes/DataHandling/DataHandler.php b/typo3/sysext/core/Classes/DataHandling/DataHandler.php
index 34a5c1a529a1048fef03d3af0d2516556003db97..f7048264ea17465c4b0967dea79e75ade0a9cbb7 100644
--- a/typo3/sysext/core/Classes/DataHandling/DataHandler.php
+++ b/typo3/sysext/core/Classes/DataHandling/DataHandler.php
@@ -16,7 +16,7 @@
 namespace TYPO3\CMS\Core\DataHandling;
 
 use Doctrine\DBAL\Exception as DBALException;
-use Doctrine\DBAL\Platforms\PostgreSQL94Platform as PostgreSqlPlatform;
+use Doctrine\DBAL\Platforms\PostgreSQL94Platform as PostgreSQLPlatform;
 use Doctrine\DBAL\Platforms\SQLServer2012Platform as SQLServerPlatform;
 use Doctrine\DBAL\Types\IntegerType;
 use Psr\Log\LoggerAwareInterface;
@@ -2374,16 +2374,13 @@ class DataHandler implements LoggerAwareInterface
         $newValue = $originalValue = $value;
         $queryBuilder = $this->getUniqueCountStatement($newValue, $table, $field, (int)$id, (int)$newPid);
         // For as long as records with the test-value existing, try again (with incremented numbers appended)
-        $statement = $queryBuilder->execute();
-        if ($statement->fetchOne()) {
+        $result = $queryBuilder->executeQuery();
+        if ($result->fetchOne()) {
             for ($counter = 0; $counter <= 100; $counter++) {
                 $newValue = $value . $counter;
-                if (class_exists(\Doctrine\DBAL\ForwardCompatibility\Result::class) && $statement instanceof \Doctrine\DBAL\ForwardCompatibility\Result) {
-                    $statement = $statement->getIterator();
-                }
-                $statement->bindValue(1, $newValue);
-                $statement->execute();
-                if (!$statement->fetchOne()) {
+                $queryBuilder->setParameter(0, $newValue);
+                $result = $queryBuilder->executeQuery();
+                if (!$result->fetchOne()) {
                     break;
                 }
             }
diff --git a/typo3/sysext/core/Classes/Database/Connection.php b/typo3/sysext/core/Classes/Database/Connection.php
index 3a40ad989825e7f4816b530eb2728147a48ad399..27f4af430ca92981d5c5446cd6b6ef51e15f210e 100644
--- a/typo3/sysext/core/Classes/Database/Connection.php
+++ b/typo3/sysext/core/Classes/Database/Connection.php
@@ -20,12 +20,9 @@ namespace TYPO3\CMS\Core\Database;
 use Doctrine\Common\EventManager;
 use Doctrine\DBAL\Configuration;
 use Doctrine\DBAL\Driver;
-use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
 use Doctrine\DBAL\Platforms\PostgreSQL94Platform as PostgreSqlPlatform;
 use Doctrine\DBAL\Platforms\SQLServer2012Platform;
 use Doctrine\DBAL\Result;
-use Doctrine\DBAL\Schema\AbstractSchemaManager;
-use Doctrine\DBAL\VersionAwarePlatformDriver;
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerAwareTrait;
 use TYPO3\CMS\Core\Database\Query\BulkInsertQuery;
@@ -400,31 +397,14 @@ class Connection extends \Doctrine\DBAL\Connection implements LoggerAwareInterfa
                 break;
         }
 
-        // Driver does not support version specific platforms.
-        if (!$this->getDriver() instanceof VersionAwarePlatformDriver) {
-            return $version;
-        }
-
-        if ($this->getWrappedConnection() instanceof ServerInfoAwareConnection
-            && !$this->getWrappedConnection()->requiresQueryForServerVersion()
-        ) {
+        // if clause can be removed with Doctrine DBAL 4.
+        if (method_exists($this->getWrappedConnection(), 'getServerVersion')) {
             $version .= ' ' . $this->getWrappedConnection()->getServerVersion();
         }
 
         return $version;
     }
 
-    /**
-     * Creates a SchemaManager that can be used to inspect or change the
-     * database schema through the connection.
-     *
-     * This is a copy from Doctrine DBAL 3.x, and can be removed once Doctrine DBAL 3.2 is included
-     */
-    public function createSchemaManager(): AbstractSchemaManager
-    {
-        return $this->_driver->getSchemaManager($this);
-    }
-
     /**
      * Execute commands after initializing a new connection.
      *
diff --git a/typo3/sysext/core/Classes/Database/ConnectionPool.php b/typo3/sysext/core/Classes/Database/ConnectionPool.php
index 36616f22afd80df345a2d9345c9a36eb27f1c001..1694f959e7cf971bb7813a33e00cb9233ac8c7a6 100644
--- a/typo3/sysext/core/Classes/Database/ConnectionPool.php
+++ b/typo3/sysext/core/Classes/Database/ConnectionPool.php
@@ -184,7 +184,6 @@ class ConnectionPool
 
         /** @var Connection $conn */
         $conn = DriverManager::getConnection($connectionParams);
-        $conn->setFetchMode(\PDO::FETCH_ASSOC);
         $conn->prepareConnection($connectionParams['initCommands'] ?? '');
 
         // Register custom data types
diff --git a/typo3/sysext/core/Classes/Database/Driver/DriverConnection.php b/typo3/sysext/core/Classes/Database/Driver/DriverConnection.php
new file mode 100644
index 0000000000000000000000000000000000000000..3f36a63f88a802695efd899f2dfc1c75c6c58ddc
--- /dev/null
+++ b/typo3/sysext/core/Classes/Database/Driver/DriverConnection.php
@@ -0,0 +1,111 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Database\Driver;
+
+use Doctrine\DBAL\Driver\Connection as ConnectionInterface;
+use Doctrine\DBAL\Driver\PDO\Connection as DoctrineDbalPDOConnection;
+use Doctrine\DBAL\Driver\PDO\Exception;
+use Doctrine\DBAL\Driver\Result as ResultInterface;
+use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
+use Doctrine\DBAL\Driver\Statement as StatementInterface;
+use Doctrine\DBAL\ParameterType;
+use PDO;
+
+/**
+ * This is a full "clone" of the class of package doctrine/dbal. Scope is to use instanatiate TYPO3's DriverResult
+ * and DriverStatement objects instead of Doctrine's native implementation.
+ *
+ * @internal this implementation is not part of TYPO3's Public API.
+ */
+class DriverConnection implements ConnectionInterface, ServerInfoAwareConnection
+{
+    protected DoctrineDbalPDOConnection $doctrineDbalPDOConnection;
+    protected PDO $connection;
+
+    public function __construct(PDO $connection)
+    {
+        $this->connection = $connection;
+        $this->doctrineDbalPDOConnection = new DoctrineDbalPDOConnection($connection);
+    }
+
+    public function exec(string $sql): int
+    {
+        return $this->doctrineDbalPDOConnection->exec($sql);
+    }
+
+    public function getServerVersion()
+    {
+        return $this->doctrineDbalPDOConnection->getServerVersion();
+    }
+
+    public function prepare(string $sql): StatementInterface
+    {
+        try {
+            $stmt = $this->connection->prepare($sql);
+            assert($stmt instanceof \PDOStatement);
+
+            // use TYPO3's Statement object in favor of Doctrine's Statement wrapper
+            return new DriverStatement($stmt);
+        } catch (\PDOException $exception) {
+            throw Exception::new($exception);
+        }
+    }
+
+    public function query(string $sql): ResultInterface
+    {
+        try {
+            $stmt = $this->connection->query($sql);
+            assert($stmt instanceof \PDOStatement);
+
+            // use TYPO3's Result object in favor of Doctrine's Result wrapper
+            return new DriverResult($stmt);
+        } catch (\PDOException $exception) {
+            throw Exception::new($exception);
+        }
+    }
+
+    public function quote($value, $type = ParameterType::STRING)
+    {
+        return $this->doctrineDbalPDOConnection->quote($value, $type);
+    }
+
+    public function lastInsertId($name = null)
+    {
+        return $this->doctrineDbalPDOConnection->lastInsertId($name);
+    }
+
+    public function beginTransaction(): bool
+    {
+        return $this->doctrineDbalPDOConnection->beginTransaction();
+    }
+
+    public function commit(): bool
+    {
+        return $this->doctrineDbalPDOConnection->commit();
+    }
+
+    public function rollBack(): bool
+    {
+        return $this->doctrineDbalPDOConnection->rollBack();
+    }
+
+    public function getWrappedConnection(): PDO
+    {
+        return $this->doctrineDbalPDOConnection->getWrappedConnection();
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Driver/DriverResult.php b/typo3/sysext/core/Classes/Database/Driver/DriverResult.php
new file mode 100644
index 0000000000000000000000000000000000000000..4a8b93b9e53d57f95f237a6bdb16da2caf0395ae
--- /dev/null
+++ b/typo3/sysext/core/Classes/Database/Driver/DriverResult.php
@@ -0,0 +1,172 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Database\Driver;
+
+use Doctrine\DBAL\Driver\PDO\Exception;
+use Doctrine\DBAL\Driver\Result as ResultInterface;
+
+/**
+ * TYPO3's custom Result object for Database statements based on Doctrine DBAL.
+ *
+ * This is a lowlevel wrapper around PDO for TYPO3 based drivers to ensure mapResourceToString()
+ * is called when retrieving data. This isn't the actual Result object (Doctrine\DBAL\Result) which
+ * is used in user-land code.
+ *
+ * Because Doctrine's DBAL Driver Result object is marked as final, all logic is copied from the ResultInterface.
+ *
+ * @internal this implementation is not part of TYPO3's Public API.
+ */
+class DriverResult implements ResultInterface
+{
+    private \PDOStatement $statement;
+
+    /**
+     * @internal The result can be only instantiated by its driver connection or statement.
+     */
+    public function __construct(\PDOStatement $statement)
+    {
+        $this->statement = $statement;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function fetchNumeric()
+    {
+        return $this->fetch(\PDO::FETCH_NUM);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function fetchAssociative()
+    {
+        return $this->fetch(\PDO::FETCH_ASSOC);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function fetchOne()
+    {
+        return $this->fetch(\PDO::FETCH_COLUMN);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function fetchAllNumeric(): array
+    {
+        return $this->fetchAll(\PDO::FETCH_NUM);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function fetchAllAssociative(): array
+    {
+        return $this->fetchAll(\PDO::FETCH_ASSOC);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function fetchFirstColumn(): array
+    {
+        return $this->fetchAll(\PDO::FETCH_COLUMN);
+    }
+
+    public function rowCount(): int
+    {
+        try {
+            return $this->statement->rowCount();
+        } catch (\PDOException $exception) {
+            throw Exception::new($exception);
+        }
+    }
+
+    public function columnCount(): int
+    {
+        try {
+            return $this->statement->columnCount();
+        } catch (\PDOException $exception) {
+            throw Exception::new($exception);
+        }
+    }
+
+    public function free(): void
+    {
+        $this->statement->closeCursor();
+    }
+
+    /**
+     * @return mixed|false
+     *
+     * @throws Exception
+     */
+    private function fetch(int $mode)
+    {
+        try {
+            $result = $this->statement->fetch($mode);
+            $result = $this->mapResourceToString($result);
+            return $result;
+        } catch (\PDOException $exception) {
+            throw Exception::new($exception);
+        }
+    }
+
+    /**
+     * @return list<mixed>
+     *
+     * @throws Exception
+     */
+    private function fetchAll(int $mode): array
+    {
+        try {
+            $data = $this->statement->fetchAll($mode);
+        } catch (\PDOException $exception) {
+            throw Exception::new($exception);
+        }
+
+        assert(is_array($data));
+        return array_map([$this, 'mapResourceToString'], $data);
+    }
+
+    /**
+     * Map resources to string like is done for e.g. in mysqli driver
+     *
+     * @param mixed $record
+     * @return mixed
+     */
+    protected function mapResourceToString($record)
+    {
+        if (is_array($record)) {
+            return array_map(
+                static function ($value) {
+                    if (is_resource($value)) {
+                        $value = stream_get_contents($value);
+                    }
+                    return $value;
+                },
+                $record
+            );
+        }
+
+        return $record;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Driver/DriverStatement.php b/typo3/sysext/core/Classes/Database/Driver/DriverStatement.php
new file mode 100644
index 0000000000000000000000000000000000000000..98075ac43807a83769c46d490a6c5629d8dae5c0
--- /dev/null
+++ b/typo3/sysext/core/Classes/Database/Driver/DriverStatement.php
@@ -0,0 +1,139 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\Database\Driver;
+
+use Doctrine\DBAL\Driver\Exception as ExceptionInterface;
+use Doctrine\DBAL\Driver\Exception\UnknownParameterType;
+use Doctrine\DBAL\Driver\PDO\Exception;
+use Doctrine\DBAL\Driver\Result as ResultInterface;
+use Doctrine\DBAL\Driver\Statement as StatementInterface;
+use Doctrine\DBAL\ParameterType;
+use Doctrine\Deprecations\Deprecation;
+
+/**
+ * TYPO3's custom Statement object for Database statements based on Doctrine DBAL in TYPO3's drivers.
+ *
+ * This is a lowlevel wrapper around PDOStatement for TYPO3 based drivers to ensure the PDOStatement is put into
+ * TYPO3's DriverResult object, and not in Doctrine's Result object. If Doctrine DBAL had a factory
+ * for DriverResults this class could be removed.
+ *
+ * Because Doctrine's DBAL Driver PDO-Statement object is marked as final, all logic is copied from that class.
+ *
+ * @internal this implementation is not part of TYPO3's Public API.
+ */
+class DriverStatement implements StatementInterface
+{
+    private const PARAM_TYPE_MAP = [
+        ParameterType::NULL => \PDO::PARAM_NULL,
+        ParameterType::INTEGER => \PDO::PARAM_INT,
+        ParameterType::STRING => \PDO::PARAM_STR,
+        ParameterType::ASCII => \PDO::PARAM_STR,
+        ParameterType::BINARY => \PDO::PARAM_LOB,
+        ParameterType::LARGE_OBJECT => \PDO::PARAM_LOB,
+        ParameterType::BOOLEAN => \PDO::PARAM_BOOL,
+    ];
+
+    /** @var \PDOStatement */
+    private $stmt;
+
+    /**
+     * @internal The statement can be only instantiated by its driver connection.
+     */
+    public function __construct(\PDOStatement $stmt)
+    {
+        $this->stmt = $stmt;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function bindValue($param, $value, $type = ParameterType::STRING)
+    {
+        $type = $this->convertParamType($type);
+
+        try {
+            return $this->stmt->bindValue($param, $value, $type);
+        } catch (\PDOException $exception) {
+            throw Exception::new($exception);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param mixed    $param
+     * @param mixed    $variable
+     * @param int      $type
+     * @param int|null $length
+     * @param mixed    $driverOptions The usage of the argument is deprecated.
+     */
+    public function bindParam(
+        $param,
+        &$variable,
+        $type = ParameterType::STRING,
+        $length = null,
+        $driverOptions = null
+    ): bool {
+        if (func_num_args() > 4) {
+            Deprecation::triggerIfCalledFromOutside(
+                'doctrine/dbal',
+                'https://github.com/doctrine/dbal/issues/4533',
+                'The $driverOptions argument of Statement::bindParam() is deprecated.'
+            );
+        }
+
+        $type = $this->convertParamType($type);
+
+        try {
+            return $this->stmt->bindParam($param, $variable, $type, ...array_slice(func_get_args(), 3));
+        } catch (\PDOException $exception) {
+            throw Exception::new($exception);
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function execute($params = null): ResultInterface
+    {
+        try {
+            $this->stmt->execute($params);
+        } catch (\PDOException $exception) {
+            throw Exception::new($exception);
+        }
+
+        // use TYPO3's Result object in favor of Doctrine's Result wrapper
+        return new DriverResult($this->stmt);
+    }
+
+    /**
+     * Converts DBAL parameter type to PDO parameter type
+     *
+     * @param int $type Parameter type
+     *
+     * @throws ExceptionInterface
+     */
+    private function convertParamType(int $type): int
+    {
+        if (! isset(self::PARAM_TYPE_MAP[$type])) {
+            throw UnknownParameterType::new($type);
+        }
+
+        return self::PARAM_TYPE_MAP[$type];
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Driver/PDOConnection.php b/typo3/sysext/core/Classes/Database/Driver/PDOConnection.php
deleted file mode 100644
index aee45f6ace5c4801f4a295bcf2083add216aad9d..0000000000000000000000000000000000000000
--- a/typo3/sysext/core/Classes/Database/Driver/PDOConnection.php
+++ /dev/null
@@ -1,42 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-namespace TYPO3\CMS\Core\Database\Driver;
-
-use Doctrine\DBAL\Driver\PDO\Connection as DoctrineDbalPDOConnection;
-use Doctrine\DBAL\Driver\PDO\Exception as PDOException;
-use PDO;
-
-/**
- * This is a full "clone" of the class of package doctrine/dbal. Scope is to use the PDOConnection of TYPO3.
- * All private methods have to be checked on every release of doctrine/dbal.
- */
-class PDOConnection extends DoctrineDbalPDOConnection
-{
-    /**
-     * {@inheritdoc}
-     */
-    public function __construct($dsn, $user = null, $password = null, ?array $options = null)
-    {
-        try {
-            parent::__construct($dsn, $user, $password, $options);
-            $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, [PDOStatement::class, []]);
-        } catch (\PDOException $exception) {
-            throw new PDOException($exception);
-        }
-    }
-}
diff --git a/typo3/sysext/core/Classes/Database/Driver/PDOMySql/Driver.php b/typo3/sysext/core/Classes/Database/Driver/PDOMySql/Driver.php
index c9d6edca6c695a3430d6ab061183cf73584afcc0..da655c72138016557404d169ca2288ca19924e3a 100644
--- a/typo3/sysext/core/Classes/Database/Driver/PDOMySql/Driver.php
+++ b/typo3/sysext/core/Classes/Database/Driver/PDOMySql/Driver.php
@@ -18,38 +18,51 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Core\Database\Driver\PDOMySql;
 
 use Doctrine\DBAL\Driver\AbstractMySQLDriver;
-use Doctrine\DBAL\Exception as DBALException;
+use Doctrine\DBAL\Driver\Connection as DriverConnectionInterface;
+use Doctrine\DBAL\Driver\PDO\Exception;
+use PDO;
 use PDOException;
-use TYPO3\CMS\Core\Database\Driver\PDOConnection;
+use TYPO3\CMS\Core\Database\Driver\DriverConnection;
 
 /**
- * This is a full "clone" of the class of package doctrine/dbal. Scope is to use the PDOConnection of TYPO3.
+ * The main change in favor of Doctrine's implementation is to use our custom DriverConnection (which in turn creates
+ * a custom Result object).
+ *
  * All private methods have to be checked on every release of doctrine/dbal.
+ *
+ * @internal this implementation is not part of TYPO3's Public API.
  */
 class Driver extends AbstractMySQLDriver
 {
     /**
      * {@inheritdoc}
+     *
+     * @return DriverConnectionInterface
      */
-    public function connect(array $params, $username = null, $password = null, array $driverOptions = [])
+    public function connect(array $params)
     {
+        $driverOptions = $params['driverOptions'] ?? [];
+
+        if (! empty($params['persistent'])) {
+            $driverOptions[PDO::ATTR_PERSISTENT] = true;
+        }
+
         try {
-            $conn = new PDOConnection(
+            $pdo = new PDO(
                 $this->constructPdoDsn($params),
-                $username,
-                $password,
+                $params['user'] ?? '',
+                $params['password'] ?? '',
                 $driverOptions
             );
-
             // use prepared statements for pdo_mysql per default to retrieve native data types
             if (!isset($driverOptions[\PDO::ATTR_EMULATE_PREPARES])) {
-                $conn->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
+                $pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
             }
-        } catch (PDOException $e) {
-            throw DBALException::driverException($this, $e);
+        } catch (PDOException $exception) {
+            throw Exception::new($exception);
         }
 
-        return $conn;
+        return new DriverConnection($pdo);
     }
 
     /**
diff --git a/typo3/sysext/core/Classes/Database/Driver/PDOPgSql/Driver.php b/typo3/sysext/core/Classes/Database/Driver/PDOPgSql/Driver.php
index 4a6e19c6f836c3cf2c1c4bcb9f47b8fffd0654a1..ba4b061c7f513d91e511710f26fa93696ab99089 100644
--- a/typo3/sysext/core/Classes/Database/Driver/PDOPgSql/Driver.php
+++ b/typo3/sysext/core/Classes/Database/Driver/PDOPgSql/Driver.php
@@ -18,28 +18,39 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Core\Database\Driver\PDOPgSql;
 
 use Doctrine\DBAL\Driver\AbstractPostgreSQLDriver;
-use Doctrine\DBAL\Exception as DBALException;
+use Doctrine\DBAL\Driver\Connection as DriverConnectionInterface;
+use Doctrine\DBAL\Driver\PDO\Exception;
 use PDO;
 use PDOException;
-use TYPO3\CMS\Core\Database\Driver\PDOConnection;
+use TYPO3\CMS\Core\Database\Driver\DriverConnection;
 
 /**
- * This is a full "clone" of the class of package doctrine/dbal. Scope is to use the PDOConnection of TYPO3.
- * All private methods have to be checked on every release of doctrine/dbal.
+ * The main change in favor of Doctrine's implementation is to use our custom DriverConnection (which in turn creates
+ * a custom Result object).
+ *
+ * @internal this implementation is not part of TYPO3's Public API.
  */
 class Driver extends AbstractPostgreSQLDriver
 {
     /**
      * {@inheritdoc}
+     *
+     * @return DriverConnectionInterface
      */
-    public function connect(array $params, $username = null, $password = null, array $driverOptions = [])
+    public function connect(array $params)
     {
+        $driverOptions = $params['driverOptions'] ?? [];
+
+        if (! empty($params['persistent'])) {
+            $driverOptions[PDO::ATTR_PERSISTENT] = true;
+        }
+
         try {
-            $pdo = new PDOConnection(
+            $pdo = new PDO(
                 $this->_constructPdoDsn($params),
-                $username,
-                $password,
-                $driverOptions
+                $params['user'] ?? '',
+                $params['password'] ?? '',
+                $driverOptions,
             );
 
             if (defined('PDO::PGSQL_ATTR_DISABLE_PREPARES')
@@ -58,11 +69,11 @@ class Driver extends AbstractPostgreSQLDriver
             if (isset($params['charset'])) {
                 $pdo->exec('SET NAMES \'' . $params['charset'] . '\'');
             }
-
-            return $pdo;
-        } catch (PDOException $e) {
-            throw DBALException::driverException($this, $e);
+        } catch (PDOException $exception) {
+            throw Exception::new($exception);
         }
+
+        return new DriverConnection($pdo);
     }
 
     /**
diff --git a/typo3/sysext/core/Classes/Database/Driver/PDOSqlite/Driver.php b/typo3/sysext/core/Classes/Database/Driver/PDOSqlite/Driver.php
index 12f6593fc2f47fc48eabb89e106fd2d65cf9b8f8..c1c3c1e0c0520eaca0e02a4fa5b16e05440c5fd4 100644
--- a/typo3/sysext/core/Classes/Database/Driver/PDOSqlite/Driver.php
+++ b/typo3/sysext/core/Classes/Database/Driver/PDOSqlite/Driver.php
@@ -18,14 +18,18 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Core\Database\Driver\PDOSqlite;
 
 use Doctrine\DBAL\Driver\AbstractSQLiteDriver;
-use Doctrine\DBAL\Exception as DBALException;
+use Doctrine\DBAL\Driver\Connection as DriverConnectionInterface;
+use Doctrine\DBAL\Driver\PDO\Exception;
 use Doctrine\DBAL\Platforms\SqlitePlatform;
+use PDO;
 use PDOException;
-use TYPO3\CMS\Core\Database\Driver\PDOConnection;
+use TYPO3\CMS\Core\Database\Driver\DriverConnection as TYPO3DriverConnection;
 
 /**
- * This is a full "clone" of the class of package doctrine/dbal. Scope is to use the PDOConnection of TYPO3.
- * All private methods have to be checked on every release of doctrine/dbal.
+ * The main change in favor of Doctrine's implementation is to use our custom DriverConnection (which in turn creates
+ * a custom Result object).
+ *
+ * @internal this implementation is not part of TYPO3's Public API.
  */
 class Driver extends AbstractSQLiteDriver
 {
@@ -40,9 +44,13 @@ class Driver extends AbstractSQLiteDriver
 
     /**
      * {@inheritdoc}
+     *
+     * @return DriverConnectionInterface
      */
-    public function connect(array $params, $username = null, $password = null, array $driverOptions = [])
+    public function connect(array $params)
     {
+        $driverOptions = $params['driverOptions'] ?? [];
+
         if (isset($driverOptions['userDefinedFunctions'])) {
             $this->_userDefinedFunctions = array_merge(
                 $this->_userDefinedFunctions,
@@ -52,21 +60,21 @@ class Driver extends AbstractSQLiteDriver
         }
 
         try {
-            $pdo = new PDOConnection(
+            $pdo = new PDO(
                 $this->_constructPdoDsn($params),
-                $username,
-                $password,
+                $params['user'] ?? '',
+                $params['password'] ?? '',
                 $driverOptions
             );
-        } catch (PDOException $ex) {
-            throw DBALException::driverException($this, $ex);
+        } catch (PDOException $exception) {
+            throw Exception::new($exception);
         }
 
         foreach ($this->_userDefinedFunctions as $fn => $data) {
             $pdo->sqliteCreateFunction($fn, $data['callback'], $data['numArgs']);
         }
 
-        return $pdo;
+        return new TYPO3DriverConnection($pdo);
     }
 
     /**
diff --git a/typo3/sysext/core/Classes/Database/Driver/PDOSqlsrv/Connection.php b/typo3/sysext/core/Classes/Database/Driver/PDOSqlsrv/Connection.php
index 61c674cdb019237226474a3faf249faf6b85d821..5ff23ead550c3e969181278351edd2ba6da4b451 100644
--- a/typo3/sysext/core/Classes/Database/Driver/PDOSqlsrv/Connection.php
+++ b/typo3/sysext/core/Classes/Database/Driver/PDOSqlsrv/Connection.php
@@ -17,26 +17,29 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Core\Database\Driver\PDOSqlsrv;
 
-use Doctrine\DBAL\Driver\Result;
-use PDO;
+use Doctrine\DBAL\Driver\PDO\Exception;
+use Doctrine\DBAL\Driver\Statement as StatementInterface;
+use TYPO3\CMS\Core\Database\Driver\DriverConnection;
 
 /**
  * This is a full "clone" of the class of package doctrine/dbal. Scope is to use the PDOConnection of TYPO3.
- * All private methods have to be checked on every release of doctrine/dbal.
+ *
+ * @internal this implementation is not part of TYPO3's Public API.
  */
-class Connection extends \Doctrine\DBAL\Driver\PDO\Connection
+class Connection extends DriverConnection
 {
-    /**
-     * @internal The connection can be only instantiated by its driver.
-     *
-     * {@inheritdoc}
-     */
-    public function __construct($dsn, $user = null, $password = null, ?array $options = null)
+    public function prepare(string $sql): StatementInterface
     {
-        parent::__construct($dsn, $user, $password, $options);
-        $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, [Statement::class, []]);
+        try {
+            $stmt = $this->connection->prepare($sql);
+            assert($stmt instanceof \PDOStatement);
+
+            // use TYPO3's Sqlsrv Statement object in favor of Doctrine's Statement wrapper
+            return new Statement($stmt);
+        } catch (\PDOException $exception) {
+            throw Exception::new($exception);
+        }
     }
-
     /**
      * {@inheritDoc}
      */
@@ -47,12 +50,7 @@ class Connection extends \Doctrine\DBAL\Driver\PDO\Connection
         }
 
         $stmt = $this->prepare('SELECT CONVERT(VARCHAR(MAX), current_value) FROM sys.sequences WHERE name = ?');
-        $stmt->execute([$name]);
-
-        if ($stmt instanceof Result) {
-            return $stmt->fetchOne();
-        }
-
-        return $stmt->fetchColumn();
+        $result = $stmt->execute([$name]);
+        return $result->fetchOne();
     }
 }
diff --git a/typo3/sysext/core/Classes/Database/Driver/PDOSqlsrv/Driver.php b/typo3/sysext/core/Classes/Database/Driver/PDOSqlsrv/Driver.php
index 4e2220a885db03ca673f9a0d7f9da49a8fe57ab8..7afaac4e6e9c525eebcf7b4ab55b39a179d3f65e 100644
--- a/typo3/sysext/core/Classes/Database/Driver/PDOSqlsrv/Driver.php
+++ b/typo3/sysext/core/Classes/Database/Driver/PDOSqlsrv/Driver.php
@@ -19,26 +19,54 @@ namespace TYPO3\CMS\Core\Database\Driver\PDOSqlsrv;
 
 use Doctrine\DBAL\Driver\AbstractSQLServerDriver;
 use Doctrine\DBAL\Driver\AbstractSQLServerDriver\Exception\PortWithoutHost;
+use Doctrine\DBAL\Driver\Connection as DriverConnection;
+use Doctrine\DBAL\Driver\PDO\Exception as PDOException;
 
 /**
- * This is a full "clone" of the class of package doctrine/dbal. Scope is to use the PDOConnection of TYPO3.
+ * The main change in favor of Doctrine's implementation is to use our custom DriverConnection (which in turn creates
+ * a custom Result object).
+ *
  * All private methods have to be checked on every release of doctrine/dbal.
+ *
+ * @internal this implementation is not part of TYPO3's Public API.
  */
 class Driver extends AbstractSQLServerDriver
 {
     /**
      * {@inheritdoc}
+     *
+     * @return DriverConnection
      */
-    public function connect(array $params, $username = null, $password = null, array $driverOptions = [])
+    public function connect(array $params)
     {
-        [$driverOptions, $connectionOptions] = $this->splitOptions($driverOptions);
-
-        return new Connection(
-            $this->_constructPdoDsn($params, $connectionOptions),
-            $username,
-            $password,
-            $driverOptions
-        );
+        $driverOptions = $dsnOptions = [];
+
+        if (isset($params['driverOptions'])) {
+            foreach ($params['driverOptions'] as $option => $value) {
+                if (is_int($option)) {
+                    $driverOptions[$option] = $value;
+                } else {
+                    $dsnOptions[$option] = $value;
+                }
+            }
+        }
+
+        if (!empty($params['persistent'])) {
+            $driverOptions[\PDO::ATTR_PERSISTENT] = true;
+        }
+
+        try {
+            $pdo = new \PDO(
+                $this->_constructPdoDsn($params, $dsnOptions),
+                $params['user'] ?? '',
+                $params['password'] ?? '',
+                $driverOptions
+            );
+        } catch (\PDOException $exception) {
+            throw PDOException::new($exception);
+        }
+
+        return new Connection($pdo);
     }
 
     /**
@@ -74,22 +102,6 @@ class Driver extends AbstractSQLServerDriver
         return $dsn . $this->getConnectionOptionsDsn($connectionOptions);
     }
 
-    private function splitOptions(array $options): array
-    {
-        $driverOptions     = [];
-        $connectionOptions = [];
-
-        foreach ($options as $optionKey => $optionValue) {
-            if (is_int($optionKey)) {
-                $driverOptions[$optionKey] = $optionValue;
-            } else {
-                $connectionOptions[$optionKey] = $optionValue;
-            }
-        }
-
-        return [$driverOptions, $connectionOptions];
-    }
-
     /**
      * Converts a connection options array to the DSN
      *
diff --git a/typo3/sysext/core/Classes/Database/Driver/PDOSqlsrv/Statement.php b/typo3/sysext/core/Classes/Database/Driver/PDOSqlsrv/Statement.php
index ebec0cdcf007b79959ea59dd5fdcbe7c8759a356..7181c25e6807b3d432ea729300e8f23125c345c0 100644
--- a/typo3/sysext/core/Classes/Database/Driver/PDOSqlsrv/Statement.php
+++ b/typo3/sysext/core/Classes/Database/Driver/PDOSqlsrv/Statement.php
@@ -18,27 +18,39 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Core\Database\Driver\PDOSqlsrv;
 
 use Doctrine\DBAL\ParameterType;
-use PDO;
-use TYPO3\CMS\Core\Database\Driver\PDOStatement;
+use TYPO3\CMS\Core\Database\Driver\DriverStatement;
 
 /**
  * This is a full "clone" of the class of package doctrine/dbal. Scope is to use the PDOConnection of TYPO3.
  * All private methods have to be checked on every release of doctrine/dbal.
+ *
+ * @internal this implementation is not part of TYPO3's Public API.
  */
-class Statement extends PDOStatement
+class Statement extends DriverStatement
 {
     /**
-     * {@inheritdoc}
+     * {@inheritDoc}
+     *
+     * @param mixed    $param
+     * @param mixed    $variable
+     * @param int      $type
+     * @param int|null $length
+     * @param mixed    $driverOptions The usage of the argument is deprecated.
      */
-    public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null, $driverOptions = null)
-    {
+    public function bindParam(
+        $param,
+        &$variable,
+        $type = ParameterType::STRING,
+        $length = null,
+        $driverOptions = null
+    ): bool {
         if (($type === ParameterType::LARGE_OBJECT || $type === ParameterType::BINARY)
             && $driverOptions === null
         ) {
-            $driverOptions = PDO::SQLSRV_ENCODING_BINARY;
+            $driverOptions = \PDO::SQLSRV_ENCODING_BINARY;
         }
 
-        return parent::bindParam($column, $variable, $type, $length, $driverOptions);
+        return parent::bindParam($param, $variable, $type, $length, $driverOptions);
     }
 
     /**
diff --git a/typo3/sysext/core/Classes/Database/Driver/PDOStatement.php b/typo3/sysext/core/Classes/Database/Driver/PDOStatement.php
deleted file mode 100644
index aa94b0312eb88c98ec60c9a57d8bc24f30db6819..0000000000000000000000000000000000000000
--- a/typo3/sysext/core/Classes/Database/Driver/PDOStatement.php
+++ /dev/null
@@ -1,91 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-namespace TYPO3\CMS\Core\Database\Driver;
-
-use Doctrine\DBAL\Driver\PDO\Exception as PDOException;
-use Doctrine\DBAL\Driver\PDO\Statement as DoctrineDbalPDOStatement;
-use PDO;
-
-class PDOStatement extends DoctrineDbalPDOStatement
-{
-    /**
-     * The method fetchAll() is moved into a separate trait to switch method signatures
-     * depending on the PHP major version in use to support PHP8
-     */
-    use PDOStatementImplementation;
-
-    /**
-     * Map resources to string like is done for e.g. in mysqli driver
-     *
-     * @param mixed $record
-     * @return mixed
-     */
-    protected function mapResourceToString($record)
-    {
-        if (is_array($record)) {
-            return array_map(
-                static function ($value) {
-                    if (is_resource($value)) {
-                        $value = stream_get_contents($value);
-                    }
-
-                    return $value;
-                },
-                $record
-            );
-        }
-
-        return $record;
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0)
-    {
-        try {
-            $record = parent::fetch($fetchMode, $cursorOrientation, $cursorOffset);
-            $record = $this->mapResourceToString($record);
-            return $record;
-        } catch (\PDOException $exception) {
-            throw new PDOException($exception);
-        }
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function fetchOne($columnIndex = 0)
-    {
-        try {
-            $record = parent::fetchColumn($columnIndex);
-            $record = $this->mapResourceToString($record);
-            return $record;
-        } catch (\PDOException $exception) {
-            throw new PDOException($exception);
-        }
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function fetchColumn($columnIndex = 0)
-    {
-        return $this->fetchOne($columnIndex);
-    }
-}
diff --git a/typo3/sysext/core/Classes/Database/Driver/PDOStatementImplementation.php b/typo3/sysext/core/Classes/Database/Driver/PDOStatementImplementation.php
deleted file mode 100644
index 2bda74a5f9bf47145d1f12d9bf56592403dc5928..0000000000000000000000000000000000000000
--- a/typo3/sysext/core/Classes/Database/Driver/PDOStatementImplementation.php
+++ /dev/null
@@ -1,64 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-namespace TYPO3\CMS\Core\Database\Driver;
-
-use Doctrine\DBAL\Driver\PDO\Exception as PDOException;
-
-if (PHP_VERSION_ID >= 80000) {
-    trait PDOStatementImplementation
-    {
-        /**
-         * {@inheritdoc}
-         */
-        public function fetchAll($mode = null, ...$args)
-        {
-            try {
-                $records = parent::fetchAll($mode, ...$args);
-
-                if ($records !== false) {
-                    $records = array_map([$this, 'mapResourceToString'], $records);
-                }
-
-                return $records;
-            } catch (\PDOException $exception) {
-                throw new PDOException($exception);
-            }
-        }
-    }
-} else {
-    trait PDOStatementImplementation
-    {
-        /**
-         * {@inheritdoc}
-         */
-        public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null)
-        {
-            try {
-                $records = parent::fetchAll($fetchMode, $fetchArgument, $ctorArgs);
-
-                if ($records !== false) {
-                    $records = array_map([$this, 'mapResourceToString'], $records);
-                }
-
-                return $records;
-            } catch (\PDOException $exception) {
-                throw new PDOException($exception);
-            }
-        }
-    }
-}
diff --git a/typo3/sysext/core/Classes/Database/Platform/PlatformInformation.php b/typo3/sysext/core/Classes/Database/Platform/PlatformInformation.php
index 736bcb3e83bb1ce138f8a7194e25684724cd989b..b59744874235a565446340640cf9e1647a612133 100644
--- a/typo3/sysext/core/Classes/Database/Platform/PlatformInformation.php
+++ b/typo3/sysext/core/Classes/Database/Platform/PlatformInformation.php
@@ -19,8 +19,8 @@ namespace TYPO3\CMS\Core\Database\Platform;
 
 use Doctrine\DBAL\Exception as DBALException;
 use Doctrine\DBAL\Platforms\AbstractPlatform;
-use Doctrine\DBAL\Platforms\MySqlPlatform;
-use Doctrine\DBAL\Platforms\PostgreSQL94Platform as PostgreSqlPlatform;
+use Doctrine\DBAL\Platforms\MySQLPlatform;
+use Doctrine\DBAL\Platforms\PostgreSQL94Platform as PostgreSQLPlatform;
 use Doctrine\DBAL\Platforms\SqlitePlatform;
 use Doctrine\DBAL\Platforms\SQLServer2012Platform as SQLServerPlatform;
 
@@ -143,7 +143,7 @@ class PlatformInformation
      */
     protected static function getPlatformIdentifier(AbstractPlatform $platform): string
     {
-        if ($platform instanceof MySqlPlatform) {
+        if ($platform instanceof MySQLPlatform) {
             return 'mysql';
         }
         if ($platform instanceof PostgreSqlPlatform) {
diff --git a/typo3/sysext/core/Classes/Database/Query/QueryBuilder.php b/typo3/sysext/core/Classes/Database/Query/QueryBuilder.php
index 26e12868d6c320d9528be6c224cbaa9b49bc5b13..7d620c7440c8abdd0b40357960abb99716b1e7f2 100644
--- a/typo3/sysext/core/Classes/Database/Query/QueryBuilder.php
+++ b/typo3/sysext/core/Classes/Database/Query/QueryBuilder.php
@@ -17,10 +17,9 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Core\Database\Query;
 
-use Doctrine\DBAL\DBALException;
-use Doctrine\DBAL\Platforms\MySqlPlatform;
+use Doctrine\DBAL\Platforms\MySQLPlatform;
 use Doctrine\DBAL\Platforms\OraclePlatform;
-use Doctrine\DBAL\Platforms\PostgreSQL94Platform as PostgreSqlPlatform;
+use Doctrine\DBAL\Platforms\PostgreSQL94Platform as PostgreSQLPlatform;
 use Doctrine\DBAL\Platforms\SqlitePlatform;
 use Doctrine\DBAL\Platforms\SQLServer2012Platform as SQLServerPlatform;
 use Doctrine\DBAL\Query\Expression\CompositeExpression;
@@ -217,8 +216,7 @@ class QueryBuilder
      * executeStatement() for 'INSERT', 'UPDATE' and 'DELETE' queries.
      *
      * @return Result|int
-     * @throws DBALException
-     * @todo Deprecate in v12 along with raise to min doctrine/dbal:^3.2 to align with doctrine/dbal deprecation.
+     * @internal use executeQuery() and executeStatement() instead. Only here for backwards-compatibility
      */
     public function execute()
     {
@@ -240,22 +238,20 @@ class QueryBuilder
      * versions and avoid deprecation warning. Additional this will ease
      * backport without the need to switch if execute() is not used anymore.
      *
-     * @throws DBALException
+     * @return Result
      */
     public function executeQuery(): Result
     {
         // Set additional query restrictions
         $originalWhereConditions = $this->addAdditionalWhereConditions();
 
-        // @todo Call $this->concreteQueryBuilder->executeQuery()
-        //        directly with doctrine/dbal:^3.2 raise in v12.
-        $return = $this->concreteQueryBuilder->execute();
+        $result = $this->concreteQueryBuilder->executeQuery();
 
         // Restore the original query conditions in case the user keeps
         // on modifying the state.
         $this->concreteQueryBuilder->add('where', $originalWhereConditions, false);
 
-        return $return;
+        return $result;
     }
 
     /**
@@ -271,14 +267,10 @@ class QueryBuilder
      * backport without the need to switch if execute() is not used anymore.
      *
      * @return int The number of affected rows.
-     *
-     * @throws DBALException
      */
     public function executeStatement(): int
     {
-        // @todo Call $this->concreteQueryBuilder->executeStatement()
-        //        directly with doctrine/dbal:^3.2 raise in v12.
-        return $this->concreteQueryBuilder->execute();
+        return $this->concreteQueryBuilder->executeStatement();
     }
 
     /**
@@ -1182,7 +1174,7 @@ class QueryBuilder
     {
         $databasePlatform = $this->connection->getDatabasePlatform();
         // https://dev.mysql.com/doc/refman/5.7/en/cast-functions.html#function_convert
-        if ($databasePlatform instanceof MySqlPlatform) {
+        if ($databasePlatform instanceof MySQLPlatform) {
             return sprintf('CONVERT(%s, CHAR)', $this->connection->quoteIdentifier($fieldName));
         }
         // https://www.postgresql.org/docs/current/sql-createcast.html
diff --git a/typo3/sysext/core/Classes/Database/Schema/Comparator.php b/typo3/sysext/core/Classes/Database/Schema/Comparator.php
index 60a4f0de1c8377fe5c55e4d72ae2b3a0b612c01f..8807439647e9b19c41b39bcb37a04a34b7748e0c 100644
--- a/typo3/sysext/core/Classes/Database/Schema/Comparator.php
+++ b/typo3/sysext/core/Classes/Database/Schema/Comparator.php
@@ -18,13 +18,17 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Core\Database\Schema;
 
 use Doctrine\DBAL\Platforms\AbstractPlatform;
-use Doctrine\DBAL\Platforms\MySqlPlatform;
+use Doctrine\DBAL\Platforms\MySQLPlatform;
 use Doctrine\DBAL\Schema\Column;
+use Doctrine\DBAL\Schema\ForeignKeyConstraint;
+use Doctrine\DBAL\Schema\Schema;
+use Doctrine\DBAL\Schema\SchemaDiff;
+use Doctrine\DBAL\Schema\SchemaException;
+use Doctrine\DBAL\Schema\Sequence;
 use Doctrine\DBAL\Schema\Table;
 use Doctrine\DBAL\Types\BlobType;
 use Doctrine\DBAL\Types\TextType;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
  * Compares two Schemas and returns an instance of SchemaDiff.
@@ -46,6 +50,7 @@ class Comparator extends \Doctrine\DBAL\Schema\Comparator
     public function __construct(AbstractPlatform $platform = null)
     {
         $this->databasePlatform = $platform;
+        parent::__construct($platform);
     }
 
     /**
@@ -70,14 +75,13 @@ class Comparator extends \Doctrine\DBAL\Schema\Comparator
         }
 
         if ($tableDifferences === false) {
-            $tableDifferences = GeneralUtility::makeInstance(TableDiff::class, $fromTable->getName());
+            $tableDifferences = new TableDiff($fromTable->getName());
             $tableDifferences->fromTable = $fromTable;
         } else {
             $renamedColumns = $tableDifferences->renamedColumns;
             $renamedIndexes = $tableDifferences->renamedIndexes;
             // Rebuild TableDiff with enhanced TYPO3 TableDiff class
-            $tableDifferences = GeneralUtility::makeInstance(
-                TableDiff::class,
+            $tableDifferences = new TableDiff(
                 $tableDifferences->name,
                 $tableDifferences->addedColumns,
                 $tableDifferences->changedColumns,
@@ -111,7 +115,7 @@ class Comparator extends \Doctrine\DBAL\Schema\Comparator
         $changedProperties = parent::diffColumn($column1, $column2);
 
         // Only MySQL has variable length versions of TEXT/BLOB
-        if (!$this->databasePlatform instanceof MySqlPlatform) {
+        if (!$this->databasePlatform instanceof MySQLPlatform) {
             return $changedProperties;
         }
 
@@ -130,4 +134,162 @@ class Comparator extends \Doctrine\DBAL\Schema\Comparator
 
         return array_unique($changedProperties);
     }
+
+    /**
+     * Returns a SchemaDiff object containing the differences between the schemas
+     * $fromSchema and $toSchema.
+     *
+     * This method should be called non-statically since it will be declared as
+     * non-static in the next doctrine/dbal major release.
+     *
+     * doctrine/dbal uses new self() in this method, which instantiate the doctrine
+     * Comparator class and not our extended class, thus not calling our overridden
+     * methods 'diffTable()' and 'diffColum()' and breaking core table engine support,
+     * which is tested in core functional tests explicity. See corresponding test
+     * `TYPO3\CMS\Core\Tests\Functional\Database\Schema\SchemaMigratorTest->changeTableEngine()`
+     *
+     * @return SchemaDiff
+     * @throws SchemaException
+     *
+     * @todo Create PR for doctrine/dbal to change to late binding 'new static()', so our
+     *       override is working correctly and remove this method after min package raise,
+     *       if PR was accepted and merged. Also remove 'isAutoIncrementSequenceInSchema()'.
+     */
+    public static function compareSchemas(
+        Schema $fromSchema,
+        Schema $toSchema
+    ) {
+        $comparator       = new self();
+        $diff             = new SchemaDiff();
+        $diff->fromSchema = $fromSchema;
+
+        $foreignKeysToTable = [];
+
+        foreach ($toSchema->getNamespaces() as $namespace) {
+            if ($fromSchema->hasNamespace($namespace)) {
+                continue;
+            }
+
+            $diff->newNamespaces[$namespace] = $namespace;
+        }
+
+        foreach ($fromSchema->getNamespaces() as $namespace) {
+            if ($toSchema->hasNamespace($namespace)) {
+                continue;
+            }
+
+            $diff->removedNamespaces[$namespace] = $namespace;
+        }
+
+        foreach ($toSchema->getTables() as $table) {
+            $tableName = $table->getShortestName($toSchema->getName());
+            if (! $fromSchema->hasTable($tableName)) {
+                $diff->newTables[$tableName] = $toSchema->getTable($tableName);
+            } else {
+                $tableDifferences = $comparator->diffTable(
+                    $fromSchema->getTable($tableName),
+                    $toSchema->getTable($tableName)
+                );
+
+                if ($tableDifferences !== false) {
+                    $diff->changedTables[$tableName] = $tableDifferences;
+                }
+            }
+        }
+
+        /* Check if there are tables removed */
+        foreach ($fromSchema->getTables() as $table) {
+            $tableName = $table->getShortestName($fromSchema->getName());
+
+            $table = $fromSchema->getTable($tableName);
+            if (! $toSchema->hasTable($tableName)) {
+                $diff->removedTables[$tableName] = $table;
+            }
+
+            // also remember all foreign keys that point to a specific table
+            foreach ($table->getForeignKeys() as $foreignKey) {
+                $foreignTable = strtolower($foreignKey->getForeignTableName());
+                if (! isset($foreignKeysToTable[$foreignTable])) {
+                    $foreignKeysToTable[$foreignTable] = [];
+                }
+
+                $foreignKeysToTable[$foreignTable][] = $foreignKey;
+            }
+        }
+
+        foreach ($diff->removedTables as $tableName => $table) {
+            if (! isset($foreignKeysToTable[$tableName])) {
+                continue;
+            }
+
+            $diff->orphanedForeignKeys = array_merge($diff->orphanedForeignKeys, $foreignKeysToTable[$tableName]);
+
+            // deleting duplicated foreign keys present on both on the orphanedForeignKey
+            // and the removedForeignKeys from changedTables
+            foreach ($foreignKeysToTable[$tableName] as $foreignKey) {
+                // strtolower the table name to make if compatible with getShortestName
+                $localTableName = strtolower($foreignKey->getLocalTableName());
+                if (! isset($diff->changedTables[$localTableName])) {
+                    continue;
+                }
+
+                foreach ($diff->changedTables[$localTableName]->removedForeignKeys as $key => $removedForeignKey) {
+                    assert($removedForeignKey instanceof ForeignKeyConstraint);
+
+                    // We check if the key is from the removed table if not we skip.
+                    if ($tableName !== strtolower($removedForeignKey->getForeignTableName())) {
+                        continue;
+                    }
+
+                    unset($diff->changedTables[$localTableName]->removedForeignKeys[$key]);
+                }
+            }
+        }
+
+        foreach ($toSchema->getSequences() as $sequence) {
+            $sequenceName = $sequence->getShortestName($toSchema->getName());
+            if (! $fromSchema->hasSequence($sequenceName)) {
+                if (! $comparator->isAutoIncrementSequenceInSchema($fromSchema, $sequence)) {
+                    $diff->newSequences[] = $sequence;
+                }
+            } else {
+                if ($comparator->diffSequence($sequence, $fromSchema->getSequence($sequenceName))) {
+                    $diff->changedSequences[] = $toSchema->getSequence($sequenceName);
+                }
+            }
+        }
+
+        foreach ($fromSchema->getSequences() as $sequence) {
+            if ($comparator->isAutoIncrementSequenceInSchema($toSchema, $sequence)) {
+                continue;
+            }
+
+            $sequenceName = $sequence->getShortestName($fromSchema->getName());
+
+            if ($toSchema->hasSequence($sequenceName)) {
+                continue;
+            }
+
+            $diff->removedSequences[] = $sequence;
+        }
+
+        return $diff;
+    }
+
+    /**
+     * @param Schema   $schema
+     * @param Sequence $sequence
+     * @todo Remove this method, when 'compareSchemas()' could removed. We needed to borrow
+     *       this method along with 'compareSchemas()' through missing late-static binding.
+     */
+    private function isAutoIncrementSequenceInSchema($schema, $sequence): bool
+    {
+        foreach ($schema->getTables() as $table) {
+            if ($sequence->isAutoIncrementsFor($table)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
 }
diff --git a/typo3/sysext/core/Classes/Database/Schema/ConnectionMigrator.php b/typo3/sysext/core/Classes/Database/Schema/ConnectionMigrator.php
index d885b8d766551c605ca0ec1bc50ad6db3d47afbc..20ba6f5b9b2ddb6c9b3c98f96ded4f7bbe5e6daa 100644
--- a/typo3/sysext/core/Classes/Database/Schema/ConnectionMigrator.php
+++ b/typo3/sysext/core/Classes/Database/Schema/ConnectionMigrator.php
@@ -18,8 +18,8 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Core\Database\Schema;
 
 use Doctrine\DBAL\Exception as DBALException;
-use Doctrine\DBAL\Platforms\MySqlPlatform;
-use Doctrine\DBAL\Platforms\PostgreSQL94Platform as PostgreSqlPlatform;
+use Doctrine\DBAL\Platforms\MySQLPlatform;
+use Doctrine\DBAL\Platforms\PostgreSQL94Platform as PostgreSQLPlatform;
 use Doctrine\DBAL\Platforms\SqlitePlatform;
 use Doctrine\DBAL\Platforms\SQLServer2012Platform as SQLServerPlatform;
 use Doctrine\DBAL\Schema\Column;
@@ -150,11 +150,10 @@ class ConnectionMigrator
             // existing columns as false positives for a column rename. In this
             // context every rename is actually a new column.
             foreach ($changedTable->renamedColumns as $columnName => $renamedColumn) {
-                $changedTable->addedColumns[$renamedColumn->getName()] = GeneralUtility::makeInstance(
-                    Column::class,
+                $changedTable->addedColumns[$renamedColumn->getName()] = new Column(
                     $renamedColumn->getName(),
                     $renamedColumn->getType(),
-                    array_diff_key($renamedColumn->toArray(), ['name', 'type'])
+                    $this->prepareColumnOptions($renamedColumn)
                 );
                 unset($changedTable->renamedColumns[$columnName]);
             }
@@ -232,7 +231,7 @@ class ConnectionMigrator
 
         // Build SchemaDiff and handle renames of tables and columns
         $comparator = GeneralUtility::makeInstance(Comparator::class, $this->connection->getDatabasePlatform());
-        $schemaDiff = $comparator->compare($fromSchema, $toSchema);
+        $schemaDiff = $comparator->compareSchemas($fromSchema, $toSchema);
         $schemaDiff = $this->migrateColumnRenamesToDistinctActions($schemaDiff);
 
         if ($renameUnused) {
@@ -248,7 +247,7 @@ class ConnectionMigrator
         // If there are no mapped tables return a SchemaDiff without any changes
         // to avoid update suggestions for tables not related to TYPO3.
         if (empty($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'] ?? null)) {
-            return GeneralUtility::makeInstance(SchemaDiff::class, [], [], [], $fromSchema);
+            return new SchemaDiff([], [], [], $fromSchema);
         }
 
         // Collect the table names that have been mapped to this connection.
@@ -304,21 +303,21 @@ class ConnectionMigrator
                 $tableName,
                 array_merge($currentTableDefinition->getColumns(), $table->getColumns()),
                 array_merge($currentTableDefinition->getIndexes(), $table->getIndexes()),
+                [],
                 array_merge($currentTableDefinition->getForeignKeys(), $table->getForeignKeys()),
-                0,
                 array_merge($currentTableDefinition->getOptions(), $table->getOptions())
             );
         }
 
         $tablesForConnection = $this->transformTablesForDatabasePlatform($tablesForConnection, $this->connection);
 
-        $schemaConfig = GeneralUtility::makeInstance(SchemaConfig::class);
+        $schemaConfig = new SchemaConfig();
         $schemaConfig->setName($this->connection->getDatabase());
         if (isset($this->connection->getParams()['tableoptions'])) {
             $schemaConfig->setDefaultTableOptions($this->connection->getParams()['tableoptions']);
         }
 
-        return GeneralUtility::makeInstance(Schema::class, $tablesForConnection, [], $schemaConfig);
+        return new Schema($tablesForConnection, [], $schemaConfig);
     }
 
     /**
@@ -332,8 +331,7 @@ class ConnectionMigrator
     protected function getNewTableUpdateSuggestions(SchemaDiff $schemaDiff): array
     {
         // Build a new schema diff that only contains added tables
-        $addTableSchemaDiff = GeneralUtility::makeInstance(
-            SchemaDiff::class,
+        $addTableSchemaDiff = new SchemaDiff(
             $schemaDiff->newTables,
             [],
             [],
@@ -365,8 +363,7 @@ class ConnectionMigrator
                 // Treat each added column with a new diff to get a dedicated suggestions
                 // just for this single column.
                 foreach ($changedTable->addedColumns as $columnName => $addedColumn) {
-                    $changedTables[$index . ':tbl_' . $addedColumn->getName()] = GeneralUtility::makeInstance(
-                        TableDiff::class,
+                    $changedTables[$index . ':tbl_' . $addedColumn->getName()] = new TableDiff(
                         $changedTable->name,
                         [$columnName => $addedColumn],
                         [],
@@ -383,8 +380,7 @@ class ConnectionMigrator
                 // Treat each added index with a new diff to get a dedicated suggestions
                 // just for this index.
                 foreach ($changedTable->addedIndexes as $indexName => $addedIndex) {
-                    $changedTables[$index . ':idx_' . $addedIndex->getName()] = GeneralUtility::makeInstance(
-                        TableDiff::class,
+                    $changedTables[$index . ':idx_' . $addedIndex->getName()] = new TableDiff(
                         $changedTable->name,
                         [],
                         [],
@@ -402,8 +398,7 @@ class ConnectionMigrator
                 // just for this foreign key.
                 foreach ($changedTable->addedForeignKeys as $addedForeignKey) {
                     $fkIndex = $index . ':fk_' . $addedForeignKey->getName();
-                    $changedTables[$fkIndex] = GeneralUtility::makeInstance(
-                        TableDiff::class,
+                    $changedTables[$fkIndex] = new TableDiff(
                         $changedTable->name,
                         [],
                         [],
@@ -419,8 +414,7 @@ class ConnectionMigrator
         }
 
         // Build a new schema diff that only contains added fields
-        $addFieldSchemaDiff = GeneralUtility::makeInstance(
-            SchemaDiff::class,
+        $addFieldSchemaDiff = new SchemaDiff(
             [],
             $changedTables,
             [],
@@ -464,8 +458,7 @@ class ConnectionMigrator
             );
             $tableOptionsDiff->setTableOptions($tableOptions);
 
-            $tableOptionsSchemaDiff = GeneralUtility::makeInstance(
-                SchemaDiff::class,
+            $tableOptionsSchemaDiff = new SchemaDiff(
                 [],
                 [$tableOptionsDiff],
                 [],
@@ -500,8 +493,7 @@ class ConnectionMigrator
             // just for this index.
             if (count($changedTable->changedIndexes) !== 0) {
                 foreach ($changedTable->changedIndexes as $indexName => $changedIndex) {
-                    $indexDiff = GeneralUtility::makeInstance(
-                        TableDiff::class,
+                    $indexDiff = new TableDiff(
                         $changedTable->name,
                         [],
                         [],
@@ -512,8 +504,7 @@ class ConnectionMigrator
                         $schemaDiff->fromSchema->getTable($changedTable->name)
                     );
 
-                    $temporarySchemaDiff = GeneralUtility::makeInstance(
-                        SchemaDiff::class,
+                    $temporarySchemaDiff = new SchemaDiff(
                         [],
                         [$indexDiff],
                         [],
@@ -531,8 +522,7 @@ class ConnectionMigrator
             if (count($changedTable->renamedIndexes) !== 0) {
                 // Create a base table diff without any changes, there's no constructor
                 // argument to pass in renamed indexes.
-                $tableDiff = GeneralUtility::makeInstance(
-                    TableDiff::class,
+                $tableDiff = new TableDiff(
                     $changedTable->name,
                     [],
                     [],
@@ -549,8 +539,7 @@ class ConnectionMigrator
                     $indexDiff = clone $tableDiff;
                     $indexDiff->renamedIndexes = [$key => $renamedIndex];
 
-                    $temporarySchemaDiff = GeneralUtility::makeInstance(
-                        SchemaDiff::class,
+                    $temporarySchemaDiff = new SchemaDiff(
                         [],
                         [$indexDiff],
                         [],
@@ -587,8 +576,7 @@ class ConnectionMigrator
                     );
 
                     // Build a dedicated diff just for the current column
-                    $tableDiff = GeneralUtility::makeInstance(
-                        TableDiff::class,
+                    $tableDiff = new TableDiff(
                         $changedTable->name,
                         [],
                         [$columnName => $changedColumn],
@@ -599,8 +587,7 @@ class ConnectionMigrator
                         $fromTable
                     );
 
-                    $temporarySchemaDiff = GeneralUtility::makeInstance(
-                        SchemaDiff::class,
+                    $temporarySchemaDiff = new SchemaDiff(
                         [],
                         [$tableDiff],
                         [],
@@ -618,8 +605,7 @@ class ConnectionMigrator
             // Treat each changed foreign key with a new diff to get a dedicated suggestions
             // just for this foreign key.
             if (count($changedTable->changedForeignKeys) !== 0) {
-                $tableDiff = GeneralUtility::makeInstance(
-                    TableDiff::class,
+                $tableDiff = new TableDiff(
                     $changedTable->name,
                     [],
                     [],
@@ -634,8 +620,7 @@ class ConnectionMigrator
                     $foreignKeyDiff = clone $tableDiff;
                     $foreignKeyDiff->changedForeignKeys = [$this->buildQuotedForeignKey($changedForeignKey)];
 
-                    $temporarySchemaDiff = GeneralUtility::makeInstance(
-                        SchemaDiff::class,
+                    $temporarySchemaDiff = new SchemaDiff(
                         [],
                         [$foreignKeyDiff],
                         [],
@@ -676,8 +661,7 @@ class ConnectionMigrator
                 continue;
             }
             // Build a new schema diff that only contains this table
-            $changedFieldDiff = GeneralUtility::makeInstance(
-                SchemaDiff::class,
+            $changedFieldDiff = new SchemaDiff(
                 [],
                 [$tableDiff],
                 [],
@@ -725,8 +709,7 @@ class ConnectionMigrator
                     continue;
                 }
 
-                $renameColumnTableDiff = GeneralUtility::makeInstance(
-                    TableDiff::class,
+                $renameColumnTableDiff = new TableDiff(
                     $changedTable->name,
                     [],
                     [$oldFieldName => $changedColumn],
@@ -748,8 +731,7 @@ class ConnectionMigrator
         }
 
         // Build a new schema diff that only contains unused fields
-        $changedFieldDiff = GeneralUtility::makeInstance(
-            SchemaDiff::class,
+        $changedFieldDiff = new SchemaDiff(
             [],
             $changedTables,
             [],
@@ -786,8 +768,7 @@ class ConnectionMigrator
                 // Treat each changed column with a new diff to get a dedicated suggestions
                 // just for this single column.
                 foreach ($changedTable->removedColumns as $columnName => $removedColumn) {
-                    $changedTables[$index . ':tbl_' . $removedColumn->getName()] = GeneralUtility::makeInstance(
-                        TableDiff::class,
+                    $changedTables[$index . ':tbl_' . $removedColumn->getName()] = new TableDiff(
                         $changedTable->name,
                         [],
                         [],
@@ -808,8 +789,7 @@ class ConnectionMigrator
                 // Treat each removed index with a new diff to get a dedicated suggestions
                 // just for this index.
                 foreach ($changedTable->removedIndexes as $indexName => $removedIndex) {
-                    $changedTables[$index . ':idx_' . $removedIndex->getName()] = GeneralUtility::makeInstance(
-                        TableDiff::class,
+                    $changedTables[$index . ':idx_' . $removedIndex->getName()] = new TableDiff(
                         $changedTable->name,
                         [],
                         [],
@@ -834,8 +814,7 @@ class ConnectionMigrator
                         continue;
                     }
                     $fkIndex = $index . ':fk_' . $removedForeignKey->getName();
-                    $changedTables[$fkIndex] = GeneralUtility::makeInstance(
-                        TableDiff::class,
+                    $changedTables[$fkIndex] = new TableDiff(
                         $changedTable->name,
                         [],
                         [],
@@ -854,8 +833,7 @@ class ConnectionMigrator
         }
 
         // Build a new schema diff that only contains removable fields
-        $removedFieldDiff = GeneralUtility::makeInstance(
-            SchemaDiff::class,
+        $removedFieldDiff = new SchemaDiff(
             [],
             $changedTables,
             [],
@@ -883,8 +861,7 @@ class ConnectionMigrator
         $updateSuggestions = [];
         foreach ($schemaDiff->removedTables as $removedTable) {
             // Build a new schema diff that only contains this table
-            $tableDiff = GeneralUtility::makeInstance(
-                SchemaDiff::class,
+            $tableDiff = new SchemaDiff(
                 [],
                 [],
                 [$this->buildQuotedTable($removedTable)],
@@ -923,8 +900,7 @@ class ConnectionMigrator
             if (strpos($removedTable->getName(), $this->deletedPrefix) === 0) {
                 continue;
             }
-            $tableDiff = GeneralUtility::makeInstance(
-                TableDiff::class,
+            $tableDiff = new TableDiff(
                 $removedTable->getQuotedName($this->connection->getDatabasePlatform()),
                 [], // added columns
                 [], // changed columns
@@ -979,12 +955,11 @@ class ConnectionMigrator
                 $renamedColumn = new Column(
                     $this->connection->quoteIdentifier($renamedColumnName),
                     $removedColumn->getType(),
-                    array_diff_key($removedColumn->toArray(), ['name', 'type'])
+                    $this->prepareColumnOptions($removedColumn)
                 );
 
                 // Build the diff object for the column to rename
-                $columnDiff = GeneralUtility::makeInstance(
-                    ColumnDiff::class,
+                $columnDiff = new ColumnDiff(
                     $removedColumn->getQuotedName($this->connection->getDatabasePlatform()),
                     $renamedColumn,
                     [], // changed properties
@@ -1021,16 +996,14 @@ class ConnectionMigrator
             // Treat each renamed column with a new diff to get a dedicated
             // suggestion just for this single column.
             foreach ($changedTable->renamedColumns as $originalColumnName => $renamedColumn) {
-                $columnOptions = array_diff_key($renamedColumn->toArray(), ['name', 'type']);
+                $columnOptions = $this->prepareColumnOptions($renamedColumn);
 
-                $changedTable->addedColumns[$renamedColumn->getName()] = GeneralUtility::makeInstance(
-                    Column::class,
+                $changedTable->addedColumns[$renamedColumn->getName()] = new Column(
                     $renamedColumn->getName(),
                     $renamedColumn->getType(),
                     $columnOptions
                 );
-                $changedTable->removedColumns[$originalColumnName] = GeneralUtility::makeInstance(
-                    Column::class,
+                $changedTable->removedColumns[$originalColumnName] = new Column(
                     $originalColumnName,
                     $renamedColumn->getType(),
                     $columnOptions
@@ -1146,7 +1119,7 @@ class ConnectionMigrator
                 // Remove the length information from column names for indexes if required.
                 $cleanedColumnNames = array_map(
                     static function (string $columnName) use ($connection) {
-                        if ($connection->getDatabasePlatform() instanceof MySqlPlatform) {
+                        if ($connection->getDatabasePlatform() instanceof MySQLPlatform) {
                             // Returning the unquoted, unmodified version of the column name since
                             // it can include the length information for BLOB/TEXT columns which
                             // may not be quoted.
@@ -1158,8 +1131,7 @@ class ConnectionMigrator
                     $index->getUnquotedColumns()
                 );
 
-                $indexes[$key] = GeneralUtility::makeInstance(
-                    Index::class,
+                $indexes[$key] = new Index(
                     $connection->quoteIdentifier($indexName),
                     $cleanedColumnNames,
                     $index->isUnique(),
@@ -1173,8 +1145,8 @@ class ConnectionMigrator
                 $table->getQuotedName($connection->getDatabasePlatform()),
                 $table->getColumns(),
                 $indexes,
+                [],
                 $table->getForeignKeys(),
-                0,
                 array_merge($defaultTableOptions, $table->getOptions())
             );
         }
@@ -1192,7 +1164,7 @@ class ConnectionMigrator
     protected function getTableOptions(array $tableNames): array
     {
         $tableOptions = [];
-        if (strpos($this->connection->getServerVersion(), 'MySQL') !== 0) {
+        if (!$this->connection->getDatabasePlatform() instanceof MySQLPlatform) {
             foreach ($tableNames as $tableName) {
                 $tableOptions[$tableName] = [];
             }
@@ -1258,8 +1230,8 @@ class ConnectionMigrator
             $databasePlatform->quoteIdentifier($table->getName()),
             $table->getColumns(),
             $table->getIndexes(),
+            [],
             $table->getForeignKeys(),
-            0,
             $table->getOptions()
         );
     }
@@ -1277,11 +1249,10 @@ class ConnectionMigrator
     {
         $databasePlatform = $this->connection->getDatabasePlatform();
 
-        return GeneralUtility::makeInstance(
-            Column::class,
+        return new Column(
             $databasePlatform->quoteIdentifier($column->getName()),
             $column->getType(),
-            array_diff_key($column->toArray(), ['name', 'type'])
+            $this->prepareColumnOptions($column)
         );
     }
 
@@ -1298,8 +1269,7 @@ class ConnectionMigrator
     {
         $databasePlatform = $this->connection->getDatabasePlatform();
 
-        return GeneralUtility::makeInstance(
-            Index::class,
+        return new Index(
             $databasePlatform->quoteIdentifier($index->getName()),
             $index->getColumns(),
             $index->isUnique(),
@@ -1322,8 +1292,7 @@ class ConnectionMigrator
     {
         $databasePlatform = $this->connection->getDatabasePlatform();
 
-        return GeneralUtility::makeInstance(
-            ForeignKeyConstraint::class,
+        return new ForeignKeyConstraint(
             $index->getLocalColumns(),
             $databasePlatform->quoteIdentifier($index->getForeignTableName()),
             $index->getForeignColumns(),
@@ -1332,6 +1301,29 @@ class ConnectionMigrator
         );
     }
 
+    protected function prepareColumnOptions(Column $column): array
+    {
+        $options = $column->toArray();
+        $platformOptions = $column->getPlatformOptions();
+        foreach ($platformOptions as $optionName => $optionValue) {
+            unset($options[$optionName]);
+            if (!isset($options['platformOptions'])) {
+                $options['platformOptions'] = [];
+            }
+            $options['platformOptions'][$optionName] = $optionValue;
+        }
+        $schemaOptions = $column->getCustomSchemaOptions();
+        foreach ($schemaOptions as $optionName => $optionValue) {
+            unset($options[$optionName]);
+            if (!isset($options['schemaOptions'])) {
+                $options['schemaOptions'] = [];
+            }
+            $options['schemaOptions'][$optionName] = $optionValue;
+        }
+        unset($options['name'], $options['type']);
+        return $options;
+    }
+
     protected function getDatabasePlatform(string $tableName): string
     {
         $databasePlatform = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($tableName)->getDatabasePlatform();
diff --git a/typo3/sysext/core/Classes/Database/Schema/EventListener/SchemaAlterTableListener.php b/typo3/sysext/core/Classes/Database/Schema/EventListener/SchemaAlterTableListener.php
index f33935ac6cc66fb592213ecf7eb400a29b0a3b28..30318397072df799605a98c82f8a83c4fe017acf 100644
--- a/typo3/sysext/core/Classes/Database/Schema/EventListener/SchemaAlterTableListener.php
+++ b/typo3/sysext/core/Classes/Database/Schema/EventListener/SchemaAlterTableListener.php
@@ -18,7 +18,7 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Core\Database\Schema\EventListener;
 
 use Doctrine\DBAL\Event\SchemaAlterTableEventArgs;
-use Doctrine\DBAL\Platforms\MySqlPlatform;
+use Doctrine\DBAL\Platforms\MySQLPlatform;
 use TYPO3\CMS\Core\Database\Schema\TableDiff;
 
 /**
@@ -46,7 +46,7 @@ class SchemaAlterTableListener
         }
 
         // Table options are only supported on MySQL, continue default processing
-        if (!$event->getPlatform() instanceof MySqlPlatform) {
+        if (!$event->getPlatform() instanceof MySQLPlatform) {
             return false;
         }
 
diff --git a/typo3/sysext/core/Classes/Database/Schema/EventListener/SchemaIndexDefinitionListener.php b/typo3/sysext/core/Classes/Database/Schema/EventListener/SchemaIndexDefinitionListener.php
index 63930931bf657199dc310863967af0f1193a434d..a318faf4ea53980744a641f66590d3730ebda493 100644
--- a/typo3/sysext/core/Classes/Database/Schema/EventListener/SchemaIndexDefinitionListener.php
+++ b/typo3/sysext/core/Classes/Database/Schema/EventListener/SchemaIndexDefinitionListener.php
@@ -18,7 +18,7 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Core\Database\Schema\EventListener;
 
 use Doctrine\DBAL\Event\SchemaIndexDefinitionEventArgs;
-use Doctrine\DBAL\Platforms\MySqlPlatform;
+use Doctrine\DBAL\Platforms\MySQLPlatform;
 use Doctrine\DBAL\Schema\Index;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 
@@ -40,7 +40,7 @@ class SchemaIndexDefinitionListener
     public function onSchemaIndexDefinition(SchemaIndexDefinitionEventArgs $event)
     {
         // Early  return for non-MySQL-compatible platforms
-        if (!($event->getConnection()->getDatabasePlatform() instanceof MySqlPlatform)) {
+        if (!($event->getConnection()->getDatabasePlatform() instanceof MySQLPlatform)) {
             return;
         }
 
diff --git a/typo3/sysext/core/Classes/Database/Schema/Parser/TableBuilder.php b/typo3/sysext/core/Classes/Database/Schema/Parser/TableBuilder.php
index df70dbd15fd893c604c1e23f1959ed01052dd562..b95755af4a8c1b4424348d73678dbfab822e0bf6 100644
--- a/typo3/sysext/core/Classes/Database/Schema/Parser/TableBuilder.php
+++ b/typo3/sysext/core/Classes/Database/Schema/Parser/TableBuilder.php
@@ -18,7 +18,7 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Core\Database\Schema\Parser;
 
 use Doctrine\DBAL\Platforms\AbstractPlatform;
-use Doctrine\DBAL\Platforms\MySqlPlatform;
+use Doctrine\DBAL\Platforms\MySQLPlatform;
 use Doctrine\DBAL\Schema\Column;
 use Doctrine\DBAL\Schema\Index;
 use Doctrine\DBAL\Schema\Table;
@@ -101,7 +101,7 @@ class TableBuilder
                 Type::addType($type, $className);
             }
         }
-        $this->platform = $platform ?: GeneralUtility::makeInstance(MySqlPlatform::class);
+        $this->platform = $platform ?: GeneralUtility::makeInstance(MySQLPlatform::class);
     }
 
     /**
@@ -121,7 +121,7 @@ class TableBuilder
             [],
             [],
             [],
-            0,
+            [],
             $this->buildTableOptions($tableStatement->tableOptions)
         );
 
@@ -260,8 +260,8 @@ class TableBuilder
                 $this->table->getQuotedName($this->platform),
                 $this->table->getColumns(),
                 array_merge($this->table->getIndexes(), [strtolower($indexName) => $index]),
+                [],
                 $this->table->getForeignKeys(),
-                0,
                 $this->table->getOptions()
             );
         }
diff --git a/typo3/sysext/core/Classes/Database/Schema/SchemaMigrator.php b/typo3/sysext/core/Classes/Database/Schema/SchemaMigrator.php
index 5ec699c9f7ab4b80fc242ee91403881db4d44840..5c404e4a399b0cbfd2555383b97935d6a67035fe 100644
--- a/typo3/sysext/core/Classes/Database/Schema/SchemaMigrator.php
+++ b/typo3/sysext/core/Classes/Database/Schema/SchemaMigrator.php
@@ -138,7 +138,7 @@ class SchemaMigrator
             $connection = $connectionPool->getConnectionByName($connectionName);
             foreach ($statementsToExecute as $hash => $statement) {
                 try {
-                    $connection->executeUpdate($statement);
+                    $connection->executeStatement($statement);
                 } catch (DBALException $e) {
                     $result[$hash] = $e->getPrevious()->getMessage();
                 }
@@ -288,8 +288,8 @@ class SchemaMigrator
                 $table->getName(),
                 array_merge($prioritizedColumns, $nonPrioritizedColumns),
                 $table->getIndexes(),
+                [],
                 $table->getForeignKeys(),
-                0,
                 $table->getOptions()
             );
         }
diff --git a/typo3/sysext/core/Documentation/Changelog/12.0/Breaking-96287-DoctrineDBAL32.rst b/typo3/sysext/core/Documentation/Changelog/12.0/Breaking-96287-DoctrineDBAL32.rst
new file mode 100644
index 0000000000000000000000000000000000000000..1a3352b7beb41cc7cddee0f8e6f6ef9cda7ad63a
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/12.0/Breaking-96287-DoctrineDBAL32.rst
@@ -0,0 +1,75 @@
+.. include:: ../../Includes.txt
+
+====================================
+Breaking: #96287 - Doctrine DBAL 3.2
+====================================
+
+See :issue:`96287`
+
+Description
+===========
+
+TYPO3 v12.0 has updated its Database Abstraction package based on Doctrine
+DBAL to the next major version Doctrine DBAL v3.
+
+
+Impact
+======
+
+Doctrine DBAL 3 has undergone major refactorings internally by separating
+Doctrine's internal driver logic from PHP's native PDO functionality.
+
+See https://www.doctrine-project.org/2021/03/29/dbal-2.13.html and
+https://www.doctrine-project.org/2020/11/17/dbal-3.0.0.html
+for more details.
+
+In addition, most database APIs which TYPO3 provides as wrappers around
+the existing functionality is already available in TYPO3 v11 and
+continue to work in TYPO3 v12.
+
+
+
+Affected Installations
+======================
+
+TYPO3 installations with custom third-party extensions using TYPO3's
+Database Abstraction functionality, or extensions using
+the Doctrine DBAL API directly.
+
+
+Migration
+=========
+
+Read Doctrine's migration paths (see links above) to migrate any existing
+code.
+
+The main change for 95% of the developers are, that queries and database result-sets
+now have more explicit APIs when querying the database.
+
+Examples:
+
+.. code-block:: php
+
+    $result = $queryBuilder
+      ->select(...)
+      ->from(...)
+      // use executeQuery() instead of execute()
+      ->executeQuery();
+
+`$result` is now of type `\Doctrine\DBAL\Result`, and not of type
+`\Doctrine\DBAL\Statement` anymore, which allows to fetch rows / columns via
+new and more speaking methods:
+
+* ->fetchAllAssociative() instead of ->fetchAll()
+* ->fetchAssociative() - instead of ->fetch()
+* ->fetchOne() - instead of ->fetchColumn(0)
+
+The method `executeQuery` - available in the QueryBuilder and
+the Connection class is now in for select/count queries and returns a Result
+object directly, whereas `executeStatement()` is used for insert / update / delete
+statements, returning an integer - the number of affected rows.
+
+Use both methods instead of the previous `execute()` method,
+which is still available for backwards-compatibility.
+
+.. index:: Database, FullyScanned, ext:core
diff --git a/typo3/sysext/core/Tests/Unit/Database/ConnectionTest.php b/typo3/sysext/core/Tests/Unit/Database/ConnectionTest.php
index 886837cc37c4447b7a574304301a9d54d06fcd14..6ce6f495b4b315d39f654277ff55404c0179d7bd 100644
--- a/typo3/sysext/core/Tests/Unit/Database/ConnectionTest.php
+++ b/typo3/sysext/core/Tests/Unit/Database/ConnectionTest.php
@@ -18,15 +18,11 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Core\Tests\Unit\Database;
 
 use Doctrine\DBAL\Configuration;
-use Doctrine\DBAL\Driver\Mysqli\Driver;
-use Doctrine\DBAL\Driver\Mysqli\MysqliConnection;
-use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
-use Doctrine\DBAL\ForwardCompatibility\Result;
+use Doctrine\DBAL\Driver\AbstractMySQLDriver;
 use Doctrine\DBAL\Platforms\AbstractPlatform;
-use Doctrine\DBAL\Platforms\MySqlPlatform;
-use Doctrine\DBAL\VersionAwarePlatformDriver;
+use Doctrine\DBAL\Platforms\MySQLPlatform;
+use Doctrine\DBAL\Result;
 use Prophecy\PhpUnit\ProphecyTrait;
-use Prophecy\Prophecy\ObjectProphecy;
 use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
@@ -69,7 +65,7 @@ class ConnectionTest extends UnitTestCase
                     'getWrappedConnection',
                 ]
             )
-            ->setConstructorArgs([['platform' => $this->prophesize(MySqlPlatform::class)->reveal()], $this->prophesize(Driver::class)->reveal(), new Configuration(), null])
+            ->setConstructorArgs([['platform' => $this->prophesize(MySQLPlatform::class)->reveal()], $this->prophesize(AbstractMySQLDriver::class)->reveal(), new Configuration(), null])
             ->getMock();
 
         $this->connection
@@ -520,19 +516,9 @@ class ConnectionTest extends UnitTestCase
      */
     public function getServerVersionReportsPlatformVersion(): void
     {
-        /** @var MysqliConnection|ObjectProphecy $driverProphet */
-        $driverProphet = $this->prophesize(Driver::class);
-        $driverProphet->willImplement(VersionAwarePlatformDriver::class);
-
-        /** @var MysqliConnection|ObjectProphecy $wrappedConnectionProphet */
-        $wrappedConnectionProphet = $this->prophesize(MysqliConnection::class);
-        $wrappedConnectionProphet->willImplement(ServerInfoAwareConnection::class);
-        $wrappedConnectionProphet->requiresQueryForServerVersion()->willReturn(false);
+        $wrappedConnectionProphet = $this->prophesize(Connection::class);
         $wrappedConnectionProphet->getServerVersion()->willReturn('5.7.11');
 
-        $this->connection
-            ->method('getDriver')
-            ->willReturn($driverProphet->reveal());
         $this->connection
             ->method('getWrappedConnection')
             ->willReturn($wrappedConnectionProphet->reveal());
diff --git a/typo3/sysext/core/Tests/Unit/Database/Platform/PlatformInformationTest.php b/typo3/sysext/core/Tests/Unit/Database/Platform/PlatformInformationTest.php
index 42880dd7a3e77777038e1477c5ec943026494d16..a78b0817afc7c738cb8bc7cc0591a9943a67acfb 100644
--- a/typo3/sysext/core/Tests/Unit/Database/Platform/PlatformInformationTest.php
+++ b/typo3/sysext/core/Tests/Unit/Database/Platform/PlatformInformationTest.php
@@ -18,8 +18,8 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Core\Tests\Unit\Database\Platform;
 
 use Doctrine\DBAL\Platforms\AbstractPlatform;
-use Doctrine\DBAL\Platforms\MySqlPlatform;
-use Doctrine\DBAL\Platforms\PostgreSQL94Platform as PostgreSqlPlatform;
+use Doctrine\DBAL\Platforms\MySQLPlatform;
+use Doctrine\DBAL\Platforms\PostgreSQL94Platform as PostgreSQLPlatform;
 use Doctrine\DBAL\Platforms\SqlitePlatform;
 use Doctrine\DBAL\Platforms\SQLServer2012Platform as SQLServerPlatform;
 use Prophecy\PhpUnit\ProphecyTrait;
@@ -41,7 +41,7 @@ class PlatformInformationTest extends UnitTestCase
     public function platformDataProvider(): array
     {
         return [
-            'mysql' => [$this->prophesize(MySqlPlatform::class)->reveal()],
+            'mysql' => [$this->prophesize(MySQLPlatform::class)->reveal()],
             'postgresql' => [$this->prophesize(PostgreSqlPlatform::class)->reveal()],
             'sqlserver' => [$this->prophesize(SQLServerPlatform::class)->reveal()],
             'sqlite' => [$this->prophesize(SqlitePlatform::class)->reveal()],
diff --git a/typo3/sysext/core/Tests/Unit/Database/Query/QueryBuilderTest.php b/typo3/sysext/core/Tests/Unit/Database/Query/QueryBuilderTest.php
index d0ca4f280e4c51b4bca2b5fbd8f073c3195fb74c..22bd9a39ec36902a04745c98f50dbfdcfe63376e 100644
--- a/typo3/sysext/core/Tests/Unit/Database/Query/QueryBuilderTest.php
+++ b/typo3/sysext/core/Tests/Unit/Database/Query/QueryBuilderTest.php
@@ -17,13 +17,13 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Core\Tests\Unit\Database\Query;
 
-use Doctrine\DBAL\ForwardCompatibility\Result;
 use Doctrine\DBAL\Platforms\AbstractPlatform;
-use Doctrine\DBAL\Platforms\MySqlPlatform;
+use Doctrine\DBAL\Platforms\MySQLPlatform;
 use Doctrine\DBAL\Platforms\OraclePlatform;
-use Doctrine\DBAL\Platforms\PostgreSQL94Platform as PostgreSqlPlatform;
+use Doctrine\DBAL\Platforms\PostgreSQL94Platform as PostgreSQLPlatform;
 use Doctrine\DBAL\Platforms\SqlitePlatform;
 use Doctrine\DBAL\Platforms\SQLServer2012Platform as SQLServerPlatform;
+use Doctrine\DBAL\Result;
 use Prophecy\Argument;
 use Prophecy\PhpUnit\ProphecyTrait;
 use Prophecy\Prophecy\ObjectProphecy;
@@ -1168,13 +1168,13 @@ class QueryBuilderTest extends UnitTestCase
     {
         return [
             'mysql' => [
-                'platform' => MySqlPlatform::class,
+                'platform' => MySQLPlatform::class,
                 'quoteChar' => '`',
                 'input' => '`anIdentifier`',
                 'expected' => 'anIdentifier',
             ],
             'mysql with spaces' => [
-                'platform' => MySqlPlatform::class,
+                'platform' => MySQLPlatform::class,
                 'quoteChar' => '`',
                 'input' => ' `anIdentifier` ',
                 'expected' => 'anIdentifier',
@@ -1406,8 +1406,8 @@ class QueryBuilderTest extends UnitTestCase
     public function castFieldToTextTypeDataProvider(): array
     {
         return [
-            'Test cast for MySqlPlatform' => [
-                new MySqlPlatform(),
+            'Test cast for MySQLPlatform' => [
+                new MySQLPlatform(),
                 'CONVERT(aField, CHAR)',
             ],
             'Test cast for PostgreSqlPlatform' => [
diff --git a/typo3/sysext/core/Tests/Unit/Database/Schema/ConnectionMigratorTest.php b/typo3/sysext/core/Tests/Unit/Database/Schema/ConnectionMigratorTest.php
index bc79b7226f2d51b2c214e9f418639019614fae3f..509c33da2c000e2066e6279614f6e423a1e10a99 100644
--- a/typo3/sysext/core/Tests/Unit/Database/Schema/ConnectionMigratorTest.php
+++ b/typo3/sysext/core/Tests/Unit/Database/Schema/ConnectionMigratorTest.php
@@ -17,7 +17,7 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Core\Tests\Unit\Database\Schema;
 
-use Doctrine\DBAL\Platforms\MySqlPlatform;
+use Doctrine\DBAL\Platforms\MySQLPlatform;
 use Doctrine\DBAL\Schema\Column;
 use Doctrine\DBAL\Schema\SchemaDiff;
 use Doctrine\DBAL\Schema\Table;
@@ -57,7 +57,7 @@ class ConnectionMigratorTest extends UnitTestCase
     {
         parent::setUp();
 
-        $platformMock = $this->prophesize(MySqlPlatform::class);
+        $platformMock = $this->prophesize(MySQLPlatform::class);
         $platformMock->quoteIdentifier(Argument::any())->willReturnArgument(0);
         $this->platform = $platformMock->reveal();
 
diff --git a/typo3/sysext/core/composer.json b/typo3/sysext/core/composer.json
index dff9f48dfa520ce2559327bdecb53cff754fafa7..895dc8c6bb99eab2799c09b55ca34ef5fd42ae60 100644
--- a/typo3/sysext/core/composer.json
+++ b/typo3/sysext/core/composer.json
@@ -31,7 +31,7 @@
 		"bacon/bacon-qr-code": "^2.0.4",
 		"christian-riesen/base32": "^1.6",
 		"doctrine/annotations": "^1.11",
-		"doctrine/dbal": "^2.13.5",
+		"doctrine/dbal": "^3.2",
 		"doctrine/event-manager": "^1.0.0",
 		"doctrine/lexer": "^1.2.1",
 		"egulias/email-validator": "^3.1",
diff --git a/typo3/sysext/indexed_search/Tests/Functional/Utility/LikeWildcardTest.php b/typo3/sysext/indexed_search/Tests/Functional/Utility/LikeWildcardTest.php
index 4833fc182fdbbcdbc6685c0faf4a79353d54a946..ac274da1cee173401287fccd38994610ca4bfc7b 100644
--- a/typo3/sysext/indexed_search/Tests/Functional/Utility/LikeWildcardTest.php
+++ b/typo3/sysext/indexed_search/Tests/Functional/Utility/LikeWildcardTest.php
@@ -17,7 +17,7 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\IndexedSearch\Tests\Functional\Utility;
 
-use Doctrine\DBAL\Platforms\MySqlPlatform;
+use Doctrine\DBAL\Platforms\MySQLPlatform;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\IndexedSearch\Utility\LikeWildcard;
@@ -43,7 +43,7 @@ class LikeWildcardTest extends FunctionalTestCase
         $subject = LikeWildcard::cast($wildcard);
         // MySQL has support for backslash escape sequences, the expected results needs to take
         // the additional quoting into account.
-        if ($connection->getDatabasePlatform() instanceof MySqlPlatform) {
+        if ($connection->getDatabasePlatform() instanceof MySQLPlatform) {
             $expected = addcslashes($expected, '\\');
         }
         $expected = $connection->quoteIdentifier($fieldName) . ' ' . $expected;
diff --git a/typo3/sysext/install/Classes/Database/PermissionsCheck.php b/typo3/sysext/install/Classes/Database/PermissionsCheck.php
index 32f25d3a0673ebf81f63dbc097cc6d0d032fbf6f..5bc7489a3c3715cb294d0dab07d4388ea9f91bd9 100644
--- a/typo3/sysext/install/Classes/Database/PermissionsCheck.php
+++ b/typo3/sysext/install/Classes/Database/PermissionsCheck.php
@@ -172,7 +172,7 @@ class PermissionsCheck
     private function checkCreateTable(string $tablename): bool
     {
         $connection = $this->getConnection();
-        $schema = $connection->getSchemaManager()->createSchema();
+        $schema = $connection->createSchemaManager()->createSchema();
         $testTable = $schema->createTable($tablename);
         $testTable->addColumn('id', 'integer', ['unsigned' => true]);
         $testTable->setPrimaryKey(['id']);
@@ -191,8 +191,8 @@ class PermissionsCheck
     {
         $connection = $this->getConnection();
         try {
-            $schemaCurrent = $connection->getSchemaManager()->createSchema();
-            $schemaNew = $connection->getSchemaManager()->createSchema();
+            $schemaCurrent = $connection->createSchemaManager()->createSchema();
+            $schemaNew = $connection->createSchemaManager()->createSchema();
 
             $schemaNew->dropTable($tablename);
             $platform = $connection->getDatabasePlatform();
diff --git a/typo3/sysext/install/Classes/Service/UpgradeWizardsService.php b/typo3/sysext/install/Classes/Service/UpgradeWizardsService.php
index 7d1f0aeaf245d8567ecfbf59dc8edd055ca9d64c..262fd279ca02062cf735d903c4c02c2f2f428250 100644
--- a/typo3/sysext/install/Classes/Service/UpgradeWizardsService.php
+++ b/typo3/sysext/install/Classes/Service/UpgradeWizardsService.php
@@ -17,7 +17,7 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Install\Service;
 
-use Doctrine\DBAL\Platforms\MySqlPlatform;
+use Doctrine\DBAL\Platforms\MySQLPlatform;
 use Doctrine\DBAL\Schema\Column;
 use Doctrine\DBAL\Schema\Table;
 use Symfony\Component\Console\Output\Output;
@@ -218,7 +218,7 @@ class UpgradeWizardsService
         $connection = GeneralUtility::makeInstance(ConnectionPool::class)
             ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
 
-        $isDefaultConnectionMysql = ($connection->getDatabasePlatform() instanceof MySqlPlatform);
+        $isDefaultConnectionMysql = ($connection->getDatabasePlatform() instanceof MySQLPlatform);
 
         if (!$isDefaultConnectionMysql) {
             // Not tested on non mysql
diff --git a/typo3/sysext/install/Classes/SystemEnvironment/DatabaseCheck.php b/typo3/sysext/install/Classes/SystemEnvironment/DatabaseCheck.php
index 4cf20444dcccd34fa74679ed3ce4186501ae8b4c..66ed9043e82a54fc04e0a1fa1ec7678a34237e34 100644
--- a/typo3/sysext/install/Classes/SystemEnvironment/DatabaseCheck.php
+++ b/typo3/sysext/install/Classes/SystemEnvironment/DatabaseCheck.php
@@ -18,12 +18,10 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Install\SystemEnvironment;
 
 use Doctrine\DBAL\Driver;
-use Doctrine\DBAL\Driver\DrizzlePDOMySql\Driver as DoctrineDrizzlePDOMySQLDriver;
-use Doctrine\DBAL\Driver\IBMDB2\DB2Driver;
+use Doctrine\DBAL\Driver\IBMDB2\Driver as DB2Driver;
 use Doctrine\DBAL\Driver\Mysqli\Driver as DoctrineMysqliDriver;
 use Doctrine\DBAL\Driver\OCI8\Driver as DoctrineOCI8Driver;
-use Doctrine\DBAL\Driver\PDOOracle\Driver as DoctrinePDOOCIDriver;
-use Doctrine\DBAL\Driver\SQLAnywhere\Driver as DoctrineSQLAnywhereDriver;
+use Doctrine\DBAL\Driver\PDO\OCI\Driver as DoctrinePDOOCIDriver;
 use Doctrine\DBAL\Driver\SQLSrv\Driver as DoctrineSQLSrvDriver;
 use TYPO3\CMS\Core\Database\Driver\PDOMySql\Driver as TYPO3PDOMySqlDriver;
 use TYPO3\CMS\Core\Database\Driver\PDOPgSql\Driver as TYPO3PDOPgSqlDriver;
@@ -107,8 +105,6 @@ class DatabaseCheck implements CheckInterface
         'ibm_db2' => DB2Driver::class,
         'pdo_sqlsrv' => TYPO3PDOSqlSrvDriver::class,
         'mysqli' => DoctrineMysqliDriver::class,
-        'drizzle_pdo_mysql' => DoctrineDrizzlePDOMySQLDriver::class,
-        'sqlanywhere' => DoctrineSQLAnywhereDriver::class,
         'sqlsrv' => DoctrineSQLSrvDriver::class,
     ];
 
diff --git a/typo3/sysext/install/composer.json b/typo3/sysext/install/composer.json
index 2035f389837f5dbeb5b1f36ea776d8820e3435f1..41c8615c8ce294ec8a10ada10d6cb050c9dcbfea 100644
--- a/typo3/sysext/install/composer.json
+++ b/typo3/sysext/install/composer.json
@@ -19,7 +19,7 @@
 		"sort-packages": true
 	},
 	"require": {
-		"doctrine/dbal": "^2.13.5",
+		"doctrine/dbal": "^3.2",
 		"guzzlehttp/promises": "^1.4.0",
 		"nikic/php-parser": "^4.10.4",
 		"symfony/finder": "^5.3.7",
diff --git a/typo3/sysext/lowlevel/Classes/Database/QueryGenerator.php b/typo3/sysext/lowlevel/Classes/Database/QueryGenerator.php
index dfa3daa7cc3f13b7c779afd5b4ff800fab61dca4..53b4739084ad0f2af00bad426e7e88c82cbf18bd 100644
--- a/typo3/sysext/lowlevel/Classes/Database/QueryGenerator.php
+++ b/typo3/sysext/lowlevel/Classes/Database/QueryGenerator.php
@@ -1343,7 +1343,7 @@ class QueryGenerator
     /**
      * Render table header
      *
-     * @param array $row Table columns
+     * @param array|null $row Table columns
      * @param array $conf Table TCA
      * @return string HTML of table header
      */
@@ -1354,7 +1354,7 @@ class QueryGenerator
         // Start header row
         $tableHeader[] = '<thead><tr>';
         // Iterate over given columns
-        foreach ($row as $fieldName => $fieldValue) {
+        foreach ($row ?? [] as $fieldName => $fieldValue) {
             if (GeneralUtility::inList($this->settings['queryFields'] ?? '', $fieldName)
                 || !($this->settings['queryFields'] ?? false)
                 && $fieldName !== 'pid'
diff --git a/typo3/sysext/redirects/composer.json b/typo3/sysext/redirects/composer.json
index df49a468590895d75765600ed5605fdfd39f8bee..26cb267ba3eeaf43093126746c2dd2526479ee7d 100644
--- a/typo3/sysext/redirects/composer.json
+++ b/typo3/sysext/redirects/composer.json
@@ -19,7 +19,7 @@
 		"sort-packages": true
 	},
 	"require": {
-		"doctrine/dbal": "^2.13.5",
+		"doctrine/dbal": "^3.2",
 		"psr/http-message": "^1.0",
 		"psr/log": "^1.0",
 		"symfony/console": "^5.3.7",