ResultCacheTest.php 10.2 KB
Newer Older
1 2
<?php

3
namespace Doctrine\DBAL\Tests\Functional;
4

Sergei Morozov's avatar
Sergei Morozov committed
5
use Doctrine\Common\Cache\ArrayCache;
6
use Doctrine\DBAL\Cache\QueryCacheProfile;
7
use Doctrine\DBAL\Driver\Result;
Gabriel Caruso's avatar
Gabriel Caruso committed
8
use Doctrine\DBAL\Logging\DebugStack;
Sergei Morozov's avatar
Sergei Morozov committed
9
use Doctrine\DBAL\Schema\Table;
10
use Doctrine\DBAL\Tests\FunctionalTestCase;
11

12 13 14 15
use function array_change_key_case;
use function array_shift;
use function array_values;
use function is_array;
16

Grégoire Paris's avatar
Grégoire Paris committed
17
use const CASE_LOWER;
18 19 20 21

/**
 * @group DDC-217
 */
22
class ResultCacheTest extends FunctionalTestCase
23
{
24
    /** @var list<array{test_int: int, test_string: string}> */
Sergei Morozov's avatar
Sergei Morozov committed
25 26 27
    private $expectedResult = [['test_int' => 100, 'test_string' => 'foo'], ['test_int' => 200, 'test_string' => 'bar'], ['test_int' => 300, 'test_string' => 'baz']];

    /** @var DebugStack */
28 29
    private $sqlLogger;

30
    protected function setUp(): void
31 32 33
    {
        parent::setUp();

Sergei Morozov's avatar
Sergei Morozov committed
34
        $table = new Table('caching');
35
        $table->addColumn('test_int', 'integer');
Sergei Morozov's avatar
Sergei Morozov committed
36 37
        $table->addColumn('test_string', 'string', ['notnull' => false]);
        $table->setPrimaryKey(['test_int']);
38

Sergei Morozov's avatar
Sergei Morozov committed
39
        $sm = $this->connection->getSchemaManager();
40
        $sm->createTable($table);
41

jeroendedauw's avatar
jeroendedauw committed
42
        foreach ($this->expectedResult as $row) {
Sergei Morozov's avatar
Sergei Morozov committed
43
            $this->connection->insert('caching', $row);
44 45
        }

46
        $config = $this->connection->getConfiguration();
Sergei Morozov's avatar
Sergei Morozov committed
47
        $config->setSQLLogger($this->sqlLogger = new DebugStack());
48

Sergei Morozov's avatar
Sergei Morozov committed
49
        $cache = new ArrayCache();
50
        $config->setResultCacheImpl($cache);
51 52
    }

53
    protected function tearDown(): void
54
    {
Sergei Morozov's avatar
Sergei Morozov committed
55
        $this->connection->getSchemaManager()->dropTable('caching');
56 57 58 59

        parent::tearDown();
    }

60
    public function testCacheFetchAssociative(): void
61
    {
62
        $this->assertCacheNonCacheSelectSameFetchModeAreEqual(
63
            $this->expectedResult,
64 65
            static function (Result $result) {
                return $result->fetchAssociative();
66
            }
67
        );
68 69
    }

70
    public function testFetchNumeric(): void
71
    {
Sergei Morozov's avatar
Sergei Morozov committed
72
        $expectedResult = [];
jeroendedauw's avatar
jeroendedauw committed
73
        foreach ($this->expectedResult as $v) {
74 75
            $expectedResult[] = array_values($v);
        }
76

77 78
        $this->assertCacheNonCacheSelectSameFetchModeAreEqual(
            $expectedResult,
79 80
            static function (Result $result) {
                return $result->fetchNumeric();
81 82
            }
        );
83
    }
84

85
    public function testFetchOne(): void
86
    {
Sergei Morozov's avatar
Sergei Morozov committed
87
        $expectedResult = [];
jeroendedauw's avatar
jeroendedauw committed
88
        foreach ($this->expectedResult as $v) {
89 90
            $expectedResult[] = array_shift($v);
        }
91

92 93
        $this->assertCacheNonCacheSelectSameFetchModeAreEqual(
            $expectedResult,
94 95
            static function (Result $result) {
                return $result->fetchOne();
96 97
            }
        );
98
    }
99

100
    public function testMixingFetch(): void
101
    {
Sergei Morozov's avatar
Sergei Morozov committed
102
        $numExpectedResult = [];
jeroendedauw's avatar
jeroendedauw committed
103
        foreach ($this->expectedResult as $v) {
104 105
            $numExpectedResult[] = array_values($v);
        }
Grégoire Paris's avatar
Grégoire Paris committed
106

107
        $stmt = $this->connection->executeQuery('SELECT * FROM caching ORDER BY test_int ASC', [], [], new QueryCacheProfile(0, 'testcachekey'));
108

109 110
        $data = $this->hydrateViaFetchAll($stmt, static function (Result $result) {
            return $result->fetchAllAssociative();
111
        });
112

113
        self::assertEquals($this->expectedResult, $data);
114

115
        $stmt = $this->connection->executeQuery('SELECT * FROM caching ORDER BY test_int ASC', [], [], new QueryCacheProfile(0, 'testcachekey'));
116

117 118
        $data = $this->hydrateViaFetchAll($stmt, static function (Result $result) {
            return $result->fetchAllNumeric();
119
        });
120

121
        self::assertEquals($numExpectedResult, $data);
122 123
    }

124
    /**
125
     * @dataProvider fetchProvider
126
     */
127
    public function testFetchViaIteration(callable $fetch, callable $fetchAll): void
128
    {
129 130
        $stmt = $this->connection->executeQuery('SELECT * FROM caching ORDER BY test_int ASC', [], [], new QueryCacheProfile(0, 'testcachekey'));
        $data = $this->hydrateViaFetchAll($stmt, $fetchAll);
131

132 133
        $stmt         = $this->connection->executeQuery('SELECT * FROM caching ORDER BY test_int ASC', [], [], new QueryCacheProfile(0, 'testcachekey'));
        $dataIterator = $this->hydrateViaIteration($stmt, $fetch);
134

Grégoire Paris's avatar
Grégoire Paris committed
135
        self::assertEquals($data, $dataIterator);
136 137
    }

138
    public function testFetchAndFinishSavesCache(): void
139
    {
140
        $result = $this->connection->executeQuery('SELECT * FROM caching ORDER BY test_int ASC', [], [], new QueryCacheProfile(0, 'testcachekey'));
141

142
        while (($row = $result->fetchAssociative()) !== false) {
143 144 145
            $data[] = $row;
        }

146
        $result = $this->connection->executeQuery('SELECT * FROM caching ORDER BY test_int ASC', [], [], new QueryCacheProfile(0, 'testcachekey'));
147

148
        while (($row = $result->fetchNumeric()) !== false) {
149 150 151
            $data[] = $row;
        }

152
        self::assertCount(1, $this->sqlLogger->queries);
153 154
    }

155
    public function testDontFinishNoCache(): void
156
    {
157
        $result = $this->connection->executeQuery('SELECT * FROM caching ORDER BY test_int ASC', [], [], new QueryCacheProfile(0, 'testcachekey'));
158

159
        $result->fetchAssociative();
160

161
        $result = $this->connection->executeQuery('SELECT * FROM caching ORDER BY test_int ASC', [], [], new QueryCacheProfile(0, 'testcachekey'));
162

163 164
        $this->hydrateViaIteration($result, static function (Result $result) {
            return $result->fetchNumeric();
165
        });
166

Gabriel Caruso's avatar
Gabriel Caruso committed
167
        self::assertCount(2, $this->sqlLogger->queries);
168 169
    }

170
    public function testFetchAllSavesCache(): void
171 172
    {
        $layerCache = new ArrayCache();
173 174 175 176 177 178 179
        $result     = $this->connection->executeQuery(
            'SELECT * FROM caching WHERE test_int > 500',
            [],
            [],
            new QueryCacheProfile(0, 'testcachekey', $layerCache)
        );
        $result->fetchAllAssociative();
180 181 182 183

        self::assertCount(1, $layerCache->fetch('testcachekey'));
    }

184
    public function testFetchColumn(): void
185 186 187 188 189 190
    {
        $query = $this->connection->getDatabasePlatform()
            ->getDummySelectSQL('1');

        $qcp = new QueryCacheProfile(0, 0, new ArrayCache());

191 192
        $result = $this->connection->executeCacheQuery($query, [], [], $qcp);
        $result->fetchFirstColumn();
193

194
        $query = $this->connection->executeCacheQuery($query, [], [], $qcp);
195

196
        self::assertEquals([1], $query->fetchFirstColumn());
197 198
    }

199
    /**
200
     * @param array<int, array<int, int|string>>|list<int> $expectedResult
201
     */
202
    private function assertCacheNonCacheSelectSameFetchModeAreEqual(array $expectedResult, callable $fetchMode): void
203
    {
204
        $stmt = $this->connection->executeQuery('SELECT * FROM caching ORDER BY test_int ASC', [], [], new QueryCacheProfile(0, 'testcachekey'));
205

206
        self::assertEquals(2, $stmt->columnCount());
207
        $data = $this->hydrateViaIteration($stmt, $fetchMode);
208
        self::assertEquals($expectedResult, $data);
209

210
        $stmt = $this->connection->executeQuery('SELECT * FROM caching ORDER BY test_int ASC', [], [], new QueryCacheProfile(0, 'testcachekey'));
211

212
        self::assertEquals(2, $stmt->columnCount());
213
        $data = $this->hydrateViaIteration($stmt, $fetchMode);
214
        self::assertEquals($expectedResult, $data);
Sergei Morozov's avatar
Sergei Morozov committed
215
        self::assertCount(1, $this->sqlLogger->queries, 'just one dbal hit');
216 217
    }

218
    public function testEmptyResultCache(): void
219
    {
220
        $stmt = $this->connection->executeQuery('SELECT * FROM caching WHERE test_int > 500', [], [], new QueryCacheProfile(0, 'emptycachekey'));
221 222
        $this->hydrateViaIteration($stmt, static function (Result $result) {
            return $result->fetchAssociative();
223
        });
224

225
        $stmt = $this->connection->executeQuery('SELECT * FROM caching WHERE test_int > 500', [], [], new QueryCacheProfile(0, 'emptycachekey'));
226 227
        $this->hydrateViaIteration($stmt, static function (Result $result) {
            return $result->fetchAssociative();
228
        });
229

Sergei Morozov's avatar
Sergei Morozov committed
230
        self::assertCount(1, $this->sqlLogger->queries, 'just one dbal hit');
231
    }
232

233
    public function testChangeCacheImpl(): void
234
    {
235
        $stmt = $this->connection->executeQuery('SELECT * FROM caching WHERE test_int > 500', [], [], new QueryCacheProfile(0, 'emptycachekey'));
236 237
        $this->hydrateViaIteration($stmt, static function (Result $result) {
            return $result->fetchAssociative();
238
        });
239

Sergei Morozov's avatar
Sergei Morozov committed
240
        $secondCache = new ArrayCache();
241 242

        $stmt = $this->connection->executeQuery('SELECT * FROM caching WHERE test_int > 500', [], [], new QueryCacheProfile(0, 'emptycachekey', $secondCache));
243 244
        $this->hydrateViaIteration($stmt, static function (Result $result) {
            return $result->fetchAssociative();
245
        });
246

Sergei Morozov's avatar
Sergei Morozov committed
247 248
        self::assertCount(2, $this->sqlLogger->queries, 'two hits');
        self::assertCount(1, $secondCache->fetch('emptycachekey'));
249 250
    }

251 252 253
    /**
     * @return iterable<string,array<int,mixed>>
     */
254
    public static function fetchProvider(): iterable
255 256
    {
        yield 'associative' => [
257 258
            static function (Result $result) {
                return $result->fetchAssociative();
259
            },
260 261
            static function (Result $result) {
                return $result->fetchAllAssociative();
262
            },
263 264 265
        ];

        yield 'numeric' => [
266 267
            static function (Result $result) {
                return $result->fetchNumeric();
268
            },
269 270
            static function (Result $result) {
                return $result->fetchAllNumeric();
271 272 273 274
            },
        ];

        yield 'column' => [
275 276
            static function (Result $result) {
                return $result->fetchOne();
277
            },
278 279
            static function (Result $result) {
                return $result->fetchFirstColumn();
280
            },
281 282 283
        ];
    }

284 285 286
    /**
     * @return array<int, mixed>
     */
287
    private function hydrateViaFetchAll(Result $result, callable $fetchAll): array
288
    {
Sergei Morozov's avatar
Sergei Morozov committed
289
        $data = [];
290

291
        foreach ($fetchAll($result) as $row) {
292
            $data[] = is_array($row) ? array_change_key_case($row, CASE_LOWER) : $row;
293
        }
Grégoire Paris's avatar
Grégoire Paris committed
294

295
        return $data;
296
    }
297

298 299 300
    /**
     * @return array<int, mixed>
     */
301
    private function hydrateViaIteration(Result $result, callable $fetch): array
302
    {
Sergei Morozov's avatar
Sergei Morozov committed
303
        $data = [];
304

305
        while (($row = $fetch($result)) !== false) {
306
            $data[] = is_array($row) ? array_change_key_case($row, CASE_LOWER) : $row;
307
        }
Grégoire Paris's avatar
Grégoire Paris committed
308

309 310
        return $data;
    }
311
}