[!!!][TASK] Migrate Extbase Query Parser to Doctrine DBAL 84/49584/13
authorBenni Mack <benni@typo3.org>
Wed, 24 Aug 2016 21:37:39 +0000 (23:37 +0200)
committerTymoteusz Motylewski <t.motylewski@gmail.com>
Sun, 9 Oct 2016 12:37:48 +0000 (14:37 +0200)
The Extbase Typo3DbQueryParser which builds the SQL
statement for most of Extbase's database calls is completely
rewritten to use Doctrine DBAL QueryBuilder.

The QueryParser now returns a properly filled QueryBuilder
object instead of an array with SQL parts.

The Typo3DbBackend of Extbase is using the QueryBuilder
under the hood then.

Resolves: #77379
Releases: master
Change-Id: I5936e639a9241a7d41ac60703efed83bda73f5f7
Reviewed-on: https://review.typo3.org/49584
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Tymoteusz Motylewski <t.motylewski@gmail.com>
Tested-by: Tymoteusz Motylewski <t.motylewski@gmail.com>
typo3/sysext/core/Documentation/Changelog/master/Breaking-77379-DoctrineTypo3DbQueryParser.rst [new file with mode: 0644]
typo3/sysext/extbase/Classes/Persistence/Generic/Storage/Typo3DbBackend.php
typo3/sysext/extbase/Classes/Persistence/Generic/Storage/Typo3DbQueryParser.php
typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/ext_tables.sql
typo3/sysext/extbase/Tests/Functional/Persistence/Fixtures/blogs.xml
typo3/sysext/extbase/Tests/Functional/Persistence/Fixtures/fe_groups.xml
typo3/sysext/extbase/Tests/Functional/Persistence/Fixtures/fe_users.xml
typo3/sysext/extbase/Tests/Functional/Persistence/QueryParserTest.php
typo3/sysext/extbase/Tests/Functional/Persistence/TranslationTest.php
typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Storage/Typo3DbBackendTest.php
typo3/sysext/extbase/Tests/Unit/Persistence/Generic/Storage/Typo3DbQueryParserTest.php

diff --git a/typo3/sysext/core/Documentation/Changelog/master/Breaking-77379-DoctrineTypo3DbQueryParser.rst b/typo3/sysext/core/Documentation/Changelog/master/Breaking-77379-DoctrineTypo3DbQueryParser.rst
new file mode 100644 (file)
index 0000000..a583b06
--- /dev/null
@@ -0,0 +1,36 @@
+.. include:: ../../Includes.txt
+
+===============================================
+Breaking: #77379 - Doctrine: Typo3DbQueryParser
+===============================================
+
+See :forge:`77379`
+
+Description
+===========
+
+While migrating the database endpoint for the persistence functionality of Extbase to Doctrine DBAL, the Typo3DbQueryParser class
+has been completely rewritten to work on a QueryBuilder object instead of plain arrays and strings. The PHP method
+`Typo3DbQueryParser->parseQuery()` has been removed, instead the new equivalent
+`Typo3DbQueryParser->convertQueryToDoctrineQueryBuilder()` is introduced.
+
+Additionally, the PHP method `Typo3DBBackend->injectQueryParser()` has been removed, as the Typo3DbQueryParser class is not a
+singleton instance anymore but always rebuilt when needed.
+
+
+Impact
+======
+
+Calling one of the methods above will result in a fatal PHP error.
+
+
+Affected Installations
+======================
+
+TYPO3 instances with custom Extbase database backend and parsing functionality.
+
+
+Migration
+=========
+
+Switch to Doctrine DBAL and `Typo3DbQueryParser->convertQueryToDoctrineQueryBuilder()` which results in the same behaviour.
index d20f11e..93d95ea 100644 (file)
@@ -79,9 +79,9 @@ class Typo3DbBackend implements BackendInterface, SingletonInterface
     protected $environmentService;
 
     /**
-     * @var \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser
+     * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
      */
-    protected $queryParser;
+    protected $objectManager;
 
     /**
      * @param \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper $dataMapper
@@ -116,11 +116,11 @@ class Typo3DbBackend implements BackendInterface, SingletonInterface
     }
 
     /**
-     * @param \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser $queryParser
+     * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
      */
-    public function injectQueryParser(\TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser $queryParser)
+    public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
     {
-        $this->queryParser = $queryParser;
+        $this->objectManager = $objectManager;
     }
 
     /**
@@ -330,6 +330,7 @@ class Typo3DbBackend implements BackendInterface, SingletonInterface
      *
      * @param QueryInterface $query
      * @return array
+     * @throws SqlErrorException
      */
     public function getObjectDataByQuery(QueryInterface $query)
     {
@@ -337,8 +338,19 @@ class Typo3DbBackend implements BackendInterface, SingletonInterface
         if ($statement instanceof Qom\Statement) {
             $rows = $this->getObjectDataByRawQuery($statement);
         } else {
-            $statementParts = $this->queryParser->parseQuery($query);
-            $rows = $this->getRowsFromDatabase($statementParts);
+            $queryBuilder = $this->objectManager->get(Typo3DbQueryParser::class)
+                    ->convertQueryToDoctrineQueryBuilder($query);
+            if ($query->getOffset()) {
+                $queryBuilder->setFirstResult($query->getOffset());
+            }
+            if ($query->getLimit()) {
+                $queryBuilder->setMaxResults($query->getLimit());
+            }
+            try {
+                $rows = $queryBuilder->execute()->fetchAll();
+            } catch (DBALException $e) {
+                throw new SqlErrorException($e->getPrevious()->getMessage(), 1472074485);
+            }
         }
 
         $rows = $this->doLanguageAndWorkspaceOverlay($query->getSource(), $rows, $query->getQuerySettings());
@@ -346,58 +358,6 @@ class Typo3DbBackend implements BackendInterface, SingletonInterface
     }
 
     /**
-     * Creates the parameters for the query methods of the database methods in the TYPO3 core, from an array
-     * that came from a parsed query.
-     *
-     * @param array $statementParts
-     * @throws \InvalidArgumentException
-     * @return array
-     */
-    protected function createQueryCommandParametersFromStatementParts(array $statementParts)
-    {
-        if (isset($statementParts['offset']) && !isset($statementParts['limit'])) {
-            throw new \InvalidArgumentException(
-                'Trying to make query with offset and no limit, the offset would become a limit. You have to set a limit to use offset. To retrieve all rows from a certain offset up to the end of the result set, you can use some large number for the limit.',
-                1465223252
-            );
-        }
-        return [
-            'selectFields' => implode(' ', $statementParts['keywords']) . ' ' . implode(',', $statementParts['fields']),
-            'fromTable'    => implode(' ', $statementParts['tables']) . ' ' . implode(' ', $statementParts['unions']),
-            'whereClause'  => (!empty($statementParts['where']) ? implode('', $statementParts['where']) : '1=1')
-                . (!empty($statementParts['additionalWhereClause'])
-                    ? ' AND ' . implode(' AND ', $statementParts['additionalWhereClause'])
-                    : ''
-            ),
-            'orderBy'      => (!empty($statementParts['orderings']) ? implode(', ', $statementParts['orderings']) : ''),
-            'limit'        => ($statementParts['offset'] ? $statementParts['offset'] . ', ' : '')
-                . ($statementParts['limit'] ? $statementParts['limit'] : '')
-        ];
-    }
-
-    /**
-     * Fetches the rows directly from the database, not using prepared statement
-     *
-     * @param array $statementParts
-     * @return array the result
-     */
-    protected function getRowsFromDatabase(array $statementParts)
-    {
-        $queryCommandParameters = $this->createQueryCommandParametersFromStatementParts($statementParts);
-        $rows = $this->databaseHandle->exec_SELECTgetRows(
-            $queryCommandParameters['selectFields'],
-            $queryCommandParameters['fromTable'],
-            $queryCommandParameters['whereClause'],
-            '',
-            $queryCommandParameters['orderBy'],
-            $queryCommandParameters['limit']
-        );
-        $this->checkSqlErrors();
-
-        return $rows;
-    }
-
-    /**
      * Returns the object data using a custom statement
      *
      * @param Qom\Statement $statement
@@ -442,6 +402,7 @@ class Typo3DbBackend implements BackendInterface, SingletonInterface
      * @param QueryInterface $query
      * @throws Exception\BadConstraintException
      * @return int The number of matching tuples
+     * @throws SqlErrorException
      */
     public function getObjectCountByQuery(QueryInterface $query)
     {
@@ -449,29 +410,19 @@ class Typo3DbBackend implements BackendInterface, SingletonInterface
             throw new \TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception\BadConstraintException('Could not execute count on queries with a constraint of type TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Qom\\Statement', 1256661045);
         }
 
