[BUGFIX] Extbase distinct query result handling 70/53670/5
authorOliver Hader <oliver@typo3.org>
Mon, 21 Aug 2017 08:13:20 +0000 (10:13 +0200)
committerOliver Hader <oliver.hader@typo3.org>
Thu, 24 Aug 2017 18:48:57 +0000 (20:48 +0200)
Since Doctrine DBAL has been integrated into the TYPO3 core during
version 8 development and Extbase queries have been adjusted with
TYPO3 version 8.4.0, the behavior on distinct query results were
mixed up as well.

Extbase queries using the query-builder until TYPO3 7 LTS contained a
dedicated `SELECT DISTINCT` when retrieving data which lead to unique
entities, especially when implicit `LEFT JOIN` statements have been
added to the query to resolve cardinalities of the types one-to-many
and many-to-many.

Besides that using `GROUP BY` is not reliable in this particular
Extbase scenario. Further details can be found in MySQL documentation:
https://dev.mysql.com/doc/refman/5.7/en/group-by-handling.html

Change-Id: Ic5fd1d4752eefec7fcff37d8d62f55ea7299e8d6
Resolves: #80380
Releases: master, 8.7
Reviewed-on: https://review.typo3.org/53658
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Susanne Moog <susanne.moog@typo3.org>
Tested-by: Susanne Moog <susanne.moog@typo3.org>
Reviewed-by: Morton Jonuschat <m.jonuschat@mojocode.de>
Tested-by: Morton Jonuschat <m.jonuschat@mojocode.de>
(cherry picked from commit f8aaf85cc5602a45a6f6b949a77ce109bbacc8f8)
Reviewed-on: https://review.typo3.org/53670
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
17 files changed:
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/Classes/Domain/Model/Person.php
typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/Post.php
typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/PersonRepository.php
typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Repository/PostRepository.php
typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_person.php
typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Configuration/TCA/tx_blogexample_domain_model_post.php
typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Resources/Private/Language/locallang_db.xml
typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/ext_tables.sql
typo3/sysext/extbase/Tests/Functional/Persistence/CountTest.php
typo3/sysext/extbase/Tests/Functional/Persistence/Fixtures/persons.xml
typo3/sysext/extbase/Tests/Functional/Persistence/Fixtures/posts.xml
typo3/sysext/extbase/Tests/Functional/Persistence/Fixtures/tags-mm.xml
typo3/sysext/extbase/Tests/Functional/Persistence/Fixtures/tags.xml
typo3/sysext/extbase/Tests/Functional/Persistence/QueryParserTest.php
typo3/sysext/extbase/Tests/Functional/Persistence/RelationTest.php

index d82e9aa..a1ee26d 100644 (file)
@@ -356,8 +356,14 @@ class Typo3DbBackend implements BackendInterface, SingletonInterface
         if ($statement instanceof Qom\Statement) {
             $rows = $this->getObjectDataByRawQuery($statement);
         } else {
-            $queryBuilder = $this->objectManager->get(Typo3DbQueryParser::class)
-                    ->convertQueryToDoctrineQueryBuilder($query);
+            $queryParser = $this->objectManager->get(Typo3DbQueryParser::class);
+            $queryBuilder = $queryParser
+                ->convertQueryToDoctrineQueryBuilder($query);
+            $selectParts = $queryBuilder->getQueryPart('select');
+            if ($queryParser->isDistinctQuerySuggested() && !empty($selectParts)) {
+                $selectParts[0] = 'DISTINCT ' . $selectParts[0];
+                $queryBuilder->selectLiteral(...$selectParts);
+            }
             if ($query->getOffset()) {
                 $queryBuilder->setFirstResult($query->getOffset());
             }
@@ -440,17 +446,18 @@ 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);
         }
 
-        $queryBuilder = $this->objectManager->get(Typo3DbQueryParser::class)
+        $queryParser = $this->objectManager->get(Typo3DbQueryParser::class);
+        $queryBuilder = $queryParser
             ->convertQueryToDoctrineQueryBuilder($query)
             ->resetQueryPart('orderBy');
 
