Skip to content

Commit

Permalink
Handle cached result column names and rows separately
Browse files Browse the repository at this point in the history
  • Loading branch information
morozov committed Aug 18, 2024
1 parent 83a9ea8 commit 97d0ed5
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 88 deletions.
44 changes: 22 additions & 22 deletions src/Cache/ArrayResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,37 @@
use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\Exception\InvalidColumnIndex;

use function array_keys;
use function array_values;
use function array_combine;
use function count;
use function reset;

/** @internal The class is internal to the caching layer implementation. */
final class ArrayResult implements Result
{
private readonly int $columnCount;
private int $num = 0;

/** @param list<array<string, mixed>> $data */
public function __construct(private array $data)
/**
* @param list<string> $columnNames The names of the result columns. Must be non-empty.
* @param list<list<mixed>> $rows The rows of the result. Each row must have the same number of columns
* as the number of column names.
*/
public function __construct(private readonly array $columnNames, private array $rows)
{
$this->columnCount = $data === [] ? 0 : count($data[0]);
}

public function fetchNumeric(): array|false
{
return $this->fetch();
}

public function fetchAssociative(): array|false
{
$row = $this->fetch();

if ($row === false) {
return false;
}

return array_values($row);
}

public function fetchAssociative(): array|false
{
return $this->fetch();
return array_combine($this->columnNames, $row);
}

public function fetchOne(): mixed
Expand All @@ -49,7 +49,7 @@ public function fetchOne(): mixed
return false;
}

return reset($row);
return $row[0];
}

/**
Expand Down Expand Up @@ -78,35 +78,35 @@ public function fetchFirstColumn(): array

public function rowCount(): int
{
return count($this->data);
return count($this->rows);
}

public function columnCount(): int
{
return $this->columnCount;
return count($this->columnNames);
}

public function getColumnName(int $index): string
{
if ($this->data === [] || $index > count($this->data[0])) {
if ($index > count($this->columnNames)) {
throw InvalidColumnIndex::new($index);
}

return array_keys($this->data[0])[$index];
return $this->columnNames[$index];
}

public function free(): void
{
$this->data = [];
$this->rows = [];
}

/** @return array<string, mixed>|false */
/** @return list<mixed>|false */
private function fetch(): array|false
{
if (! isset($this->data[$this->num])) {
if (! isset($this->rows[$this->num])) {
return false;
}

return $this->data[$this->num++];
return $this->rows[$this->num++];
}
}
17 changes: 13 additions & 4 deletions src/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -811,15 +811,24 @@ public function executeCacheQuery(string $sql, array $params, array $types, Quer
}

if (isset($value[$realKey])) {
return new Result(new ArrayResult($value[$realKey]), $this);
[$columnNames, $rows] = $value[$realKey];

return new Result(new ArrayResult($columnNames, $rows), $this);
}
} else {
$value = [];
}

$data = $this->fetchAllAssociative($sql, $params, $types);
$result = $this->executeQuery($sql, $params, $types);

$columnNames = [];
for ($i = 0; $i < $result->columnCount(); $i++) {
$columnNames[] = $result->getColumnName($i);
}

$rows = $result->fetchAllNumeric();

$value[$realKey] = $data;
$value[$realKey] = [$columnNames, $rows];

$item->set($value);

Expand All @@ -830,7 +839,7 @@ public function executeCacheQuery(string $sql, array $params, array $types, Quer

$resultCache->save($item);

return new Result(new ArrayResult($data), $this);
return new Result(new ArrayResult($columnNames, $rows), $this);
}

/**
Expand Down
85 changes: 37 additions & 48 deletions tests/Cache/ArrayStatementTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,86 +7,75 @@
use Doctrine\DBAL\Cache\ArrayResult;
use PHPUnit\Framework\TestCase;

use function array_values;

class ArrayStatementTest extends TestCase
{
/** @var list<array<string, mixed>> */
private array $users = [
[
'username' => 'jwage',
'active' => true,
],
[
'username' => 'romanb',
'active' => false,
],
];

public function testCloseCursor(): void
private ArrayResult $result;

protected function setUp(): void
{
$statement = $this->createTestArrayStatement();
parent::setUp();

$this->result = new ArrayResult(['username', 'active'], [
['jwage', true],
['romanb', false],
]);
}

self::assertSame(2, $statement->rowCount());
public function testFree(): void
{
self::assertSame(2, $this->result->rowCount());

$statement->free();
$this->result->free();

self::assertSame(0, $statement->rowCount());
self::assertSame(0, $this->result->rowCount());
}

public function testColumnCount(): void
{
$statement = $this->createTestArrayStatement();

self::assertSame(2, $statement->columnCount());
self::assertSame(2, $this->result->columnCount());
}

public function testColumnNames(): void
{
$statement = $this->createTestArrayStatement();

self::assertSame('username', $statement->getColumnName(0));
self::assertSame('active', $statement->getColumnName(1));
self::assertSame('username', $this->result->getColumnName(0));
self::assertSame('active', $this->result->getColumnName(1));
}

public function testRowCount(): void
{
$statement = $this->createTestArrayStatement();

self::assertSame(2, $statement->rowCount());
self::assertSame(2, $this->result->rowCount());
}

public function testFetchAssociative(): void
{
$statement = $this->createTestArrayStatement();

self::assertSame($this->users[0], $statement->fetchAssociative());
self::assertSame([
'username' => 'jwage',
'active' => true,
], $this->result->fetchAssociative());
}

