Skip to content

Commit

Permalink
Merge branch '4.2.x' into 5.0.x
Browse files Browse the repository at this point in the history
* 4.2.x:
  Invalidate old query cache format (doctrine#6510)
  Handle cached result column names and rows separately (doctrine#6504)
  • Loading branch information
derrabus committed Aug 30, 2024
2 parents d53d332 + c56a608 commit d8ba313
Show file tree
Hide file tree
Showing 11 changed files with 287 additions and 164 deletions.
8 changes: 8 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ all drivers and middleware.
* Upgrade to MySQL 8.0 or later.
* Upgrade to Postgres 12 or later.

# Upgrade to 4.2

## Minor BC break: incompatible query cache format

The query cache format has been changed to address the issue where a cached result with no rows would miss the metadata.
This change is not backwards compatible. If you are using the query cache, you should clear the cache before the
upgrade.

# Upgrade to 4.1

## Deprecated `TableDiff` methods
Expand Down
69 changes: 48 additions & 21 deletions src/Cache/ArrayResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,42 @@
use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\Exception\InvalidColumnIndex;

use function array_combine;
use function array_keys;
use function array_map;
use function array_values;
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)
{
$this->columnCount = $data === [] ? 0 : count($data[0]);
/**
* @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,
) {
}

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 +54,7 @@ public function fetchOne(): mixed
return false;
}

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

/**
Expand Down Expand Up @@ -78,32 +83,54 @@ 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
{
return array_keys($this->data[0] ?? [])[$index]
?? throw InvalidColumnIndex::new($index);
return $this->columnNames[$index] ?? throw InvalidColumnIndex::new($index);
}

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

/** @return array{list<string>, list<list<mixed>>} */
public function __serialize(): array
{
return [$this->columnNames, $this->rows];
}

/** @param mixed[] $data */
public function __unserialize(array $data): void
{
// Handle objects serialized with DBAL 4.1 and earlier.
if (isset($data["\0" . self::class . "\0data"])) {
/** @var list<array<string, mixed>> $legacyData */
$legacyData = $data["\0" . self::class . "\0data"];

$this->columnNames = array_keys($legacyData[0] ?? []);
$this->rows = array_map(array_values(...), $legacyData);

return;
}

[$this->columnNames, $this->rows] = $data;
}

/** @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: 12 additions & 5 deletions src/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -810,16 +810,23 @@ public function executeCacheQuery(string $sql, array $params, array $types, Quer
$value = [];
}

if (isset($value[$realKey])) {
return new Result(new ArrayResult($value[$realKey]), $this);
if (isset($value[$realKey]) && $value[$realKey] instanceof ArrayResult) {
return new Result($value[$realKey], $this);
}
} else {
$value = [];
}

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

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

$rows = $result->fetchAllNumeric();

$value[$realKey] = new ArrayResult($columnNames, $rows);

$item->set($value);

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

$resultCache->save($item);

return new Result(new ArrayResult($data), $this);
return new Result($value[$realKey], $this);
}

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

declare(strict_types=1);

namespace Doctrine\DBAL\Tests\Cache;

use Doctrine\DBAL\Cache\ArrayResult;
use Doctrine\DBAL\Exception\InvalidColumnIndex;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\TestCase;

use function assert;
use function file_get_contents;
use function serialize;
use function unserialize;

class ArrayResultTest extends TestCase
{
private ArrayResult $result;

protected function setUp(): void
{
parent::setUp();

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

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

$this->result->free();

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

public function testColumnCount(): void
{
self::assertSame(2, $this->result->columnCount());
}

public function testColumnNames(): void
{
self::assertSame('username', $this->result->getColumnName(0));
self::assertSame('active', $this->result->getColumnName(1));
}

#[TestWith([2])]
#[TestWith([-1])]
public function testColumnNameWithInvalidIndex(int $index): void
{
$this->expectException(InvalidColumnIndex::class);

$this->result->getColumnName($index);
}

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

public function testFetchAssociative(): void
{
self::assertSame([
'username' => 'jwage',
'active' => true,
], $this->result->fetchAssociative());
}

public function testFetchNumeric(): void
{
self::assertSame(['jwage', true], $this->result->fetchNumeric());
}

public function testFetchOne(): void
{
self::assertSame('jwage', $this->result->fetchOne());
self::assertSame('romanb', $this->result->fetchOne());
}

public function testFetchAllAssociative(): void
{
self::assertSame([
[
'username' => 'jwage',
'active' => true,
],
[
'username' => 'romanb',
'active' => false,
],
], $this->result->fetchAllAssociative());
}

public function testEmptyResult(): void
{
$result = new ArrayResult(['a'], []);
self::assertSame('a', $result->getColumnName(0));
}

public function testSameColumnNames(): void
{
$result = new ArrayResult(['a', 'a'], [[1, 2]]);

self::assertSame('a', $result->getColumnName(0));
self::assertSame('a', $result->getColumnName(1));

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

public function testSerialize(): void
{
$result = unserialize(serialize($this->result));

self::assertSame([
[
'username' => 'jwage',
'active' => true,
],
[
'username' => 'romanb',
'active' => false,
],
], $result->fetchAllAssociative());

self::assertSame(2, $result->columnCount());
self::assertSame('username', $result->getColumnName(0));
}

public function testRowPointerIsNotSerialized(): void
{
$this->result->fetchAssociative();
$result = unserialize(serialize($this->result));

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

#[DataProvider('provideSerializedResultFiles')]
public function testUnserialize(string $file): void
{
$serialized = file_get_contents($file);
assert($serialized !== false);
$result = unserialize($serialized);

self::assertInstanceOf(ArrayResult::class, $result);
self::assertSame([
[
'username' => 'jwage',
'active' => true,
],
[
'username' => 'romanb',
'active' => false,
],
], $result->fetchAllAssociative());

self::assertSame(2, $result->columnCount());
self::assertSame('username', $result->getColumnName(0));
}

/** @return iterable<string, array{string}> */
public static function provideSerializedResultFiles(): iterable
{
yield '4.1 format' => [__DIR__ . '/Fixtures/array-result-4.1.txt'];
yield '4.2 format' => [__DIR__ . '/Fixtures/array-result-4.2.txt'];
}
}
Loading

0 comments on commit d8ba313

Please sign in to comment.