-        if (count($queryBuilder->getQueryPart('groupBy')) !== 0) {
+        if ($queryParser->isDistinctQuerySuggested()) {
             $source = $queryBuilder->getQueryPart('from')[0];
             // Tablename is already quoted for the DBMS, we need to treat table and field names separately
             $tableName = $source['alias'] ?: $source['table'];
             $fieldName = $queryBuilder->quoteIdentifier('uid');
             $queryBuilder->resetQueryPart('groupBy')
-                ->selectLiteral(sprintf('COUNT(DISTINCT %s.%s)', $tableName, $fieldName));
+             ->selectLiteral(sprintf('COUNT(DISTINCT %s.%s)', $tableName, $fieldName));
         } else {
             $queryBuilder->count('*');
         }
index 84b0bbe..ab16e6d 100644 (file)
@@ -92,6 +92,11 @@ class Typo3DbQueryParser
     protected $tableName = '';
 
     /**
+     * @var bool
+     */
+    protected $suggestDistinctQuery = false;
+
+    /**
      * @param \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper $dataMapper
      */
     public function injectDataMapper(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper $dataMapper)
@@ -108,6 +113,18 @@ class Typo3DbQueryParser
     }
 
     /**
+     * Whether using a distinct query is suggested.
+     * This information is defined during parsing of the current query
+     * for RELATION_HAS_MANY & RELATION_HAS_AND_BELONGS_TO_MANY relations.
+     *
+     * @return bool
+     */
+    public function isDistinctQuerySuggested(): bool
+    {
+        return $this->suggestDistinctQuery;
+    }
+
+    /**
      * Returns a ready to be executed QueryBuilder object, based on the query
      *
      * @param QueryInterface $query
@@ -988,6 +1005,7 @@ class Typo3DbQueryParser
             $this->queryBuilder->andWhere(
                 $this->getAdditionalMatchFieldsStatement($this->queryBuilder->expr(), $columnMap, $childTableAlias, $realTableName)
             );
+            $this->suggestDistinctQuery = true;
         } elseif ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
             $relationTableName = $columnMap->getRelationTableName();
             $relationTableAlias = $relationTableAlias = $this->getUniqueAlias($relationTableName, $fullPropertyPath . '_mm');
@@ -1008,7 +1026,7 @@ class Typo3DbQueryParser
             );
             $this->queryBuilder->leftJoin($relationTableAlias, $childTableName, $childTableAlias, $joinConditionExpression);
             $this->unionTableAliasCache[] = $childTableAlias;
-            $this->queryBuilder->addGroupBy($this->tableName . '.uid');
+            $this->suggestDistinctQuery = true;
         } else {
             throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception('Could not determine type of relation.', 1252502725);
         }
index 5a24ef9..a783140 100644 (file)
@@ -52,6 +52,9 @@ class Person extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
         $this->setFirstname($firstname);
         $this->setLastname($lastname);
         $this->setEmail($email);
+
+        $this->tags = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage();
+        $this->tagsSpecial = new \TYPO3\CMS\Extbase\Persistence\ObjectStorage();
     }
 
     /**
@@ -123,4 +126,68 @@ class Person extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
     {
         return $this->email;
     }
+
+    /**
+     * @return \TYPO3\CMS\Extbase\Persistence\ObjectStorage|\ExtbaseTeam\BlogExample\Domain\Model\Tag[]
+     */
+    public function getTags()
+    {
+        return $this->tags;
+    }
+
+    /**
+     * @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\ExtbaseTeam\BlogExample\Domain\Model\Tag> $tags
+     */
+    public function setTags(\TYPO3\CMS\Extbase\Persistence\ObjectStorage $tags)
+    {
+        $this->tags = $tags;
+    }
+
+    /**
+     * @param Tag $tag
+     */
+    public function addTag(Tag $tag)
+    {
+        $this->tags->attach($tag);
+    }
+
+    /**
+     * @param Tag $tag
+     */
+    public function removeTag(Tag $tag)
+    {
+        $this->tags->detach($tag);
+    }
+
+    /**
+     * @return \TYPO3\CMS\Extbase\Persistence\ObjectStorage|\ExtbaseTeam\BlogExample\Domain\Model\Tag[]
+     */
+    public function getTagsSpecial()
+    {
+        return $this->tagsSpecial;
+    }
+
+    /**
+     * @param \TYPO3\CMS\Extbase\Persistence\ObjectStorage<\ExtbaseTeam\BlogExample\Domain\Model\Tag> $tagsSpecial
+     */
+    public function setTagsSpecial(\TYPO3\CMS\Extbase\Persistence\ObjectStorage $tagsSpecial)
+    {
+        $this->tagsSpecial = $tagsSpecial;
+    }
+
+    /**
+     * @param Tag $tag
+     */
+    public function addTagSpecial(Tag $tag)
+    {
+        $this->tagsSpecial->attach($tag);
+    }
+
+    /**
+     * @param Tag $tag
+     */
+    public function removeTagSpecial(Tag $tag)
+    {
+        $this->tagsSpecial->detach($tag);
+    }
 }
index ea070b5..1e4f8d4 100644 (file)
@@ -41,6 +41,11 @@ class Post extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
     protected $author = null;
 
     /**
+     * @var \ExtbaseTeam\BlogExample\Domain\Model\Person
+     */
+    protected $reviewer = null;
+
+    /**
      * @var string
      * @validate StringLength(minimum = 3)
      */
@@ -252,6 +257,22 @@ class Post extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
     }
 
     /**
+     * @return \ExtbaseTeam\BlogExample\Domain\Model\Person
+     */
+    public function getReviewer()
+    {
+        return $this->reviewer;
+    }
+
+    /**
+     * @param \ExtbaseTeam\BlogExample\Domain\Model\Person $reviewer
+     */
+    public function setReviewer(\ExtbaseTeam\BlogExample\Domain\Model\Person $reviewer)
+    {
+        $this->reviewer = $reviewer;
+    }
+
+    /**
      * Sets the content for this post
      *
      * @param string $content
index f77b072..28a7a46 100644 (file)
@@ -14,9 +14,12 @@ namespace ExtbaseTeam\BlogExample\Domain\Repository;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Extbase\Persistence\QueryInterface;
+
 /**
  * A repository for persons
  */
 class PersonRepository extends \TYPO3\CMS\Extbase\Persistence\Repository
 {
+    protected $defaultOrderings = ['lastname' => QueryInterface::ORDER_ASCENDING];
 }
index f51a4e5..8568d15 100644 (file)
@@ -14,22 +14,24 @@ namespace ExtbaseTeam\BlogExample\Domain\Repository;
  * The TYPO3 project - inspiring people to share!
  */
 
+use ExtbaseTeam\BlogExample\Domain\Model\Post;
 use TYPO3\CMS\Extbase\Persistence\QueryInterface;
