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