Skip to content

Commit d8ba313

Browse files
committed
Merge branch '4.2.x' into 5.0.x
* 4.2.x: Invalidate old query cache format (#6510) Handle cached result column names and rows separately (#6504)
2 parents d53d332 + c56a608 commit d8ba313

File tree

11 files changed

+287
-164
lines changed

11 files changed

+287
-164
lines changed

UPGRADE.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ all drivers and middleware.
1919
* Upgrade to MySQL 8.0 or later.
2020
* Upgrade to Postgres 12 or later.
2121

22+
# Upgrade to 4.2
23+
24+
## Minor BC break: incompatible query cache format
25+
26+
The query cache format has been changed to address the issue where a cached result with no rows would miss the metadata.
27+
This change is not backwards compatible. If you are using the query cache, you should clear the cache before the
28+
upgrade.
29+
2230
# Upgrade to 4.1
2331

2432
## Deprecated `TableDiff` methods

src/Cache/ArrayResult.php

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,37 +8,42 @@
88
use Doctrine\DBAL\Driver\Result;
99
use Doctrine\DBAL\Exception\InvalidColumnIndex;
1010

11+
use function array_combine;
1112
use function array_keys;
13+
use function array_map;
1214
use function array_values;
1315
use function count;
14-
use function reset;
1516

1617
/** @internal The class is internal to the caching layer implementation. */
1718
final class ArrayResult implements Result
1819
{
19-
private readonly int $columnCount;
2020
private int $num = 0;
2121

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

2833
public function fetchNumeric(): array|false
34+
{
35+
return $this->fetch();
36+
}
37+
38+
public function fetchAssociative(): array|false
2939
{
3040
$row = $this->fetch();
3141

3242
if ($row === false) {
3343
return false;
3444
}
3545

36-
return array_values($row);
37-
}
38-
39-
public function fetchAssociative(): array|false
40-
{
41-
return $this->fetch();
46+
return array_combine($this->columnNames, $row);
4247
}
4348

4449
public function fetchOne(): mixed
@@ -49,7 +54,7 @@ public function fetchOne(): mixed
4954
return false;
5055
}
5156

52-
return reset($row);
57+
return $row[0];
5358
}
5459

5560
/**
@@ -78,32 +83,54 @@ public function fetchFirstColumn(): array
7883

7984
public function rowCount(): int
8085
{
81-
return count($this->data);
86+
return count($this->rows);
8287
}
8388

8489
public function columnCount(): int
8590
{
86-
return $this->columnCount;
91+
return count($this->columnNames);
8792
}
8893

8994
public function getColumnName(int $index): string
9095
{
91-
return array_keys($this->data[0] ?? [])[$index]
92-
?? throw InvalidColumnIndex::new($index);
96+
return $this->columnNames[$index] ?? throw InvalidColumnIndex::new($index);
9397
}
9498

9599
public function free(): void
96100
{
97-
$this->data = [];
101+
$this->rows = [];
102+
}
103+
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;
98125
}
99126

100-
/** @return array<string, mixed>|false */
127+
/** @return list<mixed>|false */
101128
private function fetch(): array|false
102129
{
103-
if (! isset($this->data[$this->num])) {
130+
if (! isset($this->rows[$this->num])) {
104131
return false;
105132
}
106133

107-
return $this->data[$this->num++];
134+
return $this->rows[$this->num++];
108135
}
109136
}

src/Connection.php

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -810,16 +810,23 @@ public function executeCacheQuery(string $sql, array $params, array $types, Quer
810810
$value = [];
811811
}
812812

813-
if (isset($value[$realKey])) {
814-
return new Result(new ArrayResult($value[$realKey]), $this);
813+
if (isset($value[$realKey]) && $value[$realKey] instanceof ArrayResult) {
814+
return new Result($value[$realKey], $this);
815815
}
816816
} else {
817817
$value = [];
818818
}
819819

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

