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