Skip to content

Commit 773774e

Browse files
committed
Invalidate old query cache format
1 parent d47b6b1 commit 773774e

File tree

6 files changed

+121
-11
lines changed

6 files changed

+121
-11
lines changed

src/Cache/ArrayResult.php

+30-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
use Doctrine\DBAL\Exception\InvalidColumnIndex;
1010

1111
use function array_combine;
12+
use function array_keys;
13+
use function array_map;
14+
use function array_values;
1215
use function count;
1316

1417
/** @internal The class is internal to the caching layer implementation. */
@@ -21,8 +24,10 @@ final class ArrayResult implements Result
2124
* @param list<list<mixed>> $rows The rows of the result. Each row must have the same number of columns
2225
* as the number of column names.
2326
*/
24-
public function __construct(private readonly array $columnNames, private array $rows)
25-
{
27+
public function __construct(
28+
private readonly array $columnNames,
29+
private array $rows,
30+
) {
2631
}
2732

2833
public function fetchNumeric(): array|false
@@ -96,6 +101,29 @@ public function free(): void
96101
$this->rows = [];
97102
}
98103

104+
/** @return array{list<string>, list<list<mixed>>} */
105+
public function __serialize(): array
106+
{
107+
return [$this->columnNames, $this->rows];
108+
}
109+
110+
/** @param mixed[] $data */
111+
public function __unserialize(array $data): void
112+
{
113+
// Handle objects serialized with DBAL 4.1 and earlier.
114+
if (isset($data["\0" . self::class . "\0data"])) {
115+
/** @var list<array<string, mixed>> $legacyData */
116+
$legacyData = $data["\0" . self::class . "\0data"];
117+
118+
$this->columnNames = array_keys($legacyData[0] ?? []);
119+
$this->rows = array_map(array_values(...), $legacyData);
120+
121+
return;
122+
}
123+
124+
[$this->columnNames, $this->rows] = $data;
125+
}
126+
99127
/** @return list<mixed>|false */
100128
private function fetch(): array|false
101129
{

src/Connection.php

+4-6
Original file line numberDiff line numberDiff line change
@@ -810,10 +810,8 @@ public function executeCacheQuery(string $sql, array $params, array $types, Quer
810810
$value = [];
811811
}
812812

813-
if (isset($value[$realKey])) {
814-
[$columnNames, $rows] = $value[$realKey];
815-
816-
return new Result(new ArrayResult($columnNames, $rows), $this);
813+
if (isset($value[$realKey]) && $value[$realKey] instanceof ArrayResult) {
814+
return new Result($value[$realKey], $this);
817815
}
818816
} else {
819817
$value = [];
@@ -828,7 +826,7 @@ public function executeCacheQuery(string $sql, array $params, array $types, Quer
828826

829827
$rows = $result->fetchAllNumeric();
830828

831-
$value[$realKey] = [$columnNames, $rows];
829+
$value[$realKey] = new ArrayResult($columnNames, $rows);
832830

833831
$item->set($value);
834832

@@ -839,7 +837,7 @@ public function executeCacheQuery(string $sql, array $params, array $types, Quer
839837

840838
$resultCache->save($item);
841839

842-
return new Result(new ArrayResult($columnNames, $rows), $this);
840+
return new Result($value[$realKey], $this);
843841
}
844842

845843
/**

tests/Cache/ArrayResultTest.php

+66
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,15 @@
66

77
use Doctrine\DBAL\Cache\ArrayResult;
88
use Doctrine\DBAL\Exception\InvalidColumnIndex;
9+
use PHPUnit\Framework\Attributes\DataProvider;
910
use PHPUnit\Framework\Attributes\TestWith;
1011
use PHPUnit\Framework\TestCase;
1112

13+
use function assert;
14+
use function file_get_contents;
15+
use function serialize;
16+
use function unserialize;
17+
1218
class ArrayResultTest extends TestCase
1319
{
1420
private ArrayResult $result;
@@ -105,4 +111,64 @@ public function testSameColumnNames(): void
105111

106112
self::assertEquals([1, 2], $result->fetchNumeric());
107113
}
114+
115+
public function testSerialize(): void
116+
{
117+
$result = unserialize(serialize($this->result));
118+
119+
self::assertSame([
120+
[
121+
'username' => 'jwage',
122+
'active' => true,
123+
],
124+
[
125+
'username' => 'romanb',
126+
'active' => false,
127+
],
128+
], $result->fetchAllAssociative());
129+
130+
self::assertSame(2, $result->columnCount());
131+
self::assertSame('username', $result->getColumnName(0));
132+
}
133+
134+
public function testRowPointerIsNotSerialized(): void
135+
{
136+
$this->result->fetchAssociative();
137+
$result = unserialize(serialize($this->result));
138+
139+
self::assertSame([
140+
'username' => 'jwage',
141+
'active' => true,
142+
], $result->fetchAssociative());
143+
}
144+
145+
#[DataProvider('provideSerializedResultFiles')]
146+
public function testUnserialize(string $file): void
147+
{
148+
$serialized = file_get_contents($file);
149+
assert($serialized !== false);
150+
$result = unserialize($serialized);
151+
152+
self::assertInstanceOf(ArrayResult::class, $result);
153+
self::assertSame([
154+
[
155+
'username' => 'jwage',
156+
'active' => true,
157+
],
158+
[
159+
'username' => 'romanb',
160+
'active' => false,
161+
],
162+
], $result->fetchAllAssociative());
163+
164+
self::assertSame(2, $result->columnCount());
165+
self::assertSame('username', $result->getColumnName(0));
166+
}
167+
168+
/** @return iterable<string, array{string}> */
169+
public static function provideSerializedResultFiles(): iterable
170+
{
171+
yield '4.1 format' => [__DIR__ . '/Fixtures/array-result-4.1.txt'];
172+
yield '4.2 format' => [__DIR__ . '/Fixtures/array-result-4.2.txt'];
173+
}
108174
}
307 Bytes
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
O:31:"Doctrine\DBAL\Cache\ArrayResult":2:{i:0;a:2:{i:0;s:8:"username";i:1;s:6:"active";}i:1;a:2:{i:0;a:2:{i:0;s:5:"jwage";i:1;b:1;}i:1;a:2:{i:0;s:6:"romanb";i:1;b:0;}}}

tests/Connection/CachedQueryTest.php

+20-3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,25 @@ public function testCachedQueryWithChangedImplementationIsExecutedTwice(): void
4747
)->fetchAllAssociative());
4848
}
4949

