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