From 7fad03490056615b32c3c0833d24c45af828c312 Mon Sep 17 00:00:00 2001
From: Benni Mack <benni@typo3.org>
Date: Tue, 2 Jun 2020 19:47:15 +0200
Subject: [PATCH] [BUGFIX] Ensure proper encapsulation of comparisons with
 QueryBuilder
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

When using a TCA table without deleted, or any other enablefields
except "endtime", then the SQL query is built in a wrong fashion
both in PageRepository and BackendUtility. Thee source of this disaster
is inproper encapsulation in parenthesis when comparisons are build.

Instead of simply adding a manual "()" around the or() comparisons,
this change adjusts CompositionExpresion to properly add corresponding
parenthesis, which ensure this in every case and avoid missing usages.

A couple of tests are adjusted to properly reflect this change.

Resolves: #89616
Releases: main
Change-Id: I4aed140cbca5b4b40dfacd459a4c3aa30549b582
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/76481
Tested-by: core-ci <typo3@b13.com>
Tested-by: Benni Mack <benni@typo3.org>
Tested-by: Susanne Moog <look@susi.dev>
Tested-by: Stefan Bürk <stefan@buerk.tech>
Reviewed-by: Benni Mack <benni@typo3.org>
Reviewed-by: Susanne Moog <look@susi.dev>
Reviewed-by: Stefan Bürk <stefan@buerk.tech>
---
 .../Functional/Utility/BackendUtilityTest.php | 111 ++++++++++++++++--
 .../Query/Expression/CompositeExpression.php  |   2 +-
 .../Domain/Repository/PageRepositoryTest.php  |  12 +-
 .../BackendUserAuthenticationTest.php         |  10 +-
 .../Unit/Database/Query/QueryBuilderTest.php  |  18 +--
 .../DefaultRestrictionContainerTest.php       |   2 +-
 .../Restriction/EndTimeRestrictionTest.php    |   2 +-
 .../FrontendGroupRestrictionTest.php          |   4 +-
 .../FrontendRestrictionContainerTest.php      |  18 +--
 .../LimitToTablesRestrictionContainerTest.php |   2 +-
 .../PagePermissionRestrictionTest.php         |   4 +-
 .../Restriction/WorkspaceRestrictionTest.php  |   8 +-
 .../BackendWorkspaceRestrictionTest.php       |   8 +-
 .../FrontendWorkspaceRestrictionTest.php      |   6 +-
 .../Storage/Typo3DbQueryParserTest.php        |  84 +++++++++++--
 15 files changed, 225 insertions(+), 66 deletions(-)

diff --git a/typo3/sysext/backend/Tests/Functional/Utility/BackendUtilityTest.php b/typo3/sysext/backend/Tests/Functional/Utility/BackendUtilityTest.php
index 6c621433fd8f..4538ceca9f01 100644
--- a/typo3/sysext/backend/Tests/Functional/Utility/BackendUtilityTest.php
+++ b/typo3/sysext/backend/Tests/Functional/Utility/BackendUtilityTest.php
@@ -17,10 +17,14 @@ declare(strict_types=1);
 
 namespace TYPO3\CMS\Backend\Tests\Functional\Utility;
 
+use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
+use Doctrine\DBAL\Platforms\SqlitePlatform;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Core\Bootstrap;
+use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
 
 class BackendUtilityTest extends FunctionalTestCase
@@ -33,13 +37,15 @@ class BackendUtilityTest extends FunctionalTestCase
         'DE' => ['id' => 2, 'title' => 'German', 'locale' => 'de_DE.UTF8'],
     ];
 
+    protected BackendUserAuthentication $backendUser;
+
     public function setUp(): void
     {
         parent::setUp();
         $this->importCSVDataSet(__DIR__ . '/../Fixtures/pages.csv');
         $this->importCSVDataSet(__DIR__ . '/../Fixtures/tt_content.csv');
         $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv');
-        $this->setUpBackendUser(1);
+        $this->backendUser = $this->setUpBackendUser(1);
         Bootstrap::initializeLanguageObject();
     }
 
@@ -48,8 +54,7 @@ class BackendUtilityTest extends FunctionalTestCase
      */
     public function givenPageIdCanBeExpanded(): void
     {
-        $backendUser = $this->getBackendUser();
-        $backendUser->groupData['webmounts'] = '1';
+        $this->backendUser->groupData['webmounts'] = '1';
 
         BackendUtility::openPageTree(5, false);
 
@@ -58,7 +63,7 @@ class BackendUtilityTest extends FunctionalTestCase
             '1_1' => '1',
             '1_0' => '1',
         ];
-        $actualSiteHash = $backendUser->uc['BackendComponents']['States']['Pagetree']['stateHash'];
+        $actualSiteHash = $this->backendUser->uc['BackendComponents']['States']['Pagetree']['stateHash'];
         self::assertSame($expectedSiteHash, $actualSiteHash);
     }
 
@@ -67,8 +72,7 @@ class BackendUtilityTest extends FunctionalTestCase
      */
     public function otherBranchesCanBeClosedWhenOpeningPage(): void
     {
-        $backendUser = $this->getBackendUser();
-        $backendUser->groupData['webmounts'] = '1';
+        $this->backendUser->groupData['webmounts'] = '1';
 
         BackendUtility::openPageTree(5, false);
         BackendUtility::openPageTree(4, true);
@@ -81,7 +85,7 @@ class BackendUtilityTest extends FunctionalTestCase
             '1_1' => '1',
             '1_0' => '1',
         ];
-        $actualSiteHash = $backendUser->uc['BackendComponents']['States']['Pagetree']['stateHash'];
+        $actualSiteHash = $this->backendUser->uc['BackendComponents']['States']['Pagetree']['stateHash'];
         self::assertSame($expectedSiteHash, $actualSiteHash);
     }
 
@@ -141,8 +145,97 @@ class BackendUtilityTest extends FunctionalTestCase
         );
     }
 
-    private function getBackendUser(): BackendUserAuthentication
+    public function enableFieldsStatementIsCorrectDataProvider(): array
     {
-        return $GLOBALS['BE_USER'];
+        // Expected sql should contain identifier escaped in mysql/mariadb identifier quotings "`", which are
+        // replaced by corresponding quoting values for other database systems.
+        return [
+            'disabled' => [
+                [
+                    'disabled' => 'disabled',
+                ],
+                false,
+                ' AND `${tableName}`.`disabled` = 0',
+            ],
+            'starttime' => [
+                [
+                    'starttime' => 'starttime',
+                ],
+                false,
+                ' AND `${tableName}`.`starttime` <= 1234567890',
+            ],
+            'endtime' => [
+                [
+                    'endtime' => 'endtime',
+                ],
+                false,
+                ' AND ((`${tableName}`.`endtime` = 0) OR (`${tableName}`.`endtime` > 1234567890))',
+            ],
+            'disabled, starttime, endtime' => [
+                [
+                    'disabled' => 'disabled',
+                    'starttime' => 'starttime',
+                    'endtime' => 'endtime',
+                ],
+                false,
+                ' AND ((`${tableName}`.`disabled` = 0) AND (`${tableName}`.`starttime` <= 1234567890) AND (((`${tableName}`.`endtime` = 0) OR (`${tableName}`.`endtime` > 1234567890))))',
+            ],
+            'disabled inverted' => [
+                [
+                    'disabled' => 'disabled',
+                ],
+                true,
+                ' AND `${tableName}`.`disabled` <> 0',
+            ],
+            'starttime inverted' => [
+                [
+                    'starttime' => 'starttime',
+                ],
+                true,
+                ' AND ((`${tableName}`.`starttime` <> 0) AND (`${tableName}`.`starttime` > 1234567890))',
+            ],
+            'endtime inverted' => [
+                [
+                    'endtime' => 'endtime',
+                ],
+                true,
+                ' AND ((`${tableName}`.`endtime` <> 0) AND (`${tableName}`.`endtime` <= 1234567890))',
+            ],
+            'disabled, starttime, endtime inverted' => [
+                [
+                    'disabled' => 'disabled',
+                    'starttime' => 'starttime',
+                    'endtime' => 'endtime',
+                ],
+                true,
+                ' AND ((`${tableName}`.`disabled` <> 0) OR (((`${tableName}`.`starttime` <> 0) AND (`${tableName}`.`starttime` > 1234567890))) OR (((`${tableName}`.`endtime` <> 0) AND (`${tableName}`.`endtime` <= 1234567890))))',
+            ],
+        ];
+    }
+
+    /**
+     * @param array $enableColumns
+     * @param bool $inverted
+     * @param string $expectation
+     *
+     * @test
+     * @dataProvider enableFieldsStatementIsCorrectDataProvider
+     */
+    public function enableFieldsStatementIsCorrect(array $enableColumns, bool $inverted, string $expectation): void
+    {
+        $platform = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME)->getDatabasePlatform();
+        $tableName = uniqid('table');
+        $GLOBALS['TCA'][$tableName]['ctrl']['enablecolumns'] = $enableColumns;
+        $GLOBALS['SIM_ACCESS_TIME'] = 1234567890;
+        $statement = BackendUtility::BEenableFields($tableName, $inverted);
+        $replaces = [
+            '${tableName}' => $tableName,
+        ];
+        // replace mysql identifier quotings with sqlite identifier qotings in expected sql string
+        if ($platform instanceof SqlitePlatform || $platform instanceof PostgreSQLPlatform) {
+            $replaces['`'] = '"';
+        }
+        $expectation = str_replace(array_keys($replaces), array_values($replaces), $expectation);
+        self::assertSame($expectation, $statement);
     }
 }
