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