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