packages/ztd-query-mysqli-adapter/tests/Unit/ZtdMysqliTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit;
6
7use Container\MySql80Container;
8use Container\MySql84Container;
9use mysqli;
10use mysqli_result;
11use mysqli_sql_exception;
12use mysqli_stmt;
13use mysqli_warning;
14use PHPUnit\Framework\Attributes\CoversClass;
15use PHPUnit\Framework\Attributes\Large;
16use PHPUnit\Framework\Attributes\UsesClass;
17use PHPUnit\Framework\TestCase;
18use Testcontainers\Testcontainers;
19use ZtdQuery\Adapter\Mysqli\Driver\MysqliConnection;
20use ZtdQuery\Adapter\Mysqli\Driver\MysqliResultColumnExtractor;
21use ZtdQuery\Adapter\Mysqli\Driver\MysqliResultStatement;
22use ZtdQuery\Adapter\Mysqli\Native\MysqliPropertyReader;
23use ZtdQuery\Adapter\Mysqli\Native\MysqliStatementBindingBridge;
24use ZtdQuery\Adapter\Mysqli\Session\MysqliResultProcessor;
25use ZtdQuery\Adapter\Mysqli\ZtdMysqli;
26use ZtdQuery\Adapter\Mysqli\ZtdMysqliException;
27use ZtdQuery\Adapter\Mysqli\ZtdMysqliStatement;
28use ZtdQuery\Config\ZtdConfig;
29use ZtdQuery\Connection\ConnectionInterface;
30use ZtdQuery\Exception\UnsupportedSqlException;
31use ZtdQuery\Platform\MySql\MySqlSessionFactory;
32use ZtdQuery\Platform\MySql\MySqlTransactionStatementParser;
33use ZtdQuery\Platform\SessionFactory;
34use ZtdQuery\ResultSelectRunner;
35use ZtdQuery\Rewrite\QueryKind;
36use ZtdQuery\Rewrite\RewritePlan;
37use ZtdQuery\Rewrite\SqlRewriter;
38use ZtdQuery\Session;
39use ZtdQuery\Shadow\ShadowStore;
40use ZtdQuery\Sql\TransactionStatement;
41
42#[CoversClass(ZtdMysqli::class)]
43#[CoversClass(\ZtdQuery\Adapter\Mysqli\Session\ConnectionExecution::class)]
44#[Large]
45#[UsesClass(MysqliConnection::class)]
46#[UsesClass(MysqliPropertyReader::class)]
47#[UsesClass(ZtdMysqliStatement::class)]
48#[UsesClass(\ZtdQuery\Adapter\Mysqli\Session\StatementExecution::class)]
49#[UsesClass(ZtdMysqliException::class)]
50#[UsesClass(MysqliStatementBindingBridge::class)]
51#[UsesClass(MysqliResultProcessor::class)]
52#[UsesClass(MysqliResultStatement::class)]
53#[UsesClass(MysqliResultColumnExtractor::class)]
54final class ZtdMysqliTest extends TestCase
55{
56    public function testQueryUsesTheProvidedFactoryAndConfiguration(): void
57    {
58        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
59        try {
60            $host = str_replace('localhost', '127.0.0.1', $container->getHost());
61            $port = $container->getMappedPort(3306);
62            self::assertNotNull($port);
63            $config = new ZtdConfig();
64            $factory = self::createMock(SessionFactory::class);
65            $factory->expects(self::once())->method('create')
66                ->with(self::isInstanceOf(MysqliConnection::class), self::identicalTo($config))
67                ->willReturnCallback((new MySqlSessionFactory())->create(...));
68            $ztd = new ZtdMysqli($host, 'root', 'root', 'test', $port, null, $config, $factory);
69            self::assertSame(mysqli_get_client_info(), $ztd->client_info);
70            $result = $ztd->query('SELECT 42 AS id');
71            self::assertInstanceOf(mysqli_result::class, $result);
72            self::assertSame([['id' => 42]], $result->fetch_all(MYSQLI_ASSOC));
73            $ztd->close();
74        } finally {
75            $container->stop();
76        }
77    }
78
79    public function testBegin_transactionDefersTheDefaultSnapshotUntilTheFirstRead(): void
80    {
81        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
82        try {
83            $host = str_replace('localhost', '127.0.0.1', $container->getHost());
84            $port = $container->getMappedPort(3306);
85            self::assertNotNull($port);
86            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
87            $connection->set_charset('utf8mb4');
88            $database = 'test';
89            $other = new mysqli($host, 'root', 'root', $database, $port);
90            try {
91                $connection->query('CREATE TABLE snapshot_rows (id INT PRIMARY KEY) ENGINE=InnoDB');
92                $connection->query('SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ');
93                $ztd = ZtdMysqli::fromMysqli($connection);
94                self::assertTrue($ztd->begin_transaction());
95                $other->query('INSERT INTO snapshot_rows VALUES (1)');
96                $result = $connection->query('SELECT id FROM snapshot_rows');
97                self::assertInstanceOf(mysqli_result::class, $result);
98                self::assertSame([['id' => '1']], $result->fetch_all(MYSQLI_ASSOC));
99            } finally {
100                $connection->rollback();
101                $other->close();
102                $connection->close();
103            }
104        } finally {
105            $container->stop();
106        }
107    }
108
109    public function testFromMysqliPassesTheConnectionAndConfigurationToItsFactory(): void
110    {
111        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
112        try {
113            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
114            $connection->set_charset('utf8mb4');
115            $config = ZtdConfig::default();
116            $rewriter = self::createStub(SqlRewriter::class);
117            $factory = self::createMock(SessionFactory::class);
118            $factory->expects(self::once())->method('create')->with(self::isInstanceOf(MysqliConnection::class), self::identicalTo($config))
119                ->willReturnCallback(static fn (ConnectionInterface $native, ZtdConfig $resolved): Session => new Session($rewriter, new ShadowStore(), new ResultSelectRunner(), $resolved, $native));
120            $ztd = ZtdMysqli::fromMysqli($connection, $config, $factory);
121            self::assertTrue($ztd->isZtdEnabled());
122            $connection->close();
123        } finally {
124            $container->stop();
125        }
126    }
127
128    public function testEnableZtdRestoresDisabledMode(): void
129    {
130        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
131        try {
132            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
133            $connection->set_charset('utf8mb4');
134            $ztd = ZtdMysqli::fromMysqli($connection);
135            $ztd->disableZtd();
136            self::assertFalse($ztd->isZtdEnabled());
137            $ztd->enableZtd();
138            self::assertTrue($ztd->isZtdEnabled());
139            $connection->close();
140        } finally {
141            $container->stop();
142        }
143    }
144
145    public function testDisableZtdDisablesRewriting(): void
146    {
147        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
148        try {
149            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
150            $connection->set_charset('utf8mb4');
151            $ztd = ZtdMysqli::fromMysqli($connection);
152            self::assertTrue($ztd->isZtdEnabled());
153            $ztd->disableZtd();
154            self::assertFalse($ztd->isZtdEnabled());
155            $connection->close();
156        } finally {
157            $container->stop();
158        }
159    }
160
161    public function testIsZtdEnabledDefaultsToTrue(): void
162    {
163        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
164        try {
165            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
166            $connection->set_charset('utf8mb4');
167            $ztd = ZtdMysqli::fromMysqli($connection);
168            self::assertTrue($ztd->isZtdEnabled());
169            $connection->close();
170        } finally {
171            $container->stop();
172        }
173    }
174
175    public function testPrepareReturnsTheNativeStatementWhenDisabled(): void
176    {
177        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
178        try {
179            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
180            $connection->set_charset('utf8mb4');
181            $ztd = ZtdMysqli::fromMysqli($connection);
182            $ztd->disableZtd();
183            $statement = $ztd->prepare('SELECT 7 AS id');
184            self::assertInstanceOf(mysqli_stmt::class, $statement);
185            self::assertNotInstanceOf(ZtdMysqliStatement::class, $statement);
186            self::assertTrue($statement->execute());
187            $result = $statement->get_result();
188            self::assertInstanceOf(mysqli_result::class, $result);
189            self::assertSame([['id' => 7]], $result->fetch_all(MYSQLI_ASSOC));
190            $connection->close();
191        } finally {
192            $container->stop();
193        }
194    }
195
196    public function testPrepareReturnsASimulatedStatementWhenEnabled(): void
197    {
198        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
199        try {
200            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
201            $connection->set_charset('utf8mb4');
202            $ztd = ZtdMysqli::fromMysqli($connection);
203            $statement = $ztd->prepare('SELECT 7 AS id');
204            self::assertInstanceOf(ZtdMysqliStatement::class, $statement);
205            self::assertTrue($statement->execute());
206            $result = $statement->get_result();
207            self::assertInstanceOf(mysqli_result::class, $result);
208            self::assertSame([['id' => 7]], $result->fetch_all(MYSQLI_ASSOC));
209            $connection->close();
210        } finally {
211            $container->stop();
212        }
213    }
214
215    public function testPrepareWrapsRewriteFailures(): void
216    {
217        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
218        try {
219            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
220            $connection->set_charset('utf8mb4');
221            $store = new ShadowStore();
222            $rewriter = self::createStub(SqlRewriter::class);
223            $rewriter->method('transactionStatement')->willReturnCallback((new MySqlTransactionStatementParser())->parse(...));
224            $factory = self::createStub(SessionFactory::class);
225            $factory->method('create')->willReturnCallback(static fn (ConnectionInterface $native, ZtdConfig $config): Session => new Session($rewriter, $store, new ResultSelectRunner(), $config, $native));
226            $ztd = ZtdMysqli::fromMysqli($connection, null, $factory);
227            $rewriter->method('rewrite')->willThrowException(new UnsupportedSqlException('DROP DATABASE forbidden', 'Unsupported'));
228            try {
229                $ztd->prepare('DROP DATABASE forbidden');
230                self::fail('Expected write protection.');
231            } catch (ZtdMysqliException $exception) {
232                self::assertStringContainsString('ZTD Write Protection', $exception->getMessage());
233                self::assertSame(0, $exception->getCode());
234                self::assertNotNull($exception->getPrevious());
235            } finally {
236                $connection->close();
237            }
238        } finally {
239            $container->stop();
240        }
241    }
242
243    public function testPrepareAndQueriesPreserveNativeFalseResults(): void
244    {
245        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
246        try {
247            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
248            $connection->set_charset('utf8mb4');
249            $store = new ShadowStore();
250            $rewriter = self::createStub(SqlRewriter::class);
251            $rewriter->method('transactionStatement')->willReturnCallback((new MySqlTransactionStatementParser())->parse(...));
252            $factory = self::createStub(SessionFactory::class);
253            $factory->method('create')->willReturnCallback(static fn (ConnectionInterface $native, ZtdConfig $config): Session => new Session($rewriter, $store, new ResultSelectRunner(), $config, $native));
254            $ztd = ZtdMysqli::fromMysqli($connection, null, $factory);
255            $rewriter->method('rewrite')->willReturn(new RewritePlan('SELECT missing_column', QueryKind::READ));
256            mysqli_report(MYSQLI_REPORT_OFF);
257            try {
258                self::assertFalse($ztd->prepare('SELECT missing_column'));
259                self::assertFalse($ztd->query('SELECT missing_column'));
260                self::assertFalse($ztd->real_query('SELECT missing_column'));
261                self::assertFalse($ztd->execute_query('SELECT missing_column'));
262            } finally {
263                mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
264                $connection->close();
265            }
266        } finally {
267            $container->stop();
268        }
269    }
270
271    public function testQueryReadsTheNativeResultWhenDisabled(): void
272    {
273        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
274        try {
275            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
276            $connection->set_charset('utf8mb4');
277            $ztd = ZtdMysqli::fromMysqli($connection);
278            $ztd->disableZtd();
279            $result = $ztd->query('SELECT 7 AS id', MYSQLI_USE_RESULT);
280            self::assertInstanceOf(mysqli_result::class, $result);
281            self::assertSame([['id' => '7']], $result->fetch_all(MYSQLI_ASSOC));
282            $connection->close();
283        } finally {
284            $container->stop();
285        }
286    }
287
288    public function testQueryReturnsFalseWhenThePreparedExecutionFails(): void
289    {
290        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
291        try {
292            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
293            $connection->set_charset('utf8mb4');
294            $store = new ShadowStore();
295            $rewriter = self::createStub(SqlRewriter::class);
296            $rewriter->method('transactionStatement')->willReturnCallback((new MySqlTransactionStatementParser())->parse(...));
297            $factory = self::createStub(SessionFactory::class);
298            $factory->method('create')->willReturnCallback(static fn (ConnectionInterface $native, ZtdConfig $config): Session => new Session($rewriter, $store, new ResultSelectRunner(), $config, $native));
299            $ztd = ZtdMysqli::fromMysqli($connection, null, $factory);
300            $connection->query('CREATE TEMPORARY TABLE duplicate_keys (id INT PRIMARY KEY)');
301            $connection->query('INSERT INTO duplicate_keys VALUES (1)');
302            $rewriter->method('rewrite')->willReturn(new RewritePlan('INSERT INTO duplicate_keys VALUES (1)', QueryKind::READ));
303            mysqli_report(MYSQLI_REPORT_OFF);
304            try {
305                self::assertFalse($ztd->query('INSERT INTO duplicate_keys VALUES (1)'));
306                self::assertFalse($ztd->execute_query('INSERT INTO duplicate_keys VALUES (1)'));
307            } finally {
308                mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
309                $connection->close();
310            }
311        } finally {
312            $container->stop();
313        }
314    }
315
316    public function testQueryReturnsTrueWithoutAResultSet(): void
317    {
318        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
319        try {
320            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
321            $connection->set_charset('utf8mb4');
322            $store = new ShadowStore();
323            $rewriter = self::createStub(SqlRewriter::class);
324            $rewriter->method('transactionStatement')->willReturnCallback((new MySqlTransactionStatementParser())->parse(...));
325            $factory = self::createStub(SessionFactory::class);
326            $factory->method('create')->willReturnCallback(static fn (ConnectionInterface $native, ZtdConfig $config): Session => new Session($rewriter, $store, new ResultSelectRunner(), $config, $native));
327            $ztd = ZtdMysqli::fromMysqli($connection, null, $factory);
328            $rewriter->method('rewrite')->willReturn(new RewritePlan('DO 1', QueryKind::READ));
329            self::assertTrue($ztd->query('DO 1'));
330            $connection->close();
331        } finally {
332            $container->stop();
333        }
334    }
335
336    public function testExecute_queryBindsParametersInBothModes(): void
337    {
338        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
339        try {
340            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
341            $connection->set_charset('utf8mb4');
342            $ztd = ZtdMysqli::fromMysqli($connection);
343            $result = $ztd->execute_query('SELECT ? AS value', ['enabled']);
344            self::assertInstanceOf(mysqli_result::class, $result);
345            self::assertSame([['value' => 'enabled']], $result->fetch_all(MYSQLI_ASSOC));
346            $ztd->disableZtd();
347            $result = $ztd->execute_query('SELECT ? AS value', ['disabled']);
348            self::assertInstanceOf(mysqli_result::class, $result);
349            self::assertSame([['value' => 'disabled']], $result->fetch_all(MYSQLI_ASSOC));
350            $connection->close();
351        } finally {
352            $container->stop();
353        }
354    }
355
356    public function testLastAffectedRowsUsesTheNativeCountWhenDisabled(): void
357    {
358        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
359        try {
360            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
361            $connection->set_charset('utf8mb4');
362            $ztd = ZtdMysqli::fromMysqli($connection);
363            $ztd->disableZtd();
364            $ztd->query('CREATE TEMPORARY TABLE counts (id INT)');
365            self::assertTrue($ztd->query('INSERT INTO counts VALUES (1), (2)'));
366            self::assertSame(2, $ztd->lastAffectedRows());
367            $connection->close();
368        } finally {
369            $container->stop();
370        }
371    }
372
373    public function testBegin_transactionCreatesAShadowRollbackScope(): void
374    {
375        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
376        try {
377            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
378            $connection->set_charset('utf8mb4');
379            $store = new ShadowStore();
380            $rewriter = self::createStub(SqlRewriter::class);
381            $rewriter->method('transactionStatement')->willReturnCallback((new MySqlTransactionStatementParser())->parse(...));
382            $factory = self::createStub(SessionFactory::class);
383            $factory->method('create')->willReturnCallback(static fn (ConnectionInterface $native, ZtdConfig $config): Session => new Session($rewriter, $store, new ResultSelectRunner(), $config, $native));
384            $ztd = ZtdMysqli::fromMysqli($connection, null, $factory);
385            $store->set('items', [['id' => 1]]);
386            self::assertTrue($ztd->begin_transaction(MYSQLI_TRANS_START_READ_WRITE, 'scope'));
387            $store->insert('items', [['id' => 2]]);
388            self::assertTrue($ztd->rollback());
389            self::assertSame([['id' => 1]], $store->get('items'));
390            $connection->close();
391        } finally {
392            $container->stop();
393        }
394    }
395
396    public function testCommitAndRollbackEndNativeTransactionsByDefault(): void
397    {
398        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
399        try {
400            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
401            $connection->set_charset('utf8mb4');
402            $ztd = ZtdMysqli::fromMysqli($connection);
403            self::assertTrue($ztd->begin_transaction());
404            self::assertTrue($ztd->commit());
405            self::assertTrue($connection->query('SET TRANSACTION ISOLATION LEVEL READ COMMITTED'));
406            self::assertTrue($ztd->begin_transaction());
407            self::assertTrue($ztd->rollback());
408            self::assertTrue($connection->query('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ'));
409            $connection->close();
410        } finally {
411            $container->stop();
412        }
413    }
414
415    public function testCommitRetainsTheShadowRows(): void
416    {
417        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
418        try {
419            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
420            $connection->set_charset('utf8mb4');
421            $store = new ShadowStore();
422            $rewriter = self::createStub(SqlRewriter::class);
423            $rewriter->method('transactionStatement')->willReturnCallback((new MySqlTransactionStatementParser())->parse(...));
424            $factory = self::createStub(SessionFactory::class);
425            $factory->method('create')->willReturnCallback(static fn (ConnectionInterface $native, ZtdConfig $config): Session => new Session($rewriter, $store, new ResultSelectRunner(), $config, $native));
426            $ztd = ZtdMysqli::fromMysqli($connection, null, $factory);
427            $store->set('items', [['id' => 1]]);
428            $ztd->begin_transaction();
429            $store->insert('items', [['id' => 2]]);
430            self::assertTrue($ztd->commit(0, 'commit_scope'));
431            $ztd->rollback();
432            self::assertSame([['id' => 1], ['id' => 2]], $store->get('items'));
433            $connection->close();
434        } finally {
435            $container->stop();
436        }
437    }
438
439    public function testRollbackRestoresTheShadowSnapshot(): void
440    {
441        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
442        try {
443            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
444            $connection->set_charset('utf8mb4');
445            $store = new ShadowStore();
446            $rewriter = self::createStub(SqlRewriter::class);
447            $rewriter->method('transactionStatement')->willReturnCallback((new MySqlTransactionStatementParser())->parse(...));
448            $factory = self::createStub(SessionFactory::class);
449            $factory->method('create')->willReturnCallback(static fn (ConnectionInterface $native, ZtdConfig $config): Session => new Session($rewriter, $store, new ResultSelectRunner(), $config, $native));
450            $ztd = ZtdMysqli::fromMysqli($connection, null, $factory);
451            $store->set('items', [['id' => 1]]);
452            $ztd->begin_transaction();
453            $store->insert('items', [['id' => 2]]);
454            self::assertTrue($ztd->rollback(0, 'rollback_scope'));
455            self::assertSame([['id' => 1]], $store->get('items'));
456            $connection->close();
457        } finally {
458            $container->stop();
459        }
460    }
461
462    public function testAutocommitCommitsOrRollsBackTheShadowScope(): void
463    {
464        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
465        try {
466            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
467            $connection->set_charset('utf8mb4');
468            $store = new ShadowStore();
469            $rewriter = self::createStub(SqlRewriter::class);
470            $rewriter->method('transactionStatement')->willReturnCallback((new MySqlTransactionStatementParser())->parse(...));
471            $factory = self::createStub(SessionFactory::class);
472            $factory->method('create')->willReturnCallback(static fn (ConnectionInterface $native, ZtdConfig $config): Session => new Session($rewriter, $store, new ResultSelectRunner(), $config, $native));
473            $ztd = ZtdMysqli::fromMysqli($connection, null, $factory);
474            $store->set('items', [['id' => 1]]);
475            self::assertTrue($ztd->autocommit(false));
476            $store->insert('items', [['id' => 2]]);
477            $ztd->rollback();
478            self::assertSame([['id' => 1]], $store->get('items'));
479            self::assertTrue($ztd->autocommit(false));
480            $store->insert('items', [['id' => 3]]);
481            self::assertTrue($ztd->autocommit(true));
482            $ztd->rollback();
483            self::assertSame([['id' => 1], ['id' => 3]], $store->get('items'));
484            $connection->close();
485        } finally {
486            $container->stop();
487        }
488    }
489
490    public function testReal_queryAppliesTransactionStatements(): void
491    {
492        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
493        try {
494            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
495            $connection->set_charset('utf8mb4');
496            $store = new ShadowStore();
497            $rewriter = self::createStub(SqlRewriter::class);
498            $rewriter->method('transactionStatement')->willReturnCallback((new MySqlTransactionStatementParser())->parse(...));
499            $factory = self::createStub(SessionFactory::class);
500            $factory->method('create')->willReturnCallback(static fn (ConnectionInterface $native, ZtdConfig $config): Session => new Session($rewriter, $store, new ResultSelectRunner(), $config, $native));
501            $ztd = ZtdMysqli::fromMysqli($connection, null, $factory);
502            $store->set('items', [['id' => 1]]);
503            self::assertTrue($ztd->real_query('BEGIN'));
504            $store->insert('items', [['id' => 2]]);
505            self::assertTrue($ztd->real_query('ROLLBACK'));
506            self::assertSame([['id' => 1]], $store->get('items'));
507            $connection->close();
508        } finally {
509            $container->stop();
510        }
511    }
512
513    public function testSavepointPreservesItsShadowSnapshot(): void
514    {
515        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
516        try {
517            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
518            $connection->set_charset('utf8mb4');
519            $store = new ShadowStore();
520            $rewriter = self::createStub(SqlRewriter::class);
521            $rewriter->method('transactionStatement')->willReturnCallback((new MySqlTransactionStatementParser())->parse(...));
522            $factory = self::createStub(SessionFactory::class);
523            $factory->method('create')->willReturnCallback(static fn (ConnectionInterface $native, ZtdConfig $config): Session => new Session($rewriter, $store, new ResultSelectRunner(), $config, $native));
524            $ztd = ZtdMysqli::fromMysqli($connection, null, $factory);
525            $store->set('items', [['id' => 1]]);
526            $ztd->begin_transaction();
527            self::assertTrue($ztd->savepoint('scope'));
528            $store->insert('items', [['id' => 2]]);
529            self::assertTrue($ztd->query('ROLLBACK TO SAVEPOINT scope'));
530            self::assertSame([['id' => 1]], $store->get('items'));
531            $connection->close();
532        } finally {
533            $container->stop();
534        }
535    }
536
537    public function testRelease_savepointRemovesTheNativeAndShadowSavepoints(): void
538    {
539        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
540        try {
541            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
542            $connection->set_charset('utf8mb4');
543            $store = new ShadowStore();
544            $rewriter = self::createStub(SqlRewriter::class);
545            $rewriter->method('transactionStatement')->willReturnCallback((new MySqlTransactionStatementParser())->parse(...));
546            $session = new Session($rewriter, $store, new ResultSelectRunner(), new ZtdConfig(), new MysqliConnection($connection));
547            $factory = self::createStub(SessionFactory::class);
548            $factory->method('create')->willReturn($session);
549            $ztd = ZtdMysqli::fromMysqli($connection, null, $factory);
550            $store->set('items', [['id' => 1]]);
551            $ztd->begin_transaction();
552            $ztd->savepoint('scope');
553            $store->insert('items', [['id' => 2]]);
554            self::assertTrue($ztd->release_savepoint('scope'));
555            $session->applyTransactionStatement(TransactionStatement::rollbackTo('scope'));
556            self::assertSame([['id' => 1], ['id' => 2]], $store->get('items'));
557            $this->expectException(mysqli_sql_exception::class);
558            try {
559                $ztd->query('ROLLBACK TO SAVEPOINT scope');
560            } finally {
561                $connection->close();
562            }
563        } finally {
564            $container->stop();
565        }
566    }
567
568    public function testReal_queryExecutesTheNativeQueryWhenDisabled(): void
569    {
570        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
571        try {
572            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
573            $connection->set_charset('utf8mb4');
574            $ztd = ZtdMysqli::fromMysqli($connection);
575            $ztd->disableZtd();
576            self::assertTrue($ztd->real_query('SELECT 7 AS id'));
577            $result = $connection->store_result();
578            self::assertInstanceOf(mysqli_result::class, $result);
579            self::assertSame([['id' => '7']], $result->fetch_all(MYSQLI_ASSOC));
580            $connection->close();
581        } finally {
582            $container->stop();
583        }
584    }
585
586    public function testMulti_queryExposesEachNativeResult(): void
587    {
588        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
589        try {
590            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
591            $connection->set_charset('utf8mb4');
592            $ztd = ZtdMysqli::fromMysqli($connection);
593            self::assertTrue($ztd->multi_query('SELECT 1 AS id; SELECT 2 AS id'));
594            $first = $ztd->store_result();
595            self::assertInstanceOf(mysqli_result::class, $first);
596            self::assertSame([['id' => '1']], $first->fetch_all(MYSQLI_ASSOC));
597            self::assertTrue($ztd->more_results());
598            self::assertTrue($ztd->next_result());
599            $second = $ztd->store_result();
600            self::assertInstanceOf(mysqli_result::class, $second);
601            self::assertSame([['id' => '2']], $second->fetch_all(MYSQLI_ASSOC));
602            $connection->close();
603        } finally {
604            $container->stop();
605        }
606    }
607
608    public function testMore_resultsObservesPendingNativeStatements(): void
609    {
610        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
611        try {
612            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
613            $connection->set_charset('utf8mb4');
614            $ztd = ZtdMysqli::fromMysqli($connection);
615            $connection->multi_query('SELECT 1; SELECT 2');
616            $first = $connection->store_result();
617            self::assertInstanceOf(mysqli_result::class, $first);
618            $first->free();
619            self::assertTrue($ztd->more_results());
620            $connection->next_result();
621            $second = $connection->store_result();
622            self::assertInstanceOf(mysqli_result::class, $second);
623            $second->free();
624            self::assertFalse($ztd->more_results());
625            $connection->close();
626        } finally {
627            $container->stop();
628        }
629    }
630
631    public function testNext_resultAdvancesTheNativeResultSequence(): void
632    {
633        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
634        try {
635            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
636            $connection->set_charset('utf8mb4');
637            $ztd = ZtdMysqli::fromMysqli($connection);
638            $connection->multi_query('SELECT 1; SELECT 2');
639            $first = $connection->store_result();
640            self::assertInstanceOf(mysqli_result::class, $first);
641            $first->free();
642            self::assertTrue($ztd->next_result());
643            $second = $connection->store_result();
644            self::assertInstanceOf(mysqli_result::class, $second);
645            $second->free();
646            self::assertFalse($ztd->next_result());
647            $connection->close();
648        } finally {
649            $container->stop();
650        }
651    }
652
653    public function testSelect_dbChangesTheNativeDatabase(): void
654    {
655        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
656        try {
657            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
658            $connection->set_charset('utf8mb4');
659            $ztd = ZtdMysqli::fromMysqli($connection);
660            self::assertTrue($ztd->select_db('information_schema'));
661            $result = $connection->query('SELECT DATABASE() AS name');
662            self::assertInstanceOf(mysqli_result::class, $result);
663            self::assertSame([['name' => 'information_schema']], $result->fetch_all(MYSQLI_ASSOC));
664            $connection->close();
665        } finally {
666            $container->stop();
667        }
668    }
669
670    public function testSet_charsetChangesTheNativeEncoding(): void
671    {
672        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
673        try {
674            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
675            $connection->set_charset('utf8mb4');
676            $ztd = ZtdMysqli::fromMysqli($connection);
677            self::assertTrue($ztd->set_charset('latin1'));
678            self::assertSame('latin1', $connection->character_set_name());
679            $connection->close();
680        } finally {
681            $container->stop();
682        }
683    }
684
685    public function testCharacter_set_nameReadsTheNativeEncoding(): void
686    {
687        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
688        try {
689            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
690            $connection->set_charset('utf8mb4');
691            $ztd = ZtdMysqli::fromMysqli($connection);
692            $connection->set_charset('latin1');
693            self::assertSame('latin1', $ztd->character_set_name());
694            $connection->close();
695        } finally {
696            $container->stop();
697        }
698    }
699
700    public function testReal_escape_stringEscapesWithTheNativeConnection(): void
701    {
702        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
703        try {
704            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
705            $connection->set_charset('utf8mb4');
706            $ztd = ZtdMysqli::fromMysqli($connection);
707            self::assertSame("O\\'Reilly", $ztd->real_escape_string("O'Reilly"));
708            $connection->close();
709        } finally {
710            $container->stop();
711        }
712    }
713
714    public function testEscape_stringRetainsTheNativeAliasBehavior(): void
715    {
716        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
717        try {
718            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
719            $connection->set_charset('utf8mb4');
720            $ztd = ZtdMysqli::fromMysqli($connection);
721            self::assertSame("O\\'Reilly", $ztd->escape_string("O'Reilly"));
722            $connection->close();
723        } finally {
724            $container->stop();
725        }
726    }
727
728    public function testChange_userChangesTheSelectedDatabase(): void
729    {
730        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
731        try {
732            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
733            $connection->set_charset('utf8mb4');
734            $ztd = ZtdMysqli::fromMysqli($connection);
735            self::assertTrue($ztd->change_user('root', 'root', 'information_schema'));
736            $result = $connection->query('SELECT DATABASE() AS name');
737            self::assertInstanceOf(mysqli_result::class, $result);
738            self::assertSame([['name' => 'information_schema']], $result->fetch_all(MYSQLI_ASSOC));
739            $connection->close();
740        } finally {
741            $container->stop();
742        }
743    }
744
745    public function testGet_charsetReturnsTheNativeCharacterSet(): void
746    {
747        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
748        try {
749            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
750            $connection->set_charset('utf8mb4');
751            $ztd = ZtdMysqli::fromMysqli($connection);
752            $connection->set_charset('latin1');
753            $charset = $ztd->get_charset();
754            self::assertNotNull($charset);
755            self::assertSame('latin1', get_object_vars($charset)['charset']);
756            $connection->close();
757        } finally {
758            $container->stop();
759        }
760    }
761
762    public function testGet_server_infoReturnsTheNativeServerVersion(): void
763    {
764        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
765        try {
766            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
767            $connection->set_charset('utf8mb4');
768            $ztd = ZtdMysqli::fromMysqli($connection);
769            self::assertSame($connection->server_info, $ztd->get_server_info());
770            $connection->close();
771        } finally {
772            $container->stop();
773        }
774    }
775
776    public function testGet_connection_statsReturnsNativeMeasurements(): void
777    {
778        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
779        try {
780            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
781            $connection->set_charset('utf8mb4');
782            $ztd = ZtdMysqli::fromMysqli($connection);
783            $stats = $ztd->get_connection_stats();
784            self::assertArrayHasKey('bytes_sent', $stats);
785            self::assertGreaterThan(0, $stats['bytes_sent']);
786            $connection->close();
787        } finally {
788            $container->stop();
789        }
790    }
791
792    public function testGet_warningsReturnsTheNativeWarning(): void
793    {
794        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
795        try {
796            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
797            $connection->set_charset('utf8mb4');
798            $ztd = ZtdMysqli::fromMysqli($connection);
799            $connection->query("SELECT CAST('invalid' AS UNSIGNED)");
800            $warning = $ztd->get_warnings();
801            self::assertInstanceOf(mysqli_warning::class, $warning);
802            self::assertSame(1292, $warning->errno);
803            $connection->close();
804        } finally {
805            $container->stop();
806        }
807    }
808
809    public function testDump_debug_infoRequestsServerDiagnostics(): void
810    {
811        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
812        try {
813            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
814            $connection->set_charset('utf8mb4');
815            $ztd = ZtdMysqli::fromMysqli($connection);
816            self::assertTrue($ztd->dump_debug_info());
817            $connection->close();
818        } finally {
819            $container->stop();
820        }
821    }
822
823    public function testDebugAcceptsNativeTraceOptions(): void
824    {
825        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
826        try {
827            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
828            $connection->set_charset('utf8mb4');
829            $ztd = ZtdMysqli::fromMysqli($connection);
830            self::assertTrue($ztd->debug(''));
831            $connection->close();
832        } finally {
833            $container->stop();
834        }
835    }
836
837    public function testOptionsConfiguresTheNativeConnection(): void
838    {
839        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
840        try {
841            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
842            $connection->set_charset('utf8mb4');
843            $ztd = ZtdMysqli::fromMysqli($connection);
844            self::assertTrue($ztd->options(MYSQLI_OPT_CONNECT_TIMEOUT, 5));
845            $connection->close();
846        } finally {
847            $container->stop();
848        }
849    }
850
851    public function testSet_optPreservesTheNativeOptionsAlias(): void
852    {
853        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
854        try {
855            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
856            $connection->set_charset('utf8mb4');
857            $ztd = ZtdMysqli::fromMysqli($connection);
858            self::assertTrue($ztd->set_opt(MYSQLI_OPT_CONNECT_TIMEOUT, 5));
859            $connection->close();
860        } finally {
861            $container->stop();
862        }
863    }
864
865    public function testStatReturnsNativeServerStatus(): void
866    {
867        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
868        try {
869            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
870            $connection->set_charset('utf8mb4');
871            $ztd = ZtdMysqli::fromMysqli($connection);
872            $status = $ztd->stat();
873            self::assertIsString($status);
874            self::assertStringContainsString('Uptime:', $status);
875            $connection->close();
876        } finally {
877            $container->stop();
878        }
879    }
880
881    public function testStmt_initCreatesAUsableNativeStatement(): void
882    {
883        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
884        try {
885            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
886            $connection->set_charset('utf8mb4');
887            $ztd = ZtdMysqli::fromMysqli($connection);
888            $statement = $ztd->stmt_init();
889            self::assertTrue($statement->prepare('SELECT 7 AS id'));
890            self::assertTrue($statement->execute());
891            $result = $statement->get_result();
892            self::assertInstanceOf(mysqli_result::class, $result);
893            self::assertSame([['id' => 7]], $result->fetch_all(MYSQLI_ASSOC));
894            $connection->close();
895        } finally {
896            $container->stop();
897        }
898    }
899
900    public function testStore_resultReadsTheNativeBufferedResult(): void
901    {
902        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
903        try {
904            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
905            $connection->set_charset('utf8mb4');
906            $ztd = ZtdMysqli::fromMysqli($connection);
907            $connection->real_query('SELECT 7 AS id');
908            $result = $ztd->store_result();
909            self::assertInstanceOf(mysqli_result::class, $result);
910            self::assertSame([['id' => '7']], $result->fetch_all(MYSQLI_ASSOC));
911            $connection->close();
912        } finally {
913            $container->stop();
914        }
915    }
916
917    public function testUse_resultReadsTheNativeUnbufferedResult(): void
918    {
919        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
920        try {
921            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
922            $connection->set_charset('utf8mb4');
923            $ztd = ZtdMysqli::fromMysqli($connection);
924            $connection->real_query('SELECT 7 AS id');
925            $result = $ztd->use_result();
926            self::assertInstanceOf(mysqli_result::class, $result);
927            self::assertSame([['id' => '7']], $result->fetch_all(MYSQLI_ASSOC));
928            $connection->close();
929        } finally {
930            $container->stop();
931        }
932    }
933
934    public function testThread_safeMatchesTheNativeDriver(): void
935    {
936        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
937        try {
938            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
939            $connection->set_charset('utf8mb4');
940            $ztd = ZtdMysqli::fromMysqli($connection);
941            self::assertSame($connection->thread_safe(), $ztd->thread_safe());
942            $connection->close();
943        } finally {
944            $container->stop();
945        }
946    }
947
948    public function testPollUsesAndUpdatesNativeConnectionArrays(): void
949    {
950        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
951        try {
952            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
953            $connection->set_charset('utf8mb4');
954            $ztd = ZtdMysqli::fromMysqli($connection);
955            $connection->query('SELECT 7 AS id', MYSQLI_ASYNC);
956            $read = [$connection];
957            $error = [];
958            $reject = [];
959            self::assertSame(1, ZtdMysqli::poll($read, $error, $reject, 5));
960            self::assertSame([$connection], $read);
961            self::assertSame([], $error);
962            self::assertSame([], $reject);
963            $result = $connection->reap_async_query();
964            self::assertInstanceOf(mysqli_result::class, $result);
965            self::assertSame([['id' => '7']], $result->fetch_all(MYSQLI_ASSOC));
966            $connection->close();
967        } finally {
968            $container->stop();
969        }
970    }
971
972    public function testReap_async_queryReadsTheCompletedNativeResult(): void
973    {
974        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
975        try {
976            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
977            $connection->set_charset('utf8mb4');
978            $ztd = ZtdMysqli::fromMysqli($connection);
979            $connection->query('SELECT 7 AS id', MYSQLI_ASYNC);
980            $read = [$connection];
981            $error = [];
982            $reject = [];
983            self::assertSame(1, mysqli::poll($read, $error, $reject, 5));
984            $result = $ztd->reap_async_query();
985            self::assertInstanceOf(mysqli_result::class, $result);
986            self::assertSame([['id' => '7']], $result->fetch_all(MYSQLI_ASSOC));
987            $connection->close();
988        } finally {
989            $container->stop();
990        }
991    }
992
993    public function testCloseReleasesTheNativeConnection(): void
994    {
995        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
996        try {
997            $host = str_replace('localhost', '127.0.0.1', $container->getHost());
998            $port = $container->getMappedPort(3306);
999            self::assertNotNull($port);
1000            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
1001            $connection->set_charset('utf8mb4');
1002            $ztd = ZtdMysqli::fromMysqli($connection);
1003            $thread = $connection->thread_id;
1004            self::assertTrue($ztd->close());
1005            $observer = new mysqli($host, 'root', 'root', 'test', $port);
1006            $result = $observer->query('SELECT ID FROM information_schema.PROCESSLIST WHERE ID = ' . $thread);
1007            self::assertInstanceOf(mysqli_result::class, $result);
1008            self::assertSame([], $result->fetch_all(MYSQLI_ASSOC));
1009            $observer->close();
1010        } finally {
1011            $container->stop();
1012        }
1013    }
1014
1015    public function testReal_connectConnectsAnInitializedNativeHandle(): void
1016    {
1017        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
1018        try {
1019            $host = str_replace('localhost', '127.0.0.1', $container->getHost());
1020            $port = $container->getMappedPort(3306);
1021            self::assertNotNull($port);
1022            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
1023            $connection->set_charset('utf8mb4');
1024            $connection->close();
1025            $connection = new mysqli();
1026            $rewriter = self::createStub(SqlRewriter::class);
1027            $factory = self::createStub(SessionFactory::class);
1028            $factory->method('create')->willReturnCallback(static fn (ConnectionInterface $native, ZtdConfig $config): Session => new Session($rewriter, new ShadowStore(), new ResultSelectRunner(), $config, $native));
1029            $ztd = ZtdMysqli::fromMysqli($connection, null, $factory);
1030            self::assertTrue($ztd->real_connect($host, 'root', 'root', 'test', $port));
1031            $ztd->disableZtd();
1032            $result = $ztd->query('SELECT DATABASE() AS name');
1033            self::assertInstanceOf(mysqli_result::class, $result);
1034            self::assertSame([['name' => 'test']], $result->fetch_all(MYSQLI_ASSOC));
1035            $connection->close();
1036        } finally {
1037            $container->stop();
1038        }
1039    }
1040
1041    public function testPingChecksTheNativeConnection(): void
1042    {
1043        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
1044        try {
1045            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
1046            $connection->set_charset('utf8mb4');
1047            $ztd = ZtdMysqli::fromMysqli($connection);
1048            set_error_handler(static fn (int $severity, string $message): bool => $severity === E_DEPRECATED && str_contains($message, 'mysqli::ping'));
1049            try {
1050                self::assertTrue($ztd->ping());
1051            } finally {
1052                restore_error_handler();
1053                $connection->close();
1054            }
1055        } finally {
1056            $container->stop();
1057        }
1058    }
1059
1060    public function testGet_client_infoReportsTheClientLibrary(): void
1061    {
1062        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
1063        try {
1064            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
1065            $connection->set_charset('utf8mb4');
1066            $ztd = ZtdMysqli::fromMysqli($connection);
1067            set_error_handler(static fn (int $severity, string $message): bool => $severity === E_DEPRECATED && str_contains($message, 'mysqli::get_client_info'));
1068            try {
1069                self::assertSame(mysqli_get_client_info(), $ztd->get_client_info());
1070            } finally {
1071                restore_error_handler();
1072                $connection->close();
1073            }
1074        } finally {
1075            $container->stop();
1076        }
1077    }
1078
1079    public function testInitRetainsTheNativeInitializationContract(): void
1080    {
1081        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
1082        try {
1083            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
1084            $connection->set_charset('utf8mb4');
1085            $ztd = ZtdMysqli::fromMysqli($connection);
1086            set_error_handler(static fn (int $severity, string $message): bool => $severity === E_DEPRECATED && str_contains($message, 'mysqli::init'));
1087            try {
1088                self::assertTrue($ztd->init());
1089            } finally {
1090                restore_error_handler();
1091                $connection->close();
1092            }
1093        } finally {
1094            $container->stop();
1095        }
1096    }
1097
1098    public function testRefreshPreservesTheNativeServerResponse(): void
1099    {
1100        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
1101        try {
1102            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
1103            $connection->set_charset('utf8mb4');
1104            $ztd = ZtdMysqli::fromMysqli($connection);
1105            set_error_handler(static fn (int $severity, string $message): bool => $severity === E_DEPRECATED && str_contains($message, 'mysqli::refresh'));
1106            try {
1107                self::assertSame($connection->refresh(MYSQLI_REFRESH_STATUS), $ztd->refresh(MYSQLI_REFRESH_STATUS));
1108            } finally {
1109                restore_error_handler();
1110                $connection->close();
1111            }
1112        } finally {
1113            $container->stop();
1114        }
1115    }
1116
1117    public function testSsl_setAcceptsNativeTlsConfiguration(): void
1118    {
1119        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
1120        try {
1121            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
1122            $connection->set_charset('utf8mb4');
1123            $ztd = ZtdMysqli::fromMysqli($connection);
1124            set_error_handler(static fn (int $severity, string $message): bool => $severity === E_DEPRECATED && str_contains($message, 'mysqli::ssl_set'));
1125            try {
1126                self::assertTrue($ztd->ssl_set(null, null, null, null, null));
1127            } finally {
1128                restore_error_handler();
1129                $connection->close();
1130            }
1131        } finally {
1132            $container->stop();
1133        }
1134    }
1135
1136    public function testConnectUsesTheSuppliedNativeCredentials(): void
1137    {
1138        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
1139        try {
1140            $host = str_replace('localhost', '127.0.0.1', $container->getHost());
1141            $port = $container->getMappedPort(3306);
1142            self::assertNotNull($port);
1143            $connection = new mysqli();
1144            $factory = self::createStub(SessionFactory::class);
1145            $rewriter = self::createStub(SqlRewriter::class);
1146            $factory->method('create')->willReturnCallback(static fn (ConnectionInterface $native, ZtdConfig $config): Session => new Session($rewriter, new ShadowStore(), new ResultSelectRunner(), $config, $native));
1147            $ztd = ZtdMysqli::fromMysqli($connection, null, $factory);
1148            self::assertTrue($ztd->connect($host, 'root', 'root', 'test', $port));
1149            $result = $connection->query('SELECT DATABASE() AS name');
1150            self::assertInstanceOf(mysqli_result::class, $result);
1151            self::assertSame([['name' => 'test']], $result->fetch_all(MYSQLI_ASSOC));
1152            $connection->close();
1153        } finally {
1154            $container->stop();
1155        }
1156    }
1157
1158    public function testKillPreservesTheNativeServerResponse(): void
1159    {
1160        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
1161        try {
1162            $host = str_replace('localhost', '127.0.0.1', $container->getHost());
1163            $port = $container->getMappedPort(3306);
1164            self::assertNotNull($port);
1165            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
1166            $connection->set_charset('utf8mb4');
1167            $ztd = ZtdMysqli::fromMysqli($connection);
1168            $nativeTarget = new mysqli($host, 'root', 'root', 'test', $port);
1169            $adapterTarget = new mysqli($host, 'root', 'root', 'test', $port);
1170            mysqli_report(MYSQLI_REPORT_OFF);
1171            set_error_handler(static fn (int $severity, string $message): bool => $severity === E_DEPRECATED && str_contains($message, 'mysqli::kill'));
1172            try {
1173                $expected = $connection->kill($nativeTarget->thread_id);
1174                $expectedError = $connection->errno;
1175                self::assertSame($expected, $ztd->kill($adapterTarget->thread_id));
1176                self::assertSame($expectedError, $connection->errno);
1177                self::assertSame($nativeTarget->query('SELECT 1') === false, $adapterTarget->query('SELECT 1') === false);
1178            } finally {
1179                restore_error_handler();
1180                mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
1181                $nativeTarget->close();
1182                $adapterTarget->close();
1183                $connection->close();
1184            }
1185        } finally {
1186            $container->stop();
1187        }
1188    }
1189}
1190