packages/ztd-query-mysqli-adapter/tests/Unit/Session/ConnectionExecutionTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Session;
6
7use Container\Endpoint;
8use Container\MySqlRelease;
9use mysqli;
10use mysqli_result;
11use mysqli_stmt;
12use PHPUnit\Framework\Attributes\CoversClass;
13use PHPUnit\Framework\Attributes\Large;
14use PHPUnit\Framework\Attributes\UsesClass;
15use PHPUnit\Framework\TestCase;
16use Testcontainers\Testcontainers;
17use ZtdQuery\Adapter\Mysqli\Driver\MysqliConnection;
18use ZtdQuery\Adapter\Mysqli\Driver\MysqliResultColumnExtractor;
19use ZtdQuery\Adapter\Mysqli\Driver\MysqliResultStatement;
20use ZtdQuery\Adapter\Mysqli\Session\ConnectionExecution;
21use ZtdQuery\Adapter\Mysqli\Session\MysqliResultProcessor;
22use ZtdQuery\Adapter\Mysqli\ZtdMysqliException;
23
24#[CoversClass(ConnectionExecution::class)]
25#[Large]
26#[UsesClass(MysqliConnection::class)]
27#[UsesClass(MysqliResultStatement::class)]
28#[UsesClass(MysqliResultColumnExtractor::class)]
29#[UsesClass(MysqliResultProcessor::class)]
30#[UsesClass(ZtdMysqliException::class)]
31final class ConnectionExecutionTest extends TestCase
32{
33    public function testNativeReturnsTheProvidedConnection(): void
34    {
35        $container = Testcontainers::run(MySqlRelease::fromEnvironment());
36        $endpoint = $container->getData(Endpoint::class);
37        try {
38            $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
39            $native->set_charset('utf8mb4');
40            $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
41            $execution = new ConnectionExecution($native);
42
43            self::assertSame($native, $execution->native());
44        } finally {
45            $container->stop();
46        }
47    }
48
49    public function testExecutorKeepsSimulatedWritesOffTheNativeConnection(): void
50    {
51        $container = Testcontainers::run(MySqlRelease::fromEnvironment());
52        $endpoint = $container->getData(Endpoint::class);
53        try {
54            $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
55            $native->set_charset('utf8mb4');
56            $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
57            $execution = new ConnectionExecution($native);
58
59            self::assertSame(1, $execution->executor()->execStatement('INSERT INTO items VALUES (7)'));
60            $result = $native->query('SELECT COUNT(*) FROM items');
61            self::assertInstanceOf(mysqli_result::class, $result);
62            self::assertSame(['0'], $result->fetch_row());
63        } finally {
64            $container->stop();
65        }
66    }
67
68    public function testSimulatedAffectedRowsStartsWithoutAnOverride(): void
69    {
70        $container = Testcontainers::run(MySqlRelease::fromEnvironment());
71        $endpoint = $container->getData(Endpoint::class);
72        try {
73            $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
74            $native->set_charset('utf8mb4');
75            $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
76            $execution = new ConnectionExecution($native);
77
78            self::assertNull($execution->simulatedAffectedRows());
79        } finally {
80            $container->stop();
81        }
82    }
83
84    public function testPreparePreservesTheWrappingCallback(): void
85    {
86        $container = Testcontainers::run(MySqlRelease::fromEnvironment());
87        $endpoint = $container->getData(Endpoint::class);
88        try {
89            $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
90            $native->set_charset('utf8mb4');
91            $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
92            $execution = new ConnectionExecution($native);
93
94            $wrapped = false;
95            $stmt = $execution->prepare('SELECT 7 AS id', static function (mysqli_stmt $statement) use (&$wrapped): mysqli_stmt {
96                $wrapped = true;
97                return $statement;
98            });
99            self::assertTrue($wrapped);
100            self::assertInstanceOf(mysqli_stmt::class, $stmt);
101            self::assertTrue($stmt->execute());
102        } finally {
103            $container->stop();
104        }
105    }
106
107    public function testQueryPreservesFacadeDispatch(): void
108    {
109        $container = Testcontainers::run(MySqlRelease::fromEnvironment());
110        $endpoint = $container->getData(Endpoint::class);
111        try {
112            $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
113            $native->set_charset('utf8mb4');
114            $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
115            $execution = new ConnectionExecution($native);
116
117            $queries = [];
118            self::assertFalse($execution->query('SELECT 7 AS id', MYSQLI_STORE_RESULT, static function (string $sql) use (&$queries): bool {
119                $queries[] = $sql;
120                return false;
121            }));
122            self::assertSame(['SELECT 7 AS id'], $queries);
123        } finally {
124            $container->stop();
125        }
126    }
127
128    public function testRealQuerySynchronizesTransactionStatements(): void
129    {
130        $container = Testcontainers::run(MySqlRelease::fromEnvironment());
131        $endpoint = $container->getData(Endpoint::class);
132        try {
133            $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
134            $native->set_charset('utf8mb4');
135            $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
136            $execution = new ConnectionExecution($native);
137
138            self::assertTrue($execution->realQuery('BEGIN', $native->prepare(...)));
139            self::assertSame(1, $execution->executor()->execStatement('INSERT INTO items VALUES (7)'));
140            self::assertTrue($execution->realQuery('ROLLBACK', $native->prepare(...)));
141            $result = $native->query($execution->executor()->rewrite('SELECT id FROM items')->sql());
142            self::assertInstanceOf(mysqli_result::class, $result);
143            self::assertSame([], $result->fetch_all(MYSQLI_ASSOC));
144        } finally {
145            $container->stop();
146        }
147    }
148
149    public function testBeginTransactionCreatesAShadowRollbackScope(): void
150    {
151        $container = Testcontainers::run(MySqlRelease::fromEnvironment());
152        $endpoint = $container->getData(Endpoint::class);
153        try {
154            $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
155            $native->set_charset('utf8mb4');
156            $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
157            $execution = new ConnectionExecution($native);
158
159            self::assertTrue($execution->beginTransaction());
160            self::assertSame(1, $execution->executor()->execStatement('INSERT INTO items VALUES (7)'));
161            self::assertTrue($execution->rollBack());
162            $result = $native->query($execution->executor()->rewrite('SELECT id FROM items')->sql());
163            self::assertInstanceOf(mysqli_result::class, $result);
164            self::assertSame([], $result->fetch_all(MYSQLI_ASSOC));
165        } finally {
166            $container->stop();
167        }
168    }
169
170    public function testCommitRetainsShadowRowsAcrossRollback(): void
171    {
172        $container = Testcontainers::run(MySqlRelease::fromEnvironment());
173        $endpoint = $container->getData(Endpoint::class);
174        try {
175            $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
176            $native->set_charset('utf8mb4');
177            $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
178            $execution = new ConnectionExecution($native);
179
180            self::assertTrue($execution->beginTransaction());
181            self::assertSame(1, $execution->executor()->execStatement('INSERT INTO items VALUES (7)'));
182            self::assertTrue($execution->commit());
183            self::assertTrue($execution->beginTransaction());
184            self::assertTrue($execution->rollBack());
185            $result = $native->query($execution->executor()->rewrite('SELECT id FROM items')->sql());
186            self::assertInstanceOf(mysqli_result::class, $result);
187            self::assertSame([['id' => '7']], $result->fetch_all(MYSQLI_ASSOC));
188        } finally {
189            $container->stop();
190        }
191    }
192
193    public function testRollBackRestoresTheShadowSnapshot(): void
194    {
195        $container = Testcontainers::run(MySqlRelease::fromEnvironment());
196        $endpoint = $container->getData(Endpoint::class);
197        try {
198            $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
199            $native->set_charset('utf8mb4');
200            $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
201            $execution = new ConnectionExecution($native);
202
203            self::assertTrue($execution->beginTransaction());
204            self::assertSame(1, $execution->executor()->execStatement('INSERT INTO items VALUES (7)'));
205            self::assertTrue($execution->rollBack());
206            $result = $native->query($execution->executor()->rewrite('SELECT id FROM items')->sql());
207            self::assertInstanceOf(mysqli_result::class, $result);
208            self::assertSame([], $result->fetch_all(MYSQLI_ASSOC));
209        } finally {
210            $container->stop();
211        }
212    }
213
214    public function testAutocommitCommitsShadowRows(): void
215    {
216        $container = Testcontainers::run(MySqlRelease::fromEnvironment());
217        $endpoint = $container->getData(Endpoint::class);
218        try {
219            $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
220            $native->set_charset('utf8mb4');
221            $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
222            $execution = new ConnectionExecution($native);
223
224            self::assertTrue($execution->autocommit(false));
225            self::assertSame(1, $execution->executor()->execStatement('INSERT INTO items VALUES (7)'));
226            self::assertTrue($execution->autocommit(true));
227            self::assertTrue($execution->beginTransaction());
228            self::assertTrue($execution->rollBack());
229            $result = $native->query($execution->executor()->rewrite('SELECT id FROM items')->sql());
230            self::assertInstanceOf(mysqli_result::class, $result);
231            self::assertSame([['id' => '7']], $result->fetch_all(MYSQLI_ASSOC));
232        } finally {
233            $container->stop();
234        }
235    }
236
237    public function testReleaseSavepointRemovesTheNativeSavepoint(): void
238    {
239        $container = Testcontainers::run(MySqlRelease::fromEnvironment());
240        $endpoint = $container->getData(Endpoint::class);
241        try {
242            $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
243            $native->set_charset('utf8mb4');
244            $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
245            $execution = new ConnectionExecution($native);
246
247            self::assertTrue($execution->beginTransaction());
248            self::assertTrue($execution->savepoint('one'));
249            self::assertTrue($execution->releaseSavepoint('one'));
250            self::assertTrue($execution->rollBack());
251        } finally {
252            $container->stop();
253        }
254    }
255
256    public function testSavepointRestoresShadowRowsOnRollbackToSavepoint(): void
257    {
258        $container = Testcontainers::run(MySqlRelease::fromEnvironment());
259        $endpoint = $container->getData(Endpoint::class);
260        try {
261            $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
262            $native->set_charset('utf8mb4');
263            $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
264            $execution = new ConnectionExecution($native);
265
266            self::assertTrue($execution->beginTransaction());
267            self::assertTrue($execution->savepoint('one'));
268            self::assertSame(1, $execution->executor()->execStatement('INSERT INTO items VALUES (7)'));
269            self::assertTrue($execution->realQuery('ROLLBACK TO SAVEPOINT one', $native->prepare(...)));
270            $result = $native->query($execution->executor()->rewrite('SELECT id FROM items')->sql());
271            self::assertInstanceOf(mysqli_result::class, $result);
272            self::assertSame([], $result->fetch_all(MYSQLI_ASSOC));
273            self::assertTrue($execution->rollBack());
274        } finally {
275            $container->stop();
276        }
277    }
278
279    public function testExecuteQueryRetainsTheDispatchedAffectedRowCount(): void
280    {
281        $container = Testcontainers::run(MySqlRelease::fromEnvironment());
282        $endpoint = $container->getData(Endpoint::class);
283        try {
284            $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
285            $native->set_charset('utf8mb4');
286            $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
287            $execution = new ConnectionExecution($native);
288
289            $result = $execution->executeQuery('SELECT ? AS id', [7], $native->prepare(...), static fn (): int => 7);
290            self::assertInstanceOf(mysqli_result::class, $result);
291            self::assertSame([['id' => '7']], $result->fetch_all(MYSQLI_ASSOC));
292            self::assertSame(7, $execution->simulatedAffectedRows());
293        } finally {
294            $container->stop();
295        }
296    }
297
298    public function testAffectedRowsUsesTheNativeCountBeforeSimulation(): void
299    {
300        $container = Testcontainers::run(MySqlRelease::fromEnvironment());
301        $endpoint = $container->getData(Endpoint::class);
302        try {
303            $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
304            $native->set_charset('utf8mb4');
305            $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
306            $execution = new ConnectionExecution($native);
307
308            $native->query('INSERT INTO items VALUES (1), (2)');
309            self::assertSame(2, $execution->affectedRows());
310        } finally {
311            $container->stop();
312        }
313    }
314
315    public function testBeginTransactionDefersTheDefaultSnapshotUntilTheFirstRead(): void
316    {
317        $container = Testcontainers::run(MySqlRelease::fromEnvironment());
318        $endpoint = $container->getData(Endpoint::class);
319        try {
320            $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
321            $native->set_charset('utf8mb4');
322            $other = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
323            try {
324                $native->query('CREATE TABLE snapshot_rows (id INT PRIMARY KEY) ENGINE=InnoDB');
325                $native->query('SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ');
326                $execution = new ConnectionExecution($native);
327                self::assertTrue($execution->beginTransaction());
328                $other->query('INSERT INTO snapshot_rows VALUES (1)');
329                $result = $native->query('SELECT id FROM snapshot_rows');
330                self::assertInstanceOf(mysqli_result::class, $result);
331                self::assertSame([['id' => '1']], $result->fetch_all(MYSQLI_ASSOC));
332            } finally {
333                $native->rollback();
334                $other->close();
335                $native->close();
336            }
337        } finally {
338            $container->stop();
339        }
340    }
341
342    public function testCommitEndsTheNativeTransactionWithoutStartingAnother(): void
343    {
344        $container = Testcontainers::run(MySqlRelease::fromEnvironment());
345        $endpoint = $container->getData(Endpoint::class);
346        try {
347            $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
348            $native->set_charset('utf8mb4');
349            $execution = new ConnectionExecution($native);
350            self::assertTrue($execution->beginTransaction());
351            self::assertTrue($execution->commit());
352            self::assertTrue($native->query('SET TRANSACTION ISOLATION LEVEL READ COMMITTED'));
353            $native->close();
354        } finally {
355            $container->stop();
356        }
357    }
358
359    public function testRollBackEndsTheNativeTransactionWithoutStartingAnother(): void
360    {
361        $container = Testcontainers::run(MySqlRelease::fromEnvironment());
362        $endpoint = $container->getData(Endpoint::class);
363        try {
364            $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
365            $native->set_charset('utf8mb4');
366            $execution = new ConnectionExecution($native);
367            self::assertTrue($execution->beginTransaction());
368            self::assertTrue($execution->rollBack());
369            self::assertTrue($native->query('SET TRANSACTION ISOLATION LEVEL READ COMMITTED'));
370            $native->close();
371        } finally {
372            $container->stop();
373        }
374    }
375
376}
377