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