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

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