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\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\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(MySqlRelease::fromEnvironment());
32 $endpoint = $container->getData(Endpoint::class);
33 try {
34 $connection = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
35 $connection->set_charset('utf8mb4');
36 $native = $connection->prepare('SELECT ? AS value');
37 self::assertInstanceOf(mysqli_stmt::class, $native);
38 $statement = new MysqliStatement($native, $connection);
39 self::assertTrue($statement->execute(['Alice']));
40 self::assertSame([['value' => 'Alice']], $statement->fetchAll());
41 $connection->close();
42 } finally {
43 $container->stop();
44 }
45 }
46
47 public function testPreservesBindingsWithAnEmptyParameterArray(): void
48 {
49 $container = Testcontainers::run(MySqlRelease::fromEnvironment());
50 $endpoint = $container->getData(Endpoint::class);
51 try {
52 $connection = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
53 $connection->set_charset('utf8mb4');
54 $native = $connection->prepare('SELECT ? AS value');
55 self::assertInstanceOf(mysqli_stmt::class, $native);
56 $value = 7;
57 $native->bind_param('i', $value);
58 $statement = new MysqliStatement($native, $connection);
59 $value = 42;
60 self::assertTrue($statement->execute([]));
61 self::assertSame([['value' => 42]], $statement->fetchAll());
62 $connection->close();
63 } finally {
64 $container->stop();
65 }
66 }
67
68 public function testResultColumnsKeepsTheResultAvailableAfterReadingMetadata(): void
69 {
70 $container = Testcontainers::run(MySqlRelease::fromEnvironment());
71 $endpoint = $container->getData(Endpoint::class);
72 try {
73 $connection = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
74 $connection->set_charset('utf8mb4');
75 $native = $connection->prepare('SELECT 7 AS id UNION ALL SELECT 9 AS id');
76 self::assertInstanceOf(mysqli_stmt::class, $native);
77 $statement = new MysqliStatement($native, $connection);
78 self::assertTrue($statement->execute());
79 $resolver = self::createStub(ResultColumnTypeResolver::class);
80 $resolver->method('resolve')->willReturn(new ColumnType(ColumnTypeFamily::INTEGER, 'INT'));
81 self::assertSame('id', $statement->resultColumns($resolver)[0]->name);
82 self::assertSame('id', $statement->resultColumns($resolver)[0]->name);
83 self::assertSame(2, $statement->rowCount());
84 self::assertSame([['id' => 7], ['id' => 9]], $statement->fetchAll());
85 $status = $connection->query("SHOW SESSION STATUS LIKE 'Com_stmt_close'");
86 self::assertInstanceOf(mysqli_result::class, $status);
87 self::assertSame([['Variable_name' => 'Com_stmt_close', 'Value' => '1']], $status->fetch_all(MYSQLI_ASSOC));
88 $connection->close();
89 } finally {
90 $container->stop();
91 }
92 }
93
94 public function testFetchAllReturnsNoRowsOrMetadataForAWrite(): void
95 {
96 $container = Testcontainers::run(MySqlRelease::fromEnvironment());
97 $endpoint = $container->getData(Endpoint::class);
98 try {
99 $connection = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
100 $connection->set_charset('utf8mb4');
101 $connection->query('CREATE TEMPORARY TABLE counts (id INT)');
102 $native = $connection->prepare('INSERT INTO counts VALUES (1), (2)');
103 self::assertInstanceOf(mysqli_stmt::class, $native);
104 $statement = new MysqliStatement($native, $connection);
105 self::assertTrue($statement->execute());
106 self::assertSame(2, $statement->rowCount());
107 self::assertSame([], $statement->resultColumns(self::createStub(ResultColumnTypeResolver::class)));
108 self::assertSame([], $statement->fetchAll());
109 $status = $connection->query("SHOW SESSION STATUS LIKE 'Com_stmt_close'");
110 self::assertInstanceOf(mysqli_result::class, $status);
111 self::assertSame([['Variable_name' => 'Com_stmt_close', 'Value' => '1']], $status->fetch_all(MYSQLI_ASSOC));
112 $connection->close();
113 } finally {
114 $container->stop();
115 }
116 }
117
118 public function testReportsNativeExecutionErrorsWithoutParameters(): void
119 {
120 $container = Testcontainers::run(MySqlRelease::fromEnvironment());
121 $endpoint = $container->getData(Endpoint::class);
122 try {
123 $connection = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
124 $connection->set_charset('utf8mb4');
125 $connection->query('CREATE TEMPORARY TABLE duplicate_keys (id INT PRIMARY KEY)');
126 $connection->query('INSERT INTO duplicate_keys VALUES (1)');
127 $native = $connection->prepare('INSERT INTO duplicate_keys VALUES (1)');
128 self::assertInstanceOf(mysqli_stmt::class, $native);
129 mysqli_report(MYSQLI_REPORT_OFF);
130 try {
131 $this->expectException(DatabaseException::class);
132 $this->expectExceptionCode(1062);
133 (new MysqliStatement($native, $connection))->execute();
134 } finally {
135 mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
136 $connection->close();
137 }
138 } finally {
139 $container->stop();
140 }
141 }
142
143 public function testReportsNativeExecutionErrorsWithParameters(): void
144 {
145 $container = Testcontainers::run(MySqlRelease::fromEnvironment());
146 $endpoint = $container->getData(Endpoint::class);
147 try {
148 $connection = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
149 $connection->set_charset('utf8mb4');
150 $connection->query('CREATE TEMPORARY TABLE duplicate_keys (id INT PRIMARY KEY)');
151 $connection->query('INSERT INTO duplicate_keys VALUES (1)');
152 $native = $connection->prepare('INSERT INTO duplicate_keys VALUES (?)');
153 self::assertInstanceOf(mysqli_stmt::class, $native);
154 mysqli_report(MYSQLI_REPORT_OFF);
155 try {
156 $this->expectException(DatabaseException::class);
157 $this->expectExceptionCode(1062);
158 (new MysqliStatement($native, $connection))->execute([1]);
159 } finally {
160 mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
161 $connection->close();
162 }
163 } finally {
164 $container->stop();
165 }
166 }
167
168 public function testRowCountReportsNativeAffectedRows(): void
169 {
170 $container = Testcontainers::run(MySqlRelease::fromEnvironment());
171 $endpoint = $container->getData(Endpoint::class);
172 try {
173 $connection = new mysqli($endpoint->host, $endpoint->username, $endpoint->password, $endpoint->database, $endpoint->port);
174 $connection->set_charset('utf8mb4');
175 $connection->query('CREATE TEMPORARY TABLE row_counts (id INT)');
176 $native = $connection->prepare('INSERT INTO row_counts VALUES (1), (2)');
177 self::assertInstanceOf(mysqli_stmt::class, $native);
178 $statement = new MysqliStatement($native, $connection);
179 self::assertTrue($statement->execute());
180 self::assertSame(2, $statement->rowCount());
181 $connection->close();
182 } finally {
183 $container->stop();
184 }
185 }
186}
187