[!!!][FEATURE] Introduce Doctrine DBAL database connections 11/47111/20
authorMorton Jonuschat <m.jonuschat@mojocode.de>
Fri, 4 Mar 2016 16:11:53 +0000 (17:11 +0100)
committerChristian Kuhn <lolli@schwarzbu.ch>
Tue, 12 Apr 2016 13:17:04 +0000 (15:17 +0200)
The Doctrine DBAL library is added as a composer dependency
as a foundation to replace the current DatabaseConnection class,
EXT:dbal and EXT:adodb.

Doctrine DBAL is encapsulated within a ConnectionPool class that
manages the connections to all defined database connections.

The main parts of the patch consist of the connection management,
convenience methods for simple SQL queries, a QueryBuilder to build
complex queries in a database platform independent way and a
QueryRestriction Builder that aims to replace deleteClause and
BEenableFields in the backend context as well as enableFields in
frontend context.

Documentation and an example implementation of using the API will
follow in separate patches.

Releases: master
Resolves: #75454
Change-Id: I47837d9e77331132807bbb7fb956c359031b4f16
Reviewed-on: https://review.typo3.org/47111
Reviewed-by: Frank Naegler <frank.naegler@typo3.org>
Tested-by: Frank Naegler <frank.naegler@typo3.org>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
37 files changed:
composer.json
composer.lock
typo3/sysext/backend/Classes/Backend/ToolbarItems/SystemInformationToolbarItem.php
typo3/sysext/core/Classes/Core/Bootstrap.php
typo3/sysext/core/Classes/Database/Connection.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/ConnectionPool.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/DatabaseConnection.php
typo3/sysext/core/Classes/Database/Query/BulkInsertQuery.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Query/Expression/CompositeExpression.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Query/Expression/ExpressionBuilder.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Query/QueryBuilder.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Query/QueryContext.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Query/QueryContextType.php [new file with mode: 0644]
typo3/sysext/core/Classes/Database/Query/QueryRestrictionBuilder.php [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Breaking-75454-LocalConfigurationDBConfigStructureHasChanged.rst [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Breaking-75454-TYPO3_dbConstantsRemoved.rst [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Feature-75454-DoctrineDBALForDatabaseConnections.rst [new file with mode: 0644]
typo3/sysext/core/Tests/AcceptanceCoreEnvironment.php
typo3/sysext/core/Tests/FunctionalTestCase.php
typo3/sysext/core/Tests/Testbase.php
typo3/sysext/core/Tests/Unit/Database/ConnectionTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Mocks/MockKeywordList.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Mocks/MockPlatform.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Query/BulkInsertTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Query/Expression/ExpressionBuilderTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Query/QueryBuilderTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Query/QueryContextTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Database/Query/QueryRestrictionBuilderTest.php [new file with mode: 0644]
typo3/sysext/install/Classes/Controller/Action/AbstractAction.php
typo3/sysext/install/Classes/Controller/Action/Step/DatabaseConnect.php
typo3/sysext/install/Classes/Controller/Action/Step/DatabaseSelect.php
typo3/sysext/install/Classes/Controller/Action/Tool/ImportantActions.php
typo3/sysext/install/Classes/Service/ClearCacheService.php
typo3/sysext/install/Classes/Service/SilentConfigurationUpgradeService.php
typo3/sysext/install/Classes/Service/SqlSchemaMigrationService.php
typo3/sysext/install/Classes/SystemEnvironment/DatabaseCheck.php
typo3/sysext/install/Classes/Updates/DatabaseCharsetUpdate.php

index 94e146b..24f0a99 100644 (file)
@@ -48,7 +48,8 @@
                "cogpowered/finediff": "~0.3.1",
                "mso/idna-convert": "^0.9.1",
                "typo3fluid/fluid": "^1.0.6",
-               "guzzlehttp/guzzle": "^6.2.0"
+               "guzzlehttp/guzzle": "^6.2.0",
+               "doctrine/dbal": "~2.5.4"
        },
        "require-dev": {
                "phpunit/phpunit": "~5.2.0",
index 11eda21..64c24dc 100644 (file)
@@ -4,8 +4,8 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "hash": "8d5c9b829baf959723f0331b902a6950",
-    "content-hash": "6223c70de09731f9587fc3bd4c52e970",
+    "hash": "475c61a0b0d33a16dac25dd9a288c75f",
+    "content-hash": "aa3d371cb5c714497024983e989e7062",
     "packages": [
         {
             "name": "cogpowered/finediff",
             "time": "2014-05-19 10:25:02"
         },
         {
+            "name": "doctrine/annotations",
+            "version": "v1.2.7",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/doctrine/annotations.git",
+                "reference": "f25c8aab83e0c3e976fd7d19875f198ccf2f7535"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/doctrine/annotations/zipball/f25c8aab83e0c3e976fd7d19875f198ccf2f7535",
+                "reference": "f25c8aab83e0c3e976fd7d19875f198ccf2f7535",
+                "shasum": ""
+            },
+            "require": {
+                "doctrine/lexer": "1.*",
+                "php": ">=5.3.2"
+            },
+            "require-dev": {
+                "doctrine/cache": "1.*",
+                "phpunit/phpunit": "4.*"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.3.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "Doctrine\\Common\\Annotations\\": "lib/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Roman Borschel",
+                    "email": "roman@code-factory.org"
+                },
+                {
+                    "name": "Benjamin Eberlei",
+                    "email": "kontakt@beberlei.de"
+                },
+                {
+                    "name": "Guilherme Blanco",
+                    "email": "guilhermeblanco@gmail.com"
+                },
+                {
+                    "name": "Jonathan Wage",
+                    "email": "jonwage@gmail.com"
+                },
+                {
+                    "name": "Johannes Schmitt",
+                    "email": "schmittjoh@gmail.com"
+                }
+            ],
+            "description": "Docblock Annotations Parser",
+            "homepage": "http://www.doctrine-project.org",
+            "keywords": [
+                "annotations",
+                "docblock",
+                "parser"
+            ],
+            "time": "2015-08-31 12:32:49"
+        },
+        {
+            "name": "doctrine/cache",
+            "version": "v1.6.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/doctrine/cache.git",
+                "reference": "f8af318d14bdb0eff0336795b428b547bd39ccb6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/doctrine/cache/zipball/f8af318d14bdb0eff0336795b428b547bd39ccb6",
+                "reference": "f8af318d14bdb0eff0336795b428b547bd39ccb6",
+                "shasum": ""
+            },
+            "require": {
+                "php": "~5.5|~7.0"
+            },
+            "conflict": {
+                "doctrine/common": ">2.2,<2.4"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.8|~5.0",
+                "predis/predis": "~1.0",
+                "satooshi/php-coveralls": "~0.6"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.6.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Roman Borschel",
+                    "email": "roman@code-factory.org"
+                },
+                {
+                    "name": "Benjamin Eberlei",
+                    "email": "kontakt@beberlei.de"
+                },
+                {
+                    "name": "Guilherme Blanco",
+                    "email": "guilhermeblanco@gmail.com"
+                },
+                {
+                    "name": "Jonathan Wage",
+                    "email": "jonwage@gmail.com"
+                },
+                {
+                    "name": "Johannes Schmitt",
+                    "email": "schmittjoh@gmail.com"
+                }
+            ],
+            "description": "Caching library offering an object-oriented API for many cache backends",
+            "homepage": "http://www.doctrine-project.org",
+            "keywords": [
+                "cache",
+                "caching"
+            ],
+            "time": "2015-12-31 16:37:02"
+        },
+        {
+            "name": "doctrine/collections",
+            "version": "v1.3.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/doctrine/collections.git",
+                "reference": "6c1e4eef75f310ea1b3e30945e9f06e652128b8a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/doctrine/collections/zipball/6c1e4eef75f310ea1b3e30945e9f06e652128b8a",
+                "reference": "6c1e4eef75f310ea1b3e30945e9f06e652128b8a",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.2.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "Doctrine\\Common\\Collections\\": "lib/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Roman Borschel",
+                    "email": "roman@code-factory.org"
+                },
+                {
+                    "name": "Benjamin Eberlei",
+                    "email": "kontakt@beberlei.de"
+                },
+                {
+                    "name": "Guilherme Blanco",
+                    "email": "guilhermeblanco@gmail.com"
+                },
+                {
+                    "name": "Jonathan Wage",
+                    "email": "jonwage@gmail.com"
+                },
+                {
+                    "name": "Johannes Schmitt",
+                    "email": "schmittjoh@gmail.com"
+                }
+            ],
+            "description": "Collections Abstraction library",
+            "homepage": "http://www.doctrine-project.org",
+            "keywords": [
+                "array",
+                "collections",
+                "iterator"
+            ],
+            "time": "2015-04-14 22:21:58"
+        },
+        {
+            "name": "doctrine/common",
+            "version": "v2.6.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/doctrine/common.git",
+                "reference": "a579557bc689580c19fee4e27487a67fe60defc0"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/doctrine/common/zipball/a579557bc689580c19fee4e27487a67fe60defc0",
+                "reference": "a579557bc689580c19fee4e27487a67fe60defc0",
+                "shasum": ""
+            },
+            "require": {
+                "doctrine/annotations": "1.*",
+                "doctrine/cache": "1.*",
+                "doctrine/collections": "1.*",
+                "doctrine/inflector": "1.*",
+                "doctrine/lexer": "1.*",
+                "php": "~5.5|~7.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "~4.8|~5.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.7.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Doctrine\\Common\\": "lib/Doctrine/Common"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Roman Borschel",
+                    "email": "roman@code-factory.org"
+                },
+                {
+                    "name": "Benjamin Eberlei",
+                    "email": "kontakt@beberlei.de"
+                },
+                {
+                    "name": "Guilherme Blanco",
+                    "email": "guilhermeblanco@gmail.com"
+                },
+                {
+                    "name": "Jonathan Wage",
+                    "email": "jonwage@gmail.com"
+                },
+                {
+                    "name": "Johannes Schmitt",
+                    "email": "schmittjoh@gmail.com"
+                }
+            ],
+            "description": "Common Library for Doctrine projects",
+            "homepage": "http://www.doctrine-project.org",
+            "keywords": [
+                "annotations",
+                "collections",
+                "eventmanager",
+                "persistence",
+                "spl"
+            ],
+            "time": "2015-12-25 13:18:31"
+        },
+        {
+            "name": "doctrine/dbal",
+            "version": "v2.5.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/doctrine/dbal.git",
+                "reference": "abbdfd1cff43a7b99d027af3be709bc8fc7d4769"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/doctrine/dbal/zipball/abbdfd1cff43a7b99d027af3be709bc8fc7d4769",
+                "reference": "abbdfd1cff43a7b99d027af3be709bc8fc7d4769",
+                "shasum": ""
+            },
+            "require": {
+                "doctrine/common": ">=2.4,<2.7-dev",
+                "php": ">=5.3.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "4.*",
+                "symfony/console": "2.*"
+            },
+            "suggest": {
+                "symfony/console": "For helpful console commands such as SQL execution and import of files."
+            },
+            "bin": [
+                "bin/doctrine-dbal"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.5.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "Doctrine\\DBAL\\": "lib/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Roman Borschel",
+                    "email": "roman@code-factory.org"
+                },
+                {
+                    "name": "Benjamin Eberlei",
+                    "email": "kontakt@beberlei.de"
+                },
+                {
+                    "name": "Guilherme Blanco",
+                    "email": "guilhermeblanco@gmail.com"
+                },
+                {
+                    "name": "Jonathan Wage",
+                    "email": "jonwage@gmail.com"
+                }
+            ],
+            "description": "Database Abstraction Layer",
+            "homepage": "http://www.doctrine-project.org",
+            "keywords": [
+                "database",
+                "dbal",
+                "persistence",
+                "queryobject"
+            ],
+            "time": "2016-01-05 22:11:12"
+        },
+        {
+            "name": "doctrine/inflector",
+            "version": "v1.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/doctrine/inflector.git",
+                "reference": "90b2128806bfde671b6952ab8bea493942c1fdae"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/doctrine/inflector/zipball/90b2128806bfde671b6952ab8bea493942c1fdae",
+                "reference": "90b2128806bfde671b6952ab8bea493942c1fdae",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "4.*"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "Doctrine\\Common\\Inflector\\": "lib/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Roman Borschel",
+                    "email": "roman@code-factory.org"
+                },
+                {
+                    "name": "Benjamin Eberlei",
+                    "email": "kontakt@beberlei.de"
+                },
+                {
+                    "name": "Guilherme Blanco",
+                    "email": "guilhermeblanco@gmail.com"
+                },
+                {
+                    "name": "Jonathan Wage",
+                    "email": "jonwage@gmail.com"
+                },
+                {
+                    "name": "Johannes Schmitt",
+                    "email": "schmittjoh@gmail.com"
+                }
+            ],
+            "description": "Common String Manipulations with regard to casing and singular/plural rules.",
+            "homepage": "http://www.doctrine-project.org",
+            "keywords": [
+                "inflection",
+                "pluralize",
+                "singularize",
+                "string"
+            ],
+            "time": "2015-11-06 14:35:42"
+        },
+        {
             "name": "doctrine/instantiator",
             "version": "1.0.5",
             "source": {
             "time": "2015-06-14 21:17:01"
         },
         {
+            "name": "doctrine/lexer",
+            "version": "v1.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/doctrine/lexer.git",
+                "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c",
+                "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.2"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "Doctrine\\Common\\Lexer\\": "lib/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Roman Borschel",
+                    "email": "roman@code-factory.org"
+                },
+                {
+                    "name": "Guilherme Blanco",
+                    "email": "guilhermeblanco@gmail.com"
+                },
+                {
+                    "name": "Johannes Schmitt",
+                    "email": "schmittjoh@gmail.com"
+                }
+            ],
+            "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.",
+            "homepage": "http://www.doctrine-project.org",
+            "keywords": [
+                "lexer",
+                "parser"
+            ],
+            "time": "2014-09-09 13:34:57"
+        },
+        {
             "name": "guzzlehttp/guzzle",
             "version": "6.2.0",
             "source": {
index 68b7d99..9c38514 100644 (file)
@@ -20,6 +20,7 @@ use TYPO3\CMS\Backend\Toolbar\Enumeration\InformationStatus;
 use TYPO3\CMS\Backend\Toolbar\ToolbarItemInterface;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Core\Bootstrap;
+use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Imaging\Icon;
 use TYPO3\CMS\Core\Imaging\IconFactory;
 use TYPO3\CMS\Core\Page\PageRenderer;
@@ -168,7 +169,9 @@ class SystemInformationToolbarItem implements ToolbarItemInterface
     {
         $this->systemInformation[] = array(
             'title' => htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:toolbarItems.sysinfo.database')),
-            'value' => $this->getDatabaseConnection()->getServerVersion(),
+            'value' => GeneralUtility::makeInstance(ConnectionPool::class)
+                ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME)
+                ->getServerVersion(),
             'icon' => $this->iconFactory->getIcon('sysinfo-database', Icon::SIZE_SMALL)->render()
         );
     }
index af7c6f7..0e44f47 100644 (file)
@@ -398,7 +398,6 @@ class Bootstrap
         $this->initializeCachingFramework()
             ->initializePackageManagement($packageManagerClassName)
             ->initializeRuntimeActivatedPackagesFromConfiguration()
-            ->defineDatabaseConstants()
             ->defineUserAgentConstant()
             ->registerExtDirectComponents()
             ->setCacheHashOptions()
@@ -515,20 +514,6 @@ class Bootstrap
     }
 
     /**
-     * Define database constants
-     *
-     * @return \TYPO3\CMS\Core\Core\Bootstrap
-     */
-    protected function defineDatabaseConstants()
-    {
-        define('TYPO3_db', $GLOBALS['TYPO3_CONF_VARS']['DB']['database']);
-        define('TYPO3_db_username', $GLOBALS['TYPO3_CONF_VARS']['DB']['username']);
-        define('TYPO3_db_password', $GLOBALS['TYPO3_CONF_VARS']['DB']['password']);
-        define('TYPO3_db_host', $GLOBALS['TYPO3_CONF_VARS']['DB']['host']);
-        return $this;
-    }
-
-    /**
      * Define user agent constant
      *
      * @return \TYPO3\CMS\Core\Core\Bootstrap
@@ -809,45 +794,55 @@ class Bootstrap
     {
         /** @var $databaseConnection \TYPO3\CMS\Core\Database\DatabaseConnection */
         $databaseConnection = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\DatabaseConnection::class);
-        $databaseConnection->setDatabaseName(TYPO3_db);
-        $databaseConnection->setDatabaseUsername(TYPO3_db_username);
-        $databaseConnection->setDatabasePassword(TYPO3_db_password);
+        $databaseConnection->setDatabaseName(
+            $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname'] ?? ''
+        );
+        $databaseConnection->setDatabaseUsername(
+            $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user'] ?? ''
+        );
+        $databaseConnection->setDatabasePassword(
+            $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['password'] ?? ''
+        );
 
