diff --git a/lib/Doctrine/ORM/Query/ResultSetMapping.php b/lib/Doctrine/ORM/Query/ResultSetMapping.php index 8e4f766bd65..0b4f5763b16 100644 --- a/lib/Doctrine/ORM/Query/ResultSetMapping.php +++ b/lib/Doctrine/ORM/Query/ResultSetMapping.php @@ -19,6 +19,12 @@ namespace Doctrine\ORM\Query; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Mapping\MappingException; +use function array_keys; +use function array_values; +use function assert; + /** * A ResultSetMapping describes how a result set of an SQL query maps to a Doctrine result. * @@ -583,5 +589,39 @@ public function addMetaResult($alias, $columnName, $fieldName, $isIdentifierColu return $this; } -} + /** + * Retrieves the DBAL type name for the single identifier column of the root of a selection. + * Composite identifiers not supported! + * + * @internal only to be used by ORM internals: do not use in downstream projects! This API is a minimal abstraction + * that only ORM internals need, and it tries to make sense of the very complex and squishy array-alike + * structure inside this class. Some assumptions are coded in here, so here be dragons. + * @throws MappingException If the identifier is not a single field, or if metadata for its + * owner is incorrect/missing. + */ + final public function getTypeOfSelectionRootSingleIdentifierColumn(EntityManagerInterface $em) : string + { + assert($this->isSelect); + + if ($this->isIdentifierColumn !== []) { + // Identifier columns are already discovered here: we can use the first one directly. + assert($this->typeMappings !== []); + + return $this->typeMappings[array_keys(array_values($this->isIdentifierColumn)[0])[0]]; + } + + // We are selecting entities, and the first selected entity is our root of the selection. + if ($this->aliasMap !== []) { + $metadata = $em->getClassMetadata($this->aliasMap[array_keys($this->aliasMap)[0]]); + + return $metadata->getTypeOfField($metadata->getSingleIdentifierFieldName()); + } + + // We are selecting scalar fields - the first selected field will be assumed (!!! assumption !!!) as identifier + assert($this->scalarMappings !== []); + assert($this->typeMappings !== []); + + return $this->typeMappings[array_keys($this->scalarMappings)[0]]; + } +} diff --git a/lib/Doctrine/ORM/Tools/Pagination/Paginator.php b/lib/Doctrine/ORM/Tools/Pagination/Paginator.php index 0987dfc3492..892c65bbf35 100644 --- a/lib/Doctrine/ORM/Tools/Pagination/Paginator.php +++ b/lib/Doctrine/ORM/Tools/Pagination/Paginator.php @@ -19,11 +19,16 @@ namespace Doctrine\ORM\Tools\Pagination; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Mapping\MappingException; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\Query; use Doctrine\ORM\Query\ResultSetMapping; use Doctrine\ORM\NoResultException; +use function array_map; +use function assert; +use function current; /** * The paginator can handle various complex scenarios with DQL. @@ -150,14 +155,21 @@ public function getIterator() $subQuery->setFirstResult($offset)->setMaxResults($length); - $ids = array_map('current', $subQuery->getScalarResult()); + $foundIdRows = $subQuery->getScalarResult(); - $whereInQuery = $this->cloneQuery($this->query); // don't do this for an empty id array - if (count($ids) === 0) { + if ($foundIdRows === []) { return new \ArrayIterator([]); } + $whereInQuery = $this->cloneQuery($this->query); + $em = $subQuery->getEntityManager(); + $connection = $em->getConnection(); + $idType = $this->getIdentifiersQueryScalarResultType($subQuery, $em); + $ids = array_map(static function (array $row) use ($connection, $idType) { + return $connection->convertToDatabaseValue(current($row), $idType); + }, $foundIdRows); + $this->appendTreeWalker($whereInQuery, WhereInWalker::class); $whereInQuery->setHint(WhereInWalker::HINT_PAGINATOR_ID_COUNT, count($ids)); $whereInQuery->setFirstResult(null)->setMaxResults(null); @@ -282,4 +294,25 @@ private function unbindUnusedQueryParams(Query $query): void $query->setParameters($parameters); } + + /** + * Parses a query that is supposed to fetch a set of entity identifier only, + * and retrieves the type of said identifier. + * + * @throws MappingException If metadata couldn't be loaded, or if there isn't a single + * identifier for the given query. + */ + private function getIdentifiersQueryScalarResultType( + Query $query, + EntityManagerInterface $em + ) : ?string { + $rsm = (new Parser($query)) + ->parse() + ->getResultSetMapping(); + + assert($rsm !== null); + assert($rsm->isSelect); + + return $rsm->getTypeOfSelectionRootSingleIdentifierColumn($em); + } } diff --git a/tests/Doctrine/Tests/Models/ValueConversionType/OwningManyToOneCompositeIdForeignKeyEntity.php b/tests/Doctrine/Tests/Models/ValueConversionType/OwningManyToOneCompositeIdForeignKeyEntity.php index b6787a41854..7b94a110d73 100644 --- a/tests/Doctrine/Tests/Models/ValueConversionType/OwningManyToOneCompositeIdForeignKeyEntity.php +++ b/tests/Doctrine/Tests/Models/ValueConversionType/OwningManyToOneCompositeIdForeignKeyEntity.php @@ -1,5 +1,7 @@ assertEquals('status', $this->_rsm->getFieldName('status')); $this->assertEquals('username', $this->_rsm->getFieldName('username')); $this->assertEquals('name', $this->_rsm->getFieldName('name')); + $this->assertSame('integer', $this->_rsm->getTypeOfSelectionRootSingleIdentifierColumn($this->_em)); } /** @@ -94,6 +98,7 @@ public function testFluentInterface() $this->assertTrue($rms->isRelation('p')); $this->assertTrue($rms->hasParentAlias('p')); $this->assertTrue($rms->isMixedResult()); + $this->assertSame('integer', $this->_rsm->getTypeOfSelectionRootSingleIdentifierColumn($this->_em)); } /** @@ -269,6 +274,27 @@ public function testAddNamedNativeQueryResultClass() $this->assertEquals(CmsUser::class, $rsm->getDeclaringClass('status')); $this->assertEquals(CmsUser::class, $rsm->getDeclaringClass('username')); } + + public function testIdentifierTypeForScalarExpression() : void + { + $rsm = (new Parser($this->_em->createQuery('SELECT e.id4 FROM ' . AuxiliaryEntity::class . ' e'))) + ->parse() + ->getResultSetMapping(); + + self::assertNotNull($rsm); + self::assertSame('rot13', $rsm->getTypeOfSelectionRootSingleIdentifierColumn($this->_em)); + } + + public function testIdentifierTypeForRootEntityColumnThatHasAssociationAsIdentifier() : void + { + $rsm = (new Parser($this->_em->createQuery('SELECT e FROM ' . OwningManyToOneIdForeignKeyEntity::class . ' e'))) + ->parse() + ->getResultSetMapping(); + + self::assertNotNull($rsm); + self::assertSame('rot13', $rsm->getTypeOfSelectionRootSingleIdentifierColumn($this->_em)); + } + /** * @group DDC-117 */