Skip to content

Commit 8f60369

Browse files
authored
[Bug] Query Cache mangled if saved by-reference (#6552)
Bug emerged in #6510 (comment) The current implementation of query cache relies on the cache **not** saved by-reference; the bug has never been seen before because by default `new ArrayAdapter()` saves the cache with serialization, hence breaking the by-reference pointer. Once the _by-reference_ tecnique is used, two issues pop up: 1. `\Doctrine\DBAL\Cache\ArrayResult::$num` is never reset, so once it gets incremented in the first `\Doctrine\DBAL\Cache\ArrayResult::fetch` call, the following calls will always fail 2. Even considering fixing the `$num` property reset, a manual call on `\Doctrine\DBAL\Result::free` will by cascade call the `\Doctrine\DBAL\Cache\ArrayResult::free` method erasing all the saved results I think that the `ArrayResult` implementation is not the culprit, but rather the #6510 giving to the cache backend the internal object by reference instead of giving it a copy.
1 parent 173d1c6 commit 8f60369

File tree

2 files changed

+30
-10
lines changed

2 files changed

+30
-10
lines changed

src/Connection.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -811,7 +811,7 @@ public function executeCacheQuery(string $sql, array $params, array $types, Quer
811811
}
812812

813813
if (isset($value[$realKey]) && $value[$realKey] instanceof ArrayResult) {
814-
return new Result($value[$realKey], $this);
814+
return new Result(clone $value[$realKey], $this);
815815
}
816816
} else {
817817
$value = [];
@@ -837,7 +837,7 @@ public function executeCacheQuery(string $sql, array $params, array $types, Quer
837837

838838
$resultCache->save($item);
839839

840-
return new Result($value[$realKey], $this);
840+
return new Result(clone $value[$realKey], $this);
841841
}
842842

843843
/**

tests/Connection/CachedQueryTest.php

+28-8
Original file line numberDiff line numberDiff line change
@@ -8,49 +8,60 @@
88
use Doctrine\DBAL\Cache\QueryCacheProfile;
99
use Doctrine\DBAL\Connection;
1010
use Doctrine\DBAL\Driver;
11+
use PHPUnit\Framework\Attributes\DataProvider;
1112
use PHPUnit\Framework\TestCase;
13+
use Psr\Cache\CacheItemPoolInterface;
1214
use Symfony\Component\Cache\Adapter\ArrayAdapter;
1315

1416
class CachedQueryTest extends TestCase
1517
{
16-
public function testCachedQuery(): void
18+
#[DataProvider('providePsrCacheImplementations')]
19+
public function testCachedQuery(callable $psrCacheProvider): void
1720
{
18-
$cache = new ArrayAdapter();
21+
$cache = $psrCacheProvider();
1922

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

23-
self::assertSame([['foo' => 'bar']], $connection->executeCacheQuery('SELECT 1', [], [], $qcp)
26+
$firstResult = $connection->executeCacheQuery('SELECT 1', [], [], $qcp);
27+
self::assertSame([['foo' => 'bar']], $firstResult
28+
->fetchAllAssociative());
29+
$firstResult->free();
30+
$secondResult = $connection->executeCacheQuery('SELECT 1', [], [], $qcp);
31+
self::assertSame([['foo' => 'bar']], $secondResult
2432
->fetchAllAssociative());
33+
$secondResult->free();
2534
self::assertSame([['foo' => 'bar']], $connection->executeCacheQuery('SELECT 1', [], [], $qcp)
2635
->fetchAllAssociative());
2736

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

31-
public function testCachedQueryWithChangedImplementationIsExecutedTwice(): void
40+
#[DataProvider('providePsrCacheImplementations')]
41+
public function testCachedQueryWithChangedImplementationIsExecutedTwice(callable $psrCacheProvider): void
3242
{
3343
$connection = $this->createConnection(2, ['baz'], [['qux']]);
3444

3545
self::assertSame([['baz' => 'qux']], $connection->executeCacheQuery(
3646
'SELECT 1',
3747
[],
3848
[],
39-
new QueryCacheProfile(0, __FUNCTION__, new ArrayAdapter()),
49+
new QueryCacheProfile(0, __FUNCTION__, $psrCacheProvider()),
4050
)->fetchAllAssociative());
4151

4252
self::assertSame([['baz' => 'qux']], $connection->executeCacheQuery(
4353
'SELECT 1',
4454
[],
4555
[],
46-
new QueryCacheProfile(0, __FUNCTION__, new ArrayAdapter()),
56+
new QueryCacheProfile(0, __FUNCTION__, $psrCacheProvider()),
4757
)->fetchAllAssociative());
4858
}
4959

50-
public function testOldCacheFormat(): void
60+
#[DataProvider('providePsrCacheImplementations')]
61+
public function testOldCacheFormat(callable $psrCacheProvider): void
5162
{
5263
$connection = $this->createConnection(1, ['foo'], [['bar']]);
53-
$cache = new ArrayAdapter();
64+
$cache = $psrCacheProvider();
5465
$qcp = new QueryCacheProfile(0, __FUNCTION__, $cache);
5566

5667
[$cacheKey, $realKey] = $qcp->generateCacheKeys('SELECT 1', [], [], []);
@@ -83,4 +94,13 @@ private function createConnection(int $expectedQueryCount, array $columnNames, a
8394

8495
return new Connection([], $driver);
8596
}
97+
98+
/** @return array<non-empty-string, list<callable():CacheItemPoolInterface>> */
99+
public static function providePsrCacheImplementations(): array
100+
{
101+
return [
102+
'serialized' => [static fn () => new ArrayAdapter(0, true)],
103+
'by-reference' => [static fn () => new ArrayAdapter(0, false)],
104+
];
105+
}
86106
}

0 commit comments

Comments
 (0)