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

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