packages/ztd-query-mysqli-adapter/tests/Unit/Driver/MysqliStatementTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Driver;
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\MysqliResultColumnExtractor;
18use ZtdQuery\Adapter\Mysqli\Driver\MysqliStatement;
19use ZtdQuery\Connection\Exception\DatabaseException;
20use ZtdQuery\Platform\ResultColumnTypeResolver;
21use ZtdQuery\Schema\ColumnType;
22use ZtdQuery\Schema\ColumnTypeFamily;
23
24#[CoversClass(MysqliStatement::class)]
25#[Large]
26#[UsesClass(MysqliResultColumnExtractor::class)]
27final class MysqliStatementTest extends TestCase
28{
29 public function testExecutesExplicitParameters(): void
30 {
31 $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
32 try {
33 $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
34 $connection->set_charset('utf8mb4');
35 $native = $connection->prepare('SELECT ? AS value');
36 self::assertInstanceOf(mysqli_stmt::class, $native);
37 $statement = new MysqliStatement($native, $connection);
38 self::assertTrue($statement->execute(['Alice']));
39 self::assertSame([['value' => 'Alice']], $statement->fetchAll());
40 $connection->close();
41 } finally {
42 $container->stop();
43 }
44 }
45
46 public function testPreservesBindingsWithAnEmptyParameterArray(): void
47 {
48 $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
49 try {
50 $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
51 $connection->set_charset('utf8mb4');
52 $native = $connection->prepare('SELECT ? AS value');
53 self::assertInstanceOf(mysqli_stmt::class, $native);
54 $value = 7;
55 $native->bind_param('i', $value);
56 $statement = new MysqliStatement($native, $connection);
57 $value = 42;
58 self::assertTrue($statement->execute([]));
59 self::assertSame([['value' => 42]], $statement->fetchAll());
60 $connection->close();
61 } finally {
62 $container->stop();
63 }
64 }
65
66 public function testResultColumnsKeepsTheResultAvailableAfterReadingMetadata(): void
67 {
68 $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
69 try {
70 $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
71 $connection->set_charset('utf8mb4');
72 $native = $connection->prepare('SELECT 7 AS id UNION ALL SELECT 9 AS id');
73 self::assertInstanceOf(mysqli_stmt::class, $native);
74 $statement = new MysqliStatement($native, $connection);
75 self::assertTrue($statement->execute());
76 $resolver = self::createStub(ResultColumnTypeResolver::class);
77 $resolver->method('resolve')->willReturn(new ColumnType(ColumnTypeFamily::INTEGER, 'INT'));
78 self::assertSame('id', $statement->resultColumns($resolver)[0]->name);
79 self::assertSame('id', $statement->resultColumns($resolver)[0]->name);
80 self::assertSame(2, $statement->rowCount());
81 self::assertSame([['id' => 7], ['id' => 9]], $statement->fetchAll());
82 $status = $connection->query("SHOW SESSION STATUS LIKE 'Com_stmt_close'");
83 self::assertInstanceOf(mysqli_result::class, $status);
84 self::assertSame([['Variable_name' => 'Com_stmt_close', 'Value' => '1']], $status->fetch_all(MYSQLI_ASSOC));
85 $connection->close();
86 } finally {
87 $container->stop();
88 }
89 }
90
91 public function testFetchAllReturnsNoRowsOrMetadataForAWrite(): void
92 {
93 $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
94 try {
95 $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
96 $connection->set_charset('utf8mb4');
97 $connection->query('CREATE TEMPORARY TABLE counts (id INT)');
98 $native = $connection->prepare('INSERT INTO counts VALUES (1), (2)');
99 self::assertInstanceOf(mysqli_stmt::class, $native);
100 $statement = new MysqliStatement($native, $connection);
101 self::assertTrue($statement->execute());
102 self::assertSame(2, $statement->rowCount());
103 self::assertSame([], $statement->resultColumns(self::createStub(ResultColumnTypeResolver::class)));
104 self::assertSame([], $statement->fetchAll());
105 $status = $connection->query("SHOW SESSION STATUS LIKE 'Com_stmt_close'");
106 self::assertInstanceOf(mysqli_result::class, $status);
107 self::assertSame([['Variable_name' => 'Com_stmt_close', 'Value' => '1']], $status->fetch_all(MYSQLI_ASSOC));
108 $connection->close();
109 } finally {
110 $container->stop();
111 }
112 }
113
114 public function testReportsNativeExecutionErrorsWithoutParameters(): void
115 {
116 $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
117 try {
118 $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
119 $connection->set_charset('utf8mb4');
120 $connection->query('CREATE TEMPORARY TABLE duplicate_keys (id INT PRIMARY KEY)');
121 $connection->query('INSERT INTO duplicate_keys VALUES (1)');
122 $native = $connection->prepare('INSERT INTO duplicate_keys VALUES (1)');
123 self::assertInstanceOf(mysqli_stmt::class, $native);
124 mysqli_report(MYSQLI_REPORT_OFF);
125 try {
126 $this->expectException(DatabaseException::class);
127 $this->expectExceptionCode(1062);
128 (new MysqliStatement($native, $connection))->execute();
129 } finally {
130 mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
131 $connection->close();
132 }
133 } finally {
134 $container->stop();
135 }
136 }
137
138 public function testReportsNativeExecutionErrorsWithParameters(): void
139 {
140 $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
141 try {
142 $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
143 $connection->set_charset('utf8mb4');
144 $connection->query('CREATE TEMPORARY TABLE duplicate_keys (id INT PRIMARY KEY)');
145 $connection->query('INSERT INTO duplicate_keys VALUES (1)');
146 $native = $connection->prepare('INSERT INTO duplicate_keys VALUES (?)');
147 self::assertInstanceOf(mysqli_stmt::class, $native);
148 mysqli_report(MYSQLI_REPORT_OFF);
149 try {
150 $this->expectException(DatabaseException::class);
151 $this->expectExceptionCode(1062);
152 (new MysqliStatement($native, $connection))->execute([1]);
153 } finally {
154 mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
155 $connection->close();
156 }
157 } finally {
158 $container->stop();
159 }
160 }
161
162 public function testRowCountReportsNativeAffectedRows(): void
163 {
164 $container = Testcontainers::run(getenv('MYSQL_VERSION') === '8.4.7' ? MySql84Container::class : MySql80Container::class);
165 try {
166 $connection = new mysqli(str_replace('localhost', '127.0.0.1', $container->getHost()), 'root', 'root', 'test', $container->getMappedPort(3306));
167 $connection->set_charset('utf8mb4');
168 $connection->query('CREATE TEMPORARY TABLE row_counts (id INT)');
169 $native = $connection->prepare('INSERT INTO row_counts VALUES (1), (2)');
170 self::assertInstanceOf(mysqli_stmt::class, $native);
171 $statement = new MysqliStatement($native, $connection);
172 self::assertTrue($statement->execute());
173 self::assertSame(2, $statement->rowCount());
174 $connection->close();
175 } finally {
176 $container->stop();
177 }
178 }
179}
180