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

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit;
6
7use Container\MySql80Container;
8use Container\MySql84Container;
9use mysqli;
10use mysqli_result;
11use mysqli_stmt;
12use mysqli_warning;
13use PHPUnit\Framework\Attributes\CoversClass;
14use PHPUnit\Framework\Attributes\DataProvider;
15use PHPUnit\Framework\Attributes\Large;
16use PHPUnit\Framework\Attributes\UsesClass;
17use PHPUnit\Framework\TestCase;
18use Testcontainers\Testcontainers;
19use ZtdQuery\Adapter\Mysqli\Driver\MysqliResultColumnExtractor;
20use ZtdQuery\Adapter\Mysqli\Driver\MysqliResultStatement;
21use ZtdQuery\Adapter\Mysqli\Native\MysqliStatementBindingBridge;
22use ZtdQuery\Adapter\Mysqli\Native\MysqliStatementPropertyReader;
23use ZtdQuery\Adapter\Mysqli\Session\MysqliResultProcessor;
24use ZtdQuery\Adapter\Mysqli\ZtdMysqliException;
25use ZtdQuery\Adapter\Mysqli\ZtdMysqliStatement;
26use ZtdQuery\Config\ZtdConfig;
27use ZtdQuery\Connection\ConnectionInterface;
28use ZtdQuery\Platform\MySql\MySqlResultColumnTypeResolver;
29use ZtdQuery\ResultSelectRunner;
30use ZtdQuery\Rewrite\QueryKind;
31use ZtdQuery\Rewrite\RewritePlan;
32use ZtdQuery\Rewrite\SqlRewriter;
33use ZtdQuery\Session;
34use ZtdQuery\Shadow\Mutation\InsertMutation;
35use ZtdQuery\Shadow\ShadowStore;
36
37#[CoversClass(ZtdMysqliStatement::class)]
38#[CoversClass(\ZtdQuery\Adapter\Mysqli\Session\StatementExecution::class)]
39#[Large]
40#[UsesClass(MysqliStatementBindingBridge::class)]
41#[UsesClass(MysqliStatementPropertyReader::class)]
42#[UsesClass(MysqliResultProcessor::class)]
43#[UsesClass(MysqliResultStatement::class)]
44#[UsesClass(MysqliResultColumnExtractor::class)]
45#[UsesClass(ZtdMysqliException::class)]
46final class ZtdMysqliStatementTest extends TestCase
47{
48    #[DataProvider('providerReadPlans')]
49    public function testExecuteHonorsReadAndUnplannedParameters(?QueryKind $kind, bool $withParams): void
50    {
51        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
52        try {
53            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
54            $connection->set_charset('utf8mb4');
55            $store = new ShadowStore();
56            $session = new Session(self::createStub(SqlRewriter::class), $store, new ResultSelectRunner(), ZtdConfig::default(), self::createStub(ConnectionInterface::class), resultColumnTypeResolver: new MySqlResultColumnTypeResolver());
57            $sql = $withParams ? 'SELECT ? AS value' : 'SELECT 7 AS value';
58            $native = $connection->prepare($sql);
59            self::assertInstanceOf(mysqli_stmt::class, $native);
60            $plan = $kind === null ? null : new RewritePlan($sql, $kind);
61            $statement = new ZtdMysqliStatement($native, $session, $plan);
62            self::assertTrue($statement->execute($withParams ? [42] : null));
63            $result = $statement->get_result();
64            self::assertInstanceOf(mysqli_result::class, $result);
65            self::assertSame([['value' => $withParams ? '42' : 7]], $result->fetch_all(MYSQLI_ASSOC));
66            $connection->close();
67        } finally {
68            $container->stop();
69        }
70    }
71
72    /**
73     * @return array<string, array{?QueryKind, bool}>
74     */
75    public static function providerReadPlans(): array
76    {
77        return ['unplanned' => [null, false], 'unplanned-params' => [null, true], 'read' => [QueryKind::READ, false], 'read-params' => [QueryKind::READ, true]];
78    }
79
80    public function testExecuteDoesNotRunASkippedPlan(): void
81    {
82        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
83        try {
84            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
85            $connection->set_charset('utf8mb4');
86            $store = new ShadowStore();
87            $session = new Session(self::createStub(SqlRewriter::class), $store, new ResultSelectRunner(), ZtdConfig::default(), self::createStub(ConnectionInterface::class), resultColumnTypeResolver: new MySqlResultColumnTypeResolver());
88            $connection->query('CREATE TEMPORARY TABLE skipped_rows (id INT)');
89            $native = $connection->prepare('INSERT INTO skipped_rows VALUES (1)');
90            self::assertInstanceOf(mysqli_stmt::class, $native);
91            $statement = new ZtdMysqliStatement($native, $session, new RewritePlan('skipped', QueryKind::SKIPPED));
92            self::assertFalse($statement->execute());
93            $rows = $connection->query('SELECT * FROM skipped_rows');
94            self::assertInstanceOf(mysqli_result::class, $rows);
95            self::assertSame([], $rows->fetch_all(MYSQLI_ASSOC));
96            $connection->close();
97        } finally {
98            $container->stop();
99        }
100    }
101
102    public function testExecuteAppliesRowsToTheShadowStore(): void
103    {
104        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
105        try {
106            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
107            $connection->set_charset('utf8mb4');
108            $store = new ShadowStore();
109            $session = new Session(self::createStub(SqlRewriter::class), $store, new ResultSelectRunner(), ZtdConfig::default(), self::createStub(ConnectionInterface::class), resultColumnTypeResolver: new MySqlResultColumnTypeResolver());
110            $native = $connection->prepare('SELECT ? AS id');
111            self::assertInstanceOf(mysqli_stmt::class, $native);
112            $plan = new RewritePlan('SELECT ? AS id', QueryKind::WRITE_SIMULATED, new InsertMutation('items'));
113            $statement = new ZtdMysqliStatement($native, $session, $plan);
114            self::assertTrue($statement->execute([42]));
115            self::assertSame([['id' => '42']], $store->get('items'));
116            self::assertSame(1, $statement->ztdAffectedRows());
117            self::assertSame(1, $statement->num_rows());
118            self::assertNull($statement->fetch());
119            $connection->close();
120        } finally {
121            $container->stop();
122        }
123    }
124
125    public function testGet_resultConsumesTheCachedNativeResultOnce(): void
126    {
127        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
128        try {
129            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
130            $connection->set_charset('utf8mb4');
131            $store = new ShadowStore();
132            $session = new Session(self::createStub(SqlRewriter::class), $store, new ResultSelectRunner(), ZtdConfig::default(), self::createStub(ConnectionInterface::class), resultColumnTypeResolver: new MySqlResultColumnTypeResolver());
133            $native = $connection->prepare('SELECT 7 AS id');
134            self::assertInstanceOf(mysqli_stmt::class, $native);
135            $statement = new ZtdMysqliStatement($native, $session, new RewritePlan('SELECT 7 AS id', QueryKind::WRITE_SIMULATED, new InsertMutation('items')));
136            self::assertTrue($statement->execute());
137            self::assertInstanceOf(mysqli_result::class, $statement->get_result());
138            self::assertFalse($statement->get_result());
139            $connection->close();
140        } finally {
141            $container->stop();
142        }
143    }
144
145    public function testGet_resultReturnsFalseForAStatementWithoutRows(): void
146    {
147        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
148        try {
149            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
150            $connection->set_charset('utf8mb4');
151            $store = new ShadowStore();
152            $session = new Session(self::createStub(SqlRewriter::class), $store, new ResultSelectRunner(), ZtdConfig::default(), self::createStub(ConnectionInterface::class), resultColumnTypeResolver: new MySqlResultColumnTypeResolver());
153            $native = $connection->prepare('DO 1');
154            self::assertInstanceOf(mysqli_stmt::class, $native);
155            $statement = new ZtdMysqliStatement($native, $session, new RewritePlan('DO 1', QueryKind::WRITE_SIMULATED, new InsertMutation('items')));
156            self::assertTrue($statement->execute());
157            self::assertSame(0, $statement->ztdAffectedRows());
158            self::assertSame(0, $statement->num_rows());
159            self::assertFalse($statement->get_result());
160            self::assertFalse($statement->get_result());
161            self::assertNull($statement->fetch());
162            $connection->close();
163        } finally {
164            $container->stop();
165        }
166    }
167
168    #[DataProvider('providerFailedPlans')]
169    public function testExecuteReturnsFalseForNativeFailures(?QueryKind $kind, bool $withParams): void
170    {
171        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
172        try {
173            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
174            $connection->set_charset('utf8mb4');
175            $store = new ShadowStore();
176            $session = new Session(self::createStub(SqlRewriter::class), $store, new ResultSelectRunner(), ZtdConfig::default(), self::createStub(ConnectionInterface::class), resultColumnTypeResolver: new MySqlResultColumnTypeResolver());
177            $connection->query('CREATE TEMPORARY TABLE duplicate_keys (id INT PRIMARY KEY)');
178            $connection->query('INSERT INTO duplicate_keys VALUES (1)');
179            $sql = $withParams ? 'INSERT INTO duplicate_keys VALUES (?)' : 'INSERT INTO duplicate_keys VALUES (1)';
180            $native = $connection->prepare($sql);
181            self::assertInstanceOf(mysqli_stmt::class, $native);
182            $plan = $kind === null ? null : new RewritePlan($sql, $kind, new InsertMutation('items'));
183            $statement = new ZtdMysqliStatement($native, $session, $plan);
184            mysqli_report(MYSQLI_REPORT_OFF);
185            try {
186                self::assertFalse($statement->execute($withParams ? [1] : null));
187                self::assertSame([], $store->get('items'));
188            } finally {
189                mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
190                $connection->close();
191            }
192        } finally {
193            $container->stop();
194        }
195    }
196
197    /**
198     * @return array<string, array{?QueryKind, bool}>
199     */
200    public static function providerFailedPlans(): array
201    {
202        return ['unplanned' => [null, false], 'read' => [QueryKind::READ, true], 'write' => [QueryKind::WRITE_SIMULATED, false], 'write-params' => [QueryKind::WRITE_SIMULATED, true]];
203    }
204
205    public function testFetchWritesBoundResultVariables(): void
206    {
207        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
208        try {
209            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
210            $connection->set_charset('utf8mb4');
211            $store = new ShadowStore();
212            $session = new Session(self::createStub(SqlRewriter::class), $store, new ResultSelectRunner(), ZtdConfig::default(), self::createStub(ConnectionInterface::class), resultColumnTypeResolver: new MySqlResultColumnTypeResolver());
213            $native = $connection->prepare('SELECT 7 AS id');
214            self::assertInstanceOf(mysqli_stmt::class, $native);
215            $statement = new ZtdMysqliStatement($native, $session, null);
216            $id = null;
217            self::assertTrue($statement->bind_result($id));
218            self::assertTrue($statement->execute());
219            self::assertTrue($statement->fetch());
220            self::assertSame(7, $id);
221            self::assertNull($statement->fetch());
222            $connection->close();
223        } finally {
224            $container->stop();
225        }
226    }
227
228    public function testStore_resultBuffersTheNativeRows(): void
229    {
230        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
231        try {
232            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
233            $connection->set_charset('utf8mb4');
234            $store = new ShadowStore();
235            $session = new Session(self::createStub(SqlRewriter::class), $store, new ResultSelectRunner(), ZtdConfig::default(), self::createStub(ConnectionInterface::class), resultColumnTypeResolver: new MySqlResultColumnTypeResolver());
236            $native = $connection->prepare('SELECT 1 UNION ALL SELECT 2');
237            self::assertInstanceOf(mysqli_stmt::class, $native);
238            $statement = new ZtdMysqliStatement($native, $session, null);
239            self::assertTrue($statement->execute());
240            self::assertTrue($statement->store_result());
241            self::assertSame(2, $statement->num_rows());
242            $connection->close();
243        } finally {
244            $container->stop();
245        }
246    }
247
248    public function testData_seekMovesTheNativeCursor(): void
249    {
250        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
251        try {
252            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
253            $connection->set_charset('utf8mb4');
254            $store = new ShadowStore();
255            $session = new Session(self::createStub(SqlRewriter::class), $store, new ResultSelectRunner(), ZtdConfig::default(), self::createStub(ConnectionInterface::class), resultColumnTypeResolver: new MySqlResultColumnTypeResolver());
256            $native = $connection->prepare('SELECT 1 AS id UNION ALL SELECT 2');
257            self::assertInstanceOf(mysqli_stmt::class, $native);
258            $statement = new ZtdMysqliStatement($native, $session, null);
259            self::assertTrue($statement->execute());
260            self::assertTrue($statement->store_result());
261            $id = null;
262            $statement->bind_result($id);
263            $statement->data_seek(1);
264            self::assertTrue($statement->fetch());
265            self::assertSame(2, $id);
266            $connection->close();
267        } finally {
268            $container->stop();
269        }
270    }
271
272    public function testFree_resultReleasesBufferedRows(): void
273    {
274        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
275        try {
276            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
277            $connection->set_charset('utf8mb4');
278            $store = new ShadowStore();
279            $session = new Session(self::createStub(SqlRewriter::class), $store, new ResultSelectRunner(), ZtdConfig::default(), self::createStub(ConnectionInterface::class), resultColumnTypeResolver: new MySqlResultColumnTypeResolver());
280            $native = $connection->prepare('SELECT 1 UNION ALL SELECT 2');
281            self::assertInstanceOf(mysqli_stmt::class, $native);
282            $statement = new ZtdMysqliStatement($native, $session, null);
283            $statement->execute();
284            $statement->store_result();
285            self::assertSame(2, $statement->num_rows());
286            $statement->free_result();
287            self::assertSame(0, $native->num_rows());
288            $connection->close();
289        } finally {
290            $container->stop();
291        }
292    }
293
294    public function testNum_rowsReturnsTheNativeBufferedCount(): void
295    {
296        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
297        try {
298            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
299            $connection->set_charset('utf8mb4');
300            $store = new ShadowStore();
301            $session = new Session(self::createStub(SqlRewriter::class), $store, new ResultSelectRunner(), ZtdConfig::default(), self::createStub(ConnectionInterface::class), resultColumnTypeResolver: new MySqlResultColumnTypeResolver());
302            $native = $connection->prepare('SELECT 1 UNION ALL SELECT 2');
303            self::assertInstanceOf(mysqli_stmt::class, $native);
304            $statement = new ZtdMysqliStatement($native, $session, null);
305            $native->execute();
306            $native->store_result();
307            self::assertSame(2, $statement->num_rows());
308            $connection->close();
309        } finally {
310            $container->stop();
311        }
312    }
313
314    public function testZtdAffectedRowsReturnsTheNativeWriteCount(): void
315    {
316        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
317        try {
318            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
319            $connection->set_charset('utf8mb4');
320            $store = new ShadowStore();
321            $session = new Session(self::createStub(SqlRewriter::class), $store, new ResultSelectRunner(), ZtdConfig::default(), self::createStub(ConnectionInterface::class), resultColumnTypeResolver: new MySqlResultColumnTypeResolver());
322            $native = $connection->prepare('DO 1');
323            self::assertInstanceOf(mysqli_stmt::class, $native);
324            $statement = new ZtdMysqliStatement($native, $session, null);
325            $statement->execute();
326            self::assertSame(0, $statement->ztdAffectedRows());
327            $connection->close();
328        } finally {
329            $container->stop();
330        }
331    }
332
333    public function testResult_metadataReturnsNativeColumnInformation(): void
334    {
335        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
336        try {
337            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
338            $connection->set_charset('utf8mb4');
339            $store = new ShadowStore();
340            $session = new Session(self::createStub(SqlRewriter::class), $store, new ResultSelectRunner(), ZtdConfig::default(), self::createStub(ConnectionInterface::class), resultColumnTypeResolver: new MySqlResultColumnTypeResolver());
341            $native = $connection->prepare("SELECT 7 AS id, 'Alice' AS name");
342            self::assertInstanceOf(mysqli_stmt::class, $native);
343            $statement = new ZtdMysqliStatement($native, $session, null);
344            $result = $statement->result_metadata();
345            self::assertInstanceOf(mysqli_result::class, $result);
346            self::assertSame(['id', 'name'], array_column($result->fetch_fields(), 'name'));
347            $connection->close();
348        } finally {
349            $container->stop();
350        }
351    }
352
353    public function testAttr_setUpdatesTheNativeCursorMode(): void
354    {
355        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
356        try {
357            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
358            $connection->set_charset('utf8mb4');
359            $store = new ShadowStore();
360            $session = new Session(self::createStub(SqlRewriter::class), $store, new ResultSelectRunner(), ZtdConfig::default(), self::createStub(ConnectionInterface::class), resultColumnTypeResolver: new MySqlResultColumnTypeResolver());
361            $native = $connection->prepare('SELECT 1');
362            self::assertInstanceOf(mysqli_stmt::class, $native);
363            $statement = new ZtdMysqliStatement($native, $session, null);
364            self::assertTrue($statement->attr_set(MYSQLI_STMT_ATTR_CURSOR_TYPE, MYSQLI_CURSOR_TYPE_READ_ONLY));
365            self::assertSame(MYSQLI_CURSOR_TYPE_READ_ONLY, $native->attr_get(MYSQLI_STMT_ATTR_CURSOR_TYPE));
366            $connection->close();
367        } finally {
368            $container->stop();
369        }
370    }
371
372    public function testAttr_getReadsTheConfiguredNativeCursorMode(): void
373    {
374        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
375        try {
376            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
377            $connection->set_charset('utf8mb4');
378            $store = new ShadowStore();
379            $session = new Session(self::createStub(SqlRewriter::class), $store, new ResultSelectRunner(), ZtdConfig::default(), self::createStub(ConnectionInterface::class), resultColumnTypeResolver: new MySqlResultColumnTypeResolver());
380            $native = $connection->prepare('SELECT 1');
381            self::assertInstanceOf(mysqli_stmt::class, $native);
382            $statement = new ZtdMysqliStatement($native, $session, null);
383            $native->attr_set(MYSQLI_STMT_ATTR_CURSOR_TYPE, MYSQLI_CURSOR_TYPE_READ_ONLY);
384            self::assertSame(MYSQLI_CURSOR_TYPE_READ_ONLY, $statement->attr_get(MYSQLI_STMT_ATTR_CURSOR_TYPE));
385            $connection->close();
386        } finally {
387            $container->stop();
388        }
389    }
390
391    public function testGet_warningsReturnsTheNativeWarning(): void
392    {
393        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
394        try {
395            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
396            $connection->set_charset('utf8mb4');
397            $store = new ShadowStore();
398            $session = new Session(self::createStub(SqlRewriter::class), $store, new ResultSelectRunner(), ZtdConfig::default(), self::createStub(ConnectionInterface::class), resultColumnTypeResolver: new MySqlResultColumnTypeResolver());
399            $connection->query('CREATE TEMPORARY TABLE warning_values (value TINYINT)');
400            $native = $connection->prepare('INSERT IGNORE INTO warning_values VALUES (1000)');
401            self::assertInstanceOf(mysqli_stmt::class, $native);
402            $statement = new ZtdMysqliStatement($native, $session, null);
403            $statement->execute();
404            $warning = $statement->get_warnings();
405            self::assertInstanceOf(mysqli_warning::class, $warning);
406            self::assertSame(1264, $warning->errno);
407            $connection->close();
408        } finally {
409            $container->stop();
410        }
411    }
412
413    public function testMore_resultsReturnsFalseAfterASingleResult(): void
414    {
415        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
416        try {
417            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
418            $connection->set_charset('utf8mb4');
419            $store = new ShadowStore();
420            $session = new Session(self::createStub(SqlRewriter::class), $store, new ResultSelectRunner(), ZtdConfig::default(), self::createStub(ConnectionInterface::class), resultColumnTypeResolver: new MySqlResultColumnTypeResolver());
421            $native = $connection->prepare('SELECT 1');
422            self::assertInstanceOf(mysqli_stmt::class, $native);
423            $statement = new ZtdMysqliStatement($native, $session, null);
424            $statement->execute();
425            self::assertFalse($statement->more_results());
426            $connection->close();
427        } finally {
428            $container->stop();
429        }
430    }
431
432    public function testNext_resultReturnsFalseWithoutAnotherResult(): void
433    {
434        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
435        try {
436            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
437            $connection->set_charset('utf8mb4');
438            $store = new ShadowStore();
439            $session = new Session(self::createStub(SqlRewriter::class), $store, new ResultSelectRunner(), ZtdConfig::default(), self::createStub(ConnectionInterface::class), resultColumnTypeResolver: new MySqlResultColumnTypeResolver());
440            $native = $connection->prepare('SELECT 1');
441            self::assertInstanceOf(mysqli_stmt::class, $native);
442            $statement = new ZtdMysqliStatement($native, $session, null);
443            $statement->execute();
444            $statement->store_result();
445            self::assertFalse($statement->next_result());
446            $connection->close();
447        } finally {
448            $container->stop();
449        }
450    }
451
452    public function testPrepareReplacesTheDelegatedStatement(): void
453    {
454        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
455        try {
456            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
457            $connection->set_charset('utf8mb4');
458            $store = new ShadowStore();
459            $session = new Session(self::createStub(SqlRewriter::class), $store, new ResultSelectRunner(), ZtdConfig::default(), self::createStub(ConnectionInterface::class), resultColumnTypeResolver: new MySqlResultColumnTypeResolver());
460            $native = $connection->prepare('SELECT 1');
461            self::assertInstanceOf(mysqli_stmt::class, $native);
462            $statement = new ZtdMysqliStatement($native, $session, null);
463            self::assertTrue($statement->prepare('SELECT 42 AS value'));
464            self::assertTrue($statement->execute());
465            $result = $statement->get_result();
466            self::assertInstanceOf(mysqli_result::class, $result);
467            self::assertSame([['value' => 42]], $result->fetch_all(MYSQLI_ASSOC));
468            $connection->close();
469        } finally {
470            $container->stop();
471        }
472    }
473
474    public function testSend_long_dataConcatenatesBinaryChunks(): void
475    {
476        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
477        try {
478            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
479            $connection->set_charset('utf8mb4');
480            $store = new ShadowStore();
481            $session = new Session(self::createStub(SqlRewriter::class), $store, new ResultSelectRunner(), ZtdConfig::default(), self::createStub(ConnectionInterface::class), resultColumnTypeResolver: new MySqlResultColumnTypeResolver());
482            $native = $connection->prepare('SELECT ? AS value');
483            self::assertInstanceOf(mysqli_stmt::class, $native);
484            $statement = new ZtdMysqliStatement($native, $session, null);
485            $value = null;
486            $statement->bind_param('b', $value);
487            self::assertTrue($statement->send_long_data(0, 'first'));
488            self::assertTrue($statement->send_long_data(0, 'second'));
489            self::assertTrue($statement->execute());
490            $result = $statement->get_result();
491            self::assertInstanceOf(mysqli_result::class, $result);
492            self::assertSame([['value' => 'firstsecond']], $result->fetch_all(MYSQLI_ASSOC));
493            $connection->close();
494        } finally {
495            $container->stop();
496        }
497    }
498
499    public function testResetClearsTheSimulatedAffectedRowCount(): void
500    {
501        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
502        try {
503            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
504            $connection->set_charset('utf8mb4');
505            $store = new ShadowStore();
506            $store->set('items', [['id' => 7]]);
507            $session = new Session(self::createStub(SqlRewriter::class), $store, new ResultSelectRunner(), ZtdConfig::default(), self::createStub(ConnectionInterface::class), resultColumnTypeResolver: new MySqlResultColumnTypeResolver());
508            $native = $connection->prepare('SELECT 7 AS id');
509            self::assertInstanceOf(mysqli_stmt::class, $native);
510            $plan = new RewritePlan('SELECT 7 AS id', QueryKind::WRITE_SIMULATED, new InsertMutation('items', ['id'], true));
511            $statement = new ZtdMysqliStatement($native, $session, $plan);
512            self::assertTrue($statement->execute());
513            self::assertSame(0, $statement->ztdAffectedRows());
514            self::assertTrue($statement->reset());
515            self::assertSame((int) $native->affected_rows, $statement->ztdAffectedRows());
516            self::assertNotSame(0, $statement->ztdAffectedRows());
517            $connection->close();
518        } finally {
519            $container->stop();
520        }
521    }
522
523    public function testCloseClosesTheDelegatedStatement(): void
524    {
525        $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
526        try {
527            $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
528            $connection->set_charset('utf8mb4');
529            $store = new ShadowStore();
530            $session = new Session(self::createStub(SqlRewriter::class), $store, new ResultSelectRunner(), ZtdConfig::default(), self::createStub(ConnectionInterface::class), resultColumnTypeResolver: new MySqlResultColumnTypeResolver());
531            $native = $connection->prepare('SELECT 1');
532            self::assertInstanceOf(mysqli_stmt::class, $native);
533            $statement = new ZtdMysqliStatement($native, $session, null);
534            self::assertTrue($statement->close());
535            $closed = $connection->query("SHOW SESSION STATUS LIKE 'Com_stmt_close'");
536            self::assertInstanceOf(mysqli_result::class, $closed);
537            self::assertSame('1', $closed->fetch_all(MYSQLI_ASSOC)[0]['Value']);
538            $connection->close();
539        } finally {
540            $container->stop();
541        }
542    }
543
544}
545