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