50+
public function testOldCacheFormat(): void
51+
{
52+
$connection = $this->createConnection(1, ['foo'], [['bar']]);
53+
$cache = new ArrayAdapter();
54+
$qcp = new QueryCacheProfile(0, __FUNCTION__, $cache);
55+
56+
[$cacheKey, $realKey] = $qcp->generateCacheKeys('SELECT 1', [], [], []);
57+
$cache->save(
58+
$cache->getItem($cacheKey)->set([$realKey => [['foo' => 'bar']]]),
59+
);
60+
61+
self::assertSame([['foo' => 'bar']], $connection->executeCacheQuery('SELECT 1', [], [], $qcp)
62+
->fetchAllAssociative());
63+
self::assertSame([['foo' => 'bar']], $connection->executeCacheQuery('SELECT 1', [], [], $qcp)
64+
->fetchAllAssociative());
65+
66+
self::assertCount(1, $cache->getItem(__FUNCTION__)->get());
67+
}
68+
5069
/**
5170
* @param list<string> $columnNames
5271
* @param list<list<mixed>> $rows
@@ -56,9 +75,7 @@ private function createConnection(int $expectedQueryCount, array $columnNames, a
5675
$connection = $this->createMock(Driver\Connection::class);
5776
$connection->expects(self::exactly($expectedQueryCount))
5877
->method('query')
59-
->willReturnCallback(static function () use ($columnNames, $rows): ArrayResult {
60-
return new ArrayResult($columnNames, $rows);
61-
});
78+
->willReturnCallback(static fn (): ArrayResult => new ArrayResult($columnNames, $rows));
6279

6380
$driver = $this->createMock(Driver::class);
6481
$driver->method('connect')

0 commit comments

Comments
 (0)