diff --git a/typo3/sysext/core/Classes/Database/Query/Expression/CompositeExpression.php b/typo3/sysext/core/Classes/Database/Query/Expression/CompositeExpression.php
index dfd312585a5b..5ffc73212770 100644
--- a/typo3/sysext/core/Classes/Database/Query/Expression/CompositeExpression.php
+++ b/typo3/sysext/core/Classes/Database/Query/Expression/CompositeExpression.php
@@ -194,6 +194,6 @@ class CompositeExpression extends \Doctrine\DBAL\Query\Expression\CompositeExpre
         if ($this->count() === 1) {
             return (string)$this->parts[0];
         }
-        return '(' . implode(') ' . $this->type . ' (', $this->parts) . ')';
+        return '((' . implode(') ' . $this->type . ' (', $this->parts) . '))';
     }
 }
diff --git a/typo3/sysext/core/Tests/Functional/Domain/Repository/PageRepositoryTest.php b/typo3/sysext/core/Tests/Functional/Domain/Repository/PageRepositoryTest.php
index 7c56fe219645..a4b4f784feca 100644
--- a/typo3/sysext/core/Tests/Functional/Domain/Repository/PageRepositoryTest.php
+++ b/typo3/sysext/core/Tests/Functional/Domain/Repository/PageRepositoryTest.php
@@ -394,7 +394,7 @@ class PageRepositoryTest extends FunctionalTestCase
         $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('pages');
 
         $expectedSQL = sprintf(
-            ' AND (%s = 0) AND ((%s = 0) OR (%s = 2)) AND (%s <> 255)',
+            ' AND ((%s = 0) AND (((%s = 0) OR (%s = 2))) AND (%s <> 255))',
             $connection->quoteIdentifier('pages.deleted'),
             $connection->quoteIdentifier('pages.t3ver_wsid'),
             $connection->quoteIdentifier('pages.t3ver_wsid'),
@@ -415,7 +415,7 @@ class PageRepositoryTest extends FunctionalTestCase
 
         $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('pages');
         $expectedSQL = sprintf(
-            ' AND ((%s = 0) AND (%s <= 0) AND (%s = 0) AND ((%s = 0) OR (%s = 4)) AND (%s = 0) AND (%s <= 1451779200) AND ((%s = 0) OR (%s > 1451779200))) AND (%s <> 255)',
+            ' AND ((((%s = 0) AND (%s <= 0) AND (%s = 0) AND (((%s = 0) OR (%s = 4))) AND (%s = 0) AND (%s <= 1451779200) AND (((%s = 0) OR (%s > 1451779200))))) AND (%s <> 255))',
             $connection->quoteIdentifier('pages.deleted'),
             $connection->quoteIdentifier('pages.t3ver_state'),
             $connection->quoteIdentifier('pages.t3ver_wsid'),
@@ -529,12 +529,12 @@ class PageRepositoryTest extends FunctionalTestCase
 
         self::assertThat(
             $conditions,
-            self::stringContains(' AND (' . $connection->quoteIdentifier($table . '.t3ver_state') . ' <= 0)'),
+            self::stringContains(' AND ((' . $connection->quoteIdentifier($table . '.t3ver_state') . ' <= 0) '),
             'Versioning placeholders'
         );
         self::assertThat(
             $conditions,
-            self::stringContains(' AND ((' . $connection->quoteIdentifier($table . '.t3ver_oid') . ' = 0) OR (' . $connection->quoteIdentifier($table . '.t3ver_state') . ' = 4))'),
+            self::stringContains(' AND (((' . $connection->quoteIdentifier($table . '.t3ver_oid') . ' = 0) OR (' . $connection->quoteIdentifier($table . '.t3ver_state') . ' = 4)))'),
             'Records with online version'
         );
     }
@@ -565,7 +565,7 @@ class PageRepositoryTest extends FunctionalTestCase
         );
         self::assertThat(
             $conditions,
-            self::stringContains(' AND ((' . $connection->quoteIdentifier($table . '.t3ver_oid') . ' = 0) OR (' . $connection->quoteIdentifier($table . '.t3ver_state') . ' = 4))'),
+            self::stringContains(' AND (((' . $connection->quoteIdentifier($table . '.t3ver_oid') . ' = 0) OR (' . $connection->quoteIdentifier($table . '.t3ver_state') . ' = 4)))'),
             'Records from online versions'
         );
     }
@@ -591,7 +591,7 @@ class PageRepositoryTest extends FunctionalTestCase
 
         self::assertThat(
             $conditions,
-            self::stringContains(' AND ((' . $connection->quoteIdentifier($table . '.t3ver_wsid') . ' = 0) OR (' . $connection->quoteIdentifier($table . '.t3ver_wsid') . ' = 2))'),
+            self::stringContains(' AND ((((' . $connection->quoteIdentifier($table . '.t3ver_wsid') . ' = 0) OR (' . $connection->quoteIdentifier($table . '.t3ver_wsid') . ' = 2)))'),
             'No versioning placeholders'
         );
     }
diff --git a/typo3/sysext/core/Tests/Unit/Authentication/BackendUserAuthenticationTest.php b/typo3/sysext/core/Tests/Unit/Authentication/BackendUserAuthenticationTest.php
index bef98e1b56a5..a67ed5824ea0 100644
--- a/typo3/sysext/core/Tests/Unit/Authentication/BackendUserAuthenticationTest.php
+++ b/typo3/sysext/core/Tests/Unit/Authentication/BackendUserAuthenticationTest.php
@@ -659,16 +659,16 @@ class BackendUserAuthenticationTest extends UnitTestCase
                 2,
                 false,
                 [],
-                ' ((`pages`.`perms_everybody` & 2 = 2) OR' .
-                ' ((`pages`.`perms_userid` = 123) AND (`pages`.`perms_user` & 2 = 2)))',
+                ' (((`pages`.`perms_everybody` & 2 = 2) OR' .
+                ' (((`pages`.`perms_userid` = 123) AND (`pages`.`perms_user` & 2 = 2)))))',
             ],
             'for user with groups' => [
                 8,
                 false,
                 [1, 2],
-                ' ((`pages`.`perms_everybody` & 8 = 8) OR' .
-                ' ((`pages`.`perms_userid` = 123) AND (`pages`.`perms_user` & 8 = 8))' .
-                ' OR ((`pages`.`perms_groupid` IN (1, 2)) AND (`pages`.`perms_group` & 8 = 8)))',
+                ' (((`pages`.`perms_everybody` & 8 = 8) OR' .
+                ' (((`pages`.`perms_userid` = 123) AND (`pages`.`perms_user` & 8 = 8)))' .
+                ' OR (((`pages`.`perms_groupid` IN (1, 2)) AND (`pages`.`perms_group` & 8 = 8)))))',
             ],
         ];
     }
diff --git a/typo3/sysext/core/Tests/Unit/Database/Query/QueryBuilderTest.php b/typo3/sysext/core/Tests/Unit/Database/Query/QueryBuilderTest.php
index ce4ca0e681e2..2aea13b50ee0 100644
--- a/typo3/sysext/core/Tests/Unit/Database/Query/QueryBuilderTest.php
+++ b/typo3/sysext/core/Tests/Unit/Database/Query/QueryBuilderTest.php
@@ -961,7 +961,7 @@ class QueryBuilderTest extends UnitTestCase
             ->from('pages')
             ->where('uid=1');
 
-        $expectedSQL = 'SELECT * FROM pages WHERE (uid=1) AND ((pages.deleted = 0) AND (pages.hidden = 0))';
+        $expectedSQL = 'SELECT * FROM pages WHERE (uid=1) AND (((pages.deleted = 0) AND (pages.hidden = 0)))';
         $this->connection->executeQuery($expectedSQL, Argument::cetera())
             ->willReturn($this->prophesize(Result::class)->reveal());
 
