From 5ae9b28a6069d995b6451ba238f5f9c9d50ec7c3 Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Wed, 30 Sep 2026 11:22:07 +0200 Subject: [PATCH] fix(task) #30 DatabaseReaderTask no longer drops the row following each full page with the paginate option Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 3 + docs/reference/tasks/database_reader_task.md | 1 - src/Task/Database/DatabaseReaderTask.php | 7 +-- .../Task/Database/DatabaseReaderTaskTest.php | 62 +++++++++++++++++++ 4 files changed, 68 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d822c67..d7ab2d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ Latest * [#26](https://github.com/cleverage/doctrine-process-bundle/issues/26) Update quality stack: use Rector `withComposerBased()` sets (removed `SYMFONY_64` / `PHPUNIT_100` sets), declare used Symfony packages and PHPUnit range in composer.json, apply quality tools fixes * [#28](https://github.com/cleverage/doctrine-process-bundle/issues/28) Add missing documentations: complete reference pages for every Task (inherited options, iterable/flushable behaviours, examples, notes), Database to CSV export and CSV to entities import cookbooks. Harmonize index and task template, fix existing documentation. +### Fixes +* [#30](https://github.com/cleverage/doctrine-process-bundle/issues/30) DatabaseReaderTask no longer drops the row following each full page with the `paginate` option + v3.0 ------ diff --git a/docs/reference/tasks/database_reader_task.md b/docs/reference/tasks/database_reader_task.md index 873fe4f..9a7c770 100644 --- a/docs/reference/tasks/database_reader_task.md +++ b/docs/reference/tasks/database_reader_task.md @@ -110,4 +110,3 @@ Notes `ids: !php/enum Doctrine\DBAL\ArrayParameterType::INTEGER` with Doctrine DBAL 4. * The task is designed to be executed once per process run (typically as the entry point): if it receives a new input after having iterated over all the rows, that input only resets the task, which is skipped. -* With `paginate`, the row following each full page is currently not output. diff --git a/src/Task/Database/DatabaseReaderTask.php b/src/Task/Database/DatabaseReaderTask.php index 9e1e430..6718e32 100644 --- a/src/Task/Database/DatabaseReaderTask.php +++ b/src/Task/Database/DatabaseReaderTask.php @@ -99,11 +99,10 @@ public function execute(ProcessState $state): void } if (null !== $options['paginate']) { - $results = []; - $i = 0; - while (false !== $result && $i++ < $options['paginate']) { + $results = [$result]; + // Stop fetching as soon as the page is full: a row fetched beyond it would be lost + while (\count($results) < $options['paginate'] && false !== ($result = $this->statement->fetchAssociative())) { $results[] = $result; - $result = $this->statement->fetchAssociative(); } $state->setOutput($results); } else { diff --git a/tests/Task/Database/DatabaseReaderTaskTest.php b/tests/Task/Database/DatabaseReaderTaskTest.php index 82b7c98..9de29aa 100644 --- a/tests/Task/Database/DatabaseReaderTaskTest.php +++ b/tests/Task/Database/DatabaseReaderTaskTest.php @@ -140,6 +140,68 @@ protected function getConnection(?ProcessState $state = null): Connection $task->execute($state); } + public function testExecuteWithPaginationDoesNotLoseRows(): void + { + $state = $this->createStub(ProcessState::class); + $options = [ + 'table' => 'my_table', + 'sql' => 'SELECT * FROM my_table', + 'limit' => null, + 'offset' => null, + 'paginate' => 2, + 'input_as_params' => false, + 'params' => [], + 'types' => [], + 'empty_log_level' => LogLevel::WARNING, + 'connection' => null, + ]; + + $resultData1 = ['id' => 1, 'name' => 'test1']; + $resultData2 = ['id' => 2, 'name' => 'test2']; + $resultData3 = ['id' => 3, 'name' => 'test3']; + $result = $this->createStub(Result::class); + $result->method('fetchAssociative')->willReturnOnConsecutiveCalls($resultData1, $resultData2, $resultData3, false, false); + + $connection = $this->createStub(Connection::class); + $connection->method('executeQuery')->willReturn($result); + + $task = new class($this->logger, $this->doctrine, $options, $connection) extends DatabaseReaderTask { + /** + * @param array $testOptions + */ + public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine, private readonly array $testOptions, private readonly Connection $testConnection) + { + parent::__construct($logger, $doctrine); + } + + /** + * @return array + */ + protected function getOptions(?ProcessState $state = null): array + { + return $this->testOptions; + } + + protected function getConnection(?ProcessState $state = null): Connection + { + return $this->testConnection; + } + }; + + $outputs = []; + $state->method('setOutput')->willReturnCallback(static function (mixed $output) use (&$outputs): void { + $outputs[] = $output; + }); + + $task->initialize($state); + $task->execute($state); + self::assertTrue($task->next($state)); + $task->execute($state); + self::assertFalse($task->next($state)); + + self::assertSame([[$resultData1, $resultData2], [$resultData3]], $outputs); + } + public function testFinalize(): void { $task = new DatabaseReaderTask($this->logger, $this->doctrine);