From 6fcf73d0fc1852797263a87ff1e85b1d5d06861b Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 29 Aug 2024 11:35:02 +0200 Subject: [PATCH] Invalidate old query cache format --- src/Cache/ArrayResult.php | 38 ++++++++++--- src/Connection.php | 10 ++-- tests/Cache/ArrayResultTest.php | 66 ++++++++++++++++++++++ tests/Cache/Fixtures/array-result-4.1.txt | Bin 0 -> 307 bytes tests/Cache/Fixtures/array-result-4.2.txt | 1 + tests/Connection/CachedQueryTest.php | 23 +++++++- 6 files changed, 122 insertions(+), 16 deletions(-) create mode 100644 tests/Cache/Fixtures/array-result-4.1.txt create mode 100644 tests/Cache/Fixtures/array-result-4.2.txt diff --git a/src/Cache/ArrayResult.php b/src/Cache/ArrayResult.php index ff54865d34..d1b11749c9 100644 --- a/src/Cache/ArrayResult.php +++ b/src/Cache/ArrayResult.php @@ -9,6 +9,9 @@ use Doctrine\DBAL\Exception\InvalidColumnIndex; use function array_combine; +use function array_keys; +use function array_map; +use function array_values; use function count; /** @internal The class is internal to the caching layer implementation. */ @@ -21,8 +24,10 @@ final class ArrayResult implements Result * @param list> $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 __construct( + private readonly array $columnNames, + private array $rows, + ) { } public function fetchNumeric(): array|false @@ -96,13 +101,32 @@ public function free(): void $this->rows = []; } - /** @return list|false */ - private function fetch(): array|false + /** @return array{list, list>} */ + public function __serialize(): array { - if (! isset($this->rows[$this->num])) { - return false; + 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> $legacyData */ + $legacyData = $data["\0" . self::class . "\0data"]; + + $this->columnNames = array_keys($legacyData[0] ?? []); + $this->rows = array_map(array_values(...), $legacyData); + + return; } - return $this->rows[$this->num++]; + [$this->columnNames, $this->rows] = $data; + } + + /** @return list|false */ + private function fetch(): array|false + { + return $this->rows[$this->num++] ?? false; } } diff --git a/src/Connection.php b/src/Connection.php index f4ba096330..c936b4a302 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -810,10 +810,8 @@ public function executeCacheQuery(string $sql, array $params, array $types, Quer $value = []; } - if (isset($value[$realKey])) { - [$columnNames, $rows] = $value[$realKey]; - - return new Result(new ArrayResult($columnNames, $rows), $this); + if (isset($value[$realKey]) && $value[$realKey] instanceof ArrayResult) { + return new Result($value[$realKey], $this); } } else { $value = []; @@ -828,7 +826,7 @@ public function executeCacheQuery(string $sql, array $params, array $types, Quer $rows = $result->fetchAllNumeric(); - $value[$realKey] = [$columnNames, $rows]; + $value[$realKey] = new ArrayResult($columnNames, $rows); $item->set($value); @@ -839,7 +837,7 @@ public function executeCacheQuery(string $sql, array $params, array $types, Quer $resultCache->save($item); - return new Result(new ArrayResult($columnNames, $rows), $this); + return new Result($value[$realKey], $this); } /** diff --git a/tests/Cache/ArrayResultTest.php b/tests/Cache/ArrayResultTest.php index ce293f87d4..6b107cccc7 100644 --- a/tests/Cache/ArrayResultTest.php +++ b/tests/Cache/ArrayResultTest.php @@ -6,9 +6,15 @@ 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; @@ -105,4 +111,64 @@ public function testSameColumnNames(): void 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 */ + 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']; + } } diff --git a/tests/Cache/Fixtures/array-result-4.1.txt b/tests/Cache/Fixtures/array-result-4.1.txt new file mode 100644 index 0000000000000000000000000000000000000000..c78a6d9c3542e4a772c536ec5b215c8246a4a6c1 GIT binary patch literal 307 zcma)$%L>9U5Jmef^96nRV6v+1QV?8sGmSwbO#*o+Qu6O6-3fwjhI`?h%bc*O5C+4& zm0FC$p*xN}@tYVsYw2|sF3cXV!NpzdOO#Jzl^GGWD{Oy|3mNNW`7DNlT8C5th> E0eBB&^Z)<= literal 0 HcmV?d00001 diff --git a/tests/Cache/Fixtures/array-result-4.2.txt b/tests/Cache/Fixtures/array-result-4.2.txt new file mode 100644 index 0000000000..6372f234b5 --- /dev/null +++ b/tests/Cache/Fixtures/array-result-4.2.txt @@ -0,0 +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;}}} \ No newline at end of file diff --git a/tests/Connection/CachedQueryTest.php b/tests/Connection/CachedQueryTest.php index 7c908a1554..3d9c825a1b 100644 --- a/tests/Connection/CachedQueryTest.php +++ b/tests/Connection/CachedQueryTest.php @@ -47,6 +47,25 @@ public function testCachedQueryWithChangedImplementationIsExecutedTwice(): void )->fetchAllAssociative()); } + public function testOldCacheFormat(): void + { + $connection = $this->createConnection(1, ['foo'], [['bar']]); + $cache = new ArrayAdapter(); + $qcp = new QueryCacheProfile(0, __FUNCTION__, $cache); + + [$cacheKey, $realKey] = $qcp->generateCacheKeys('SELECT 1', [], [], []); + $cache->save( + $cache->getItem($cacheKey)->set([$realKey => [['foo' => 'bar']]]), + ); + + 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()); + } + /** * @param list $columnNames * @param list> $rows @@ -56,9 +75,7 @@ private function createConnection(int $expectedQueryCount, array $columnNames, a $connection = $this->createMock(Driver\Connection::class); $connection->expects(self::exactly($expectedQueryCount)) ->method('query') - ->willReturnCallback(static function () use ($columnNames, $rows): ArrayResult { - return new ArrayResult($columnNames, $rows); - }); + ->willReturnCallback(static fn (): ArrayResult => new ArrayResult($columnNames, $rows)); $driver = $this->createMock(Driver::class); $driver->method('connect')