packages/ztd-query-mysqli-adapter/tests/Unit/Session/ConnectionExecutionTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Session;
6
7use Container\Endpoint;
8use Container\MySqlRelease;
9use mysqli;
10use mysqli_result;
11use mysqli_stmt;
12use PHPUnit\Framework\Attributes\CoversClass;
13use PHPUnit\Framework\Attributes\Large;
14use PHPUnit\Framework\Attributes\UsesClass;
15use PHPUnit\Framework\TestCase;
16use Testcontainers\Testcontainers;
17use ZtdQuery\Adapter\Mysqli\Driver\MysqliConnection;
18use ZtdQuery\Adapter\Mysqli\Driver\MysqliResultColumnExtractor;
19use ZtdQuery\Adapter\Mysqli\Driver\MysqliResultStatement;
20use ZtdQuery\Adapter\Mysqli\Session\ConnectionExecution;
21use ZtdQuery\Adapter\Mysqli\Session\MysqliResultProcessor;
22use ZtdQuery\Adapter\Mysqli\ZtdMysqliException;
23
24#[CoversClass(ConnectionExecution::class)]
25#[Large]
26#[UsesClass(MysqliConnection::class)]
27#[UsesClass(MysqliResultStatement::class)]
28#[UsesClass(MysqliResultColumnExtractor::class)]
29#[UsesClass(MysqliResultProcessor::class)]
30#[UsesClass(ZtdMysqliException::class)]
31final class ConnectionExecutionTest extends TestCase
32{
33 public function testNativeReturnsTheProvidedConnection(): void
34 {
35 $container = Testcontainers::run(MySqlRelease::fromEnvironment());
36 $endpoint = $container->getData(Endpoint::class);
37 try {
38 $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
39 $native->set_charset('utf8mb4');
40 $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
41 $execution = new ConnectionExecution($native);
42
43 self::assertSame($native, $execution->native());
44 } finally {
45 $container->stop();
46 }
47 }
48
49 public function testExecutorKeepsSimulatedWritesOffTheNativeConnection(): void
50 {
51 $container = Testcontainers::run(MySqlRelease::fromEnvironment());
52 $endpoint = $container->getData(Endpoint::class);
53 try {
54 $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
55 $native->set_charset('utf8mb4');
56 $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
57 $execution = new ConnectionExecution($native);
58
59 self::assertSame(1, $execution->executor()->execStatement('INSERT INTO items VALUES (7)'));
60 $result = $native->query('SELECT COUNT(*) FROM items');
61 self::assertInstanceOf(mysqli_result::class, $result);
62 self::assertSame(['0'], $result->fetch_row());
63 } finally {
64 $container->stop();
65 }
66 }
67
68 public function testSimulatedAffectedRowsStartsWithoutAnOverride(): void
69 {
70 $container = Testcontainers::run(MySqlRelease::fromEnvironment());
71 $endpoint = $container->getData(Endpoint::class);
72 try {
73 $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
74 $native->set_charset('utf8mb4');
75 $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
76 $execution = new ConnectionExecution($native);
77
78 self::assertNull($execution->simulatedAffectedRows());
79 } finally {
80 $container->stop();
81 }
82 }
83
84 public function testPreparePreservesTheWrappingCallback(): void
85 {
86 $container = Testcontainers::run(MySqlRelease::fromEnvironment());
87 $endpoint = $container->getData(Endpoint::class);
88 try {
89 $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
90 $native->set_charset('utf8mb4');
91 $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
92 $execution = new ConnectionExecution($native);
93
94 $wrapped = false;
95 $stmt = $execution->prepare('SELECT 7 AS id', static function (mysqli_stmt $statement) use (&$wrapped): mysqli_stmt {
96 $wrapped = true;
97 return $statement;
98 });
99 self::assertTrue($wrapped);
100 self::assertInstanceOf(mysqli_stmt::class, $stmt);
101 self::assertTrue($stmt->execute());
102 } finally {
103 $container->stop();
104 }
105 }
106
107 public function testQueryPreservesFacadeDispatch(): void
108 {
109 $container = Testcontainers::run(MySqlRelease::fromEnvironment());
110 $endpoint = $container->getData(Endpoint::class);
111 try {
112 $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
113 $native->set_charset('utf8mb4');
114 $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
115 $execution = new ConnectionExecution($native);
116
117 $queries = [];
118 self::assertFalse($execution->query('SELECT 7 AS id', MYSQLI_STORE_RESULT, static function (string $sql) use (&$queries): bool {
119 $queries[] = $sql;
120 return false;
121 }));
122 self::assertSame(['SELECT 7 AS id'], $queries);
123 } finally {
124 $container->stop();
125 }
126 }
127
128 public function testRealQuerySynchronizesTransactionStatements(): void
129 {
130 $container = Testcontainers::run(MySqlRelease::fromEnvironment());
131 $endpoint = $container->getData(Endpoint::class);
132 try {
133 $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
134 $native->set_charset('utf8mb4');
135 $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
136 $execution = new ConnectionExecution($native);
137
138 self::assertTrue($execution->realQuery('BEGIN', $native->prepare(...)));
139 self::assertSame(1, $execution->executor()->execStatement('INSERT INTO items VALUES (7)'));
140 self::assertTrue($execution->realQuery('ROLLBACK', $native->prepare(...)));
141 $result = $native->query($execution->executor()->rewrite('SELECT id FROM items')->sql());
142 self::assertInstanceOf(mysqli_result::class, $result);
143 self::assertSame([], $result->fetch_all(MYSQLI_ASSOC));
144 } finally {
145 $container->stop();
146 }
147 }
148
149 public function testBeginTransactionCreatesAShadowRollbackScope(): void
150 {
151 $container = Testcontainers::run(MySqlRelease::fromEnvironment());
152 $endpoint = $container->getData(Endpoint::class);
153 try {
154 $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
155 $native->set_charset('utf8mb4');
156 $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
157 $execution = new ConnectionExecution($native);
158
159 self::assertTrue($execution->beginTransaction());
160 self::assertSame(1, $execution->executor()->execStatement('INSERT INTO items VALUES (7)'));
161 self::assertTrue($execution->rollBack());
162 $result = $native->query($execution->executor()->rewrite('SELECT id FROM items')->sql());
163 self::assertInstanceOf(mysqli_result::class, $result);
164 self::assertSame([], $result->fetch_all(MYSQLI_ASSOC));
165 } finally {
166 $container->stop();
167 }
168 }
169
170 public function testCommitRetainsShadowRowsAcrossRollback(): void
171 {
172 $container = Testcontainers::run(MySqlRelease::fromEnvironment());
173 $endpoint = $container->getData(Endpoint::class);
174 try {
175 $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
176 $native->set_charset('utf8mb4');
177 $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
178 $execution = new ConnectionExecution($native);
179
180 self::assertTrue($execution->beginTransaction());
181 self::assertSame(1, $execution->executor()->execStatement('INSERT INTO items VALUES (7)'));
182 self::assertTrue($execution->commit());
183 self::assertTrue($execution->beginTransaction());
184 self::assertTrue($execution->rollBack());
185 $result = $native->query($execution->executor()->rewrite('SELECT id FROM items')->sql());
186 self::assertInstanceOf(mysqli_result::class, $result);
187 self::assertSame([['id' => '7']], $result->fetch_all(MYSQLI_ASSOC));
188 } finally {
189 $container->stop();
190 }
191 }
192
193 public function testRollBackRestoresTheShadowSnapshot(): void
194 {
195 $container = Testcontainers::run(MySqlRelease::fromEnvironment());
196 $endpoint = $container->getData(Endpoint::class);
197 try {
198 $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
199 $native->set_charset('utf8mb4');
200 $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
201 $execution = new ConnectionExecution($native);
202
203 self::assertTrue($execution->beginTransaction());
204 self::assertSame(1, $execution->executor()->execStatement('INSERT INTO items VALUES (7)'));
205 self::assertTrue($execution->rollBack());
206 $result = $native->query($execution->executor()->rewrite('SELECT id FROM items')->sql());
207 self::assertInstanceOf(mysqli_result::class, $result);
208 self::assertSame([], $result->fetch_all(MYSQLI_ASSOC));
209 } finally {
210 $container->stop();
211 }
212 }
213
214 public function testAutocommitCommitsShadowRows(): void
215 {
216 $container = Testcontainers::run(MySqlRelease::fromEnvironment());
217 $endpoint = $container->getData(Endpoint::class);
218 try {
219 $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
220 $native->set_charset('utf8mb4');
221 $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
222 $execution = new ConnectionExecution($native);
223
224 self::assertTrue($execution->autocommit(false));
225 self::assertSame(1, $execution->executor()->execStatement('INSERT INTO items VALUES (7)'));
226 self::assertTrue($execution->autocommit(true));
227 self::assertTrue($execution->beginTransaction());
228 self::assertTrue($execution->rollBack());
229 $result = $native->query($execution->executor()->rewrite('SELECT id FROM items')->sql());
230 self::assertInstanceOf(mysqli_result::class, $result);
231 self::assertSame([['id' => '7']], $result->fetch_all(MYSQLI_ASSOC));
232 } finally {
233 $container->stop();
234 }
235 }
236
237 public function testReleaseSavepointRemovesTheNativeSavepoint(): void
238 {
239 $container = Testcontainers::run(MySqlRelease::fromEnvironment());
240 $endpoint = $container->getData(Endpoint::class);
241 try {
242 $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
243 $native->set_charset('utf8mb4');
244 $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
245 $execution = new ConnectionExecution($native);
246
247 self::assertTrue($execution->beginTransaction());
248 self::assertTrue($execution->savepoint('one'));
249 self::assertTrue($execution->releaseSavepoint('one'));
250 self::assertTrue($execution->rollBack());
251 } finally {
252 $container->stop();
253 }
254 }
255
256 public function testSavepointRestoresShadowRowsOnRollbackToSavepoint(): void
257 {
258 $container = Testcontainers::run(MySqlRelease::fromEnvironment());
259 $endpoint = $container->getData(Endpoint::class);
260 try {
261 $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
262 $native->set_charset('utf8mb4');
263 $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
264 $execution = new ConnectionExecution($native);
265
266 self::assertTrue($execution->beginTransaction());
267 self::assertTrue($execution->savepoint('one'));
268 self::assertSame(1, $execution->executor()->execStatement('INSERT INTO items VALUES (7)'));
269 self::assertTrue($execution->realQuery('ROLLBACK TO SAVEPOINT one', $native->prepare(...)));
270 $result = $native->query($execution->executor()->rewrite('SELECT id FROM items')->sql());
271 self::assertInstanceOf(mysqli_result::class, $result);
272 self::assertSame([], $result->fetch_all(MYSQLI_ASSOC));
273 self::assertTrue($execution->rollBack());
274 } finally {
275 $container->stop();
276 }
277 }
278
279 public function testExecuteQueryRetainsTheDispatchedAffectedRowCount(): void
280 {
281 $container = Testcontainers::run(MySqlRelease::fromEnvironment());
282 $endpoint = $container->getData(Endpoint::class);
283 try {
284 $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
285 $native->set_charset('utf8mb4');
286 $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
287 $execution = new ConnectionExecution($native);
288
289 $result = $execution->executeQuery('SELECT ? AS id', [7], $native->prepare(...), static fn (): int => 7);
290 self::assertInstanceOf(mysqli_result::class, $result);
291 self::assertSame([['id' => '7']], $result->fetch_all(MYSQLI_ASSOC));
292 self::assertSame(7, $execution->simulatedAffectedRows());
293 } finally {
294 $container->stop();
295 }
296 }
297
298 public function testAffectedRowsUsesTheNativeCountBeforeSimulation(): void
299 {
300 $container = Testcontainers::run(MySqlRelease::fromEnvironment());
301 $endpoint = $container->getData(Endpoint::class);
302 try {
303 $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
304 $native->set_charset('utf8mb4');
305 $native->query('CREATE TABLE items (id INT PRIMARY KEY)');
306 $execution = new ConnectionExecution($native);
307
308 $native->query('INSERT INTO items VALUES (1), (2)');
309 self::assertSame(2, $execution->affectedRows());
310 } finally {
311 $container->stop();
312 }
313 }
314
315 public function testBeginTransactionDefersTheDefaultSnapshotUntilTheFirstRead(): void
316 {
317 $container = Testcontainers::run(MySqlRelease::fromEnvironment());
318 $endpoint = $container->getData(Endpoint::class);
319 try {
320 $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
321 $native->set_charset('utf8mb4');
322 $other = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
323 try {
324 $native->query('CREATE TABLE snapshot_rows (id INT PRIMARY KEY) ENGINE=InnoDB');
325 $native->query('SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ');
326 $execution = new ConnectionExecution($native);
327 self::assertTrue($execution->beginTransaction());
328 $other->query('INSERT INTO snapshot_rows VALUES (1)');
329 $result = $native->query('SELECT id FROM snapshot_rows');
330 self::assertInstanceOf(mysqli_result::class, $result);
331 self::assertSame([['id' => '1']], $result->fetch_all(MYSQLI_ASSOC));
332 } finally {
333 $native->rollback();
334 $other->close();
335 $native->close();
336 }
337 } finally {
338 $container->stop();
339 }
340 }
341
342 public function testCommitEndsTheNativeTransactionWithoutStartingAnother(): void
343 {
344 $container = Testcontainers::run(MySqlRelease::fromEnvironment());
345 $endpoint = $container->getData(Endpoint::class);
346 try {
347 $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
348 $native->set_charset('utf8mb4');
349 $execution = new ConnectionExecution($native);
350 self::assertTrue($execution->beginTransaction());
351 self::assertTrue($execution->commit());
352 self::assertTrue($native->query('SET TRANSACTION ISOLATION LEVEL READ COMMITTED'));
353 $native->close();
354 } finally {
355 $container->stop();
356 }
357 }
358
359 public function testRollBackEndsTheNativeTransactionWithoutStartingAnother(): void
360 {
361 $container = Testcontainers::run(MySqlRelease::fromEnvironment());
362 $endpoint = $container->getData(Endpoint::class);
363 try {
364 $native = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
365 $native->set_charset('utf8mb4');
366 $execution = new ConnectionExecution($native);
367 self::assertTrue($execution->beginTransaction());
368 self::assertTrue($execution->rollBack());
369 self::assertTrue($native->query('SET TRANSACTION ISOLATION LEVEL READ COMMITTED'));
370 $native->close();
371 } finally {
372 $container->stop();
373 }
374 }
375
376}
377