-        $statementParts = $this->queryParser->parseQuery($query);
-
-        $fields = '*';
-        if (isset($statementParts['keywords']['distinct'])) {
-            $fields = 'DISTINCT ' . reset($statementParts['tables']) . '.uid';
+        $queryBuilder = $this->objectManager->get(Typo3DbQueryParser::class)
+                ->convertQueryToDoctrineQueryBuilder($query);
+        try {
+            $count = $queryBuilder->count('*')->execute()->fetchColumn(0);
+        } catch (DBALException $e) {
+            throw new SqlErrorException($e->getPrevious()->getMessage(), 1472074379);
         }
-
-        $queryCommandParameters = $this->createQueryCommandParametersFromStatementParts($statementParts);
-        $count = $this->databaseHandle->exec_SELECTcountRows(
-            $fields,
-            $queryCommandParameters['fromTable'],
-            $queryCommandParameters['whereClause']
-        );
-        $this->checkSqlErrors();
-
-        if ($statementParts['offset']) {
-            $count -= $statementParts['offset'];
+        if ($query->getOffset()) {
+            $count -= $query->getOffset();
         }
-
-        if ($statementParts['limit']) {
-            $count = min($count, $statementParts['limit']);
+        if ($query->getLimit()) {
+            $count = min($count, $query->getLimit());
         }
-
         return (int)max(0, $count);
     }
 
@@ -631,22 +582,6 @@ class Typo3DbBackend implements BackendInterface, SingletonInterface
     }
 
     /**
-     * Checks if there are SQL errors in the last query, and if yes, throw an exception.
-     *
-     * @return void
-     * @param string $sql The SQL statement
-     * @throws SqlErrorException
-     */
-    protected function checkSqlErrors($sql = '')
-    {
-        $error = $this->databaseHandle->sql_error();
-        if ($error !== '') {
-            $error .= $sql ? ': ' . $sql : '';
-            throw new SqlErrorException($error, 1247602160);
-        }
-    }
-
-    /**
      * Clear the TYPO3 page cache for the given record.
      * If the record lies on a page, then we clear the cache of this page.
      * If the record has no PID column, we clear the cache of the current page as best-effort.
index 9ff7add..e709856 100644 (file)
@@ -16,26 +16,26 @@ namespace TYPO3\CMS\Extbase\Persistence\Generic\Storage;
 
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
+use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
+use TYPO3\CMS\Core\Database\Query\QueryBuilder;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Extbase\Persistence\Generic\Exception\InconsistentQuerySettingsException;
+use TYPO3\CMS\Extbase\Persistence\Generic\Exception\InvalidRelationConfigurationException;
+use TYPO3\CMS\Extbase\Persistence\Generic\Exception\MissingColumnMapException;
+use TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnsupportedOrderException;
 use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap;
 use TYPO3\CMS\Extbase\Persistence\Generic\Qom;
 use TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface;
 use TYPO3\CMS\Extbase\Persistence\QueryInterface;
+use TYPO3\CMS\Frontend\Page\PageRepository;
 
 /**
  * QueryParser, converting the qom to string representation
  */
-class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface
+class Typo3DbQueryParser
 {
     /**
-     * The TYPO3 database object
-     *
-     * @var \TYPO3\CMS\Core\Database\DatabaseConnection
-     */
-    protected $databaseHandle;
-
-    /**
      * @var \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper
      */
     protected $dataMapper;
@@ -43,7 +43,7 @@ class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface
     /**
      * The TYPO3 page repository. Used for language and workspace overlay
      *
-     * @var \TYPO3\CMS\Frontend\Page\PageRepository
+     * @var PageRepository
      */
     protected $pageRepository;
 
@@ -53,20 +53,11 @@ class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface
     protected $environmentService;
 
     /**
-     * @param \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper $dataMapper
-     */
-    public function injectDataMapper(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper $dataMapper)
-    {
-        $this->dataMapper = $dataMapper;
-    }
-
-    /**
-     * @param \TYPO3\CMS\Extbase\Service\EnvironmentService $environmentService
+     * Instance of the Doctrine query builder
+     *
+     * @var QueryBuilder
      */
-    public function injectEnvironmentService(\TYPO3\CMS\Extbase\Service\EnvironmentService $environmentService)
-    {
-        $this->environmentService = $environmentService;
-    }
+    protected $queryBuilder;
 
     /**
      * Maps domain model properties to their corresponding table aliases that are used in the query, e.g.:
@@ -79,95 +70,103 @@ class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface
     protected $tablePropertyMap = [];
 
     /**
-     * Constructor. takes the database handle from $GLOBALS['TYPO3_DB']
+     * Maps tablenames to their aliases to be used in where clauses etc.
+     * Mainly used for joins on the same table etc.
+     *
+     * @var array
      */
-    public function __construct()
-    {
-        $this->databaseHandle = $GLOBALS['TYPO3_DB'];
-    }
+    protected $tableAliasMap = [];
 
     /**
-     * Parses the query and returns the SQL statement parts.
+     * Stores all tables used in for SQL joins
      *
-     * @param QueryInterface $query The query
-     * @return array The SQL statement parts
+     * @var array
      */
-    public function parseQuery(QueryInterface $query)
-    {
-        $this->tablePropertyMap = [];
-        $sql = [];
-        $sql['keywords'] = [];
-        $sql['tables'] = [];
-        $sql['unions'] = [];
-        $sql['fields'] = [];
-        $sql['where'] = [];
-        $sql['additionalWhereClause'] = [];
-        $sql['orderings'] = [];
-        $sql['limit'] = ((int)$query->getLimit() ?: null);
-        $sql['offset'] = ((int)$query->getOffset() ?: null);
-        $sql['tableAliasMap'] = [];
-        $source = $query->getSource();
-        $this->parseSource($source, $sql);
-        $this->parseConstraint($query->getConstraint(), $source, $sql);
-        $this->parseOrderings($query->getOrderings(), $source, $sql);
-
-        foreach ($sql['tableAliasMap'] as $tableAlias => $tableName) {
-            $additionalWhereClause = $this->getAdditionalWhereClause($query->getQuerySettings(), $tableName, $tableAlias);
-            if ($additionalWhereClause !== '') {
-                $additionalWhereClause = $this->addNullConditionToStatementIfRequired($sql, $additionalWhereClause, $tableAlias);
-                $sql['additionalWhereClause'][] = $additionalWhereClause;
-            }
-        }
+    protected $unionTableAliasCache = [];
 
-        foreach ($sql['tableAliasMap'] as $tableAlias => $tableName) {
-            $statement = $this->getVisibilityConstraintStatement($query->getQuerySettings(), $tableName, $tableAlias);
-            if ($statement !== '') {
-                $statement = $this->addNullConditionToStatementIfRequired($sql, $statement, $tableAlias);
-                $sql['additionalWhereClause'][] = $statement;
-            }
-        }
+    /**
+     * @var string
+     */
+    protected $tableName = '';
 
-        return $sql;
+    /**
+     * @param \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper $dataMapper
+     */
+    public function injectDataMapper(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper $dataMapper)
+    {
+        $this->dataMapper = $dataMapper;
+    }
+
+    /**
+     * @param \TYPO3\CMS\Extbase\Service\EnvironmentService $environmentService
+     */
+    public function injectEnvironmentService(\TYPO3\CMS\Extbase\Service\EnvironmentService $environmentService)
+    {
+        $this->environmentService = $environmentService;
     }
 
     /**
-     * If the given table alias is used in a UNION statement it is required to
-     * add an additional condition that allows the fields of the joined table
-     * to be NULL. Otherwise the condition would be too strict and filter out
-     * records that are actually valid.
+     * Returns a ready to be executed QueryBuilder object, based on the query
      *
-     * @param array $sql The current SQL query parts.
-     * @param string $statement The SQL statement to which the NULL condition should be added.
-     * @param string $tableAlias The table alias used in the SQL statement.
-     * @return string The statement including the NULL condition or the original statement.
+     * @param QueryInterface $query
+     * @return QueryBuilder
      */
-    protected function addNullConditionToStatementIfRequired(array $sql, $statement, $tableAlias)
+    public function convertQueryToDoctrineQueryBuilder(QueryInterface $query)
     {
-        if (isset($sql['unions'][$tableAlias])) {
-            $statement = '((' . $statement . ') OR ' . $tableAlias . '.uid' . ' IS NULL)';
-        }
+        // Reset all properties
+        $this->tablePropertyMap = [];
+        $this->tableAliasMap = [];
+        $this->unionTableAliasCache = [];
+        $this->tableName = '';
+        // Find the right table name
+        $source = $query->getSource();
+        $this->initializeQueryBuilder($source);
+        $wherePredicates = $this->parseConstraint($query->getConstraint(), $source);
+        $this->queryBuilder->andWhere($wherePredicates);
+        $this->parseOrderings($query->getOrderings(), $source);
+        $this->addTypo3Constraints($query);
 
-        return $statement;
+        return $this->queryBuilder;
     }
 
     /**
-     * Transforms a Query Source into SQL and parameter arrays
+     * Creates the queryBuilder object whether it is a regular select or a JOIN
      *
      * @param Qom\SourceInterface $source The source
-     * @param array &$sql
      * @return void
      */
-    protected function parseSource(Qom\SourceInterface $source, array &$sql)
+    protected function initializeQueryBuilder(Qom\SourceInterface $source)
     {
         if ($source instanceof Qom\SelectorInterface) {
             $className = $source->getNodeTypeName();
             $tableName = $this->dataMapper->getDataMap($className)->getTableName();
-            $this->addRecordTypeConstraint($className, $sql);
-            $tableName = $this->getUniqueAlias($sql, $tableName);
-            $sql['fields'][$tableName] = $tableName . '.*';
-            $sql['tables'][$tableName] = $tableName;
+            $this->tableName = $tableName;
+
+            $this->queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+                ->getQueryBuilderForTable($tableName);
+
+            $this->queryBuilder
+                ->getRestrictions()
+                ->removeAll();
+
+            $tableAlias = $this->getUniqueAlias($tableName);
+
+            $this->queryBuilder
+                ->select($tableAlias . '.*')
+                ->from($tableName, $tableAlias);
+
+            $this->addRecordTypeConstraint($className);
         } elseif ($source instanceof Qom\JoinInterface) {
-            $this->parseJoin($source, $sql);
+            $leftSource = $source->getLeft();
+            $leftTableName = $leftSource->getSelectorName();
+
+            $this->queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+                ->getQueryBuilderForTable($leftTableName);
+            $leftTableAlias = $this->getUniqueAlias($leftTableName);
+            $this->queryBuilder
+                ->select($leftTableAlias . '.*')
+                ->from($leftTableName, $leftTableAlias);
+            $this->parseJoin($source, $leftTableAlias);
         }
     }
 
@@ -176,29 +175,26 @@ class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface
      *
      * @param Qom\ConstraintInterface $constraint The constraint
      * @param Qom\SourceInterface $source The source
-     * @param array &$sql The query parts
-     * @return void
+     * @return CompositeExpression|string
      */
-    protected function parseConstraint(Qom\ConstraintInterface $constraint = null, Qom\SourceInterface $source, array &$sql)
+    protected function parseConstraint(Qom\ConstraintInterface $constraint = null, Qom\SourceInterface $source)
     {
         if ($constraint instanceof Qom\AndInterface) {
-            $sql['where'][] = '(';
-            $this->parseConstraint($constraint->getConstraint1(), $source, $sql);
-            $sql['where'][] = ' AND ';
-            $this->parseConstraint($constraint->getConstraint2(), $source, $sql);
-            $sql['where'][] = ')';
+            return $this->queryBuilder->expr()->andX(
+                $this->parseConstraint($constraint->getConstraint1(), $source),
+                $this->parseConstraint($constraint->getConstraint2(), $source)
+            );
         } elseif ($constraint instanceof Qom\OrInterface) {
-            $sql['where'][] = '(';
-            $this->parseConstraint($constraint->getConstraint1(), $source, $sql);
-            $sql['where'][] = ' OR ';
-            $this->parseConstraint($constraint->getConstraint2(), $source, $sql);
-            $sql['where'][] = ')';
+            return $this->queryBuilder->expr()->orX(
+                $this->parseConstraint($constraint->getConstraint1(), $source),
+                $this->parseConstraint($constraint->getConstraint2(), $source)
+            );
         } elseif ($constraint instanceof Qom\NotInterface) {
-            $sql['where'][] = 'NOT (';
-            $this->parseConstraint($constraint->getConstraint(), $source, $sql);
-            $sql['where'][] = ')';
+            return ' NOT(' . $this->parseConstraint($constraint->getConstraint(), $source) . ')';
         } elseif ($constraint instanceof Qom\ComparisonInterface) {
-            $this->parseComparison($constraint, $source, $sql);
+            return $this->parseComparison($constraint, $source);
+        } else {
+            // exception?
         }
     }
 
@@ -207,40 +203,61 @@ class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface
      *
      * @param array $orderings An array of orderings (Qom\Ordering)
      * @param Qom\SourceInterface $source The source
-     * @param array &$sql The query parts
-     * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnsupportedOrderException
+     * @throws UnsupportedOrderException
      * @return void
      */
-    protected function parseOrderings(array $orderings, Qom\SourceInterface $source, array &$sql)
+    protected function parseOrderings(array $orderings, Qom\SourceInterface $source)
     {
         foreach ($orderings as $propertyName => $order) {
-            switch ($order) {
-                case QueryInterface::ORDER_ASCENDING:
-                    $order = 'ASC';
-                    break;
-                case QueryInterface::ORDER_DESCENDING:
-                    $order = 'DESC';
-                    break;
-                default:
-                    throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnsupportedOrderException('Unsupported order encountered.', 1242816074);
+            if ($order !== QueryInterface::ORDER_ASCENDING && $order !== QueryInterface::ORDER_DESCENDING) {
+                throw new UnsupportedOrderException('Unsupported order encountered.', 1242816074);
             }
-            $className = '';
+            $className = null;
             $tableName = '';
             if ($source instanceof Qom\SelectorInterface) {
                 $className = $source->getNodeTypeName();
                 $tableName = $this->dataMapper->convertClassNameToTableName($className);
                 $fullPropertyPath = '';
                 while (strpos($propertyName, '.') !== false) {
-                    $this->addUnionStatement($className, $tableName, $propertyName, $sql, $fullPropertyPath);
+                    $this->addUnionStatement($className, $tableName, $propertyName, $fullPropertyPath);
                 }
             } elseif ($source instanceof Qom\JoinInterface) {
                 $tableName = $source->getLeft()->getSelectorName();
             }
             $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
             if ($tableName !== '') {
-                $sql['orderings'][] = $tableName . '.' . $columnName . ' ' . $order;
+                $this->queryBuilder->addOrderBy($tableName . '.' . $columnName, $order);
             } else {
-                $sql['orderings'][] = $columnName . ' ' . $order;
+                $this->queryBuilder->addOrderBy($columnName, $order);
+            }
+        }
+    }
+
+    /**
+     * add TYPO3 Constraints for all tables to the queryBuilder
+     *
+     * @param QueryInterface $query
+     * @return void
+     */
+    protected function addTypo3Constraints(QueryInterface $query)
+    {
+        foreach ($this->tableAliasMap as $tableAlias => $tableName) {
+            $additionalWhereClauses = $this->getAdditionalWhereClause($query->getQuerySettings(), $tableName, $tableAlias);
+            $statement = $this->getVisibilityConstraintStatement($query->getQuerySettings(), $tableName, $tableAlias);
+            if ($statement !== '') {
+                $additionalWhereClauses[] = $statement;
+            }
+            if (!empty($additionalWhereClauses)) {
+                if (in_array($tableAlias, $this->unionTableAliasCache, true)) {
+                    $this->queryBuilder->andWhere(
+                        $this->queryBuilder->expr()->orX(
+                            $this->queryBuilder->expr()->andX(...$additionalWhereClauses),
+                            $this->queryBuilder->expr()->isNull($tableAlias . '.uid')
+                        )
+                    );
+                } else {
+                    $this->queryBuilder->andWhere(...$additionalWhereClauses);
+                }
             }
         }
     }
@@ -250,33 +267,17 @@ class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface
      *
      * @param Qom\ComparisonInterface $comparison The comparison to parse
      * @param Qom\SourceInterface $source The source
-     * @param array &$sql SQL query parts to add to
      * @throws \RuntimeException
      * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\RepositoryException
-     * @return void
+     * @return string
      */
-    protected function parseComparison(Qom\ComparisonInterface $comparison, Qom\SourceInterface $source, array &$sql)
+    protected function parseComparison(Qom\ComparisonInterface $comparison, Qom\SourceInterface $source)
     {
-        $operator = $comparison->getOperator();
-        $operand2 = $comparison->getOperand2();
-        if ($operator === QueryInterface::OPERATOR_IN) {
-            $hasValue = false;
-            foreach ($operand2 as $value) {
-                if ($this->dataMapper->getPlainValue($value) !== null) {
-                    $hasValue = true;
-                    break;
-                }
-            }
-            if ($hasValue === false) {
-                $sql['where'][] = '1<>1';
-            } else {
-                $this->parseDynamicOperand($comparison, $source, $sql);
-            }
-        } elseif ($operator === QueryInterface::OPERATOR_CONTAINS) {
-            if ($operand2 === null) {
-                $sql['where'][] = '1<>1';
+        if ($comparison->getOperator() === QueryInterface::OPERATOR_CONTAINS) {
+            if ($comparison->getOperand2() === null) {
+                return '1<>1';
             } else {
-                $value = $this->dataMapper->getPlainValue($operand2);
+                $value = $this->dataMapper->getPlainValue($comparison->getOperand2());
                 if (!$source instanceof Qom\SelectorInterface) {
                     throw new \RuntimeException('Source is not of type "SelectorInterface"', 1395362539);
                 }
@@ -286,7 +287,7 @@ class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface
                 $propertyName = $operand1->getPropertyName();
                 $fullPropertyPath = '';
                 while (strpos($propertyName, '.') !== false) {
-                    $this->addUnionStatement($className, $tableName, $propertyName, $sql, $fullPropertyPath);
+                    $this->addUnionStatement($className, $tableName, $propertyName, $fullPropertyPath);
                 }
                 $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
                 $dataMap = $this->dataMapper->getDataMap($className);
@@ -294,22 +295,62 @@ class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface
                 $typeOfRelation = $columnMap instanceof ColumnMap ? $columnMap->getTypeOfRelation() : null;
                 if ($typeOfRelation === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
                     $relationTableName = $columnMap->getRelationTableName();
-                    $additionalWhereForMatchFields = $this->getAdditionalMatchFieldsStatement($columnMap, $relationTableName, $relationTableName);
-                    $sql['where'][] = $tableName . '.uid IN (SELECT ' . $columnMap->getParentKeyFieldName() . ' FROM ' . $relationTableName . ' WHERE ' . $columnMap->getChildKeyFieldName() . '=' . $this->databaseHandle->fullQuoteStr($value, $relationTableName) . $additionalWhereForMatchFields . ')';
+                    $queryBuilderForSubselect = $this->queryBuilder->getConnection()->createQueryBuilder();
+                    $queryBuilderForSubselect
+                        ->select($columnMap->getParentKeyFieldName())
+                        ->from($relationTableName)
+                        ->where(
+                            $queryBuilderForSubselect->expr()->eq(
+                                $columnMap->getChildKeyFieldName(),
+                                $this->queryBuilder->createNamedParameter($value)
+                            )
+                        );
+                    $additionalWhereForMatchFields = $this->getAdditionalMatchFieldsStatement($queryBuilderForSubselect->expr(), $columnMap, $relationTableName, $relationTableName);
+                    if ($additionalWhereForMatchFields) {
+                        $queryBuilderForSubselect->andWhere($additionalWhereForMatchFields);
+                    }
+
+                    $this->queryBuilder->andWhere(
+                        $this->queryBuilder->expr()->comparison(
+                            $this->queryBuilder->quoteIdentifier($tableName . '.uid'),
+                            'IN',
+                            '(' . $queryBuilderForSubselect->getSQL() . ')'
+                        )
+                    );
                 } elseif ($typeOfRelation === ColumnMap::RELATION_HAS_MANY) {
                     $parentKeyFieldName = $columnMap->getParentKeyFieldName();
                     if (isset($parentKeyFieldName)) {
                         $childTableName = $columnMap->getChildTableName();
-                        $sql['where'][] = $tableName . '.uid=(SELECT ' . $childTableName . '.' . $parentKeyFieldName . ' FROM ' . $childTableName . ' WHERE ' . $childTableName . '.uid=' . $this->databaseHandle->fullQuoteStr($value, $childTableName) . ')';
+
+                        // Build the SQL statement of the subselect
+                        $queryBuilderForSubselect = $this->queryBuilder->getConnection()->createQueryBuilder();
+                        $queryBuilderForSubselect
+                            ->select($parentKeyFieldName)
+                            ->from($childTableName)
+                            ->where(
+                                $queryBuilderForSubselect->expr()->eq(
+                                    'uid',
+                                    (int)$value
+                                )
+                            );
+
+                        // Add it to the main query
+                        return $this->queryBuilder->expr()->eq(
+                            $tableName . '.uid',
+                            $queryBuilderForSubselect->getSQL()
+                        );
                     } else {
-                        $sql['where'][] = 'FIND_IN_SET(' . $this->databaseHandle->fullQuoteStr($value, $tableName) . ', ' . $tableName . '.' . $columnName . ')';
+                        return $this->queryBuilder->expr()->inSet(
+                            $tableName . '.' . $columnName,
+                            $this->queryBuilder->createNamedParameter($value)
+                        );
                     }
                 } else {
                     throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\RepositoryException('Unsupported or non-existing property name "' . $propertyName . '" used in relation matching.', 1327065745);
                 }
             }
         } else {
-            $this->parseDynamicOperand($comparison, $source, $sql);
+            return $this->parseDynamicOperand($comparison, $source);
         }
     }
 
@@ -318,66 +359,108 @@ class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface
      *
      * @param Qom\ComparisonInterface $comparison
      * @param Qom\SourceInterface $source The source
-     * @param array &$sql The query parts
-     * @return void
+     * @return string
+     * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception
      */
-    protected function parseDynamicOperand(Qom\ComparisonInterface $comparison, Qom\SourceInterface $source, array &$sql)
+    protected function parseDynamicOperand(Qom\ComparisonInterface $comparison, Qom\SourceInterface $source)
     {
-        // workaround to find a suitable tablename
-        $tableName = reset($sql['tables']) ?: 'foo';
-        $operator = $this->resolveOperator($comparison->getOperator());
-        $operand = $comparison->getOperand1();
-        $operand2 = $comparison->getOperand2();
-
-        $constraintSQL = $this->parseOperand($operand, $source, $sql) . ' ' . $operator . ' ';
-        if ($comparison->getOperator() === QueryInterface::OPERATOR_IN) {
-            $constraintSQL .= '(';
-            $values = [];
-            foreach ($operand2 as $value) {
-                $values[] = $this->databaseHandle->fullQuoteStr($this->dataMapper->getPlainValue($value), $tableName);
-            }
-            $constraintSQL .= implode(',', $values);
-            $constraintSQL .= ')';
-        } else {
-            if ($operand2 === null) {
-                $constraintSQL .= $this->dataMapper->getPlainValue($operand2);
-            } else {
-                $constraintSQL .= $this->databaseHandle->fullQuoteStr($this->dataMapper->getPlainValue($operand2), $tableName);
-            }
+        $value = $comparison->getOperand2();
+        $fieldName = $this->parseOperand($comparison->getOperand1(), $source);
+        $expr = null;
+        $exprBuilder = $this->queryBuilder->expr();
+        switch ($comparison->getOperator()) {
+            case QueryInterface::OPERATOR_IN:
+                $hasValue = false;
+                $plainValues = [];
+                foreach ($value as $singleValue) {
+                    $plainValue = $this->dataMapper->getPlainValue($singleValue);
+                    if ($plainValue !== null) {
+                        $hasValue = true;
+                        $plainValues[] = $plainValue;
+                    }
+                }
+                if ($hasValue) {
+                    $expr = $exprBuilder->comparison($fieldName, 'IN', '(' . implode(', ', $plainValues) . ')');
+                } else {
+                    $expr = '1<>1';
+                }
+                break;
+            case QueryInterface::OPERATOR_EQUAL_TO:
+                if ($value === null) {
+                    $expr = $fieldName . ' IS NULL';
+                } else {
+                    $value = $this->queryBuilder->createNamedParameter($this->dataMapper->getPlainValue($value));
+                    $expr = $exprBuilder->comparison($fieldName, $exprBuilder::EQ, $value);
+                }
+                break;
+            case QueryInterface::OPERATOR_EQUAL_TO_NULL:
+                $expr = $fieldName . ' IS NULL';
+                break;
+            case QueryInterface::OPERATOR_NOT_EQUAL_TO:
+                if ($value === null) {
+                    $expr = $fieldName . ' IS NOT NULL';
+                } else {
+                    $value = $this->queryBuilder->createNamedParameter($this->dataMapper->getPlainValue($value));
+                    $expr = $exprBuilder->comparison($fieldName, $exprBuilder::NEQ, $value);
+                }
+                break;
+            case QueryInterface::OPERATOR_NOT_EQUAL_TO_NULL:
+                $expr = $fieldName . ' IS NOT NULL';
+                break;
+            case QueryInterface::OPERATOR_LESS_THAN:
+                $value = $this->queryBuilder->createNamedParameter($this->dataMapper->getPlainValue($value), \PDO::PARAM_INT);
+                $expr = $exprBuilder->comparison($fieldName, $exprBuilder::LT, $value);
+                break;
+            case QueryInterface::OPERATOR_LESS_THAN_OR_EQUAL_TO:
+                $value = $this->queryBuilder->createNamedParameter($this->dataMapper->getPlainValue($value), \PDO::PARAM_INT);
+                $expr = $exprBuilder->comparison($fieldName, $exprBuilder::LTE, $value);
+                break;
+            case QueryInterface::OPERATOR_GREATER_THAN:
+                $value = $this->queryBuilder->createNamedParameter($this->dataMapper->getPlainValue($value), \PDO::PARAM_INT);
+                $expr = $exprBuilder->comparison($fieldName, $exprBuilder::GT, $value);
+                break;
+            case QueryInterface::OPERATOR_GREATER_THAN_OR_EQUAL_TO:
+                $value = $this->queryBuilder->createNamedParameter($this->dataMapper->getPlainValue($value), \PDO::PARAM_INT);
+                $expr = $exprBuilder->comparison($fieldName, $exprBuilder::GTE, $value);
+                break;
+            case QueryInterface::OPERATOR_LIKE:
+                $value = $this->queryBuilder->createNamedParameter($this->dataMapper->getPlainValue($value));
+                $expr = $exprBuilder->comparison($fieldName, 'LIKE', $value);
+                break;
+            default:
+                throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception('Unsupported operator encountered.', 1242816073);
         }
-
-        $sql['where'][] = $constraintSQL;
+        return $expr;
     }
 
     /**
      * @param Qom\DynamicOperandInterface $operand
      * @param Qom\SourceInterface $source The source
-     * @param array &$sql The query parts
      * @return string
      * @throws \InvalidArgumentException
      */
-    protected function parseOperand(Qom\DynamicOperandInterface $operand, Qom\SourceInterface $source, array &$sql)
+    protected function parseOperand(Qom\DynamicOperandInterface $operand, Qom\SourceInterface $source)
     {
         if ($operand instanceof Qom\LowerCaseInterface) {
-            $constraintSQL = 'LOWER(' . $this->parseOperand($operand->getOperand(), $source, $sql) . ')';
+            $constraintSQL = 'LOWER(' . $this->parseOperand($operand->getOperand(), $source) . ')';
         } elseif ($operand instanceof Qom\UpperCaseInterface) {
-            $constraintSQL = 'UPPER(' . $this->parseOperand($operand->getOperand(), $source, $sql) . ')';
+            $constraintSQL = 'UPPER(' . $this->parseOperand($operand->getOperand(), $source) . ')';
         } elseif ($operand instanceof Qom\PropertyValueInterface) {
             $propertyName = $operand->getPropertyName();
             $className = '';
             if ($source instanceof Qom\SelectorInterface) {
-                // @todo Only necessary to differ from  Join
                 $className = $source->getNodeTypeName();
                 $tableName = $this->dataMapper->convertClassNameToTableName($className);
                 $fullPropertyPath = '';
                 while (strpos($propertyName, '.') !== false) {
-                    $this->addUnionStatement($className, $tableName, $propertyName, $sql, $fullPropertyPath);
+                    $this->addUnionStatement($className, $tableName, $propertyName, $fullPropertyPath);
                 }
             } elseif ($source instanceof Qom\JoinInterface) {
                 $tableName = $source->getJoinCondition()->getSelector1Name();
             }
             $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
             $constraintSQL = (!empty($tableName) ? $tableName . '.' : '') . $columnName;
+            $constraintSQL = $this->queryBuilder->getConnection()->quoteIdentifier($constraintSQL);
         } else {
             throw new \InvalidArgumentException('Given operand has invalid type "' . get_class($operand) . '".', 1395710211);
         }
@@ -388,10 +471,9 @@ class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface
      * Add a constraint to ensure that the record type of the returned tuples is matching the data type of the repository.
      *
      * @param string $className The class name
-     * @param array &$sql The query parts
      * @return void
      */
-    protected function addRecordTypeConstraint($className, &$sql)
+    protected function addRecordTypeConstraint($className)
     {
         if ($className !== null) {
             $dataMap = $this->dataMapper->getDataMap($className);
@@ -410,9 +492,14 @@ class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface
                     $recordTypeStatements = [];
                     foreach ($recordTypes as $recordType) {
                         $tableName = $dataMap->getTableName();
-                        $recordTypeStatements[] = $tableName . '.' . $dataMap->getRecordTypeColumnName() . '=' . $this->databaseHandle->fullQuoteStr($recordType, $tableName);
+                        $recordTypeStatements[] = $this->queryBuilder->expr()->eq(
+                            $tableName . '.' . $dataMap->getRecordTypeColumnName(),
+                            $this->queryBuilder->createNamedParameter($recordType)
+                        );
                     }
-                    $sql['additionalWhereClause'][] = '(' . implode(' OR ', $recordTypeStatements) . ')';
+                    $this->queryBuilder->andWhere(
+                        $this->queryBuilder->expr()->orX(...$recordTypeStatements)
+                    );
                 }
             }
         }
@@ -422,33 +509,34 @@ class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface
      * Builds a condition for filtering records by the configured match field,
      * e.g. MM_match_fields, foreign_match_fields or foreign_table_field.
      *
+     * @param ExpressionBuilder $exprBuilder
      * @param ColumnMap $columnMap The column man for which the condition should be build.
-     * @param string $childTableName The real name of the child record table.
      * @param string $childTableAlias The alias of the child record table used in the query.
      * @param string $parentTable The real name of the parent table (used for building the foreign_table_field condition).
      * @return string The match field conditions or an empty string.
      */
-    protected function getAdditionalMatchFieldsStatement($columnMap, $childTableName, $childTableAlias, $parentTable = null)
+    protected function getAdditionalMatchFieldsStatement($exprBuilder, $columnMap, $childTableAlias, $parentTable = null)
     {
-        $additionalWhereForMatchFields = '';
-
+        $additionalWhereForMatchFields = [];
         $relationTableMatchFields = $columnMap->getRelationTableMatchFields();
         if (is_array($relationTableMatchFields) && !empty($relationTableMatchFields)) {
-            $additionalWhere = [];
             foreach ($relationTableMatchFields as $fieldName => $value) {
-                $additionalWhere[] = $childTableAlias . '.' . $fieldName . ' = ' . $this->databaseHandle->fullQuoteStr($value, $childTableName);
+                $additionalWhereForMatchFields[] = $exprBuilder->eq($childTableAlias . '.' . $fieldName, $this->queryBuilder->createNamedParameter($value));
             }
-            $additionalWhereForMatchFields .= ' AND ' . implode(' AND ', $additionalWhere);
         }
 
         if (isset($parentTable)) {
             $parentTableFieldName = $columnMap->getParentTableFieldName();
-            if (isset($parentTableFieldName) && $parentTableFieldName !== '') {
-                $additionalWhereForMatchFields .= ' AND ' . $childTableAlias . '.' . $parentTableFieldName . ' = ' . $this->databaseHandle->fullQuoteStr($parentTable, $childTableAlias);
+            if (!empty($parentTableFieldName)) {
+                $additionalWhereForMatchFields[] = $exprBuilder->eq($childTableAlias . '.' . $parentTableFieldName, $this->queryBuilder->createNamedParameter($parentTable));
             }
         }
 
-        return $additionalWhereForMatchFields;
+        if (!empty($additionalWhereForMatchFields)) {
+            return $exprBuilder->andX(...$additionalWhereForMatchFields);
+        } else {
+            return '';
+        }
     }
 
     /**
@@ -457,26 +545,17 @@ class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface
      * @param QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
      * @param string $tableName The table name to add the additional where clause for
      * @param string $tableAlias The table alias used in the query.
-     * @return string
+     * @return array
      */
     protected function getAdditionalWhereClause(QuerySettingsInterface $querySettings, $tableName, $tableAlias = null)
     {
-        $sysLanguageStatement = '';
+        $whereClause = [];
         if ($querySettings->getRespectSysLanguage()) {
-            $sysLanguageStatement = $this->getSysLanguageStatement($tableName, $tableAlias, $querySettings);
+            $whereClause[] = $this->getSysLanguageStatement($tableName, $tableAlias, $querySettings);
         }
 
-        $pageIdStatement = '';
         if ($querySettings->getRespectStoragePage()) {
-            $pageIdStatement = $this->getPageIdStatement($tableName, $tableAlias, $querySettings->getStoragePageIds());
-        }
-
-        if ($sysLanguageStatement !== '' && $pageIdStatement !== '') {
-            $whereClause = $sysLanguageStatement . ' AND ' . $pageIdStatement;
-        } elseif ($sysLanguageStatement !== '') {
-            $whereClause = $sysLanguageStatement;
-        } else {
-            $whereClause = $pageIdStatement;
+            $whereClause[] = $this->getPageIdStatement($tableName, $tableAlias, $querySettings->getStoragePageIds());
         }
 
         return $whereClause;
@@ -569,44 +648,76 @@ class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface
      */
     protected function getSysLanguageStatement($tableName, $tableAlias, $querySettings)
     {
-        $sysLanguageStatement = '';
         if (is_array($GLOBALS['TCA'][$tableName]['ctrl'])) {
             if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])) {
                 // Select all entries for the current language
-                $additionalWhereClause = $tableAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . ' IN (' . (int)$querySettings->getLanguageUid() . ',-1)';
                 // If any language is set -> get those entries which are not translated yet
                 // They will be removed by \TYPO3\CMS\Frontend\Page\PageRepository::getRecordOverlay if not matching overlay mode
+                $languageField = $GLOBALS['TCA'][$tableName]['ctrl']['languageField'];
+
                 if (isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
                     && $querySettings->getLanguageUid() > 0
                 ) {
                     $mode = $querySettings->getLanguageMode();
+
                     if ($mode === 'strict') {
-                        $additionalWhereClause = $tableAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '=-1' .
-                            ' OR (' . $tableAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . ' = ' . (int)$querySettings->getLanguageUid() .
-                            ' AND ' . $tableAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] . '=0' .
-                            ') OR (' . $tableAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '=0' .
-                            ' AND ' . $tableAlias . '.uid IN (SELECT ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] .
-                            ' FROM ' . $tableName .
-                            ' WHERE ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] . '>0' .
-                            ' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '=' . (int)$querySettings->getLanguageUid();
+                        $queryBuilderForSubselect = $this->queryBuilder->getConnection()->createQueryBuilder();
+                        $queryBuilderForSubselect
+                            ->select($tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
+                            ->from($tableName)
+                            ->where(
+                                $queryBuilderForSubselect->expr()->andX(
+                                    $queryBuilderForSubselect->expr()->gt($tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'], 0),
+                                    $queryBuilderForSubselect->expr()->eq($tableName . '.' . $languageField, (int)$querySettings->getLanguageUid())
+                                )
+                            );
+                        return $this->queryBuilder->expr()->orX(
+                            $this->queryBuilder->expr()->eq($tableAlias . '.' . $languageField, -1),
+                            $this->queryBuilder->expr()->andX(
+                                $this->queryBuilder->expr()->eq($tableAlias . '.' . $languageField, (int)$querySettings->getLanguageUid()),
+                                $this->queryBuilder->expr()->eq($tableAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'], 0)
+                            ),
+                            $this->queryBuilder->expr()->andX(
+                                $this->queryBuilder->expr()->eq($tableAlias . '.' . $languageField, 0),
+                                $this->queryBuilder->expr()->in(
+                                    $tableAlias . '.uid',
+                                    $queryBuilderForSubselect->getSQL()
+
+                                )
+                            )
+                        );
                     } else {
-                        $additionalWhereClause .= ' OR (' . $tableAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '=0' .
-                            ' AND ' . $tableAlias . '.uid NOT IN (SELECT ' . $tableAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] .
-                            ' FROM ' . $tableName .
-                            ' WHERE ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] . '>0' .
-                            ' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['languageField'] . '=' . (int)$querySettings->getLanguageUid();
-                    }
-
-                    // Add delete clause to ensure all entries are loaded
-                    if (isset($GLOBALS['TCA'][$tableName]['ctrl']['delete'])) {
-                        $additionalWhereClause .= ' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['delete'] . '=0';
+                        $queryBuilderForSubselect = $this->queryBuilder->getConnection()->createQueryBuilder();
+                        $queryBuilderForSubselect
+                            ->select($tableAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
+                            ->from($tableName)
+                            ->where(
+                                $queryBuilderForSubselect->expr()->andX(
+                                    $queryBuilderForSubselect->expr()->gt($tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'], 0),
+                                    $queryBuilderForSubselect->expr()->eq($tableName . '.' . $languageField, (int)$querySettings->getLanguageUid())
+                                )
+                            );
+                        return $this->queryBuilder->expr()->orX(
+                            $this->queryBuilder->expr()->in($tableAlias . '.' . $languageField, [(int)$querySettings->getLanguageUid(), -1]),
+                            $this->queryBuilder->expr()->andX(
+                                $this->queryBuilder->expr()->eq($tableAlias . '.' . $languageField, 0),
+                                $this->queryBuilder->expr()->notIn(
+                                    $tableAlias . '.uid',
+                                    $queryBuilderForSubselect->getSQL()
+
+                                )
+                            )
+                        );
                     }
-                    $additionalWhereClause .= '))';
+                } else {
+                    return $this->queryBuilder->expr()->in(
+                        $tableAlias . '.' . $languageField,
+                        [(int)$querySettings->getLanguageUid(), -1]
+                    );
                 }
-                $sysLanguageStatement = '(' . $additionalWhereClause . ')';
             }
         }
-        return $sysLanguageStatement;
+        return '';
     }
 
     /**
@@ -628,13 +739,15 @@ class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface
         switch ($rootLevel) {
             // Only in pid 0
             case 1:
-                return $tableAlias . '.pid = 0';
+                $storagePageIds = [0];
+                break;
             // Pid 0 and pagetree
             case -1:
                 if (empty($storagePageIds)) {
-                    return $tableAlias . '.pid = 0';
+                    $storagePageIds = [0];
+                } else {
+                    $storagePageIds[] = 0;
                 }
-                $storagePageIds[] = 0;
                 break;
             // Only pagetree or not set
             case 0:
@@ -647,23 +760,25 @@ class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface
                 return '';
         }
         $storagePageIds = array_map('intval', $storagePageIds);
-
-        return $tableAlias . '.pid IN (' . implode(',', $storagePageIds) . ')';
+        if (count($storagePageIds) === 1) {
+            return $this->queryBuilder->expr()->eq($tableAlias . '.pid', reset($storagePageIds));
+        } else {
+            return $this->queryBuilder->expr()->in($tableAlias . '.pid', $storagePageIds);
+        }
     }
 
     /**
      * Transforms a Join into SQL and parameter arrays
      *
      * @param Qom\JoinInterface $join The join
-     * @param array &$sql The query parts
+     * @param string $leftTableAlias The alias from the table to main
      * @return void
      */
-    protected function parseJoin(Qom\JoinInterface $join, array &$sql)
+    protected function parseJoin(Qom\JoinInterface $join, $leftTableAlias)
     {
         $leftSource = $join->getLeft();
         $leftClassName = $leftSource->getNodeTypeName();
-        $leftTableName = $leftSource->getSelectorName();
-        $this->addRecordTypeConstraint($leftClassName, $sql);
+        $this->addRecordTypeConstraint($leftClassName);
         $rightSource = $join->getRight();
         if ($rightSource instanceof Qom\JoinInterface) {
             $left = $rightSource->getLeft();
@@ -672,21 +787,25 @@ class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface
         } else {
             $rightClassName = $rightSource->getNodeTypeName();
             $rightTableName = $rightSource->getSelectorName();
-            $sql['fields'][$leftTableName] = $rightTableName . '.*';
+            $this->queryBuilder->addSelect($rightTableName . '.*');
         }
-        $this->addRecordTypeConstraint($rightClassName, $sql);
-        $leftTableName = $this->getUniqueAlias($sql, $leftTableName);
-        $sql['tables'][$leftTableName] = $leftTableName;
-        $rightTableName = $this->getUniqueAlias($sql, $rightTableName);
-        $sql['unions'][$rightTableName] = 'LEFT JOIN ' . $rightTableName;
+        $this->addRecordTypeConstraint($rightClassName);
+        $rightTableAlias = $this->getUniqueAlias($rightTableName);
         $joinCondition = $join->getJoinCondition();
+        $joinConditionExpression = null;
+        $this->unionTableAliasCache[] = $rightTableAlias;
         if ($joinCondition instanceof Qom\EquiJoinCondition) {
             $column1Name = $this->dataMapper->convertPropertyNameToColumnName($joinCondition->getProperty1Name(), $leftClassName);
             $column2Name = $this->dataMapper->convertPropertyNameToColumnName($joinCondition->getProperty2Name(), $rightClassName);
-            $sql['unions'][$rightTableName] .= ' ON ' . $leftTableName . '.' . $column1Name . ' = ' . $rightTableName . '.' . $column2Name;
+
+            $joinConditionExpression =  $this->queryBuilder->expr()->eq(
+                $leftTableAlias . '.' . $column1Name,
+                $rightTableAlias . '.' . $column2Name
+            );
         }
+        $this->queryBuilder->leftJoin($leftTableAlias, $rightTableName, $rightTableAlias, $joinConditionExpression);
         if ($rightSource instanceof Qom\JoinInterface) {
-            $this->parseJoin($rightSource, $sql);
+            $this->parseJoin($rightSource, $rightTableAlias);
         }
     }
 
@@ -694,12 +813,11 @@ class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface
      * Generates a unique alias for the given table and the given property path.
      * The property path will be mapped to the generated alias in the tablePropertyMap.
      *
-     * @param array $sql The SQL satement parts, will be filled with the tableAliasMap.
      * @param string $tableName The name of the table for which the alias should be generated.
      * @param string $fullPropertyPath The full property path that is related to the given table.
      * @return string The generated table alias.
      */
-    protected function getUniqueAlias(array &$sql, $tableName, $fullPropertyPath = null)
+    protected function getUniqueAlias($tableName, $fullPropertyPath = null)
     {
         if (isset($fullPropertyPath) && isset($this->tablePropertyMap[$fullPropertyPath])) {
             return $this->tablePropertyMap[$fullPropertyPath];
@@ -707,12 +825,12 @@ class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface
 
         $alias = $tableName;
         $i = 0;
-        while (isset($sql['tableAliasMap'][$alias])) {
+        while (isset($this->tableAliasMap[$alias])) {
             $alias = $tableName . $i;
             $i++;
         }
 
-        $sql['tableAliasMap'][$alias] = $tableName;
+        $this->tableAliasMap[$alias] = $tableName;
 
         if (isset($fullPropertyPath)) {
             $this->tablePropertyMap[$fullPropertyPath] = $alias;
@@ -728,13 +846,12 @@ class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface
      * @param string &$className The name of the parent class, will be set to the child class after processing.
      * @param string &$tableName The name of the parent table, will be set to the table alias that is used in the union statement.
      * @param array &$propertyPath The remaining property path, will be cut of by one part during the process.
-     * @param array &$sql The SQL statement parts, will be filled with the union statements.
      * @param string $fullPropertyPath The full path the the current property, will be used to make table names unique.
      * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception
-     * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InvalidRelationConfigurationException
-     * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\MissingColumnMapException
+     * @throws InvalidRelationConfigurationException
+     * @throws MissingColumnMapException
      */
-    protected function addUnionStatement(&$className, &$tableName, &$propertyPath, array &$sql, &$fullPropertyPath)
+    protected function addUnionStatement(&$className, &$tableName, &$propertyPath, &$fullPropertyPath)
     {
         $explodedPropertyPath = explode('.', $propertyPath, 2);
         $propertyName = $explodedPropertyPath[0];
@@ -744,53 +861,82 @@ class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface
         $columnMap = $this->dataMapper->getDataMap($className)->getColumnMap($propertyName);
 
         if ($columnMap === null) {
-            throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\MissingColumnMapException('The ColumnMap for property "' . $propertyName . '" of class "' . $className . '" is missing.', 1355142232);
+            throw new MissingColumnMapException('The ColumnMap for property "' . $propertyName . '" of class "' . $className . '" is missing.', 1355142232);
         }
 
         $parentKeyFieldName = $columnMap->getParentKeyFieldName();
         $childTableName = $columnMap->getChildTableName();
 
         if ($childTableName === null) {
-            throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InvalidRelationConfigurationException('The relation information for property "' . $propertyName . '" of class "' . $className . '" is missing.', 1353170925);
+            throw new InvalidRelationConfigurationException('The relation information for property "' . $propertyName . '" of class "' . $className . '" is missing.', 1353170925);
         }
 
         $fullPropertyPath .= ($fullPropertyPath === '') ? $propertyName : '.' . $propertyName;
-        $childTableAlias = $this->getUniqueAlias($sql, $childTableName, $fullPropertyPath);
+        $childTableAlias = $this->getUniqueAlias($childTableName, $fullPropertyPath);
 
         // If there is already exists a union with the current identifier we do not need to build it again and exit early.
-        if (isset($sql['unions'][$childTableAlias])) {
-            $propertyPath = $explodedPropertyPath[1];
-            $tableName = $childTableAlias;
-            $className = $this->dataMapper->getType($className, $propertyName);
+        if (in_array($childTableAlias, $this->unionTableAliasCache, true)) {
             return;
         }
 
         if ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_ONE) {
             if (isset($parentKeyFieldName)) {
-                $sql['unions'][$childTableAlias] = 'LEFT JOIN ' . $childTableName . ' AS ' . $childTableAlias . ' ON ' . $tableName . '.uid=' . $childTableAlias . '.' . $parentKeyFieldName;
+                // @todo: no test for this part yet
+                $joinConditionExpression = $this->queryBuilder->expr()->eq(
+                    $tableName . '.uid',
+                    $childTableAlias . '.' . $parentKeyFieldName
+                );
             } else {
-                $sql['unions'][$childTableAlias] = 'LEFT JOIN ' . $childTableName . ' AS ' . $childTableAlias . ' ON ' . $tableName . '.' . $columnName . '=' . $childTableAlias . '.uid';
+                $joinConditionExpression = $this->queryBuilder->expr()->eq(
+                    $tableName . '.' . $columnName,
+                    $childTableAlias . '.uid'
+                );
             }
-            $sql['unions'][$childTableAlias] .= $this->getAdditionalMatchFieldsStatement($columnMap, $childTableName, $childTableAlias, $realTableName);
+            $this->queryBuilder->leftJoin($tableName, $childTableName, $childTableAlias, $joinConditionExpression);
+            $this->unionTableAliasCache[] = $childTableAlias;
+            $this->queryBuilder->andWhere(
+                $this->getAdditionalMatchFieldsStatement($this->queryBuilder->expr(), $columnMap, $childTableAlias, $realTableName)
+            );
         } elseif ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_MANY) {
+            // @todo: no tests for this part yet
             if (isset($parentKeyFieldName)) {
-                $sql['unions'][$childTableAlias] = 'LEFT JOIN ' . $childTableName . ' AS ' . $childTableAlias . ' ON ' . $tableName . '.uid=' . $childTableAlias . '.' . $parentKeyFieldName;
+                $joinConditionExpression = $this->queryBuilder->expr()->eq(
+                    $tableName . '.uid',
+                    $childTableAlias . '.' . $parentKeyFieldName
+                );
             } else {
-                $onStatement = '(FIND_IN_SET(' . $childTableAlias . '.uid, ' . $tableName . '.' . $columnName . '))';
-                $sql['unions'][$childTableAlias] = 'LEFT JOIN ' . $childTableName . ' AS ' . $childTableAlias . ' ON ' . $onStatement;
+                $joinConditionExpression = $this->queryBuilder->expr()->inSet(
+                    $tableName . '.' . $columnName,
+                    $childTableAlias . '.uid'
+                );
             }
-            $sql['unions'][$childTableAlias] .= $this->getAdditionalMatchFieldsStatement($columnMap, $childTableName, $childTableAlias, $realTableName);
+            $this->queryBuilder->leftJoin($tableName, $childTableName, $childTableAlias, $joinConditionExpression);
+            $this->unionTableAliasCache[] = $childTableAlias;
+            $this->queryBuilder->andWhere(
+                $this->getAdditionalMatchFieldsStatement($this->queryBuilder->expr(), $columnMap, $childTableAlias, $realTableName)
+            );
         } elseif ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
             $relationTableName = $columnMap->getRelationTableName();
-            $relationTableAlias = $relationTableAlias = $this->getUniqueAlias($sql, $relationTableName, $fullPropertyPath . '_mm');
-            $sql['unions'][$relationTableAlias] = 'LEFT JOIN ' . $relationTableName . ' AS ' . $relationTableAlias . ' ON ' . $tableName . '.uid=' . $relationTableAlias . '.' . $columnMap->getParentKeyFieldName();
-            $sql['unions'][$childTableAlias] = 'LEFT JOIN ' . $childTableName . ' AS ' . $childTableAlias . ' ON ' . $relationTableAlias . '.' . $columnMap->getChildKeyFieldName() . '=' . $childTableAlias . '.uid';
-            $sql['unions'][$childTableAlias] .= $this->getAdditionalMatchFieldsStatement($columnMap, $relationTableName, $relationTableAlias, $realTableName);
+            $relationTableAlias = $relationTableAlias = $this->getUniqueAlias($relationTableName, $fullPropertyPath . '_mm');
+
+            $joinConditionExpression = $this->queryBuilder->expr()->eq(
+                $tableName . '.uid',
+                $relationTableAlias . '.' . $columnMap->getParentKeyFieldName()
+            );
+            $this->queryBuilder->leftJoin($tableName, $relationTableName, $relationTableAlias, $joinConditionExpression);
+            $joinConditionExpression = $this->queryBuilder->expr()->eq(
+                $relationTableAlias . '.' . $columnMap->getChildKeyFieldName(),
+                $childTableAlias . '.uid'
+            );
+            $this->queryBuilder->leftJoin($relationTableAlias, $childTableName, $childTableAlias, $joinConditionExpression);
+            $this->queryBuilder->andWhere(
+                $this->getAdditionalMatchFieldsStatement($this->queryBuilder->expr(), $columnMap, $relationTableAlias, $realTableName)
+            );
+            $this->unionTableAliasCache[] = $childTableAlias;
+            $this->queryBuilder->addGroupBy($this->tableName . '.uid');
         } else {
             throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception('Could not determine type of relation.', 1252502725);
         }
-        // @todo check if there is another solution for this
-        $sql['keywords']['distinct'] = 'DISTINCT';
         $propertyPath = $explodedPropertyPath[1];
         $tableName = $childTableAlias;
         $className = $this->dataMapper->getType($className, $propertyName);
@@ -810,10 +956,10 @@ class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface
         if ($tableAlias !== $tableName) {
             $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($tableName);
             $quotedTableName = $connection->quoteIdentifier($tableName);
-            $quotedTableAliase = $connection->quoteIdentifier($tableAlias);
+            $quotedTableAlias = $connection->quoteIdentifier($tableAlias);
             $statement = str_replace(
                 [$tableName . '.', $quotedTableName . '.'],
-                [$tableAlias . '.', $quotedTableAliase . '.'],
+                [$tableAlias . '.', $quotedTableAlias . '.'],
                 $statement
             );
         }
@@ -822,61 +968,15 @@ class Typo3DbQueryParser implements \TYPO3\CMS\Core\SingletonInterface
     }
 
     /**
-     * Returns the SQL operator for the given JCR operator type.
-     *
-     * @param string $operator One of the JCR_OPERATOR_* constants
-     * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception
-     * @return string an SQL operator
-     */
-    protected function resolveOperator($operator)
-    {
-        switch ($operator) {
-            case QueryInterface::OPERATOR_IN:
-                $operator = 'IN';
-                break;
-            case QueryInterface::OPERATOR_EQUAL_TO:
-                $operator = '=';
-                break;
-            case QueryInterface::OPERATOR_EQUAL_TO_NULL:
-                $operator = 'IS';
-                break;
-            case QueryInterface::OPERATOR_NOT_EQUAL_TO:
-                $operator = '!=';
-                break;
-            case QueryInterface::OPERATOR_NOT_EQUAL_TO_NULL:
-                $operator = 'IS NOT';
-                break;
-            case QueryInterface::OPERATOR_LESS_THAN:
-                $operator = '<';
-                break;
-            case QueryInterface::OPERATOR_LESS_THAN_OR_EQUAL_TO:
-                $operator = '<=';
-                break;
-            case QueryInterface::OPERATOR_GREATER_THAN:
-                $operator = '>';
-                break;
-            case QueryInterface::OPERATOR_GREATER_THAN_OR_EQUAL_TO:
-                $operator = '>=';
-                break;
-            case QueryInterface::OPERATOR_LIKE:
-                $operator = 'LIKE';
-                break;
-            default:
-                throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception('Unsupported operator encountered.', 1242816073);
-        }
-        return $operator;
-    }
-
-    /**
-     * @return \TYPO3\CMS\Frontend\Page\PageRepository
+     * @return PageRepository
      */
     protected function getPageRepository()
     {
-        if (!$this->pageRepository instanceof \TYPO3\CMS\Frontend\Page\PageRepository) {
+        if (!$this->pageRepository instanceof PageRepository) {
             if ($this->environmentService->isEnvironmentInFrontendMode() && is_object($GLOBALS['TSFE'])) {
                 $this->pageRepository = $GLOBALS['TSFE']->sys_page;
             } else {
-                $this->pageRepository = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\Page\PageRepository::class);
+                $this->pageRepository = GeneralUtility::makeInstance(PageRepository::class);
             }
         }
 
index f2d8bf8..39131fb 100644 (file)
@@ -8,7 +8,7 @@ CREATE TABLE tx_blogexample_domain_model_blog (
        title varchar(255) DEFAULT '' NOT NULL,
        description text NOT NULL,
        logo tinyblob NOT NULL,
-       administrator varchar(255) DEFAULT '',
+       administrator int(11) DEFAULT '0' NOT NULL,
 
        posts varchar(255) DEFAULT '' NOT NULL,
 
index f002eca..bf51895 100644 (file)
@@ -43,6 +43,7 @@
        </tx_blogexample_domain_model_blog>
        <tx_blogexample_domain_model_blog>
                <uid>5</uid>
+               <administrator>3</administrator>
                <pid>0</pid>
                <title>Blog5Deleted</title>
                <description>Blog5 Description</description>
index 78e2997..6a99a75 100644 (file)
@@ -2,7 +2,7 @@
 <dataset>
        <fe_groups>
                <uid>1</uid>
-               <pid>1</pid>
+               <pid>0</pid>
                <title>Group A</title>
                <subgroup></subgroup>
                <TSconfig></TSconfig>
@@ -11,7 +11,7 @@
        </fe_groups>
        <fe_groups>
                <uid>2</uid>
-               <pid>1</pid>
+               <pid>0</pid>
                <title>Group B</title>
                <subgroup></subgroup>
                <TSconfig></TSconfig>
index 4aad90d..4b06741 100644 (file)
                <usergroup>2</usergroup>
                <disable>0</disable>
        </fe_users>
+       <fe_users>
+               <uid>3</uid>
+               <pid>0</pid>
+               <username>administrator</username>
+               <deleted>0</deleted>
+               <usergroup>1,2</usergroup>
+               <disable>0</disable>
+               <tx_extbase_type>Tx_BlogExample_Domain_Model_Administrator</tx_extbase_type>
+       </fe_users>
 </dataset>
index dec81c0..d3fcfe2 100644 (file)
@@ -18,6 +18,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 class QueryParserTest extends \TYPO3\CMS\Core\Tests\FunctionalTestCase
 {
+
     /**
      * @var array
      */
@@ -45,11 +46,16 @@ class QueryParserTest extends \TYPO3\CMS\Core\Tests\FunctionalTestCase
     {
         parent::setUp();
 
+        $this->importDataSet(ORIGINAL_ROOT . 'typo3/sysext/extbase/Tests/Functional/Persistence/Fixtures/categories.xml');
         $this->importDataSet(ORIGINAL_ROOT . 'typo3/sysext/extbase/Tests/Functional/Persistence/Fixtures/tags.xml');
+        $this->importDataSet(ORIGINAL_ROOT . 'typo3/sysext/extbase/Tests/Functional/Persistence/Fixtures/blogs.xml');
         $this->importDataSet(ORIGINAL_ROOT . 'typo3/sysext/extbase/Tests/Functional/Persistence/Fixtures/tags-mm.xml');
         $this->importDataSet(ORIGINAL_ROOT . 'typo3/sysext/extbase/Tests/Functional/Persistence/Fixtures/persons.xml');
         $this->importDataSet(ORIGINAL_ROOT . 'typo3/sysext/extbase/Tests/Functional/Persistence/Fixtures/posts.xml');
         $this->importDataSet(ORIGINAL_ROOT . 'typo3/sysext/extbase/Tests/Functional/Persistence/Fixtures/post-tag-mm.xml');
+        $this->importDataSet(ORIGINAL_ROOT . 'typo3/sysext/extbase/Tests/Functional/Persistence/Fixtures/category-mm.xml');
+        $this->importDataSet(ORIGINAL_ROOT . 'typo3/sysext/extbase/Tests/Functional/Persistence/Fixtures/fe_users.xml');
+        $this->importDataSet(ORIGINAL_ROOT . 'typo3/sysext/extbase/Tests/Functional/Persistence/Fixtures/fe_groups.xml');
 
         $this->objectManager = GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Object\ObjectManager::class);
         $this->blogRepository = $this->objectManager->get(\ExtbaseTeam\BlogExample\Domain\Repository\BlogRepository::class);
@@ -72,11 +78,81 @@ class QueryParserTest extends \TYPO3\CMS\Core\Tests\FunctionalTestCase
                 )
             )
         );
+
         $result = $query->execute()->toArray();
         $this->assertEquals(3, count($result));
     }
 
     /**
+     * Test ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY
+     *
+     * @test
+     */
+    public function queryWithRelationHasAndBelongsToManyReturnsExpectedResult()
+    {
+        /** @var \ExtbaseTeam\BlogExample\Domain\Repository\PostRepository $postRepository */
+        $postRepository = $this->objectManager->get('ExtbaseTeam\\BlogExample\\Domain\\Repository\\PostRepository');
+        $query = $postRepository->createQuery();
+        $query->matching(
+            $query->equals('tags.name', 'Tag12')
+        );
+        $result = $query->execute()->toArray();
+        $this->assertEquals(2, count($result));
+    }
+
+    /**
+     * Test ColumnMap::RELATION_HAS_MANY
+     *
+     * @test
+     */
+    public function queryWithRelationHasManyWithoutParentKeyFieldNameReturnsExpectedResult()
+    {
+        /** @var \TYPO3\CMS\Extbase\Domain\Repository\FrontendUserRepository $frontendUserRepository */
+        $frontendUserRepository = $this->objectManager->get('TYPO3\\CMS\\Extbase\\Domain\\Repository\\FrontendUserRepository');
+        $query = $frontendUserRepository->createQuery();
+
+        $result = $query->matching(
+            $query->equals('usergroup.title', 'Group A')
+        )->execute();
+        $this->assertSame(2, count($result));
+    }
+
+    /**
+     * Test ColumnMap::RELATION_HAS_ONE, ColumnMap::ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY
+     *
+     * @test
+     */
+    public function queryWithRelationHasOneAndHasAndBelongsToManyWithoutParentKeyFieldNameReturnsExpectedResult()
+    {
+        /** @var \ExtbaseTeam\BlogExample\Domain\Repository\PostRepository $postRepository */
+        $postRepository = $this->objectManager->get('ExtbaseTeam\\BlogExample\\Domain\\Repository\\PostRepository');
+        $query = $postRepository->createQuery();
+        $query->matching(
+            $query->equals('author.firstname', 'Author')
+        );
+        $result = $query->execute()->toArray();
+        $this->assertEquals(2, count($result));
+    }
+
+    /**
+     * @test
+     */
+    public function orReturnsExpectedResult()
+    {
+        /** @var \ExtbaseTeam\BlogExample\Domain\Repository\PostRepository $postRepository */
+        $postRepository = $this->objectManager->get('ExtbaseTeam\\BlogExample\\Domain\\Repository\\PostRepository');
+        $query = $postRepository->createQuery();
+        $query->matching(
+            $query->logicalOr(
+                $query->equals('tags.name', 'Tag12'),
+                $query->equals('tags.name', 'Tag11')
+            )
+        );
+        $result = $query->execute()->toArray();
+        $this->assertEquals(2, count($result));
+    }
+
+    /**
      * @test
      */
     public function queryWithMultipleRelationsToIdenticalTablesReturnsExpectedResultForAndQuery()
@@ -94,4 +170,30 @@ class QueryParserTest extends \TYPO3\CMS\Core\Tests\FunctionalTestCase
         $result = $query->execute()->toArray();
         $this->assertEquals(1, count($result));
     }
+
+    /**
+     * @test
+     */
+    public function queryWithFindInSetReturnsExpectedResult()
+    {
+        /** @var \TYPO3\CMS\Extbase\Domain\Repository\FrontendUserRepository $frontendUserRepository */
+        $frontendUserRepository = $this->objectManager->get('TYPO3\\CMS\\Extbase\\Domain\\Repository\\FrontendUserRepository');
+        $query = $frontendUserRepository->createQuery();
+
+        $result = $query->matching(
+                $query->contains('usergroup', 1)
+        )->execute();
+        $this->assertSame(2, count($result));
+    }
+
+    /**
+     * @test
+     */
+    public function queryForPostWithCategoriesReturnsPostWithCategories()
+    {
+        $postRepository = $this->objectManager->get('ExtbaseTeam\\BlogExample\\Domain\\Repository\\PostRepository');
+        $query = $postRepository->createQuery();
+        $post = $query->matching($query->equals('uid', 1))->execute()->current();
+        $this->assertSame(3, count($post->getCategories()));
+    }
 }
index 8ed72a0..b87d04f 100644 (file)
@@ -104,6 +104,23 @@ class TranslationTest extends \TYPO3\CMS\Core\Tests\FunctionalTestCase
     /**
      * @test
      */
+    public function countReturnsCorrectNumberOfPostsInEnglishLanguageForStrictMode()
+    {
+        $query = $this->postRepository->createQuery();
+
+        $querySettings = $query->getQuerySettings();
+        $querySettings->setStoragePageIds([1]);
+        $querySettings->setRespectSysLanguage(true);
+        $querySettings->setLanguageUid(1);
+        $querySettings->setLanguageMode('strict');
+
+        $postCount = $query->execute()->count();
+        $this->assertSame(2, $postCount);
+    }
+
+    /**
+     * @test
+     */
     public function countReturnsCorrectNumberOfPostsInEnglishLanguage()
     {
         $query = $this->postRepository->createQuery();
index 6597a2a..d3cc831 100644 (file)
@@ -21,10 +21,6 @@ use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
 use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
 use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper;
-use TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface;
-use TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbBackend;
-use TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser;
-use TYPO3\CMS\Extbase\Persistence\QueryInterface;
 use TYPO3\CMS\Extbase\Service\EnvironmentService;
 
 /**
@@ -171,29 +167,4 @@ class Typo3DbBackendTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
         $mockTypo3DbBackend->_set('pageRepository', $pageRepositoryMock);
         $this->assertSame([$comparisonRow], $mockTypo3DbBackend->_call('doLanguageAndWorkspaceOverlay', $sourceMock, [$row], $mockQuerySettings, $workspaceUid));
     }
-
-    /**
-     * @test
-     * @return void
-     */
-    public function getObjectCountByQueryThrowsExceptionIfOffsetWithoutLimitIsUsed()
-    {
-        $querySettingsProphecy = $this->prophesize(QuerySettingsInterface::class);
-        $queryInterfaceProphecy = $this->prophesize(QueryInterface::class);
-        $queryParserProphecy = $this->prophesize(Typo3DbQueryParser::class);
-        $queryParserProphecy->parseQuery($queryInterfaceProphecy->reveal())->willReturn(
-            ['tables' => ['tt_content'], 'offset' => 10, 'limit' => null]
-        );
-        $queryInterfaceProphecy->getQuerySettings()->willReturn($querySettingsProphecy->reveal());
-        $queryInterfaceProphecy->getConstraint()->willReturn();
-        $queryInterfaceProphecy->getLimit()->willReturn();
-        $queryInterfaceProphecy->getOffset()->willReturn(10);
-
-        $this->expectException(\InvalidArgumentException::class);
-        $this->expectExceptionCode(1465223252);
-
-        $typo3DbBackend = new Typo3DbBackend();
-        $typo3DbBackend->injectQueryParser($queryParserProphecy->reveal());
-        $typo3DbBackend->getObjectCountByQuery($queryInterfaceProphecy->reveal());
-    }
 }
index 70780e0..11cd41b 100644 (file)
@@ -49,6 +49,44 @@ class Typo3DbQueryParserTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
     }
 
     /**
+     * @return \Prophecy\Prophecy\ObjectProphecy
+     */
+    protected function getQueryBuilderWithExpressionBuilderProphet()
+    {
+        $connectionProphet = $this->prophesize(Connection::class);
+        $connectionProphet->quoteIdentifier(Argument::cetera())->willReturnArgument(0);
+        $querBuilderProphet = $this->prophesize(QueryBuilder::class, $connectionProphet->reveal());
+        $expr = GeneralUtility::makeInstance(ExpressionBuilder::class, $connectionProphet->reveal());
+        $querBuilderProphet->expr()->willReturn($expr);
+        return $querBuilderProphet;
+    }
+
+    /**
+     * @return \Prophecy\Prophecy\ObjectProphecy
+     */
+    protected function getQueryBuilderProphetWithQueryBuilderForSubselect()
+    {
+        $connectionProphet = $this->prophesize(Connection::class);
+        $connectionProphet->quoteIdentifier(Argument::cetera())->willReturnArgument(0);
+        $queryBuilderProphet = $this->prophesize(QueryBuilder::class, $connectionProphet->reveal());
+        $expr = GeneralUtility::makeInstance(ExpressionBuilder::class, $connectionProphet->reveal());
+        $queryBuilderProphet->expr()->willReturn(
+            $expr
+        );
+        $queryBuilderProphet->getConnection()->willReturn($connectionProphet->reveal());
+        $queryBuilderForSubselectMock = $this->getMockBuilder(QueryBuilder::class)
+            ->setMethods(['expr', 'unquoteSingleIdentifier'])
+            ->setConstructorArgs([$connectionProphet->reveal()])
+            ->getMock();
+        $connectionProphet->createQueryBuilder()->willReturn($queryBuilderForSubselectMock);
+        $queryBuilderForSubselectMock->expects($this->any())->method('expr')->will($this->returnValue($expr));
+        $queryBuilderForSubselectMock->expects($this->any())->method('unquoteSingleIdentifier')->will($this->returnCallback(function ($identifier) {
+            return $identifier;
+        }));
+        return $queryBuilderProphet;
+    }
+
+    /**
      * @test
      */
     public function addSysLanguageStatementWorksForDefaultLanguage()
@@ -60,8 +98,10 @@ class Typo3DbQueryParserTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
         /** @var \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings|\PHPUnit_Framework_MockObject_MockObject $querySettings */
         $querySettings = $this->createMock(\TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings::class);
         $mockTypo3DbQueryParser = $this->getAccessibleMock(\TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser::class, ['dummy'], [], '', false);
+        $queryBuilderProphet = $this->getQueryBuilderWithExpressionBuilderProphet();
+        $mockTypo3DbQueryParser->_set('queryBuilder', $queryBuilderProphet->reveal());
         $sql = $mockTypo3DbQueryParser->_callRef('getSysLanguageStatement', $table, $table, $querySettings);
-        $expectedSql = '(' . $table . '.sys_language_uid IN (0,-1))';
+        $expectedSql = $table . '.sys_language_uid IN (0, -1)';
         $this->assertSame($expectedSql, $sql);
     }
 
@@ -80,8 +120,10 @@ class Typo3DbQueryParserTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
             ->getMock();
         $querySettings->setLanguageUid('1');
         $mockTypo3DbQueryParser = $this->getAccessibleMock(\TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser::class, ['dummy'], [], '', false);
+        $queryBuilderProphet = $this->getQueryBuilderWithExpressionBuilderProphet();
+        $mockTypo3DbQueryParser->_set('queryBuilder', $queryBuilderProphet->reveal());
         $sql = $mockTypo3DbQueryParser->_callRef('getSysLanguageStatement', $table, $table, $querySettings);
-        $result = '(' . $table . '.sys_language_uid IN (1,-1))';
+        $result = $table . '.sys_language_uid IN (1, -1)';
         $this->assertSame($result, $sql);
     }
 
@@ -96,8 +138,10 @@ class Typo3DbQueryParserTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
         ];
         $querySettings = new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings();
         $mockTypo3DbQueryParser = $this->getAccessibleMock(\TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser::class, ['dummy'], [], '', false);
+        $queryBuilderProphet = $this->getQueryBuilderWithExpressionBuilderProphet();
+        $mockTypo3DbQueryParser->_set('queryBuilder', $queryBuilderProphet->reveal());
         $sql = $mockTypo3DbQueryParser->_callRef('getSysLanguageStatement', $table, $table, $querySettings);
-        $expectedSql = '(' . $table . '.sys_language_uid IN (0,-1))';
+        $expectedSql = $table . '.sys_language_uid IN (0, -1)';
         $this->assertSame($expectedSql, $sql);
     }
 
@@ -114,8 +158,10 @@ class Typo3DbQueryParserTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
         $querySettings = new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings();
         $querySettings->setLanguageUid(0);
         $mockTypo3DbQueryParser = $this->getAccessibleMock(\TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser::class, ['dummy'], [], '', false);
+        $queryBuilderProphet = $this->getQueryBuilderWithExpressionBuilderProphet();
+        $mockTypo3DbQueryParser->_set('queryBuilder', $queryBuilderProphet->reveal());
         $sql = $mockTypo3DbQueryParser->_callRef('getSysLanguageStatement', $table, $table, $querySettings);
-        $expectedSql = '(' . $table . '.sys_language_uid IN (0,-1))';
+        $expectedSql = $table . '.sys_language_uid IN (0, -1)';
         $this->assertSame($expectedSql, $sql);
     }
 
@@ -131,8 +177,10 @@ class Typo3DbQueryParserTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
         $querySettings = new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings();
         $querySettings->setLanguageUid(2);
         $mockTypo3DbQueryParser = $this->getAccessibleMock(\TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser::class, ['dummy'], [], '', false);
+        $queryBuilderProphet = $this->getQueryBuilderWithExpressionBuilderProphet();
+        $mockTypo3DbQueryParser->_set('queryBuilder', $queryBuilderProphet->reveal());
         $sql = $mockTypo3DbQueryParser->_callRef('getSysLanguageStatement', $table, $table, $querySettings);
-        $expectedSql = '(' . $table . '.sys_language_uid IN (2,-1))';
+        $expectedSql = $table . '.sys_language_uid IN (2, -1)';
         $this->assertSame($expectedSql, $sql);
     }
 
@@ -149,9 +197,14 @@ class Typo3DbQueryParserTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
         $querySettings = new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings();
         $querySettings->setLanguageUid(2);
         $mockTypo3DbQueryParser = $this->getAccessibleMock(\TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser::class, ['dummy'], [], '', false);
-        $sql = $mockTypo3DbQueryParser->_callRef('getSysLanguageStatement', $table, $table, $querySettings);
-        $expectedSql = '(' . $table . '.sys_language_uid IN (2,-1) OR (' . $table . '.sys_language_uid=0 AND ' . $table . '.uid NOT IN (SELECT ' . $table . '.l10n_parent FROM ' . $table . ' WHERE ' . $table . '.l10n_parent>0 AND ' . $table . '.sys_language_uid=2)))';
-        $this->assertSame($expectedSql, $sql);
+
+        $queryBuilderProphet = $this->getQueryBuilderProphetWithQueryBuilderForSubselect();
+
+        $mockTypo3DbQueryParser->_set('queryBuilder', $queryBuilderProphet->reveal());
+
+        $compositeExpression = $mockTypo3DbQueryParser->_callRef('getSysLanguageStatement', $table, $table, $querySettings);
+        $expectedSql = '(' . $table . '.sys_language_uid IN (2, -1)) OR ((' . $table . '.sys_language_uid = 0) AND (' . $table . '.uid NOT IN (SELECT ' . $table . '.l10n_parent FROM ' . $table . ' WHERE (' . $table . '.l10n_parent > 0) AND (' . $table . '.sys_language_uid = 2))))';
+        $this->assertSame($expectedSql, $compositeExpression->__toString());
     }
 
     /**
@@ -168,14 +221,16 @@ class Typo3DbQueryParserTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
         $querySettings = new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings();
         $querySettings->setLanguageUid(2);
         $mockTypo3DbQueryParser = $this->getAccessibleMock(\TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser::class, ['dummy'], [], '', false);
-        $sql= $mockTypo3DbQueryParser->_callRef('getSysLanguageStatement', $table, $table, $querySettings);
-        $expectedSql = '(' . $table . '.sys_language_uid IN (2,-1)' .
-                ' OR (' . $table . '.sys_language_uid=0 AND ' . $table . '.uid NOT IN (' .
+        $queryBuilderProphet = $this->getQueryBuilderProphetWithQueryBuilderForSubselect();
+        $mockTypo3DbQueryParser->_set('queryBuilder', $queryBuilderProphet->reveal());
+        $compositeExpression= $mockTypo3DbQueryParser->_callRef('getSysLanguageStatement', $table, $table, $querySettings);
+        $expectedSql =  '(' . $table . '.sys_language_uid IN (2, -1))' .
+                ' OR ((' . $table . '.sys_language_uid = 0) AND (' . $table . '.uid NOT IN (' .
                 'SELECT ' . $table . '.l10n_parent FROM ' . $table .
-                ' WHERE ' . $table . '.l10n_parent>0 AND ' .
-                $table . '.sys_language_uid=2 AND ' .
-                $table . '.deleted=0)))';
-        $this->assertSame($expectedSql, $sql);
+                ' WHERE (' . $table . '.l10n_parent > 0) AND (' .
+                $table . '.sys_language_uid = 2) AND (' .
+                $table . '.deleted = 0))))';
+        $this->assertSame($expectedSql, $compositeExpression->__toString());
     }
 
     /**
@@ -192,14 +247,18 @@ class Typo3DbQueryParserTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
         $querySettings = new \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings();
         $querySettings->setLanguageUid(2);
         $mockTypo3DbQueryParser = $this->getAccessibleMock(\TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser::class, ['dummy'], [], '', false);
-        $sql = $mockTypo3DbQueryParser->_callRef('getSysLanguageStatement', $table, $table, $querySettings);
-        $expectedSql = '(' . $table . '.sys_language_uid IN (2,-1)' .
-                ' OR (' . $table . '.sys_language_uid=0 AND ' . $table . '.uid NOT IN (' .
+
+        $queryBuilderProphet = $this->getQueryBuilderProphetWithQueryBuilderForSubselect();
+
+        $mockTypo3DbQueryParser->_set('queryBuilder', $queryBuilderProphet->reveal());
+        $compositeExpression = $mockTypo3DbQueryParser->_callRef('getSysLanguageStatement', $table, $table, $querySettings);
+        $expectedSql = '(' . $table . '.sys_language_uid IN (2, -1))' .
+                ' OR ((' . $table . '.sys_language_uid = 0) AND (' . $table . '.uid NOT IN (' .
                 'SELECT ' . $table . '.l10n_parent FROM ' . $table .
-                ' WHERE ' . $table . '.l10n_parent>0 AND ' .
-                $table . '.sys_language_uid=2 AND ' .
-                $table . '.deleted=0)))';
-        $this->assertSame($expectedSql, $sql);
+                ' WHERE (' . $table . '.l10n_parent > 0) AND (' .
+                $table . '.sys_language_uid = 2) AND (' .
+                $table . '.deleted = 0))))';
+        $this->assertSame($expectedSql, $compositeExpression->__toString());
     }
 
     /**
@@ -211,20 +270,21 @@ class Typo3DbQueryParserTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
             ->setMethods(['getNodeTypeName'])
             ->disableOriginalConstructor()
             ->getMock();
-        $mockSource->expects($this->any())->method('getNodeTypeName')->will($this->returnValue('Tx_MyExt_ClassName'));
+        $mockSource->expects($this->any())->method('getNodeTypeName')->will($this->returnValue('foo'));
         $mockDataMapper = $this->getMockBuilder(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper::class)
             ->setMethods(['convertPropertyNameToColumnName', 'convertClassNameToTableName'])
             ->disableOriginalConstructor()
             ->getMock();
-        $mockDataMapper->expects($this->once())->method('convertClassNameToTableName')->with('Tx_MyExt_ClassName')->will($this->returnValue('tx_myext_tablename'));
-        $mockDataMapper->expects($this->once())->method('convertPropertyNameToColumnName')->with('fooProperty', 'Tx_MyExt_ClassName')->will($this->returnValue('converted_fieldname'));
-        $sql = [];
+        $mockDataMapper->expects($this->once())->method('convertClassNameToTableName')->with('foo')->will($this->returnValue('tx_myext_tablename'));
+        $mockDataMapper->expects($this->once())->method('convertPropertyNameToColumnName')->with('fooProperty', 'foo')->will($this->returnValue('converted_fieldname'));
+        $queryBuilderProphet = $this->prophesize(QueryBuilder::class);
+        $queryBuilderProphet->addOrderBy('tx_myext_tablename.converted_fieldname', 'ASC')->shouldBeCalledTimes(1);
+
         $orderings = ['fooProperty' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING];
         $mockTypo3DbQueryParser = $this->getAccessibleMock(\TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser::class, ['dummy'], [], '', false);
         $mockTypo3DbQueryParser->_set('dataMapper', $mockDataMapper);
-        $mockTypo3DbQueryParser->_callRef('parseOrderings', $orderings, $mockSource, $sql);
-        $expectedSql = ['orderings' => ['tx_myext_tablename.converted_fieldname ASC']];
-        $this->assertSame($expectedSql, $sql);
+        $mockTypo3DbQueryParser->_set('queryBuilder', $queryBuilderProphet->reveal());
+        $mockTypo3DbQueryParser->_callRef('parseOrderings', $orderings, $mockSource);
     }
 
     /**
@@ -245,11 +305,11 @@ class Typo3DbQueryParserTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
             ->getMock();
         $mockDataMapper->expects($this->never())->method('convertClassNameToTableName');
         $mockDataMapper->expects($this->never())->method('convertPropertyNameToColumnName');
-        $sql = [];
         $orderings = ['fooProperty' => 'unsupported_order'];
         $mockTypo3DbQueryParser = $this->getAccessibleMock(\TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser::class, ['dummy'], [], '', false);
         $mockTypo3DbQueryParser->_set('dataMapper', $mockDataMapper);
-        $mockTypo3DbQueryParser->_callRef('parseOrderings', $orderings, $mockSource, $sql);
+
+        $mockTypo3DbQueryParser->_callRef('parseOrderings', $orderings, $mockSource);
     }
 
     /**
@@ -268,16 +328,22 @@ class Typo3DbQueryParserTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
             ->getMock();
         $mockDataMapper->expects($this->any())->method('convertClassNameToTableName')->with('Tx_MyExt_ClassName')->will($this->returnValue('tx_myext_tablename'));
         $mockDataMapper->expects($this->any())->method('convertPropertyNameToColumnName')->will($this->returnValue('converted_fieldname'));
-        $sql = [];
         $orderings = [
             'fooProperty' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING,
             'barProperty' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_DESCENDING
         ];
         $mockTypo3DbQueryParser = $this->getAccessibleMock(\TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser::class, ['dummy'], [], '', false);
         $mockTypo3DbQueryParser->_set('dataMapper', $mockDataMapper);
-        $mockTypo3DbQueryParser->_callRef('parseOrderings', $orderings, $mockSource, $sql);
-        $expectedSql = ['orderings' => ['tx_myext_tablename.converted_fieldname ASC', 'tx_myext_tablename.converted_fieldname DESC']];
-        $this->assertSame($expectedSql, $sql);
+
+        $queryBuilder = $this->getMockBuilder(QueryBuilder::class)
+            ->disableOriginalConstructor()
+            ->setMethods(['addOrderBy'])
+            ->getMock();
+        $queryBuilder->expects($this->at(0))->method('addOrderBy')->with('tx_myext_tablename.converted_fieldname', 'ASC');
+        $queryBuilder->expects($this->at(1))->method('addOrderBy')->with('tx_myext_tablename.converted_fieldname', 'DESC');
+
+        $mockTypo3DbQueryParser->_set('queryBuilder', $queryBuilder);
+        $mockTypo3DbQueryParser->_callRef('parseOrderings', $orderings, $mockSource);
     }
 
     public function providerForVisibilityConstraintStatement()
@@ -455,12 +521,12 @@ class Typo3DbQueryParserTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
             'set Pid to given Pids if rootLevel = 0' => [
                 '0',
                 $table,
-                $table . '.pid IN (42,27)'
+                $table . '.pid IN (42, 27)'
             ],
             'add 0 to given Pids if rootLevel = -1' => [
                 '-1',
                 $table,
-                $table . '.pid IN (42,27,0)'
+                $table . '.pid IN (42, 27, 0)'
             ],
             'set Pid to zero if rootLevel = -1 and no further pids given' => [
                 '-1',
@@ -486,6 +552,8 @@ class Typo3DbQueryParserTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
             'rootLevel' => $rootLevel
         ];
         $mockTypo3DbQueryParser = $this->getAccessibleMock(\TYPO3\CMS\Extbase\Persistence\Generic\Storage\Typo3DbQueryParser::class, ['dummy'], [], '', false);
+        $queryBuilderProphet = $this->getQueryBuilderWithExpressionBuilderProphet();
+        $mockTypo3DbQueryParser->_set('queryBuilder', $queryBuilderProphet->reveal());
         $sql = $mockTypo3DbQueryParser->_callRef('getPageIdStatement', $table, $table, $storagePageIds);
 
         $this->assertSame($expectedSql, $sql);