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