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