@@ -1008,7 +1008,7 @@ class QueryBuilderTest extends UnitTestCase
             ->from('pages')
             ->where('uid=1');
 
-        $expectedSQL = 'SELECT COUNT(uid) FROM pages WHERE (uid=1) AND ((pages.deleted = 0) AND (pages.hidden = 0))';
+        $expectedSQL = 'SELECT COUNT(uid) FROM pages WHERE (uid=1) AND (((pages.deleted = 0) AND (pages.hidden = 0)))';
         $this->connection->executeQuery($expectedSQL, Argument::cetera())
             ->willReturn($this->prophesize(Result::class)->reveal());
 
@@ -1053,7 +1053,7 @@ class QueryBuilderTest extends UnitTestCase
             ->from('pages')
             ->where('uid=1');
 
-        $expectedSQL = 'SELECT * FROM pages WHERE (uid=1) AND ((pages.deleted = 0) AND (pages.hidden = 0))';
+        $expectedSQL = 'SELECT * FROM pages WHERE (uid=1) AND (((pages.deleted = 0) AND (pages.hidden = 0)))';
         self::assertSame($expectedSQL, $subject->getSQL());
 
         $subject->getRestrictions()->removeAll()->add(new DeletedRestriction());
@@ -1110,7 +1110,7 @@ class QueryBuilderTest extends UnitTestCase
 
         $subject->resetRestrictions();
 
-        $expectedSQL = 'SELECT * FROM pages WHERE (uid=1) AND ((pages.deleted = 0) AND (pages.hidden = 0))';
+        $expectedSQL = 'SELECT * FROM pages WHERE (uid=1) AND (((pages.deleted = 0) AND (pages.hidden = 0)))';
         $this->connection->executeQuery($expectedSQL, Argument::cetera())
             ->willReturn($this->prophesize(Result::class)->reveal());
 
@@ -1246,7 +1246,7 @@ class QueryBuilderTest extends UnitTestCase
             ->from('pages')
             ->where('uid=1');
 
-        $expectedSQL = 'SELECT * FROM pages WHERE (uid=1) AND ((pages.deleted = 0) AND (pages.hidden = 0))';
+        $expectedSQL = 'SELECT * FROM pages WHERE (uid=1) AND (((pages.deleted = 0) AND (pages.hidden = 0)))';
         self::assertSame($expectedSQL, $subject->getSQL());
 
         $clonedQueryBuilder = clone $subject;
@@ -1255,7 +1255,7 @@ class QueryBuilderTest extends UnitTestCase
 
         //change cloned QueryBuilder
         $clonedQueryBuilder->count('*');
-        $expectedCountSQL = 'SELECT COUNT(*) FROM pages WHERE (uid=1) AND ((pages.deleted = 0) AND (pages.hidden = 0))';
+        $expectedCountSQL = 'SELECT COUNT(*) FROM pages WHERE (uid=1) AND (((pages.deleted = 0) AND (pages.hidden = 0)))';
         self::assertSame($expectedCountSQL, $clonedQueryBuilder->getSQL());
 
         //check if the original QueryBuilder has not changed
@@ -1478,7 +1478,7 @@ class QueryBuilderTest extends UnitTestCase
             ->where($expressionBuilder->eq('uid', 1));
 
         $this->connection->executeQuery(
-            'SELECT * FROM pages LEFT JOIN tt_content content ON pages.uid = content.pid WHERE (uid = 1) AND ((pages.deleted = 0) AND (pages.hidden = 0))',
+            'SELECT * FROM pages LEFT JOIN tt_content content ON pages.uid = content.pid WHERE (uid = 1) AND (((pages.deleted = 0) AND (pages.hidden = 0)))',
             Argument::cetera()
         )->willReturn($this->prophesize(Result::class)->reveal());
 
@@ -1578,7 +1578,7 @@ class QueryBuilderTest extends UnitTestCase
                 ->where($expressionBuilder->eq('uid', 1));
 
         $this->connection->executeQuery(
-            'SELECT * FROM pages LEFT JOIN tt_content content ON (pages.uid = content.pid) AND ((content.deleted = 0) AND (content.hidden = 0)) WHERE (uid = 1) AND ((pages.deleted = 0) AND (pages.hidden = 0))',
+            'SELECT * FROM pages LEFT JOIN tt_content content ON ((pages.uid = content.pid) AND (((content.deleted = 0) AND (content.hidden = 0)))) WHERE (uid = 1) AND (((pages.deleted = 0) AND (pages.hidden = 0)))',
             Argument::cetera()
         )->willReturn($this->prophesize(Result::class)->reveal());
 
@@ -1627,7 +1627,7 @@ class QueryBuilderTest extends UnitTestCase
                 ->where($expressionBuilder->eq('uid', 1));
 
         $this->connection->executeQuery(
-            'SELECT * FROM tt_content RIGHT JOIN pages pages ON (pages.uid = tt_content.pid) AND ((tt_content.deleted = 0) AND (tt_content.hidden = 0)) WHERE (uid = 1) AND ((pages.deleted = 0) AND (pages.hidden = 0))',
+            'SELECT * FROM tt_content RIGHT JOIN pages pages ON ((pages.uid = tt_content.pid) AND (((tt_content.deleted = 0) AND (tt_content.hidden = 0)))) WHERE (uid = 1) AND (((pages.deleted = 0) AND (pages.hidden = 0)))',
             Argument::cetera()
         )->willReturn($this->prophesize(Result::class)->reveal());
 
diff --git a/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/DefaultRestrictionContainerTest.php b/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/DefaultRestrictionContainerTest.php
index 45cae38374ed..28461547bc43 100644
--- a/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/DefaultRestrictionContainerTest.php
+++ b/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/DefaultRestrictionContainerTest.php
@@ -39,6 +39,6 @@ class DefaultRestrictionContainerTest extends AbstractRestrictionTestCase
         $expression = $subject->buildExpression(['aTable' => 'aTable'], $this->expressionBuilder);
         $expression = $this->expressionBuilder->and($expression);
 
-        self::assertSame('("aTable"."deleted" = 0) AND ("aTable"."myHiddenField" = 0) AND ("aTable"."myStartTimeField" <= 123) AND (("aTable"."myEndTimeField" = 0) OR ("aTable"."myEndTimeField" > 123))', (string)$expression);
+        self::assertSame('(("aTable"."deleted" = 0) AND ("aTable"."myHiddenField" = 0) AND ("aTable"."myStartTimeField" <= 123) AND ((("aTable"."myEndTimeField" = 0) OR ("aTable"."myEndTimeField" > 123))))', (string)$expression);
     }
 }
diff --git a/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/EndTimeRestrictionTest.php b/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/EndTimeRestrictionTest.php
index d49fe7edf7f9..ddb487b1dd3e 100644
--- a/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/EndTimeRestrictionTest.php
+++ b/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/EndTimeRestrictionTest.php
@@ -53,6 +53,6 @@ class EndTimeRestrictionTest extends AbstractRestrictionTestCase
 
         $subject = new EndTimeRestriction(42);
         $expression = $subject->buildExpression(['aTable' => 'aTable'], $this->expressionBuilder);
-        self::assertSame('("aTable"."myEndTimeField" = 0) OR ("aTable"."myEndTimeField" > 42)', (string)$expression);
+        self::assertSame('(("aTable"."myEndTimeField" = 0) OR ("aTable"."myEndTimeField" > 42))', (string)$expression);
     }
 }
diff --git a/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/FrontendGroupRestrictionTest.php b/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/FrontendGroupRestrictionTest.php
index a421568868b8..7192d4de1bfc 100644
--- a/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/FrontendGroupRestrictionTest.php
+++ b/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/FrontendGroupRestrictionTest.php
@@ -33,7 +33,7 @@ class FrontendGroupRestrictionTest extends AbstractRestrictionTestCase
         ];
         $subject = new FrontendGroupRestriction([]);
         $expression = $subject->buildExpression(['aTable' => 'aTable'], $this->expressionBuilder);
