orm: Paginator breaks when DQL query is re-used with different identifier parameters that require conversion

Bug Report

Q A
BC Break no
Version 2.6.4

Summary

#7820 and #7821 fix pagination issues happening when the paginator is relying on identifiers that require custom DBAL type conversion.

The type conversion works, but since it has been performed inside the WhereInWalker (to keep things a bit simpler), it is only performed when the query is in Doctrine\ORM\Query::STATE_DIRTY.

If the parameters are changed on a Doctrine\ORM\Query, the query doesn’t change parser state, and therefore the WhereInWalker is completely skipped.

I will try to write a small reproducer and fix ASAP, hoping to not cause too big performance regressions in the ORM. I think it may come down to forcing query parsing to be repeated.

Current behavior

$id = generateCustomId();
$a  = new A($id);

$this->em->persist($a);
$this->em->flush();

$query = $this->em->createQuery('SELECT a FROM ' . A::class);

$paginator = new Paginator($query);

$query->setFirstResult(1);

Assert::count(0, \iterator_to_array($paginator));

$query->setFirstResult(0);

Assert::same([$a], \iterator_to_array($paginator));

Expected behavior

The last assertion in the test above fails: this is an unfortunate case of mutable state shared inside Doctrine\ORM\Query.

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 1
  • Comments: 20 (19 by maintainers)

Commits related to this issue

Most upvoted comments

Ok, the bug is related to caching: the WhereInWalker is only hit if the query is not present in the query cache.

If there’s a cache hit for the parser result, then the walker is not reached, and parameters aren’t converted.

I’ll write up a test case tonight.

I will try to restrict this to a more specific use-case 👌

On Wed, Oct 2, 2019, 00:55 Luís Cobucci notifications@github.com wrote:

@Ocramius https://github.com/Ocramius this is an interesting one (which I didn’t manage to reproduce in the way you described)…

As far as I’ve seen the Paginator doesn’t influence the original query state because it clones it before applying the changes.

However, things get a bit weird when using (and changing) a query builder instead of a query object. This happens because QueryBuilder#getQuery() always returns a new query object:

<?phpdeclare(strict_types=1);namespace Doctrine\Tests\ORM\Functional\Ticket;use Doctrine\Common\Collections\Criteria;use Doctrine\DBAL\Platforms\AbstractPlatform;use Doctrine\DBAL\Types\StringType;use Doctrine\DBAL\Types\Type;use Doctrine\ORM\Tools\Pagination\Paginator;use Doctrine\Tests\OrmFunctionalTestCase;use function is_string;use function iterator_to_array;/** * @group GH7837 */final class GH7837Test extends OrmFunctionalTestCase{ private const SONG = [ 'What is this song all about?', 'Can\'t figure any lyrics out', 'How do the words to it go?', 'I wish you\'d tell me, I don\'t know', 'Don\'t know, don\'t know, don\'t know, I don\'t know!', 'Don\'t know, don\'t know, don\'t know...', ]; protected function setUp() : void { parent::setUp(); if (! Type::hasType(GH7837LineTextType::class)) { Type::addType(GH7837LineTextType::class, GH7837LineTextType::class); } $this->setUpEntitySchema([GH7837Line::class, GH7837LineAuthor::class]); foreach (self::SONG as $index => $line) { $this->_em->persist(new GH7837Line(GH7837LineText::fromText($line), $index)); } $this->_em->flush(); } /** @test */ public function paginatorWithQueryBuilder() : void // fails { $query = $this->_em->getRepository(GH7837Line::class) ->createQueryBuilder('l') ->orderBy('l.lineNumber', Criteria::ASC); $paginator = new Paginator($query); $query->setFirstResult(6); self::assertCount(0, iterator_to_array($paginator)); $query->setFirstResult(5); $result = iterator_to_array($paginator); self::assertCount(1, $result); self::assertSame(self::SONG[5], $result[0]->toString()); } /** @test */ public function paginatorWithQueryFromQueryBuilder() : void // passes { $query = $this->_em->getRepository(GH7837Line::class) ->createQueryBuilder('l') ->orderBy('l.lineNumber', Criteria::ASC) ->getQuery(); $paginator = new Paginator($query); $query->setFirstResult(6); self::assertCount(0, iterator_to_array($paginator)); $query->setFirstResult(5); $result = iterator_to_array($paginator); self::assertCount(1, $result); self::assertSame(self::SONG[5], $result[0]->toString()); } /** @test */ public function paginatorWithDQL() : void // passes { $query = $this->_em->createQuery('SELECT l FROM ' . GH7837Line::class . ' l ORDER BY l.lineNumber ASC'); $paginator = new Paginator($query); $query->setFirstResult(6); self::assertCount(0, iterator_to_array($paginator)); $query->setFirstResult(5); $result = iterator_to_array($paginator); self::assertCount(1, $result); self::assertSame(self::SONG[5], $result[0]->toString()); }}/** @Entity */class GH7837LineAuthor{ /** * @Id * @Column(type="integer") * * @var int */ public $id; /** * @Column * * @var string */ public $name; /** * @ManyToOne(targetEntity=GH7837Line::class) * @JoinColumn(referencedColumnName="text") * * @var GH7837Line */ public $line; public function __construct(int $id, string $name, GH7837Line $line) { $this->id = $id; $this->name = $name; $this->line = $line; }}/** @Entity */class GH7837Line{ /** * @var GH7837LineText * @Id() * @Column(type="Doctrine\Tests\ORM\Functional\Ticket\GH7837LineTextType") */ private $text; /** * @var int * @Column(type="integer") */ private $lineNumber; public function __construct(GH7837LineText $text, int $index) { $this->text = $text; $this->lineNumber = $index; } public function toString() : string { return $this->text->getText(); }}final class GH7837LineText{ /** @var string */ private $text; private function __construct(string $text) { $this->text = $text; } public static function fromText(string $text) : self { return new self($text); } public function getText() : string { return $this->text; } public function __toString() : string { return 'Line: ' . $this->text; }}final class GH7837LineTextType extends StringType{ public function convertToPHPValue($value, AbstractPlatform $platform) { $text = parent::convertToPHPValue($value, $platform); if (! is_string($text)) { return $text; } return GH7837LineText::fromText($text); } public function convertToDatabaseValue($value, AbstractPlatform $platform) { if (! $value instanceof GH7837LineText) { return parent::convertToDatabaseValue($value, $platform); } return parent::convertToDatabaseValue($value->getText(), $platform); } /** {@inheritdoc} */ public function getName() : string { return self::class; }} — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub <https://github.com/doctrine/orm/issues/7837?email_source=notifications&email_token=AABFVEAQ3TLQJI54BLDJ7E3QMPIOXA5CNFSM4I3JPGWKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEADAKVI#issuecomment-537265493>, or mute the thread <https://github.com/notifications/unsubscribe-auth/AABFVEGNSZTGAPIBQTKPKALQMPIOXANCNFSM4I3JPGWA> .