+use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
 
 /**
  * A repository for blog posts
  *
- * @method \ExtbaseTeam\BlogExample\Domain\Model\Post findByUid($uid)
+ * @method Post findByUid($uid)
  */
 class PostRepository extends \TYPO3\CMS\Extbase\Persistence\Repository
 {
-    protected $defaultOrderings = ['date' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_DESCENDING];
+    protected $defaultOrderings = ['date' => QueryInterface::ORDER_DESCENDING];
 
     /**
      * Finds all posts by the specified blog
      *
      * @param \ExtbaseTeam\BlogExample\Domain\Model\Blog $blog The blog the post must refer to
-     * @return \TYPO3\CMS\Extbase\Persistence\QueryResultInterface The posts
+     * @return QueryResultInterface The posts
      */
     public function findAllByBlog(\ExtbaseTeam\BlogExample\Domain\Model\Blog $blog)
     {
@@ -46,7 +48,7 @@ class PostRepository extends \TYPO3\CMS\Extbase\Persistence\Repository
      *
      * @param string $tag
      * @param \ExtbaseTeam\BlogExample\Domain\Model\Blog $blog The blog the post must refer to
-     * @return \TYPO3\CMS\Extbase\Persistence\QueryResultInterface The posts
+     * @return QueryResultInterface The posts
      */
     public function findByTagAndBlog($tag, \ExtbaseTeam\BlogExample\Domain\Model\Blog $blog)
     {
@@ -64,10 +66,10 @@ class PostRepository extends \TYPO3\CMS\Extbase\Persistence\Repository
     /**
      * Finds all remaining posts of the blog
      *
-     * @param \ExtbaseTeam\BlogExample\Domain\Model\Post $post The reference post
-     * @return \TYPO3\CMS\Extbase\Persistence\QueryResultInterface The posts
+     * @param Post $post The reference post
+     * @return QueryResultInterface The posts
      */
-    public function findRemaining(\ExtbaseTeam\BlogExample\Domain\Model\Post $post)
+    public function findRemaining(Post $post)
     {
         $blog = $post->getBlog();
         $query = $this->createQuery();
@@ -86,10 +88,10 @@ class PostRepository extends \TYPO3\CMS\Extbase\Persistence\Repository
     /**
      * Finds the previous of the given post
      *
-     * @param \ExtbaseTeam\BlogExample\Domain\Model\Post $post The reference post
-     * @return \ExtbaseTeam\BlogExample\Domain\Model\Post
+     * @param Post $post The reference post
+     * @return Post
      */
-    public function findPrevious(\ExtbaseTeam\BlogExample\Domain\Model\Post $post)
+    public function findPrevious(Post $post)
     {
         $query = $this->createQuery();
         return $query
@@ -103,10 +105,10 @@ class PostRepository extends \TYPO3\CMS\Extbase\Persistence\Repository
     /**
      * Finds the post next to the given post
      *
-     * @param \ExtbaseTeam\BlogExample\Domain\Model\Post $post The reference post
-     * @return \ExtbaseTeam\BlogExample\Domain\Model\Post
+     * @param Post $post The reference post
+     * @return Post
      */
-    public function findNext(\ExtbaseTeam\BlogExample\Domain\Model\Post $post)
+    public function findNext(Post $post)
     {
         $query = $this->createQuery();
         return $query
@@ -122,7 +124,7 @@ class PostRepository extends \TYPO3\CMS\Extbase\Persistence\Repository
      *
      * @param \ExtbaseTeam\BlogExample\Domain\Model\Blog $blog The blog the post must refer to
      * @param int $limit The number of posts to return at max
-     * @return \TYPO3\CMS\Extbase\Persistence\QueryResultInterface The posts
+     * @return QueryResultInterface The posts
      */
     public function findRecentByBlog(\ExtbaseTeam\BlogExample\Domain\Model\Blog $blog, $limit = 5)
     {
@@ -139,7 +141,7 @@ class PostRepository extends \TYPO3\CMS\Extbase\Persistence\Repository
      * Find posts by category
      *
      * @param int $categoryUid
-     * @return \TYPO3\CMS\Extbase\Persistence\QueryResultInterface
+     * @return QueryResultInterface
      */
     public function findByCategory($categoryUid)
     {
index ea7824f..d8ca486 100644 (file)
@@ -60,8 +60,7 @@ return [
                 'type' => 'inline',
                 'foreign_table' => 'tx_blogexample_domain_model_tag',
                 'MM' => 'tx_blogexample_domain_model_tag_mm',
-                'foreign_table_field' => 'tablenames',
-                'foreign_match_fields' => [
+                'MM_match_fields' => [
                     'fieldname' => 'tags'
                 ],
                 'appearance' => [
@@ -79,8 +78,7 @@ return [
                 'type' => 'inline',
                 'foreign_table' => 'tx_blogexample_domain_model_tag',
                 'MM' => 'tx_blogexample_domain_model_tag_mm',
-                'foreign_table_field' => 'tablenames',
-                'foreign_match_fields' => [
+                'MM_match_fields' => [
                     'fieldname' => 'tags_special'
                 ],
                 'appearance' => [
index 0168cc4..088a5ad 100644 (file)
@@ -118,6 +118,26 @@ return [
                 ],
             ],
         ],
+        'reviewer' => [
+            'exclude' => true,
+            'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xml:tx_blogexample_domain_model_post.reviewer',
+            'config' => [
+                'type' => 'select',
+                'renderType' => 'selectSingle',
+                'foreign_table' => 'tx_blogexample_domain_model_person',
+                'fieldControl' => [
+                    'editPopup' => [
+                        'disabled' => false,
+                    ],
+                    'addRecord' => [
+                        'disabled' => false,
+                        'options' => [
+                            'setValue' => 'prepend',
+                        ],
+                    ],
+                ],
+            ],
+        ],
         'content' => [
             'exclude' => true,
             'label' => 'LLL:EXT:blog_example/Resources/Private/Language/locallang_db.xml:tx_blogexample_domain_model_post.content',
index d3d400a..f2be394 100644 (file)
@@ -33,6 +33,7 @@
                        <label index="tx_blogexample_domain_model_comment">Comment</label>
                        <label index="tx_blogexample_domain_model_comment.date">Date</label>
                        <label index="tx_blogexample_domain_model_comment.author">Author</label>
+                       <label index="tx_blogexample_domain_model_comment.reviewer">Reviewer</label>
                        <label index="tx_blogexample_domain_model_comment.email">Email</label>
                        <label index="tx_blogexample_domain_model_comment.content">Content</label>
                        <label index="tx_blogexample_domain_model_tag">Tag</label>
@@ -41,4 +42,4 @@
                        <label index="fe_users.tx_extbase_type.Tx_BlogExample_Domain_Model_Administrator">Blog Admin (BlogExample)</label>
                </languageKey>
        </data>
-</T3locallang>
\ No newline at end of file
+</T3locallang>
index 39131fb..ed5dfac 100644 (file)
@@ -50,6 +50,7 @@ CREATE TABLE tx_blogexample_domain_model_post (
        title varchar(255) DEFAULT '' NOT NULL,
        date int(11) DEFAULT '0' NOT NULL,
        author int(255) DEFAULT '0' NOT NULL,
+       reviewer int(255) DEFAULT '0' NOT NULL,
        content text NOT NULL,
        tags int(11) unsigned DEFAULT '0' NOT NULL,
        comments int(11) unsigned DEFAULT '0' NOT NULL,
@@ -219,4 +220,4 @@ CREATE TABLE tx_blogexample_domain_model_dateexample (
 
        PRIMARY KEY (uid),
        KEY parent (pid)
-);
\ No newline at end of file
+);
index 5354161..2e7c7b1 100644 (file)
@@ -185,7 +185,6 @@ class CountTest extends \TYPO3\TestingFramework\Core\Functional\FunctionalTestCa
      * Test if count works with subproperties in multiple left join.
      *
      * @test
-     * @group not-mssql
      */
     public function subpropertyInMultipleLeftJoinCountTest()
     {
index 693e88e..b5acc89 100644 (file)
@@ -5,14 +5,16 @@
                <pid>0</pid>
                <firstname>Author</firstname>
                <lastname>With tag and special tag</lastname>
-               <tags>1</tags>
-               <tags_special>1</tags_special>
+               <email>author-1@test.typo3.org</email>
+               <tags>2</tags>
+               <tags_special>2</tags_special>
        </tx_blogexample_domain_model_person>
        <tx_blogexample_domain_model_person>
                <uid>2</uid>
                <pid>0</pid>
                <firstname>Author</firstname>
                <lastname>With tag</lastname>
+               <email>author-2@test.typo3.org</email>
                <tags>1</tags>
                <tags_special>0</tags_special>
        </tx_blogexample_domain_model_person>
@@ -21,6 +23,7 @@
                <pid>0</pid>
                <firstname>Author</firstname>
                <lastname>With special tag</lastname>
+               <email>author-3@test.typo3.org</email>
                <tags>0</tags>
                <tags_special>1</tags_special>
        </tx_blogexample_domain_model_person>
@@ -29,6 +32,7 @@
                <pid>0</pid>
                <firstname>Another Author</firstname>
                <lastname>With special tag</lastname>
+               <email>author-4@test.typo3.org</email>
                <tags>0</tags>
                <tags_special>1</tags_special>
        </tx_blogexample_domain_model_person>
index 4676719..c83e4e7 100644 (file)
@@ -5,7 +5,10 @@
                <pid>0</pid>
                <tstamp>121319</tstamp>
                <blog>1</blog>
+               <author>3</author>
+               <reviewer>2</reviewer>
                <tags>10</tags>
+               <date>1502275450</date>
                <categories>3</categories>
                <title>Post1</title>
                <content>Lorem ipsum...</content>
                <uid>2</uid>
                <pid>0</pid>
                <blog>1</blog>
+               <author>2</author>
+               <reviewer>2</reviewer>
                <tags>1</tags>
+               <date>1502275450</date>
                <categories>0</categories>
                <title>Post2</title>
                <content>Lorem ipsum...</content>
                <uid>3</uid>
                <pid>0</pid>
                <blog>1</blog>
+               <author>2</author>
+               <reviewer>1</reviewer>
                <tags>1</tags>
+               <date>1502275450</date>
                <categories>0</categories>
                <title>Post3</title>
                <content>Lorem ipsum...</content>
                <uid>4</uid>
                <pid>0</pid>
                <blog>1</blog>
+               <author>1</author>
+               <reviewer>2</reviewer>
                <tags>1</tags>
+               <date>1502275450</date>
                <categories>0</categories>
                <title>Post4</title>
                <content>Lorem ipsum...</content>
                <uid>5</uid>
                <pid>0</pid>
                <blog>1</blog>
+               <author>1</author>
+               <reviewer>0</reviewer>
                <tags>1</tags>
+               <date>1502275450</date>
                <categories>0</categories>
                <title>Post5</title>
                <content>Lorem ipsum...</content>
                <uid>6</uid>
                <pid>0</pid>
                <blog>1</blog>
+               <author>0</author>
+               <reviewer>0</reviewer>
                <tags>1</tags>
+               <date>1502275450</date>
                <categories>0</categories>
                <title>Post6</title>
                <content>Lorem ipsum...</content>
                <uid>7</uid>
                <pid>0</pid>
                <blog>1</blog>
+               <author>1</author>
+               <reviewer>2</reviewer>
                <tags>1</tags>
+               <date>1502275450</date>
                <categories>0</categories>
                <title>Post7</title>
                <content>Lorem ipsum...</content>
                <uid>8</uid>
                <pid>0</pid>
                <blog>1</blog>
+               <author>1</author>
+               <reviewer>2</reviewer>
                <tags>1</tags>
+               <date>1502275450</date>
                <categories>0</categories>
                <title>Post8</title>
                <content>Lorem ipsum...</content>
                <uid>9</uid>
                <pid>0</pid>
                <blog>1</blog>
+               <author>1</author>
+               <reviewer>2</reviewer>
                <tags>1</tags>
+               <date>1502275450</date>
                <categories>0</categories>
                <content>Lorem ipsum...</content>
                <l18n_diffsource></l18n_diffsource>
                <uid>10</uid>
                <pid>0</pid>
                <blog>1</blog>
+               <author>1</author>
+               <reviewer>2</reviewer>
                <tags>1</tags>
+               <date>1502275450</date>
                <categories>0</categories>
                <title>Post10</title>
                <content>Lorem ipsum...</content>
                <uid>11</uid>
                <pid>0</pid>
                <blog>2</blog>
+               <author>2</author>
+               <reviewer>1</reviewer>
                <tags>0</tags>
+               <date>1502275450</date>
                <categories>0</categories>
                <title>post1</title>
                <content>Lorem ipsum...</content>
                <pid>0</pid>
                <blog>3</blog>
                <author>0</author>
+               <reviewer>2</reviewer>
                <tags>1</tags>
+               <date>1502275450</date>
                <categories>0</categories>
                <title>post with tag</title>
                <content>Lorem ipsum...</content>
                <pid>0</pid>
                <blog>3</blog>
                <author>1</author>
+               <reviewer>2</reviewer>
                <tags>0</tags>
+               <date>1502275450</date>
                <categories>0</categories>
                <title>post with tagged author</title>
                <content>Lorem ipsum...</content>
                <pid>0</pid>
                <blog>3</blog>
                <author>1</author>
+               <reviewer>2</reviewer>
                <tags>1</tags>
+               <date>1502275450</date>
                <categories>0</categories>
                <title>post with tag and tagged author</title>
                <content>Lorem ipsum...</content>
                <uid>20</uid>
                <pid>0</pid>
                <blog>3</blog>
+               <author>1</author>
+               <reviewer>3</reviewer>
                <tags>0</tags>
+               <date>1502275450</date>
                <categories>0</categories>
                <title>post20 hidden</title>
                <content>Lorem ipsum...</content>
                <uid>30</uid>
                <pid>0</pid>
                <blog>3</blog>
+               <author>1</author>
+               <reviewer>3</reviewer>
                <tags>0</tags>
+               <date>1502275450</date>
                <categories>0</categories>
                <title>post30 deleted</title>
                <content>Lorem ipsum...</content>
index cc46786..86ccbfe 100644 (file)
                <sorting>1</sorting>
                <sorting_foreign>1</sorting_foreign>
        </tx_blogexample_domain_model_tag_mm>
+       <tx_blogexample_domain_model_tag_mm>
+               <uid_local>1</uid_local>
+               <uid_foreign>14</uid_foreign>
+               <tablenames>tx_blogexample_domain_model_person</tablenames>
+               <fieldname>tags</fieldname>
+               <sorting>1</sorting>
+               <sorting_foreign>1</sorting_foreign>
+       </tx_blogexample_domain_model_tag_mm>
+       <tx_blogexample_domain_model_tag_mm>
+               <uid_local>1</uid_local>
+               <uid_foreign>14</uid_foreign>
+               <tablenames>tx_blogexample_domain_model_person</tablenames>
+               <fieldname>tags_special</fieldname>
+               <sorting>1</sorting>
+               <sorting_foreign>1</sorting_foreign>
+       </tx_blogexample_domain_model_tag_mm>
 </dataset>
index e1a2b8c..5c1b204 100644 (file)
                <name>SpecialTagForAuthor1</name>
                <deleted>0</deleted>
        </tx_blogexample_domain_model_tag>
+       <tx_blogexample_domain_model_tag>
+               <uid>14</uid>
+               <pid>0</pid>
+               <posts>0</posts>
+               <name>SharedTag</name>
+               <deleted>0</deleted>
+       </tx_blogexample_domain_model_tag>
 </dataset>
index 50b8f62..0474bab 100644 (file)
@@ -63,7 +63,6 @@ class QueryParserTest extends \TYPO3\TestingFramework\Core\Functional\Functional
 
     /**
      * @test
-     * @group not-mssql
      */
     public function queryWithMultipleRelationsToIdenticalTablesReturnsExpectedResultForOrQuery()
     {
@@ -88,7 +87,6 @@ class QueryParserTest extends \TYPO3\TestingFramework\Core\Functional\Functional
      * Test ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY
      *
      * @test
-     * @group not-mssql
      */
     public function queryWithRelationHasAndBelongsToManyReturnsExpectedResult()
     {
@@ -125,7 +123,6 @@ class QueryParserTest extends \TYPO3\TestingFramework\Core\Functional\Functional
      * Test ColumnMap::RELATION_HAS_ONE, ColumnMap::ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY
      *
      * @test
-     * @group not-mssql
      */
     public function queryWithRelationHasOneAndHasAndBelongsToManyWithoutParentKeyFieldNameReturnsExpectedResult()
     {
@@ -136,12 +133,12 @@ class QueryParserTest extends \TYPO3\TestingFramework\Core\Functional\Functional
             $query->equals('author.firstname', 'Author')
         );
         $result = $query->execute()->toArray();
-        $this->assertCount(2, $result);
+        // there are 16 post in total, 2 without author, 1 hidden, 1 deleted => 12 posts
+        $this->assertCount(12, $result);
     }
 
     /**
      * @test
-     * @group not-mssql
      */
     public function orReturnsExpectedResult()
     {
@@ -160,7 +157,6 @@ class QueryParserTest extends \TYPO3\TestingFramework\Core\Functional\Functional
 
     /**
      * @test
-     * @group not-mssql
      */
     public function queryWithMultipleRelationsToIdenticalTablesReturnsExpectedResultForAndQuery()
     {
@@ -180,7 +176,6 @@ class QueryParserTest extends \TYPO3\TestingFramework\Core\Functional\Functional
 
     /**
      * @test
-     * @group not-mssql
      */
     public function queryWithFindInSetReturnsExpectedResult()
     {
index 0adbc23..836d9cc 100644 (file)
@@ -14,16 +14,23 @@ namespace TYPO3\CMS\Extbase\Tests\Functional\Persistence;
  * The TYPO3 project - inspiring people to share!
  */
 
+use ExtbaseTeam\BlogExample\Domain\Model\Blog;
 use ExtbaseTeam\BlogExample\Domain\Model\Post;
+use ExtbaseTeam\BlogExample\Domain\Model\Tag;
+use ExtbaseTeam\BlogExample\Domain\Repository\BlogRepository;
+use ExtbaseTeam\BlogExample\Domain\Repository\PersonRepository;
+use ExtbaseTeam\BlogExample\Domain\Repository\PostRepository;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
 use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
+use TYPO3\CMS\Extbase\Persistence\QueryInterface;
 
 class RelationTest extends \TYPO3\TestingFramework\Core\Functional\FunctionalTestCase
 {
     /**
-     * @var \ExtbaseTeam\BlogExample\Domain\Model\Blog
+     * @var Blog
      */
     protected $blog;
 
@@ -51,7 +58,9 @@ class RelationTest extends \TYPO3\TestingFramework\Core\Functional\FunctionalTes
         $this->importDataSet('PACKAGE:typo3/testing-framework/Resources/Core/Functional/Fixtures/pages.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/posts.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/tags.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/post-tag-mm.xml');
         $this->importDataSet(ORIGINAL_ROOT . 'typo3/sysext/extbase/Tests/Functional/Persistence/Fixtures/categories.xml');
         $this->importDataSet(ORIGINAL_ROOT . 'typo3/sysext/extbase/Tests/Functional/Persistence/Fixtures/category-mm.xml');
@@ -59,7 +68,7 @@ class RelationTest extends \TYPO3\TestingFramework\Core\Functional\FunctionalTes
         $this->objectManager = GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Object\ObjectManager::class);
         $this->persistentManager = $this->objectManager->get(\TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager::class);
         /* @var $blogRepository \TYPO3\CMS\Extbase\Persistence\Repository */
-        $blogRepository = $this->objectManager->get(\ExtbaseTeam\BlogExample\Domain\Repository\BlogRepository::class);
+        $blogRepository = $this->objectManager->get(BlogRepository::class);
         $this->blog = $blogRepository->findByUid(1);
     }
 
@@ -84,8 +93,8 @@ class RelationTest extends \TYPO3\TestingFramework\Core\Functional\FunctionalTes
             ->fetchColumn(0);
 
         $newPostTitle = 'sdufhisdhuf';
-        /** @var \ExtbaseTeam\BlogExample\Domain\Model\Post $newPost */
-        $newPost = $this->objectManager->get(\ExtbaseTeam\BlogExample\Domain\Model\Post::class);
+        /** @var Post $newPost */
+        $newPost = $this->objectManager->get(Post::class);
         $newPost->setBlog($this->blog);
         $newPost->setTitle($newPostTitle);
         $newPost->setContent('Bla Bla Bla');
@@ -215,8 +224,8 @@ class RelationTest extends \TYPO3\TestingFramework\Core\Functional\FunctionalTes
         ->execute()
         ->fetchColumn(0);
 
-        /** @var \ExtbaseTeam\BlogExample\Domain\Model\Post $newPost */
-        $newPost = $this->objectManager->get(\ExtbaseTeam\BlogExample\Domain\Model\Post::class);
+        /** @var Post $newPost */
+        $newPost = $this->objectManager->get(Post::class);
 
         $posts = clone $this->blog->getPosts();
         $this->blog->getPosts()->removeAll($posts);
@@ -420,11 +429,11 @@ class RelationTest extends \TYPO3\TestingFramework\Core\Functional\FunctionalTes
 
         $newTagTitle = 'sdufhisdhuf';
 
-        /** @var \ExtbaseTeam\BlogExample\Domain\Model\Tag $newTag */
+        /** @var Tag $newTag */
         $newTag = $this->objectManager->get('ExtbaseTeam\\BlogExample\\Domain\\Model\\Tag', $newTagTitle);
 
-        /** @var \ExtbaseTeam\BlogExample\Domain\Repository\PostRepository $postRepository */
-        $postRepository = $this->objectManager->get(\ExtbaseTeam\BlogExample\Domain\Repository\PostRepository::class);
+        /** @var PostRepository $postRepository */
+        $postRepository = $this->objectManager->get(PostRepository::class);
         $post = $postRepository->findByUid(1);
         $post->addTag($newTag);
 
@@ -472,8 +481,8 @@ class RelationTest extends \TYPO3\TestingFramework\Core\Functional\FunctionalTes
             ->execute()
             ->fetchColumn(0);
 
-        /** @var \ExtbaseTeam\BlogExample\Domain\Repository\PostRepository $postRepository */
-        $postRepository = $this->objectManager->get(\ExtbaseTeam\BlogExample\Domain\Repository\PostRepository::class);
+        /** @var PostRepository $postRepository */
+        $postRepository = $this->objectManager->get(PostRepository::class);
         $post = $postRepository->findByUid(1);
         $tags = $post->getTags();
         $tagsArray = $tags->toArray();
@@ -550,14 +559,14 @@ class RelationTest extends \TYPO3\TestingFramework\Core\Functional\FunctionalTes
             ->execute()
             ->fetchColumn(0);
 
-        /** @var \ExtbaseTeam\BlogExample\Domain\Repository\PostRepository $postRepository */
-        $postRepository = $this->objectManager->get(\ExtbaseTeam\BlogExample\Domain\Repository\PostRepository::class);
+        /** @var PostRepository $postRepository */
+        $postRepository = $this->objectManager->get(PostRepository::class);
         $post = $postRepository->findByUid(1);
         $tags = clone $post->getTags();
         $post->setTags(new ObjectStorage());
 
-        /** @var \ExtbaseTeam\BlogExample\Domain\Model\Tag $newTag */
-        $newTag = $this->objectManager->get(\ExtbaseTeam\BlogExample\Domain\Model\Tag::class, 'INSERTED TAG at position 6 : ' . strftime(''));
+        /** @var Tag $newTag */
+        $newTag = $this->objectManager->get(Tag::class, 'INSERTED TAG at position 6 : ' . strftime(''));
 
         $counter = 1;
         foreach ($tags as $tag) {
@@ -634,8 +643,8 @@ class RelationTest extends \TYPO3\TestingFramework\Core\Functional\FunctionalTes
             ->fetchColumn(0);
         $this->assertEquals(10, $countTags);
 
-        /** @var \ExtbaseTeam\BlogExample\Domain\Repository\PostRepository $postRepository */
-        $postRepository = $this->objectManager->get(\ExtbaseTeam\BlogExample\Domain\Repository\PostRepository::class);
+        /** @var PostRepository $postRepository */
+        $postRepository = $this->objectManager->get(PostRepository::class);
         $post = $postRepository->findByUid(1);
         $tags = clone $post->getTags();
         $counter = 1;
@@ -713,8 +722,8 @@ class RelationTest extends \TYPO3\TestingFramework\Core\Functional\FunctionalTes
             ->fetchColumn(0);
         $this->assertEquals(10, $countTags);
 
-        /** @var \ExtbaseTeam\BlogExample\Domain\Repository\PostRepository $postRepository */
-        $postRepository = $this->objectManager->get(\ExtbaseTeam\BlogExample\Domain\Repository\PostRepository::class);
+        /** @var PostRepository $postRepository */
+        $postRepository = $this->objectManager->get(PostRepository::class);
         $post = $postRepository->findByUid(1);
         $tags = clone $post->getTags();
         $tagsArray = $tags->toArray();
@@ -805,8 +814,8 @@ class RelationTest extends \TYPO3\TestingFramework\Core\Functional\FunctionalTes
             ->execute()
             ->fetch();
 
-        /** @var \ExtbaseTeam\BlogExample\Domain\Repository\PostRepository $postRepository */
-        $postRepository = $this->objectManager->get(\ExtbaseTeam\BlogExample\Domain\Repository\PostRepository::class);
+        /** @var PostRepository $postRepository */
+        $postRepository = $this->objectManager->get(PostRepository::class);
         $post = $postRepository->findByUid(1);
         $post->setTitle('newTitle');
 
@@ -832,8 +841,8 @@ class RelationTest extends \TYPO3\TestingFramework\Core\Functional\FunctionalTes
      */
     public function mmRelationWithoutMatchFieldIsResolved()
     {
-        /** @var \ExtbaseTeam\BlogExample\Domain\Repository\PostRepository $postRepository */
-        $postRepository = $this->objectManager->get(\ExtbaseTeam\BlogExample\Domain\Repository\PostRepository::class);
+        /** @var PostRepository $postRepository */
+        $postRepository = $this->objectManager->get(PostRepository::class);
         $posts = $postRepository->findByTagAndBlog('Tag2', $this->blog);
         $this->assertCount(1, $posts);
     }
@@ -866,8 +875,8 @@ class RelationTest extends \TYPO3\TestingFramework\Core\Functional\FunctionalTes
             ->fetchColumn(0);
         $this->assertEquals(3, $countCategories);
 
-        /** @var \ExtbaseTeam\BlogExample\Domain\Repository\PostRepository $postRepository */
-        $postRepository = $this->objectManager->get(\ExtbaseTeam\BlogExample\Domain\Repository\PostRepository::class);
+        /** @var PostRepository $postRepository */
+        $postRepository = $this->objectManager->get(PostRepository::class);
         $post = $postRepository->findByUid(1);
         $this->assertSame(3, count($post->getCategories()));
     }
@@ -879,8 +888,8 @@ class RelationTest extends \TYPO3\TestingFramework\Core\Functional\FunctionalTes
      */
     public function mmRelationWithMatchFieldIsResolvedFromForeignSide()
     {
-        /** @var \ExtbaseTeam\BlogExample\Domain\Repository\PostRepository $postRepository */
-        $postRepository = $this->objectManager->get(\ExtbaseTeam\BlogExample\Domain\Repository\PostRepository::class);
+        /** @var PostRepository $postRepository */
+        $postRepository = $this->objectManager->get(PostRepository::class);
         $posts = $postRepository->findByCategory(1);
         $this->assertSame(2, count($posts));
 
@@ -916,8 +925,8 @@ class RelationTest extends \TYPO3\TestingFramework\Core\Functional\FunctionalTes
             ->fetchColumn(0);
         $this->assertEquals(3, $countCategories);
 
-        /** @var \ExtbaseTeam\BlogExample\Domain\Repository\PostRepository $postRepository */
-        $postRepository = $this->objectManager->get(\ExtbaseTeam\BlogExample\Domain\Repository\PostRepository::class);
+        /** @var PostRepository $postRepository */
+        $postRepository = $this->objectManager->get(PostRepository::class);
         $post = $postRepository->findByUid(1);
 
         /** @var \TYPO3\CMS\Extbase\Domain\Model\Category $newCategory */
@@ -958,9 +967,9 @@ class RelationTest extends \TYPO3\TestingFramework\Core\Functional\FunctionalTes
      */
     public function adjustingMmRelationWithTablesnameAndFieldnameFieldDoNotTouchOtherRelations()
     {
-        /** @var \ExtbaseTeam\BlogExample\Domain\Repository\PostRepository $postRepository */
-        $postRepository = $this->objectManager->get(\ExtbaseTeam\BlogExample\Domain\Repository\PostRepository::class);
-        /** @var \ExtbaseTeam\BlogExample\Domain\Model\Post $post */
+        /** @var PostRepository $postRepository */
+        $postRepository = $this->objectManager->get(PostRepository::class);
+        /** @var Post $post */
         $post = $postRepository->findByUid(1);
         // Move category down
         foreach ($post->getCategories() as $category) {
@@ -1001,13 +1010,373 @@ class RelationTest extends \TYPO3\TestingFramework\Core\Functional\FunctionalTes
     }
 
     /**
+     * @return array
+     */
+    public function distinctDataProvider()
+    {
+        return [
+            'order default' => [
+                []
+            ],
+            'order default, offset 0' => [
+                [
+                    'offset' => 0
+                ]
+            ],
+            'order default, limit 100' => [
+                [
+                    'limit' => 100
+                ]
+            ],
+            'order default, offset 0, limit 100' => [
+                [
+                    'offset' => 0,
+                    'limit' => 100
+                ]
+            ],
+            'order false' => [
+                [
+                    'order' => false
+                ]
+            ],
+            'order false, offset 0' => [
+                [
+                    'order' => false,
+                    'offset' => 0
+                ]
+            ],
+            'order false, limit 100' => [
+                [
+                    'order' => false, 'limit' => 100
+                ]
+            ],
+            'order false, offset 0, limit 100' => [
+                [
+                    'order' => false,
+                    'offset' => 0,
+                    'limit' => 100
+                ]
+            ],
+            'order uid, offset 0' => [
+                [
+                    'order' => ['uid' => QueryInterface::ORDER_ASCENDING],
+                    'offset' => 0
+                ]
+            ],
+            'order uid, limit 100' => [
+                [
+                    'order' => ['uid' => QueryInterface::ORDER_ASCENDING],
+                    'limit' => 100
+                ]
+            ],
+            'order uid, offset 0, limit 100' => [
+                [
+                    'order' => ['uid' => QueryInterface::ORDER_ASCENDING],
+                    'offset' => 0,
+                    'limit' => 100
+                ]
+            ],
+        ];
+    }
+
+    /**
+     * @param QueryInterface $query
+     * @param array $queryRequest
+     */
+    protected function applyQueryRequest(QueryInterface $query, array $queryRequest)
+    {
+        if (isset($queryRequest['order']) && !$queryRequest['order']) {
+            $query->setOrderings([]);
+        } elseif (!empty($queryRequest['order'])) {
+            $query->setOrderings($queryRequest['order']);
+        }
+        if (isset($queryRequest['offset'])) {
+            $query->setOffset($queryRequest['offset']);
+        }
+        if (isset($queryRequest['limit'])) {
+            $query->setLimit($queryRequest['limit']);
+        }
+    }
+
+    /**
+     * Addresses ColumnMap::RELATION_HAS_ONE relations.
+     *
+     * @param $queryRequest
+     *
+     * @test
+     * @dataProvider distinctDataProvider
+     */
+    public function distinctPersonEntitiesAreFoundByPublisher(array $queryRequest)
+    {
+        $query = $this->provideFindPostsByPublisherQuery(1);
+        $this->applyQueryRequest($query, $queryRequest);
+        $posts = $query->execute();
+        $postCount = $posts->count();
+
+        $postIds = $this->resolveEntityIds($posts->toArray());
+
+        $this->assertEquals($this->countDistinctIds($postIds), $postCount);
+        $this->assertDistinctIds($postIds);
+    }
+
+    /**
+     * Addresses ColumnMap::RELATION_HAS_ONE relations.
+     *
+     * @param $queryRequest
+     *
+     * @test
+     * @dataProvider distinctDataProvider
+     */
+    public function distinctPersonRecordsAreFoundByPublisher(array $queryRequest)
+    {
+        $query = $this->provideFindPostsByPublisherQuery(1);
+        $this->applyQueryRequest($query, $queryRequest);
+        $postRecords = $query->execute(true);
+        $postIds = $this->resolveRecordIds($postRecords);
+
+        $this->assertDistinctIds($postIds);
+    }
+
+    /**
+     * @param int $publisherId
+     * @return QueryInterface
+     */
+    protected function provideFindPostsByPublisherQuery(int $publisherId)
+    {
+        $postRepository = $this->objectManager->get(PostRepository::class);
+        $query = $postRepository->createQuery();
+        $query->matching(
+            $query->logicalOr([
+                $query->equals('author.uid', $publisherId),
+                $query->equals('reviewer.uid', $publisherId)
+            ])
+        );
+        return $query;
+    }
+
+    /**
+     * Addresses ColumnMap::RELATION_HAS_MANY relations.
+     *
+     * @param $queryRequest
+     *
+     * @test
+     * @dataProvider distinctDataProvider
+     */
+    public function distinctBlogEntitiesAreFoundByPostsSince(array $queryRequest)
+    {
+        $query = $this->provideFindBlogsByPostsSinceQuery(
+            new \DateTime('2017-08-01')
+        );
+        $this->applyQueryRequest($query, $queryRequest);
+        $blogs = $query->execute();
+        $blogCount = $blogs->count();
+
+        $blogIds = $this->resolveEntityIds($blogs->toArray());
+
+        $this->assertEquals($this->countDistinctIds($blogIds), $blogCount);
+        $this->assertDistinctIds($blogIds);
+    }
+
+    /**
+     * Addresses ColumnMap::RELATION_HAS_MANY relations.
+     *
+     * @param $queryRequest
+     *
+     * @test
+     * @dataProvider distinctDataProvider
+     */
+    public function distinctBlogRecordsAreFoundByPostsSince(array $queryRequest)
+    {
+        $query = $this->provideFindBlogsByPostsSinceQuery(
+            new \DateTime('2017-08-01')
+        );
+        $this->applyQueryRequest($query, $queryRequest);
+        $blogRecords = $query->execute(true);
+        $blogIds = $this->resolveRecordIds($blogRecords);
+
+        $this->assertDistinctIds($blogIds);
+    }
+
+    /**
+     * @param \DateTime $date
+     * @return QueryInterface
+     */
+    protected function provideFindBlogsByPostsSinceQuery(\DateTime $date)
+    {
+        $blogRepository = $this->objectManager->get(BlogRepository::class);
+        $query = $blogRepository->createQuery();
+        $query->matching(
+            $query->greaterThanOrEqual('posts.date', $date)
+        );
+        return $query;
+    }
+
+    /**
+     * Addresses ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY relations.
+     *
+     * @param $queryRequest
+     *
+     * @test
+     * @dataProvider distinctDataProvider
+     */
+    public function distinctPersonEntitiesAreFoundByTagNameAreFiltered(array $queryRequest)
+    {
+        $query = $this->provideFindPersonsByTagNameQuery('SharedTag');
+        $this->applyQueryRequest($query, $queryRequest);
+        $persons = $query->execute();
+        $personCount = $persons->count();
+
+        $personIds = $this->resolveEntityIds($persons->toArray());
+
+        $this->assertEquals($this->countDistinctIds($personIds), $personCount);
+        $this->assertDistinctIds($personIds);
+    }
+
+    /**
+     * Addresses ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY relations.
+     *
+     * @param $queryRequest
+     *
+     * @test
+     * @dataProvider distinctDataProvider
+     */
+    public function distinctPersonRecordsAreFoundByTagNameAreFiltered(array $queryRequest)
+    {
+        $query = $this->provideFindPersonsByTagNameQuery('SharedTag');
+        $this->applyQueryRequest($query, $queryRequest);
+        $personRecords = $query->execute(true);
+        $personIds = $this->resolveRecordIds($personRecords);
+
+        $this->assertDistinctIds($personIds);
+    }
+
+    /**
+     * @param string $tagName
+     * @return QueryInterface
+     */
+    protected function provideFindPersonsByTagNameQuery(string $tagName)
+    {
+        $personRepository = $this->objectManager->get(PersonRepository::class);
+        $query = $personRepository->createQuery();
+        $query->matching(
+            $query->logicalOr([
+                $query->equals('tags.name', $tagName),
+                $query->equals('tagsSpecial.name', $tagName)
+            ])
+        );
+        return $query;
+    }
+
+    /**
+     * Addresses ColumnMap::RELATION_HAS_ONE, ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY relations.
+     *
+     * @param $queryRequest
+     *
+     * @test
+     * @dataProvider distinctDataProvider
+     */
+    public function distinctPostEntitiesAreFoundByAuthorTagNameAreFiltered(array $queryRequest)
+    {
+        $query = $this->provideFindPostsByAuthorTagName('SharedTag');
+        $this->applyQueryRequest($query, $queryRequest);
+        $posts = $query->execute();
+        $postCount = $posts->count();
+
+        $postsIds = $this->resolveEntityIds($posts->toArray());
+
+        $this->assertEquals($this->countDistinctIds($postsIds), $postCount);
+        $this->assertDistinctIds($postsIds);
+    }
+
+    /**
+     * Addresses ColumnMap::RELATION_HAS_ONE, ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY relations.
+     *
+     * @param $queryRequest
+     *
+     * @test
+     * @dataProvider distinctDataProvider
+     */
+    public function distinctPostRecordsAreFoundByAuthorTagNameAreFiltered(array $queryRequest)
+    {
+        $query = $this->provideFindPostsByAuthorTagName('SharedTag');
+        $this->applyQueryRequest($query, $queryRequest);
+        $postRecords = $query->execute(true);
+        $postsIds = $this->resolveRecordIds($postRecords);
+
+        $this->assertDistinctIds($postsIds);
+    }
+
+    /**
+     * @param string $tagName
+     * @return QueryInterface
+     */
+    protected function provideFindPostsByAuthorTagName(string $tagName)
+    {
+        $postRepository = $this->objectManager->get(PostRepository::class);
+        $query = $postRepository->createQuery();
+        $query->matching(
+            $query->logicalOr([
+                $query->equals('author.tags.name', $tagName),
+                $query->equals('author.tagsSpecial.name', $tagName)
+            ])
+        );
+        return $query;
+    }
+
+    /**
      * Helper method for persisting blog
      */
     protected function updateAndPersistBlog()
     {
-        /** @var \ExtbaseTeam\BlogExample\Domain\Repository\BlogRepository $blogRepository */
-        $blogRepository = $this->objectManager->get(\ExtbaseTeam\BlogExample\Domain\Repository\BlogRepository::class);
+        /** @var BlogRepository $blogRepository */
+        $blogRepository = $this->objectManager->get(BlogRepository::class);
         $blogRepository->update($this->blog);
         $this->persistentManager->persistAll();
     }
+
+    /**
+     * @param AbstractEntity[] $entities
+     * @return int[]
+     */
+    protected function resolveEntityIds(array $entities)
+    {
+        return array_map(
+            function (AbstractEntity $entity) {
+                return $entity->getUid();
+            },
+            $entities
+        );
+    }
+
+    /**
+     * @param array $records
+     * @return int[]
+     */
+    protected function resolveRecordIds(array $records)
+    {
+        return array_column($records, 'uid');
+    }
+
+    /**
+     * Counts amount of distinct IDS.
+     *
+     * @param array $ids
+     * @return int
+     */
+    protected function countDistinctIds(array $ids)
+    {
+        return count(array_count_values($ids));
+    }
+
+    /**
+     * Asserts distinct IDs by comparing the sum of the occurrence of
+     * a particular ID to the amount of existing distinct IDs.
+     *
+     * @param array $ids
+     */
+    protected function assertDistinctIds(array $ids)
+    {
+        $counts = array_count_values($ids);
+        $this->assertEquals(count($counts), array_sum($counts));
+    }
 }