-        self::assertSame('("aTable"."myGroupField" IS NULL) OR ("aTable"."myGroupField" = \'\') OR ("aTable"."myGroupField" = \'0\')', (string)$expression);
+        self::assertSame('(("aTable"."myGroupField" IS NULL) OR ("aTable"."myGroupField" = \'\') OR ("aTable"."myGroupField" = \'0\'))', (string)$expression);
     }
 
     /**
@@ -48,6 +48,6 @@ class FrontendGroupRestrictionTest extends AbstractRestrictionTestCase
         ];
         $subject = new FrontendGroupRestriction([2, 3]);
         $expression = $subject->buildExpression(['aTable' => 'aTable'], $this->expressionBuilder);
-        self::assertSame('("aTable"."myGroupField" IS NULL) OR ("aTable"."myGroupField" = \'\') OR ("aTable"."myGroupField" = \'0\') OR (FIND_IN_SET(\'2\', "aTable"."myGroupField")) OR (FIND_IN_SET(\'3\', "aTable"."myGroupField"))', (string)$expression);
+        self::assertSame('(("aTable"."myGroupField" IS NULL) OR ("aTable"."myGroupField" = \'\') OR ("aTable"."myGroupField" = \'0\') OR (FIND_IN_SET(\'2\', "aTable"."myGroupField")) OR (FIND_IN_SET(\'3\', "aTable"."myGroupField")))', (string)$expression);
     }
 }
diff --git a/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/FrontendRestrictionContainerTest.php b/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/FrontendRestrictionContainerTest.php
index a94b67973dc3..241ff6a3af18 100644
--- a/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/FrontendRestrictionContainerTest.php
+++ b/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/FrontendRestrictionContainerTest.php
@@ -39,7 +39,7 @@ class FrontendRestrictionContainerTest extends AbstractRestrictionTestCase
                 'hiddenPagePreview' => false,
                 'hiddenRecordPreview' => false,
                 'frontendUserGroups' => [0, -1],
-                'expectedSQL' => '("aTable"."deleted" = 0) AND (("aTable"."t3ver_wsid" = 0) AND (("aTable"."t3ver_oid" = 0) OR ("aTable"."t3ver_state" = 4))) AND ("aTable"."myHiddenField" = 0) AND ("aTable"."myStartTimeField" <= 42) AND (("aTable"."myEndTimeField" = 0) OR ("aTable"."myEndTimeField" > 42)) AND (("aTable"."myGroupField" IS NULL) OR ("aTable"."myGroupField" = \'\') OR ("aTable"."myGroupField" = \'0\') OR (FIND_IN_SET(\'0\', "aTable"."myGroupField")) OR (FIND_IN_SET(\'-1\', "aTable"."myGroupField")))',
+                'expectedSQL' => '(("aTable"."deleted" = 0) AND ((("aTable"."t3ver_wsid" = 0) AND ((("aTable"."t3ver_oid" = 0) OR ("aTable"."t3ver_state" = 4))))) AND ("aTable"."myHiddenField" = 0) AND ("aTable"."myStartTimeField" <= 42) AND ((("aTable"."myEndTimeField" = 0) OR ("aTable"."myEndTimeField" > 42))) AND ((("aTable"."myGroupField" IS NULL) OR ("aTable"."myGroupField" = \'\') OR ("aTable"."myGroupField" = \'0\') OR (FIND_IN_SET(\'0\', "aTable"."myGroupField")) OR (FIND_IN_SET(\'-1\', "aTable"."myGroupField")))))',
             ],
             'Live, with hidden record preview' => [
                 'tableName' => 'aTable',
@@ -48,7 +48,7 @@ class FrontendRestrictionContainerTest extends AbstractRestrictionTestCase
                 'hiddenPagePreview' => true,
                 'hiddenRecordPreview' => true,
                 'frontendUserGroups' => [0, -1],
-                'expectedSQL' => '("aTable"."deleted" = 0) AND (("aTable"."t3ver_wsid" = 0) AND (("aTable"."t3ver_oid" = 0) OR ("aTable"."t3ver_state" = 4))) AND ("aTable"."myStartTimeField" <= 42) AND (("aTable"."myEndTimeField" = 0) OR ("aTable"."myEndTimeField" > 42)) AND (("aTable"."myGroupField" IS NULL) OR ("aTable"."myGroupField" = \'\') OR ("aTable"."myGroupField" = \'0\') OR (FIND_IN_SET(\'0\', "aTable"."myGroupField")) OR (FIND_IN_SET(\'-1\', "aTable"."myGroupField")))',
+                'expectedSQL' => '(("aTable"."deleted" = 0) AND ((("aTable"."t3ver_wsid" = 0) AND ((("aTable"."t3ver_oid" = 0) OR ("aTable"."t3ver_state" = 4))))) AND ("aTable"."myStartTimeField" <= 42) AND ((("aTable"."myEndTimeField" = 0) OR ("aTable"."myEndTimeField" > 42))) AND ((("aTable"."myGroupField" IS NULL) OR ("aTable"."myGroupField" = \'\') OR ("aTable"."myGroupField" = \'0\') OR (FIND_IN_SET(\'0\', "aTable"."myGroupField")) OR (FIND_IN_SET(\'-1\', "aTable"."myGroupField")))))',
             ],
             'Workspace, with WS preview' => [
                 'tableName' => 'aTable',
@@ -57,7 +57,7 @@ class FrontendRestrictionContainerTest extends AbstractRestrictionTestCase
                 'hiddenPagePreview' => false,
                 'hiddenRecordPreview' => false,
                 'frontendUserGroups' => [0, -1],
-                'expectedSQL' => '("aTable"."deleted" = 0) AND (("aTable"."t3ver_wsid" IN (0, 1)) AND (("aTable"."t3ver_oid" = 0) OR ("aTable"."t3ver_state" = 4))) AND ("aTable"."myHiddenField" = 0) AND ("aTable"."myStartTimeField" <= 42) AND (("aTable"."myEndTimeField" = 0) OR ("aTable"."myEndTimeField" > 42)) AND (("aTable"."myGroupField" IS NULL) OR ("aTable"."myGroupField" = \'\') OR ("aTable"."myGroupField" = \'0\') OR (FIND_IN_SET(\'0\', "aTable"."myGroupField")) OR (FIND_IN_SET(\'-1\', "aTable"."myGroupField")))',
+                'expectedSQL' => '(("aTable"."deleted" = 0) AND ((("aTable"."t3ver_wsid" IN (0, 1)) AND ((("aTable"."t3ver_oid" = 0) OR ("aTable"."t3ver_state" = 4))))) AND ("aTable"."myHiddenField" = 0) AND ("aTable"."myStartTimeField" <= 42) AND ((("aTable"."myEndTimeField" = 0) OR ("aTable"."myEndTimeField" > 42))) AND ((("aTable"."myGroupField" IS NULL) OR ("aTable"."myGroupField" = \'\') OR ("aTable"."myGroupField" = \'0\') OR (FIND_IN_SET(\'0\', "aTable"."myGroupField")) OR (FIND_IN_SET(\'-1\', "aTable"."myGroupField")))))',
             ],
             'Workspace, with WS preview and hidden record preview' => [
                 'tableName' => 'aTable',
@@ -66,7 +66,7 @@ class FrontendRestrictionContainerTest extends AbstractRestrictionTestCase
                 'hiddenPagePreview' => true,
                 'hiddenRecordPreview' => true,
                 'frontendUserGroups' => [0, -1],
-                'expectedSQL' => '("aTable"."deleted" = 0) AND (("aTable"."t3ver_wsid" IN (0, 1)) AND (("aTable"."t3ver_oid" = 0) OR ("aTable"."t3ver_state" = 4))) AND ("aTable"."myStartTimeField" <= 42) AND (("aTable"."myEndTimeField" = 0) OR ("aTable"."myEndTimeField" > 42)) AND (("aTable"."myGroupField" IS NULL) OR ("aTable"."myGroupField" = \'\') OR ("aTable"."myGroupField" = \'0\') OR (FIND_IN_SET(\'0\', "aTable"."myGroupField")) OR (FIND_IN_SET(\'-1\', "aTable"."myGroupField")))',
+                'expectedSQL' => '(("aTable"."deleted" = 0) AND ((("aTable"."t3ver_wsid" IN (0, 1)) AND ((("aTable"."t3ver_oid" = 0) OR ("aTable"."t3ver_state" = 4))))) AND ("aTable"."myStartTimeField" <= 42) AND ((("aTable"."myEndTimeField" = 0) OR ("aTable"."myEndTimeField" > 42))) AND ((("aTable"."myGroupField" IS NULL) OR ("aTable"."myGroupField" = \'\') OR ("aTable"."myGroupField" = \'0\') OR (FIND_IN_SET(\'0\', "aTable"."myGroupField")) OR (FIND_IN_SET(\'-1\', "aTable"."myGroupField")))))',
             ],
             'Live page, no preview' => [
                 'tableName' => 'pages',
@@ -75,7 +75,7 @@ class FrontendRestrictionContainerTest extends AbstractRestrictionTestCase
                 'hiddenPagePreview' => false,
                 'hiddenRecordPreview' => false,
                 'frontendUserGroups' => [0, -1],
-                'expectedSQL' => '("pages"."deleted" = 0) AND (("pages"."t3ver_wsid" = 0) AND (("pages"."t3ver_oid" = 0) OR ("pages"."t3ver_state" = 4))) AND ("pages"."hidden" = 0) AND ("pages"."starttime" <= 42) AND (("pages"."endtime" = 0) OR ("pages"."endtime" > 42)) AND (("pages"."fe_group" IS NULL) OR ("pages"."fe_group" = \'\') OR ("pages"."fe_group" = \'0\') OR (FIND_IN_SET(\'0\', "pages"."fe_group")) OR (FIND_IN_SET(\'-1\', "pages"."fe_group")))',
+                'expectedSQL' => '(("pages"."deleted" = 0) AND ((("pages"."t3ver_wsid" = 0) AND ((("pages"."t3ver_oid" = 0) OR ("pages"."t3ver_state" = 4))))) AND ("pages"."hidden" = 0) AND ("pages"."starttime" <= 42) AND ((("pages"."endtime" = 0) OR ("pages"."endtime" > 42))) AND ((("pages"."fe_group" IS NULL) OR ("pages"."fe_group" = \'\') OR ("pages"."fe_group" = \'0\') OR (FIND_IN_SET(\'0\', "pages"."fe_group")) OR (FIND_IN_SET(\'-1\', "pages"."fe_group")))))',
             ],
             'Live page, with hidden page preview' => [
                 'tableName' => 'pages',
@@ -84,7 +84,7 @@ class FrontendRestrictionContainerTest extends AbstractRestrictionTestCase
                 'hiddenPagePreview' => true,
                 'hiddenRecordPreview' => true,
                 'frontendUserGroups' => [0, -1],
-                'expectedSQL' => '("pages"."deleted" = 0) AND (("pages"."t3ver_wsid" = 0) AND (("pages"."t3ver_oid" = 0) OR ("pages"."t3ver_state" = 4))) AND ("pages"."starttime" <= 42) AND (("pages"."endtime" = 0) OR ("pages"."endtime" > 42)) AND (("pages"."fe_group" IS NULL) OR ("pages"."fe_group" = \'\') OR ("pages"."fe_group" = \'0\') OR (FIND_IN_SET(\'0\', "pages"."fe_group")) OR (FIND_IN_SET(\'-1\', "pages"."fe_group")))',
+                'expectedSQL' => '(("pages"."deleted" = 0) AND ((("pages"."t3ver_wsid" = 0) AND ((("pages"."t3ver_oid" = 0) OR ("pages"."t3ver_state" = 4))))) AND ("pages"."starttime" <= 42) AND ((("pages"."endtime" = 0) OR ("pages"."endtime" > 42))) AND ((("pages"."fe_group" IS NULL) OR ("pages"."fe_group" = \'\') OR ("pages"."fe_group" = \'0\') OR (FIND_IN_SET(\'0\', "pages"."fe_group")) OR (FIND_IN_SET(\'-1\', "pages"."fe_group")))))',
             ],
             'Workspace page, with WS preview' => [
                 'tableName' => 'pages',
@@ -93,7 +93,7 @@ class FrontendRestrictionContainerTest extends AbstractRestrictionTestCase
                 'hiddenPagePreview' => false,
                 'hiddenRecordPreview' => false,
                 'frontendUserGroups' => [0, -1],
-                'expectedSQL' => '("pages"."deleted" = 0) AND (("pages"."t3ver_wsid" IN (0, 1)) AND (("pages"."t3ver_oid" = 0) OR ("pages"."t3ver_state" = 4))) AND ("pages"."hidden" = 0) AND ("pages"."starttime" <= 42) AND (("pages"."endtime" = 0) OR ("pages"."endtime" > 42)) AND (("pages"."fe_group" IS NULL) OR ("pages"."fe_group" = \'\') OR ("pages"."fe_group" = \'0\') OR (FIND_IN_SET(\'0\', "pages"."fe_group")) OR (FIND_IN_SET(\'-1\', "pages"."fe_group")))',
+                'expectedSQL' => '(("pages"."deleted" = 0) AND ((("pages"."t3ver_wsid" IN (0, 1)) AND ((("pages"."t3ver_oid" = 0) OR ("pages"."t3ver_state" = 4))))) AND ("pages"."hidden" = 0) AND ("pages"."starttime" <= 42) AND ((("pages"."endtime" = 0) OR ("pages"."endtime" > 42))) AND ((("pages"."fe_group" IS NULL) OR ("pages"."fe_group" = \'\') OR ("pages"."fe_group" = \'0\') OR (FIND_IN_SET(\'0\', "pages"."fe_group")) OR (FIND_IN_SET(\'-1\', "pages"."fe_group")))))',
             ],
             'Workspace page, with WS preview and hidden pages preview' => [
                 'tableName' => 'pages',
@@ -102,7 +102,7 @@ class FrontendRestrictionContainerTest extends AbstractRestrictionTestCase
                 'hiddenPagePreview' => true,
                 'hiddenRecordPreview' => true,
                 'frontendUserGroups' => [0, -1],
-                'expectedSQL' => '("pages"."deleted" = 0) AND (("pages"."t3ver_wsid" IN (0, 1)) AND (("pages"."t3ver_oid" = 0) OR ("pages"."t3ver_state" = 4))) AND ("pages"."starttime" <= 42) AND (("pages"."endtime" = 0) OR ("pages"."endtime" > 42)) AND (("pages"."fe_group" IS NULL) OR ("pages"."fe_group" = \'\') OR ("pages"."fe_group" = \'0\') OR (FIND_IN_SET(\'0\', "pages"."fe_group")) OR (FIND_IN_SET(\'-1\', "pages"."fe_group")))',
+                'expectedSQL' => '(("pages"."deleted" = 0) AND ((("pages"."t3ver_wsid" IN (0, 1)) AND ((("pages"."t3ver_oid" = 0) OR ("pages"."t3ver_state" = 4))))) AND ("pages"."starttime" <= 42) AND ((("pages"."endtime" = 0) OR ("pages"."endtime" > 42))) AND ((("pages"."fe_group" IS NULL) OR ("pages"."fe_group" = \'\') OR ("pages"."fe_group" = \'0\') OR (FIND_IN_SET(\'0\', "pages"."fe_group")) OR (FIND_IN_SET(\'-1\', "pages"."fe_group")))))',
             ],
             'Live, no preview with alias' => [
                 'tableName' => 'aTable',
@@ -111,7 +111,7 @@ class FrontendRestrictionContainerTest extends AbstractRestrictionTestCase
                 'hiddenPagePreview' => false,
                 'hiddenRecordPreview' => false,
                 'frontendUserGroups' => [0, -1],
-                'expectedSQL' => '("a"."deleted" = 0) AND (("a"."t3ver_wsid" = 0) AND (("a"."t3ver_oid" = 0) OR ("a"."t3ver_state" = 4))) AND ("a"."myHiddenField" = 0) AND ("a"."myStartTimeField" <= 42) AND (("a"."myEndTimeField" = 0) OR ("a"."myEndTimeField" > 42)) AND (("a"."myGroupField" IS NULL) OR ("a"."myGroupField" = \'\') OR ("a"."myGroupField" = \'0\') OR (FIND_IN_SET(\'0\', "a"."myGroupField")) OR (FIND_IN_SET(\'-1\', "a"."myGroupField")))',
+                'expectedSQL' => '(("a"."deleted" = 0) AND ((("a"."t3ver_wsid" = 0) AND ((("a"."t3ver_oid" = 0) OR ("a"."t3ver_state" = 4))))) AND ("a"."myHiddenField" = 0) AND ("a"."myStartTimeField" <= 42) AND ((("a"."myEndTimeField" = 0) OR ("a"."myEndTimeField" > 42))) AND ((("a"."myGroupField" IS NULL) OR ("a"."myGroupField" = \'\') OR ("a"."myGroupField" = \'0\') OR (FIND_IN_SET(\'0\', "a"."myGroupField")) OR (FIND_IN_SET(\'-1\', "a"."myGroupField")))))',
             ],
         ];
     }
diff --git a/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/LimitToTablesRestrictionContainerTest.php b/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/LimitToTablesRestrictionContainerTest.php
index 327e5930bdd9..5e00a89b05f3 100644
--- a/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/LimitToTablesRestrictionContainerTest.php
+++ b/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/LimitToTablesRestrictionContainerTest.php
@@ -52,7 +52,7 @@ class LimitToTablesRestrictionContainerTest extends AbstractRestrictionTestCase
         $subject->addForTables(new DefaultRestrictionContainer(), ['bt']);
         $expression = $subject->buildExpression(['aTable' => 'aTable', 'bTable' => 'bTable', 'bt' => 'bTable'], $this->expressionBuilder);
 
-        self::assertSame('("bt"."deleted" = 0) AND ("bt"."hidden" = 0)', (string)$expression);
+        self::assertSame('(("bt"."deleted" = 0) AND ("bt"."hidden" = 0))', (string)$expression);
     }
 
     /**
diff --git a/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/PagePermissionRestrictionTest.php b/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/PagePermissionRestrictionTest.php
index c841c9d54a31..ec13799a9651 100644
--- a/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/PagePermissionRestrictionTest.php
+++ b/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/PagePermissionRestrictionTest.php
@@ -111,7 +111,7 @@ class PagePermissionRestrictionTest extends AbstractRestrictionTestCase
         $aspect = $this->getPreparedUserAspect(true, false, 2, [13, 14, 15, 16]);
         $subject = new PagePermissionRestriction($aspect, Permission::PAGE_SHOW);
         $expression = $subject->buildExpression(['pages' => 'pages'], $this->expressionBuilder);
-        self::assertEquals('("pages"."perms_everybody" & 1 = 1) OR (("pages"."perms_userid" = 2) AND ("pages"."perms_user" & 1 = 1)) OR (("pages"."perms_groupid" IN (13, 14, 15, 16)) AND ("pages"."perms_group" & 1 = 1))', (string)$expression);
+        self::assertEquals('(("pages"."perms_everybody" & 1 = 1) OR ((("pages"."perms_userid" = 2) AND ("pages"."perms_user" & 1 = 1))) OR ((("pages"."perms_groupid" IN (13, 14, 15, 16)) AND ("pages"."perms_group" & 1 = 1))))', (string)$expression);
     }
 
     /**
@@ -122,6 +122,6 @@ class PagePermissionRestrictionTest extends AbstractRestrictionTestCase
         $aspect = $this->getPreparedUserAspect(true, false, 42, [13, 14, 15, 16]);
         $subject = new PagePermissionRestriction($aspect, Permission::PAGE_DELETE);
         $expression = $subject->buildExpression(['pages' => 'pages'], $this->expressionBuilder);
-        self::assertEquals('("pages"."perms_everybody" & 4 = 4) OR (("pages"."perms_userid" = 42) AND ("pages"."perms_user" & 4 = 4)) OR (("pages"."perms_groupid" IN (13, 14, 15, 16)) AND ("pages"."perms_group" & 4 = 4))', (string)$expression);
+        self::assertEquals('(("pages"."perms_everybody" & 4 = 4) OR ((("pages"."perms_userid" = 42) AND ("pages"."perms_user" & 4 = 4))) OR ((("pages"."perms_groupid" IN (13, 14, 15, 16)) AND ("pages"."perms_group" & 4 = 4))))', (string)$expression);
     }
 }
diff --git a/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/WorkspaceRestrictionTest.php b/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/WorkspaceRestrictionTest.php
index 2b4c27b09867..3df2568b1d2f 100644
--- a/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/WorkspaceRestrictionTest.php
+++ b/typo3/sysext/core/Tests/Unit/Database/Query/Restriction/WorkspaceRestrictionTest.php
@@ -31,7 +31,7 @@ class WorkspaceRestrictionTest extends AbstractRestrictionTestCase
         ];
         $subject = new WorkspaceRestriction(0);
         $expression = $subject->buildExpression(['aTable' => 'aTable'], $this->expressionBuilder);
-        self::assertSame('("aTable"."t3ver_wsid" = 0) AND (("aTable"."t3ver_oid" = 0) OR ("aTable"."t3ver_state" = 4))', (string)$expression);
+        self::assertSame('(("aTable"."t3ver_wsid" = 0) AND ((("aTable"."t3ver_oid" = 0) OR ("aTable"."t3ver_state" = 4))))', (string)$expression);
     }
 
     /**
@@ -44,7 +44,7 @@ class WorkspaceRestrictionTest extends AbstractRestrictionTestCase
         ];
         $subject = new WorkspaceRestriction(42);
         $expression = $subject->buildExpression(['aTable' => 'aTable'], $this->expressionBuilder);
-        self::assertSame('("aTable"."t3ver_wsid" IN (0, 42)) AND (("aTable"."t3ver_oid" = 0) OR ("aTable"."t3ver_state" = 4))', (string)$expression);
+        self::assertSame('(("aTable"."t3ver_wsid" IN (0, 42)) AND ((("aTable"."t3ver_oid" = 0) OR ("aTable"."t3ver_state" = 4))))', (string)$expression);
     }
 
     /**
@@ -83,7 +83,7 @@ class WorkspaceRestrictionTest extends AbstractRestrictionTestCase
         ];
         $subject = new WorkspaceRestriction(42, true);
         $expression = $subject->buildExpression(['aTable' => 'aTable'], $this->expressionBuilder);
-        self::assertSame('("aTable"."t3ver_wsid" IN (0, 42)) AND ("t3ver_state" <> 2)', (string)$expression);
+        self::assertSame('(("aTable"."t3ver_wsid" IN (0, 42)) AND ("t3ver_state" <> 2))', (string)$expression);
     }
     /**
      * @test
@@ -95,6 +95,6 @@ class WorkspaceRestrictionTest extends AbstractRestrictionTestCase
         ];
         $subject = new WorkspaceRestriction(0, true);
         $expression = $subject->buildExpression(['aTable' => 'aTable'], $this->expressionBuilder);
-        self::assertSame('("aTable"."t3ver_wsid" = 0) AND ("t3ver_state" <> 2)', (string)$expression);
+        self::assertSame('(("aTable"."t3ver_wsid" = 0) AND ("t3ver_state" <> 2))', (string)$expression);
     }
 }
diff --git a/typo3/sysext/core/Tests/UnitDeprecated/Database/Query/Restriction/BackendWorkspaceRestrictionTest.php b/typo3/sysext/core/Tests/UnitDeprecated/Database/Query/Restriction/BackendWorkspaceRestrictionTest.php
index 2c4baf126edc..2c465753a315 100644
--- a/typo3/sysext/core/Tests/UnitDeprecated/Database/Query/Restriction/BackendWorkspaceRestrictionTest.php
+++ b/typo3/sysext/core/Tests/UnitDeprecated/Database/Query/Restriction/BackendWorkspaceRestrictionTest.php
@@ -32,7 +32,7 @@ class BackendWorkspaceRestrictionTest extends AbstractRestrictionTestCase
         ];
         $subject = new BackendWorkspaceRestriction(0);
         $expression = $subject->buildExpression(['aTable' => 'aTable'], $this->expressionBuilder);
-        self::assertSame('("aTable"."t3ver_wsid" = 0) OR ("aTable"."t3ver_state" <= 0)', (string)$expression);
+        self::assertSame('(("aTable"."t3ver_wsid" = 0) OR ("aTable"."t3ver_state" <= 0))', (string)$expression);
     }
 
     /**
@@ -45,7 +45,7 @@ class BackendWorkspaceRestrictionTest extends AbstractRestrictionTestCase
         ];
         $subject = new BackendWorkspaceRestriction(42);
         $expression = $subject->buildExpression(['aTable' => 'aTable'], $this->expressionBuilder);
-        self::assertSame('("aTable"."t3ver_wsid" = 42) OR ("aTable"."t3ver_state" <= 0)', (string)$expression);
+        self::assertSame('(("aTable"."t3ver_wsid" = 42) OR ("aTable"."t3ver_state" <= 0))', (string)$expression);
     }
 
     /**
@@ -58,7 +58,7 @@ class BackendWorkspaceRestrictionTest extends AbstractRestrictionTestCase
         ];
         $subject = new BackendWorkspaceRestriction(0, false);
         $expression = $subject->buildExpression(['aTable' => 'aTable'], $this->expressionBuilder);
-        self::assertSame('("aTable"."t3ver_wsid" = 0) AND ("aTable"."t3ver_oid" = 0)', (string)$expression);
+        self::assertSame('(("aTable"."t3ver_wsid" = 0) AND ("aTable"."t3ver_oid" = 0))', (string)$expression);
     }
 
     /**
@@ -71,6 +71,6 @@ class BackendWorkspaceRestrictionTest extends AbstractRestrictionTestCase
         ];
         $subject = new BackendWorkspaceRestriction(42, false);
         $expression = $subject->buildExpression(['aTable' => 'aTable'], $this->expressionBuilder);
-        self::assertSame('("aTable"."t3ver_wsid" = 42) AND ("aTable"."t3ver_oid" > 0)', (string)$expression);
+        self::assertSame('(("aTable"."t3ver_wsid" = 42) AND ("aTable"."t3ver_oid" > 0))', (string)$expression);
     }
 }
diff --git a/typo3/sysext/core/Tests/UnitDeprecated/Database/Query/Restriction/FrontendWorkspaceRestrictionTest.php b/typo3/sysext/core/Tests/UnitDeprecated/Database/Query/Restriction/FrontendWorkspaceRestrictionTest.php
index 3fbe100dbaef..1cbc08ccf906 100644
--- a/typo3/sysext/core/Tests/UnitDeprecated/Database/Query/Restriction/FrontendWorkspaceRestrictionTest.php
+++ b/typo3/sysext/core/Tests/UnitDeprecated/Database/Query/Restriction/FrontendWorkspaceRestrictionTest.php
@@ -39,7 +39,7 @@ class FrontendWorkspaceRestrictionTest extends AbstractRestrictionTestCase
 
         $subject = new FrontendWorkspaceRestriction(0);
         $expression = $subject->buildExpression(['aTable' => 'aTable'], $this->expressionBuilder);
-        self::assertSame('("aTable"."t3ver_state" <= 0) AND ("aTable"."t3ver_oid" = 0)', (string)$expression);
+        self::assertSame('(("aTable"."t3ver_state" <= 0) AND ("aTable"."t3ver_oid" = 0))', (string)$expression);
     }
 
     /**
@@ -57,7 +57,7 @@ class FrontendWorkspaceRestrictionTest extends AbstractRestrictionTestCase
 
         $subject = new FrontendWorkspaceRestriction(42, true);
         $expression = $subject->buildExpression(['aTable' => 'aTable'], $this->expressionBuilder);
-        self::assertSame('(("aTable"."t3ver_wsid" = 0) OR ("aTable"."t3ver_wsid" = 42)) AND ("aTable"."t3ver_oid" = 0)', (string)$expression);
+        self::assertSame('(((("aTable"."t3ver_wsid" = 0) OR ("aTable"."t3ver_wsid" = 42))) AND ("aTable"."t3ver_oid" = 0))', (string)$expression);
     }
 
     /**
@@ -75,6 +75,6 @@ class FrontendWorkspaceRestrictionTest extends AbstractRestrictionTestCase
 
         $subject = new FrontendWorkspaceRestriction(42, true, false);
         $expression = $subject->buildExpression(['aTable' => 'aTable'], $this->expressionBuilder);
-        self::assertSame('("aTable"."t3ver_wsid" = 0) OR ("aTable"."t3ver_wsid" = 42)', (string)$expression);
+        self::assertSame('(("aTable"."t3ver_wsid" = 0) OR ("aTable"."t3ver_wsid" = 42))', (string)$expression);
     }
 }
diff --git a/typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Storage/Typo3DbQueryParserTest.php b/typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Storage/Typo3DbQueryParserTest.php
index 6cc5f08785ef..0acbfcb46850 100644
--- a/typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Storage/Typo3DbQueryParserTest.php
+++ b/typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Storage/Typo3DbQueryParserTest.php
@@ -442,7 +442,7 @@ class Typo3DbQueryParserTest extends UnitTestCase
         $mockTypo3DbQueryParser->_set('queryBuilder', $queryBuilderProphet->reveal());
 
         $compositeExpression = $mockTypo3DbQueryParser->_call('getLanguageStatement', $table, $table, $querySettings);
-        $expectedSql = '(' . $table . '.sys_language_uid = -1) OR ((' . $table . '.sys_language_uid = 2) AND (' . $table . '.l10n_parent IN (SELECT ' . $table . '_dl.uid FROM ' . $table . ' ' . $table . '_dl WHERE (' . $table . '_dl.l10n_parent = 0) AND (' . $table . '_dl.sys_language_uid = 0)))) OR ((' . $table . '.sys_language_uid = 0) AND (' . $table . '.uid NOT IN (SELECT ' . $table . '_to.l10n_parent FROM ' . $table . ' ' . $table . '_dl, ' . $table . ' ' . $table . '_to WHERE (' . $table . '_to.l10n_parent > 0) AND (' . $table . '_to.sys_language_uid = 2))))';
+        $expectedSql = '((' . $table . '.sys_language_uid = -1) OR (((' . $table . '.sys_language_uid = 2) AND (' . $table . '.l10n_parent IN (SELECT ' . $table . '_dl.uid FROM ' . $table . ' ' . $table . '_dl WHERE ((' . $table . '_dl.l10n_parent = 0) AND (' . $table . '_dl.sys_language_uid = 0)))))) OR (((' . $table . '.sys_language_uid = 0) AND (' . $table . '.uid NOT IN (SELECT ' . $table . '_to.l10n_parent FROM ' . $table . ' ' . $table . '_dl, ' . $table . ' ' . $table . '_to WHERE ((' . $table . '_to.l10n_parent > 0) AND (' . $table . '_to.sys_language_uid = 2)))))))';
         self::assertSame($expectedSql, $compositeExpression->__toString());
     }
 
@@ -468,7 +468,7 @@ class Typo3DbQueryParserTest extends UnitTestCase
         $queryBuilderProphet = $this->getQueryBuilderProphetWithQueryBuilderForSubselect();
         $mockTypo3DbQueryParser->_set('queryBuilder', $queryBuilderProphet->reveal());
         $compositeExpression= $mockTypo3DbQueryParser->_call('getLanguageStatement', $table, $table, $querySettings);
-        $expectedSql = '(' . $table . '.sys_language_uid = -1) OR ((' . $table . '.sys_language_uid = 2) AND (' . $table . '.l10n_parent IN (SELECT ' . $table . '_dl.uid FROM ' . $table . ' ' . $table . '_dl WHERE (' . $table . '_dl.l10n_parent = 0) AND (' . $table . '_dl.sys_language_uid = 0) AND (' . $table . '_dl.deleted = 0)))) OR ((' . $table . '.sys_language_uid = 0) AND (' . $table . '.uid NOT IN (SELECT ' . $table . '_to.l10n_parent FROM ' . $table . ' ' . $table . '_dl, ' . $table . ' ' . $table . '_to WHERE (' . $table . '_to.l10n_parent > 0) AND (' . $table . '_to.sys_language_uid = 2) AND ((' . $table . '_dl.deleted = 0) AND (' . $table . '_to.deleted = 0)))))';
+        $expectedSql = '((' . $table . '.sys_language_uid = -1) OR (((' . $table . '.sys_language_uid = 2) AND (' . $table . '.l10n_parent IN (SELECT ' . $table . '_dl.uid FROM ' . $table . ' ' . $table . '_dl WHERE ((' . $table . '_dl.l10n_parent = 0) AND (' . $table . '_dl.sys_language_uid = 0) AND (' . $table . '_dl.deleted = 0)))))) OR (((' . $table . '.sys_language_uid = 0) AND (' . $table . '.uid NOT IN (SELECT ' . $table . '_to.l10n_parent FROM ' . $table . ' ' . $table . '_dl, ' . $table . ' ' . $table . '_to WHERE ((' . $table . '_to.l10n_parent > 0) AND (' . $table . '_to.sys_language_uid = 2) AND (((' . $table . '_dl.deleted = 0) AND (' . $table . '_to.deleted = 0)))))))))';
         self::assertSame($expectedSql, $compositeExpression->__toString());
     }
 
@@ -496,7 +496,7 @@ class Typo3DbQueryParserTest extends UnitTestCase
 
         $mockTypo3DbQueryParser->_set('queryBuilder', $queryBuilderProphet->reveal());
         $compositeExpression = $mockTypo3DbQueryParser->_call('getLanguageStatement', $table, $table, $querySettings);
-        $expectedSql = '(' . $table . '.sys_language_uid = -1) OR ((' . $table . '.sys_language_uid = 2) AND (' . $table . '.l10n_parent IN (SELECT ' . $table . '_dl.uid FROM ' . $table . ' ' . $table . '_dl WHERE (' . $table . '_dl.l10n_parent = 0) AND (' . $table . '_dl.sys_language_uid = 0) AND (' . $table . '_dl.deleted = 0)))) OR ((' . $table . '.sys_language_uid = 0) AND (' . $table . '.uid NOT IN (SELECT ' . $table . '_to.l10n_parent FROM ' . $table . ' ' . $table . '_dl, ' . $table . ' ' . $table . '_to WHERE (' . $table . '_to.l10n_parent > 0) AND (' . $table . '_to.sys_language_uid = 2) AND ((' . $table . '_dl.deleted = 0) AND (' . $table . '_to.deleted = 0)))))';
+        $expectedSql = '((' . $table . '.sys_language_uid = -1) OR (((' . $table . '.sys_language_uid = 2) AND (' . $table . '.l10n_parent IN (SELECT ' . $table . '_dl.uid FROM ' . $table . ' ' . $table . '_dl WHERE ((' . $table . '_dl.l10n_parent = 0) AND (' . $table . '_dl.sys_language_uid = 0) AND (' . $table . '_dl.deleted = 0)))))) OR (((' . $table . '.sys_language_uid = 0) AND (' . $table . '.uid NOT IN (SELECT ' . $table . '_to.l10n_parent FROM ' . $table . ' ' . $table . '_dl, ' . $table . ' ' . $table . '_to WHERE ((' . $table . '_to.l10n_parent > 0) AND (' . $table . '_to.sys_language_uid = 2) AND (((' . $table . '_dl.deleted = 0) AND (' . $table . '_to.deleted = 0)))))))))';
         self::assertSame($expectedSql, $compositeExpression->__toString());
     }
 
@@ -593,12 +593,12 @@ class Typo3DbQueryParserTest extends UnitTestCase
         return [
             'in be: include all' => ['BE', true, [], true, ''],
             'in be: ignore enable fields but do not include deleted' => ['BE', true, [], false, 'tx_foo_table.deleted_column=0'],
-            'in be: respect enable fields but include deleted' => ['BE', false, [], true, '(tx_foo_table.disabled_column = 0) AND (tx_foo_table.starttime_column <= 1451779200)'],
-            'in be: respect enable fields and do not include deleted' => ['BE', false, [], false, '(tx_foo_table.disabled_column = 0) AND (tx_foo_table.starttime_column <= 1451779200) AND tx_foo_table.deleted_column=0'],
+            'in be: respect enable fields but include deleted' => ['BE', false, [], true, '((tx_foo_table.disabled_column = 0) AND (tx_foo_table.starttime_column <= 1451779200))'],
+            'in be: respect enable fields and do not include deleted' => ['BE', false, [], false, '((tx_foo_table.disabled_column = 0) AND (tx_foo_table.starttime_column <= 1451779200)) AND tx_foo_table.deleted_column=0'],
             'in fe: include all' => ['FE', true, [], true, ''],
             'in fe: ignore enable fields but do not include deleted' => ['FE', true, [], false, 'tx_foo_table.deleted_column=0'],
-            'in fe: ignore only starttime and do not include deleted' => ['FE', true, ['starttime'], false, '(tx_foo_table.deleted_column = 0) AND (tx_foo_table.disabled_column = 0)'],
-            'in fe: respect enable fields and do not include deleted' => ['FE', false, [], false, '(tx_foo_table.deleted_column = 0) AND (tx_foo_table.disabled_column = 0) AND (tx_foo_table.starttime_column <= 1451779200)'],
+            'in fe: ignore only starttime and do not include deleted' => ['FE', true, ['starttime'], false, '((tx_foo_table.deleted_column = 0) AND (tx_foo_table.disabled_column = 0))'],
+            'in fe: respect enable fields and do not include deleted' => ['FE', false, [], false, '((tx_foo_table.deleted_column = 0) AND (tx_foo_table.disabled_column = 0) AND (tx_foo_table.starttime_column <= 1451779200))'],
         ];
     }
 
@@ -664,9 +664,9 @@ class Typo3DbQueryParserTest extends UnitTestCase
     {
         return [
             'in be: respectEnableFields=false' => ['BE', false, ''],
-            'in be: respectEnableFields=true' => ['BE', true, '(tx_foo_table.disabled_column = 0) AND (tx_foo_table.starttime_column <= 1451779200) AND tx_foo_table.deleted_column=0'],
+            'in be: respectEnableFields=true' => ['BE', true, '((tx_foo_table.disabled_column = 0) AND (tx_foo_table.starttime_column <= 1451779200)) AND tx_foo_table.deleted_column=0'],
             'in FE: respectEnableFields=false' => ['FE', false, ''],
-            'in FE: respectEnableFields=true' => ['FE', true, '(tx_foo_table.deleted_column = 0) AND (tx_foo_table.disabled_column = 0) AND (tx_foo_table.starttime_column <= 1451779200)'],
+            'in FE: respectEnableFields=true' => ['FE', true, '((tx_foo_table.deleted_column = 0) AND (tx_foo_table.disabled_column = 0) AND (tx_foo_table.starttime_column <= 1451779200))'],
         ];
     }
 
@@ -726,6 +726,72 @@ class Typo3DbQueryParserTest extends UnitTestCase
         unset($GLOBALS['TCA'][$tableName]);
     }
 
+    public function providerForRespectEnableFieldsWithOnlyEndtime()
+    {
+        return [
+            'in be: respectEnableFields=false' => ['BE', false, false, ''],
+            'in be: respectEnableFields=true' => ['BE', true, true, '((tx_foo_table.endtime_column = 0) OR (tx_foo_table.endtime_column > 1451779200)) AND tx_foo_table.deleted_column=0'],
+            'in be: respectEnableFields=true and includeDeleted=false' => ['BE', true, false, '((tx_foo_table.endtime_column = 0) OR (tx_foo_table.endtime_column > 1451779200))'],
+            'in FE: respectEnableFields=false' => ['FE', false, false, ''],
+            'in FE: respectEnableFields=true' => ['FE', true, true, '((tx_foo_table.endtime_column = 0) OR (tx_foo_table.endtime_column > 1451779200)) AND tx_foo_table.deleted_column=0'],
+            'in FE: respectEnableFields=true and includeDeleted=false' => ['FE', true, false, '((tx_foo_table.endtime_column = 0) OR (tx_foo_table.endtime_column > 1451779200))'],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider providerForRespectEnableFieldsWithOnlyEndtime
+     */
+    public function respectEnableFieldsSettingGeneratesCorrectStatementWithOnlyEndTime($mode, $respectEnableFields, $respectDeletedElements, $expectedSql)
+    {
+        $tableName = 'tx_foo_table';
+        $GLOBALS['TCA'][$tableName]['ctrl'] = [
+            'enablecolumns' => [
+                'endtime' => 'endtime_column',
+            ],
+            'delete' => 'deleted_column',
+        ];
+        if (!$respectDeletedElements) {
+            unset($GLOBALS['TCA'][$tableName]['ctrl']['delete']);
+        }
+        // simulate time for backend enable fields
+        $GLOBALS['SIM_ACCESS_TIME'] = 1451779200;
+        // simulate time for frontend (PageRepository) enable fields
+        $dateAspect = new DateTimeAspect(new \DateTimeImmutable('3.1.2016'));
+        $context = new Context(['date' => $dateAspect]);
+        GeneralUtility::setSingletonInstance(Context::class, $context);
+
+        $connectionProphet = $this->prophesize(Connection::class);
+        $connectionProphet->quoteIdentifier(Argument::cetera())->willReturnArgument(0);
+        $connectionProphet->getExpressionBuilder(Argument::cetera())->willReturn(
+            GeneralUtility::makeInstance(ExpressionBuilder::class, $connectionProphet->reveal())
+        );
+        $queryBuilderProphet = $this->prophesize(QueryBuilder::class);
+        $queryBuilderProphet->expr()->willReturn(
+            GeneralUtility::makeInstance(ExpressionBuilder::class, $connectionProphet->reveal())
+        );
+        $queryBuilderProphet->createNamedParameter(Argument::cetera())->willReturnArgument(0);
+
+        $connectionPoolProphet = $this->prophesize(ConnectionPool::class);
+        $connectionPoolProphet->getQueryBuilderForTable(Argument::any())->willReturn($queryBuilderProphet->reveal());
+        $connectionPoolProphet->getConnectionForTable(Argument::any())->willReturn($connectionProphet->reveal());
+        GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal());
+        GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphet->reveal());
+
+        /** @var \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings $mockQuerySettings */
+        $mockQuerySettings = $this->getMockBuilder(Typo3QuerySettings::class)
+            ->setMethods(['dummy'])
+            ->disableOriginalConstructor()
+            ->getMock();
+        $mockQuerySettings->setIgnoreEnableFields(!$respectEnableFields);
+        $mockQuerySettings->setIncludeDeleted(!$respectEnableFields);
+
+        $mockTypo3DbQueryParser = $this->getAccessibleMock(Typo3DbQueryParser::class, ['dummy'], [], '', false);
+        $actualSql = $mockTypo3DbQueryParser->_call('getVisibilityConstraintStatement', $mockQuerySettings, $tableName, $tableName);
+        self::assertSame($expectedSql, $actualSql);
+        unset($GLOBALS['TCA'][$tableName]);
+    }
+
     /**
      * @test
      */
-- 
GitLab