From 3a970347e549837a477f688e15bee1122687154b Mon Sep 17 00:00:00 2001
From: Benni Mack <benni@typo3.org>
Date: Tue, 30 Nov 2021 16:30:57 +0100
Subject: [PATCH] [!!!][TASK] Require Doctrine/DBAL 3.2
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The database abstraction layer Doctrine/DBAL 3.2 is now
used throughout TYPO3 Core.

Some class names have changed, the original
Doctrine\DBAL\Result interface is now an object
which is used in TYPO3 Core.

Our internal drivers are now wrappers around
PDOStatement instead of extending PDO's internal APIs.

> composer req "doctrine/dbal:^3.2" -W

Resolves: #96287
Releases: main
Change-Id: I817dbcddf6406e44c94abcd25b3eb805032aec68
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/72372
Tested-by: core-ci <typo3@b13.com>
Tested-by: Stefan Bürk <stefan@buerk.tech>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Stefan Bürk <stefan@buerk.tech>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Benni Mack <benni@typo3.org>
---
 composer.json                                 |   2 +-
 composer.lock                                 | 238 ++++++++++--------
 .../Classes/Authentication/PasswordReset.php  |   4 +-
 .../Classes/Utility/BackendUtility.php        |  16 +-
 .../core/Classes/DataHandling/DataHandler.php |  15 +-
 .../core/Classes/Database/Connection.php      |  24 +-
 .../core/Classes/Database/ConnectionPool.php  |   1 -
 .../Database/Driver/DriverConnection.php      | 111 ++++++++
 .../Classes/Database/Driver/DriverResult.php  | 172 +++++++++++++
 .../Database/Driver/DriverStatement.php       | 139 ++++++++++
 .../Classes/Database/Driver/PDOConnection.php |  42 ----
 .../Database/Driver/PDOMySql/Driver.php       |  37 ++-
 .../Database/Driver/PDOPgSql/Driver.php       |  37 ++-
 .../Database/Driver/PDOSqlite/Driver.php      |  30 ++-
 .../Database/Driver/PDOSqlsrv/Connection.php  |  38 ++-
 .../Database/Driver/PDOSqlsrv/Driver.php      |  64 +++--
 .../Database/Driver/PDOSqlsrv/Statement.php   |  28 ++-
 .../Classes/Database/Driver/PDOStatement.php  |  91 -------
 .../Driver/PDOStatementImplementation.php     |  64 -----
 .../Database/Platform/PlatformInformation.php |   6 +-
 .../Classes/Database/Query/QueryBuilder.php   |  24 +-
 .../Classes/Database/Schema/Comparator.php    | 174 ++++++++++++-
 .../Database/Schema/ConnectionMigrator.php    | 146 +++++------
 .../SchemaAlterTableListener.php              |   4 +-
 .../SchemaIndexDefinitionListener.php         |   4 +-
 .../Database/Schema/Parser/TableBuilder.php   |   8 +-
 .../Database/Schema/SchemaMigrator.php        |   4 +-
 .../12.0/Breaking-96287-DoctrineDBAL32.rst    |  75 ++++++
 .../Tests/Unit/Database/ConnectionTest.php    |  24 +-
 .../Platform/PlatformInformationTest.php      |   6 +-
 .../Unit/Database/Query/QueryBuilderTest.php  |  14 +-
 .../Schema/ConnectionMigratorTest.php         |   4 +-
 typo3/sysext/core/composer.json               |   2 +-
 .../Functional/Utility/LikeWildcardTest.php   |   4 +-
 .../Classes/Database/PermissionsCheck.php     |   6 +-
 .../Classes/Service/UpgradeWizardsService.php |   4 +-
 .../SystemEnvironment/DatabaseCheck.php       |   8 +-
 typo3/sysext/install/composer.json            |   2 +-
 .../Classes/Database/QueryGenerator.php       |   4 +-
 typo3/sysext/redirects/composer.json          |   2 +-
 40 files changed, 1076 insertions(+), 602 deletions(-)
 create mode 100644 typo3/sysext/core/Classes/Database/Driver/DriverConnection.php
 create mode 100644 typo3/sysext/core/Classes/Database/Driver/DriverResult.php
 create mode 100644 typo3/sysext/core/Classes/Database/Driver/DriverStatement.php
 delete mode 100644 typo3/sysext/core/Classes/Database/Driver/PDOConnection.php
 delete mode 100644 typo3/sysext/core/Classes/Database/Driver/PDOStatement.php
 delete mode 100644 typo3/sysext/core/Classes/Database/Driver/PDOStatementImplementation.php
 create mode 100644 typo3/sysext/core/Documentation/Changelog/12.0/Breaking-96287-DoctrineDBAL32.rst

diff --git a/composer.json b/composer.json
index aafa9d73dc0b..e2f61233d64c 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 29bcf845df6c..fca81a469de3 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 4a95c85010b5..4d765aebdb22 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 a0cc5baca367..3ecffca87524 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 34a5c1a529a1..f7048264ea17 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 3a40ad989825..27f4af430ca9 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 36616f22afd8..1694f959e7cf 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 000000000000..3f36a63f88a8
--- /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 000000000000..4a8b93b9e53d
--- /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 000000000000..98075ac43807
--- /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 aee45f6ace5c..000000000000
--- 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 c9d6edca6c69..da655c721380 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 4a6e19c6f836..ba4b061c7f51 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 12f6593fc2f4..c1c3c1e0c052 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 61c674cdb019..5ff23ead550c 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 4e2220a885db..7afaac4e6e9c 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 ebec0cdcf007..7181c25e6807 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 aa94b0312eb8..000000000000
--- 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 2bda74a5f9bf..000000000000
--- 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 736bcb3e83bb..b59744874235 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 26e12868d6c3..7d620c7440c8 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 60a4f0de1c83..8807439647e9 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 d885b8d76655..20ba6f5b9b2d 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 f33935ac6cc6..30318397072d 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 63930931bf65..a318faf4ea53 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 df70dbd15fd8..b95755af4a8c 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 5ec699c9f7ab..5c404e4a399b 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 000000000000..1a3352b7beb4
--- /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 886837cc37c4..6ce6f495b4b3 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 42880dd7a3e7..a78b0817afc7 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 d0ca4f280e4c..22bd9a39ec36 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 bc79b7226f2d..509c33da2c00 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 dff9f48dfa52..895dc8c6bb99 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 4833fc182fdb..ac274da1cee1 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 32f25d3a0673..5bc7489a3c37 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 7d1f0aeaf245..262fd279ca02 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 4cf20444dccc..66ed9043e82a 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 2035f389837f..41c8615c8ce2 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 dfa3daa7cc3f..53b4739084ad 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 df49a4685908..26cb267ba3ee 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",
-- 
GitLab