diff --git a/system/BaseModel.php b/system/BaseModel.php index ee3d1a859955..b9d21d5a974c 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -582,6 +582,7 @@ abstract public function countAllResults(bool $reset = true, bool $test = false) * @return void * * @throws DataException + * @throws InvalidArgumentException if $size is not a positive integer */ abstract public function chunk(int $size, Closure $userFunc); diff --git a/system/Model.php b/system/Model.php index dc3e4db94db2..307d66a52cb6 100644 --- a/system/Model.php +++ b/system/Model.php @@ -21,6 +21,7 @@ use CodeIgniter\Database\Exceptions\DataException; use CodeIgniter\Entity\Entity; use CodeIgniter\Exceptions\BadMethodCallException; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Exceptions\ModelException; use CodeIgniter\Validation\ValidationInterface; use Config\Database; @@ -533,10 +534,14 @@ public function countAllResults(bool $reset = true, bool $test = false) */ public function chunk(int $size, Closure $userFunc) { + if ($size <= 0) { + throw new InvalidArgumentException('chunk() requires a positive integer for the $size argument.'); + } + $total = $this->builder()->countAllResults(false); $offset = 0; - while ($offset <= $total) { + while ($offset < $total) { $builder = clone $this->builder(); $rows = $builder->get($size, $offset); diff --git a/tests/system/Models/MiscellaneousModelTest.php b/tests/system/Models/MiscellaneousModelTest.php index 556c1b441c18..e95a709e61bd 100644 --- a/tests/system/Models/MiscellaneousModelTest.php +++ b/tests/system/Models/MiscellaneousModelTest.php @@ -14,6 +14,8 @@ namespace CodeIgniter\Models; use CodeIgniter\Database\Exceptions\DataException; +use CodeIgniter\Events\Events; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\I18n\Time; use PHPUnit\Framework\Attributes\Group; use Tests\Support\Models\EntityModel; @@ -39,6 +41,62 @@ public function testChunk(): void $this->assertSame(4, $rowCount); } + public function testChunkThrowsOnZeroSize(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('chunk() requires a positive integer for the $size argument.'); + + $this->createModel(UserModel::class)->chunk(0, static function ($row): void {}); + } + + public function testChunkThrowsOnNegativeSize(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('chunk() requires a positive integer for the $size argument.'); + + $this->createModel(UserModel::class)->chunk(-1, static function ($row): void {}); + } + + public function testChunkEarlyExit(): void + { + $rowCount = 0; + + $this->createModel(UserModel::class)->chunk(2, static function ($row) use (&$rowCount): bool { + $rowCount++; + + return false; + }); + + $this->assertSame(1, $rowCount); + } + + public function testChunkDoesNotRunExtraQuery(): void + { + $queryCount = 0; + $listener = static function () use (&$queryCount): void { + $queryCount++; + }; + + Events::on('DBQuery', $listener); + $this->createModel(UserModel::class)->chunk(4, static function ($row): void {}); + Events::removeListener('DBQuery', $listener); + + $this->assertSame(2, $queryCount); + } + + public function testChunkEmptyTable(): void + { + $this->db->table('user')->truncate(); + + $rowCount = 0; + + $this->createModel(UserModel::class)->chunk(2, static function ($row) use (&$rowCount): void { + $rowCount++; + }); + + $this->assertSame(0, $rowCount); + } + public function testCanCreateAndSaveEntityClasses(): void { $entity = $this->createModel(EntityModel::class)->where('name', 'Developer')->first(); diff --git a/user_guide_src/source/changelogs/v4.7.1.rst b/user_guide_src/source/changelogs/v4.7.1.rst index 5f2ca988ce63..848be1a93a86 100644 --- a/user_guide_src/source/changelogs/v4.7.1.rst +++ b/user_guide_src/source/changelogs/v4.7.1.rst @@ -41,6 +41,7 @@ Bugs Fixed - **ContentSecurityPolicy:** Fixed a bug where custom CSP tags were not removed from generated HTML when CSP was disabled. The method now ensures that all custom CSP tags are removed from the generated HTML. - **ContentSecurityPolicy:** Fixed a bug where ``generateNonces()`` produces corrupted JSON responses by replacing CSP nonce placeholders with unescaped double quotes. The method now automatically JSON-escapes nonce attributes when the response Content-Type is JSON. - **Model:** Fixed a bug where ``BaseModel::updateBatch()`` threw an exception when ``updateOnlyChanged`` was ``true`` and the index field value did not change. +- **Model:** Fixed a bug where ``Model::chunk()`` ran an unnecessary extra database query at the end of iteration. ``chunk()`` now also throws ``InvalidArgumentException`` when called with a non-positive chunk size. - **Session:** Fixed a bug in ``MemcachedHandler`` where the constructor incorrectly threw an exception when ``savePath`` was not empty. - **Toolbar:** Fixed a bug where the standalone toolbar page loaded from ``?debugbar_time=...`` was not interactive.