Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions system/Database/BaseResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -329,11 +333,16 @@ 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;
}

return $result[$this->currentRow];
return $result[$this->currentRow] ?? null;
}

/**
Expand All @@ -350,11 +359,15 @@ public function getRowObject(int $n = 0)
return null;
}

if ($n !== $this->customResultObject && isset($result[$n])) {
// 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;
}

return $result[$this->currentRow];
return $result[$this->currentRow] ?? null;
}

/**
Expand Down
328 changes: 328 additions & 0 deletions tests/system/Database/BaseResultTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,328 @@
<?php

declare(strict_types=1);

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* 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('DatabaseLive')]
final class BaseResultTest extends CIUnitTestCase
{
/**
* Create a minimal concrete implementation of BaseResult for testing.
*
* @param list<array<string,mixed>> $resultArray Result set as arrays.
* @param list<object> $resultObject Result set as objects.
*/
private function createResultDouble(array $resultArray, array $resultObject): BaseResult
{
return new class ($resultArray, $resultObject) extends BaseResult {

Check failure on line 34 in tests/system/Database/BaseResultTest.php

View workflow job for this annotation

GitHub Actions / Psalm Analysis (8.2)

MissingTemplateParam

tests/system/Database/BaseResultTest.php:34:20: MissingTemplateParam: CodeIgniter\Database\_home_runner_work_CodeIgniter4_CodeIgniter4_tests_system_Database_BaseResultTest_php_34_870 has missing template params when extending CodeIgniter\Database\BaseResult, expecting 2 (see https://psalm.dev/182)

/**
* @param list<array<string,mixed>> $resultArray Result set as arrays.
* @param list<object> $resultObject Result set as objects.
*/
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;
}

/**
* @return list<string>
*/
public function getFieldNames(): array
{
return [];
}

/**
* @return list<object>
*/
public function getFieldData(): array
{
return [];
}

public function freeResult(): void
{
}

public function dataSeek(int $n = 0): bool
{
return true;
}

/**
* @return list<array<string,mixed>>|false|null
*/
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->assertSame($row1, $result->getRowObject(0));
$this->assertSame($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->assertSame($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->assertSame($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->assertSame($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));
}

// --------------------------------------------------------------------
// 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());
}
}
Loading