-        $databaseHost = TYPO3_db_host;
-        if (isset($GLOBALS['TYPO3_CONF_VARS']['DB']['port'])) {
-            $databaseConnection->setDatabasePort($GLOBALS['TYPO3_CONF_VARS']['DB']['port']);
+        $databaseHost = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host'] ?? '';
+        if (isset($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['port'])) {
+            $databaseConnection->setDatabasePort($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['port']);
         } elseif (strpos($databaseHost, ':') > 0) {
             // @TODO: Find a way to handle this case in the install tool and drop this
             list($databaseHost, $databasePort) = explode(':', $databaseHost);
             $databaseConnection->setDatabasePort($databasePort);
         }
-        if (isset($GLOBALS['TYPO3_CONF_VARS']['DB']['socket'])) {
-            $databaseConnection->setDatabaseSocket($GLOBALS['TYPO3_CONF_VARS']['DB']['socket']);
+        if (isset($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['unix_socket'])) {
+            $databaseConnection->setDatabaseSocket(
+                $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['unix_socket']
+            );
         }
         $databaseConnection->setDatabaseHost($databaseHost);
 
         $databaseConnection->debugOutput = $GLOBALS['TYPO3_CONF_VARS']['SYS']['sqlDebug'];
 
-        if (
-            isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['no_pconnect'])
-            && !$GLOBALS['TYPO3_CONF_VARS']['SYS']['no_pconnect']
+        if (isset($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['persistentConnection'])
+            && $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['persistentConnection']
         ) {
             $databaseConnection->setPersistentDatabaseConnection(true);
         }
 
-        $isDatabaseHostLocalHost = $databaseHost === 'localhost' || $databaseHost === '127.0.0.1' || $databaseHost === '::1';
-        if (
-            isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['dbClientCompress'])
-            && $GLOBALS['TYPO3_CONF_VARS']['SYS']['dbClientCompress']
+        $isDatabaseHostLocalHost = in_array($databaseHost, ['localhost', '127.0.0.1', '::1'], true);
+        if (isset($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driverOptions'])
+            && $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['driverOptions'] & MYSQLI_CLIENT_COMPRESS
             && !$isDatabaseHostLocalHost
         ) {
             $databaseConnection->setConnectionCompression(true);
         }
 
-        if (!empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['setDBinit'])) {
+        if (!empty($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['initCommands'])) {
             $commandsAfterConnect = GeneralUtility::trimExplode(
                 LF,
-                str_replace('\' . LF . \'', LF, $GLOBALS['TYPO3_CONF_VARS']['SYS']['setDBinit']),
+                str_replace(
+                    '\' . LF . \'',
+                    LF,
+                    $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['initCommands']
+                ),
                 true
             );
             $databaseConnection->setInitializeCommandsAfterConnect($commandsAfterConnect);
diff --git a/typo3/sysext/core/Classes/Database/Connection.php b/typo3/sysext/core/Classes/Database/Connection.php
new file mode 100644 (file)
index 0000000..c23f886
--- /dev/null
@@ -0,0 +1,392 @@
+<?php
+declare (strict_types=1);
+namespace TYPO3\CMS\Core\Database;
+
+/*
+ * 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!
+ */
+
+use Doctrine\Common\EventManager;
+use Doctrine\DBAL\Configuration;
+use Doctrine\DBAL\Driver;
+use Doctrine\DBAL\Driver\Statement;
+use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
+use TYPO3\CMS\Core\Database\Query\QueryBuilder;
+use TYPO3\CMS\Core\Database\Query\QueryContext;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+class Connection extends \Doctrine\DBAL\Connection
+{
+    /**
+     * Represents a SQL NULL data type.
+     */
+    const PARAM_NULL = \PDO::PARAM_NULL; // 0
+
+    /**
+     * Represents a SQL INTEGER data type.
+     */
+    const PARAM_INT = \PDO::PARAM_INT; // 1
+
+    /**
+     * Represents a SQL CHAR, VARCHAR data type.
+     */
+    const PARAM_STR = \PDO::PARAM_STR; // 2
+
+    /**
+     * Represents a SQL large object data type.
+     */
+    const PARAM_LOB = \PDO::PARAM_LOB; // 3
+
+    /**
+     * Represents a recordset type. Not currently supported by any drivers.
+     */
+    const PARAM_STMT = \PDO::PARAM_STMT; // 4
+
+    /**
+     * Represents a boolean data type.
+     */
+    const PARAM_BOOL = \PDO::PARAM_BOOL; // 5
+
+    /**
+     * Initializes a new instance of the Connection class.
+     *
+     * @param array $params The connection parameters.
+     * @param Driver $driver The driver to use.
+     * @param Configuration|null $config The configuration, optional.
+     * @param EventManager|null $em The event manager, optional.
+     *
+     * @throws \Doctrine\DBAL\DBALException
+     */
+    public function __construct(array $params, Driver $driver, Configuration $config = null, EventManager $em = null)
+    {
+        parent::__construct($params, $driver, $config, $em);
+        $this->_expr = GeneralUtility::makeInstance(ExpressionBuilder::class, $this);
+    }
+
+    /**
+     * Creates a new instance of a SQL query builder.
+     *
+     * @param \TYPO3\CMS\Core\Database\Query\QueryContext $queryContext
+     * @return \TYPO3\CMS\Core\Database\Query\QueryBuilder
+     */
+    public function createQueryBuilder(QueryContext $queryContext = null): QueryBuilder
+    {
+        return GeneralUtility::makeInstance(QueryBuilder::class, $this, $queryContext);
+    }
+
+    /**
+     * Quotes a string so it can be safely used as a table or column name, even if
+     * it is a reserved name.
+     * EXAMPLE: tableName.fieldName => "tableName"."fieldName"
+     *
+     * Delimiting style depends on the underlying database platform that is being used.
+     *
+     * @param string $identifier The name to be quoted.
+     *
+     * @return string The quoted name.
+     */
+    public function quoteIdentifier($identifier): string
+    {
+        if ($identifier === '*') {
+            return $identifier;
+        }
+
+        return parent::quoteIdentifier($identifier);
+    }
+
+    /**
+     * Quotes an array of column names so it can be safely used, even if the name is a reserved name.
+     *
+     * Delimiting style depends on the underlying database platform that is being used.
+     *
+     * @param array $input
+     *
+     * @return array
+     */
+    public function quoteIdentifiers(array $input): array
+    {
+        return array_map([$this, 'quoteIdentifier'], $input);
+    }
+
+    /**
+     * Quotes an associative array of column-value so the column names can be safely used, even
+     * if the name is a reserved name.
+     *
+     * Delimiting style depends on the underlying database platform that is being used.
+     *
+     * @param array $input
+     *
+     * @return array
+     */
+    public function quoteColumnValuePairs(array $input): array
+    {
+        return array_combine($this->quoteIdentifiers(array_keys($input)), array_values($input));
+    }
+
+    /**
+     * Inserts a table row with specified data.
+     *
+     * All SQL identifiers are expected to be unquoted and will be quoted when building the query.
+     * Table expression and columns are not escaped and are not safe for user-input.
+     *
+     * @param string $tableName The name of the table to insert data into.
+     * @param array $data An associative array containing column-value pairs.
+     * @param array $types Types of the inserted data.
+     *
+     * @return int The number of affected rows.
+     */
+    public function insert($tableName, array $data, array $types = []): int
+    {
+        return parent::insert(
+            $this->quoteIdentifier($tableName),
+            $this->quoteColumnValuePairs($data),
+            $types
+        );
+    }
+
+    /**
+     * Bulk inserts table rows with specified data.
+     *
+     * All SQL identifiers are expected to be unquoted and will be quoted when building the query.
+     * Table expression and columns are not escaped and are not safe for user-input.
+     *
+     * @param string $tableName The name of the table to insert data into.
+     * @param array $data An array containing associative arrays of column-value pairs.
+     * @param array $columns An array containing associative arrays of column-value pairs.
+     * @param array $types Types of the inserted data.
+     *
+     * @return int The number of affected rows.
+     */
+    public function bulkInsert(string $tableName, array $data, array $columns = [], array $types = []): int
+    {
+        $query = GeneralUtility::makeInstance(Query\BulkInsertQuery::class, $this, $tableName, $columns);
+        foreach ($data as $values) {
+            $query->addValues($values, $types);
+        }
+
+        return $query->execute();
+    }
+
+    /**
+     * Executes an SQL SELECT statement on a table.
+     *
+     * All SQL identifiers are expected to be unquoted and will be quoted when building the query.
+     * Table expression and columns are not escaped and are not safe for user-input.
+     *
+     * @param string[] $columns The columns of the table which to select.
+     * @param string $tableName The name of the table on which to select.
+     * @param array $identifiers The selection criteria. An associative array containing column-value pairs.
+     * @param string[] $groupBy The columns to group the results by.
+     * @param array $orderBy Associative array of column name/sort directions pairs.
+     * @param int $limit The maximum number of rows to return.
+     * @param int $offset The first result row to select (when used with limit)
+     *
+     * @return Statement The executed statement.
+     */
+    public function select(
+        array $columns,
+        string $tableName,
+        array $identifiers = [],
+        array $groupBy = [],
+        array $orderBy = [],
+        int $limit = 0,
+        int $offset = 0
+    ): Statement {
+        $query = $this->createQueryBuilder();
+        $query->select(...$columns)
+            ->from($tableName);
+
+        foreach ($identifiers as $identifier => $value) {
+            $query->andWhere($query->expr()->eq($identifier, $query->createNamedParameter($value)));
+        }
+
+        foreach ($orderBy as $fieldName => $order) {
+            $query->addOrderBy($fieldName, $order);
+        }
+
+        if (!empty($groupBy)) {
+            $query->groupBy(...$groupBy);
+        }
+
+        if ($limit > 0) {
+            $query->setMaxResults($limit);
+            $query->setFirstResult($offset);
+        }
+
+        return $query->execute();
+    }
+
+    /**
+     * Executes an SQL UPDATE statement on a table.
+     *
+     * All SQL identifiers are expected to be unquoted and will be quoted when building the query.
+     * Table expression and columns are not escaped and are not safe for user-input.
+     *
+     * @param string $tableName The name of the table to update.
+     * @param array $data An associative array containing column-value pairs.
+     * @param array $identifier The update criteria. An associative array containing column-value pairs.
+     * @param array $types Types of the merged $data and $identifier arrays in that order.
+     *
+     * @return int The number of affected rows.
+     */
+    public function update($tableName, array $data, array $identifier, array $types = []): int
+    {
+        return parent::update(
+            $this->quoteIdentifier($tableName),
+            $this->quoteColumnValuePairs($data),
+            $this->quoteColumnValuePairs($identifier),
+            $types
+        );
+    }
+
+    /**
+     * Executes an SQL DELETE statement on a table.
+     *
+     * All SQL identifiers are expected to be unquoted and will be quoted when building the query.
+     * Table expression and columns are not escaped and are not safe for user-input.
+     *
+     * @param string $tableName The name of the table on which to delete.
+     * @param array $identifier The deletion criteria. An associative array containing column-value pairs.
+     * @param array $types The types of identifiers.
+     *
+     * @return int The number of affected rows.
+     */
+    public function delete($tableName, array $identifier, array $types = []): int
+    {
+        return parent::delete(
+            $this->quoteIdentifier($tableName),
+            $this->quoteColumnValuePairs($identifier),
+            $types
+        );
+    }
+
+    /**
+     * Executes an SQL TRUNCATE statement on a table.
+     *
+     * All SQL identifiers are expected to be unquoted and will be quoted when building the query.
+     * Table expression is not escaped and not safe for user-input.
+     *
+     * @param string $tableName The name of the table to truncate.
+     * @param bool $cascade Not supported on many platforms but would cascade the truncate by following foreign keys.
+     *
+     * @return int The number of affected rows. For a truncate this is unreliable as theres no meaningful information.
+     */
+    public function truncate(string $tableName, bool $cascade = false): int
+    {
+        return $this->executeUpdate(
+            $this->getDatabasePlatform()->getTruncateTableSQL(
+                $this->quoteIdentifier($tableName),
+                $cascade
+            )
+        );
+    }
+
+    /**
+     * Executes an SQL SELECT COUNT() statement on a table and returns the count result.
+     *
+     * @param string $item The column/expression of the table which to count
+     * @param string $tableName The name of the table on which to count.
+     * @param array $identifiers The selection criteria. An associative array containing column-value pairs.
+     *
+     * @return int The number of rows counted
+     */
+    public function count(string $item, string $tableName, array $identifiers): int
+    {
+        $query = $this->createQueryBuilder();
+        $query->count($item)
+            ->from($tableName);
+
+        foreach ($identifiers as $identifier => $value) {
+            $query->andWhere($query->expr()->eq($identifier, $query->createNamedParameter($value)));
+        }
+
+        return $query->execute()->fetchColumn(0);
+    }
+
+    /**
+     * Returns the version of the current platform if applicable.
+     *
+     * If no version information is available only the platform name will be shown.
+     * If the platform name is unknown or unsupported the driver name will be shown.
+     *
+     * @return string
+     * @internal
+     */
+    public function getServerVersion(): string
+    {
+        $version = $this->getDatabasePlatform()->getName();
+        switch ($version) {
+            case 'mysql':
+            case 'pdo_mysql':
+            case 'drizzle_pdo_mysql':
+                $version = 'MySQL';
+                break;
+            case 'postgresql':
+            case 'pdo_postgresql':
+                $version = 'PostgreSQL';
+                break;
+            case 'oci8':
+            case 'pdo_oracle':
+                $version = 'Oracle';
+                break;
+            case 'sqlsrv':
+            case 'pdo_sqlsrv':
+                $version = 'SQLServer';
+                break;
+        }
+
+        // Driver does not support version specific platforms.
+        if (!$this->getDriver() instanceof \Doctrine\DBAL\VersionAwarePlatformDriver) {
+            return $version;
+        }
+
+        if ($this->getWrappedConnection() instanceof \Doctrine\DBAL\Driver\ServerInfoAwareConnection
+            && !$this->getWrappedConnection()->requiresQueryForServerVersion()
+        ) {
+            $version .= ' ' . $this->getWrappedConnection()->getServerVersion();
+        }
+
+        return $version;
+    }
+
+    /**
+     * Execute commands after initializing a new connection.
+     *
+     * @param string $commands
+     */
+    public function prepareConnection(string $commands)
+    {
+        if (empty($commands)) {
+            return;
+        }
+
+        $commandsToPerform = GeneralUtility::trimExplode(
+            LF,
+            str_replace(
+                '\' . LF . \'',
+                LF,
+                $commands
+            ),
+            true
+        );
+
+        foreach ($commandsToPerform as $command) {
+            if ($this->executeUpdate($command) === false) {
+                GeneralUtility::sysLog(
+                    'Could not initialize DB connection with query "' . $command . '": ' . $this->errorInfo(),
+                    'core',
+                    GeneralUtility::SYSLOG_SEVERITY_ERROR
+                );
+            }
+        }
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/ConnectionPool.php b/typo3/sysext/core/Classes/Database/ConnectionPool.php
new file mode 100644 (file)
index 0000000..3ce0861
--- /dev/null
@@ -0,0 +1,159 @@
+<?php
+declare (strict_types = 1);
+namespace TYPO3\CMS\Core\Database;
+
+/*
+ * 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!
+ */
+
+use Doctrine\DBAL\Configuration;
+use Doctrine\DBAL\DriverManager;
+use TYPO3\CMS\Core\Database\Query\QueryBuilder;
+
+/**
+ * Manager that handles opening/retrieving database connections.
+ *
+ * It's a facade to the actual Doctrine DBAL DriverManager that implements TYPO3
+ * specific functionality like mapping individual tables to different database
+ * connections.
+ *
+ * getConnectionFotTable() is the only supported way to get a connection that
+ * honors the table mapping configuration.
+ */
+class ConnectionPool
+{
+    /**
+     * @var string
+     */
+    const DEFAULT_CONNECTION_NAME = 'Default';
+
+    /**
+     * @var Connection[]
+     */
+    protected static $connections = [];
+
+    /**
+     * Creates a connection object based on the specified table name.
+     *
+     * This is the official entry point to get a database connection to ensure
+     * that the mapping of table names to database connections is honored.
+     *
+     * @param string $tableName
+     * @return Connection
+     */
+    public function getConnectionForTable(string $tableName): Connection
+    {
+        if (empty($tableName)) {
+            throw new \UnexpectedValueException(
+                'ConnectionPool->getConnectionForTable() requires a table name to be provided.',
+                1459421719
+            );
+        }
+
+        $connectionName = ConnectionPool::DEFAULT_CONNECTION_NAME;
+        if (!empty($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName])) {
+            $connectionName = (string)$GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'][$tableName];
+        }
+
+        return $this->getConnectionByName($connectionName);
+    }
+
+    /**
+     * Creates a connection object based on the specified identifier.
+     *
+     * This method should only be used in edge cases. Use getConnectionForTable() so
+     * that the tablename<>databaseConnection mapping will be taken into account.
+     *
+     * @param string $connectionName
+     * @return Connection
+     * @throws \Doctrine\DBAL\DBALException
+     * @internal
+     */
+    public function getConnectionByName(string $connectionName): Connection
+    {
+        if (empty($connectionName)) {
+            throw new \UnexpectedValueException(
+                'ConnectionPool->getConnectionByName() requires a connection name to be provided.',
+                1459422125
+            );
+        }
+
+        if (isset(static::$connections[$connectionName])) {
+            return static::$connections[$connectionName];
+        }
+
+        if (empty($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][$connectionName])
+            || !is_array($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][$connectionName])
+        ) {
+            throw new \RuntimeException(
+                'The requested database connection named "' . $connectionName . '" has not been configured.',
+                1459422492
+            );
+        }
+
+        $connectionParams = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][$connectionName];
+        if (empty($connectionParams['wrapperClass'])) {
+            $connectionParams['wrapperClass'] = Connection::class;
+        }
+
+        if (!is_a($connectionParams['wrapperClass'], Connection::class, true)) {
+            throw new \UnexpectedValueException(
+                'The "wrapperClass" for the connection name "' . $connectionName .
+                '" needs to be a subclass of "' . Connection::class . '".',
+                1459422968
+            );
+        }
+
+        static::$connections[$connectionName] = $this->getDatabaseConnection($connectionParams);
+
+        return static::$connections[$connectionName];
+    }
+
+    /**
+     * Creates a connection object based on the specified parameters
+     *
+     * @param array $connectionParams
+     * @return Connection
+     */
+    protected function getDatabaseConnection(array $connectionParams): Connection
+    {
+        // Default to UTF-8 connection charset
+        if (empty($connectionParams['charset'])) {
+            $connectionParams['charset'] = 'utf-8';
+        }
+        /** @var Connection $conn */
+        $conn = DriverManager::getConnection($connectionParams);
+        $conn->setFetchMode(\PDO::FETCH_ASSOC);
+        $conn->prepareConnection($connectionParams['initCommands'] ?? '');
+
+        return $conn;
+    }
+
+    /**
+     * Returns the connection specific query builder object that can be used to build
+     * complex SQL queries using and object oriented approach.
+     *
+     * @param string $tableName
+     * @return QueryBuilder
+     */
+    public function getQueryBuilderForTable(string $tableName): QueryBuilder
+    {
+        if (empty($tableName)) {
+            throw new \UnexpectedValueException(
+                'ConnectionPool->getQueryBuilderForTable() requires a connection name to be provided.',
+                1459423448
+            );
+        }
+
+        return $this->getConnectionForTable($tableName)->createQueryBuilder();
+    }
+}
index 2313b96..cfbd7dc 100644 (file)
@@ -1248,28 +1248,32 @@ class DatabaseConnection
             ? 'p:' . $this->databaseHost
             : $this->databaseHost;
 
-        $this->link = mysqli_init();
-        $connected = $this->link->real_connect(
-            $host,
-            $this->databaseUsername,
-            $this->databaseUserPassword,
-            null,
-            (int)$this->databasePort,
-            $this->databaseSocket,
-            $this->connectionCompression ? MYSQLI_CLIENT_COMPRESS : 0
-        );
+        // We are not using the TYPO3 CMS shim here as the database parameters in this class
+        // are settable externally. This requires building the connection parameter array
+        // just in time when establishing the connection.
+        $connection = \Doctrine\DBAL\DriverManager::getConnection([
+            'driver' => 'mysqli',
+            'wrapperClass' => Connection::class,
+            'host' => $host,
+            'port' => (int)$this->databasePort,
+            'unix_socket' => $this->databaseSocket,
+            'user' => $this->databaseUsername,
+            'password' => $this->databaseUserPassword,
+            'charset' => $this->connectionCharset,
+        ]);
+
+        // Mimic the previous behavior of returning false on connection errors
+        try {
+            /** @var \Doctrine\DBAL\Driver\Mysqli\MysqliConnection $mysqliConnection */
+            $mysqliConnection = $connection->getWrappedConnection();
+            $this->link = $mysqliConnection->getWrappedResourceHandle();
+        } catch (\Doctrine\DBAL\Exception\ConnectionException $exception) {
+            return false;
+        }
 
-        if ($connected) {
+        if ($connection->isConnected()) {
             $this->isConnected = true;
 
-            if ($this->link->set_charset($this->connectionCharset) === false) {
-                GeneralUtility::sysLog(
-                    'Error setting connection charset to "' . $this->connectionCharset . '"',
-                    'core',
-                    GeneralUtility::SYSLOG_SEVERITY_ERROR
-                );
-            }
-
             foreach ($this->initializeCommandsAfterConnect as $command) {
                 if ($this->query($command) === false) {
                     GeneralUtility::sysLog(
@@ -1285,11 +1289,13 @@ class DatabaseConnection
             $error_msg = $this->link->connect_error;
             $this->link = null;
             GeneralUtility::sysLog(
-                'Could not connect to MySQL server ' . $host . ' with user ' . $this->databaseUsername . ': ' . $error_msg,
+                'Could not connect to MySQL server ' . $host . ' with user ' . $this->databaseUsername . ': '
+                . $error_msg,
                 'core',
                 GeneralUtility::SYSLOG_SEVERITY_FATAL
             );
         }
+
         return $this->link;
     }
 
diff --git a/typo3/sysext/core/Classes/Database/Query/BulkInsertQuery.php b/typo3/sysext/core/Classes/Database/Query/BulkInsertQuery.php
new file mode 100644 (file)
index 0000000..3a67de0
--- /dev/null
@@ -0,0 +1,283 @@
+<?php
+declare (strict_types = 1);
+namespace TYPO3\CMS\Core\Database\Query;
+
+/*
+ * 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!
+ */
+
+use Doctrine\DBAL\Connection;
+
+/**
+ * Provides functionality to generate and execute row based bulk INSERT statements.
+ *
+ * Based on work by Steve Müller <st.mueller@dzh-online.de> for the Doctrine project,
+ * licensend under the MIT license.
+ *
+ * This class will be removed from core and the functionality will be provided by
+ * the upstream implemention once the pull request has been merged into Doctrine DBAL.
+ *
+ * @see https://github.com/doctrine/dbal/pull/682
+ * @internal
+ */
+class BulkInsertQuery
+{
+    /**
+     * @var string[]
+     */
+    protected $columns;
+
+    /**
+     * @var Connection
+     */
+    protected $connection;
+
+    /**
+     * @var string
+     */
+    protected $table;
+
+    /**
+     * @var array
+     */
+    protected $parameters = [];
+
+    /**
+     * @var array
+     */
+    protected $types = [];
+
+    /**
+     * @var array
+     */
+    protected $values = [];
+
+    /**
+     * Constructor.
+     *
+     * @param Connection $connection The connection to use for query execution.
+     * @param string $table The name of the table to insert rows into.
+     * @param string[] $columns The names of the columns to insert values into.
+     *                          Can be left empty to allow arbitrary row inserts based on the table's column order.
+     */
+    public function __construct(Connection $connection, string $table, array $columns = [])
+    {
+        $this->connection = $connection;
+        $this->table = $connection->quoteIdentifier($table);
+        $this->columns = $columns;
+    }
+
+    /**
+     * Render the bulk insert statement as string.
+     *
+     * @return string
+     */
+    public function __toString(): string
+    {
+        return $this->getSQL();
+    }
+
+    /**
+     * Adds a set of values to the bulk insert query to be inserted as a row into the specified table.
+     *
+     * @param array $values The set of values to be inserted as a row into the table.
+     *                      If no columns have been specified for insertion, this can be
+     *                      an arbitrary list of values to be inserted into the table.
+     *                      Otherwise the values' keys have to match either one of the
+     *                      specified column names or indexes.
+     * @param array $types The types for the given values to bind to the query.
+     *                     If no columns have been specified for insertion, the types'
+     *                     keys will be matched against the given values' keys.
+     *                     Otherwise the types' keys will be matched against the
+     *                     specified column names and indexes.
+     *                     Non-matching keys will be discarded, missing keys will not
+     *                     be bound to a specific type.
+     *
+     * @throws \InvalidArgumentException if columns were specified for this query
+     *                                   and either no value for one of the specified
+     *                                   columns is given or multiple values are given
+     *                                   for a single column (named and indexed) or
+     *                                   multiple types are given for a single column
+     *                                   (named and indexed).
+     *
+     * @return void
+     */
+    public function addValues(array $values, array $types = [])
+    {
+        $valueSet = [];
+
+        if (empty($this->columns)) {
+            foreach ($values as $index => $value) {
+                $this->parameters[] = $value;
+                $this->types[] = isset($types[$index]) ? $types[$index] : null;
+                $valueSet[] = '?';
+            }
+
+            $this->values[] = $valueSet;
+
+            return;
+        }
+
+        foreach ($this->columns as $index => $column) {
+            $namedValue = isset($values[$column]) || array_key_exists($column, $values);
+            $positionalValue = isset($values[$index]) || array_key_exists($index, $values);
+
+            if (!$namedValue && !$positionalValue) {
+                throw new \InvalidArgumentException(
+                    sprintf('No value specified for column %s (index %d).', $column, $index)
+                );
+            }
+
+            if ($namedValue && $positionalValue && $values[$column] !== $values[$index]) {
+                throw new \InvalidArgumentException(
+                    sprintf('Multiple values specified for column %s (index %d).', $column, $index)
+                );
+            }
+
+            $this->parameters[] = $namedValue ? $values[$column] : $values[$index];
+            $valueSet[] = '?';
+
+            $namedType = isset($types[$column]);
+            $positionalType = isset($types[$index]);
+
+            if ($namedType && $positionalType && $types[$column] !== $types[$index]) {
+                throw new \InvalidArgumentException(
+                    sprintf('Multiple types specified for column %s (index %d).', $column, $index)
+                );
+            }
+
+            if ($namedType) {
+                $this->types[] = $types[$column];
+
+                continue;
+            }
+
+            if ($positionalType) {
+                $this->types[] = $types[$index];
+
+                continue;
+            }
+
+            $this->types[] = null;
+        }
+
+        $this->values[] = $valueSet;
+    }
+
+    /**
+     * Executes this INSERT query using the bound parameters and their types.
+     *
+     * @return int The number of affected rows.
+     *
+     * @throws \LogicException if this query contains more rows than acceptable
+     *                         for a single INSERT statement by the underlying platform.
+     */
+    public function execute(): int
+    {
+        $platform = $this->connection->getDatabasePlatform();
+        $insertMaxRows = $this->getInsertMaxRows();
+
+        if ($insertMaxRows > 0 && count($this->values) > $insertMaxRows) {
+            throw new \LogicException(
+                sprintf(
+                    'You can only insert %d rows in a single INSERT statement with platform "%s".',
+                    $insertMaxRows,
+                    $platform->getName()
+                )
+            );
+        }
+
+        return $this->connection->executeUpdate($this->getSQL(), $this->parameters, $this->types);
+    }
+
+    /**
+     * Return the maximum number of rows that can be inserted at the same time.
+     *
+     * @return int
+     */
+    protected function getInsertMaxRows(): int
+    {
+        $platform = $this->connection->getDatabasePlatform();
+        if ($platform->getName() === 'mssql' && $platform->getReservedKeywordsList()->isKeyword('MERGE')) {
+            return 1000;
+        }
+
+        return 0;
+    }
+
+    /**
+     * Returns the parameters for this INSERT query being constructed indexed by parameter index.
+     *
+     * @return array
+     */
+    public function getParameters(): array
+    {
+        return $this->parameters;
+    }
+
+    /**
+     * Returns the parameter types for this INSERT query being constructed indexed by parameter index.
+     *
+     * @return array
+     */
+    public function getParameterTypes(): array
+    {
+        return $this->types;
+    }
+
+    /**
+     * Returns the SQL formed by the current specifications of this INSERT query.
+     *
+     * @return string
+     *
+     * @throws \LogicException if no values have been specified yet.
+     */
+    public function getSQL(): string
+    {
+        if (empty($this->values)) {
+            throw new \LogicException('You need to add at least one set of values before generating the SQL.');
+        }
+
+        $connection = $this->connection;
+        $columnList = '';
+
+        if (!empty($this->columns)) {
+            $columnList = sprintf(
+                ' (%s)',
+                implode(
+                    ', ',
+                    array_map(
+                        function ($column) use ($connection) {
+                            return $connection->quoteIdentifier($column);
+                        },
+                        $this->columns
+                    )
+                )
+            );
+        }
+
+        return sprintf(
+            'INSERT INTO %s%s VALUES (%s)',
+            $this->table,
+            $columnList,
+            implode(
+                '), (',
+                array_map(
+                    function (array $valueSet) {
+                        return implode(', ', $valueSet);
+                    },
+                    $this->values
+                )
+            )
+        );
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Query/Expression/CompositeExpression.php b/typo3/sysext/core/Classes/Database/Query/Expression/CompositeExpression.php
new file mode 100644 (file)
index 0000000..14affb6
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+declare (strict_types = 1);
+namespace TYPO3\CMS\Core\Database\Query\Expression;
+
+/*
+ * 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!
+ */
+
+/**
+ * Facade of the Doctrine DBAL CompositeExpression to have
+ * all Query related classes with in TYPO3\CMS namespace.
+ */
+class CompositeExpression extends \Doctrine\DBAL\Query\Expression\CompositeExpression
+{
+    /**
+     * Retrieves the string representation of this composite expression.
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        if ($this->count() === 0) {
+            return '';
+        }
+
+        return parent::__toString();
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Query/Expression/ExpressionBuilder.php b/typo3/sysext/core/Classes/Database/Query/Expression/ExpressionBuilder.php
new file mode 100644 (file)
index 0000000..f6e8635
--- /dev/null
@@ -0,0 +1,340 @@
+<?php
+declare (strict_types = 1);
+namespace TYPO3\CMS\Core\Database\Query\Expression;
+
+/*
+ * 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!
+ */
+
+use TYPO3\CMS\Core\Database\Connection;
+
+/**
+ * ExpressionBuilder class is responsible to dynamically create SQL query parts.
+ *
+ * It takes care building query conditions while ensuring table and column names
+ * are quoted within the created expressions / SQL fragments. It is a facade to
+ * the actual Doctrine ExpressionBuilder.
+ *
+ * The ExpressionBuilder is used within the context of the QueryBuilder to ensure
+ * queries are being build based on the requirements of the database platform in
+ * use.
+ */
+class ExpressionBuilder
+{
+    const EQ = '=';
+    const NEQ = '<>';
+    const LT = '<';
+    const LTE = '<=';
+    const GT = '>';
+    const GTE = '>=';
+
+    const QUOTE_NOTHING = 0;
+    const QUOTE_IDENTIFIER = 1;
+    const QUOTE_PARAMETER = 2;
+
+    /**
+     * The DBAL Connection.
+     *
+     * @var Connection
+     */
+    protected $connection;
+
+    /**
+     * Initializes a new ExpressionBuilder
+     *
+     * @param Connection $connection
+     */
+    public function __construct(Connection $connection)
+    {
+        $this->connection = $connection;
+    }
+
+    /**
+     * Creates a conjunction of the given boolean expressions
+     *
+     * @param mixed,... $expressions Optional clause. Requires at least one defined when converting to string.
+     *
+     * @return CompositeExpression
+     */
+    public function andX(...$expressions): CompositeExpression
+    {
+        return new CompositeExpression(CompositeExpression::TYPE_AND, $expressions);
+    }
+
+    /**
+     * Creates a disjunction of the given boolean expressions.
+     *
+     * @param mixed,... $expressions Optional clause. Requires at least one defined when converting to string.
+     *
+     * @return CompositeExpression
+     */
+    public function orX(...$expressions): CompositeExpression
+    {
+        return new CompositeExpression(CompositeExpression::TYPE_OR, $expressions);
+    }
+
+    /**
+     * Creates a comparison expression.
+     *
+     * @param mixed $leftExpression The left expression.
+     * @param string $operator One of the ExpressionBuilder::* constants.
+     * @param mixed $rightExpression The right expression.
+     *
+     * @return string
+     */
+    public function comparison($leftExpression, string $operator, $rightExpression): string
+    {
+        return $leftExpression . ' ' . $operator . ' ' . $rightExpression;
+    }
+
+    /**
+     * Creates an equality comparison expression with the given arguments.
+     *
+     * @param string $fieldName The fieldname. Will be quoted according to database platform automatically.
+     * @param mixed $value The value. No automatic quoting/escaping is done.
+     *
+     * @return string
+     */
+    public function eq(string $fieldName, $value): string
+    {
+        return $this->comparison($this->connection->quoteIdentifier($fieldName), static::EQ, $value);
+    }
+
+    /**
+     * Creates a non equality comparison expression with the given arguments.
+     * First argument is considered the left expression and the second is the right expression.
+     * When converted to string, it will generated a <left expr> <> <right expr>. Example:
+     *
+     *     [php]
+     *     // u.id <> 1
+     *     $q->where($q->expr()->neq('u.id', '1'));
+     *
+     * @param string $fieldName The fieldname. Will be quoted according to database platform automatically.
+     * @param mixed $value The value. No automatic quoting/escaping is done.
+     *
+     * @return string
+     */
+    public function neq(string $fieldName, $value): string
+    {
+        return $this->comparison($this->connection->quoteIdentifier($fieldName), static::NEQ, $value);
+    }
+
+    /**
+     * Creates a lower-than comparison expression with the given arguments.
+     *
+     * @param string $fieldName The fieldname. Will be quoted according to database platform automatically.
+     * @param mixed $value The value. No automatic quoting/escaping is done.
+     *
+     * @return string
+     */
+    public function lt($fieldName, $value): string
+    {
+        return $this->comparison($this->connection->quoteIdentifier($fieldName), static::LT, $value);
+    }
+
+    /**
+     * Creates a lower-than-equal comparison expression with the given arguments.
+     *
+     * @param string $fieldName The fieldname. Will be quoted according to database platform automatically.
+     * @param mixed $value The value. No automatic quoting/escaping is done.
+     *
+     * @return string
+     */
+    public function lte(string $fieldName, $value): string
+    {
+        return $this->comparison($this->connection->quoteIdentifier($fieldName), static::LTE, $value);
+    }
+
+    /**
+     * Creates a greater-than comparison expression with the given arguments.
+     *
+     * @param string $fieldName The fieldname. Will be quoted according to database platform automatically.
+     * @param mixed $value The value. No automatic quoting/escaping is done.
+     *
+     * @return string
+     */
+    public function gt(string $fieldName, $value): string
+    {
+        return $this->comparison($this->connection->quoteIdentifier($fieldName), static::GT, $value);
+    }
+
+    /**
+     * Creates a greater-than-equal comparison expression with the given arguments.
+     *
+     * @param string $fieldName The fieldname. Will be quoted according to database platform automatically.
+     * @param mixed $value The value. No automatic quoting/escaping is done.
+     *
+     * @return string
+     */
+    public function gte(string $fieldName, $value): string
+    {
+        return $this->comparison($this->connection->quoteIdentifier($fieldName), static::GTE, $value);
+    }
+
+    /**
+     * Creates an IS NULL expression with the given arguments.
+     *
+     * @param string $fieldName The fieldname. Will be quoted according to database platform automatically.
+     *
+     * @return string
+     */
+    public function isNull(string $fieldName): string
+    {
+        return $this->connection->quoteIdentifier($fieldName) . ' IS NULL';
+    }
+
+    /**
+     * Creates an IS NOT NULL expression with the given arguments.
+     *
+     * @param string $fieldName The fieldname. Will be quoted according to database platform automatically.
+     *
+     * @return string
+     */
+    public function isNotNull(string $fieldName): string
+    {
+        return $this->connection->quoteIdentifier($fieldName) . ' IS NOT NULL';
+    }
+
+    /**
+     * Creates a LIKE() comparison expression with the given arguments.
+     *
+     * @param string $fieldName The fieldname. Will be quoted according to database platform automatically.
+     * @param mixed $value Argument to be used in LIKE() comparison. No automatic quoting/escaping is done.
+     *
+     * @return string
+     */
+    public function like(string $fieldName, $value): string
+    {
+        return $this->comparison($this->connection->quoteIdentifier($fieldName), 'LIKE', $value);
+    }
+
+    /**
+     * Creates a NOT LIKE() comparison expression with the given arguments.
+     *
+     * @param string $fieldName The fieldname. Will be quoted according to database platform automatically.
+     * @param mixed $value Argument to be used in NOT LIKE() comparison. No automatic quoting/escaping is done.
+     *
+     * @return string
+     */
+    public function notLike(string $fieldName, $value): string
+    {
+        return $this->comparison($this->connection->quoteIdentifier($fieldName), 'NOT LIKE', $value);
+    }
+
+    /**
+     * Creates a IN () comparison expression with the given arguments.
+     *
+     * @param string $fieldName The fieldname. Will be quoted according to database platform automatically.
+     * @param string|array $value The placeholder or the array of values to be used by IN() comparison.
+     *                            No automatic quoting/escaping is done.
+     *
+     * @return string
+     */
+    public function in(string $fieldName, $value): string
+    {
+        return $this->comparison(
+            $this->connection->quoteIdentifier($fieldName),
+            'IN',
+            '(' . implode(', ', (array)$value) . ')'
+        );
+    }
+
+    /**
+     * Creates a NOT IN () comparison expression with the given arguments.
+     *
+     * @param string $fieldName The fieldname. Will be quoted according to database platform automatically.
+     * @param string|array $value The placeholder or the array of values to be used by NOT IN() comparison.
+     *                            No automatic quoting/escaping is done.
+     *
+     * @return string
+     */
+    public function notIn(string $fieldName, $value): string
+    {
+        return $this->comparison(
+            $this->connection->quoteIdentifier($fieldName),
+            'NOT IN',
+            '(' . implode(', ', (array)$value) . ')'
+        );
+    }
+
+    /**
+     * Returns a comparison that can find a value in a list field (CSV).
+     *
+     * @param string $fieldName The fieldname. Will be quoted according to database platform automatically.
+     * @param string $value Argument to be used in FIND_IN_SET() comparison. No automatic quoting/escaping is done.
+     * @return string
+     * @throws \RuntimeException
+     */
+    public function inSet(string $fieldName, string $value): string
+    {
+        if ($value === '') {
+            throw new \InvalidArgumentException(
+                'ExpressionBuilder::inSet() can not be used with an empty string value.',
+                1459696089
+            );
+        }
+
+        if (strpos($value, ',') !== false) {
+            throw new \InvalidArgumentException(
+                'ExpressionBuilder::inSet() can not be used with values that contain a comma (",").',
+                1459696090
+            );
+        }
+
+        switch ($this->connection->getDatabasePlatform()->getName()) {
+            case 'postgresql':
+            case 'pdo_postgresql':
+                return $this->eq(
+                    sprintf(
+                        'any(string_to_array(%s, %s))',
+                        $this->connection->quoteIdentifier($fieldName),
+                        $this->literal(',')
+                    ),
+                    $value
+                );
+                break;
+            case 'oci8':
+            case 'pdo_oracle':
+                throw new \RuntimeException(
+                    'FIND_IN_SET support for database platform "Oracle" not yet implemented.',
+                    1459696680
+                );
+                break;
+            case 'sqlsrv':
+            case 'pdo_sqlsrv':
+                throw new \RuntimeException(
+                    'FIND_IN_SET support for database platform "SQLServer" not yet implemented.',
+                    1459696681
+                );
+                break;
+            default:
+                return sprintf(
+                    'FIND_IN_SET(%s, %s)',
+                    $value,
+                    $this->connection->quoteIdentifier($fieldName)
+                );
+        }
+    }
+
+    /**
+     * Quotes a given input parameter.
+     *
+     * @param mixed $input The parameter to be quoted.
+     * @param string|null $type The type of the parameter.
+     *
+     * @return string
+     */
+    public function literal($input, string $type = null): string
+    {
+        return $this->connection->quote($input, $type);
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Query/QueryBuilder.php b/typo3/sysext/core/Classes/Database/Query/QueryBuilder.php
new file mode 100644 (file)
index 0000000..09105bc
--- /dev/null
@@ -0,0 +1,1049 @@
+<?php
+declare (strict_types = 1);
+namespace TYPO3\CMS\Core\Database\Query;
+
+/*
+ * 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!
+ */
+
+use Doctrine\DBAL\Query\Expression\CompositeExpression;
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Object oriented approach to building SQL queries.
+ *
+ * It's a facade to the Doctrine DBAL QueryBuilder that implements PHP7 type hinting and automatic
+ * quoting of table and column names.
+ *
+ * <code>
+ * $query->select('aField', 'anotherField')
+ *       ->from('aTable')
+ *       ->where($query->expr()->eq('aField', 1))
+ *       ->andWhere($query->expr()->gte('anotherField',10'))
+ *       ->execute()
+ * </code>
+ *
+ * Additional functionality included is support for COUNT() and TRUNCATE() statements.
+ */
+class QueryBuilder
+{
+    /**
+     * The DBAL Connection.
+     *
+     * @var Connection
+     */
+    protected $connection;
+
+    /**
+     * @var \Doctrine\DBAL\Query\QueryBuilder
+     */
+    protected $concreteQueryBuilder;
+
+    /**
+     * @var QueryContext
+     */
+    protected $queryContext;
+
+    /**
+     * Initializes a new QueryBuilder.
+     *
+     * @param Connection $connection The DBAL Connection.
+     * @param QueryContext $queryContext
+     * @param \Doctrine\DBAL\Query\QueryBuilder $concreteQueryBuilder
+     */
+    public function __construct(
+        Connection $connection,
+        QueryContext $queryContext = null,
+        \Doctrine\DBAL\Query\QueryBuilder $concreteQueryBuilder = null
+    ) {
+        $this->connection = $connection;
+
+        if ($queryContext === null) {
+            $queryContext = GeneralUtility::makeInstance(QueryContext::class);
+        }
+        $this->queryContext = $queryContext;
+
+        if ($concreteQueryBuilder === null) {
+            $concreteQueryBuilder = GeneralUtility::makeInstance(
+                \Doctrine\DBAL\Query\QueryBuilder::class,
+                $connection
+            );
+        }
+        $this->concreteQueryBuilder = $concreteQueryBuilder;
+    }
+
+    /**
+     * @return QueryContext
+     */
+    public function getQueryContext(): QueryContext
+    {
+        return $this->queryContext;
+    }
+
+    /**
+     * @param QueryContext $queryContext
+     */
+    public function setQueryContext(QueryContext $queryContext)
+    {
+        $this->queryContext = $queryContext;
+    }
+
+    /**
+     * Gets an ExpressionBuilder used for object-oriented construction of query expressions.
+     * This producer method is intended for convenient inline usage. Example:
+     *
+     * For more complex expression construction, consider storing the expression
+     * builder object in a local variable.
+     *
+     * @return ExpressionBuilder
+     */
+    public function expr(): ExpressionBuilder
+    {
+        return $this->connection->getExpressionBuilder();
+    }
+
+    /**
+     * Gets the type of the currently built query.
+     *
+     * @return int
+     * @internal
+     */
+    public function getType(): int
+    {
+        return $this->concreteQueryBuilder->getType();
+    }
+
+    /**
+     * Gets the associated DBAL Connection for this query builder.
+     *
+     * @return Connection
+     */
+    public function getConnection(): Connection
+    {
+        return $this->connection;
+    }
+
+    /**
+     * Gets the state of this query builder instance.
+     *
+     * @return int Either QueryBuilder::STATE_DIRTY or QueryBuilder::STATE_CLEAN.
+     * @internal
+     */
+    public function getState(): int
+    {
+        return $this->concreteQueryBuilder->getState();
+    }
+
+    /**
+     * Gets the concrete implementation of the query builder
+     *
+     * @return \Doctrine\DBAL\Query\QueryBuilder
+     * @internal
+     */
+    public function getConcreteQueryBuilder(): \Doctrine\DBAL\Query\QueryBuilder
+    {
+        return $this->concreteQueryBuilder;
+    }
+
+    /**
+     * Executes this query using the bound parameters and their types.
+     *
+     * @return \Doctrine\DBAL\Driver\Statement|int
+     */
+    public function execute()
+    {
+        if ($this->getType() !== \Doctrine\DBAL\Query\QueryBuilder::SELECT) {
+            return $this->concreteQueryBuilder->execute();
+        }
+
+        // set additional query restrictions based on context & TCA config
+        $originalWhereConditions = $this->addAdditonalWhereConditions();
+
+        $result = $this->concreteQueryBuilder->execute();
+
+        // restore the original query conditions in case the user keeps
+        // on modifying the state.
+        $this->concreteQueryBuilder->add('where', $originalWhereConditions, false);
+
+        return $result;
+    }
+
+    /**
+     * Gets the complete SQL string formed by the current specifications of this QueryBuilder.
+     *
+     * If the statement is a SELECT TYPE query restrictions based on TCA settings will
+     * automatically be applied based on the current QuerySettings.
+     *
+     * @return string The SQL query string.
+     */
+    public function getSQL(): string
+    {
+        if ($this->getType() !== \Doctrine\DBAL\Query\QueryBuilder::SELECT) {
+            return $this->concreteQueryBuilder->getSQL();
+        }
+
+        // set additional query restrictions based on context & TCA config
+        $originalWhereConditions = $this->addAdditonalWhereConditions();
+
+        $sql = $this->concreteQueryBuilder->getSQL();
+
+        // restore the original query conditions in case the user keeps
+        // on modifying the state.
+        $this->concreteQueryBuilder->add('where', $originalWhereConditions, false);
+
+        return $sql;
+    }
+
+    /**
+     * Sets a query parameter for the query being constructed.
+     *
+     * @param string|int $key The parameter position or name.
+     * @param mixed $value The parameter value.
+     * @param string|null $type One of the Connection::PARAM_* constants.
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function setParameter($key, $value, string $type = null): QueryBuilder
+    {
+        $this->concreteQueryBuilder->setParameter($key, $value, $type);
+
+        return $this;
+    }
+
+    /**
+     * Sets a collection of query parameters for the query being constructed.
+     *
+     * @param array $params The query parameters to set.
+     * @param array $types The query parameters types to set.
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function setParameters(array $params, array $types = []): QueryBuilder
+    {
+        $this->concreteQueryBuilder->setParameters($params, $types);
+
+        return $this;
+    }
+
+    /**
+     * Gets all defined query parameters for the query being constructed indexed by parameter index or name.
+     *
+     * @return array The currently defined query parameters indexed by parameter index or name.
+     */
+    public function getParameters(): array
+    {
+        return $this->concreteQueryBuilder->getParameters();
+    }
+
+    /**
+     * Gets a (previously set) query parameter of the query being constructed.
+     *
+     * @param string|int $key The key (index or name) of the bound parameter.
+     *
+     * @return mixed The value of the bound parameter.
+     */
+    public function getParameter($key)
+    {
+        return $this->concreteQueryBuilder->getParameter($key);
+    }
+
+    /**
+     * Gets all defined query parameter types for the query being constructed indexed by parameter index or name.
+     *
+     * @return array The currently defined query parameter types indexed by parameter index or name.
+     */
+    public function getParameterTypes(): array
+    {
+        return $this->concreteQueryBuilder->getParameterTypes();
+    }
+
+    /**
+     * Gets a (previously set) query parameter type of the query being constructed.
+     *
+     * @param string|int $key The key (index or name) of the bound parameter type.
+     *
+     * @return mixed The value of the bound parameter type.
+     */
+    public function getParameterType($key)
+    {
+        return $this->concreteQueryBuilder->getParameterType($key);
+    }
+
+    /**
+     * Sets the position of the first result to retrieve (the "offset").
+     *
+     * @param int $firstResult The first result to return.
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function setFirstResult(int $firstResult): QueryBuilder
+    {
+        $this->concreteQueryBuilder->setFirstResult($firstResult);
+
+        return $this;
+    }
+
+    /**
+     * Gets the position of the first result the query object was set to retrieve (the "offset").
+     * Returns NULL if {@link setFirstResult} was not applied to this QueryBuilder.
+     *
+     * @return int The position of the first result.
+     */
+    public function getFirstResult(): int
+    {
+        return $this->concreteQueryBuilder->getFirstResult();
+    }
+
+    /**
+     * Sets the maximum number of results to retrieve (the "limit").
+     *
+     * @param int $maxResults The maximum number of results to retrieve.
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function setMaxResults(int $maxResults): QueryBuilder
+    {
+        $this->concreteQueryBuilder->setMaxResults($maxResults);
+
+        return $this;
+    }
+
+    /**
+     * Gets the maximum number of results the query object was set to retrieve (the "limit").
+     * Returns 0 if setMaxResults was not applied to this query builder.
+     *
+     * @return int The maximum number of results.
+     */
+    public function getMaxResults(): int
+    {
+        return (int)$this->concreteQueryBuilder->getMaxResults();
+    }
+
+    /**
+     * Either appends to or replaces a single, generic query part.
+     *
+     * The available parts are: 'select', 'from', 'set', 'where',
+     * 'groupBy', 'having' and 'orderBy'.
+     *
+     * @param string $sqlPartName
+     * @param string $sqlPart
+     * @param bool $append
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function add(string $sqlPartName, string $sqlPart, bool $append = false): QueryBuilder
+    {
+        $this->concreteQueryBuilder->add($sqlPartName, $sqlPart, $append);
+
+        return $this;
+    }
+
+    /**
+     * Specifies the item that is to be counted in the query result.
+     * Replaces any previously specified selections, if any.
+     *
+     * @param string $item Will be quoted according to database platform automatically.
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function count(string $item): QueryBuilder
+    {
+        $countExpr = $this->getConnection()->getDatabasePlatform()->getCountExpression(
+            $item === '*' ? $item : $this->quoteIdentifier($item)
+        );
+        $this->concreteQueryBuilder->select($countExpr);
+
+        return $this;
+    }
+
+    /**
+     * Specifies items that are to be returned in the query result.
+     * Replaces any previously specified selections, if any.
+     *
+     *
+     * @param string[] $selects
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function select(string ...$selects): QueryBuilder
+    {
+        $this->concreteQueryBuilder->select(...$this->quoteIdentifiersForSelect($selects));
+
+        return $this;
+    }
+
+    /**
+     * Adds an item that is to be returned in the query result.
+     *
+     * @param string[] $selects The selection expression.
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function addSelect(string ...$selects): QueryBuilder
+    {
+        $this->concreteQueryBuilder->addSelect(...$this->quoteIdentifiersForSelect($selects));
+
+        return $this;
+    }
+
+    /**
+     * Turns the query being built into a bulk delete query that ranges over
+     * a certain table.
+     *
+     * @param string $delete The table whose rows are subject to the deletion.
+     *                       Will be quoted according to database platform automatically.
+     * @param string $alias The table alias used in the constructed query.
+     *                      Will be quoted according to database platform automatically.
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function delete(string $delete, string $alias = null): QueryBuilder
+    {
+        $this->concreteQueryBuilder->delete(
+            $this->quoteIdentifier($delete),
+            empty($alias) ? $alias : $this->quoteIdentifier($alias)
+        );
+
+        return $this;
+    }
+
+    /**
+     * Turns the query being built into a bulk update query that ranges over
+     * a certain table
+     *
+     * @param string $update The table whose rows are subject to the update.
+     * @param string $alias The table alias used in the constructed query.
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function update(string $update, string $alias = null): QueryBuilder
+    {
+        $this->concreteQueryBuilder->update(
+            $this->quoteIdentifier($update),
+            empty($alias) ? $alias : $this->quoteIdentifier($alias)
+        );
+
+        return $this;
+    }
+
+    /**
+     * Turns the query being built into an insert query that inserts into
+     * a certain table
+     *
+     * @param string $insert The table into which the rows should be inserted.
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function insert(string $insert): QueryBuilder
+    {
+        $this->concreteQueryBuilder->insert($this->quoteIdentifier($insert));
+
+        return $this;
+    }
+
+    /**
+     * Creates and adds a query root corresponding to the table identified by the
+     * given alias, forming a cartesian product with any existing query roots.
+     *
+     * @param string $from The table. Will be quoted according to database platform automatically.
+     * @param string $alias The alias of the table. Will be quoted according to database platform automatically.
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function from(string $from, string $alias = null): QueryBuilder
+    {
+        $this->concreteQueryBuilder->from(
+            $this->quoteIdentifier($from),
+            empty($alias) ? $alias : $this->quoteIdentifier($alias)
+        );
+
+        return $this;
+    }
+
+    /**
+     * Creates and adds a join to the query.
+     *
+     * @param string $fromAlias The alias that points to a from clause.
+     * @param string $join The table name to join.
+     * @param string $alias The alias of the join table.
+     * @param string $condition The condition for the join.
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function join(string $fromAlias, string $join, string $alias, string $condition = null): QueryBuilder
+    {
+        $this->concreteQueryBuilder->innerJoin(
+            $this->quoteIdentifier($fromAlias),
+            $this->quoteIdentifier($join),
+            $this->quoteIdentifier($alias),
+            $condition
+        );
+
+        return $this;
+    }
+
+    /**
+     * Creates and adds a join to the query.
+     *
+     * @param string $fromAlias The alias that points to a from clause.
+     * @param string $join The table name to join.
+     * @param string $alias The alias of the join table.
+     * @param string $condition The condition for the join.
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function innerJoin(string $fromAlias, string $join, string $alias, string $condition = null): QueryBuilder
+    {
+        $this->concreteQueryBuilder->innerJoin(
+            $this->quoteIdentifier($fromAlias),
+            $this->quoteIdentifier($join),
+            $this->quoteIdentifier($alias),
+            $condition
+        );
+
+        return $this;
+    }
+
+    /**
+     * Creates and adds a left join to the query.
+     *
+     * @param string $fromAlias The alias that points to a from clause.
+     * @param string $join The table name to join.
+     * @param string $alias The alias of the join table.
+     * @param string $condition The condition for the join.
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function leftJoin(string $fromAlias, string $join, string $alias, string $condition = null): QueryBuilder
+    {
+        $this->concreteQueryBuilder->leftJoin(
+            $this->quoteIdentifier($fromAlias),
+            $this->quoteIdentifier($join),
+            $this->quoteIdentifier($alias),
+            $condition
+        );
+
+        return $this;
+    }
+
+    /**
+     * Creates and adds a right join to the query.
+     *
+     * @param string $fromAlias The alias that points to a from clause.
+     * @param string $join The table name to join.
+     * @param string $alias The alias of the join table.
+     * @param string $condition The condition for the join.
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function rightJoin(string $fromAlias, string $join, string $alias, string $condition = null): QueryBuilder
+    {
+        $this->concreteQueryBuilder->rightJoin(
+            $this->quoteIdentifier($fromAlias),
+            $this->quoteIdentifier($join),
+            $this->quoteIdentifier($alias),
+            $condition
+        );
+
+        return $this;
+    }
+
+    /**
+     * Sets a new value for a column in a bulk update query.
+     *
+     * @param string $key The column to set.
+     * @param string $value The value, expression, placeholder, etc.
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function set(string $key, $value): QueryBuilder
+    {
+        $this->concreteQueryBuilder->set(
+            $this->quoteIdentifier($key),
+            $value
+        );
+
+        return $this;
+    }
+
+    /**
+     * Specifies one or more restrictions to the query result.
+     * Replaces any previously specified restrictions, if any.
+     *
+     * @param mixed,... $predicates
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function where(...$predicates): QueryBuilder
+    {
+        $this->concreteQueryBuilder->where(...$predicates);
+
+        return $this;
+    }
+
+    /**
+     * Adds one or more restrictions to the query results, forming a logical
+     * conjunction with any previously specified restrictions.
+     *
+     * @param mixed,... $where The query restrictions.
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     *
+     * @see where()
+     */
+    public function andWhere(...$where): QueryBuilder
+    {
+        $this->concreteQueryBuilder->andWhere(...$where);
+
+        return $this;
+    }
+
+    /**
+     * Adds one or more restrictions to the query results, forming a logical
+     * disjunction with any previously specified restrictions.
+     *
+     * @param mixed,... $where The WHERE statement.
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     *
+     * @see where()
+     */
+    public function orWhere(...$where): QueryBuilder
+    {
+        $this->concreteQueryBuilder->orWhere(...$where);
+
+        return $this;
+    }
+
+    /**
+     * Specifies a grouping over the results of the query.
+     * Replaces any previously specified groupings, if any.
+     *
+     * @param mixed,... $groupBy The grouping expression.
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function groupBy(...$groupBy): QueryBuilder
+    {
+        $this->concreteQueryBuilder->groupBy(...$this->quoteIdentifiers($groupBy));
+
+        return $this;
+    }
+
+    /**
+     * Adds a grouping expression to the query.
+     *
+     * @param mixed,... $groupBy The grouping expression.
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function addGroupBy(...$groupBy)
+    {
+        $this->concreteQueryBuilder->addGroupBy(...$this->quoteIdentifiers($groupBy));
+
+        return $this;
+    }
+
+    /**
+     * Sets a value for a column in an insert query.
+     *
+     * @param string $column The column into which the value should be inserted.
+     * @param string $value The value that should be inserted into the column.
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function setValue(string $column, $value)
+    {
+        $this->concreteQueryBuilder->setValue(
+            $this->quoteIdentifier($column),
+            $value
+        );
+
+        return $this;
+    }
+
+    /**
+     * Specifies values for an insert query indexed by column names.
+     * Replaces any previous values, if any.
+     *
+     * @param array $values The values to specify for the insert query indexed by column names.
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function values(array $values)
+    {
+        $this->concreteQueryBuilder->values($this->quoteColumnValuePairs($values));
+
+        return $this;
+    }
+
+    /**
+     * Specifies a restriction over the groups of the query.
+     * Replaces any previous having restrictions, if any.
+     *
+     * @param mixed,... $having The restriction over the groups.
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function having(...$having): QueryBuilder
+    {
+        $this->concreteQueryBuilder->having(...$having);
+        return $this;
+    }
+
+    /**
+     * Adds a restriction over the groups of the query, forming a logical
+     * conjunction with any existing having restrictions.
+     *
+     * @param mixed,... $having The restriction to append.
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function andHaving(...$having): QueryBuilder
+    {
+        $this->concreteQueryBuilder->andHaving(...$having);
+
+        return $this;
+    }
+
+    /**
+     * Adds a restriction over the groups of the query, forming a logical
+     * disjunction with any existing having restrictions.
+     *
+     * @param mixed,... $having The restriction to add.
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function orHaving(...$having): QueryBuilder
+    {
+        $this->concreteQueryBuilder->orHaving(...$having);
+
+        return $this;
+    }
+
+    /**
+     * Specifies an ordering for the query results.
+     * Replaces any previously specified orderings, if any.
+     *
+     * @param string $fieldName The fieldName to order by. Will be quoted according to database platform automatically.
+     * @param string $order The ordering direction. No automatic quoting/escaping.
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function orderBy(string $fieldName, string $order = null): QueryBuilder
+    {
+        $this->concreteQueryBuilder->orderBy($this->connection->quoteIdentifier($fieldName), $order);
+
+        return $this;
+    }
+
+    /**
+     * Adds an ordering to the query results.
+     *
+     * @param string $fieldName The fieldName to order by. Will be quoted according to database platform automatically.
+     * @param string $order The ordering direction.
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function addOrderBy(string $fieldName, string $order = null): QueryBuilder
+    {
+        $this->concreteQueryBuilder->addOrderBy($this->connection->quoteIdentifier($fieldName), $order);
+
+        return $this;
+    }
+
+    /**
+     * Gets a query part by its name.
+     *
+     * @param string $queryPartName
+     *
+     * @return mixed
+     */
+    public function getQueryPart(string $queryPartName)
+    {
+        return $this->concreteQueryBuilder->getQueryPart($queryPartName);
+    }
+
+    /**
+     * Gets all query parts.
+     *
+     * @return array
+     */
+    public function getQueryParts(): array
+    {
+        return $this->concreteQueryBuilder->getQueryParts();
+    }
+
+    /**
+     * Resets SQL parts.
+     *
+     * @param array|null $queryPartNames
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function resetQueryParts(array $queryPartNames = null): QueryBuilder
+    {
+        $this->concreteQueryBuilder->resetQueryParts($queryPartNames);
+
+        return $this;
+    }
+
+    /**
+     * Resets a single SQL part.
+     *
+     * @param string $queryPartName
+     *
+     * @return QueryBuilder This QueryBuilder instance.
+     */
+    public function resetQueryPart($queryPartName): QueryBuilder
+    {
+        $this->concreteQueryBuilder->resetQueryPart($queryPartName);
+
+        return $this;
+    }
+
+    /**
+     * Gets a string representation of this QueryBuilder which corresponds to
+     * the final SQL query being constructed.
+     *
+     * @return string The string representation of this QueryBuilder.
+     */
+    public function __toString(): string
+    {
+        return $this->getSQL();
+    }
+
+    /**
+     * Creates a new named parameter and bind the value $value to it.
+     *
+     * This method provides a shortcut for PDOStatement::bindValue
+     * when using prepared statements.
+     *
+     * The parameter $value specifies the value that you want to bind. If
+     * $placeholder is not provided bindValue() will automatically create a
+     * placeholder for you. An automatic placeholder will be of the name
+     * ':dcValue1', ':dcValue2' etc.
+     *
+     * @param mixed $value
+     * @param int $type
+     * @param string $placeHolder The name to bind with. The string must start with a colon ':'.
+     *
+     * @return string the placeholder name used.
+     */
+    public function createNamedParameter($value, int $type = \PDO::PARAM_STR, string $placeHolder = null): string
+    {
+        return $this->concreteQueryBuilder->createNamedParameter($value, $type, $placeHolder);
+    }
+
+    /**
+     * Creates a new positional parameter and bind the given value to it.
+     *
+     * Attention: If you are using positional parameters with the query builder you have
+     * to be very careful to bind all parameters in the order they appear in the SQL
+     * statement , otherwise they get bound in the wrong order which can lead to serious
+     * bugs in your code.
+     *
+     * @param mixed $value
+     * @param int $type
+     *
+     * @return string
+     */
+    public function createPositionalParameter($value, int $type = \PDO::PARAM_STR)
+    {
+        return $this->concreteQueryBuilder->createPositionalParameter($value, $type);
+    }
+
+    /**
+     * Returns the WHERE clause "[tablename].[deleted-field] = 0" if a deleted-field
+     * is configured in $GLOBALS['TCA'] for the tablename $table
+     * This function should ALWAYS be called in the backend for selection on tables which
+     * are configured in $GLOBALS['TCA'] since it will ensure consistent selection of records,
+     * even if they are marked deleted (in which case the system must always treat them as non-existent!)
+     *
+     * @param string $table Table name present in $GLOBALS['TCA']
+     * @param string $tableAlias Table alias if any
+     * @return string WHERE clause for filtering out deleted records, eg " AND tablename.deleted=0
+     */
+    public function deleteConstraint(string $table, string $tableAlias = null): string
+    {
+        if (empty($GLOBALS['TCA'][$table]['ctrl']['delete'])) {
+            return '';
+        }
+
+        return $this->quoteIdentifier(($tableAlias ?? $table) . '.' . $GLOBALS['TCA'][$table]['ctrl']['delete']) . '=0';
+    }
+
+    /**
+     * Quotes a given input parameter.
+     *
+     * @param mixed $input The parameter to be quoted.
+     * @param string|null $type The type of the parameter.
+     *
+     * @return string The quoted parameter.
+     */
+    public function quote($input, string $type = null): string
+    {
+        return $this->getConnection()->quote($input, $type);
+    }
+
+    /**
+     * Quotes a string so it can be safely used as a table or column name, even if
+     * it is a reserved name.
+     *
+     * Delimiting style depends on the underlying database platform that is being used.
+     *
+     * @param string $identifier The name to be quoted.
+     *
+     * @return string The quoted name.
+     */
+    public function quoteIdentifier(string $identifier): string
+    {
+        return $this->getConnection()->quoteIdentifier($identifier);
+    }
+
+    /**
+     * Quotes an array of column names so it can be safely used, even if the name is a reserved name.
+     *
+     * Delimiting style depends on the underlying database platform that is being used.
+     *
+     * @param array $input
+     *
+     * @return array
+     */
+    public function quoteIdentifiers(array $input): array
+    {
+        return $this->getConnection()->quoteIdentifiers($input);
+    }
+
+    /**
+     * Quotes an array of column names so it can be safely used, even if the name is a reserved name.
+     * Takes into account the special case of the * placeholder that can only be used in SELECT type
+     * statements.
+     *
+     * Delimiting style depends on the underlying database platform that is being used.
+     *
+     * @param array $input
+     *
+     * @return array
+     */
+    public function quoteIdentifiersForSelect(array $input): array
+    {
+        // The SQL * operator must not be quoted. As it can only occur either by itself
+        // or preceded by a tablename (tablename.*) check if the last character of a select
+        // expression is the * and quote only prepended table name. In all other cases the
+        // full expression is being quoted.
+        foreach ($input as &$select) {
+            if (substr($select, -2) === '.*') {
+                $select = $this->quoteIdentifier(substr($select, 0, -2)) . '.*';
+            } elseif ($select !== '*') {
+                $select = $this->quoteIdentifier($select);
+            }
+        }
+        return $input;
+    }
+
+    /**
+     * Quotes an associative array of column-value so the column names can be safely used, even
+     * if the name is a reserved name.
+     *
+     * Delimiting style depends on the underlying database platform that is being used.
+     *
+     * @param array $input
+     *
+     * @return array
+     */
+    public function quoteColumnValuePairs(array $input): array
+    {
+        return $this->getConnection()->quoteColumnValuePairs($input);
+    }
+
+    /**
+     * Unquote a single identifier (no dot expansion). Used to unquote the table names
+     * from the expressionBuilder so that the table can be found in the TCA definition.
+     *
+     * @param string $identifier The identifier / table name
+     * @return string The unquoted table name / identifier
+     */
+    protected function unquoteSingleIdentifier(string $identifier): string
+    {
+        $quoteChar = $this->getConnection()
+            ->getDatabasePlatform()
+            ->getIdentifierQuoteCharacter();
+
+        $unquotedIdentifier = trim($identifier, $quoteChar);
+
+        return str_replace($quoteChar . $quoteChar, $quoteChar, $unquotedIdentifier);
+    }
+
+    /**
+     * Return all tables/aliases used in FROM or JOIN query parts from the query builder.
+     *
+     * The table names are automatically unquoted. This is a helper for to build the list
+     * of queried tables for the QueryRestrictionBuilder.
+     *
+     * @return string[]
+     */
+    protected function getQueriedTables(): array
+    {
+        $queriedTables = [];
+
+        // Loop through all FROM tables
+        foreach ($this->getQueryPart('from') as $from) {
+            $tableName = $this->unquoteSingleIdentifier($from['table']);
+            $tableAlias = isset($from['alias']) ? $this->unquoteSingleIdentifier($from['alias']) : null;
+            $queriedTables[$tableName] = $tableAlias;
+        }
+
+        // Loop through all JOIN tables
+        foreach ($this->getQueryPart('join') as $fromTable => $joins) {
+            foreach ($joins as $join) {
+                $tableName = $this->unquoteSingleIdentifier($join['joinTable']);
+                $tableAlias = isset($join['joinAlias']) ? $this->unquoteSingleIdentifier($join['joinAlias']) : null;
+                $queriedTables[$tableName] = $tableAlias;
+            }
+        }
+
+        return $queriedTables;
+    }
+
+    /**
+     * Add the additional query conditions returned by the QueryRestrictionBuilder
+     * to the current query and return the original set of conditions so that they
+     * can be restored after the query has been built/executed.
+     *
+     * @return \Doctrine\DBAL\Query\Expression\CompositeExpression|mixed
+     */
+    protected function addAdditonalWhereConditions()
+    {
+        $queryRestrictionBuilder = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            $this->getQueriedTables(),
+            $this->expr(),
+            $this->getQueryContext()
+        );
+
+        $originalWhereConditions = $this->concreteQueryBuilder->getQueryPart('where');
+        if ($originalWhereConditions instanceof CompositeExpression) {
+            $originalWhereConditions = clone($originalWhereConditions);
+        }
+
+        $additionalQueryRestrictions = $queryRestrictionBuilder->getVisibilityConstraints();
+
+        if ($additionalQueryRestrictions->count() !== 0) {
+            // save the original query conditions so we can restore
+            // them after the query has been built.
+
+            $this->concreteQueryBuilder->andWhere($additionalQueryRestrictions);
+        }
+
+        return $originalWhereConditions;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Query/QueryContext.php b/typo3/sysext/core/Classes/Database/Query/QueryContext.php
new file mode 100644 (file)
index 0000000..fbd3d8f
--- /dev/null
@@ -0,0 +1,662 @@
+<?php
+declare (strict_types = 1);
+namespace TYPO3\CMS\Core\Database\Query;
+
+/*
+ * 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!
+ */
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Frontend\Page\PageRepository;
+
+/**
+ * TYPO3 / TCA specific query settings that deal with enable/hidden fields,
+ * frontend groups and start-/endtimes.
+ */
+class QueryContext
+{
+    /**
+     * Global settings
+     *
+     * - showHidden 0/1/-1 => -1 = Inherit from TSFE, based on the table being queried :(
+     *      Effectively requires showHiddenPages & showHiddenRecords from TSFE
+     *      *** Only Frontend ***
+     * - isVersioningPreview => Inherited from PageRepository/TSFE
+     *      If unset filters out records not in DEFAULT versioning state
+     *      If set shows records from the live and the current Workspace (from TSFE!), ignored for table pages
+     *
+     *      => Context effectively only needs the workspace id to do its magic!
+     *
+     * - noVersionPreview => External parameter
+     *      Only used in frontend context, eliminate restrictions from isVersioningPreview
+     *      *** Only Frontend ***
+     * - includeDeleted => External parameter
+     *      Always set for frontend context
+     *      *** Only Backend ***
+     * - fe_user groups list from TSFE
+     *      *** Only Frontend ***
+     * - ignoreEnableFields
+     *      Boolean to select if enable columns are to be checked
+     *      *** Frontend/Backend ***
+     * - enableFieldsToBeIgnored
+     *      List of fields to not use for restriction building
+     *      *** Only Frontend ***
+     *
+     * TSFE Dependencies
+     *
+     * - showHidden
+     *      Inherited from TSFE when -1, depends on the showHiddenPages & showHiddenRecords flags
+     *      to decide the final setting
+     * - isVersioningPreview
+     *      Inherited from PageRepository
+     * - currentWorkspaceId
+     *      Inherited from PageRepository
+     * TCA Dependencies
+     *
+     * - Deleted Field (string or null)
+     * - Enable Columns (array of strings)
+     * - Versioning Information (boolean)
+     *
+     * Goal:
+     *
+     * - QueryRestrictionBuilder needs no Access to TSFE / PageRepository / TCA
+     * - Based on the information in the context object the restrictions will be built.
+     * - Each restriction should be controllable per table.
+     *
+     *
+     * RequireMents
+     *  - Global flags
+     *      showHidden
+     *      includeDeleted
+     *      isVersioningPreview
+     *      noVersionPreview
+     *      currentWorkSpaceId
+     *      frontendUserGroups
+     *      ignoreEnableFields
+     *      enableFieldsToBeIgnored
+     *
+     *  - Per Table
+     *      showHidden => enableField, included in per Table enable field list
+     *      includeDeleted => treat as enableField?
+     *      frontendUserGroups => included in per Table enableFieldList
+     */
+
+    /**
+     * The context for which the restraints are to be built.
+     *
+     * @var QueryContextType
+     */
+    protected $context;
+
+    /**
+     * @var int[]
+     */
+    protected $memberGroups = null;
+
+    /**
+     * @var int
+     */
+    protected $currentWorkspace = null;
+
+    /**
+     * @var int
+     */
+    protected $accessTime = null;
+
+    /**
+     * Global flag if hidden records are to be included in the query result.
+     *
+     * In PageRepository::enableFields() this is called showHidden
+     *
+     * @var bool
+     */
+    protected $includeHidden = null;
+
+    /**
+     * Global flag if deleted records are to be included in the query result.
+     *
+     * @var bool
+     */
+    protected $includeDeleted = false;
+
+    /**
+     * Per table flag if deleted records are to be included in the query result.
+     *
+     * @var array
+     */
+    protected $includeDeletedForTable = [];
+
+    /**
+     * Global flag if records in a non-default versioned state should be
+     * included in the query results.
+     *
+     * In PageRepository the flag is called versioningPreview
+     *
+     * @var bool
+     */
+    protected $includePlaceholders = null;
+
+    /**
+     * Global flag if versioned records are to be included in the query result.
+     * Also influences if enable fields are respected for the query.
+     *
+     * In PageRepository the flag is called noVersionPreview
+     *
+     * @var bool
+     */
+    protected $includeVersionedRecords = false;
+
+    /**
+     * Global flag if enable fields are going to be checked for the query.
+     *
+     * @var bool
+     */
+    protected $ignoreEnableFields = false;
+
+    /**
+     * Global list of enable columns that are not checked for the query.
+     * This list is only checked if $ignoreEnableFields is enabled.
+     *
+     * @var string[]
+     */
+    protected $ignoredEnableFields = [];
+
+    /**
+     * Per table list of enable columns that are not checked for the query.
+     * This list is only checked if $ignoreEnableFields is enabled.
+     *
+     * @var string[]
+     */
+    protected $ignoredEnableFieldsForTable = []; // Per Table list of ignored columns
+
+    /**
+     * Associative array of table configs to override the TCA definition. If a table
+     * is not configured here the setup information from the TCA will be used.
+     *
+     * The array key is the table name, the value is in the format
+     * [
+     *   'deleted' => 'fieldName',
+     *   'versioningWS' => true,
+     *   'enablecolumns' => [ 'disabled' => hidden, ... ]
+     * ]
+     *
+     * @var array
+     */
+    protected $tableConfigs = [];
+
+    /**
+     * QueryContext constructor.
+     *
+     * @param string $context A valid QueryContextType
+     */
+    public function __construct(string $context = QueryContextType::AUTO)
+    {
+        $this->context = GeneralUtility::makeInstance(QueryContextType::class, $context);
+    }
+
+    /**
+     * @return string
+     */
+    public function getContext(): string
+    {
+        if ($this->context->equals(QueryContextType::AUTO)) {
+            if (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_FE) {
+                return QueryContextType::FRONTEND;
+            } elseif (TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_BE) {
+                return QueryContextType::BACKEND;
+            } else {
+                return QueryContextType::NONE;
+            }
+        }
+
+        return (string)$this->context;
+    }
+
+    /**
+     * Set the context in which the query is going to be run.
+     * Used by the QueryRestrictionBuilder to determine the restrictions to be placed.
+     *
+     * @param string $context
+     * @return \TYPO3\CMS\Core\Database\Query\QueryContext
+     */
+    public function setContext(string $context): QueryContext
+    {
+        $this->context = GeneralUtility::makeInstance(QueryContextType::class, $context);
+
+        return $this;
+    }
+
+    /**
+     * Get a list of member groups (fe_groups) that will be used in when building
+     * query restrictions in FE context.
+     *
+     * @return int[]
+     */
+    public function getMemberGroups(): array
+    {
+        // If the member groups have not been explicitly set
+        // the group list from the frontend controller context
+        // will be inherited
+        if ($this->memberGroups === null) {
+            $this->memberGroups = GeneralUtility::intExplode(
+                ',',
+                $this->getTypoScriptFrontendController()->gr_list,
+                true
+            );
+        }
+
+        return (array)$this->memberGroups;
+    }
+
+    /**
+     * Set the member groups that will be checked in frontend context.
+     *
+     * @param int[] $memberGroups
+     * @return \TYPO3\CMS\Core\Database\Query\QueryContext
+     */
+    public function setMemberGroups(array $memberGroups): QueryContext
+    {
+        $this->memberGroups = $memberGroups;
+
+        return $this;
+    }
+
+    /**
+     * Get the current workspace. If not actively defined it will fall back
+     * to the current workspace set in the PageRepository.
+     *
+     * @return int
+     */
+    public function getCurrentWorkspace(): int
+    {
+        return $this->currentWorkspace ?? (int)$this->getPageRepository()->versioningWorkspaceId;
+    }
+
+    /**
+     * Set the current workspace id.
+     *
+     * @param int $currentWorkspace
+     * @return \TYPO3\CMS\Core\Database\Query\QueryContext
+     */
+    public function setCurrentWorkspace(int $currentWorkspace): QueryContext
+    {
+        $this->currentWorkspace = $currentWorkspace;
+
+        return $this;
+    }
+
+    /**
+     * Return the current accesstime. If not explictly set fall back to the
+     * value of $GLOBALS['SIM_ACCESS_TIME']
+     *
+     * @return int
+     */
+    public function getAccessTime(): int
+    {
+        if ($this->accessTime === null) {
+            return empty($GLOBALS['SIM_ACCESS_TIME']) ? 0 : (int)$GLOBALS['SIM_ACCESS_TIME'];
+        }
+
+        return $this->accessTime;
+    }
+
+    /**
+     * Set the current access time.
+     *
+     * @param int $accessTime
+     * @return \TYPO3\CMS\Core\Database\Query\QueryContext
+     */
+    public function setAccessTime(int $accessTime): QueryContext
+    {
+        $this->accessTime = $accessTime;
+
+        return $this;
+    }
+
+    /**
+     * Returns the global setting wether hidden records should be included
+     * in the query result. Preferrably getIncludeHiddenForTable() should
+     * be used as the proper information from TSFE can be inherited by
+     * using the table name information.
+     *
+     * Defaults to false in case the flag has not been explictly set.
+     *
+     * @return bool
+     * @internal
+     */
+    public function getIncludeHidden(): bool
+    {
+        // Casting to bool to accomodate for the legacy fallback:
+        // When showHidden has not been explicitly set it's going to
+        // be determined by the settings in the TyposcriptFrontendController.
+        // As we don't now the table being queried here it's better to use
+        // getIncludeHiddenForTable()
+        return (bool)$this->includeHidden;
+    }
+
+    /**
+     * Flag if hidden records for the given table should be included in the query result.
+     * If $includeHidden has not been explictly set the information from TSFE will be
+     * used to determine the setting.
+     *
+     * @param string $table
+     * @return bool
+     */
+    public function getIncludeHiddenForTable(string $table): bool
+    {
+        if ($this->includeHidden === null && is_object($this->getTypoScriptFrontendController())) {
+            $showHidden = $table === 'pages' || $table === 'pages_language_overlay'
+                ? $this->getTypoScriptFrontendController()->showHiddenPage
+                : $this->getTypoScriptFrontendController()->showHiddenRecords;
+
+            if ($showHidden === -1) {
+                $showHidden = false;
+            }
+
+            $this->includeHidden = (bool)$showHidden;
+        }
+
+        return (bool)$this->includeHidden;
+    }
+
+    /**
+     * Set if hidden records should be part of the query result set.
+     *
+     * @param bool $includeHidden
+     * @return \TYPO3\CMS\Core\Database\Query\QueryContext
+     */
+    public function setIncludeHidden(bool $includeHidden): QueryContext
+    {
+        $this->includeHidden = $includeHidden;
+
+        return $this;
+    }
+
+    /**
+     * Get if deleted records should be part of the query result set at all.
+     *
+     * @return bool
+     */
+    public function getIncludeDeleted(): bool
+    {
+        return $this->includeDeleted;
+    }
+
+    /**
+     * Set wether deleted records shoult be part of the query result.
+     *
+     * @param bool $includeDeleted
+     * @return \TYPO3\CMS\Core\Database\Query\QueryContext
+     */
+    public function setIncludeDeleted(bool $includeDeleted): QueryContext
+    {
+        $this->includeDeleted = $includeDeleted;
+
+        return $this;
+    }
+
+    /**
+     * Get if records in a non-default versioning state should be part of the query result set.
+     *
+     * @return bool
+     */
+    public function getIncludePlaceholders(): bool
+    {
+        if ($this->includePlaceholders === null) {
+            $this->includePlaceholders = $this->getPageRepository()->versioningPreview;
+        }
+
+        return (bool)$this->includePlaceholders;
+    }
+
+    /**
+     * Set if records in a non-default versioning state should be part of the query result set.
+     *
+     * @param bool $includePlaceholders
+     * @return \TYPO3\CMS\Core\Database\Query\QueryContext
+     */
+    public function setIncludePlaceholders(bool $includePlaceholders): QueryContext
+    {
+        $this->includePlaceholders = $includePlaceholders;
+
+        return $this;
+    }
+
+    /**
+     * Get if versioned records shoult be part of the query result set.
+     *
+     * @return bool
+     */
+    public function getIncludeVersionedRecords(): bool
+    {
+        return $this->includeVersionedRecords;
+    }
+
+    /**
+     * Set if versioned records should be part of the query result set.
+     *
+     * @param bool $includeVersionedRecords
+     * @return \TYPO3\CMS\Core\Database\Query\QueryContext
+     */
+    public function setIncludeVersionedRecords(bool $includeVersionedRecords): QueryContext
+    {
+        $this->includeVersionedRecords = $includeVersionedRecords;
+
+        return $this;
+    }
+
+    /**
+     * Get if enable fields should be ignored for this query.
+     *
+     * @return bool
+     */
+    public function getIgnoreEnableFields(): bool
+    {
+        return $this->ignoreEnableFields;
+    }
+
+    /**
+     * Set if enable fields should be ignored for this query.
+     *
+     * @param bool $ignoreEnableFields
+     * @return \TYPO3\CMS\Core\Database\Query\QueryContext
+     */
+    public function setIgnoreEnableFields(bool $ignoreEnableFields): QueryContext
+    {
+        $this->ignoreEnableFields = $ignoreEnableFields;
+
+        return $this;
+    }
+
+    /**
+     * Return global list of ignored enable columns for the query.
+     * Can be overridden per table. Only checked if $ignoreEnableFields is enabled.
+     *
+     * @return string[]
+     */
+    public function getIgnoredEnableFields()
+    {
+        return $this->ignoredEnableFields;
+    }
+
+    /**
+     * Set the global list of ignored enable columns.
+     *
+     * @param string[] $ignoredEnableFields
+     * @return \TYPO3\CMS\Core\Database\Query\QueryContext
+     */
+    public function setIgnoredEnableFields($ignoredEnableFields): QueryContext
+    {
+        $this->ignoredEnableFields = $ignoredEnableFields;
+
+        return $this;
+    }
+
+    /**
+     * Get the ignored enable columns for this table.
+     * If no specific list has been defined the global list will be returned.
+     *
+     * @param string $table
+     * @return string[]
+     */
+    public function getIgnoredEnableFieldsForTable(string $table): array
+    {
+        if (isset($this->ignoredEnableFieldsForTable[$table])) {
+            return $this->ignoredEnableFieldsForTable[$table];
+        } elseif (!empty($this->ignoredEnableFields)) {
+            return $this->ignoredEnableFields;
+        }
+
+        return [];
+    }
+
+    /**
+     * @param string $table
+     * @param string[] $ignoredEnableFieldsForTable
+     * @return \TYPO3\CMS\Core\Database\Query\QueryContext
+     */
+    public function setIgnoredEnableFieldsForTable(string $table, array $ignoredEnableFieldsForTable): QueryContext
+    {
+        $this->ignoredEnableFieldsForTable[$table] = $ignoredEnableFieldsForTable;
+
+        return $this;
+    }
+
+    /**
+     * Get if deleted records for this table should be included in the query result set.
+     *
+     * @param string $table
+     * @return bool
+     */
+    public function getIncludeDeletedForTable(string $table): bool
+    {
+        return $this->includeDeletedForTable[$table] ?? false;
+    }
+
+    /**
+     * Set if deleted records for this table should be included in the query result.
+     *
+     * @param string $table
+     * @param bool $includeDeletedForTable
+     * @return \TYPO3\CMS\Core\Database\Query\QueryContext
+     */
+    public function setIncludeDeletedForTable(string $table, bool $includeDeletedForTable): QueryContext
+    {
+        $this->includeDeletedForTable[$table] = $includeDeletedForTable;
+
+        return $this;
+    }
+
+    /**
+     * Get the table configuration information for all tables.
+     *
+     * @return array
+     */
+    public function getTableConfigs(): array
+    {
+        return $this->tableConfigs;
+    }
+
+    /**
+     * Set the table configuration for all tables.
+     *
+     * @param array $tableConfigs
+     * @return \TYPO3\CMS\Core\Database\Query\QueryContext
+     */
+    public function setTableConfigs(array $tableConfigs): QueryContext
+    {
+        $this->tableConfigs = $tableConfigs;
+
+        return $this;
+    }
+
+    /**
+     * Get the table configuration for a single table.
+     *
+     * @param string $table
+     * @return array
+     */
+    public function getTableConfig(string $table): array
+    {
+        return $this->tableConfigs[$table] ?? $this->getTcaDefiniton($table);
+    }
+
+    /**
+     * Get the TCA definition for a tables and extract the relevant parts
+     * of the table configuration.
+     *
+     * @param string $table
+     * @return array
+     */
+    protected function getTcaDefiniton(string $table): array
+    {
+        $ctrlDefiniton = $GLOBALS['TCA'][$table]['ctrl'] ?? [];
+        return array_intersect_key(
+            $ctrlDefiniton,
+            ['delete' => true, 'versioningWS' => true, 'enablecolumns' => true]
+        );
+    }
+
+    /**
+     * Add a table configuration entry to the table config array.
+     *
+     * @param string $table
+     * @param string $deletedField
+     * @param bool $versioningSupport
+     * @param array $enableColumns
+     * @return \TYPO3\CMS\Core\Database\Query\QueryContext
+     */
+    public function addTableConfig(
+        string $table,
+        string $deletedField = null,
+        bool $versioningSupport = false,
+        array $enableColumns = []
+    ): QueryContext {
+        $this->tableConfigs[$table] = [
+            'deleted' => $deletedField,
+            'versioningWS' => $versioningSupport,
+            'enablecolumns' => $enableColumns
+        ];
+    }
+
+    /**
+     * Remove a table override from the config array.
+     *
+     * @param string $table
+     * @return \TYPO3\CMS\Core\Database\Query\QueryContext
+     */
+    public function removeTableConfig(string $table): QueryContext
+    {
+        unset($this->tableConfigs[$table]);
+
+        return $this;
+    }
+
+    /**
+     * @return \TYPO3\CMS\Frontend\Page\PageRepository
+     */
+    protected function getPageRepository(): PageRepository
+    {
+        if ($this->getContext() === QueryContextType::FRONTEND && is_object($this->getTypoScriptFrontendController())) {
+            return $this->getTypoScriptFrontendController()->sys_page;
+        } else {
+            return GeneralUtility::makeInstance(PageRepository::class);
+        }
+    }
+
+    /**
+     * @return \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
+     */
+    protected function getTypoScriptFrontendController()
+    {
+        return $GLOBALS['TSFE'];
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Query/QueryContextType.php b/typo3/sysext/core/Classes/Database/Query/QueryContextType.php
new file mode 100644 (file)
index 0000000..f716411
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+declare (strict_types = 1);
+namespace TYPO3\CMS\Core\Database\Query;
+
+/*
+ * 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!
+ */
+
+/**
+ * Enumeration object for query context type
+ *
+ */
+class QueryContextType extends \TYPO3\CMS\Core\Type\Enumeration
+{
+    const __default = self::AUTO;
+
+    /**
+     * Constants reflecting the query context type
+     */
+    const AUTO = 'AUTO';
+    const NONE = 'NONE';
+    const FRONTEND = 'FRONTEND';
+    const BACKEND = 'BACKEND';
+
+    /**
+     * @param mixed $type
+     */
+    public function __construct($type = null)
+    {
+        if ($type !== null) {
+            $type = strtoupper((string)$type);
+        }
+
+        parent::__construct($type);
+    }
+}
diff --git a/typo3/sysext/core/Classes/Database/Query/QueryRestrictionBuilder.php b/typo3/sysext/core/Classes/Database/Query/QueryRestrictionBuilder.php
new file mode 100644 (file)
index 0000000..d46da94
--- /dev/null
@@ -0,0 +1,371 @@
+<?php
+declare (strict_types = 1);
+namespace TYPO3\CMS\Core\Database\Query;
+
+/*
+ * 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!
+ */
+
+use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
+use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Versioning\VersionState;
+
+/**
+ * Builder for SQL query constraints based on TCA settings.
+ * The resulting composite expressions can be added to a query
+ * being built using the QueryBuilder object.
+ *
+ * The restrictions being built by this class are to be used for all
+ * select queries done by the QueryBuilder to avoid returning data
+ * that should not be available to the caller based on the current
+ * TYPO3 context.
+ *
+ * Restrictions that will be created can be configured using the
+ * QuerySettings on the main QueryBuilder object.
+ *
+ * WARNING: This code has cross cutting concerns as it requires access
+ * to the TypoScriptFrontEndController and $GLOBALS['TCA'] to build the
+ * right queries.
+ */
+class QueryRestrictionBuilder
+{
+    /**
+     * @var \TYPO3\CMS\Frontend\Page\PageRepository
+     */
+    protected $pageRepository;
+
+    /**
+     * @var \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
+     */
+    protected $expressionBuilder;
+
+    /**
+     * @var \TYPO3\CMS\Core\Database\Query\QueryContext
+     */
+    protected $queryContext;
+
+    /**
+     * @var string[]
+     */
+    protected $queriedTables = [];
+
+    /**
+     * Initializes a new QueryBuilder.
+     *
+     * @param string[] $queriedTables
+     * @param ExpressionBuilder $expressionBuilder The ExpressionBuilder with which to create restrictions
+     * @param \TYPO3\CMS\Core\Database\Query\QueryContext $queryContext
+     */
+    public function __construct(
+        array $queriedTables,
+        ExpressionBuilder $expressionBuilder,
+        QueryContext $queryContext = null
+    ) {
+        $this->queriedTables = $queriedTables;
+        $this->expressionBuilder = $expressionBuilder;
+        $this->queryContext = $queryContext ?? GeneralUtility::makeInstance(QueryContext::class);
+    }
+
+    /**
+     * Returns a composite expression to add visibility restrictions for
+     * the selected tables based on the current context (FE/BE).
+     *
+     * You need to check if any conditions are added to the CompositeExpression
+     * before adding it to your query using `->count()`.
+     *
+     * @return CompositeExpression
+     */
+    public function getVisibilityConstraints(): CompositeExpression
+    {
+        switch ($this->queryContext->getContext()) {
+            case QueryContextType::FRONTEND:
+                return $this->getFrontendVisibilityRestrictions();
+            case QueryContextType::BACKEND:
+                return $this->getBackendVisibilityConstraints();
+            case QueryContextType::NONE:
+                return $this->expressionBuilder->andX();
+            default:
+                throw new \RuntimeException(
+                    'Unknown TYPO3 Context / Request type: "' . TYPO3_REQUESTTYPE . '".',
+                    1459708283
+                );
+        }
+    }
+
+    /**
+     * Returns a composite expression takeing into account visibility restrictions
+     * imposed by enableFields, versioning/workspaces and deletion.
+     *
+     * @return \TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression
+     * @throws \LogicException
+     */
+    protected function getFrontendVisibilityRestrictions(): CompositeExpression
+    {
+        $queryContext = $this->queryContext;
+        $ignoreEnableFields = $queryContext->getIgnoreEnableFields();
+        $includeDeleted = $queryContext->getIncludeDeleted();
+
+        if (!$ignoreEnableFields && $includeDeleted) {
+            throw new \LogicException(
+                'The query settings "ignoreEnableFields=FALSE" and "includeDeleted=TRUE" can not be used together '
+                . 'in frontend context.',
+                1459690516
+            );
+        }
+
+        $constraints = [];
+        foreach ($this->queriedTables as $tableName => $tableAlias) {
+            $tableConfig = $queryContext->getTableConfig($tableName);
+            if (!$ignoreEnableFields && !$includeDeleted) {
+                $constraint = $this->getEnableFieldConstraints(
+                    $tableName,
+                    $tableAlias,
+                    $queryContext->getIncludeHiddenForTable($tableName),
+                    [],
+                    $queryContext->getIncludeVersionedRecords()
+                );
+                if ($constraint->count() !== 0) {
+                    $constraints[] = $constraint;
+                }
+            } elseif ($ignoreEnableFields && !$includeDeleted) {
+                if (!empty($queryContext->getIgnoredEnableFieldsForTable($tableName))) {
+                    $constraint = $this->getEnableFieldConstraints(
+                        $tableName,
+                        $tableAlias,
+                        $queryContext->getIncludeHiddenForTable($tableName),
+                        $queryContext->getIgnoredEnableFieldsForTable($tableName),
+                        $queryContext->getIncludeVersionedRecords()
+                    );
+                    if ($constraint->count() !== 0) {
+                        $constraints[] = $constraint;
+                    }
+                } elseif (!empty($tableConfig['delete'])) {
+                    $tablePrefix = empty($tableAlias) ? $tableName : $tableAlias;
+                    $constraints[] = $this->expressionBuilder->eq(
+                        $tablePrefix . '.' . $tableConfig['delete'],
+                        0
+                    );
+                }
+            }
+        }
+
+        return $this->expressionBuilder->andX(...$constraints);
+    }
+
+    /**
+     * Returns a composite expression to restrict access to records for the backend context.
+     *
+     * @return CompositeExpression
+     * @todo: Lots of code duplication, check how/if this can be merged with the "getEnableFieldConstraints"
+     * @todo: after the test cases are done for backend and frontend.
+     */
+    protected function getBackendVisibilityConstraints(): CompositeExpression
+    {
+        $queryContext = $this->queryContext;
+        $ignoreEnableFields = $queryContext->getIgnoreEnableFields();
+        $includeDeleted = $queryContext->getIncludeDeleted();
+
+        $constraints = [];
+        $expressionBuilder = $this->expressionBuilder;
+
+        foreach ($this->queriedTables as $tableName => $tableAlias) {
+            $tableConfig = $queryContext->getTableConfig($tableName);
+            $tablePrefix = empty($tableAlias) ? $tableName : $tableAlias;
+
+            if (empty($tableConfig)) {
+                // No restrictions for this table, not configured by TCA
+                continue;
+            }
+
+            if (!$ignoreEnableFields && is_array($tableConfig['enablecolumns'])) {
+                $enableColumns = $tableConfig['enablecolumns'];
+
+                if (isset($enableColumns['disabled'])) {
+                    $constraints[] = $expressionBuilder->eq(
+                        $tablePrefix . '.' . $enableColumns['disabled'],
+                        0
+                    );
+                }
+                if ($enableColumns['starttime']) {
+                    $constraints[] = $expressionBuilder->lte(
+                        $tablePrefix . '.' . $enableColumns['starttime'],
+                        $queryContext->getAccessTime()
+                    );
+                }
+                if ($enableColumns['endtime']) {
+                    $fieldName = $tablePrefix . '.' . $enableColumns['endtime'];
+                    $constraints[] = $expressionBuilder->orX(
+                        $expressionBuilder->eq($fieldName, 0),
+                        $expressionBuilder->gt($fieldName, $queryContext->getAccessTime())
+                    );
+                }
+            }
+
+            if (!$includeDeleted && !empty($tableConfig['delete'])) {
+                $tablePrefix = empty($tableAlias) ? $tableName : $tableAlias;
+                $constraints[] = $this->expressionBuilder->eq(
+                    $tablePrefix . '.' . $tableConfig['delete'],
+                    0
+                );
+            }
+        }
+
+        return $expressionBuilder->andX(...$constraints);
+    }
+
+    /**
+     * @param string $tableName The table name to query
+     * @param string|null $tableAlias The table alias to use for constraints. $tableName used when empty.
+     * @param bool $showHidden Select hidden records
+     * @param string[] $ignoreFields Names of enable columns to be ignored
+     * @param bool $noVersionPreview If set, enableFields will be applied regardless of any versioning preview
+     *                               settings which might otherwise disable enableFields
+     * @return \TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression
+     */
+    protected function getEnableFieldConstraints(
+        string $tableName,
+        string $tableAlias = null,
+        bool $showHidden = false,
+        array $ignoreFields = [],
+        bool $noVersionPreview = false
+    ): CompositeExpression {
+        $queryContext = $this->queryContext;
+        $tableConfig = $queryContext->getTableConfig($tableName);
+
+        if (empty($tableConfig)) {
+            // No restrictions for this table, not configured by TCA
+            return $this->expressionBuilder->andX();
+        }
+
+        $tablePrefix = empty($tableAlias) ? $tableName : $tableAlias;
+
+        $constraints = [];
+        $expressionBuilder = $this->expressionBuilder;
+
+        // Restrict based on deleted flag of records
+        if (!empty($tableConfig['delete'])) {
+            $constraints[] = $expressionBuilder->eq($tablePrefix . '.deleted', 0);
+        }
+
+        // Restrict based on Workspaces / Versioning
+        if (!empty($tableConfig['versioningWS'])) {
+            if (!$queryContext->getIncludePlaceholders()) {
+                // Filter out placeholder records (new/moved/deleted items) in case we are NOT in a versioning preview
+                // (This means that means we are online!)
+                $constraints[] = $expressionBuilder->lte(
+                    $tablePrefix . '.t3ver_state',
+                    new VersionState(VersionState::DEFAULT_STATE)
+                );
+            } elseif ($tableName !== 'pages') {
+                // Show only records of the live and current workspace in case we are in a versioning preview
+                $constraints[] = $expressionBuilder->orX(
+                    $expressionBuilder->eq($tablePrefix . '.t3ver_wsid', 0),
+                    $expressionBuilder->eq($tablePrefix . '.t3ver_wsid', $queryContext->getCurrentWorkspace())
+                );
+            }
+
+            // Filter out versioned records
+            if (!$noVersionPreview && !in_array('pid', $ignoreFields)) {
+                $constraints[] = $expressionBuilder->neq($tablePrefix . '.pid', -1);
+            }
+        }
+
+        // Restrict based on enable fields. In case of versioning-preview, enableFields are ignored
+        // and later checked in versionOL().
+        if (is_array($tableConfig['enablecolumns'])
+            && (!$queryContext->getIncludePlaceholders() || empty($tableConfig['versioningWS']) || $noVersionPreview)
+        ) {
+            $enableColumns = $tableConfig['enablecolumns'];
+
+            // Filter out disabled records
+            if (isset($enableColumns['disabled']) && !$showHidden && !in_array('disabled', $ignoreFields)) {
+                $constraints[] = $expressionBuilder->eq(
+                    $tablePrefix . '.' . $enableColumns['disabled'],
+                    0
+                );
+            }
+
+            // Filter out records where the starttime has not yet been reached.
+            if (isset($enableColumns['starttime']) && !in_array('starttime', $ignoreFields)) {
+                $constraints[] = $expressionBuilder->lte(
+                    $tablePrefix . '.' . $enableColumns['starttime'],
+                    $queryContext->getAccessTime()
+                );
+            }
+
+            // Filter out records with a set endtime where the time is in the past.
+            if (isset($enableColumns['endtime']) && !in_array('endtime', $ignoreFields)) {
+                $constraints[] = $expressionBuilder->orX(
+                    $expressionBuilder->eq($tablePrefix . '.' . $enableColumns['endtime'], 0),
+                    $expressionBuilder->gt(
+                        $tablePrefix . '.' . $enableColumns['endtime'],
+                        $queryContext->getAccessTime()
+                    )
+                );
+            }
+
+            // Filter out records based on the frondend user groups
+            if ($enableColumns['fe_group'] && !in_array('fe_group', $ignoreFields)) {
+                $constraints[] = $this->getFrontendUserGroupConstraints(
+                    $tablePrefix,
+                    $enableColumns['fe_group']
+                );
+            }
+
+            // Call hook functions for additional enableColumns
+            if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['addEnableColumns'])) {
+                $_params = [
+                    'table' => $tableName,
+                    'tableAlias' => $tableAlias,
+                    'tablePrefix' => $tablePrefix,
+                    'show_hidden' => $showHidden,
+                    'ignore_array' => $ignoreFields,
+                    'ctrl' => $tableConfig
+                ];
+                foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['addEnableColumns'] as $_funcRef) {
+                    $constraint = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
+
+                    $constraints[] = preg_replace('/^(?:AND[[:space:]]*)+/i', '', trim($constraint));
+                }
+            }
+        }
+
+        return $expressionBuilder->andX(...$constraints);
+    }
+
+    /**
+     * @param string $tableName The table name to build constraints for
+     * @param string $fieldName The field name to build constraints for
+     *
+     * @return CompositeExpression
+     */
+    protected function getFrontendUserGroupConstraints(string $tableName, string $fieldName): CompositeExpression
+    {
+        $expressionBuilder = $this->expressionBuilder;
+        // Allow records where no group access has been configured (field values NULL, 0 or empty string)
+        $constraints = [
+            $expressionBuilder->isNull($tableName . '.' . $fieldName),
+            $expressionBuilder->eq($tableName . '.' . $fieldName, $expressionBuilder->literal('')),
+            $expressionBuilder->eq($tableName . '.' . $fieldName, $expressionBuilder->literal('0')),
+        ];
+
+        foreach ($this->queryContext->getMemberGroups() as $value) {
+            $constraints[] = $expressionBuilder->inSet(
+                $tableName . '.' . $fieldName,
+                $expressionBuilder->literal((string)$value)
+            );
+        }
+
+        return $expressionBuilder->orX(...$constraints);
+    }
+}
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Breaking-75454-LocalConfigurationDBConfigStructureHasChanged.rst b/typo3/sysext/core/Documentation/Changelog/master/Breaking-75454-LocalConfigurationDBConfigStructureHasChanged.rst
new file mode 100644 (file)
index 0000000..c3b56f9
--- /dev/null
@@ -0,0 +1,76 @@
+=====================================================================
+Breaking: #75454 - LocalConfiguration DB config structure has changed
+=====================================================================
+
+Description
+===========
+
+To provide support for multiple database connections and remapping tables to different
+database systems within the TYPO3 Core the configuration format for database connections
+in ``LocalConfiguration.php`` / ``$GLOBALS['TYPO3_CONF_VARS']['DB']`` has changed.
+
+The new configuration array structure:
+
+.. code-block:: php
+
+       'DB' => [
+               'Connections' => [
+                       'Default' => [
+                               'driver' => 'mysqli',
+                               'dbname' => 'typo3_database',
+                               'password' => 'typo3',
+                               'host' => '127.0.0.1',
+                               'port' => 3306,
+                               'user' => 'typo3',
+                               'socket' => ''
+                               'charset' => 'utf-8',
+                       ],
+               ],
+       ],
+
+Be aware that besides the deeper nesting below 'Connections/Default' some of the configuration
+keys have been renamed. It is required to provide the new configuration key ``driver`` with a
+value of ``mysqli`` explicitly.
+
+The following table lists the changed configuration keys and the appropriate values if these
+have changed.
+
+============================   ===============================================
+Old name                       New name
+============================   ===============================================
+DB/username                    DB/Connections/Default/user
+DB/password                    DB/Connections/Default/password
+DB/host                        DB/Connections/Default/host
+DB/port                        DB/Connections/Default/port
+DB/socket                      DB/Connections/Default/unix_socket
+DB/database                    DB/Connections/Default/dbname
+SYS/setDBinit                  DB/Connections/Default/initCommands
+SYS/no_pconnect                DB/Connections/Default/persistentConnection
+SYS/dbClientCompress           DB/Connections/Default/driverOptions
+                               Valid values for MySQLi connections:
+                               0  compression disabled
+                               32 compression enabled
+============================   ===============================================
+
+
+Impact
+======
+
+Connections to the database will fail with an exception until the configuration has been migrated
+to the new structure.
+
+
+Affected Installations
+======================
+
+All Installations
+
+
+Migration
+=========
+
+The Install Tool will migrate the configuration information for the default connection to the new
+format. Installations overriding the database configuration using ``AdditionalConfiguration.php``
+or other means need to ensure the new format is being used.
+
+.. index:: php, setting
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Breaking-75454-TYPO3_dbConstantsRemoved.rst b/typo3/sysext/core/Documentation/Changelog/master/Breaking-75454-TYPO3_dbConstantsRemoved.rst
new file mode 100644 (file)
index 0000000..776eec0
--- /dev/null
@@ -0,0 +1,31 @@
+=============================================
+Breaking: #75454 - TYPO3_db Constants removed
+=============================================
+
+Description
+===========
+
+The PHP constants ``TYPO3_db``, ``TYPO3_db_username``, ``TYPO3_db_password`` and ``TYPO3_db_host`` have been
+removed which were used when a TYPO3 initialized the database connection have been removed.
+
+
+Impact
+======
+
+Checking for or using the mentioned constants may lead to unexpected behavior or errors.
+If not checked if the constant even was defined, the application will stop immediately.
+
+
+Affected Installations
+======================
+
+Any installation which uses a third-party extension using these constants.
+
+
+Migration
+=========
+
+Use the configuration data within ``$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']``
+to determine the username, password and host information for the default database connection.
+
+.. index:: php, setting
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-75454-DoctrineDBALForDatabaseConnections.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-75454-DoctrineDBALForDatabaseConnections.rst
new file mode 100644 (file)
index 0000000..6156acb
--- /dev/null
@@ -0,0 +1,73 @@
+=========================================================================================
+Feature: #75454 - Added PHP library "Doctrine DBAL" for Database Connections within TYPO3
+=========================================================================================
+
+Description
+===========
+
+The PHP library ``Doctrine DBAL`` is added as a composer dependency as a powerful database
+abstraction layer with many features for database abstraction, schema introspection and
+schema management within TYPO3.
+
+A TYPO3-specific PHP class called ``TYPO3\CMS\Core\Database\ConnectionPool`` is added as a
+manager for database connections.
+
+All connections configured below ``$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']`` are
+accessible using this manager, enabling the parallel usage of multiple database systems.
+
+By using the database abstraction options and the QueryBuilder provided SQL statements being
+built will be properly quoted and compatible with different DBMS out of the box as far as
+possible.
+
+Existing ``$GLOBALS['TYPO3_CONF_VARS']['DB']`` options are removed and/or migrated to the
+new Doctrine-compliant options.
+
+Documentation for Doctrine DBAL can be found at http://www.doctrine-project.org/projects/dbal.html.
+
+The :php:``Connection`` provides convenience methods for ``insert``, ``select``, ``update``,
+``delete``and ``truncate`` statements. For ``select``, ``update`` an ``delete`` only simple
+equality comparisons (``WHERE "aField" = 'aValue') are supported. For complex statements its
+required to use the :php:``QueryBuilder``.
+
+
+Impact
+======
+
+Currently the :php:``DatabaseConnection`` class only uses Doctrine to establish the database
+connection to MySQL, no advanced options are being used yet.
+
+Connections will always need to be requested with a table name so that the abstraction of
+table names to database connections stays intact.
+
+The :php:``ConnectionPool`` class can be used like this:
+
+.. code-block:: php
+
+   // Get a connection which can be used for muliple operations
+   /** @var \TYPO3\CMS\Core\Database\Connecction $conn */
+   $conn = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('aTable');
+   $affectedRows = $conn->insert(
+      'aTable',
+      $fields, // Assocative array of column/value pairs, automatically quoted & escaped
+   );
+
+.. code-block:: php
+
+   // Get a QueryBuilder, which should only be used a single time
+   $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('aTable);
+   $query->select('*')
+      ->from('aTable)
+      ->where($query->expr()->eq('aField', $query->createNamedParameter($aValue)))
+      ->andWhere(
+         $query->expr()->lte(
+            'anotherField',
+            $query->createNamedParameter($anotherValue)
+         )
+      )
+   $rows = $query->execute()->fetchAll();
+
+Extension authors are advised to use the ConnectionPool and Connections classes instead of using
+the Doctrine DBAL directly in order to ensure a clear upgrade path when updates to the underlying
+API need to be done.
+
+.. index:: php
index bcf55ba..c34d935 100644 (file)
@@ -184,9 +184,9 @@ class AcceptanceCoreEnvironment extends Extension
         $testbase->linkTestExtensionsToInstance($instancePath, $testExtensionsToLoad);
         $testbase->linkPathsInTestInstance($instancePath, $this->pathsToLinkInTestInstance);
         $localConfiguration = $testbase->getOriginalDatabaseSettingsFromEnvironmentOrLocalConfiguration();
-        $originalDatabaseName = $localConfiguration['DB']['database'];
+        $originalDatabaseName = $localConfiguration['DB']['Connections']['Default']['dbname'];
         // Append the unique identifier to the base database name to end up with a single database per test case
-        $localConfiguration['DB']['database'] = $originalDatabaseName . '_at';
+        $localConfiguration['DB']['Connections']['Default']['dbname'] = $originalDatabaseName . '_at';
         $testbase->testDatabaseNameIsNotTooLong($originalDatabaseName, $localConfiguration);
         // Set some hard coded base settings for the instance. Those could be overruled by
         // $this->configurationToUseInTestInstance if needed again.
@@ -223,7 +223,7 @@ class AcceptanceCoreEnvironment extends Extension
         ];
         $testbase->setUpPackageStates($instancePath, $defaultCoreExtensionsToLoad, $this->coreExtensionsToLoad, $testExtensionsToLoad);
         $testbase->setUpBasicTypo3Bootstrap($instancePath);
-        $testbase->setUpTestDatabase($localConfiguration['DB']['database'], $originalDatabaseName);
+        $testbase->setUpTestDatabase($localConfiguration['DB']['Connections']['Default']['dbname'], $originalDatabaseName);
         $testbase->loadExtensionTables();
         $testbase->createDatabaseStructure();
 
index 491e6eb..9e60acd 100644 (file)
@@ -223,9 +223,9 @@ abstract class FunctionalTestCase extends BaseTestCase
             $testbase->linkTestExtensionsToInstance($this->instancePath, $this->testExtensionsToLoad);
             $testbase->linkPathsInTestInstance($this->instancePath, $this->pathsToLinkInTestInstance);
             $localConfiguration = $testbase->getOriginalDatabaseSettingsFromEnvironmentOrLocalConfiguration();
-            $originalDatabaseName = $localConfiguration['DB']['database'];
+            $originalDatabaseName = $localConfiguration['DB']['Connections']['Default']['dbname'];
             // Append the unique identifier to the base database name to end up with a single database per test case
-            $localConfiguration['DB']['database'] = $originalDatabaseName . '_ft' . $this->identifier;
+            $localConfiguration['DB']['Connections']['Default']['dbname'] = $originalDatabaseName . '_ft' . $this->identifier;
             $testbase->testDatabaseNameIsNotTooLong($originalDatabaseName, $localConfiguration);
             // Set some hard coded base settings for the instance. Those could be overruled by
             // $this->configurationToUseInTestInstance if needed again.
@@ -246,7 +246,7 @@ abstract class FunctionalTestCase extends BaseTestCase
             ];
             $testbase->setUpPackageStates($this->instancePath, $defaultCoreExtensionsToLoad, $this->coreExtensionsToLoad, $this->testExtensionsToLoad);
             $testbase->setUpBasicTypo3Bootstrap($this->instancePath);
-            $testbase->setUpTestDatabase($localConfiguration['DB']['database'], $originalDatabaseName);
+            $testbase->setUpTestDatabase($localConfiguration['DB']['Connections']['Default']['dbname'], $originalDatabaseName);
             $testbase->loadExtensionTables();
             $testbase->createDatabaseStructure();
         }
index 8d15484..4e77f6a 100644 (file)
@@ -316,26 +316,32 @@ class Testbase
         $databaseSocket = trim(getenv('typo3DatabaseSocket'));
         if ($databaseName || $databaseHost || $databaseUsername || $databasePassword || $databasePort || $databaseSocket) {
             // Try to get database credentials from environment variables first
-            $originalConfigurationArray = array(
-                'DB' => array(),
-            );
+            $originalConfigurationArray = [
+                'DB' => [
+                    'Connections' => [
+                        'Default' => [
+                            'driver' => 'mysqli'
+                        ],
+                    ],
+                ],
+            ];
             if ($databaseName) {
-                $originalConfigurationArray['DB']['database'] = $databaseName;
+                $originalConfigurationArray['DB']['Connections']['Default']['dbname'] = $databaseName;
             }
             if ($databaseHost) {
-                $originalConfigurationArray['DB']['host'] = $databaseHost;
+                $originalConfigurationArray['DB']['Connections']['Default']['host'] = $databaseHost;
             }
             if ($databaseUsername) {
-                $originalConfigurationArray['DB']['username'] = $databaseUsername;
+                $originalConfigurationArray['DB']['Connections']['Default']['user'] = $databaseUsername;
             }
             if ($databasePassword) {
-                $originalConfigurationArray['DB']['password'] = $databasePassword;
+                $originalConfigurationArray['DB']['Connections']['Default']['password'] = $databasePassword;
             }
             if ($databasePort) {
-                $originalConfigurationArray['DB']['port'] = $databasePort;
+                $originalConfigurationArray['DB']['Connections']['Default']['port'] = $databasePort;
             }
             if ($databaseSocket) {
-                $originalConfigurationArray['DB']['socket'] = $databaseSocket;
+                $originalConfigurationArray['DB']['Connections']['Default']['unix_socket'] = $databaseSocket;
             }
         } elseif (file_exists(ORIGINAL_ROOT . 'typo3conf/LocalConfiguration.php')) {
             // See if a LocalConfiguration file exists in "parent" instance to get db credentials from
@@ -361,8 +367,8 @@ class Testbase
     public function testDatabaseNameIsNotTooLong($originalDatabaseName, array $configuration)
     {
         // Maximum database name length for mysql is 64 characters
-        if (strlen($configuration['DB']['database']) > 64) {
-            $suffixLength = strlen($configuration['DB']['database']) - strlen($originalDatabaseName);
+        if (strlen($configuration['DB']['Connections']['Default']['dbname']) > 64) {
+            $suffixLength = strlen($configuration['DB']['Connections']['Default']['dbname']) - strlen($originalDatabaseName);
             $maximumOriginalDatabaseName = 64 - $suffixLength;
             throw new Exception(
                 'The name of the database that is used for the functional test (' . $originalDatabaseName . ')' .
@@ -507,8 +513,8 @@ class Testbase
         $database->admin_query('DROP DATABASE IF EXISTS `' . $databaseName . '`');
         $createDatabaseResult = $database->admin_query('CREATE DATABASE `' . $databaseName . '` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci');
         if (!$createDatabaseResult) {
-            $user = $GLOBALS['TYPO3_CONF_VARS']['DB']['username'];
-            $host = $GLOBALS['TYPO3_CONF_VARS']['DB']['host'];
+            $user = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user'];
+            $host = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host'];
             throw new Exception(
                 'Unable to create database with name ' . $databaseName . '. This is probably a permission problem.'
                 . ' For this instance this could be fixed executing:'
@@ -565,7 +571,7 @@ class Testbase
                 1377620117
             );
         }
-        $database->setDatabaseName($GLOBALS['TYPO3_CONF_VARS']['DB']['database']);
+        $database->setDatabaseName($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname']);
         $database->sql_select_db();
         foreach ($database->admin_get_tables() as $table) {
             $database->admin_query('TRUNCATE ' . $table['Name'] . ';');
diff --git a/typo3/sysext/core/Tests/Unit/Database/ConnectionTest.php b/typo3/sysext/core/Tests/Unit/Database/ConnectionTest.php
new file mode 100644 (file)
index 0000000..6d3f70f
--- /dev/null
@@ -0,0 +1,500 @@
+<?php
+declare (strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Unit\Database;
+
+/*
+ * 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!
+ */
+
+use Doctrine\DBAL\Driver\Mysqli\MysqliConnection;
+use Doctrine\DBAL\Statement;
+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;
+use TYPO3\CMS\Core\Tests\Unit\Database\Mocks\MockPlatform;
+use TYPO3\CMS\Core\Tests\UnitTestCase;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Test case
+ *
+ */
+class ConnectionTest extends UnitTestCase
+{
+    /**
+     * @var Connection|\PHPUnit_Framework_MockObject_MockObject
+     */
+    protected $connection;
+
+    /**
+     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
+     */
+    protected $platform;
+
+    /**
+     * @var string
+     */
+    protected $testTable = 'testTable';
+
+    /**
+     * Create a new database connection mock object for every test.
+     *
+     * @return void
+     */
+    protected function setUp()
+    {
+        parent::setUp();
+
+        $this->connection = $this->getMockBuilder(Connection::class)
+            ->disableOriginalConstructor()
+            ->setMethods(
+                [
+                    'connect',
+                    'executeQuery',
+                    'executeUpdate',
+                    'getDatabasePlatform',
+                    'getDriver',
+                    'getExpressionBuilder',
+                    'getWrappedConnection',
+                ]
+            )
+            ->getMock();
+
+        $this->connection->expects($this->any())
+            ->method('getExpressionBuilder')
+            ->will($this->returnValue(GeneralUtility::makeInstance(ExpressionBuilder::class, $this->connection)));
+
+        $this->connection->expects($this->any())
+            ->method('connect');
+
+        $this->connection->expects($this->any())
+            ->method('getDatabasePlatform')
+            ->will($this->returnValue(new MockPlatform()));
+    }
+
+    /**
+     * @test
+     */
+    public function createQueryBuilderReturnsInstanceOfTypo3QueryBuilder()
+    {
+        $this->assertInstanceOf(QueryBuilder::class, $this->connection->createQueryBuilder());
+    }
+
+    /**
+     * @return array
+     */
+    public function quoteIdentifierDataProvider()
+    {
+        return [
+            'SQL star' => [
+                '*',
+                '*',
+            ],
+            'fieldname' => [
+                'aField',
+                '"aField"',
+            ],
+            'whitespace' => [
+                'with blanks',
+                '"with blanks"',
+            ],
+            'double quotes' => [
+                '"double" quotes',
+                '"""double"" quotes"',
+            ],
+            'single quotes' => [
+                "'single'",
+                '"\'single\'"',
+
+            ],
+            'multiple double quotes' => [
+                '""multiple""',
+                '"""""multiple"""""',
+            ],
+            'multiple single quotes' => [
+                "''multiple''",
+                '"\'\'multiple\'\'"',
+            ],
+            'backticks' => [
+                '`backticks`',
+                '"`backticks`"',
+            ],
+            'slashes' => [
+                '/slashes/',
+                '"/slashes/"',
+            ],
+            'backslashes' => [
+                '\\backslashes\\',
+                '"\\backslashes\\"',
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider quoteIdentifierDataProvider
+     * @param string $input
+     * @param string $expected
+     */
+    public function quoteIdentifier(string $input, string $expected)
+    {
+        $this->assertSame($expected, $this->connection->quoteIdentifier($input));
+    }
+
+    /**
+     * @test
+     */
+    public function quoteIdentifiers()
+    {
+        $input = [
+            'aField',
+            'anotherField',
+        ];
+
+        $expected = [
+            '"aField"',
+            '"anotherField"',
+        ];
+
+        $this->assertSame($expected, $this->connection->quoteIdentifiers($input));
+    }
+
+    /**
+     * @return array
+     */
+    public function insertQueriesDataProvider()
+    {
+        return [
+            'single value' => [
+                ['aTestTable', ['aField' => 'aValue']],
+                'INSERT INTO "aTestTable" ("aField") VALUES (?)',
+                ['aValue'],
+                [],
+            ],
+            'multiple values' => [
+                ['aTestTable', ['aField' => 'aValue', 'bField' => 'bValue']],
+                'INSERT INTO "aTestTable" ("aField", "bField") VALUES (?, ?)',
+                ['aValue', 'bValue'],
+                [],
+            ],
+            'with types' => [
+                ['aTestTable', ['aField' => 'aValue', 'bField' => 'bValue'], [Connection::PARAM_STR, Connection::PARAM_STR]],
+                'INSERT INTO "aTestTable" ("aField", "bField") VALUES (?, ?)',
+                ['aValue', 'bValue'],
+                [Connection::PARAM_STR, Connection::PARAM_STR],
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider insertQueriesDataProvider
+     * @param array $args
+     * @param string $expectedQuery
+     * @param array $expectedValues
+     * @param array $expectedTypes
+     */
+    public function insertQueries(array $args, string $expectedQuery, array $expectedValues, array $expectedTypes)
+    {
+        $this->connection->expects($this->once())
+            ->method('executeUpdate')
+            ->with($expectedQuery, $expectedValues, $expectedTypes)
+            ->will($this->returnValue(1));
+
+        $this->connection->insert(...$args);
+    }
+
+    /**
+     * @test
+     */
+    public function bulkInsert()
+    {
+        $this->connection->expects($this->once())
+            ->method('executeUpdate')
+            ->with('INSERT INTO "aTestTable" ("aField") VALUES (?), (?)', ['aValue', 'anotherValue'])
+            ->will($this->returnValue(2));
+
+        $this->connection->bulkInsert('aTestTable', [['aField' => 'aValue'], ['aField' => 'anotherValue']], ['aField']);
+    }
+
+    /**
+     * @return array
+     */
+    public function updateQueriesDataProvider()
+    {
+        return [
+            'single value' => [
+                ['aTestTable', ['aField' => 'aValue'], ['uid' => 1]],
+                'UPDATE "aTestTable" SET "aField" = ? WHERE "uid" = ?',
+                ['aValue', 1],
+                [],
+            ],
+            'multiple values' => [
+                ['aTestTable', ['aField' => 'aValue', 'bField' => 'bValue'], ['uid' => 1]],
+                'UPDATE "aTestTable" SET "aField" = ?, "bField" = ? WHERE "uid" = ?',
+                ['aValue', 'bValue', 1],
+                [],
+            ],
+            'with types' => [
+                ['aTestTable', ['aField' => 'aValue'], ['uid' => 1], [Connection::PARAM_STR]],
+                'UPDATE "aTestTable" SET "aField" = ? WHERE "uid" = ?',
+                ['aValue', 1],
+                [Connection::PARAM_STR],
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider updateQueriesDataProvider
+     * @param array $args
+     * @param string $expectedQuery
+     * @param array $expectedValues
+     * @param array $expectedTypes
+     */
+    public function updateQueries(array $args, string $expectedQuery, array $expectedValues, array $expectedTypes)
+    {
+        $this->connection->expects($this->once())
+            ->method('executeUpdate')
+            ->with($expectedQuery, $expectedValues, $expectedTypes)
+            ->will($this->returnValue(1));
+
+        $this->connection->update(...$args);
+    }
+
+    /**
+     * @return array
+     */
+    public function deleteQueriesDataProvider()
+    {
+        return [
+            'single condition' => [
+                ['aTestTable', ['aField' => 'aValue']],
+                'DELETE FROM "aTestTable" WHERE "aField" = ?',
+                ['aValue'],
+                [],
+            ],
+            'multiple conditions' => [
+                ['aTestTable', ['aField' => 'aValue', 'bField' => 'bValue']],
+                'DELETE FROM "aTestTable" WHERE "aField" = ? AND "bField" = ?',
+                ['aValue', 'bValue'],
+                [],
+            ],
+            'with types' => [
+                ['aTestTable', ['aField' => 'aValue'], [Connection::PARAM_STR]],
+                'DELETE FROM "aTestTable" WHERE "aField" = ?',
+                ['aValue'],
+                [Connection::PARAM_STR],
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider deleteQueriesDataProvider
+     * @param array $args
+     * @param string $expectedQuery
+     * @param array $expectedValues
+     * @param array $expectedTypes
+     */
+    public function deleteQueries(array $args, string $expectedQuery, array $expectedValues, array $expectedTypes)
+    {
+        $this->connection->expects($this->once())
+            ->method('executeUpdate')
+            ->with($expectedQuery, $expectedValues, $expectedTypes)
+            ->will($this->returnValue(1));
+
+        $this->connection->delete(...$args);
+    }
+
+    /**
+     * Data provider for select query tests
+     *
+     * Each array item consists of
+     *  - array of parameters for select call
+     *  - expected SQL string
+     *  - expected named parameter values
+     *
+     * @return array
+     */
+    public function selectQueriesDataProvider()
+    {
+        return [
+            'all columns' => [
+                [['*'], 'aTable'],
+                'SELECT * FROM "aTable"',
+                [],
+            ],
+            'subset of columns' => [
+                [['aField', 'anotherField'], 'aTable'],
+                'SELECT "aField", "anotherField" FROM "aTable"',
+                [],
+            ],
+            'conditions' => [
+                [['*'], 'aTable', ['aField' => 'aValue']],
+                'SELECT * FROM "aTable" WHERE "aField" = :dcValue1',
+                ['dcValue1' => 'aValue'],
+            ],
+            'grouping' => [
+                [['*'], 'aTable', [], ['aField']],
+                'SELECT * FROM "aTable" GROUP BY "aField"',
+                [],
+            ],
+            'ordering' => [
+                [['*'], 'aTable', [], [], ['aField' => 'ASC']],
+                'SELECT * FROM "aTable" ORDER BY "aField" ASC',
+                [],
+            ],
+            'limit' => [
+                [['*'], 'aTable', [], [], [], 1],
+                'SELECT * FROM "aTable" LIMIT 1 OFFSET 0',
+                [],
+            ],
+            'offset' => [
+                [['*'], 'aTable', [], [], [], 1, 10],
+                'SELECT * FROM "aTable" LIMIT 1 OFFSET 10',
+                [],
+            ],
+            'everything' => [
+                [
+                    ['aField', 'anotherField'],
+                    'aTable',
+                    ['aField' => 'aValue'],
+                    ['anotherField'],
+                    ['aField' => 'ASC'],
+                    1,
+                    10,
+                ],
+                'SELECT "aField", "anotherField" FROM "aTable" WHERE "aField" = :dcValue1 ' .
+                'GROUP BY "anotherField" ORDER BY "aField" ASC LIMIT 1 OFFSET 10',
+                ['dcValue1' => 'aValue'],
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider selectQueriesDataProvider
+     * @param array $args
+     * @param string $expectedQuery
+     * @param array $expectedParameters
+     */
+    public function selectQueries(array $args, string $expectedQuery, array $expectedParameters)
+    {
+        $resultStatement = $this->getMockBuilder(Statement::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $this->connection->expects($this->once())
+            ->method('executeQuery')
+            ->with($expectedQuery, $expectedParameters)
+            ->will($this->returnValue($resultStatement));
+
+        $this->connection->select(...$args);
+    }
+
+    /**
+     * Data provider for select query tests
+     *
+     * Each array item consists of
+     *  - array of parameters for select call
+     *  - expected SQL string
+     *  - expected named parameter values
+     *
+     * @return array
+     */
+    public function countQueriesDataProvider()
+    {
+        return [
+            'all columns' => [
+                ['*', 'aTable', []],
+                'SELECT COUNT(*) FROM "aTable"',
+                [],
+            ],
+            'specified columns' => [
+                ['aField', 'aTable', []],
+                'SELECT COUNT("aField") FROM "aTable"',
+                [],
+            ],
+            'conditions' => [
+                ['aTable.aField', 'aTable', ['aField' => 'aValue']],
+                'SELECT COUNT("aTable"."aField") FROM "aTable" WHERE "aField" = :dcValue1',
+                ['dcValue1' => 'aValue'],
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider countQueriesDataProvider
+     * @param array $args
+     * @param string $expectedQuery
+     * @param array $expectedParameters
+     */
+    public function countQueries(array $args, string $expectedQuery, array $expectedParameters)
+    {
+        $resultStatement = $this->getMockBuilder(Statement::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $resultStatement->expects($this->once())
+            ->method('fetchColumn')
+            ->with(0)
+            ->will($this->returnValue(0));
+
+        $this->connection->expects($this->once())
+            ->method('executeQuery')
+            ->with($expectedQuery, $expectedParameters)
+            ->will($this->returnValue($resultStatement));
+
+        $this->connection->count(...$args);
+    }
+
+    /**
+     * @test
+     */
+    public function truncateQuery()
+    {
+        $this->connection->expects($this->once())
+            ->method('executeUpdate')
+            ->with('TRUNCATE "aTestTable"')
+            ->will($this->returnValue(0));
+
+        $this->connection->truncate('aTestTable', false);
+    }
+
+    /**
+     * @test
+     */
+    public function getServerVersionReportsPlatformVersion()
+    {
+        /** @var MysqliConnection|ObjectProphecy $driverProphet */
+        $driverProphet = $this->prophesize(\Doctrine\DBAL\Driver\Mysqli\Driver::class);
+        $driverProphet->willImplement(\Doctrine\DBAL\VersionAwarePlatformDriver::class);
+
+        /** @var MysqliConnection|ObjectProphecy $wrappedConnectionProphet */
+        $wrappedConnectionProphet = $this->prophesize(\Doctrine\DBAL\Driver\Mysqli\MysqliConnection::class);
+        $wrappedConnectionProphet->willImplement(\Doctrine\DBAL\Driver\ServerInfoAwareConnection::class);
+        $wrappedConnectionProphet->requiresQueryForServerVersion()->willReturn(false);
+        $wrappedConnectionProphet->getServerVersion()->willReturn('5.7.11');
+
+        $this->connection->expects($this->any())
+            ->method('getDriver')
+            ->willReturn($driverProphet->reveal());
+        $this->connection->expects($this->any())
+            ->method('getWrappedConnection')
+            ->willReturn($wrappedConnectionProphet->reveal());
+
+        $this->assertSame('mock 5.7.11', $this->connection->getServerVersion());
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Mocks/MockKeywordList.php b/typo3/sysext/core/Tests/Unit/Database/Mocks/MockKeywordList.php
new file mode 100644 (file)
index 0000000..075208f
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+namespace TYPO3\CMS\Core\Tests\Unit\Database\Mocks;
+
+/*
+ * 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!
+ */
+
+use Doctrine\DBAL\Platforms\Keywords\KeywordList;
+
+class MockKeywordList extends KeywordList
+{
+    /**
+     * Returns the name of this keyword list.
+     *
+     * @return string
+     */
+    public function getName()
+    {
+        return 'mock';
+    }
+
+    /**
+     * Returns the list of keywords.
+     *
+     * @return array
+     */
+    protected function getKeywords()
+    {
+        return [
+            'RESERVED',
+        ];
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Mocks/MockPlatform.php b/typo3/sysext/core/Tests/Unit/Database/Mocks/MockPlatform.php
new file mode 100644 (file)
index 0000000..477491a
--- /dev/null
@@ -0,0 +1,192 @@
+<?php
+namespace TYPO3\CMS\Core\Tests\Unit\Database\Mocks;
+
+/*
+ * 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!
+ */
+
+use Doctrine\DBAL\DBALException;
+use Doctrine\DBAL\Platforms\AbstractPlatform;
+
+class MockPlatform extends AbstractPlatform
+{
+    /**
+     * Gets the SQL Snippet used to declare a BLOB column type.
+     *
+     * @param array $field
+     * @return string|void
+     * @throws \Doctrine\DBAL\DBALException
+     */
+    public function getBlobTypeDeclarationSQL(array $field)
+    {
+        throw DBALException::notSupported(__METHOD__);
+    }
+
+    /**
+     * Returns the SQL snippet that declares a boolean column.
+     *
+     * @param array $columnDef
+     *
+     * @return string
+     */
+    public function getBooleanTypeDeclarationSQL(array $columnDef)
+    {
+    }
+
+    /**
+     * Returns the SQL snippet that declares a 4 byte integer column.
+     *
+     * @param array $columnDef
+     *
+     * @return string
+     */
+    public function getIntegerTypeDeclarationSQL(array $columnDef)
+    {
+    }
+
+    /**
+     * Returns the SQL snippet that declares an 8 byte integer column.
+     *
+     * @param array $columnDef
+     *
+     * @return string
+     */
+    public function getBigIntTypeDeclarationSQL(array $columnDef)
+    {
+    }
+
+    /**
+     * Returns the SQL snippet that declares a 2 byte integer column.
+     *
+     * @param array $columnDef
+     *
+     * @return string
+     */
+    public function getSmallIntTypeDeclarationSQL(array $columnDef)
+    {
+    }
+
+    /**
+     * Returns the SQL snippet that declares common properties of an integer column.
+     *
+     * @param array $columnDef
+     *
+     * @return string
+     */
+    public function _getCommonIntegerTypeDeclarationSQL(array $columnDef)
+    {
+    }
+
+    /**
+     * Returns the SQL snippet used to declare a VARCHAR column type.
+     *
+     * @param array $field
+     *
+     * @return string
+     */
+    public function getVarcharTypeDeclarationSQL(array $field)
+    {
+        return 'DUMMYVARCHAR()';
+    }
+
+    /**
+     * Returns the SQL snippet used to declare a CLOB column type.
+     *
+     * @param array $field
+     *
+     * @return string
+     */
+    public function getClobTypeDeclarationSQL(array $field)
+    {
+        return 'DUMMYCLOB';
+    }
+
+    /**
+     * Returns the SQL snippet to declare a JSON field.
+     *
+     * By default this maps directly to a CLOB and only maps to more
+     * special datatypes when the underlying databases support this datatype.
+     *
+     * @param array $field
+     *
+     * @return string
+     */
+    public function getJsonTypeDeclarationSQL(array $field)
+    {
+        return 'DUMMYJSON';
+    }
+
+    /**
+     * Returns the SQL snippet used to declare a BINARY/VARBINARY column type.
+     *
+     * @param array $field The column definition.
+     *
+     * @return string
+     */
+    public function getBinaryTypeDeclarationSQL(array $field)
+    {
+        return 'DUMMYBINARY';
+    }
+
+    /**
+     * Gets the default length of a varchar field.
+     *
+     * @return int
+     */
+    public function getVarcharDefaultLength()
+    {
+        return 255;
+    }
+
+    /**
+     * Gets the name of the platform.
+     *
+     * @return string
+     */
+    public function getName()
+    {
+        return 'mock';
+    }
+
+    /**
+     * Lazy load Doctrine Type Mappings.
+     *
+     * @return void
+     */
+    protected function initializeDoctrineTypeMappings()
+    {
+    }
+
+    /**
+     * @param int $length
+     * @param bool $fixed
+     *
+     * @return string
+     *
+     * @throws \Doctrine\DBAL\DBALException If not supported on this platform.
+     */
+    protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed)
+    {
+    }
+
+    /**
+     * Returns the class name of the reserved keywords list.
+     *
+     * @return string
+     *
+     * @throws \Doctrine\DBAL\DBALException If not supported on this platform.
+     */
+    protected function getReservedKeywordsClass()
+    {
+        return MockKeywordList::class;
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Query/BulkInsertTest.php b/typo3/sysext/core/Tests/Unit/Database/Query/BulkInsertTest.php
new file mode 100644 (file)
index 0000000..23c718e
--- /dev/null
@@ -0,0 +1,333 @@
+<?php
+declare (strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Unit\Database\Query;
+
+/*
+ * 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!
+ */
+
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\Query\BulkInsertQuery;
+use TYPO3\CMS\Core\Tests\Unit\Database\Mocks\MockPlatform;
+use TYPO3\CMS\Core\Tests\UnitTestCase;
+
+class BulkInsertTest extends UnitTestCase
+{
+    /**
+     * @var Connection
+     */
+    protected $connection;
+
+    /**
+     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
+     */
+    protected $platform;
+
+    /**
+     * @var string
+     */
+    protected $testTable = 'testTable';
+
+    /**
+     * Create a new database connection mock object for every test.
+     *
+     * @return void
+     */
+    protected function setUp()
+    {
+        parent::setUp();
+
+        $this->connection = $this->getMockBuilder(Connection::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $this->connection->expects($this->any())
+            ->method('quoteIdentifier')
+            ->will($this->returnArgument(0));
+        $this->connection->expects($this->any())
+            ->method('getDatabasePlatform')
+            ->will($this->returnValue(new MockPlatform()));
+    }
+
+    /**
+     * @test
+     * @expectedException \LogicException
+     * @expectedExceptionMessage You need to add at least one set of values before generating the SQL.
+     */
+    public function getSQLWithoutSpecifiedValuesThrowsException()
+    {
+        $query = new BulkInsertQuery($this->connection, $this->testTable);
+
+        $query->getSQL();
+    }
+
+    /**
+     * @test
+     */
+    public function insertWithoutColumnAndTypeSpecification()
+    {
+        $query = new BulkInsertQuery($this->connection, $this->testTable);
+
+        $query->addValues([]);
+
+        $this->assertSame("INSERT INTO {$this->testTable} VALUES ()", (string)$query);
+        $this->assertSame([], $query->getParameters());
+        $this->assertSame([], $query->getParameterTypes());
+    }
+
+    public function insertWithoutColumnSpecification()
+    {
+        $query = new BulkInsertQuery($this->connection, $this->testTable);
+
+        $query->addValues([], [Connection::PARAM_BOOL]);
+
+        $this->assertSame("INSERT INTO {$this->testTable} VALUES ()", (string)$query);
+        $this->assertSame([], $query->getParameters());
+        $this->assertSame([], $query->getParameterTypes());
+    }
+
+    /**
+     * @test
+     */
+    public function singleInsertWithoutColumnSpecification()
+    {
+        $query = new BulkInsertQuery($this->connection, $this->testTable);
+
+        $query->addValues(['bar', 'baz', 'named' => 'bloo']);
+
+        $this->assertSame("INSERT INTO {$this->testTable} VALUES (?, ?, ?)", (string)$query);
+        $this->assertSame(['bar', 'baz', 'bloo'], $query->getParameters());
+        $this->assertSame([null, null, null], $query->getParameterTypes());
+
+        $query = new BulkInsertQuery($this->connection, $this->testTable);
+
+        $query->addValues(
+            ['bar', 'baz', 'named' => 'bloo'],
+            ['named' => Connection::PARAM_BOOL, null, Connection::PARAM_INT]
+        );
+
+        $this->assertSame("INSERT INTO {$this->testTable} VALUES (?, ?, ?)", (string)$query);
+        $this->assertSame(['bar', 'baz', 'bloo'], $query->getParameters());
+        $this->assertSame([null, Connection::PARAM_INT, Connection::PARAM_BOOL], $query->getParameterTypes());
+    }
+
+    /**
+     * @test
+     */
+    public function multiInsertWithoutColumnSpecification()
+    {
+        $query = new BulkInsertQuery($this->connection, $this->testTable);
+
+        $query->addValues([]);
+        $query->addValues(['bar', 'baz']);
+        $query->addValues(['bar', 'baz', 'bloo']);
+        $query->addValues(['bar', 'baz', 'named' => 'bloo']);
+
+        $this->assertSame("INSERT INTO {$this->testTable} VALUES (), (?, ?), (?, ?, ?), (?, ?, ?)", (string)$query);
+        $this->assertSame(['bar', 'baz', 'bar', 'baz', 'bloo', 'bar', 'baz', 'bloo'], $query->getParameters());
+        $this->assertSame([null, null, null, null, null, null, null, null], $query->getParameterTypes());
+
+        $query = new BulkInsertQuery($this->connection, $this->testTable);
+
+        $query->addValues([], [Connection::PARAM_INT]);
+        $query->addValues(['bar', 'baz'], [1 => Connection::PARAM_BOOL]);
+        $query->addValues(['bar', 'baz', 'bloo'], [Connection::PARAM_INT, null, Connection::PARAM_BOOL]);
+        $query->addValues(
+            ['bar', 'baz', 'named' => 'bloo'],
+            ['named' => Connection::PARAM_INT, null, Connection::PARAM_BOOL]
+        );
+
+        $this->assertSame("INSERT INTO {$this->testTable} VALUES (), (?, ?), (?, ?, ?), (?, ?, ?)", (string)$query);
+        $this->assertSame(['bar', 'baz', 'bar', 'baz', 'bloo', 'bar', 'baz', 'bloo'], $query->getParameters());
+        $this->assertSame(
+            [null, Connection::PARAM_BOOL, Connection::PARAM_INT, null, Connection::PARAM_BOOL, null, Connection::PARAM_BOOL, Connection::PARAM_INT],
+            $query->getParameterTypes()
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function singleInsertWithColumnSpecificationAndPositionalTypeValues()
+    {
+        $query = new BulkInsertQuery($this->connection, $this->testTable, ['bar', 'baz']);
+
+        $query->addValues(['bar', 'baz']);
+
+        $this->assertSame("INSERT INTO {$this->testTable} (bar, baz) VALUES (?, ?)", (string)$query);
+        $this->assertSame(['bar', 'baz'], $query->getParameters());
+        $this->assertSame([null, null], $query->getParameterTypes());
+
+        $query = new BulkInsertQuery($this->connection, $this->testTable, ['bar', 'baz']);
+
+        $query->addValues(['bar', 'baz'], [1 => Connection::PARAM_BOOL]);
+
+        $this->assertSame("INSERT INTO {$this->testTable} (bar, baz) VALUES (?, ?)", (string)$query);
+        $this->assertSame(['bar', 'baz'], $query->getParameters());
+        $this->assertSame([null, Connection::PARAM_BOOL], $query->getParameterTypes());
+    }
+
+    /**
+     * @test
+     */
+    public function singleInsertWithColumnSpecificationAndNamedTypeValues()
+    {
+        $query = new BulkInsertQuery($this->connection, $this->testTable, ['bar', 'baz']);
+
+        $query->addValues(['baz' => 'baz', 'bar' => 'bar']);
+
+        $this->assertSame("INSERT INTO {$this->testTable} (bar, baz) VALUES (?, ?)", (string)$query);
+        $this->assertSame(['bar', 'baz'], $query->getParameters());
+        $this->assertSame([null, null], $query->getParameterTypes());
+
+        $query = new BulkInsertQuery($this->connection, $this->testTable, ['bar', 'baz']);
+
+        $query->addValues(['baz' => 'baz', 'bar' => 'bar'], [null, Connection::PARAM_INT]);
+
+        $this->assertSame("INSERT INTO {$this->testTable} (bar, baz) VALUES (?, ?)", (string)$query);
+        $this->assertSame(['bar', 'baz'], $query->getParameters());
+        $this->assertSame([null, Connection::PARAM_INT], $query->getParameterTypes());
+    }
+
+    /**
+     * @test
+     */
+    public function singleInsertWithColumnSpecificationAndMixedTypeValues()
+    {
+        $query = new BulkInsertQuery($this->connection, $this->testTable, ['bar', 'baz']);
+
+        $query->addValues([1 => 'baz', 'bar' => 'bar']);
+
+        $this->assertSame("INSERT INTO {$this->testTable} (bar, baz) VALUES (?, ?)", (string)$query);
+        $this->assertSame(['bar', 'baz'], $query->getParameters());
+        $this->assertSame([null, null], $query->getParameterTypes());
+
+        $query = new BulkInsertQuery($this->connection, $this->testTable, ['bar', 'baz']);
+
+        $query->addValues([1 => 'baz', 'bar' => 'bar'], [Connection::PARAM_INT, Connection::PARAM_BOOL]);
+
+        $this->assertSame("INSERT INTO {$this->testTable} (bar, baz) VALUES (?, ?)", (string)$query);
+        $this->assertSame(['bar', 'baz'], $query->getParameters());
+        $this->assertSame([Connection::PARAM_INT, Connection::PARAM_BOOL], $query->getParameterTypes());
+    }
+
+    /**
+     * @test
+     */
+    public function multiInsertWithColumnSpecification()
+    {
+        $query = new BulkInsertQuery($this->connection, $this->testTable, ['bar', 'baz']);
+
+        $query->addValues(['bar', 'baz']);
+        $query->addValues([1 => 'baz', 'bar' => 'bar']);
+        $query->addValues(['bar', 'baz' => 'baz']);
+        $query->addValues(['bar' => 'bar', 'baz' => 'baz']);
+
+        $this->assertSame(
+            "INSERT INTO {$this->testTable} (bar, baz) VALUES (?, ?), (?, ?), (?, ?), (?, ?)",
+            (string)$query
+        );
+        $this->assertSame(['bar', 'baz', 'bar', 'baz', 'bar', 'baz', 'bar', 'baz'], $query->getParameters());
+        $this->assertSame([null, null, null, null, null, null, null, null], $query->getParameterTypes());
+
+        $query = new BulkInsertQuery($this->connection, $this->testTable, ['bar', 'baz']);
+
+        $query->addValues(['bar', 'baz'], ['baz' => Connection::PARAM_BOOL, 'bar' => Connection::PARAM_INT]);
+        $query->addValues([1 => 'baz', 'bar' => 'bar'], [1 => Connection::PARAM_BOOL, 'bar' => Connection::PARAM_INT]);
+        $query->addValues(['bar', 'baz' => 'baz'], [null, null]);
+        $query->addValues(
+            ['bar' => 'bar', 'baz' => 'baz'],
+            ['bar' => Connection::PARAM_INT, 'baz' => Connection::PARAM_BOOL]
+        );
+
+        $this->assertSame(
+            "INSERT INTO {$this->testTable} (bar, baz) VALUES (?, ?), (?, ?), (?, ?), (?, ?)",
+            (string)$query
+        );
+        $this->assertSame(['bar', 'baz', 'bar', 'baz', 'bar', 'baz', 'bar', 'baz'], $query->getParameters());
+        $this->assertSame(
+            [
+                Connection::PARAM_INT,
+                Connection::PARAM_BOOL,
+                Connection::PARAM_INT,
+                Connection::PARAM_BOOL,
+                null,
+                null,
+                Connection::PARAM_INT,
+                Connection::PARAM_BOOL,
+            ],
+            $query->getParameterTypes()
+        );
+    }
+
+    /**
+     * @test
+     * @expectedException \InvalidArgumentException
+     * @expectedExceptionMessage No value specified for column bar (index 0).
+     */
+    public function emptyInsertWithColumnSpecificationThrowsException()
+    {
+        $query = new BulkInsertQuery($this->connection, $this->testTable, ['bar', 'baz']);
+
+        $query->addValues([]);
+    }
+
+    /**
+     * @test
+     * @expectedException \InvalidArgumentException
+     * @expectedExceptionMessage Multiple values specified for column baz (index 1).
+     */
+    public function insertWithColumnSpecificationAndMultipleValuesForColumnThrowsException()
+    {
+        $query = new BulkInsertQuery($this->connection, $this->testTable, ['bar', 'baz']);
+
+        $query->addValues(['bar', 'baz', 'baz' => 666]);
+    }
+
+    /**
+     * @test
+     * @expectedException \InvalidArgumentException
+     * @expectedExceptionMessage Multiple types specified for column baz (index 1).
+     */
+    public function insertWithColumnSpecificationAndMultipleTypesForColumnThrowsException()
+    {
+        $query = new BulkInsertQuery($this->connection, $this->testTable, ['bar', 'baz']);
+
+        $query->addValues(['bar', 'baz'], [Connection::PARAM_INT, Connection::PARAM_INT, 'baz' => Connection::PARAM_STR]);
+    }
+
+    /**
+     * @test
+     * @expectedException \LogicException
+     * @expectedExceptionMessage You can only insert 10 rows in a single INSERT statement with platform "mock".
+     */
+    public function executeWithMaxInsertRowsPerStatementExceededThrowsException()
+    {
+        /** @var \PHPUnit_Framework_MockObject_MockObject|BulkInsertQuery $subject */
+        $subject = $this->getAccessibleMock(
+            BulkInsertQuery::class,
+            ['getInsertMaxRows'],
+            [$this->connection, $this->testTable],
+            ''
+        );
+
+        $subject->expects($this->any())
+            ->method('getInsertMaxRows')
+            ->will($this->returnValue(10));
+
+        for ($i = 0; $i <= 10; $i++) {
+            $subject->addValues([]);
+        }
+
+        $subject->execute();
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Query/Expression/ExpressionBuilderTest.php b/typo3/sysext/core/Tests/Unit/Database/Query/Expression/ExpressionBuilderTest.php
new file mode 100644 (file)
index 0000000..6d6bba8
--- /dev/null
@@ -0,0 +1,246 @@
+<?php
+declare (strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Unit\Database\Query;
+
+/*
+ * 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!
+ */
+
+use Prophecy\Argument;
+use Prophecy\Prophecy\ObjectProphecy;
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
+use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
+use TYPO3\CMS\Core\Tests\UnitTestCase;
+
+class ExpressionBuilderTest extends UnitTestCase
+{
+    /**
+     * @var Connection
+     */
+    protected $connectionProphet;
+
+    /**
+     * @var ExpressionBuilder
+     */
+    protected $subject;
+
+    /**
+     * @var string
+     */
+    protected $testTable = 'testTable';
+
+    /**
+     * Create a new database connection mock object for every test.
+     *
+     * @return void
+     */
+    protected function setUp()
+    {
+        parent::setUp();
+
+        /** @var Connection|ObjectProphecy $connectionProphet */
+        $this->connectionProphet = $this->prophesize(Connection::class);
+        $this->connectionProphet->quoteIdentifier(Argument::cetera())->willReturnArgument(0);
+
+        $this->subject = new ExpressionBuilder($this->connectionProphet->reveal());
+    }
+
+    /**
+     * @test
+     */
+    public function andXReturnType()
+    {
+        $result = $this->subject->andX('"uid" = 1', '"pid" = 0');
+
+        $this->assertInstanceOf(CompositeExpression::class, $result);
+        $this->assertSame(CompositeExpression::TYPE_AND, $result->getType());
+    }
+
+    /**
+     * @test
+     */
+    public function orXReturnType()
+    {
+        $result = $this->subject->orX('"uid" = 1', '"uid" = 7');
+
+        $this->assertInstanceOf(CompositeExpression::class, $result);
+        $this->assertSame(CompositeExpression::TYPE_OR, $result->getType());
+    }
+
+    /**
+     * @test
+     */
+    public function eqQuotesIdentifier()
+    {
+        $result = $this->subject->eq('aField', 1);
+
+        $this->connectionProphet->quoteIdentifier('aField')->shouldHaveBeenCalled();
+        $this->assertSame('aField = 1', $result);
+    }
+
+    /**
+     * @test
+     */
+    public function neqQuotesIdentifier()
+    {
+        $result = $this->subject->neq('aField', 1);
+
+        $this->connectionProphet->quoteIdentifier('aField')->shouldHaveBeenCalled();
+        $this->assertSame('aField <> 1', $result);
+    }
+
+    /**
+     * @test
+     */
+    public function ltQuotesIdentifier()
+    {
+        $result = $this->subject->lt('aField', 1);
+
+        $this->connectionProphet->quoteIdentifier('aField')->shouldHaveBeenCalled();
+        $this->assertSame('aField < 1', $result);
+    }
+
+    /**
+     * @test
+     */
+    public function lteQuotesIdentifier()
+    {
+        $result = $this->subject->lte('aField', 1);
+
+        $this->connectionProphet->quoteIdentifier('aField')->shouldHaveBeenCalled();
+        $this->assertSame('aField <= 1', $result);
+    }
+
+    /**
+     * @test
+     */
+    public function gtQuotesIdentifier()
+    {
+        $result = $this->subject->gt('aField', 1);
+
+        $this->connectionProphet->quoteIdentifier('aField')->shouldHaveBeenCalled();
+        $this->assertSame('aField > 1', $result);
+    }
+
+    /**
+     * @test
+     */
+    public function gteQuotesIdentifier()
+    {
+        $result = $this->subject->gte('aField', 1);
+
+        $this->connectionProphet->quoteIdentifier('aField')->shouldHaveBeenCalled();
+        $this->assertSame('aField >= 1', $result);
+    }
+
+    /**
+     * @test
+     */
+    public function isNullQuotesIdentifier()
+    {
+        $result = $this->subject->isNull('aField');
+
+        $this->connectionProphet->quoteIdentifier('aField')->shouldHaveBeenCalled();
+        $this->assertSame('aField IS NULL', $result);
+    }
+
+    /**
+     * @test
+     */
+    public function isNotNullQuotesIdentifier()
+    {
+        $result = $this->subject->isNotNull('aField');
+
+        $this->connectionProphet->quoteIdentifier('aField')->shouldHaveBeenCalled();
+        $this->assertSame('aField IS NOT NULL', $result);
+    }
+
+    /**
+     * @test
+     */
+    public function likeQuotesIdentifier()
+    {
+        $result = $this->subject->like('aField', "'aValue%'");
+
+        $this->connectionProphet->quoteIdentifier('aField')->shouldHaveBeenCalled();
+        $this->assertSame("aField LIKE 'aValue%'", $result);
+    }
+
+    /**
+     * @test
+     */
+    public function notLikeQuotesIdentifier()
+    {
+        $result = $this->subject->notLike('aField', "'aValue%'");
+
+        $this->connectionProphet->quoteIdentifier('aField')->shouldHaveBeenCalled();
+        $this->assertSame("aField NOT LIKE 'aValue%'", $result);
+    }
+
+    /**
+     * @test
+     */
+    public function inWithStringQuotesIdentifier()
+    {
+        $result = $this->subject->in('aField', '1,2,3');
+
+        $this->connectionProphet->quoteIdentifier('aField')->shouldHaveBeenCalled();
+        $this->assertSame('aField IN (1,2,3)', $result);
+    }
+
+    /**
+     * @test
+     */
+    public function inWithArrayQuotesIdentifier()
+    {
+        $result = $this->subject->in('aField', [1, 2, 3]);
+
+        $this->connectionProphet->quoteIdentifier('aField')->shouldHaveBeenCalled();
+        $this->assertSame('aField IN (1, 2, 3)', $result);
+    }
+
+    /**
+     * @test
+     */
+    public function notInWithStringQuotesIdentifier()
+    {
+        $result = $this->subject->notIn('aField', '1,2,3');
+
+        $this->connectionProphet->quoteIdentifier('aField')->shouldHaveBeenCalled();
+        $this->assertSame('aField NOT IN (1,2,3)', $result);
+    }
+
+    /**
+     * @test
+     */
+    public function notInWithArrayQuotesIdentifier()
+    {
+        $result = $this->subject->notIn('aField', [1, 2, 3]);
+
+        $this->connectionProphet->quoteIdentifier('aField')->shouldHaveBeenCalled();
+        $this->assertSame('aField NOT IN (1, 2, 3)', $result);
+    }
+
+    /**
+     * @test
+     */
+    public function literalQuotesValue()
+    {
+        $this->connectionProphet->quote('aField', 'Doctrine\DBAL\Types\StringType')
+            ->shouldBeCalled()
+            ->willReturn('"aField"');
+        $result = $this->subject->literal('aField', 'Doctrine\DBAL\Types\StringType');
+
+        $this->assertSame('"aField"', $result);
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Query/QueryBuilderTest.php b/typo3/sysext/core/Tests/Unit/Database/Query/QueryBuilderTest.php
new file mode 100644 (file)
index 0000000..d1419bb
--- /dev/null
@@ -0,0 +1,969 @@
+<?php
+declare (strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Unit\Database\Query;
+
+/*
+ * 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!
+ */
+
+use Prophecy\Argument;
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
+use TYPO3\CMS\Core\Database\Query\QueryBuilder;
+use TYPO3\CMS\Core\Tests\Unit\Database\Mocks\MockPlatform;
+use TYPO3\CMS\Core\Tests\UnitTestCase;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+class QueryBuilderTest extends UnitTestCase
+{
+    /**
+     * @var Connection|\Prophecy\Prophecy\ObjectProphecy
+     */
+    protected $connection;
+
+    /**
+     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
+     */
+    protected $platform;
+
+    /**
+     * @var QueryBuilder
+     */
+    protected $subject;
+
+    /**
+     * @var \Doctrine\DBAL\Query\QueryBuilder|\Prophecy\Prophecy\ObjectProphecy
+     */
+    protected $concreteQueryBuilder;
+
+    /**
+     * Create a new database connection mock object for every test.
+     *
+     * @return void
+     */
+    protected function setUp()
+    {
+        parent::setUp();
+
+        $this->concreteQueryBuilder = $this->prophesize(\Doctrine\DBAL\Query\QueryBuilder::class);
+
+        $this->connection = $this->prophesize(Connection::class);
+        $this->connection->getDatabasePlatform()->willReturn(new MockPlatform());
+
+        $this->subject = GeneralUtility::makeInstance(
+            QueryBuilder::class,
+            $this->connection->reveal(),
+            null,
+            $this->concreteQueryBuilder->reveal()
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function exprReturnsExpressionBuilderForConnection()
+    {
+        $this->connection->getExpressionBuilder()
+            ->shouldBeCalled()
+            ->willReturn(GeneralUtility::makeInstance(ExpressionBuilder::class, $this->connection->reveal()));
+
+        $this->subject->expr();
+    }
+
+    /**
+     * @test
+     */
+    public function getTypeDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->getType()
+            ->shouldBeCalled()
+            ->willReturn(\Doctrine\DBAL\Query\QueryBuilder::INSERT);
+
+        $this->subject->getType();
+    }
+
+    /**
+     * @test
+     */
+    public function getStateDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->getState()
+            ->shouldBeCalled()
+            ->willReturn(\Doctrine\DBAL\Query\QueryBuilder::STATE_CLEAN);
+
+        $this->subject->getState();
+    }
+
+    /**
+     * @test
+     */
+    public function getSQLDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->getSQL()
+            ->shouldBeCalled()
+            ->willReturn('UPDATE aTable SET pid = 7');
+        $this->concreteQueryBuilder->getType()
+            ->willReturn(2); // Update Type
+
+        $this->subject->getSQL();
+    }
+
+    /**
+     * @test
+     */
+    public function setParameterDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->setParameter(Argument::exact('aField'), Argument::exact(5), Argument::cetera())
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->setParameter('aField', 5);
+    }
+
+    /**
+     * @test
+     */
+    public function setParametersDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->setParameters(Argument::exact(['aField' => 'aValue']), Argument::exact([]))
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->setParameters(['aField' => 'aValue']);
+    }
+
+    /**
+     * @test
+     */
+    public function getParametersDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->getParameters()
+            ->shouldBeCalled()
+            ->willReturn(['aField' => 'aValue']);
+
+        $this->subject->getParameters();
+    }
+
+    /**
+     * @test
+     */
+    public function getParameterDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->getParameter(Argument::exact('aField'))
+            ->shouldBeCalled()
+            ->willReturn('aValue');
+
+        $this->subject->getParameter('aField');
+    }
+
+    /**
+     * @test
+     */
+    public function getParameterTypesDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->getParameterTypes()
+            ->shouldBeCalled()
+            ->willReturn([]);
+
+        $this->subject->getParameterTypes();
+    }
+
+    /**
+     * @test
+     */
+    public function getParameterTypeDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->getParameterType(Argument::exact('aField'))
+            ->shouldBeCalled()
+            ->willReturn(Connection::PARAM_STR);
+
+        $this->subject->getParameterType('aField');
+    }
+
+    /**
+     * @test
+     */
+    public function setFirstResultDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->setFirstResult(Argument::cetera())
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->setFirstResult(1);
+    }
+
+    /**
+     * @test
+     */
+    public function getFirstResultDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->getFirstResult()
+            ->shouldBeCalled()
+            ->willReturn(1);
+
+        $this->subject->getFirstResult();
+    }
+
+    /**
+     * @test
+     */
+    public function setMaxResultsDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->setMaxResults(Argument::cetera())
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->setMaxResults(1);
+    }
+
+    /**
+     * @test
+     */
+    public function getMaxResultsDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->getMaxResults()
+            ->shouldBeCalled()
+            ->willReturn(1);
+
+        $this->subject->getMaxResults();
+    }
+
+    /**
+     * @test
+     */
+    public function addDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->add(Argument::exact('select'), Argument::exact('aField'), Argument::cetera())
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->add('select', 'aField');
+    }
+
+    /**
+     * @test
+     */
+    public function countBuildsExpressionAndCallsSelect()
+    {
+        $this->concreteQueryBuilder->select(Argument::exact('COUNT(*)'))
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->count('*');
+    }
+
+    /**
+     * @test
+     */
+    public function selectQuotesIdentifiersAndDelegatesToConcreteQueryBuilder()
+    {
+        $this->connection->quoteIdentifier('aField')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->connection->quoteIdentifier('anotherField')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->concreteQueryBuilder->select(Argument::exact('aField'), Argument::exact('anotherField'))
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->select('aField', 'anotherField');
+    }
+
+    /**
+     * @test
+     */
+    public function selectDoesNotQuoteStarPlaceholder()
+    {
+        $this->connection->quoteIdentifier('aField')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->connection->quoteIdentifier('*')
+            ->shouldNotBeCalled();
+        $this->concreteQueryBuilder->select(Argument::exact('aField'), Argument::exact('*'))
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->select('aField', '*');
+    }
+
+    /**
+     * @test
+     */
+    public function addSelectQuotesIdentifiersAndDelegatesToConcreteQueryBuilder()
+    {
+        $this->connection->quoteIdentifier('aField')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->connection->quoteIdentifier('anotherField')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->concreteQueryBuilder->addSelect(Argument::exact('aField'), Argument::exact('anotherField'))
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->addSelect('aField', 'anotherField');
+    }
+
+    /**
+     * @test
+     */
+    public function addSelectDoesNotQuoteStarPlaceholder()
+    {
+        $this->connection->quoteIdentifier('aField')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->connection->quoteIdentifier('*')
+            ->shouldNotBeCalled();
+        $this->concreteQueryBuilder->addSelect(Argument::exact('aField'), Argument::exact('*'))
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->addSelect('aField', '*');
+    }
+
+    /**
+     * @test
+     * @todo: Test with alias
+     */
+    public function deleteQuotesIdentifierAndDelegatesToConcreteQueryBuilder()
+    {
+        $this->connection->quoteIdentifier('aTable')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->concreteQueryBuilder->delete(Argument::exact('aTable'), Argument::cetera())
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->delete('aTable');
+    }
+
+    /**
+     * @test
+     * @todo: Test with alias
+     */
+    public function updateQuotesIdentifierAndDelegatesToConcreteQueryBuilder()
+    {
+        $this->connection->quoteIdentifier('aTable')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->concreteQueryBuilder->update(Argument::exact('aTable'), Argument::cetera())
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->update('aTable');
+    }
+
+    /**
+     * @test
+     */
+    public function insertQuotesIdentifierAndDelegatesToConcreteQueryBuilder()
+    {
+        $this->connection->quoteIdentifier('aTable')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->concreteQueryBuilder->insert(Argument::exact('aTable'))
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->insert('aTable');
+    }
+
+    /**
+     * @test
+     * @todo: Test with alias
+     */
+    public function fromQuotesIdentifierAndDelegatesToConcreteQueryBuilder()
+    {
+        $this->connection->quoteIdentifier('aTable')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->concreteQueryBuilder->from(Argument::exact('aTable'), Argument::cetera())
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->from('aTable');
+    }
+
+    /**
+     * @test
+     */
+    public function joinQuotesIdentifiersAndDelegatesToConcreteQueryBuilder()
+    {
+        $this->connection->quoteIdentifier('fromAlias')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->connection->quoteIdentifier('join')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->connection->quoteIdentifier('alias')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->concreteQueryBuilder->innerJoin('fromAlias', 'join', 'alias', null)
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->join('fromAlias', 'join', 'alias');
+    }
+
+    /**
+     * @test
+     */
+    public function innerJoinQuotesIdentifiersAndDelegatesToConcreteQueryBuilder()
+    {
+        $this->connection->quoteIdentifier('fromAlias')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->connection->quoteIdentifier('join')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->connection->quoteIdentifier('alias')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->concreteQueryBuilder->innerJoin('fromAlias', 'join', 'alias', null)
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->innerJoin('fromAlias', 'join', 'alias');
+    }
+
+    /**
+     * @test
+     */
+    public function leftJoinQuotesIdentifiersAndDelegatesToConcreteQueryBuilder()
+    {
+        $this->connection->quoteIdentifier('fromAlias')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->connection->quoteIdentifier('join')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->connection->quoteIdentifier('alias')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->concreteQueryBuilder->leftJoin('fromAlias', 'join', 'alias', null)
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->leftJoin('fromAlias', 'join', 'alias');
+    }
+
+    /**
+     * @test
+     */
+    public function rightJoinQuotesIdentifiersAndDelegatesToConcreteQueryBuilder()
+    {
+        $this->connection->quoteIdentifier('fromAlias')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->connection->quoteIdentifier('join')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->connection->quoteIdentifier('alias')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->concreteQueryBuilder->rightJoin('fromAlias', 'join', 'alias', null)
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->rightJoin('fromAlias', 'join', 'alias');
+    }
+
+    /**
+     * @test
+     */
+    public function setQuotesIdentifierAndDelegatesToConcreteQueryBuilder()
+    {
+        $this->connection->quoteIdentifier('aField')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->concreteQueryBuilder->set('aField', 'aValue')
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->set('aField', 'aValue');
+    }
+
+    /**
+     * @test
+     */
+    public function whereDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->where('uid=1', 'type=9')
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->where('uid=1', 'type=9');
+    }
+
+    /**
+     * @test
+     */
+    public function andWhereDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->andWhere('uid=1', 'type=9')
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->andWhere('uid=1', 'type=9');
+    }
+
+    /**
+     * @test
+     */
+    public function orWhereDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->orWhere('uid=1', 'type=9')
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->orWhere('uid=1', 'type=9');
+    }
+
+    /**
+     * @test
+     */
+    public function groupByQuotesIdentifierAndDelegatesToConcreteQueryBuilder()
+    {
+        $this->connection->quoteIdentifiers(['aField', 'anotherField'])
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->concreteQueryBuilder->groupBy('aField', 'anotherField')
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->groupBy('aField', 'anotherField');
+    }
+
+    /**
+     * @test
+     */
+    public function addGroupByQuotesIdentifierAndDelegatesToConcreteQueryBuilder()
+    {
+        $this->connection->quoteIdentifiers(['aField', 'anotherField'])
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->concreteQueryBuilder->addGroupBy('aField', 'anotherField')
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->addGroupBy('aField', 'anotherField');
+    }
+
+    /**
+     * @test
+     */
+    public function setValueQuotesIdentifierAndDelegatesToConcreteQueryBuilder()
+    {
+        $this->connection->quoteIdentifier('aField')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->concreteQueryBuilder->setValue('aField', 'aValue')
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->setValue('aField', 'aValue');
+    }
+
+    /**
+     * @test
+     */
+    public function valuesQuotesIdentifiersAndDelegatesToConcreteQueryBuilder()
+    {
+        $this->connection->quoteColumnValuePairs(['aField' => 1, 'aValue' => 2])
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->concreteQueryBuilder->values(['aField' => 1, 'aValue' => 2])
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->values(['aField' => 1, 'aValue' => 2]);
+    }
+
+    /**
+     * @test
+     */
+    public function havingDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->having('uid=1', 'type=9')
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->having('uid=1', 'type=9');
+    }
+
+    /**
+     * @test
+     */
+    public function andHavingDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->andHaving('uid=1', 'type=9')
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->andHaving('uid=1', 'type=9');
+    }
+
+    /**
+     * @test
+     */
+    public function orHavingDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->orHaving('uid=1', 'type=9')
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->orHaving('uid=1', 'type=9');
+    }
+
+    /**
+     * @test
+     */
+    public function orderByQuotesIdentifierAndDelegatesToConcreteQueryBuilder()
+    {
+        $this->connection->quoteIdentifier('aField')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->concreteQueryBuilder->orderBy('aField', null)
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->orderBy('aField');
+    }
+
+    /**
+     * @test
+     */
+    public function addOrderByQuotesIdentifierAndDelegatesToConcreteQueryBuilder()
+    {
+        $this->connection->quoteIdentifier('aField')
+            ->shouldBeCalled()
+            ->willReturnArgument(0);
+        $this->concreteQueryBuilder->addOrderBy('aField', 'DESC')
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->addOrderBy('aField', 'DESC');
+    }
+
+    /**
+     * @test
+     */
+    public function getQueryPartDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->getQueryPart('from')
+            ->shouldBeCalled()
+            ->willReturn('aTable');
+
+        $this->subject->getQueryPart('from');
+    }
+
+    /**
+     * @test
+     */
+    public function getQueryPartsDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->getQueryParts()
+            ->shouldBeCalled()
+            ->willReturn([]);
+
+        $this->subject->getQueryParts();
+    }
+
+    /**
+     * @test
+     */
+    public function resetQueryPartsDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->resetQueryParts(['select', 'from'])
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->resetQueryParts(['select', 'from']);
+    }
+
+    /**
+     * @test
+     */
+    public function resetQueryPartDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->resetQueryPart('select')
+            ->shouldBeCalled()
+            ->willReturn($this->subject);
+
+        $this->subject->resetQueryPart('select');
+    }
+
+    /**
+     * @test
+     */
+    public function createNamedParameterDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->createNamedParameter(5, Argument::cetera())
+            ->shouldBeCalled()
+            ->willReturn(':dcValue1');
+
+        $this->subject->createNamedParameter(5);
+    }
+
+    /**
+     * @test
+     */
+    public function createPositionalParameterDelegatesToConcreteQueryBuilder()
+    {
+        $this->concreteQueryBuilder->createPositionalParameter(5, Argument::cetera())
+            ->shouldBeCalled()
+            ->willReturn('?');
+
+        $this->subject->createPositionalParameter(5);
+    }
+
+    /**
+     * @return array
+     */
+    public function deleteConstraintProvider()
+    {
+        return [
+            'no delete field' => [
+                ['aTable'],
+                '',
+                ''
+            ],
+            'without alias' => [
+                ['aTable'],
+                'deleted',
+                'aTable.deleted=0'
+            ],
+            'with alias' => [
+                ['aTable', 'a'],
+                'deleted',
+                'a.deleted=0'
+            ]
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider deleteConstraintProvider
+     * @param array $args
+     * @param string $deleteField
+     * @param string $expected
+     */
+    public function deleteConstraint(array $args, string $deleteField, string $expected)
+    {
+        $this->connection->quoteIdentifier(Argument::cetera())->willReturnArgument(0);
+        $GLOBALS['TCA'][$args[0]]['ctrl']['delete'] = $deleteField;
+
+        $this->assertSame($expected, $this->subject->deleteConstraint(...$args));
+    }
+
+    /**
+     * @test
+     */
+    public function queryRestrictionsAreAddedForSelectOnExecute()
+    {
+        $GLOBALS['TCA']['pages']['ctrl'] = [
+            'tstamp' => 'tstamp',
+            'versioningWS' => true,
+            'delete' => 'deleted',
+            'crdate' => 'crdate',
+            'enablecolumns' => [
+                'disabled' => 'hidden',
+            ],
+        ];
+
+        $this->connection->quoteIdentifier(Argument::cetera())
+            ->willReturnArgument(0);
+        $this->connection->quoteIdentifiers(Argument::cetera())
+            ->willReturnArgument(0);
+
+        $connectionBuilder = GeneralUtility::makeInstance(
+            \Doctrine\DBAL\Query\QueryBuilder::class,
+            $this->connection->reveal()
+        );
+
+        $expressionBuilder = GeneralUtility::makeInstance(ExpressionBuilder::class, $this->connection->reveal());
+        $this->connection->getExpressionBuilder()
+            ->willReturn($expressionBuilder);
+
+        $subject = GeneralUtility::makeInstance(
+            QueryBuilder::class,
+            $this->connection->reveal(),
+            null,
+            $connectionBuilder
+        );
+
+        $subject->select('*')
+            ->from('pages')
+            ->where('uid=1');
+
+        $expectedSQL = 'SELECT * FROM pages WHERE (uid=1) AND ((pages.hidden = 0) AND (pages.deleted = 0))';
+        $this->connection->executeQuery($expectedSQL, Argument::cetera())
+            ->shouldBeCalled();
+
+        $subject->execute();
+    }
+
+    /**
+     * @test
+     */
+    public function queryRestrictionsAreAddedForCountOnExecute()
+    {
+        $GLOBALS['TCA']['pages']['ctrl'] = [
+            'tstamp' => 'tstamp',
+            'versioningWS' => true,
+            'delete' => 'deleted',
+            'crdate' => 'crdate',
+            'enablecolumns' => [
+                'disabled' => 'hidden',
+            ],
+        ];
+
+        $this->connection->quoteIdentifier(Argument::cetera())
+            ->willReturnArgument(0);
+        $this->connection->quoteIdentifiers(Argument::cetera())
+            ->willReturnArgument(0);
+
+        $connectionBuilder = GeneralUtility::makeInstance(
+            \Doctrine\DBAL\Query\QueryBuilder::class,
+            $this->connection->reveal()
+        );
+
+        $expressionBuilder = GeneralUtility::makeInstance(ExpressionBuilder::class, $this->connection->reveal());
+        $this->connection->getExpressionBuilder()
+            ->willReturn($expressionBuilder);
+
+        $subject = GeneralUtility::makeInstance(
+            QueryBuilder::class,
+            $this->connection->reveal(),
+            null,
+            $connectionBuilder
+        );
+
+        $subject->count('uid')
+            ->from('pages')
+            ->where('uid=1');
+
+        $expectedSQL = 'SELECT COUNT(uid) FROM pages WHERE (uid=1) AND ((pages.hidden = 0) AND (pages.deleted = 0))';
+        $this->connection->executeQuery($expectedSQL, Argument::cetera())
+            ->shouldBeCalled();
+
+        $subject->execute();
+    }
+
+    /**
+     * @test
+     */
+    public function queryRestrictionsAreReevaluatedOnSettingsChangeForGetSQL()
+    {
+        $GLOBALS['TCA']['pages']['ctrl'] = [
+            'tstamp' => 'tstamp',
+            'versioningWS' => true,
+            'delete' => 'deleted',
+            'crdate' => 'crdate',
+            'enablecolumns' => [
+                'disabled' => 'hidden',
+            ],
+        ];
+
+        $this->connection->quoteIdentifier(Argument::cetera())
+            ->willReturnArgument(0);
+        $this->connection->quoteIdentifiers(Argument::cetera())
+            ->willReturnArgument(0);
+        $this->connection->getExpressionBuilder()
+            ->willReturn(GeneralUtility::makeInstance(ExpressionBuilder::class, $this->connection->reveal()));
+
+        $concreteQueryBuilder = GeneralUtility::makeInstance(
+            \Doctrine\DBAL\Query\QueryBuilder::class,
+            $this->connection->reveal()
+        );
+
+        $subject = GeneralUtility::makeInstance(
+            QueryBuilder::class,
+            $this->connection->reveal(),
+            null,
+            $concreteQueryBuilder
+        );
+
+        $subject->select('*')
+            ->from('pages')
+            ->where('uid=1');
+
+        $expectedSQL = 'SELECT * FROM pages WHERE (uid=1) AND ((pages.hidden = 0) AND (pages.deleted = 0))';
+        $this->assertSame($expectedSQL, $subject->getSQL());
+
+        $subject->getQueryContext()
+            ->setIgnoreEnableFields(true)
+            ->setIgnoredEnableFields(['disabled']);
+
+        $expectedSQL = 'SELECT * FROM pages WHERE (uid=1) AND (pages.deleted = 0)';
+        $this->assertSame($expectedSQL, $subject->getSQL());
+    }
+
+    /**
+     * @test
+     */
+    public function queryRestrictionsAreReevaluatedOnSettingsChangeForExecute()
+    {
+        $GLOBALS['TCA']['pages']['ctrl'] = [
+            'tstamp' => 'tstamp',
+            'versioningWS' => true,
+            'delete' => 'deleted',
+            'crdate' => 'crdate',
+            'enablecolumns' => [
+                'disabled' => 'hidden',
+            ],
+        ];
+
+        $this->connection->quoteIdentifier(Argument::cetera())
+            ->willReturnArgument(0);
+        $this->connection->quoteIdentifiers(Argument::cetera())
+            ->willReturnArgument(0);
+        $this->connection->getExpressionBuilder()
+            ->willReturn(GeneralUtility::makeInstance(ExpressionBuilder::class, $this->connection->reveal()));
+
+        $concreteQueryBuilder = GeneralUtility::makeInstance(
+            \Doctrine\DBAL\Query\QueryBuilder::class,
+            $this->connection->reveal()
+        );
+
+        $subject = GeneralUtility::makeInstance(
+            QueryBuilder::class,
+            $this->connection->reveal(),
+            null,
+            $concreteQueryBuilder
+        );
+
+        $subject->select('*')
+            ->from('pages')
+            ->where('uid=1');
+
+        $subject->getQueryContext()
+            ->setIgnoreEnableFields(true)
+            ->setIgnoredEnableFields(['disabled']);
+
+        $expectedSQL = 'SELECT * FROM pages WHERE (uid=1) AND (pages.deleted = 0)';
+        $this->connection->executeQuery($expectedSQL, Argument::cetera())
+            ->shouldBeCalled();
+
+        $subject->execute();
+
+        $subject->getQueryContext()
+            ->setIgnoreEnableFields(false);
+
+        $expectedSQL = 'SELECT * FROM pages WHERE (uid=1) AND ((pages.hidden = 0) AND (pages.deleted = 0))';
+        $this->connection->executeQuery($expectedSQL, Argument::cetera())
+            ->shouldBeCalled();
+
+        $subject->execute();
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Query/QueryContextTest.php b/typo3/sysext/core/Tests/Unit/Database/Query/QueryContextTest.php
new file mode 100644 (file)
index 0000000..a3d2862
--- /dev/null
@@ -0,0 +1,283 @@
+<?php
+declare (strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Unit\Database\Query;
+
+/*
+ * 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!
+ */
+
+use Prophecy\Prophecy\ObjectProphecy;
+use TYPO3\CMS\Core\Database\Query\QueryContext;
+use TYPO3\CMS\Core\Tests\UnitTestCase;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
+use TYPO3\CMS\Frontend\Page\PageRepository;
+
+class QueryContextTest extends UnitTestCase
+{
+    /**
+     * @var QueryContext
+     */
+    protected $subject;
+
+    /**
+     * @var TypoScriptFrontendController|ObjectProphecy
+     */
+    protected $typoScriptFrontendController;
+
+    /**
+     * Create a new database connection mock object for every test.
+     *
+     * @return void
+     */
+    protected function setUp()
+    {
+        parent::setUp();
+
+        $this->typoScriptFrontendController = $this->prophesize(TypoScriptFrontendController::class);
+        $GLOBALS['TSFE'] = $this->typoScriptFrontendController->reveal();
+
+        $this->subject = GeneralUtility::makeInstance(QueryContext::class);
+    }
+
+    /**
+     * @test
+     */
+    public function contextCanBeSetByConstructiorArgument()
+    {
+        $subject = GeneralUtility::makeInstance(QueryContext::class, 'FRONTEND');
+
+        $this->assertSame('FRONTEND', $subject->getContext());
+    }
+
+    /**
+     * @test
+     * @expectedException \TYPO3\CMS\Core\Type\Exception\InvalidEnumerationValueException
+     * @expectedExceptionMessage Invalid value DUMMY for TYPO3\CMS\Core\Database\Query\QueryContextType
+     */
+    public function unknownContextThrowExceptionInConstructor()
+    {
+        GeneralUtility::makeInstance(QueryContext::class, 'DUMMY');
+    }
+
+    /**
+     * @test
+     * @expectedException \TYPO3\CMS\Core\Type\Exception\InvalidEnumerationValueException
+     * @expectedExceptionMessage Invalid value DUMMY for TYPO3\CMS\Core\Database\Query\QueryContextType
+     */
+    public function unknownContextThrowExceptionWhenSet()
+    {
+        $this->subject->setContext('DUMMY');
+    }
+
+    /**
+     * @test
+     */
+    public function getMemberGroupsPrefersExplicitlySetInformation()
+    {
+        $GLOBALS['TSFE']->gr_list = '3,5';
+        $this->subject->setMemberGroups([1, 2]);
+
+        $this->assertSame([1, 2], $this->subject->getMemberGroups());
+    }
+
+    /**
+     * @test
+     */
+    public function getMemberGroupsFallsBackToTSFE()
+    {
+        $GLOBALS['TSFE']->gr_list = '3,5';
+
+        $this->assertSame([3, 5], $this->subject->getMemberGroups());
+    }
+
+    /**
+     * @test
+     */
+    public function getCurrentWorkspacePrefersExplicitlySetInformation()
+    {
+        /** @var PageRepository|ObjectProphecy $pageRepository */
+        $pageRepository = $this->prophesize(PageRepository::class);
+        $pageRepository->versioningWorkspaceId = 3;
+
+        $GLOBALS['TSFE']->sys_page = $pageRepository->reveal();
+
+        $this->subject->setCurrentWorkspace(1);
+        $this->subject->setContext('FRONTEND');
+
+        $this->assertSame(1, $this->subject->getCurrentWorkspace());
+    }
+
+    /**
+     * @test
+     */
+    public function getCurrentWorkspaceFallsBackToTSFE()
+    {
+        /** @var PageRepository|ObjectProphecy $pageRepository */
+        $pageRepository = $this->prophesize(PageRepository::class);
+        $pageRepository->versioningWorkspaceId = 3;
+
+        $GLOBALS['TSFE']->sys_page = $pageRepository->reveal();
+
+        $this->subject->setContext('FRONTEND');
+
+        $this->assertSame(3, $this->subject->getCurrentWorkspace());
+    }
+
+    /**
+     * @test
+     */
+    public function getAccessTimePrefersExplicitlySetInformation()
+    {
+        $GLOBALS['SIM_ACCESS_TIME'] = 100;
+        $this->subject->setAccessTime(200);
+
+        $this->assertSame(200, $this->subject->getAccessTime());
+    }
+
+    /**
+     * @test
+     */
+    public function getAccessTimeFallsBackToTSFE()
+    {
+        $GLOBALS['SIM_ACCESS_TIME'] = 100;
+
+        $this->assertSame(100, $this->subject->getAccessTime());
+    }
+
+    /**
+     * @test
+     */
+    public function getIncludeHiddenForTablePrefersExplicitlySetInformation()
+    {
+        $GLOBALS['TSFE']->showHiddenPage = false;
+        $GLOBALS['TSFE']->showHiddenRecords = false;
+        $this->subject->setIncludeHidden(true);
+
+        $this->assertSame(true, $this->subject->getIncludeHiddenForTable('pages'));
+    }
+
+    /**
+     * @test
+     */
+    public function getIncludeHiddenForTablePagesFallsBackToTSFE()
+    {
+        $GLOBALS['TSFE']->showHiddenPage = true;
+
+        $this->assertSame(true, $this->subject->getIncludeHiddenForTable('pages'));
+    }
+
+    /**
+     * @test
+     */
+    public function getIncludeHiddenForTablePagesLanguageOverlayFallsBackToTSFE()
+    {
+        $GLOBALS['TSFE']->showHiddenPage = true;
+
+        $this->assertSame(true, $this->subject->getIncludeHiddenForTable('pages'));
+    }
+
+    /**
+     * @test
+     */
+    public function getIncludeHiddenForRecordsFallsBackToTSFE()
+    {
+        $GLOBALS['TSFE']->showHiddenRecords = true;
+
+        $this->assertSame(true, $this->subject->getIncludeHiddenForTable('tt_content'));
+    }
+
+    /**
+     * @test
+     */
+    public function getIncludePlaceholdersPrefersExplicitlySetInformation()
+    {
+        $this->subject->setIncludePlaceholders(true);
+
+        $this->assertSame(true, $this->subject->getIncludePlaceholders());
+    }
+
+    /**
+     * @test
+     */
+    public function getIncludePlaceholdersFallsBackToTSFE()
+    {
+        /** @var PageRepository|ObjectProphecy $pageRepository */
+        $pageRepository = $this->prophesize(PageRepository::class);
+        $pageRepository->versioningPreview = true;
+
+        $GLOBALS['TSFE']->sys_page = $pageRepository->reveal();
+
+        $this->subject->setContext('FRONTEND');
+        $this->assertSame(true, $this->subject->getIncludePlaceholders());
+    }
+
+    /**
+     * @test
+     */
+    public function getIgnoredEnableFieldsForTableFallsBackToGlobalList()
+    {
+        $this->subject->setIgnoredEnableFields(['disabled']);
+
+        $this->assertSame(['disabled'], $this->subject->getIgnoredEnableFieldsForTable('pages'));
+    }
+
+    /**
+     * @test
+     */
+    public function getIgnoredEnableFieldsForTablePrefersExplictlySetInformation()
+    {
+        $this->subject->setIgnoredEnableFields(['disabled']);
+        $this->subject->setIgnoredEnableFieldsForTable('pages', ['starttime', 'endtime']);
+
+        $this->assertSame(['starttime', 'endtime'], $this->subject->getIgnoredEnableFieldsForTable('pages'));
+    }
+
+    /**
+     * @test
+     */
+    public function getIgnoredEnableFieldsForTableReturnsEmptyArrayWithoutInformation()
+    {
+        $this->assertSame([], $this->subject->getIgnoredEnableFieldsForTable('pages'));
+    }
+
+    /**
+     * @test
+     */
+    public function getTableConfigPrefersExplicitlySetInformation()
+    {
+        $this->subject->setTableConfigs(['pages' => ['delete' => 'deleted']]);
+        $GLOBALS['TCA']['pages']['ctrl'] = ['delete' => 'deleted'];
+
+        $this->assertSame(['delete' => 'deleted'], $this->subject->getTableConfig('pages'));
+    }
+
+    /**
+     * @test
+     */
+    public function getTableConfigFallsBackToTCA()
+    {
+        $GLOBALS['TCA']['pages']['ctrl'] = [
+            'label' => 'title',
+            'tstamp' => 'tstamp',
+            'delete' => 'deleted',
+            'enablecolumns' => [
+                'disabled' => 'hidden',
+            ],
+        ];
+
+        $this->assertSame(
+            ['delete' => 'deleted', 'enablecolumns' => ['disabled' => 'hidden']],
+            $this->subject->getTableConfig('pages')
+        );
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Query/QueryRestrictionBuilderTest.php b/typo3/sysext/core/Tests/Unit/Database/Query/QueryRestrictionBuilderTest.php
new file mode 100644 (file)
index 0000000..e32b070
--- /dev/null
@@ -0,0 +1,913 @@
+<?php
+declare (strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Unit\Database\Query;
+
+/*
+ * 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!
+ */
+
+use Prophecy\Argument;
+use TYPO3\CMS\Core\Database\Connection;
+use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
+use TYPO3\CMS\Core\Database\Query\QueryContext;
+use TYPO3\CMS\Core\Database\Query\QueryRestrictionBuilder;
+use TYPO3\CMS\Core\Tests\Unit\Database\Mocks\MockPlatform;
+use TYPO3\CMS\Core\Tests\UnitTestCase;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+class QueryRestrictionBuilderTest extends UnitTestCase
+{
+    /**
+     * @var array
+     */
+    protected $defaultTableConfig = [
+        'versioningWS' => true,
+        'delete' => 'deleted',
+        'enablecolumns' => [
+            'disabled' => 'hidden',
+            'starttime' => 'starttime',
+            'endtime' => 'endtime',
+            'fe_group' => 'fe_group',
+        ],
+    ];
+
+    /**
+     * @var \TYPO3\CMS\Frontend\Page\PageRepository
+     */
+    protected $pageRepository;
+
+    /**
+     * @var \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
+     */
+    protected $expressionBuilder;
+
+    /**
+     * @var Connection|\Prophecy\Prophecy\ObjectProphecy
+     */
+    protected $connection;
+
+    /**
+     * @var QueryContext
+     */
+    protected $queryContext;
+
+    /**
+     * Create a new database connection mock object for every test.
+     *
+     * @return void
+     */
+    protected function setUp()
+    {
+        parent::setUp();
+
+        $this->connection = $this->prophesize(Connection::class);
+        $this->connection->quoteIdentifier(Argument::cetera())->will(function ($args) {
+            return '"' . join('"."', explode('.', $args[0])) . '"';
+        });
+        $this->connection->quote(Argument::cetera())->will(function ($args) {
+            return "'" . $args[0] . "'";
+        });
+        $this->connection->getDatabasePlatform()->willReturn(new MockPlatform());
+
+        $this->queryContext = GeneralUtility::makeInstance(QueryContext::class);
+        $this->expressionBuilder = GeneralUtility::makeInstance(ExpressionBuilder::class, $this->connection->reveal());
+    }
+
+    /**
+     * @test
+     */
+    public function getVisibilityConstraintsReturnsEmptyConstraintForNoneContext()
+    {
+        $this->queryContext->setContext('none');
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            [],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $this->assertEmpty((string)$subject->getVisibilityConstraints());
+    }
+
+    /**
+     * @test
+     */
+    public function getFrontendVisibilityRestrictionsSkipsUnconfiguredTables()
+    {
+        $this->queryContext->setContext('frontend');
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            ['pages' => ''],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $this->assertEmpty((string)$subject->getVisibilityConstraints());
+    }
+
+    /**
+     * @test
+     */
+    public function getFrontendVisibilityRestrictionsWithDefaultSettings()
+    {
+        $this->queryContext->setContext('frontend')
+            ->setAccessTime(1459706700)
+            ->setTableConfigs(['pages' => $this->defaultTableConfig]);
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            ['pages' => ''],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $expectedSql = join(' AND ', [
+            '("pages"."deleted" = 0)',
+            '("pages"."t3ver_state" <= 0)',
+            '("pages"."pid" <> -1)',
+            '("pages"."hidden" = 0)',
+            '("pages"."starttime" <= 1459706700)',
+            '(("pages"."endtime" = 0) OR ("pages"."endtime" > 1459706700))',
+            '(("pages"."fe_group" IS NULL) OR ("pages"."fe_group" = \'\') OR ("pages"."fe_group" = \'0\'))'
+        ]);
+
+        $this->assertSame($expectedSql, (string)$subject->getVisibilityConstraints());
+    }
+
+    /**
+     * @test
+     */
+    public function getFrontendVisibilityRestrictionsWithUserGroups()
+    {
+        $this->queryContext->setContext('frontend')
+            ->setAccessTime(1459706700)
+            ->setMemberGroups([1, 2])
+            ->setTableConfigs(['pages' => $this->defaultTableConfig]);
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            ['pages' => ''],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $expectedSql = join(' AND ', [
+            '("pages"."deleted" = 0)',
+            '("pages"."t3ver_state" <= 0)',
+            '("pages"."pid" <> -1)',
+            '("pages"."hidden" = 0)',
+            '("pages"."starttime" <= 1459706700)',
+            '(("pages"."endtime" = 0) OR ("pages"."endtime" > 1459706700))',
+            '(("pages"."fe_group" IS NULL) OR ("pages"."fe_group" = \'\') OR ("pages"."fe_group" = \'0\') OR (FIND_IN_SET(\'1\', "pages"."fe_group")) OR (FIND_IN_SET(\'2\', "pages"."fe_group")))'
+        ]);
+
+        $this->assertSame($expectedSql, (string)$subject->getVisibilityConstraints());
+    }
+
+    /**
+     * @test
+     */
+    public function getFrontendVisibilityRestrictionsWithVersioningPreview()
+    {
+        $this->queryContext->setContext('frontend')
+            ->setAccessTime(1459706700)
+            ->setIncludePlaceholders(true)
+            ->setTableConfigs(['pages' => $this->defaultTableConfig]);
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            ['pages' => ''],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $expectedSql = '("pages"."deleted" = 0) AND ("pages"."pid" <> -1)';
+
+        $this->assertSame($expectedSql, (string)$subject->getVisibilityConstraints());
+    }
+
+    /**
+     * @test
+     */
+    public function getFrontendVisibilityRestrictionsWithVersioningPreviewAndNoPreviewSet()
+    {
+        $this->queryContext->setContext('frontend')
+            ->setAccessTime(1459706700)
+            ->setIncludePlaceholders(true)
+            ->setIncludeVersionedRecords(true)
+            ->setTableConfigs(['pages' => $this->defaultTableConfig]);
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            ['pages' => ''],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $expectedSql = join(' AND ', [
+            '("pages"."deleted" = 0)',
+            '("pages"."hidden" = 0)',
+            '("pages"."starttime" <= 1459706700)',
+            '(("pages"."endtime" = 0) OR ("pages"."endtime" > 1459706700))',
+            '(("pages"."fe_group" IS NULL) OR ("pages"."fe_group" = \'\') OR ("pages"."fe_group" = \'0\'))'
+        ]);
+
+        $this->assertSame($expectedSql, (string)$subject->getVisibilityConstraints());
+    }
+
+    /**
+     * @test
+     */
+    public function getFrontendVisibilityRestrictionsWithoutDisabledColumn()
+    {
+        $this->queryContext->setContext('frontend')
+            ->setAccessTime(1459706700)
+            ->setTableConfigs(['pages' => [
+                'versioningWS' => true,
+                'delete' => 'deleted',
+                'enablecolumns' => [
+                    'starttime' => 'starttime',
+                    'endtime' => 'endtime',
+                    'fe_group' => 'fe_group',
+                ]
+            ]]);
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            ['pages' => ''],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $expectedSql = join(' AND ', [
+            '("pages"."deleted" = 0)',
+            '("pages"."t3ver_state" <= 0)',
+            '("pages"."pid" <> -1)',
+            '("pages"."starttime" <= 1459706700)',
+            '(("pages"."endtime" = 0) OR ("pages"."endtime" > 1459706700))',
+            '(("pages"."fe_group" IS NULL) OR ("pages"."fe_group" = \'\') OR ("pages"."fe_group" = \'0\'))'
+        ]);
+
+        $this->assertSame($expectedSql, (string)$subject->getVisibilityConstraints());
+    }
+
+    /**
+     * @test
+     */
+    public function getFrontendVisibilityRestrictionsWithoutStarttimeColumn()
+    {
+        $this->queryContext->setContext('frontend')
+            ->setAccessTime(1459706700)
+            ->setTableConfigs([
+                'pages' => [
+                    'versioningWS' => true,
+                    'delete' => 'deleted',
+                    'enablecolumns' => [
+                        'disabled' => 'hidden',
+                        'endtime' => 'endtime',
+                        'fe_group' => 'fe_group',
+                    ]
+                ]
+            ]);
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            ['pages' => ''],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $expectedSql = join(' AND ', [
+            '("pages"."deleted" = 0)',
+            '("pages"."t3ver_state" <= 0)',
+            '("pages"."pid" <> -1)',
+            '("pages"."hidden" = 0)',
+            '(("pages"."endtime" = 0) OR ("pages"."endtime" > 1459706700))',
+            '(("pages"."fe_group" IS NULL) OR ("pages"."fe_group" = \'\') OR ("pages"."fe_group" = \'0\'))'
+        ]);
+
+        $this->assertSame($expectedSql, (string)$subject->getVisibilityConstraints());
+    }
+
+    /**
+     * @test
+     */
+    public function getFrontendVisibilityRestrictionsWithoutEndtimeColumn()
+    {
+        $this->queryContext->setContext('frontend')
+            ->setAccessTime(1459706700)
+            ->setTableConfigs([
+                'pages' => [
+                    'versioningWS' => true,
+                    'delete' => 'deleted',
+                    'enablecolumns' => [
+                        'disabled' => 'hidden',
+                        'starttime' => 'starttime',
+                        'fe_group' => 'fe_group',
+                    ]
+                ]
+            ]);
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            ['pages' => ''],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $expectedSql = join(' AND ', [
+            '("pages"."deleted" = 0)',
+            '("pages"."t3ver_state" <= 0)',
+            '("pages"."pid" <> -1)',
+            '("pages"."hidden" = 0)',
+            '("pages"."starttime" <= 1459706700)',
+            '(("pages"."fe_group" IS NULL) OR ("pages"."fe_group" = \'\') OR ("pages"."fe_group" = \'0\'))'
+        ]);
+
+        $this->assertSame($expectedSql, (string)$subject->getVisibilityConstraints());
+    }
+
+    /**
+     * @test
+     */
+    public function getFrontendVisibilityRestrictionsWithoutUsergroupsColumn()
+    {
+        $this->queryContext->setContext('frontend')
+            ->setAccessTime(1459706700)
+            ->setTableConfigs([
+                'pages' => [
+                    'versioningWS' => true,
+                    'delete' => 'deleted',
+                    'enablecolumns' => [
+                        'disabled' => 'hidden',
+                        'starttime' => 'starttime',
+                        'endtime' => 'endtime',
+                    ]
+                ]
+            ]);
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            ['pages' => ''],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $expectedSql = join(' AND ', [
+            '("pages"."deleted" = 0)',
+            '("pages"."t3ver_state" <= 0)',
+            '("pages"."pid" <> -1)',
+            '("pages"."hidden" = 0)',
+            '("pages"."starttime" <= 1459706700)',
+            '(("pages"."endtime" = 0) OR ("pages"."endtime" > 1459706700))',
+        ]);
+
+        $this->assertSame($expectedSql, (string)$subject->getVisibilityConstraints());
+    }
+
+    /**
+     * @test
+     */
+    public function getFrontendVisibilityRestrictionsWithIgnoreEnableFieldsSet()
+    {
+        $this->queryContext->setContext('frontend')
+            ->setAccessTime(1459706700)
+            ->setTableConfigs(['pages' => $this->defaultTableConfig])
+            ->setIgnoreEnableFields(true);
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            ['pages' => ''],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $expectedSql = '"pages"."deleted" = 0';
+
+        $this->assertSame($expectedSql, (string)$subject->getVisibilityConstraints());
+    }
+
+    /**
+     * Data provider for getFrontendVisibilityRestrictionsWithSelectiveIgnoreEnableFieldsSet
+     *
+     * @return array
+     */
+    public function getFrontendVisibilityRestrictionsIgnoreEnableFieldsDataProvider()
+    {
+        return [
+            'disabled' => [
+                ['disabled'],
+            ],
+            'starttime' => [
+                ['starttime'],
+            ],
+            'endtime' => [
+                ['endtime'],
+            ],
+            'starttime, endtime' => [
+                ['starttime', 'endtime'],
+            ],
+            'fe_group' => [
+                ['fe_group'],
+            ],
+            'disabled, starttime, endtime' => [
+                ['disabled', 'starttime', 'endtime'],
+            ],
+            'disabled, starttime, endtime, fe_group' => [
+                ['disabled', 'starttime', 'endtime', 'fe_group'],
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider getFrontendVisibilityRestrictionsIgnoreEnableFieldsDataProvider
+     * @param string[] $ignoreFields
+     */
+    public function getFrontendVisibilityRestrictionsWithSelectiveIgnoreEnableFieldsSet(array $ignoreFields)
+    {
+        $this->queryContext->setContext('frontend')
+            ->setAccessTime(1459706700)
+            ->setTableConfigs(['pages' => $this->defaultTableConfig])
+            ->setIgnoreEnableFields(true)
+            ->setIgnoredEnableFields($ignoreFields);
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            ['pages' => ''],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $expectedSqlFragments = [
+            'deleted' => '("pages"."deleted" = 0)',
+            'versioningWS' => '("pages"."t3ver_state" <= 0) AND ("pages"."pid" <> -1)',
+            'disabled' => '("pages"."hidden" = 0)',
+            'starttime' => '("pages"."starttime" <= 1459706700)',
+            'endtime' => '(("pages"."endtime" = 0) OR ("pages"."endtime" > 1459706700))',
+            'fe_group' => '(("pages"."fe_group" IS NULL) OR ("pages"."fe_group" = \'\') OR ("pages"."fe_group" = \'0\'))'
+        ];
+
+        foreach ($ignoreFields as $fragmentName) {
+            unset($expectedSqlFragments[$fragmentName]);
+        }
+
+        $this->assertSame(join(' AND ', $expectedSqlFragments), (string)$subject->getVisibilityConstraints());
+    }
+
+    /**
+     * @test
+     */
+    public function getFrontendVisibilityRestrictionsForMultipleTablesWithDefaultSettings()
+    {
+        $this->queryContext->setContext('frontend')
+            ->setAccessTime(1459706700)
+            ->setTableConfigs([
+                'pages' => $this->defaultTableConfig,
+                'tt_content' => $this->defaultTableConfig,
+            ]);
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            ['pages' => '', 'tt_content' => ''],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $expectedSqlPages = join(' AND ', [
+            '("pages"."deleted" = 0)',
+            '("pages"."t3ver_state" <= 0)',
+            '("pages"."pid" <> -1)',
+            '("pages"."hidden" = 0)',
+            '("pages"."starttime" <= 1459706700)',
+            '(("pages"."endtime" = 0) OR ("pages"."endtime" > 1459706700))',
+            '(("pages"."fe_group" IS NULL) OR ("pages"."fe_group" = \'\') OR ("pages"."fe_group" = \'0\'))'
+        ]);
+
+        $expectedSqlTtContent = join(' AND ', [
+            '("tt_content"."deleted" = 0)',
+            '("tt_content"."t3ver_state" <= 0)',
+            '("tt_content"."pid" <> -1)',
+            '("tt_content"."hidden" = 0)',
+            '("tt_content"."starttime" <= 1459706700)',
+            '(("tt_content"."endtime" = 0) OR ("tt_content"."endtime" > 1459706700))',
+            '(("tt_content"."fe_group" IS NULL) OR ("tt_content"."fe_group" = \'\') OR ("tt_content"."fe_group" = \'0\'))'
+        ]);
+
+        $this->assertSame(
+            '(' . $expectedSqlPages . ') AND (' . $expectedSqlTtContent . ')',
+            (string)$subject->getVisibilityConstraints()
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function getFrontendVisibilityRestrictionsForMultipleTablesWithIgnoreEnableFields()
+    {
+        $this->queryContext->setContext('frontend')
+            ->setAccessTime(1459706700)
+            ->setTableConfigs([
+                'pages' => $this->defaultTableConfig,
+                'tt_content' => $this->defaultTableConfig,
+            ])
+            ->setIgnoreEnableFields(true);
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            ['pages' => '', 'tt_content' => ''],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $expectedSql = join(' AND ', [
+            '("pages"."deleted" = 0)',
+            '("tt_content"."deleted" = 0)',
+        ]);
+
+        $this->assertSame($expectedSql, (string)$subject->getVisibilityConstraints());
+    }
+
+    /**
+     * @test
+     */
+    public function getFrontendVisibilityRestrictionsForMultipleTablesWithDifferentEnableColumns()
+    {
+        $this->queryContext->setContext('frontend')
+            ->setAccessTime(1459706700)
+            ->setTableConfigs([
+                'pages' => $this->defaultTableConfig,
+                'tt_content' => [
+                    'versioningWS' => false,
+                    'delete' => 'deleted',
+                    'enablecolumns' => [
+                        'disabled' => 'hidden',
+                    ],
+                ],
+            ]);
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            ['pages' => '', 'tt_content' => ''],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $expectedSql = join(' AND ', [
+            '("pages"."deleted" = 0)',
+            '("pages"."t3ver_state" <= 0)',
+            '("pages"."pid" <> -1)',
+            '("pages"."hidden" = 0)',
+            '("pages"."starttime" <= 1459706700)',
+            '(("pages"."endtime" = 0) OR ("pages"."endtime" > 1459706700))',
+            '(("pages"."fe_group" IS NULL) OR ("pages"."fe_group" = \'\') OR ("pages"."fe_group" = \'0\'))'
+        ]);
+
+        $this->assertSame(
+            '(' . $expectedSql . ') AND (("tt_content"."deleted" = 0) AND ("tt_content"."hidden" = 0))',
+            (string)$subject->getVisibilityConstraints()
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function getFrontendVisibilityRestrictionsForJoinedTablesWithDefaultSettings()
+    {
+        $this->queryContext->setContext('frontend')
+            ->setAccessTime(1459706700)
+            ->setTableConfigs([
+                'pages' => $this->defaultTableConfig,
+                'tt_content' => $this->defaultTableConfig,
+            ]);
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            ['pages' => '', 'tt_content' => 't'],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $expectedSqlPages = join(' AND ', [
+            '("pages"."deleted" = 0)',
+            '("pages"."t3ver_state" <= 0)',
+            '("pages"."pid" <> -1)',
+            '("pages"."hidden" = 0)',
+            '("pages"."starttime" <= 1459706700)',
+            '(("pages"."endtime" = 0) OR ("pages"."endtime" > 1459706700))',
+            '(("pages"."fe_group" IS NULL) OR ("pages"."fe_group" = \'\') OR ("pages"."fe_group" = \'0\'))'
+        ]);
+
+        $expectedSqlTtContent = join(' AND ', [
+            '("t"."deleted" = 0)',
+            '("t"."t3ver_state" <= 0)',
+            '("t"."pid" <> -1)',
+            '("t"."hidden" = 0)',
+            '("t"."starttime" <= 1459706700)',
+            '(("t"."endtime" = 0) OR ("t"."endtime" > 1459706700))',
+            '(("t"."fe_group" IS NULL) OR ("t"."fe_group" = \'\') OR ("t"."fe_group" = \'0\'))'
+        ]);
+
+        $this->assertSame(
+            '(' . $expectedSqlPages . ') AND (' . $expectedSqlTtContent . ')',
+            (string)$subject->getVisibilityConstraints()
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function getFrontendVisibilityRestrictionsForJoinedTablesWithIgnoreEnableFields()
+    {
+        $this->queryContext->setContext('frontend')
+            ->setAccessTime(1459706700)
+            ->setTableConfigs([
+                'pages' => $this->defaultTableConfig,
+                'tt_content' => $this->defaultTableConfig,
+            ])
+            ->setIgnoreEnableFields(true);
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            ['pages' => '', 'tt_content' => 't'],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $expectedSql = join(' AND ', [
+            '("pages"."deleted" = 0)',
+            '("t"."deleted" = 0)',
+        ]);
+
+        $this->assertSame($expectedSql, (string)$subject->getVisibilityConstraints());
+    }
+
+    /**
+     * @test
+     */
+    public function getFrontendVisibilityRestrictionsForJoinedTablesWithDifferentEnableColumns()
+    {
+        $this->queryContext->setContext('frontend')
+            ->setAccessTime(1459706700)
+            ->setTableConfigs([
+                'pages' => $this->defaultTableConfig,
+                'tt_content' => [
+                    'versioningWS' => false,
+                    'delete' => 'deleted',
+                    'enablecolumns' => [
+                        'disabled' => 'hidden',
+                    ],
+                ],
+            ]);
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            ['pages' => '', 'tt_content' => 't'],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $expectedSql = join(' AND ', [
+            '("pages"."deleted" = 0)',
+            '("pages"."t3ver_state" <= 0)',
+            '("pages"."pid" <> -1)',
+            '("pages"."hidden" = 0)',
+            '("pages"."starttime" <= 1459706700)',
+            '(("pages"."endtime" = 0) OR ("pages"."endtime" > 1459706700))',
+            '(("pages"."fe_group" IS NULL) OR ("pages"."fe_group" = \'\') OR ("pages"."fe_group" = \'0\'))'
+        ]);
+
+        $this->assertSame(
+            '(' . $expectedSql . ') AND (("t"."deleted" = 0) AND ("t"."hidden" = 0))',
+            (string)$subject->getVisibilityConstraints()
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function getBackendVisibilityRestrictionsSkipsUnconfiguredTables()
+    {
+        $this->queryContext->setContext('backend');
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            ['pages' => ''],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $this->assertEmpty((string)$subject->getVisibilityConstraints());
+    }
+
+    /**
+     * @test
+     */
+    public function getBackendVisibilityRestrictionsWithDefaultSettings()
+    {
+        $this->queryContext->setContext('backend')
+            ->setAccessTime(1459706700)
+            ->setTableConfigs(['pages' => $this->defaultTableConfig]);
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            ['pages' => ''],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $expectedSql = join(' AND ', [
+            '("pages"."hidden" = 0)',
+            '("pages"."starttime" <= 1459706700)',
+            '(("pages"."endtime" = 0) OR ("pages"."endtime" > 1459706700))',
+            '("pages"."deleted" = 0)',
+        ]);
+
+        $this->assertSame($expectedSql, (string)$subject->getVisibilityConstraints());
+    }
+
+    /**
+     * @test
+     */
+    public function getBackendVisibilityRestrictionsWithoutDisabledColumn()
+    {
+        $this->queryContext->setContext('backend')
+            ->setAccessTime(1459706700)
+            ->setTableConfigs(['pages' => [
+                'versioningWS' => true,
+                'delete' => 'deleted',
+                'enablecolumns' => [
+                    'starttime' => 'starttime',
+                    'endtime' => 'endtime',
+                    'fe_group' => 'fe_group',
+                ],
+            ]]);
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            ['pages' => ''],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $expectedSql = join(' AND ', [
+            '("pages"."starttime" <= 1459706700)',
+            '(("pages"."endtime" = 0) OR ("pages"."endtime" > 1459706700))',
+            '("pages"."deleted" = 0)',
+        ]);
+
+        $this->assertSame($expectedSql, (string)$subject->getVisibilityConstraints());
+    }
+
+    /**
+     * @test
+     */
+    public function getBackendVisibilityRestrictionsWithoutStarttimeColumn()
+    {
+        $this->queryContext->setContext('backend')
+            ->setAccessTime(1459706700)
+            ->setTableConfigs([
+                'pages' => [
+                    'versioningWS' => true,
+                    'delete' => 'deleted',
+                    'enablecolumns' => [
+                        'disabled' => 'hidden',
+                        'endtime' => 'endtime',
+                        'fe_group' => 'fe_group',
+                    ],
+                ]
+            ]);
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            ['pages' => ''],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $expectedSql = join(' AND ', [
+            '("pages"."hidden" = 0)',
+            '(("pages"."endtime" = 0) OR ("pages"."endtime" > 1459706700))',
+            '("pages"."deleted" = 0)',
+        ]);
+
+        $this->assertSame($expectedSql, (string)$subject->getVisibilityConstraints());
+    }
+
+    /**
+     * @test
+     */
+    public function getBackendVisibilityRestrictionsWithoutEndtimeColumn()
+    {
+        $this->queryContext->setContext('backend')
+            ->setAccessTime(1459706700)
+            ->setTableConfigs([
+                'pages' => [
+                    'versioningWS' => true,
+                    'delete' => 'deleted',
+                    'enablecolumns' => [
+                        'disabled' => 'hidden',
+                        'starttime' => 'starttime',
+                        'fe_group' => 'fe_group',
+                    ],
+                ]
+            ]);
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            ['pages' => ''],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $expectedSql = join(' AND ', [
+            '("pages"."hidden" = 0)',
+            '("pages"."starttime" <= 1459706700)',
+            '("pages"."deleted" = 0)',
+        ]);
+
+        $this->assertSame($expectedSql, (string)$subject->getVisibilityConstraints());
+    }
+
+    /**
+     * @test
+     */
+    public function getBackendVisibilityRestrictionsWithIgnoreEnableFieldsSet()
+    {
+        $this->queryContext->setContext('backend')
+            ->setAccessTime(1459706700)
+            ->setTableConfigs(['pages' => $this->defaultTableConfig])
+            ->setIgnoreEnableFields(true);
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            ['pages' => ''],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $expectedSql = '"pages"."deleted" = 0';
+
+        $this->assertSame($expectedSql, (string)$subject->getVisibilityConstraints());
+    }
+
+    /**
+     * @test
+     */
+    public function getBackendVisibilityRestrictionsWithIncludeDeletedSet()
+    {
+        $this->queryContext->setContext('backend')
+            ->setAccessTime(1459706700)
+            ->setTableConfigs(['pages' => $this->defaultTableConfig])
+            ->setIncludeDeleted(true);
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            ['pages' => ''],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $expectedSql = join(' AND ', [
+            'disabled' => '("pages"."hidden" = 0)',
+            'starttime' => '("pages"."starttime" <= 1459706700)',
+            'endtime' => '(("pages"."endtime" = 0) OR ("pages"."endtime" > 1459706700))',
+        ]);
+
+        $this->assertSame($expectedSql, (string)$subject->getVisibilityConstraints());
+    }
+
+    /**
+     * @test
+     */
+    public function getBackendVisibilityRestrictionsWithoutRestrictions()
+    {
+        $this->queryContext->setContext('backend')
+            ->setAccessTime(1459706700)
+            ->setTableConfigs(['pages' => $this->defaultTableConfig])
+            ->setIncludeDeleted(true)
+            ->setIgnoreEnableFields(true);
+
+        $subject = GeneralUtility::makeInstance(
+            QueryRestrictionBuilder::class,
+            ['pages' => ''],
+            $this->expressionBuilder,
+            $this->queryContext
+        );
+
+        $this->assertSame('', (string)$subject->getVisibilityConstraints());
+    }
+
+    // @todo: Test for per table overrides
+}
index 63de344..292003a 100644 (file)
@@ -213,12 +213,12 @@ abstract class AbstractAction implements ActionInterface
         if (!is_object($database)) {
             /** @var \TYPO3\CMS\Core\Database\DatabaseConnection $database */
             $database = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\DatabaseConnection::class);
-            $database->setDatabaseUsername($GLOBALS['TYPO3_CONF_VARS']['DB']['username']);
-            $database->setDatabasePassword($GLOBALS['TYPO3_CONF_VARS']['DB']['password']);
-            $database->setDatabaseHost($GLOBALS['TYPO3_CONF_VARS']['DB']['host']);
-            $database->setDatabasePort($GLOBALS['TYPO3_CONF_VARS']['DB']['port']);
-            $database->setDatabaseSocket($GLOBALS['TYPO3_CONF_VARS']['DB']['socket']);
-            $database->setDatabaseName($GLOBALS['TYPO3_CONF_VARS']['DB']['database']);
+            $database->setDatabaseUsername($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user']);
+            $database->setDatabasePassword($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['password']);
+            $database->setDatabaseHost($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host']);
+            $database->setDatabasePort($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['port']);
+            $database->setDatabaseSocket($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['unix_socket']);
+            $database->setDatabaseName($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname']);
             $database->initialize();
             $database->connectDB();
         }
index d86cd33..5dba4ad 100644 (file)
@@ -90,7 +90,7 @@ class DatabaseConnect extends AbstractStepAction
             if (isset($postValues['username'])) {
                 $value = $postValues['username'];
                 if (strlen($value) <= 50) {
-                    $localConfigurationPathValuePairs['DB/username'] = $value;
+                    $localConfigurationPathValuePairs['DB/Connections/Default/user'] = $value;
                 } else {
                     /** @var $errorStatus \TYPO3\CMS\Install\Status\ErrorStatus */
                     $errorStatus = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
@@ -103,7 +103,7 @@ class DatabaseConnect extends AbstractStepAction
             if (isset($postValues['password'])) {
                 $value = $postValues['password'];
                 if (strlen($value) <= 50) {
-                    $localConfigurationPathValuePairs['DB/password'] = $value;
+                    $localConfigurationPathValuePairs['DB/Connections/Default/password'] = $value;
                 } else {
                     /** @var $errorStatus \TYPO3\CMS\Install\Status\ErrorStatus */
                     $errorStatus = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
@@ -116,7 +116,7 @@ class DatabaseConnect extends AbstractStepAction
             if (isset($postValues['host'])) {
                 $value = $postValues['host'];
                 if (preg_match('/^[a-zA-Z0-9_\\.-]+(:.+)?$/', $value) && strlen($value) <= 50) {
-                    $localConfigurationPathValuePairs['DB/host'] = $value;
+                    $localConfigurationPathValuePairs['DB/Connections/Default/host'] = $value;
                 } else {
                     /** @var $errorStatus \TYPO3\CMS\Install\Status\ErrorStatus */
                     $errorStatus = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
@@ -129,7 +129,7 @@ class DatabaseConnect extends AbstractStepAction
             if (isset($postValues['port']) && $postValues['host'] !== 'localhost') {
                 $value = $postValues['port'];
                 if (preg_match('/^[0-9]+(:.+)?$/', $value) && $value > 0 && $value <= 65535) {
-                    $localConfigurationPathValuePairs['DB/port'] = (int)$value;
+                    $localConfigurationPathValuePairs['DB/Connections/Default/port'] = (int)$value;
                 } else {
                     /** @var $errorStatus \TYPO3\CMS\Install\Status\ErrorStatus */
                     $errorStatus = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
@@ -141,7 +141,7 @@ class DatabaseConnect extends AbstractStepAction
 
             if (isset($postValues['socket']) && $postValues['socket'] !== '') {
                 if (@file_exists($postValues['socket'])) {
-                    $localConfigurationPathValuePairs['DB/socket'] = $postValues['socket'];
+                    $localConfigurationPathValuePairs['DB/Connections/Default/unix_socket'] = $postValues['socket'];
                 } else {
                     /** @var $errorStatus \TYPO3\CMS\Install\Status\ErrorStatus */
                     $errorStatus = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
@@ -154,7 +154,7 @@ class DatabaseConnect extends AbstractStepAction
             if (isset($postValues['database'])) {
                 $value = $postValues['database'];
                 if (strlen($value) <= 50) {
-                    $localConfigurationPathValuePairs['DB/database'] = $value;
+                    $localConfigurationPathValuePairs['DB/Connections/Default/dbname'] = $value;
                 } else {
                     /** @var $errorStatus \TYPO3\CMS\Install\Status\ErrorStatus */
                     $errorStatus = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
@@ -225,8 +225,8 @@ class DatabaseConnect extends AbstractStepAction
             ->assign('password', $this->getConfiguredPassword())
             ->assign('host', $this->getConfiguredHost())
             ->assign('port', $this->getConfiguredOrDefaultPort())
-            ->assign('database', $GLOBALS['TYPO3_CONF_VARS']['DB']['database'] ?: '')
-            ->assign('socket', $GLOBALS['TYPO3_CONF_VARS']['DB']['socket'] ?: '');
+            ->assign('database', $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname'] ?: '')
+            ->assign('socket', $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['unix_socket'] ?: '');
 
         if ($isDbalEnabled) {
             $this->view->assign('selectedDbalDriver', $this->getSelectedDbalDriver());
@@ -292,8 +292,8 @@ class DatabaseConnect extends AbstractStepAction
         if ($this->isDbalEnabled()) {
             // Set additional connect information based on dbal driver. postgres for example needs
             // database name already for connect.
-            if (isset($GLOBALS['TYPO3_CONF_VARS']['DB']['database'])) {
-                $databaseConnection->setDatabaseName($GLOBALS['TYPO3_CONF_VARS']['DB']['database']);
+            if (isset($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname'])) {
+                $databaseConnection->setDatabaseName($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname']);
             }
         }
 
@@ -318,12 +318,11 @@ class DatabaseConnect extends AbstractStepAction
     protected function isHostConfigured()
     {
         $hostConfigured = true;
-        if (empty($GLOBALS['TYPO3_CONF_VARS']['DB']['host'])) {
+        if (empty($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host'])) {
             $hostConfigured = false;
         }
-        if (
-            !isset($GLOBALS['TYPO3_CONF_VARS']['DB']['port'])
-            && !isset($GLOBALS['TYPO3_CONF_VARS']['DB']['socket'])
+        if (!isset($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['port'])
+            && !isset($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['unix_socket'])
         ) {
             $hostConfigured = false;
         }
@@ -341,10 +340,10 @@ class DatabaseConnect extends AbstractStepAction
     protected function isConfigurationComplete()
     {
         $configurationComplete = $this->isHostConfigured();
-        if (!isset($GLOBALS['TYPO3_CONF_VARS']['DB']['username'])) {
+        if (!isset($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user'])) {
             $configurationComplete = false;
         }
-        if (!isset($GLOBALS['TYPO3_CONF_VARS']['DB']['password'])) {
+        if (!isset($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['password'])) {
             $configurationComplete = false;
         }
         return $configurationComplete;
@@ -370,32 +369,32 @@ class DatabaseConnect extends AbstractStepAction
     {
         $localConfigurationPathValuePairs = array();
 
-        $localConfigurationPathValuePairs['DB/host'] = $this->getConfiguredHost();
+        $localConfigurationPathValuePairs['DB/Connections/Default/host'] = $this->getConfiguredHost();
 
         // If host is "local" either by upgrading or by first install, we try a socket
         // connection first and use TCP/IP as fallback
-        if ($localConfigurationPathValuePairs['DB/host'] === 'localhost'
-            || GeneralUtility::cmpIP($localConfigurationPathValuePairs['DB/host'], '127.*.*.*')
-            || (string)$localConfigurationPathValuePairs['DB/host'] === ''
+        if ($localConfigurationPathValuePairs['DB/Connections/Default/host'] === 'localhost'
+            || GeneralUtility::cmpIP($localConfigurationPathValuePairs['DB/Connections/Default/host'], '127.*.*.*')
+            || (string)$localConfigurationPathValuePairs['DB/Connections/Default/host'] === ''
         ) {
             if ($this->isConnectionWithUnixDomainSocketPossible()) {
-                $localConfigurationPathValuePairs['DB/host'] = 'localhost';
-                $localConfigurationPathValuePairs['DB/socket'] = $this->getConfiguredSocket();
+                $localConfigurationPathValuePairs['DB/Connections/Default/host'] = 'localhost';
+                $localConfigurationPathValuePairs['DB/Connections/Default/unix_socket'] = $this->getConfiguredSocket();
             } else {
-                if (!GeneralUtility::isFirstPartOfStr($localConfigurationPathValuePairs['DB/host'], '127.')) {
-                    $localConfigurationPathValuePairs['DB/host'] = '127.0.0.1';
+                if (!GeneralUtility::isFirstPartOfStr($localConfigurationPathValuePairs['DB/Connections/Default/host'], '127.')) {
+                    $localConfigurationPathValuePairs['DB/Connections/Default/host'] = '127.0.0.1';
                 }
             }
         }
 
-        if (!isset($localConfigurationPathValuePairs['DB/socket'])) {
+        if (!isset($localConfigurationPathValuePairs['DB/Connections/Default/unix_socket'])) {
             // Make sure a default port is set if not configured yet
             // This is independent from any host configuration
             $port = $this->getConfiguredPort();
             if ($port > 0) {
-                $localConfigurationPathValuePairs['DB/port'] = $port;
+                $localConfigurationPathValuePairs['DB/Connections/Default/port'] = $port;
             } else {
-                $localConfigurationPathValuePairs['DB/port'] = $this->getConfiguredOrDefaultPort();
+                $localConfigurationPathValuePairs['DB/Connections/Default/port'] = $this->getConfiguredOrDefaultPort();
             }
         }
 
@@ -605,7 +604,7 @@ class DatabaseConnect extends AbstractStepAction
      */
     protected function getConfiguredUsername()
     {
-        $username = isset($GLOBALS['TYPO3_CONF_VARS']['DB']['username']) ? $GLOBALS['TYPO3_CONF_VARS']['DB']['username'] : '';
+        $username = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user'] ?? '';
         return $username;
     }
 
@@ -616,7 +615,7 @@ class DatabaseConnect extends AbstractStepAction
      */
     protected function getConfiguredPassword()
     {
-        $password = isset($GLOBALS['TYPO3_CONF_VARS']['DB']['password']) ? $GLOBALS['TYPO3_CONF_VARS']['DB']['password'] : '';
+        $password = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['password'] ?? '';
         return $password;
     }
 
@@ -627,8 +626,8 @@ class DatabaseConnect extends AbstractStepAction
      */
     protected function getConfiguredHost()
     {
-        $host = isset($GLOBALS['TYPO3_CONF_VARS']['DB']['host']) ? $GLOBALS['TYPO3_CONF_VARS']['DB']['host'] : '';
-        $port = isset($GLOBALS['TYPO3_CONF_VARS']['DB']['port']) ? $GLOBALS['TYPO3_CONF_VARS']['DB']['port'] : '';
+        $host = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host'] ?? '';
+        $port = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['port'] ?? '';
         if (strlen($port) < 1 && substr_count($host, ':') === 1) {
             list($host) = explode(':', $host);
         }
@@ -642,8 +641,8 @@ class DatabaseConnect extends AbstractStepAction
      */
     protected function getConfiguredPort()
     {
-        $host = isset($GLOBALS['TYPO3_CONF_VARS']['DB']['host']) ? $GLOBALS['TYPO3_CONF_VARS']['DB']['host'] : '';
-        $port = isset($GLOBALS['TYPO3_CONF_VARS']['DB']['port']) ? $GLOBALS['TYPO3_CONF_VARS']['DB']['port'] : '';
+        $host = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host'] ?? '';
+        $port = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['port'] ?? '';
         if ($port === '' && substr_count($host, ':') === 1) {
             $hostPortArray = explode(':', $host);
             $port = $hostPortArray[1];
@@ -658,7 +657,7 @@ class DatabaseConnect extends AbstractStepAction
      */
     protected function getConfiguredSocket()
     {
-        $socket = isset($GLOBALS['TYPO3_CONF_VARS']['DB']['socket']) ? $GLOBALS['TYPO3_CONF_VARS']['DB']['socket'] : '';
+        $socket = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['unix_socket'] ?? '';
         return $socket;
     }
 }
index 99c6302..f6c12c9 100644 (file)
@@ -47,7 +47,7 @@ class DatabaseSelect extends AbstractStepAction
             if ($this->isValidDatabaseName($newDatabaseName)) {
                 $createDatabaseResult = $this->databaseConnection->admin_query('CREATE DATABASE ' . $newDatabaseName . ' CHARACTER SET utf8');
                 if ($createDatabaseResult) {
-                    $localConfigurationPathValuePairs['DB/database'] = $newDatabaseName;
+                    $localConfigurationPathValuePairs['DB/Connections/Default/dbname'] = $newDatabaseName;
                 } else {
                     /** @var $errorStatus \TYPO3\CMS\Install\Status\ErrorStatus */
                     $errorStatus = GeneralUtility::makeInstance(\TYPO3\CMS\Install\Status\ErrorStatus::class);
@@ -118,8 +118,8 @@ class DatabaseSelect extends AbstractStepAction
     {
         $this->initializeDatabaseConnection();
         $result = true;
-        if ((string)$GLOBALS['TYPO3_CONF_VARS']['DB']['database'] !== '') {
-            $this->databaseConnection->setDatabaseName($GLOBALS['TYPO3_CONF_VARS']['DB']['database']);
+        if ((string)$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname'] !== '') {
+            $this->databaseConnection->setDatabaseName($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname']);
             try {
                 $selectResult = $this->databaseConnection->sql_select_db();
                 if ($selectResult === true) {
@@ -188,11 +188,11 @@ class DatabaseSelect extends AbstractStepAction
     protected function initializeDatabaseConnection()
     {
         $this->databaseConnection = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\DatabaseConnection::class);
-        $this->databaseConnection->setDatabaseUsername($GLOBALS['TYPO3_CONF_VARS']['DB']['username']);
-        $this->databaseConnection->setDatabasePassword($GLOBALS['TYPO3_CONF_VARS']['DB']['password']);
-        $this->databaseConnection->setDatabaseHost($GLOBALS['TYPO3_CONF_VARS']['DB']['host']);
-        $this->databaseConnection->setDatabasePort($GLOBALS['TYPO3_CONF_VARS']['DB']['port']);
-        $this->databaseConnection->setDatabaseSocket($GLOBALS['TYPO3_CONF_VARS']['DB']['socket']);
+        $this->databaseConnection->setDatabaseUsername($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user']);
+        $this->databaseConnection->setDatabasePassword($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['password']);
+        $this->databaseConnection->setDatabaseHost($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host']);
+        $this->databaseConnection->setDatabasePort($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['port']);
+        $this->databaseConnection->setDatabaseSocket($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['unix_socket']);
         $this->databaseConnection->sql_pconnect();
     }
 
index 47fb2b2..08e3645 100644 (file)
@@ -79,11 +79,11 @@ class ImportantActions extends Action\AbstractAction
             ->assign('composerMode', Bootstrap::usesComposerClassLoading())
             ->assign('operatingSystem', $operatingSystem)
             ->assign('cgiDetected', GeneralUtility::isRunningOnCgiServerApi())
-            ->assign('databaseName', $GLOBALS['TYPO3_CONF_VARS']['DB']['database'])
-            ->assign('databaseUsername', $GLOBALS['TYPO3_CONF_VARS']['DB']['username'])
-            ->assign('databaseHost', $GLOBALS['TYPO3_CONF_VARS']['DB']['host'])
-            ->assign('databasePort', $GLOBALS['TYPO3_CONF_VARS']['DB']['port'])
-            ->assign('databaseSocket', $GLOBALS['TYPO3_CONF_VARS']['DB']['socket'])
+            ->assign('databaseName', $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname'])
+            ->assign('databaseUsername', $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user'])
+            ->assign('databaseHost', $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host'])
+            ->assign('databasePort', $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['port'])
+            ->assign('databaseSocket', $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['unix_socket'])
             ->assign('databaseNumberOfTables', count($this->getDatabaseConnection()->admin_get_tables()))
             ->assign('extensionCompatibilityTesterProtocolFile', GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . 'typo3temp/assets/ExtensionCompatibilityTester.txt')
             ->assign('extensionCompatibilityTesterErrorProtocolFile', GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . 'typo3temp/assets/ExtensionCompatibilityTesterErrors.json')
index e858de6..bec5761 100644 (file)
@@ -97,12 +97,12 @@ class ClearCacheService
         if (!is_object($database)) {
             /** @var \TYPO3\CMS\Core\Database\DatabaseConnection $database */
             $database = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\DatabaseConnection::class);
-            $database->setDatabaseUsername($GLOBALS['TYPO3_CONF_VARS']['DB']['username']);
-            $database->setDatabasePassword($GLOBALS['TYPO3_CONF_VARS']['DB']['password']);
-            $database->setDatabaseHost($GLOBALS['TYPO3_CONF_VARS']['DB']['host']);
-            $database->setDatabasePort($GLOBALS['TYPO3_CONF_VARS']['DB']['port']);
-            $database->setDatabaseSocket($GLOBALS['TYPO3_CONF_VARS']['DB']['socket']);
-            $database->setDatabaseName($GLOBALS['TYPO3_CONF_VARS']['DB']['database']);
+            $database->setDatabaseUsername($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user']);
+            $database->setDatabasePassword($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['password']);
+            $database->setDatabaseHost($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host']);
+            $database->setDatabasePort($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['port']);
+            $database->setDatabaseSocket($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['unix_socket']);
+            $database->setDatabaseName($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname']);
             $database->initialize();
             $database->connectDB();
         }
index f804739..cd6eb7c 100755 (executable)
@@ -46,7 +46,25 @@ class SilentConfigurationUpgradeService
      *
      * @var array
      */
-    protected $obsoleteLocalConfigurationSettings = array(
+    protected $obsoleteLocalConfigurationSettings = [
+        // #72367
+        'INSTALL/wizardDone/TYPO3\\CMS\\Install\\Updates\\AccessRightParametersUpdate',
+        'INSTALL/wizardDone/TYPO3\\CMS\\Install\\Updates\\BackendUserStartModuleUpdate',
+        'INSTALL/wizardDone/TYPO3\\CMS\\Install\\Updates\\Compatibility6ExtractionUpdate',
+        'INSTALL/wizardDone/TYPO3\\CMS\\Install\\Updates\\ContentTypesToTextMediaUpdate',
+        'INSTALL/wizardDone/TYPO3\\CMS\\Install\\Updates\\FileListIsStartModuleUpdate',
+        'INSTALL/wizardDone/TYPO3\\CMS\\Install\\Updates\\FilesReplacePermissionUpdate',
+        'INSTALL/wizardDone/TYPO3\\CMS\\Install\\Updates\\LanguageIsoCodeUpdate',
+        'INSTALL/wizardDone/TYPO3\\CMS\\Install\\Updates\\MediaceExtractionUpdate',
+        'INSTALL/wizardDone/TYPO3\\CMS\\Install\\Updates\\MigrateMediaToAssetsForTextMediaCe',
+        'INSTALL/wizardDone/TYPO3\\CMS\\Install\\Updates\\MigrateShortcutUrlsAgainUpdate',
+        'INSTALL/wizardDone/TYPO3\\CMS\\Install\\Updates\\OpenidExtractionUpdate',
+        'INSTALL/wizardDone/TYPO3\\CMS\\Install\\Updates\\PageShortcutParentUpdate',
+        'INSTALL/wizardDone/TYPO3\\CMS\\Install\\Updates\\ProcessedFileChecksumUpdate',
+        'INSTALL/wizardDone/TYPO3\\CMS\\Install\\Updates\\TableFlexFormToTtContentFieldsUpdate',
+        'INSTALL/wizardDone/TYPO3\\CMS\\Install\\Updates\\WorkspacesNotificationSettingsUpdate',
+        'INSTALL/wizardDone/TYPO3\\CMS\\Rtehtmlarea\\Hook\\Install\\DeprecatedRteProperties',
+        'INSTALL/wizardDone/TYPO3\\CMS\\Rtehtmlarea\\Hook\\Install\\RteAcronymButtonRenamedToAbbreviation',
         // #72400
         'BE/spriteIconGenerator_handler',
         // #72417
@@ -79,7 +97,7 @@ class SilentConfigurationUpgradeService
         // #75355
         'BE/niceFlexFormXMLtags',
         'BE/compactFlexFormXML'
-    );
+    ];
 
     public function __construct(ConfigurationManager $configurationManager = null)
     {
@@ -104,6 +122,7 @@ class SilentConfigurationUpgradeService
         $this->removeObsoleteLocalConfigurationSettings();
         $this->migrateThumbnailsPngSetting();
         $this->migrateLockSslSetting();
+        $this->migrateDatabaseConnectionSettings();
     }
 
     /**
@@ -145,7 +164,8 @@ class SilentConfigurationUpgradeService
             }
         } catch (\RuntimeException $e) {
             // If an exception is thrown, the value is not set in LocalConfiguration
-            $this->configurationManager->setLocalConfigurationValueByPath('BE/loginSecurityLevel', $rsaauthLoaded ? 'rsa' : 'normal');
+            $this->configurationManager->setLocalConfigurationValueByPath('BE/loginSecurityLevel',
+                $rsaauthLoaded ? 'rsa' : 'normal');
             $this->throwRedirectException();
         }
     }
@@ -163,7 +183,7 @@ class SilentConfigurationUpgradeService
         try {
             $extensionConfiguration = @unserialize($this->configurationManager->getLocalConfigurationValueByPath('EXT/extConf/saltedpasswords'));
         } catch (\RuntimeException $e) {
-            $extensionConfiguration = array();
+            $extensionConfiguration = [];
         }
         if (is_array($extensionConfiguration) && !empty($extensionConfiguration)) {
             if (isset($extensionConfiguration['BE.']['enabled'])) {
@@ -414,7 +434,7 @@ class SilentConfigurationUpgradeService
      */
     protected function disableImageMagickDetailSettingsIfImageMagickIsDisabled()
     {
-        $changedValues = array();
+        $changedValues = [];
         try {
             $currentImValue = $this->configurationManager->getLocalConfigurationValueByPath('GFX/processor_enabled');
         } catch (\RuntimeException $e) {
@@ -477,7 +497,7 @@ class SilentConfigurationUpgradeService
      */
     protected function setImageMagickDetailSettings()
     {
-        $changedValues = array();
+        $changedValues = [];
         try {
             $currentProcessorValue = $this->configurationManager->getLocalConfigurationValueByPath('GFX/processor');
         } catch (\RuntimeException $e) {
@@ -520,8 +540,8 @@ class SilentConfigurationUpgradeService
      */
     protected function migrateImageProcessorSetting()
     {
-        $changedSettings = array();
-        $settingsToRename = array(
+        $changedSettings = [];
+        $settingsToRename = [
             'GFX/im' => 'GFX/processor_enabled',
             'GFX/im_version_5' => 'GFX/processor',
             'GFX/im_v5effects' => 'GFX/processor_effects',
@@ -533,7 +553,7 @@ class SilentConfigurationUpgradeService
             'GFX/im_stripProfileCommand' => 'GFX/processor_stripColorProfileCommand',
             'GFX/im_useStripProfileByDefault' => 'GFX/processor_stripColorProfileByDefault',
             'GFX/colorspace' => 'GFX/processor_colorspace',
-        );
+        ];
 
         foreach ($settingsToRename as $oldPath => $newPath) {
             try {
@@ -555,19 +575,22 @@ class SilentConfigurationUpgradeService
         if (!empty($changedSettings['GFX/im_noScaleUp'])) {
             $currentProcessorValue = $this->configurationManager->getLocalConfigurationValueByPath('GFX/im_noScaleUp');
             $newProcessorValue = !$currentProcessorValue;
-            $this->configurationManager->setLocalConfigurationValueByPath('GFX/processor_allowUpscaling', $newProcessorValue);
+            $this->configurationManager->setLocalConfigurationValueByPath('GFX/processor_allowUpscaling',
+                $newProcessorValue);
         }
 
         if (!empty($changedSettings['GFX/im_noFramePrepended'])) {
             $currentProcessorValue = $this->configurationManager->getLocalConfigurationValueByPath('GFX/im_noFramePrepended');
             $newProcessorValue = !$currentProcessorValue;
-            $this->configurationManager->setLocalConfigurationValueByPath('GFX/processor_allowFrameSelection', $newProcessorValue);
+            $this->configurationManager->setLocalConfigurationValueByPath('GFX/processor_allowFrameSelection',
+                $newProcessorValue);
         }
 
         if (!empty($changedSettings['GFX/im_mask_temp_ext_gif'])) {
             $currentProcessorValue = $this->configurationManager->getLocalConfigurationValueByPath('GFX/im_mask_temp_ext_gif');
             $newProcessorValue = !$currentProcessorValue;
-            $this->configurationManager->setLocalConfigurationValueByPath('GFX/processor_allowTemporaryMasksAsPng', $newProcessorValue);
+            $this->configurationManager->setLocalConfigurationValueByPath('GFX/processor_allowTemporaryMasksAsPng',
+                $newProcessorValue);
         }
 
         if (!empty(array_filter($changedSettings))) {
@@ -596,7 +619,7 @@ class SilentConfigurationUpgradeService
      */
     protected function migrateThumbnailsPngSetting()
     {
-        $changedValues = array();
+        $changedValues = [];
         try {
             $currentThumbnailsPngValue = $this->configurationManager->getLocalConfigurationValueByPath('GFX/thumbnails_png');
         } catch (\RuntimeException $e) {
@@ -630,4 +653,76 @@ class SilentConfigurationUpgradeService
             // no change inside the LocalConfiguration.php found, so nothing needs to be modified
         }
     }
+
+    /**
+     * Move the database connection settings to a "Default" connection
+     *
+     * @return void
+     */
+    protected function migrateDatabaseConnectionSettings()
+    {
+        $changedSettings = [];
+        $settingsToRename = [
+            'DB/username' => 'DB/Connections/Default/user',
+            'DB/password' => 'DB/Connections/Default/password',
+            'DB/host' => 'DB/Connections/Default/host',
+            'DB/port' => 'DB/Connections/Default/port',
+            'DB/socket' => 'DB/Connections/Default/unix_socket',
+            'DB/database' => 'DB/Connections/Default/dbname',
+            'SYS/setDBinit' => 'DB/Connections/Default/initCommands',
+            'SYS/no_pconnect' => 'DB/Connections/Default/persistentConnection',
+            'SYS/dbClientCompress' => 'DB/Connections/Default/driverOptions',
+
+        ];
+
+        $confManager = $this->configurationManager;
+
+        foreach ($settingsToRename as $oldPath => $newPath) {
+            try {
+                $value = $confManager->getLocalConfigurationValueByPath($oldPath);
+                $confManager->setLocalConfigurationValueByPath($newPath, $value);
+                $changedSettings[$oldPath] = true;
+            } catch (\RuntimeException $e) {
+                // If an exception is thrown, the value is not set in LocalConfiguration
+                $changedSettings[$oldPath] = false;
+            }
+        }
+
+        // Remove empty socket connects
+        if (!empty($changedSettings['DB/Connections/Default/unix_socket'])) {
+            $value = $confManager->getLocalConfigurationValueByPath('DB/Connections/Default/unix_socket');
+            if (empty($value)) {
+                $confManager->removeLocalConfigurationKeysByPath(array_keys('DB/Connections/Default/unix_socket'));
+            }
+        }
+
+        // Convert the dbClientCompress flag to a mysqli driver option
+        if (!empty($changedSettings['DB/Connections/Default/driverOptions'])) {
+            $value = $confManager->getLocalConfigurationValueByPath('DB/Connections/Default/driverOptions');
+            $confManager->setLocalConfigurationValueByPath(
+                'DB/Connections/Default/driverOptions',
+                (bool)$value ? MYSQLI_CLIENT_COMPRESS : 0
+            );
+        }
+
+        // Swap value as the semantics have changed
+        if (!empty($changedSettings['DB/Connections/Default/persistentConnection'])) {
+            $value = $confManager->getLocalConfigurationValueByPath('DB/Connections/Default/persistentConnection');
+            $confManager->setLocalConfigurationValueByPath(
+                'DB/Connections/Default/persistentConnection',
+                !$value
+            );
+        }
+
+        // Set the utf-8 connection charset by default
+        $confManager->setLocalConfigurationValueByPath('DB/Connections/Default/charset', 'utf-8');
+
+        // Use the mysqli driver by default
+        $confManager->setLocalConfigurationValueByPath('DB/Connections/Default/driver', 'mysqli');
+
+        if (!empty(array_filter($changedSettings))) {
+            $confManager->removeLocalConfigurationKeysByPath(array_keys($changedSettings));
+            $this->throwRedirectException();
+        }
+    }
 }
index 12a64fd..8dea386 100644 (file)
@@ -668,7 +668,7 @@ class SqlSchemaMigrationService
      */
     public function getListOfTables()
     {
-        $whichTables = $this->getDatabaseConnection()->admin_get_tables(TYPO3_db);
+        $whichTables = $this->getDatabaseConnection()->admin_get_tables();
         foreach ($whichTables as $key => &$value) {
             $value = $key;
         }
index 7ec87f5..5a1c64b 100644 (file)
@@ -176,12 +176,12 @@ class DatabaseCheck
         if (!is_object($database)) {
             /** @var \TYPO3\CMS\Core\Database\DatabaseConnection $database */
             $database = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\DatabaseConnection::class);
-            $database->setDatabaseUsername($GLOBALS['TYPO3_CONF_VARS']['DB']['username']);
-            $database->setDatabasePassword($GLOBALS['TYPO3_CONF_VARS']['DB']['password']);
-            $database->setDatabaseHost($GLOBALS['TYPO3_CONF_VARS']['DB']['host']);
-            $database->setDatabasePort($GLOBALS['TYPO3_CONF_VARS']['DB']['port']);
-            $database->setDatabaseSocket($GLOBALS['TYPO3_CONF_VARS']['DB']['socket']);
-            $database->setDatabaseName($GLOBALS['TYPO3_CONF_VARS']['DB']['database']);
+            $database->setDatabaseUsername($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user']);
+            $database->setDatabasePassword($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['password']);
+            $database->setDatabaseHost($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host']);
+            $database->setDatabasePort($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['port']);
+            $database->setDatabaseSocket($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['unix_socket']);
+            $database->setDatabaseName($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname']);
             $database->initialize();
             $database->connectDB();
         }
index 2e75ec0..6f1dd91 100644 (file)
@@ -36,7 +36,7 @@ class DatabaseCharsetUpdate extends AbstractUpdate
         $description = 'Sets the default database charset to utf-8 to ensure new tables are created with correct charset.
         WARNING: This will NOT convert any existing data.';
 
-        if($this->isDbalEnabled()) {
+        if ($this->isDbalEnabled()) {
             return $result;
         }
         // check if database charset is utf-8
@@ -60,7 +60,7 @@ class DatabaseCharsetUpdate extends AbstractUpdate
     {
         $result = true;
         $db = $this->getDatabaseConnection();
-        $query = 'ALTER DATABASE `' . $GLOBALS['TYPO3_CONF_VARS']['DB']['database'] . '` DEFAULT CHARACTER SET utf8';
+        $query = 'ALTER DATABASE `' . $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['dbname'] . '` DEFAULT CHARACTER SET utf8';
         $db->admin_query($query);
         $dbQueries[] = $query;
         if ($db->sql_error()) {