packages/ztd-query-mysqli-adapter/tests/Integration/MysqliCteShadowingTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Integration;
6
7use Container\MySql80Container;
8use Container\MySql84Container;
9use mysqli;
10use mysqli_result;
11use PHPUnit\Framework\Attributes\Large;
12use PHPUnit\Framework\TestCase;
13use Testcontainers\Testcontainers;
14use ZtdQuery\Adapter\Mysqli\ZtdMysqli;
15
16#[\PHPUnit\Framework\Attributes\CoversClass(ZtdMysqli::class)]
17#[\PHPUnit\Framework\Attributes\CoversClass(\ZtdQuery\Adapter\Mysqli\Session\ConnectionExecution::class)]
18#[\PHPUnit\Framework\Attributes\CoversClass(\ZtdQuery\Adapter\Mysqli\ZtdMysqliStatement::class)]
19#[\PHPUnit\Framework\Attributes\CoversClass(\ZtdQuery\Adapter\Mysqli\Session\StatementExecution::class)]
20#[\PHPUnit\Framework\Attributes\CoversClass(\ZtdQuery\Adapter\Mysqli\Driver\MysqliConnection::class)]
21#[\PHPUnit\Framework\Attributes\CoversClass(\ZtdQuery\Adapter\Mysqli\Driver\MysqliResultStatement::class)]
22#[\PHPUnit\Framework\Attributes\CoversClass(\ZtdQuery\Adapter\Mysqli\Driver\MysqliResultColumnExtractor::class)]
23#[\PHPUnit\Framework\Attributes\CoversClass(\ZtdQuery\Adapter\Mysqli\Native\MysqliStatementBindingBridge::class)]
24#[\PHPUnit\Framework\Attributes\CoversClass(\ZtdQuery\Adapter\Mysqli\ZtdMysqliException::class)]
25#[\PHPUnit\Framework\Attributes\CoversClass(\ZtdQuery\Adapter\Mysqli\Native\MysqliPropertyReader::class)]
26#[\PHPUnit\Framework\Attributes\CoversClass(\ZtdQuery\Adapter\Mysqli\Session\MysqliResultProcessor::class)]
27#[Large]
28final class MysqliCteShadowingTest extends TestCase
29{
30    public function testUpdatesAndDeletesEveryListedTable(): void
31    {
32        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
33        try {
34            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
35            $rawMysqli->set_charset('utf8mb4');
36            $users = 'users_' . bin2hex(random_bytes(8));
37            $orders = 'orders_' . bin2hex(random_bytes(8));
38            $rawMysqli->query("CREATE TABLE `{$users}` (id INT PRIMARY KEY, name VARCHAR(50))");
39            $rawMysqli->query("CREATE TABLE `{$orders}` (order_id INT PRIMARY KEY, user_id INT, status VARCHAR(50))");
40            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
41
42            self::assertNotFalse($ztdMysqli->query("INSERT INTO `{$users}` VALUES (1, 'Alice'), (2, 'Bob')"));
43            self::assertNotFalse($ztdMysqli->query("INSERT INTO `{$orders}` VALUES (10, 1, 'pending'), (20, 2, 'pending')"));
44            self::assertNotFalse($ztdMysqli->query(
45                "UPDATE `{$users}` u, `{$orders}` o SET u.name = 'Updated', o.status = 'done'"
46                . ' WHERE u.id = o.user_id AND u.id = 2',
47            ));
48
49            $updatedUsers = $ztdMysqli->query("SELECT id, name FROM `{$users}` ORDER BY id");
50            $updatedOrders = $ztdMysqli->query("SELECT order_id, status FROM `{$orders}` ORDER BY order_id");
51            self::assertInstanceOf(mysqli_result::class, $updatedUsers);
52            self::assertInstanceOf(mysqli_result::class, $updatedOrders);
53            self::assertSame(
54                [['id' => 1, 'name' => 'Alice'], ['id' => 2, 'name' => 'Updated']],
55                $updatedUsers->fetch_all(MYSQLI_ASSOC),
56            );
57            self::assertSame(
58                [['order_id' => 10, 'status' => 'pending'], ['order_id' => 20, 'status' => 'done']],
59                $updatedOrders->fetch_all(MYSQLI_ASSOC),
60            );
61
62            self::assertNotFalse($ztdMysqli->query(
63                "DELETE u, o FROM `{$users}` u JOIN `{$orders}` o ON u.id = o.user_id WHERE u.id = 2",
64            ));
65
66            $remainingUsers = $ztdMysqli->query("SELECT id FROM `{$users}` ORDER BY id");
67            $remainingOrders = $ztdMysqli->query("SELECT order_id FROM `{$orders}` ORDER BY order_id");
68            self::assertInstanceOf(mysqli_result::class, $remainingUsers);
69            self::assertInstanceOf(mysqli_result::class, $remainingOrders);
70            self::assertSame([['id' => 1]], $remainingUsers->fetch_all(MYSQLI_ASSOC));
71            self::assertSame([['order_id' => 10]], $remainingOrders->fetch_all(MYSQLI_ASSOC));
72
73            $physicalUsers = $rawMysqli->query("SELECT * FROM `{$users}`");
74            $physicalOrders = $rawMysqli->query("SELECT * FROM `{$orders}`");
75            self::assertInstanceOf(mysqli_result::class, $physicalUsers);
76            self::assertInstanceOf(mysqli_result::class, $physicalOrders);
77            self::assertSame([], $physicalUsers->fetch_all(MYSQLI_ASSOC));
78            self::assertSame([], $physicalOrders->fetch_all(MYSQLI_ASSOC));
79        } finally {
80            $container->stop();
81        }
82    }
83
84    public function testExecuteQueryReplaceRemovesExistingPrimaryKey(): void
85    {
86        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
87        try {
88            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
89            $rawMysqli->set_charset('utf8mb4');
90            $table = 'prefix_' . bin2hex(random_bytes(8));
91            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT PRIMARY KEY, name VARCHAR(50))', $table));
92            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
93
94            $ztdMysqli->query(sprintf("INSERT INTO `%s` VALUES (1, 'original')", $table));
95            self::assertNotFalse($ztdMysqli->execute_query(
96                sprintf('REPLACE INTO `%s` VALUES (?, ?)', $table),
97                ['1', 'replaced'],
98            ));
99
100            $rows = $ztdMysqli->query(sprintf('SELECT * FROM `%s` WHERE id = 1', $table));
101            self::assertInstanceOf(mysqli_result::class, $rows);
102            self::assertSame([['id' => 1, 'name' => 'replaced']], $rows->fetch_all(MYSQLI_ASSOC));
103        } finally {
104            $container->stop();
105        }
106    }
107
108    public function testExecuteQueryOnDuplicateKeyUpdateReplacesExistingValues(): void
109    {
110        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
111        try {
112            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
113            $rawMysqli->set_charset('utf8mb4');
114            $table = 'prefix_' . bin2hex(random_bytes(8));
115            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT PRIMARY KEY, name VARCHAR(50))', $table));
116            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
117
118            $ztdMysqli->query(sprintf("INSERT INTO `%s` VALUES (1, 'original')", $table));
119            self::assertNotFalse($ztdMysqli->execute_query(
120                sprintf('INSERT INTO `%s` VALUES (?, ?) ON DUPLICATE KEY UPDATE name = VALUES(name)', $table),
121                ['1', 'updated'],
122            ));
123
124            $rows = $ztdMysqli->query(sprintf('SELECT * FROM `%s` WHERE id = 1', $table));
125            self::assertInstanceOf(mysqli_result::class, $rows);
126            self::assertSame([['id' => 1, 'name' => 'updated']], $rows->fetch_all(MYSQLI_ASSOC));
127        } finally {
128            $container->stop();
129        }
130    }
131
132    public function testUpdateReplacesExistingTextWithEmptyString(): void
133    {
134        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
135        try {
136            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
137            $rawMysqli->set_charset('utf8mb4');
138            $table = 'prefix_' . bin2hex(random_bytes(8));
139            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT PRIMARY KEY, name VARCHAR(100), notes TEXT)', $table));
140            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
141
142            self::assertNotFalse($ztdMysqli->query(sprintf("INSERT INTO `%s` VALUES (1, 'Alice', 'some notes')", $table)));
143            self::assertNotFalse($ztdMysqli->query(sprintf("UPDATE `%s` SET notes = '' WHERE name = 'Alice'", $table)));
144
145            $result = $ztdMysqli->query(sprintf('SELECT notes FROM `%s` WHERE id = 1', $table));
146            self::assertInstanceOf(mysqli_result::class, $result);
147            self::assertSame([['notes' => '']], $result->fetch_all(MYSQLI_ASSOC));
148        } finally {
149            $container->stop();
150        }
151    }
152
153    public function testUpdatePreservesIntroducedHexLiteral(): void
154    {
155        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
156        try {
157            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
158            $rawMysqli->set_charset('utf8mb4');
159            $table = 'prefix_' . bin2hex(random_bytes(8));
160            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT PRIMARY KEY, payload VARBINARY(255))', $table));
161            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
162
163            self::assertNotFalse($ztdMysqli->query(sprintf("INSERT INTO `%s` VALUES (1, X'48656C6C6F')", $table)));
164            self::assertNotFalse($ztdMysqli->query(sprintf("UPDATE `%s` SET payload = X'576F726C64' WHERE id = 1", $table)));
165
166            $result = $ztdMysqli->query(sprintf('SELECT payload FROM `%s` WHERE id = 1', $table));
167            self::assertInstanceOf(mysqli_result::class, $result);
168            self::assertSame([['payload' => 'World']], $result->fetch_all(MYSQLI_ASSOC));
169
170            $physical = $rawMysqli->query(sprintf('SELECT payload FROM `%s`', $table));
171            self::assertInstanceOf(mysqli_result::class, $physical);
172            self::assertSame([], $physical->fetch_all(MYSQLI_ASSOC));
173        } finally {
174            $container->stop();
175        }
176    }
177
178    public function testUpdatePreservesIntervalUnit(): void
179    {
180        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
181        try {
182            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
183            $rawMysqli->set_charset('utf8mb4');
184            $table = 'prefix_' . bin2hex(random_bytes(8));
185            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT PRIMARY KEY, created_at DATETIME NOT NULL, due_at DATETIME)', $table));
186            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
187
188            self::assertNotFalse($ztdMysqli->query(sprintf("INSERT INTO `%s` VALUES (1, '2025-01-01 10:00:00', NULL)", $table)));
189            self::assertNotFalse($ztdMysqli->query(sprintf('UPDATE `%s` SET due_at = created_at + INTERVAL 30 DAY WHERE id = 1', $table)));
190
191            $result = $ztdMysqli->query(sprintf('SELECT due_at FROM `%s` WHERE id = 1', $table));
192            self::assertInstanceOf(mysqli_result::class, $result);
193            self::assertSame([['due_at' => '2025-01-31 10:00:00']], $result->fetch_all(MYSQLI_ASSOC));
194
195            $physical = $rawMysqli->query(sprintf('SELECT due_at FROM `%s`', $table));
196            self::assertInstanceOf(mysqli_result::class, $physical);
197            self::assertSame([], $physical->fetch_all(MYSQLI_ASSOC));
198        } finally {
199            $container->stop();
200        }
201    }
202
203    public function testUpdateAndDeleteRestrictRowsWithCaseExpression(): void
204    {
205        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
206        try {
207            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
208            $rawMysqli->set_charset('utf8mb4');
209            $updates = 'prefix_' . bin2hex(random_bytes(8));
210            $deletes = 'prefix_' . bin2hex(random_bytes(8));
211            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT PRIMARY KEY, score INT)', $updates));
212            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT PRIMARY KEY, score INT)', $deletes));
213            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
214
215            $rows = 'VALUES (1, 85), (2, 60), (3, 95), (4, 45)';
216            self::assertNotFalse($ztdMysqli->query(sprintf('INSERT INTO `%s` %s', $updates, $rows)));
217            self::assertNotFalse($ztdMysqli->query(sprintf('INSERT INTO `%s` %s', $deletes, $rows)));
218            self::assertNotFalse($ztdMysqli->query(sprintf('UPDATE `%s` SET score = 0 WHERE CASE WHEN score > 80 THEN 1 ELSE 0 END = 1', $updates)));
219            self::assertSame(2, $ztdMysqli->lastAffectedRows());
220            self::assertNotFalse($ztdMysqli->query(sprintf('DELETE FROM `%s` WHERE CASE WHEN score > 80 THEN 1 ELSE 0 END = 1', $deletes)));
221            self::assertSame(2, $ztdMysqli->lastAffectedRows());
222
223            $updated = $ztdMysqli->query(sprintf('SELECT id, score FROM `%s` ORDER BY id', $updates));
224            $remaining = $ztdMysqli->query(sprintf('SELECT id, score FROM `%s` ORDER BY id', $deletes));
225            self::assertInstanceOf(mysqli_result::class, $updated);
226            self::assertInstanceOf(mysqli_result::class, $remaining);
227            self::assertSame([
228                ['id' => 1, 'score' => 0],
229                ['id' => 2, 'score' => 60],
230                ['id' => 3, 'score' => 0],
231                ['id' => 4, 'score' => 45],
232            ], $updated->fetch_all(MYSQLI_ASSOC));
233            self::assertSame([
234                ['id' => 2, 'score' => 60],
235                ['id' => 4, 'score' => 45],
236            ], $remaining->fetch_all(MYSQLI_ASSOC));
237
238            $physicalUpdates = $rawMysqli->query(sprintf('SELECT * FROM `%s`', $updates));
239            $physicalDeletes = $rawMysqli->query(sprintf('SELECT * FROM `%s`', $deletes));
240            self::assertInstanceOf(mysqli_result::class, $physicalUpdates);
241            self::assertInstanceOf(mysqli_result::class, $physicalDeletes);
242            self::assertSame([], $physicalUpdates->fetch_all(MYSQLI_ASSOC));
243            self::assertSame([], $physicalDeletes->fetch_all(MYSQLI_ASSOC));
244        } finally {
245            $container->stop();
246        }
247    }
248
249    public function testInsertWithoutColumnListIgnoresNamedForeignKeyConstraint(): void
250    {
251        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
252        try {
253            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
254            $rawMysqli->set_charset('utf8mb4');
255            $parent = 'prefix_' . bin2hex(random_bytes(8));
256            $child = 'prefix_' . bin2hex(random_bytes(8));
257            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT PRIMARY KEY, name VARCHAR(50)) ENGINE=InnoDB', $parent));
258            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT PRIMARY KEY, parent_id INT NOT NULL, label VARCHAR(50) NOT NULL, CONSTRAINT `fk_parent` FOREIGN KEY (parent_id) REFERENCES `%s`(id)) ENGINE=InnoDB', $child, $parent));
259            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
260
261            self::assertNotFalse($ztdMysqli->query(sprintf("INSERT INTO `%s` VALUES (1, 'Parent')", $parent)));
262            self::assertNotFalse($ztdMysqli->query(sprintf("INSERT INTO `%s` VALUES (10, 1, 'Child')", $child)));
263
264            $result = $ztdMysqli->query(sprintf('SELECT * FROM `%s`', $child));
265            self::assertInstanceOf(mysqli_result::class, $result);
266            self::assertSame([['id' => 10, 'parent_id' => 1, 'label' => 'Child']], $result->fetch_all(MYSQLI_ASSOC));
267
268            $physical = $rawMysqli->query(sprintf('SELECT * FROM `%s`', $child));
269            self::assertInstanceOf(mysqli_result::class, $physical);
270            self::assertSame([], $physical->fetch_all(MYSQLI_ASSOC));
271        } finally {
272            $container->stop();
273        }
274    }
275
276    public function testSelfReferencingUpsertMatchesNativeMySql(): void
277    {
278        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
279        try {
280            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
281            $rawMysqli->set_charset('utf8mb4');
282            $nativeTable = 'prefix_' . bin2hex(random_bytes(8));
283            $shadowTable = 'prefix_' . bin2hex(random_bytes(8));
284            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT PRIMARY KEY, quantity INT NOT NULL)', $nativeTable));
285            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT PRIMARY KEY, quantity INT NOT NULL)', $shadowTable));
286            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
287
288            $rawMysqli->query(sprintf('INSERT INTO `%s` VALUES (1, 100)', $nativeTable));
289            $ztdMysqli->query(sprintf('INSERT INTO `%s` VALUES (1, 100)', $shadowTable));
290            $rawMysqli->query(sprintf('INSERT INTO `%1$s` VALUES (1, 5), (1, 7) ON DUPLICATE KEY UPDATE quantity = `%1$s`.quantity + VALUES(quantity)', $nativeTable));
291            $ztdMysqli->query(sprintf('INSERT INTO `%1$s` VALUES (1, 5), (1, 7) ON DUPLICATE KEY UPDATE quantity = `%1$s`.quantity + VALUES(quantity)', $shadowTable));
292
293            $rawRows = $rawMysqli->query(sprintf('SELECT id, quantity FROM `%s`', $nativeTable));
294            $ztdRows = $ztdMysqli->query(sprintf('SELECT id, quantity FROM `%s`', $shadowTable));
295            $physicalShadowRows = $rawMysqli->query(sprintf('SELECT * FROM `%s`', $shadowTable));
296            self::assertInstanceOf(mysqli_result::class, $rawRows);
297            self::assertInstanceOf(mysqli_result::class, $ztdRows);
298            self::assertInstanceOf(mysqli_result::class, $physicalShadowRows);
299            self::assertEquals($rawRows->fetch_all(MYSQLI_ASSOC), $ztdRows->fetch_all(MYSQLI_ASSOC));
300            self::assertSame([], $physicalShadowRows->fetch_all(MYSQLI_ASSOC));
301        } finally {
302            $container->stop();
303        }
304    }
305
306    public function testAffectedRowsCountsOnlyChangedMySqlRows(): void
307    {
308        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
309        try {
310            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
311            $rawMysqli->set_charset('utf8mb4');
312            $table = 'prefix_' . bin2hex(random_bytes(8));
313            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT PRIMARY KEY, score INT)', $table));
314            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
315
316            $ztdMysqli->query(sprintf('INSERT INTO `%s` VALUES (1, 10)', $table));
317            $ztdMysqli->query(sprintf('UPDATE `%s` SET score = 10 WHERE id = 1', $table));
318            self::assertSame(0, $ztdMysqli->lastAffectedRows());
319            $ztdMysqli->query(sprintf('UPDATE `%s` SET score = 11 WHERE id = 1', $table));
320            self::assertSame(1, $ztdMysqli->lastAffectedRows());
321        } finally {
322            $container->stop();
323        }
324    }
325
326    public function testTransactionsAndSavepointsRestoreShadowRows(): void
327    {
328        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
329        try {
330            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
331            $rawMysqli->set_charset('utf8mb4');
332            $table = 'prefix_' . bin2hex(random_bytes(8));
333            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT PRIMARY KEY, name VARCHAR(20))', $table));
334            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
335
336            $ztdMysqli->query(sprintf("INSERT INTO `%s` VALUES (1, 'one')", $table));
337            $ztdMysqli->begin_transaction();
338            $ztdMysqli->query(sprintf("INSERT INTO `%s` VALUES (2, 'two')", $table));
339            $ztdMysqli->savepoint('nested');
340            $ztdMysqli->query(sprintf("INSERT INTO `%s` VALUES (3, 'three')", $table));
341            $ztdMysqli->query('ROLLBACK TO SAVEPOINT nested');
342            $ztdMysqli->commit();
343
344            $committed = $ztdMysqli->query(sprintf('SELECT * FROM `%s` ORDER BY id', $table));
345            self::assertInstanceOf(mysqli_result::class, $committed);
346            self::assertSame([
347                ['id' => 1, 'name' => 'one'],
348                ['id' => 2, 'name' => 'two'],
349            ], $committed->fetch_all(MYSQLI_ASSOC));
350
351            $ztdMysqli->begin_transaction();
352            $ztdMysqli->query(sprintf("UPDATE `%s` SET name = 'changed'", $table));
353            $ztdMysqli->rollback();
354            $rolledBack = $ztdMysqli->query(sprintf('SELECT * FROM `%s` ORDER BY id', $table));
355            self::assertInstanceOf(mysqli_result::class, $rolledBack);
356            self::assertSame([
357                ['id' => 1, 'name' => 'one'],
358                ['id' => 2, 'name' => 'two'],
359            ], $rolledBack->fetch_all(MYSQLI_ASSOC));
360        } finally {
361            $container->stop();
362        }
363    }
364
365    public function testRecursiveAndUserOwnedCteNamespacesRemainValid(): void
366    {
367        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
368        try {
369            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
370            $rawMysqli->set_charset('utf8mb4');
371            $table = 'prefix_' . bin2hex(random_bytes(8));
372            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT PRIMARY KEY, parent_id INT)', $table));
373            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
374
375            $ztdMysqli->query(sprintf('INSERT INTO `%s` VALUES (1, NULL), (2, 1), (3, 2)', $table));
376            $recursive = $ztdMysqli->query(sprintf(
377                'WITH RECURSIVE tree AS (SELECT id, parent_id FROM `%1$s` WHERE parent_id IS NULL UNION ALL SELECT n.id, n.parent_id FROM `%1$s` n JOIN tree t ON n.parent_id = t.id) SELECT id FROM tree ORDER BY id',
378                $table,
379            ));
380            self::assertInstanceOf(mysqli_result::class, $recursive);
381            self::assertSame([['id' => 1], ['id' => 2], ['id' => 3]], $recursive->fetch_all(MYSQLI_ASSOC));
382
383            $owned = $ztdMysqli->query(sprintf('WITH `%1$s` AS (SELECT 9 AS id, NULL AS parent_id) SELECT id FROM `%1$s`', $table));
384            self::assertInstanceOf(mysqli_result::class, $owned);
385            self::assertSame([['id' => 9]], $owned->fetch_all(MYSQLI_ASSOC));
386        } finally {
387            $container->stop();
388        }
389    }
390
391    public function testCteDefinitionsRemainVisibleToSimulatedDml(): void
392    {
393        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
394        try {
395            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
396            $rawMysqli->set_charset('utf8mb4');
397            $table = 'prefix_' . bin2hex(random_bytes(8));
398            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT PRIMARY KEY, value VARCHAR(20))', $table));
399            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
400
401            self::assertNotFalse($ztdMysqli->query(sprintf("WITH source AS (SELECT 1 AS id, 'one' AS value UNION ALL SELECT 2, 'two') INSERT INTO `%s` SELECT * FROM source", $table)));
402            self::assertNotFalse($ztdMysqli->query(sprintf("WITH chosen AS (SELECT id FROM `%1\$s` WHERE value = 'two') UPDATE `%1\$s` SET value = 'changed' WHERE id IN (SELECT id FROM chosen)", $table)));
403            self::assertNotFalse($ztdMysqli->query(sprintf("WITH chosen AS (SELECT id FROM `%1\$s` WHERE value = 'one') DELETE FROM `%1\$s` WHERE id IN (SELECT id FROM chosen)", $table)));
404
405            $result = $ztdMysqli->query(sprintf('SELECT * FROM `%s`', $table));
406            self::assertInstanceOf(mysqli_result::class, $result);
407            self::assertSame([['id' => 2, 'value' => 'changed']], $result->fetch_all(MYSQLI_ASSOC));
408        } finally {
409            $container->stop();
410        }
411    }
412
413    public function testOrderedLimitedUpdateKeepsOriginalIdentityAndSwapSnapshot(): void
414    {
415        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
416        try {
417            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
418            $rawMysqli->set_charset('utf8mb4');
419            $table = 'prefix_' . bin2hex(random_bytes(8));
420            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT PRIMARY KEY, left_value VARCHAR(20), right_value VARCHAR(20))', $table));
421            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
422
423            $ztdMysqli->query(sprintf("INSERT INTO `%s` VALUES (1, 'a1', 'b1'), (2, 'a2', 'b2'), (3, 'a3', 'b3')", $table));
424            $ztdMysqli->query(sprintf('UPDATE `%s` SET id = 30, left_value = right_value, right_value = left_value ORDER BY id DESC LIMIT 1', $table));
425
426            $result = $ztdMysqli->query(sprintf('SELECT * FROM `%s` ORDER BY id', $table));
427            self::assertInstanceOf(mysqli_result::class, $result);
428            self::assertSame([
429                ['id' => 1, 'left_value' => 'a1', 'right_value' => 'b1'],
430                ['id' => 2, 'left_value' => 'a2', 'right_value' => 'b2'],
431                ['id' => 30, 'left_value' => 'b3', 'right_value' => 'a3'],
432            ], $result->fetch_all(MYSQLI_ASSOC));
433        } finally {
434            $container->stop();
435        }
436    }
437
438    public function testInsertSelectPreservesStarJoinsAggregatesDistinctAndRollup(): void
439    {
440        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
441        try {
442            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
443            $rawMysqli->set_charset('utf8mb4');
444            $source = 'prefix_' . bin2hex(random_bytes(8));
445            $target = 'prefix_' . bin2hex(random_bytes(8));
446            $orders = 'prefix_' . bin2hex(random_bytes(8));
447            $summary = 'prefix_' . bin2hex(random_bytes(8));
448            $regions = 'prefix_' . bin2hex(random_bytes(8));
449            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT PRIMARY KEY, region VARCHAR(20), amount INT)', $source));
450            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT PRIMARY KEY, region VARCHAR(20), amount INT)', $target));
451            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT PRIMARY KEY, source_id INT)', $orders));
452            $rawMysqli->query(sprintf('CREATE TABLE `%s` (region VARCHAR(20), order_count INT, total_amount INT)', $summary));
453            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(20))', $regions));
454            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
455
456            $ztdMysqli->query(sprintf("INSERT INTO `%s` VALUES (1, 'east', 100), (2, 'east', 200), (3, 'west', 300)", $source));
457            $ztdMysqli->query(sprintf('INSERT INTO `%s` VALUES (1, 1), (2, 1), (3, 3)', $orders));
458            $ztdMysqli->query(sprintf('INSERT INTO `%s` SELECT * FROM `%s`', $target, $source));
459            $ztdMysqli->query(sprintf(
460                'INSERT INTO `%s` (region, order_count, total_amount) SELECT s.region, COUNT(o.id), SUM(s.amount) FROM `%s` s JOIN `%s` o ON o.source_id = s.id GROUP BY s.region WITH ROLLUP',
461                $summary,
462                $source,
463                $orders,
464            ));
465            $ztdMysqli->query(sprintf('INSERT INTO `%s` (name) SELECT DISTINCT region FROM `%s` ORDER BY region', $regions, $source));
466
467            $targetRows = $ztdMysqli->query(sprintf('SELECT * FROM `%s` ORDER BY id', $target));
468            $summaryRows = $ztdMysqli->query(sprintf('SELECT * FROM `%s` ORDER BY region', $summary));
469            $regionRows = $ztdMysqli->query(sprintf('SELECT * FROM `%s` ORDER BY id', $regions));
470            self::assertInstanceOf(mysqli_result::class, $targetRows);
471            self::assertInstanceOf(mysqli_result::class, $summaryRows);
472            self::assertInstanceOf(mysqli_result::class, $regionRows);
473            self::assertEquals([
474                ['id' => 1, 'region' => 'east', 'amount' => 100],
475                ['id' => 2, 'region' => 'east', 'amount' => 200],
476                ['id' => 3, 'region' => 'west', 'amount' => 300],
477            ], $targetRows->fetch_all(MYSQLI_ASSOC));
478            self::assertEquals([
479                ['region' => null, 'order_count' => 3, 'total_amount' => 500],
480                ['region' => 'east', 'order_count' => 2, 'total_amount' => 200],
481                ['region' => 'west', 'order_count' => 1, 'total_amount' => 300],
482            ], $summaryRows->fetch_all(MYSQLI_ASSOC));
483            self::assertEquals([['id' => 1, 'name' => 'east'], ['id' => 2, 'name' => 'west']], $regionRows->fetch_all(MYSQLI_ASSOC));
484        } finally {
485            $container->stop();
486        }
487    }
488
489    public function testGroupedSelfReferencingSubqueryRestrictsUpdate(): void
490    {
491        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
492        try {
493            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
494            $rawMysqli->set_charset('utf8mb4');
495            $table = 'prefix_' . bin2hex(random_bytes(8));
496            $definition = '(id INT PRIMARY KEY, name VARCHAR(50), dept_id INT, salary DECIMAL(10,2), active TINYINT DEFAULT 1)';
497            $rawMysqli->query(sprintf('CREATE TABLE `%s` %s', $table, $definition));
498            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
499
500            $rows = "VALUES (1, 'Alice', 1, 120000, 1), (2, 'Bob', 1, 110000, 1), (3, 'Charlie', 2, 90000, 1), (4, 'Diana', 3, 95000, 1), (5, 'Eve', 1, 130000, 1)";
501            $ztdMysqli->query(sprintf('INSERT INTO `%s` %s', $table, $rows));
502
503            $ztdMysqli->query(sprintf('UPDATE `%1$s` SET active = 0 WHERE dept_id IN (SELECT dept_id FROM `%1$s` GROUP BY dept_id HAVING AVG(salary) > 100000)', $table));
504
505            $result = $ztdMysqli->query(sprintf('SELECT id, active FROM `%s` ORDER BY id', $table));
506            self::assertInstanceOf(mysqli_result::class, $result);
507            self::assertSame([
508                ['id' => 1, 'active' => 0],
509                ['id' => 2, 'active' => 0],
510                ['id' => 3, 'active' => 1],
511                ['id' => 4, 'active' => 1],
512                ['id' => 5, 'active' => 0],
513            ], $result->fetch_all(MYSQLI_ASSOC));
514        } finally {
515            $container->stop();
516        }
517    }
518
519    public function testGroupedSelfReferencingSubqueryRestrictsDelete(): void
520    {
521        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
522        try {
523            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
524            $rawMysqli->set_charset('utf8mb4');
525            $table = 'prefix_' . bin2hex(random_bytes(8));
526            $definition = '(id INT PRIMARY KEY, customer_id INT, amount DECIMAL(10,2), status VARCHAR(20))';
527            $rawMysqli->query(sprintf('CREATE TABLE `%s` %s', $table, $definition));
528            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
529
530            $rows = "VALUES (1, 1, 50, 'completed'), (2, 1, 75, 'completed'), (3, 1, 30, 'cancelled'), (4, 2, 200, 'completed'), (5, 3, 10, 'completed'), (6, 3, 15, 'completed')";
531            $ztdMysqli->query(sprintf('INSERT INTO `%s` %s', $table, $rows));
532
533            $ztdMysqli->query(sprintf("DELETE FROM `%1\$s` WHERE customer_id IN (SELECT customer_id FROM `%1\$s` WHERE status = 'cancelled' GROUP BY customer_id HAVING COUNT(*) >= 1)", $table));
534
535            $result = $ztdMysqli->query(sprintf('SELECT * FROM `%s` ORDER BY id', $table));
536            self::assertInstanceOf(mysqli_result::class, $result);
537            self::assertSame([
538                ['id' => 4, 'customer_id' => 2, 'amount' => '200.00', 'status' => 'completed'],
539                ['id' => 5, 'customer_id' => 3, 'amount' => '10.00', 'status' => 'completed'],
540                ['id' => 6, 'customer_id' => 3, 'amount' => '15.00', 'status' => 'completed'],
541            ], $result->fetch_all(MYSQLI_ASSOC));
542        } finally {
543            $container->stop();
544        }
545    }
546
547    public function testAutoIncrementUsesShadowCounterWithoutModifyingPhysicalTable(): void
548    {
549        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
550        try {
551            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
552            $rawMysqli->set_charset('utf8mb4');
553            $table = 'prefix_' . bin2hex(random_bytes(8));
554            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(20) NOT NULL)', $table));
555            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
556
557            $ztdMysqli->query(sprintf("INSERT INTO `%s` (name) VALUES ('Alice'), ('Bob')", $table));
558
559            $ztdRows = $ztdMysqli->query(sprintf('SELECT id, name FROM `%s` ORDER BY id', $table));
560            $rawRows = $rawMysqli->query(sprintf('SELECT * FROM `%s`', $table));
561            self::assertInstanceOf(mysqli_result::class, $ztdRows);
562            self::assertInstanceOf(mysqli_result::class, $rawRows);
563            self::assertEquals([['id' => 1, 'name' => 'Alice'], ['id' => 2, 'name' => 'Bob']], $ztdRows->fetch_all(MYSQLI_ASSOC));
564            self::assertSame([], $rawRows->fetch_all(MYSQLI_ASSOC));
565        } finally {
566            $container->stop();
567        }
568    }
569
570    public function testOmittedExplicitAndDefaultOnlyValuesMatchMySql(): void
571    {
572        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
573        try {
574            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
575            $rawMysqli->set_charset('utf8mb4');
576            $table = 'prefix_' . bin2hex(random_bytes(8));
577            $rawMysqli->query(sprintf(
578                "CREATE TABLE `%s` (id INT DEFAULT 7, status ENUM('new','active') DEFAULT 'active', note VARCHAR(20) DEFAULT NULL)",
579                $table,
580            ));
581            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
582
583            $rawMysqli->query(sprintf('INSERT INTO `%s` (id, status) VALUES (1, DEFAULT)', $table));
584            $rawMysqli->query(sprintf('INSERT INTO `%s` () VALUES ()', $table));
585            $ztdMysqli->query(sprintf('INSERT INTO `%s` (id, status) VALUES (1, DEFAULT)', $table));
586            $ztdMysqli->query(sprintf('INSERT INTO `%s` () VALUES ()', $table));
587
588            $raw = $rawMysqli->query(sprintf('SELECT * FROM `%s` ORDER BY id', $table));
589            $ztd = $ztdMysqli->query(sprintf('SELECT * FROM `%s` ORDER BY id', $table));
590            self::assertInstanceOf(mysqli_result::class, $raw);
591            self::assertInstanceOf(mysqli_result::class, $ztd);
592            $rawRows = $raw->fetch_all(MYSQLI_ASSOC);
593            $ztdRows = $ztd->fetch_all(MYSQLI_ASSOC);
594            self::assertEquals($rawRows, $ztdRows);
595        } finally {
596            $container->stop();
597        }
598    }
599
600    public function testEnumUsesDeclarationRanksForOrderingAndComparison(): void
601    {
602        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
603        try {
604            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
605            $rawMysqli->set_charset('utf8mb4');
606            $table = 'prefix_' . bin2hex(random_bytes(8));
607            $rawMysqli->query(sprintf("CREATE TABLE `%s` (id INT PRIMARY KEY, size ENUM('small','medium','large'))", $table));
608            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
609
610            $ztdMysqli->query(sprintf("INSERT INTO `%s` VALUES (1, 'large'), (2, 'small'), (3, 'medium')", $table));
611
612            $ordered = $ztdMysqli->query(sprintf('SELECT size FROM `%s` ORDER BY size', $table));
613            $compared = $ztdMysqli->query(sprintf("SELECT size FROM `%s` WHERE size > 'small' ORDER BY size", $table));
614            self::assertInstanceOf(mysqli_result::class, $ordered);
615            self::assertInstanceOf(mysqli_result::class, $compared);
616            self::assertSame(['small', 'medium', 'large'], array_column($ordered->fetch_all(MYSQLI_ASSOC), 'size'));
617            self::assertSame(['medium', 'large'], array_column($compared->fetch_all(MYSQLI_ASSOC), 'size'));
618        } finally {
619            $container->stop();
620        }
621    }
622
623    public function testSetExpressionsRemainSingleStatements(): void
624    {
625        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
626        try {
627            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
628            $rawMysqli->set_charset('utf8mb4');
629            $users = 'prefix_' . bin2hex(random_bytes(8));
630            $vip = 'prefix_' . bin2hex(random_bytes(8));
631            $rawMysqli->query(sprintf('CREATE TABLE `%s` (name VARCHAR(255) PRIMARY KEY)', $users));
632            $rawMysqli->query(sprintf('CREATE TABLE `%s` (name VARCHAR(255) PRIMARY KEY)', $vip));
633            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
634
635            $ztdMysqli->query(sprintf("INSERT INTO `%s` (name) VALUES ('Alice'), ('Bob')", $users));
636            $ztdMysqli->query(sprintf("INSERT INTO `%s` (name) VALUES ('Alice')", $vip));
637
638            $except = $ztdMysqli->query(sprintf(
639                'SELECT name FROM `%s` EXCEPT SELECT name FROM `%s` ORDER BY name',
640                $users,
641                $vip,
642            ));
643            $intersect = $ztdMysqli->query(sprintf(
644                'SELECT name FROM `%s` INTERSECT SELECT name FROM `%s` ORDER BY name',
645                $users,
646                $vip,
647            ));
648            self::assertInstanceOf(mysqli_result::class, $except);
649            self::assertInstanceOf(mysqli_result::class, $intersect);
650            self::assertSame([['name' => 'Bob']], $except->fetch_all(MYSQLI_ASSOC));
651            self::assertSame([['name' => 'Alice']], $intersect->fetch_all(MYSQLI_ASSOC));
652        } finally {
653            $container->stop();
654        }
655    }
656
657    public function testSelectOnCleanShadowReturnsEmpty(): void
658    {
659        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
660        try {
661            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
662            $rawMysqli->set_charset('utf8mb4');
663            $table = 'prefix_' . bin2hex(random_bytes(8));
664            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, age INT NOT NULL)', $table));
665            $rawMysqli->query(sprintf("INSERT INTO `%s` (name, age) VALUES ('Alice', 30), ('Bob', 25)", $table));
666            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
667            $result = $ztdMysqli->query(sprintf('SELECT * FROM `%s` ORDER BY id', $table));
668            self::assertNotFalse($result);
669            self::assertInstanceOf(mysqli_result::class, $result);
670
671            $rows = $result->fetch_all(MYSQLI_ASSOC);
672            self::assertCount(0, $rows);
673        } finally {
674            $container->stop();
675        }
676    }
677
678    public function testInsertDoesNotModifyPhysicalDatabase(): void
679    {
680        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
681        try {
682            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
683            $rawMysqli->set_charset('utf8mb4');
684            $table = 'prefix_' . bin2hex(random_bytes(8));
685            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, age INT NOT NULL)', $table));
686            $rawMysqli->query(sprintf("INSERT INTO `%s` (name, age) VALUES ('Alice', 30), ('Bob', 25)", $table));
687            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
688            $ztdMysqli->query(sprintf(
689                "INSERT INTO `%s` (name, age) VALUES ('Charlie', 35)",
690                $table
691            ));
692
693            $result = $rawMysqli->query(sprintf('SELECT * FROM `%s`', $table));
694            self::assertNotFalse($result);
695            self::assertInstanceOf(mysqli_result::class, $result);
696
697            $rows = $result->fetch_all(MYSQLI_ASSOC);
698            self::assertCount(2, $rows);
699        } finally {
700            $container->stop();
701        }
702    }
703
704    public function testInsertIsVisibleViaZtdSelect(): void
705    {
706        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
707        try {
708            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
709            $rawMysqli->set_charset('utf8mb4');
710            $table = 'prefix_' . bin2hex(random_bytes(8));
711            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, age INT NOT NULL)', $table));
712            $rawMysqli->query(sprintf("INSERT INTO `%s` (name, age) VALUES ('Alice', 30), ('Bob', 25)", $table));
713            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
714            $ztdMysqli->query(sprintf(
715                "INSERT INTO `%s` (name, age) VALUES ('Charlie', 35)",
716                $table
717            ));
718
719            $result = $ztdMysqli->query(sprintf('SELECT * FROM `%s` ORDER BY id', $table));
720            self::assertNotFalse($result);
721            self::assertInstanceOf(mysqli_result::class, $result);
722
723            $rows = $result->fetch_all(MYSQLI_ASSOC);
724            self::assertCount(1, $rows);
725            self::assertSame('Charlie', $rows[0]['name']);
726            /**
727             * @var string|int $age
728             */
729            $age = $rows[0]['age'];
730            self::assertSame('35', (string) $age);
731        } finally {
732            $container->stop();
733        }
734    }
735
736    public function testMultipleInsertsAccumulate(): void
737    {
738        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
739        try {
740            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
741            $rawMysqli->set_charset('utf8mb4');
742            $table = 'prefix_' . bin2hex(random_bytes(8));
743            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, age INT NOT NULL)', $table));
744            $rawMysqli->query(sprintf("INSERT INTO `%s` (name, age) VALUES ('Alice', 30), ('Bob', 25)", $table));
745            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
746            $ztdMysqli->query(sprintf(
747                "INSERT INTO `%s` (name, age) VALUES ('Charlie', 35)",
748                $table
749            ));
750            $ztdMysqli->query(sprintf(
751                "INSERT INTO `%s` (name, age) VALUES ('Diana', 28)",
752                $table
753            ));
754
755            $result = $ztdMysqli->query(sprintf('SELECT * FROM `%s` ORDER BY name', $table));
756            self::assertNotFalse($result);
757            self::assertInstanceOf(mysqli_result::class, $result);
758
759            $rows = $result->fetch_all(MYSQLI_ASSOC);
760            self::assertCount(2, $rows);
761
762            $names = array_column($rows, 'name');
763            self::assertContains('Charlie', $names);
764            self::assertContains('Diana', $names);
765        } finally {
766            $container->stop();
767        }
768    }
769
770    public function testSelectWithWhereOnShadowData(): void
771    {
772        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
773        try {
774            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
775            $rawMysqli->set_charset('utf8mb4');
776            $table = 'prefix_' . bin2hex(random_bytes(8));
777            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, age INT NOT NULL)', $table));
778            $rawMysqli->query(sprintf("INSERT INTO `%s` (name, age) VALUES ('Alice', 30), ('Bob', 25)", $table));
779            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
780            $ztdMysqli->query(sprintf(
781                "INSERT INTO `%s` (name, age) VALUES ('Charlie', 35)",
782                $table
783            ));
784            $ztdMysqli->query(sprintf(
785                "INSERT INTO `%s` (name, age) VALUES ('Diana', 28)",
786                $table
787            ));
788
789            $result = $ztdMysqli->query(sprintf('SELECT * FROM `%s` WHERE age > 30', $table));
790            self::assertNotFalse($result);
791            self::assertInstanceOf(mysqli_result::class, $result);
792
793            $rows = $result->fetch_all(MYSQLI_ASSOC);
794            self::assertCount(1, $rows);
795            self::assertSame('Charlie', $rows[0]['name']);
796        } finally {
797            $container->stop();
798        }
799    }
800
801    public function testPhysicalDatabaseRemainsUnchangedAfterMutations(): void
802    {
803        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
804        try {
805            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
806            $rawMysqli->set_charset('utf8mb4');
807            $table = 'prefix_' . bin2hex(random_bytes(8));
808            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, age INT NOT NULL)', $table));
809            $rawMysqli->query(sprintf("INSERT INTO `%s` (name, age) VALUES ('Alice', 30), ('Bob', 25)", $table));
810            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
811            $ztdMysqli->query(sprintf(
812                "INSERT INTO `%s` (name, age) VALUES ('Charlie', 35)",
813                $table
814            ));
815            $ztdMysqli->query(sprintf(
816                "INSERT INTO `%s` (name, age) VALUES ('Diana', 28)",
817                $table
818            ));
819
820            $result = $rawMysqli->query(sprintf('SELECT * FROM `%s` ORDER BY id', $table));
821            self::assertNotFalse($result);
822            self::assertInstanceOf(mysqli_result::class, $result);
823
824            $rows = $result->fetch_all(MYSQLI_ASSOC);
825            self::assertCount(2, $rows);
826            self::assertSame('Alice', $rows[0]['name']);
827            self::assertSame('Bob', $rows[1]['name']);
828        } finally {
829            $container->stop();
830        }
831    }
832
833    public function testEnableDisableToggle(): void
834    {
835        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
836        try {
837            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
838            $rawMysqli->set_charset('utf8mb4');
839            $table = 'prefix_' . bin2hex(random_bytes(8));
840            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, age INT NOT NULL)', $table));
841            $rawMysqli->query(sprintf("INSERT INTO `%s` (name, age) VALUES ('Alice', 30), ('Bob', 25)", $table));
842            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
843            self::assertTrue($ztdMysqli->isZtdEnabled());
844
845            $ztdMysqli->disableZtd();
846            self::assertFalse($ztdMysqli->isZtdEnabled());
847
848            $ztdMysqli->enableZtd();
849            self::assertTrue($ztdMysqli->isZtdEnabled());
850        } finally {
851            $container->stop();
852        }
853    }
854
855    public function testDisableZtdBypassesRewriting(): void
856    {
857        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
858        try {
859            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
860            $rawMysqli->set_charset('utf8mb4');
861            $table = 'prefix_' . bin2hex(random_bytes(8));
862            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, age INT NOT NULL)', $table));
863            $rawMysqli->query(sprintf("INSERT INTO `%s` (name, age) VALUES ('Alice', 30), ('Bob', 25)", $table));
864            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
865            $ztdMysqli->disableZtd();
866
867            $ztdMysqli->query(sprintf(
868                "INSERT INTO `%s` (name, age) VALUES ('Direct', 40)",
869                $table
870            ));
871
872            $result = $rawMysqli->query(sprintf('SELECT * FROM `%s`', $table));
873            self::assertNotFalse($result);
874            self::assertInstanceOf(mysqli_result::class, $result);
875
876            $rows = $result->fetch_all(MYSQLI_ASSOC);
877            self::assertCount(3, $rows);
878
879            $rawMysqli->query(sprintf("DELETE FROM `%s` WHERE name = 'Direct'", $table));
880        } finally {
881            $container->stop();
882        }
883    }
884
885    public function testPreparedStatementSelectWithZtd(): void
886    {
887        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
888        try {
889            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
890            $rawMysqli->set_charset('utf8mb4');
891            $table = 'prefix_' . bin2hex(random_bytes(8));
892            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, age INT NOT NULL)', $table));
893            $rawMysqli->query(sprintf("INSERT INTO `%s` (name, age) VALUES ('Alice', 30), ('Bob', 25)", $table));
894            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
895            $ztdMysqli->query(sprintf(
896                "INSERT INTO `%s` (name, age) VALUES ('Charlie', 35)",
897                $table
898            ));
899
900            $stmt = $ztdMysqli->prepare(sprintf('SELECT * FROM `%s` WHERE name = ?', $table));
901            self::assertNotFalse($stmt);
902
903            $name = 'Charlie';
904            $stmt->bind_param('s', $name);
905            $stmt->execute();
906            $result = $stmt->get_result();
907            self::assertNotFalse($result);
908
909            $rows = $result->fetch_all(MYSQLI_ASSOC);
910            self::assertCount(1, $rows);
911            self::assertSame('Charlie', $rows[0]['name']);
912        } finally {
913            $container->stop();
914        }
915    }
916
917    public function testPreparedStatementSelectNonExistent(): void
918    {
919        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
920        try {
921            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
922            $rawMysqli->set_charset('utf8mb4');
923            $table = 'prefix_' . bin2hex(random_bytes(8));
924            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, age INT NOT NULL)', $table));
925            $rawMysqli->query(sprintf("INSERT INTO `%s` (name, age) VALUES ('Alice', 30), ('Bob', 25)", $table));
926            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
927            $ztdMysqli->query(sprintf(
928                "INSERT INTO `%s` (name, age) VALUES ('Charlie', 35)",
929                $table
930            ));
931
932            $stmt = $ztdMysqli->prepare(sprintf('SELECT * FROM `%s` WHERE name = ?', $table));
933            self::assertNotFalse($stmt);
934
935            $name = 'Alice';
936            $stmt->bind_param('s', $name);
937            $stmt->execute();
938            $result = $stmt->get_result();
939            self::assertNotFalse($result);
940
941            $rows = $result->fetch_all(MYSQLI_ASSOC);
942            self::assertCount(0, $rows);
943        } finally {
944            $container->stop();
945        }
946    }
947
948    public function testAffectedRowsAfterInsert(): void
949    {
950        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
951        try {
952            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
953            $rawMysqli->set_charset('utf8mb4');
954            $table = 'prefix_' . bin2hex(random_bytes(8));
955            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, age INT NOT NULL)', $table));
956            $rawMysqli->query(sprintf("INSERT INTO `%s` (name, age) VALUES ('Alice', 30), ('Bob', 25)", $table));
957            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
958            $ztdMysqli->query(sprintf(
959                "INSERT INTO `%s` (name, age) VALUES ('Charlie', 35)",
960                $table
961            ));
962
963            self::assertSame(1, $ztdMysqli->lastAffectedRows());
964        } finally {
965            $container->stop();
966        }
967    }
968
969    public function testAffectedRowsAfterMultipleInserts(): void
970    {
971        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
972        try {
973            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
974            $rawMysqli->set_charset('utf8mb4');
975            $table = 'prefix_' . bin2hex(random_bytes(8));
976            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, age INT NOT NULL)', $table));
977            $rawMysqli->query(sprintf("INSERT INTO `%s` (name, age) VALUES ('Alice', 30), ('Bob', 25)", $table));
978            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
979            $ztdMysqli->query(sprintf(
980                "INSERT INTO `%s` (name, age) VALUES ('Charlie', 35)",
981                $table
982            ));
983            self::assertSame(1, $ztdMysqli->lastAffectedRows());
984
985            $ztdMysqli->query(sprintf(
986                "INSERT INTO `%s` (name, age) VALUES ('Diana', 28)",
987                $table
988            ));
989            self::assertSame(1, $ztdMysqli->lastAffectedRows());
990        } finally {
991            $container->stop();
992        }
993    }
994
995    public function testExecuteQuerySelect(): void
996    {
997        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
998        try {
999            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
1000            $rawMysqli->set_charset('utf8mb4');
1001            $table = 'prefix_' . bin2hex(random_bytes(8));
1002            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, age INT NOT NULL)', $table));
1003            $rawMysqli->query(sprintf("INSERT INTO `%s` (name, age) VALUES ('Alice', 30), ('Bob', 25)", $table));
1004            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
1005            $ztdMysqli->query(sprintf(
1006                "INSERT INTO `%s` (name, age) VALUES ('Charlie', 35)",
1007                $table
1008            ));
1009
1010            $result = $ztdMysqli->execute_query(
1011                sprintf('SELECT * FROM `%s` WHERE name = ?', $table),
1012                ['Charlie']
1013            );
1014            self::assertNotFalse($result);
1015            self::assertInstanceOf(mysqli_result::class, $result);
1016
1017            $rows = $result->fetch_all(MYSQLI_ASSOC);
1018            self::assertCount(1, $rows);
1019            self::assertSame('Charlie', $rows[0]['name']);
1020        } finally {
1021            $container->stop();
1022        }
1023    }
1024
1025    public function testRealQueryInsert(): void
1026    {
1027        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
1028        try {
1029            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
1030            $rawMysqli->set_charset('utf8mb4');
1031            $table = 'prefix_' . bin2hex(random_bytes(8));
1032            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, age INT NOT NULL)', $table));
1033            $rawMysqli->query(sprintf("INSERT INTO `%s` (name, age) VALUES ('Alice', 30), ('Bob', 25)", $table));
1034            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
1035            $result = $ztdMysqli->real_query(sprintf(
1036                "INSERT INTO `%s` (name, age) VALUES ('Charlie', 35)",
1037                $table
1038            ));
1039            self::assertTrue($result);
1040
1041            $selectResult = $ztdMysqli->query(sprintf('SELECT * FROM `%s`', $table));
1042            self::assertNotFalse($selectResult);
1043            self::assertInstanceOf(mysqli_result::class, $selectResult);
1044
1045            $rows = $selectResult->fetch_all(MYSQLI_ASSOC);
1046            self::assertCount(1, $rows);
1047        } finally {
1048            $container->stop();
1049        }
1050    }
1051
1052    public function testPreparedBackslashesRoundTripWithoutMysqlEscapeCorruption(): void
1053    {
1054        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
1055        try {
1056            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
1057            $rawMysqli->set_charset('utf8mb4');
1058            $table = 'typed_' . bin2hex(random_bytes(8));
1059            $rawMysqli->query(sprintf('CREATE TABLE `%s` (id INT PRIMARY KEY, value VARCHAR(255))', $table));
1060            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
1061
1062            $insert = $ztdMysqli->prepare(sprintf('INSERT INTO `%s` (id, value) VALUES (?, ?)', $table));
1063            self::assertNotFalse($insert);
1064            $id = 1;
1065            $value = 'path\\to\\file';
1066            self::assertTrue($insert->bind_param('is', $id, $value));
1067            self::assertTrue($insert->execute());
1068
1069            $result = $ztdMysqli->query(sprintf('SELECT value FROM `%s` WHERE id = 1', $table));
1070            self::assertNotFalse($result);
1071            self::assertInstanceOf(mysqli_result::class, $result);
1072            self::assertSame([['value' => $value]], $result->fetch_all(MYSQLI_ASSOC));
1073        } finally {
1074            $container->stop();
1075        }
1076    }
1077
1078    public function testRangePartitionSelectionMatchesShadowRows(): void
1079    {
1080        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
1081        try {
1082            $rawMysqli = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
1083            $rawMysqli->set_charset('utf8mb4');
1084            $rawMysqli->query('CREATE TABLE events (id INT NOT NULL, event_date DATE NOT NULL, '
1085                . 'PRIMARY KEY (id, event_date)) PARTITION BY RANGE (YEAR(event_date)) ('
1086                . 'PARTITION p2023 VALUES LESS THAN (2024), '
1087                . 'PARTITION p2024 VALUES LESS THAN (2025), '
1088                . 'PARTITION pmax VALUES LESS THAN MAXVALUE)');
1089            $ztdMysqli = ZtdMysqli::fromMysqli($rawMysqli, null);
1090
1091            self::assertNotFalse($ztdMysqli->query('INSERT INTO events VALUES '
1092                . "(1, '2023-06-01'), (2, '2024-01-15'), (3, '2024-11-20'), (4, '2025-02-01')"));
1093
1094            $selected = $ztdMysqli->query('SELECT id FROM events PARTITION (p2024) ORDER BY id');
1095            self::assertInstanceOf(mysqli_result::class, $selected);
1096            self::assertSame([['id' => 2], ['id' => 3]], $selected->fetch_all(MYSQLI_ASSOC));
1097
1098            $combined = $ztdMysqli->query('SELECT e.id FROM events PARTITION (p2023, pmax) e ORDER BY e.id');
1099            self::assertInstanceOf(mysqli_result::class, $combined);
1100            self::assertSame([['id' => 1], ['id' => 4]], $combined->fetch_all(MYSQLI_ASSOC));
1101
1102            $physical = $rawMysqli->query('SELECT COUNT(*) AS total FROM events');
1103            self::assertInstanceOf(mysqli_result::class, $physical);
1104            self::assertSame([['total' => '0']], $physical->fetch_all(MYSQLI_ASSOC));
1105        } finally {
1106            $container->stop();
1107        }
1108    }
1109    public function testYearShorthandMatchesNativeCoercionWithoutPhysicalWrites(): void
1110    {
1111        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
1112        try {
1113            $native = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
1114            $native->set_charset('utf8mb4');
1115            $native->query('CREATE TABLE years (id INT PRIMARY KEY, value YEAR)');
1116            $ztd = ZtdMysqli::fromMysqli($native);
1117            self::assertNotFalse($ztd->query("INSERT INTO years VALUES (1, 78), (2, 69), (3, 0), (4, '0')"));
1118            self::assertNotFalse($ztd->query('INSERT INTO years (id, value) SELECT 5, 93'));
1119            $result = $ztd->query('SELECT value FROM years ORDER BY id');
1120            self::assertInstanceOf(mysqli_result::class, $result);
1121            self::assertSame([['value' => 1978], ['value' => 2069], ['value' => 0], ['value' => 2000], ['value' => 1993]], $result->fetch_all(MYSQLI_ASSOC));
1122            $physical = $native->query('SELECT * FROM years');
1123            self::assertInstanceOf(mysqli_result::class, $physical);
1124            self::assertSame([], $physical->fetch_all(MYSQLI_ASSOC));
1125        } finally {
1126            $container->stop();
1127        }
1128    }
1129
1130    public function testNativeConstructorCreatesAnIsolatedDefaultSession(): void
1131    {
1132        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
1133        try {
1134            $host = str_replace('localhost', '127.0.0.1', $container->getHost());
1135            $port = $container->getMappedPort(3306);
1136            self::assertNotNull($port);
1137            $native = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
1138            $native->set_charset('utf8mb4');
1139            $database = 'test';
1140            $native->query('CREATE TABLE users (id INT)');
1141            $ztd = new ZtdMysqli($host, 'root', 'root', $database, $port);
1142            self::assertTrue($ztd->isZtdEnabled());
1143            self::assertNotFalse($ztd->query('INSERT INTO users VALUES (42)'));
1144            $simulated = $ztd->query('SELECT * FROM users');
1145            self::assertInstanceOf(mysqli_result::class, $simulated);
1146            self::assertSame([['id' => 42]], $simulated->fetch_all(MYSQLI_ASSOC));
1147            $physical = $native->query('SELECT * FROM users');
1148            self::assertInstanceOf(mysqli_result::class, $physical);
1149            self::assertSame([], $physical->fetch_all(MYSQLI_ASSOC));
1150            self::assertTrue($ztd->close());
1151        } finally {
1152            $container->stop();
1153        }
1154    }
1155
1156}
1157