822-
$value[$realKey] = $data;
822+
$columnNames = [];
823+
for ($i = 0; $i < $result->columnCount(); $i++) {
824+
$columnNames[] = $result->getColumnName($i);
825+
}
826+
827+
$rows = $result->fetchAllNumeric();
828+
829+
$value[$realKey] = new ArrayResult($columnNames, $rows);
823830

824831
$item->set($value);
825832

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

831838
$resultCache->save($item);
832839

833-
return new Result(new ArrayResult($data), $this);
840+
return new Result($value[$realKey], $this);
834841
}
835842

836843
/**

tests/Cache/ArrayResultTest.php

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\DBAL\Tests\Cache;
6+
7+
use Doctrine\DBAL\Cache\ArrayResult;
8+
use Doctrine\DBAL\Exception\InvalidColumnIndex;
9+
use PHPUnit\Framework\Attributes\DataProvider;
10+
use PHPUnit\Framework\Attributes\TestWith;
11+
use PHPUnit\Framework\TestCase;
12+
13+
use function assert;
14+
use function file_get_contents;
15+
use function serialize;
16+
use function unserialize;
17+
18+
class ArrayResultTest extends TestCase
19+
{
20+
private ArrayResult $result;
21+
22+
protected function setUp(): void
23+
{
24+
parent::setUp();
25+
26+
$this->result = new ArrayResult(['username', 'active'], [
27+
['jwage', true],
28+
['romanb', false],
29+
]);
30+
}
31+
32+
public function testFree(): void
33+
{
34+
self::assertSame(2, $this->result->rowCount());
35+
36+
$this->result->free();
37+
38+
self::assertSame(0, $this->result->rowCount());
39+
}
40+
41+
public function testColumnCount(): void
42+
{
43+
self::assertSame(2, $this->result->columnCount());
44+
}
45+
46+
public function testColumnNames(): void
47+
{
48+
self::assertSame('username', $this->result->getColumnName(0));
49+
self::assertSame('active', $this->result->getColumnName(1));
50+
}
51+
52+
#[TestWith([2])]
53+
#[TestWith([-1])]
54+
public function testColumnNameWithInvalidIndex(int $index): void
55+
{
56+
$this->expectException(InvalidColumnIndex::class);
57+
58+
$this->result->getColumnName($index);
59+
}
60+
61+
public function testRowCount(): void
62+
{
63+
self::assertSame(2, $this->result->rowCount());
64+
}
65+
66+
public function testFetchAssociative(): void
67+
{
68+
self::assertSame([
69+
'username' => 'jwage',
70+
'active' => true,
71+
], $this->result->fetchAssociative());
72+
}
73+
74+
public function testFetchNumeric(): void
75+
{
76+
self::assertSame(['jwage', true], $this->result->fetchNumeric());
77+
}
78+
79+
public function testFetchOne(): void
80+
{
81+
self::assertSame('jwage', $this->result->fetchOne());
82+
self::assertSame('romanb', $this->result->fetchOne());
83+
}
84+
85+
public function testFetchAllAssociative(): void
86+
{
87+
self::assertSame([
88+
[
89+
'username' => 'jwage',
90+
'active' => true,
91+
],
92+
[
93+
'username' => 'romanb',
94+
'active' => false,
95+
],
96+
], $this->result->fetchAllAssociative());
97+
}
98+
99+
public function testEmptyResult(): void
100+
{
101+
$result = new ArrayResult(['a'], []);
102+
self::assertSame('a', $result->getColumnName(0));
103+
}
104+
105+
public function testSameColumnNames(): void
106+
{
107+
$result = new ArrayResult(['a', 'a'], [[1, 2]]);
108+
109+
self::assertSame('a', $result->getColumnName(0));
110+
self::assertSame('a', $result->getColumnName(1));
111+
112+
self::assertEquals([1, 2], $result->fetchNumeric());
113+
}
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+
}
174+
}

0 commit comments

Comments
 (0)