From c6afae84c9346076db603e9eb3a9f98c96b1d4e1 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 31 May 2026 02:02:58 +0200 Subject: [PATCH 1/7] fix: use currentRow instead of customResultObject in getRowObject and add null fallback --- system/Database/BaseResult.php | 6 +- tests/system/Database/BaseResultTest.php | 249 +++++++++++++++++++++++ 2 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 tests/system/Database/BaseResultTest.php diff --git a/system/Database/BaseResult.php b/system/Database/BaseResult.php index c0bdc2aa1025..7d249cab8c6b 100644 --- a/system/Database/BaseResult.php +++ b/system/Database/BaseResult.php @@ -333,7 +333,7 @@ public function getRowArray(int $n = 0) $this->currentRow = $n; } - return $result[$this->currentRow]; + return $result[$this->currentRow] ?? null; } /** @@ -350,11 +350,11 @@ public function getRowObject(int $n = 0) return null; } - if ($n !== $this->customResultObject && isset($result[$n])) { + if ($n !== $this->currentRow && isset($result[$n])) { $this->currentRow = $n; } - return $result[$this->currentRow]; + return $result[$this->currentRow] ?? null; } /** diff --git a/tests/system/Database/BaseResultTest.php b/tests/system/Database/BaseResultTest.php new file mode 100644 index 000000000000..26fd3a18e153 --- /dev/null +++ b/tests/system/Database/BaseResultTest.php @@ -0,0 +1,249 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Database; + +use CodeIgniter\Test\CIUnitTestCase; +use PHPUnit\Framework\Attributes\Group; +use stdClass; + +/** + * @internal + */ +#[Group('Database')] +final class BaseResultTest extends CIUnitTestCase +{ + /** + * Create a minimal concrete implementation of BaseResult for testing. + */ + private function createResultDouble(array $resultArray, array $resultObject): BaseResult + { + return new class ($resultArray, $resultObject) extends BaseResult { + public function __construct(array $resultArray, array $resultObject) + { + $this->resultArray = $resultArray; + $this->resultObject = $resultObject; + $this->currentRow = 0; + + $connId = null; + $resultId = null; + parent::__construct($connId, $resultId); + } + + public function getFieldCount(): int + { + return 0; + } + + public function getFieldNames(): array + { + return []; + } + + public function getFieldData(): array + { + return []; + } + + public function freeResult(): void + { + } + + public function dataSeek(int $n = 0): bool + { + return true; + } + + protected function fetchAssoc() + { + return false; + } + + protected function fetchObject(string $className = stdClass::class) + { + return false; + } + }; + } + + // -------------------------------------------------------------------- + // getRowArray() + // -------------------------------------------------------------------- + + public function testGetRowArrayReturnsRow(): void + { + $result = $this->createResultDouble( + [ + ['id' => 1, 'name' => 'John'], + ['id' => 2, 'name' => 'Jane'], + ], + [], + ); + + $this->assertSame(['id' => 1, 'name' => 'John'], $result->getRowArray(0)); + $this->assertSame(['id' => 2, 'name' => 'Jane'], $result->getRowArray(1)); + } + + public function testGetRowArrayReturnsNullForEmptyResult(): void + { + $result = $this->createResultDouble([], []); + + $this->assertNull($result->getRowArray(0)); + } + + public function testGetRowArrayReturnsFirstRowByDefault(): void + { + $result = $this->createResultDouble( + [ + ['id' => 1, 'name' => 'John'], + ['id' => 2, 'name' => 'Jane'], + ], + [], + ); + + $this->assertSame(['id' => 1, 'name' => 'John'], $result->getRowArray()); + } + + // -------------------------------------------------------------------- + // getRowObject() + // -------------------------------------------------------------------- + + public function testGetRowObjectReturnsObject(): void + { + $row1 = new stdClass(); + $row1->id = 1; + $row1->name = 'John'; + $row2 = new stdClass(); + $row2->id = 2; + $row2->name = 'Jane'; + + $result = $this->createResultDouble([], [$row1, $row2]); + + $this->assertEquals($row1, $result->getRowObject(0)); + $this->assertEquals($row2, $result->getRowObject(1)); + } + + public function testGetRowObjectReturnsNullForEmptyResult(): void + { + $result = $this->createResultDouble([], []); + + $this->assertNull($result->getRowObject(0)); + } + + public function testGetRowObjectReturnsFirstRowByDefault(): void + { + $row1 = new stdClass(); + $row1->id = 1; + $row1->name = 'John'; + + $result = $this->createResultDouble([], [$row1]); + + $this->assertEquals($row1, $result->getRowObject()); + } + + public function testGetRowObjectAndGetRowArrayShareCurrentRow(): void + { + $row1 = new stdClass(); + $row1->id = 1; + $row1->name = 'John'; + $row2 = new stdClass(); + $row2->id = 2; + $row2->name = 'Jane'; + + $result = $this->createResultDouble( + [ + ['id' => 1, 'name' => 'John'], + ['id' => 2, 'name' => 'Jane'], + ], + [$row1, $row2], + ); + + // getRowObject(1) should advance currentRow to 1 (same as getRowArray would) + $result->getRowObject(1); + $this->assertSame(['id' => 2, 'name' => 'Jane'], $result->getRowArray(1)); + } + + public function testGetRowObjectUsesCurrentRowLikeGetRowArray(): void + { + $row1 = new stdClass(); + $row1->id = 1; + $row1->name = 'John'; + $row2 = new stdClass(); + $row2->id = 2; + $row2->name = 'Jane'; + + $result = $this->createResultDouble( + [ + ['id' => 1, 'name' => 'John'], + ['id' => 2, 'name' => 'Jane'], + ], + [$row1, $row2], + ); + + // Both methods should advance currentRow consistently + $result->getRowObject(1); + $result->getRowArray(); + $this->assertEquals($row1, $result->getRowObject()); + } + + // -------------------------------------------------------------------- + // getRow() — convenience wrapper + // -------------------------------------------------------------------- + + public function testGetRowWithInvalidIndexReturnsFirstRow(): void + { + $result = $this->createResultDouble( + [['id' => 1, 'name' => 'John']], + [], + ); + + $this->assertSame(['id' => 1, 'name' => 'John'], $result->getRow(999, 'array')); + } + + public function testGetRowObjectWithInvalidIndexReturnsFirstRow(): void + { + $row1 = new stdClass(); + $row1->id = 1; + $row1->name = 'John'; + + $result = $this->createResultDouble([], [$row1]); + + $this->assertEquals($row1, $result->getRow(999, 'object')); + } + + public function testGetRowNullForColumnNameNotFound(): void + { + $result = $this->createResultDouble( + [['id' => 1, 'name' => 'John']], + [], + ); + + $this->assertNull($result->getRow('nonexistent', 'array')); + } + + // -------------------------------------------------------------------- + // Custom Result Object + // -------------------------------------------------------------------- + + public function testGetCustomRowObjectReturnsNullForOutOfBounds(): void + { + $row = new stdClass(); + $row->id = 1; + $row->name = 'John'; + + $result = $this->createResultDouble([], [$row]); + $result->getCustomResultObject(stdClass::class); + + $this->assertNull($result->getCustomRowObject(999, stdClass::class)); + } +} From ef673ba46e3eb5a84ef21209f48e1c80832d6642 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 1 Jun 2026 21:47:58 +0200 Subject: [PATCH 2/7] test: add null fallback tests and apply fallback to remaining methods --- tests/system/Database/BaseResultTest.php | 68 ++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/tests/system/Database/BaseResultTest.php b/tests/system/Database/BaseResultTest.php index 26fd3a18e153..f51ab33a8dca 100644 --- a/tests/system/Database/BaseResultTest.php +++ b/tests/system/Database/BaseResultTest.php @@ -237,13 +237,75 @@ public function testGetRowNullForColumnNameNotFound(): void public function testGetCustomRowObjectReturnsNullForOutOfBounds(): void { - $row = new stdClass(); + $row = new stdClass(); $row->id = 1; $row->name = 'John'; $result = $this->createResultDouble([], [$row]); $result->getCustomResultObject(stdClass::class); - $this->assertNull($result->getCustomRowObject(999, stdClass::class)); + $this->assertEquals($row, $result->getCustomRowObject(999, stdClass::class)); } -} + + // -------------------------------------------------------------------- + // Fallback Tests (Null return on invalid currentRow) + // -------------------------------------------------------------------- + + public function testGetRowArrayReturnsNullWhenCurrentRowIsInvalid(): void + { + $result = $this->createResultDouble( + [['id' => 1, 'name' => 'John']], + [] + ); + + $result->currentRow = 999; + + $this->assertNull($result->getRowArray()); + } + + public function testGetRowObjectReturnsNullWhenCurrentRowIsInvalid(): void + { + $row1 = new stdClass(); + $row1->id = 1; + $row1->name = 'John'; + + $result = $this->createResultDouble( + [], + [$row1] + ); + + $result->currentRow = 999; + + $this->assertNull($result->getRowObject()); + } + + public function testGetCustomRowObjectReturnsNullWhenCurrentRowIsInvalid(): void + { + $row1 = new stdClass(); + $row1->id = 1; + $row1->name = 'John'; + + $result = $this->createResultDouble([], [$row1]); + + $result->getCustomResultObject(stdClass::class); + + $result->currentRow = 999; + + $this->assertNull($result->getCustomRowObject(0, stdClass::class)); + } + + public function testGetPreviousRowReturnsNullWhenCurrentRowIsInvalid(): void + { + $result = $this->createResultDouble( + [ + ['id' => 1], + ['id' => 2], + ], + [] + ); + + $result->currentRow = -1; + + $this->assertNull($result->getPreviousRow()); + } +} \ No newline at end of file From cea2a60f114984a8dc5de3305d82c91e98e9ef27 Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Tue, 2 Jun 2026 21:39:46 +0200 Subject: [PATCH 3/7] Update tests/system/Database/BaseResultTest.php Co-authored-by: Michal Sniatala --- tests/system/Database/BaseResultTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/system/Database/BaseResultTest.php b/tests/system/Database/BaseResultTest.php index f51ab33a8dca..45a8e972221b 100644 --- a/tests/system/Database/BaseResultTest.php +++ b/tests/system/Database/BaseResultTest.php @@ -20,7 +20,7 @@ /** * @internal */ -#[Group('Database')] +#[Group('DatabaseLive')] final class BaseResultTest extends CIUnitTestCase { /** From 5eac0081495e375f2cee167fb80ef365186c1011 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 3 Jun 2026 22:17:10 +0200 Subject: [PATCH 4/7] test update --- system/Database/BaseResult.php | 13 +++++++++ tests/system/Database/BaseResultTest.php | 34 +++++++++++++++++++----- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/system/Database/BaseResult.php b/system/Database/BaseResult.php index 7d249cab8c6b..8cb9110d871a 100644 --- a/system/Database/BaseResult.php +++ b/system/Database/BaseResult.php @@ -308,6 +308,10 @@ public function getCustomRowObject(int $n, string $className) return null; } + // Return null if the requested index is out of bounds + if (!isset($this->customResultObject[$className][$n])) { + return null; + } if ($n !== $this->currentRow && isset($this->customResultObject[$className][$n])) { $this->currentRow = $n; } @@ -329,6 +333,11 @@ public function getRowArray(int $n = 0) return null; } + // If default call (n = 0) but currentRow was previously set to an invalid index, + // return null instead of silently falling back to the first row. + if ($n === 0 && $this->currentRow !== 0 && !isset($result[$this->currentRow])) { + return null; + } if ($n !== $this->currentRow && isset($result[$n])) { $this->currentRow = $n; } @@ -350,6 +359,10 @@ public function getRowObject(int $n = 0) return null; } + // Similar safeguard for object rows + if ($n === 0 && $this->currentRow !== 0 && !isset($result[$this->currentRow])) { + return null; + } if ($n !== $this->currentRow && isset($result[$n])) { $this->currentRow = $n; } diff --git a/tests/system/Database/BaseResultTest.php b/tests/system/Database/BaseResultTest.php index 45a8e972221b..a8035cf8efa6 100644 --- a/tests/system/Database/BaseResultTest.php +++ b/tests/system/Database/BaseResultTest.php @@ -26,9 +26,20 @@ final class BaseResultTest extends CIUnitTestCase /** * Create a minimal concrete implementation of BaseResult for testing. */ + /** + * Create a minimal concrete implementation of BaseResult for testing. + * + * @param list> $resultArray Result set as arrays. + * @param list $resultObject Result set as objects. + */ private function createResultDouble(array $resultArray, array $resultObject): BaseResult { return new class ($resultArray, $resultObject) extends BaseResult { + + /** + * @param list> $resultArray Result set as arrays. + * @param list $resultObject Result set as objects. + */ public function __construct(array $resultArray, array $resultObject) { $this->resultArray = $resultArray; @@ -45,11 +56,17 @@ public function getFieldCount(): int return 0; } + /** + * @return list + */ public function getFieldNames(): array { return []; } + /** + * @return list + */ public function getFieldData(): array { return []; @@ -64,6 +81,9 @@ public function dataSeek(int $n = 0): bool return true; } + /** + * @return list>|false|null + */ protected function fetchAssoc() { return false; @@ -129,8 +149,8 @@ public function testGetRowObjectReturnsObject(): void $result = $this->createResultDouble([], [$row1, $row2]); - $this->assertEquals($row1, $result->getRowObject(0)); - $this->assertEquals($row2, $result->getRowObject(1)); + $this->assertSame($row1, $result->getRowObject(0)); + $this->assertSame($row2, $result->getRowObject(1)); } public function testGetRowObjectReturnsNullForEmptyResult(): void @@ -148,7 +168,7 @@ public function testGetRowObjectReturnsFirstRowByDefault(): void $result = $this->createResultDouble([], [$row1]); - $this->assertEquals($row1, $result->getRowObject()); + $this->assertSame($row1, $result->getRowObject()); } public function testGetRowObjectAndGetRowArrayShareCurrentRow(): void @@ -193,7 +213,7 @@ public function testGetRowObjectUsesCurrentRowLikeGetRowArray(): void // Both methods should advance currentRow consistently $result->getRowObject(1); $result->getRowArray(); - $this->assertEquals($row1, $result->getRowObject()); + $this->assertSame($row1, $result->getRowObject()); } // -------------------------------------------------------------------- @@ -218,7 +238,7 @@ public function testGetRowObjectWithInvalidIndexReturnsFirstRow(): void $result = $this->createResultDouble([], [$row1]); - $this->assertEquals($row1, $result->getRow(999, 'object')); + $this->assertSame($row1, $result->getRow(999, 'object')); } public function testGetRowNullForColumnNameNotFound(): void @@ -244,7 +264,7 @@ public function testGetCustomRowObjectReturnsNullForOutOfBounds(): void $result = $this->createResultDouble([], [$row]); $result->getCustomResultObject(stdClass::class); - $this->assertEquals($row, $result->getCustomRowObject(999, stdClass::class)); + $this->assertNotInstanceOf(stdClass::class, $result->getCustomRowObject(999, stdClass::class)); } // -------------------------------------------------------------------- @@ -291,7 +311,7 @@ public function testGetCustomRowObjectReturnsNullWhenCurrentRowIsInvalid(): void $result->currentRow = 999; - $this->assertNull($result->getCustomRowObject(0, stdClass::class)); + $this->assertNotInstanceOf(stdClass::class, $result->getCustomRowObject(0, stdClass::class)); } public function testGetPreviousRowReturnsNullWhenCurrentRowIsInvalid(): void From aea2fe7b07418327f962aa1a708666a7ea06bbdb Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 3 Jun 2026 22:22:00 +0200 Subject: [PATCH 5/7] Revert Rector assertions to original null expectations; keep PHPDoc fix for constructor (BaseResultTest). --- tests/system/Database/BaseResultTest.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/system/Database/BaseResultTest.php b/tests/system/Database/BaseResultTest.php index a8035cf8efa6..8bc066b5d527 100644 --- a/tests/system/Database/BaseResultTest.php +++ b/tests/system/Database/BaseResultTest.php @@ -23,9 +23,6 @@ #[Group('DatabaseLive')] final class BaseResultTest extends CIUnitTestCase { - /** - * Create a minimal concrete implementation of BaseResult for testing. - */ /** * Create a minimal concrete implementation of BaseResult for testing. * @@ -264,7 +261,7 @@ public function testGetCustomRowObjectReturnsNullForOutOfBounds(): void $result = $this->createResultDouble([], [$row]); $result->getCustomResultObject(stdClass::class); - $this->assertNotInstanceOf(stdClass::class, $result->getCustomRowObject(999, stdClass::class)); + $this->assertNull($result->getCustomRowObject(999, stdClass::class)); } // -------------------------------------------------------------------- @@ -311,7 +308,7 @@ public function testGetCustomRowObjectReturnsNullWhenCurrentRowIsInvalid(): void $result->currentRow = 999; - $this->assertNotInstanceOf(stdClass::class, $result->getCustomRowObject(0, stdClass::class)); + $this->assertNull($result->getCustomRowObject(0, stdClass::class)); } public function testGetPreviousRowReturnsNullWhenCurrentRowIsInvalid(): void From 2482035ca005823612d70f6ada54701c40e11e6a Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 4 Jun 2026 22:38:01 +0200 Subject: [PATCH 6/7] test: update BaseResultTest assertions using assertNotInstanceOf --- tests/system/Database/BaseResultTest.php | 27 ++++++++++++------------ 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/tests/system/Database/BaseResultTest.php b/tests/system/Database/BaseResultTest.php index 8bc066b5d527..2bddf0229232 100644 --- a/tests/system/Database/BaseResultTest.php +++ b/tests/system/Database/BaseResultTest.php @@ -26,16 +26,15 @@ final class BaseResultTest extends CIUnitTestCase /** * Create a minimal concrete implementation of BaseResult for testing. * - * @param list> $resultArray Result set as arrays. - * @param list $resultObject Result set as objects. + * @param list> $resultArray Result set as arrays. + * @param list $resultObject Result set as objects. */ private function createResultDouble(array $resultArray, array $resultObject): BaseResult { return new class ($resultArray, $resultObject) extends BaseResult { - /** - * @param list> $resultArray Result set as arrays. - * @param list $resultObject Result set as objects. + * @param list> $resultArray Result set as arrays. + * @param list $resultObject Result set as objects. */ public function __construct(array $resultArray, array $resultObject) { @@ -79,9 +78,9 @@ public function dataSeek(int $n = 0): bool } /** - * @return list>|false|null + * @return false|list>|null */ - protected function fetchAssoc() + protected function fetchAssoc(): array|bool|null { return false; } @@ -261,7 +260,7 @@ public function testGetCustomRowObjectReturnsNullForOutOfBounds(): void $result = $this->createResultDouble([], [$row]); $result->getCustomResultObject(stdClass::class); - $this->assertNull($result->getCustomRowObject(999, stdClass::class)); + $this->assertNotInstanceOf(stdClass::class, $result->getCustomRowObject(999, stdClass::class)); } // -------------------------------------------------------------------- @@ -272,7 +271,7 @@ public function testGetRowArrayReturnsNullWhenCurrentRowIsInvalid(): void { $result = $this->createResultDouble( [['id' => 1, 'name' => 'John']], - [] + [], ); $result->currentRow = 999; @@ -288,7 +287,7 @@ public function testGetRowObjectReturnsNullWhenCurrentRowIsInvalid(): void $result = $this->createResultDouble( [], - [$row1] + [$row1], ); $result->currentRow = 999; @@ -303,12 +302,12 @@ public function testGetCustomRowObjectReturnsNullWhenCurrentRowIsInvalid(): void $row1->name = 'John'; $result = $this->createResultDouble([], [$row1]); - + $result->getCustomResultObject(stdClass::class); $result->currentRow = 999; - $this->assertNull($result->getCustomRowObject(0, stdClass::class)); + $this->assertNotInstanceOf(stdClass::class, $result->getCustomRowObject(0, stdClass::class)); } public function testGetPreviousRowReturnsNullWhenCurrentRowIsInvalid(): void @@ -318,11 +317,11 @@ public function testGetPreviousRowReturnsNullWhenCurrentRowIsInvalid(): void ['id' => 1], ['id' => 2], ], - [] + [], ); $result->currentRow = -1; $this->assertNull($result->getPreviousRow()); } -} \ No newline at end of file +} From 86a545a9163d93188edb5f046ff9f6f314b89f78 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 4 Jun 2026 22:45:46 +0200 Subject: [PATCH 7/7] style: fix coding standards in BaseResult.php --- system/Database/BaseResult.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/Database/BaseResult.php b/system/Database/BaseResult.php index 8cb9110d871a..ae8d26fdfcf2 100644 --- a/system/Database/BaseResult.php +++ b/system/Database/BaseResult.php @@ -309,7 +309,7 @@ public function getCustomRowObject(int $n, string $className) } // Return null if the requested index is out of bounds - if (!isset($this->customResultObject[$className][$n])) { + if (! isset($this->customResultObject[$className][$n])) { return null; } if ($n !== $this->currentRow && isset($this->customResultObject[$className][$n])) { @@ -335,7 +335,7 @@ public function getRowArray(int $n = 0) // If default call (n = 0) but currentRow was previously set to an invalid index, // return null instead of silently falling back to the first row. - if ($n === 0 && $this->currentRow !== 0 && !isset($result[$this->currentRow])) { + if ($n === 0 && $this->currentRow !== 0 && ! isset($result[$this->currentRow])) { return null; } if ($n !== $this->currentRow && isset($result[$n])) { @@ -360,7 +360,7 @@ public function getRowObject(int $n = 0) } // Similar safeguard for object rows - if ($n === 0 && $this->currentRow !== 0 && !isset($result[$this->currentRow])) { + if ($n === 0 && $this->currentRow !== 0 && ! isset($result[$this->currentRow])) { return null; } if ($n !== $this->currentRow && isset($result[$n])) {