public function testFetchNumeric(): void
{
$statement = $this->createTestArrayStatement();

self::assertSame(array_values($this->users[0]), $statement->fetchNumeric());
self::assertSame(['jwage', true], $this->result->fetchNumeric());
}

public function testFetchOne(): void
{
$statement = $this->createTestArrayStatement();

self::assertSame('jwage', $statement->fetchOne());
self::assertSame('romanb', $statement->fetchOne());
}

public function testFetchAll(): void
{
$statement = $this->createTestArrayStatement();

self::assertSame($this->users, $statement->fetchAllAssociative());
self::assertSame('jwage', $this->result->fetchOne());
self::assertSame('romanb', $this->result->fetchOne());
}

private function createTestArrayStatement(): ArrayResult
public function testFetchAllAssociative(): void
{
return new ArrayResult($this->users);
self::assertSame([
[
'username' => 'jwage',
'active' => true,
],
[
'username' => 'romanb',
'active' => false,
],
], $this->result->fetchAllAssociative());
}
}
28 changes: 15 additions & 13 deletions tests/Connection/CachedQueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,46 +16,48 @@ class CachedQueryTest extends TestCase
public function testCachedQuery(): void
{
$cache = new ArrayAdapter();
$data = [['foo' => 'bar']];

$connection = $this->createConnection(1, $data);
$connection = $this->createConnection(1, ['foo'], [['bar']]);
$qcp = new QueryCacheProfile(0, __FUNCTION__, $cache);

self::assertSame($data, $connection->executeCacheQuery('SELECT 1', [], [], $qcp)->fetchAllAssociative());
self::assertSame($data, $connection->executeCacheQuery('SELECT 1', [], [], $qcp)->fetchAllAssociative());
self::assertSame([['foo' => 'bar']], $connection->executeCacheQuery('SELECT 1', [], [], $qcp)
->fetchAllAssociative());
self::assertSame([['foo' => 'bar']], $connection->executeCacheQuery('SELECT 1', [], [], $qcp)
->fetchAllAssociative());

self::assertCount(1, $cache->getItem(__FUNCTION__)->get());
}

public function testCachedQueryWithChangedImplementationIsExecutedTwice(): void
{
$data = [['baz' => 'qux']];
$connection = $this->createConnection(2, ['baz'], [['qux']]);

$connection = $this->createConnection(2, $data);

self::assertSame($data, $connection->executeCacheQuery(
self::assertSame([['baz' => 'qux']], $connection->executeCacheQuery(
'SELECT 1',
[],
[],
new QueryCacheProfile(0, __FUNCTION__, new ArrayAdapter()),
)->fetchAllAssociative());

self::assertSame($data, $connection->executeCacheQuery(
self::assertSame([['baz' => 'qux']], $connection->executeCacheQuery(
'SELECT 1',
[],
[],
new QueryCacheProfile(0, __FUNCTION__, new ArrayAdapter()),
)->fetchAllAssociative());
}

/** @param list<array<string, mixed>> $data */
private function createConnection(int $expectedQueryCount, array $data): Connection
/**
* @param list<string> $columnNames
* @param list<list<mixed>> $rows
*/
private function createConnection(int $expectedQueryCount, array $columnNames, array $rows): Connection
{
$connection = $this->createMock(Driver\Connection::class);
$connection->expects(self::exactly($expectedQueryCount))
->method('query')
->willReturnCallback(static function () use ($data): ArrayResult {
return new ArrayResult($data);
->willReturnCallback(static function () use ($columnNames, $rows): ArrayResult {
return new ArrayResult($columnNames, $rows);
});

$driver = $this->createMock(Driver::class);
Expand Down
2 changes: 1 addition & 1 deletion tests/ConnectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ public function testConnectionParamsArePassedToTheQueryCacheProfileInExecuteCach
{
$cacheItemMock = $this->createMock(CacheItemInterface::class);
$cacheItemMock->method('isHit')->willReturn(true);
$cacheItemMock->method('get')->willReturn(['realKey' => []]);
$cacheItemMock->method('get')->willReturn(['realKey' => [[], []]]);

$resultCacheMock = $this->createMock(CacheItemPoolInterface::class);

Expand Down
45 changes: 45 additions & 0 deletions tests/Functional/Cache/ArrayResultTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Tests\Functional\Cache;

use Doctrine\DBAL\Cache\QueryCacheProfile;
use Doctrine\DBAL\Tests\FunctionalTestCase;
use Doctrine\DBAL\Tests\TestUtil;
use Symfony\Component\Cache\Adapter\ArrayAdapter;

class ArrayResultTest extends FunctionalTestCase
{
public function testEmptyResult(): void
{
$query = TestUtil::generateResultSetQuery(['a'], [], $this->connection->getDatabasePlatform());
$result = $this->connection->executeCacheQuery(
$query,
[],
[],
new QueryCacheProfile(0, __FUNCTION__, new ArrayAdapter()),
);
self::assertSame('a', $result->getColumnName(0));
}

public function testSameColumnNames(): void
{
$query = TestUtil::generateResultSetQuery(
['a', 'a'],
[[1, 2]],
$this->connection->getDatabasePlatform(),
);

$result = $this->connection->executeCacheQuery(
$query,
[],
[],
new QueryCacheProfile(0, __FUNCTION__, new ArrayAdapter()),
);
self::assertSame('a', $result->getColumnName(0));
self::assertSame('a', $result->getColumnName(1));

self::assertEquals([1, 2], $result->fetchNumeric());
}
}

0 comments on commit 97d0ed5

Please sign in to comment.