Passed
Pull Request — 4.11 (#10334)
by Steve
07:08
created

testUtf8mb4GeneralCollation()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 25
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 17
nc 2
nop 0
dl 0
loc 25
rs 9.7
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\ORM\Tests;
4
5
use PDO;
6
use SilverStripe\Core\Config\Config;
7
use SilverStripe\ORM\Connect\MySQLDatabase;
8
use SilverStripe\Dev\SapphireTest;
9
use SilverStripe\Dev\TestOnly;
10
use SilverStripe\ORM\Tests\MySQLPDOConnectorTest\PDOConnector;
11
use SilverStripe\ORM\DB;
12
13
/**
14
 * @requires extension PDO
15
 * @requires extension pdo_mysql
16
 */
17
class MySQLPDOConnectorTest extends SapphireTest implements TestOnly
18
{
19
    /**
20
     * @dataProvider charsetProvider
21
     */
22
    public function testConnectionCharsetControl($charset, $defaultCollation)
23
    {
24
        $config = DB::getConfig();
25
        $config['driver'] = 'mysql';
26
        $config['charset'] = $charset;
27
        $config['database'] = 'information_schema';
28
        Config::inst()->set(MySQLDatabase::class, 'connection_collation', $defaultCollation);
0 ignored issues
show
Bug introduced by
The method set() does not exist on SilverStripe\Config\Coll...nfigCollectionInterface. It seems like you code against a sub-type of SilverStripe\Config\Coll...nfigCollectionInterface such as SilverStripe\Config\Coll...nfigCollectionInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

28
        Config::inst()->/** @scrutinizer ignore-call */ set(MySQLDatabase::class, 'connection_collation', $defaultCollation);
Loading history...
29
30
        if (strtolower(substr($config['type'] ?? '', 0, 5)) !== 'mysql') {
31
            return $this->markTestSkipped('The test only relevant for MySQL');
32
        }
33
34
        $connector = new PDOConnector();
35
        $connector->connect($config);
36
        $connection = $connector->getPDOConnection();
37
38
        $cset = $connection->query('show variables like "character_set_connection"')->fetch(PDO::FETCH_NUM)[1];
39
        $collation = $connection->query('show variables like "collation_connection"')->fetch(PDO::FETCH_NUM)[1];
40
41
        $this->assertEquals($this->getUpdatedUtfCharset($charset), $cset);
42
        $this->assertEquals($this->getUpdatedUtfCollation($defaultCollation), $collation);
43
44
        unset($cset, $connection, $connector, $config);
45
    }
46
47
    /**
48
     * @dataProvider charsetProvider
49
     */
50
    public function testConnectionCollationControl($charset, $defaultCollation, $customCollation)
51
    {
52
        $config = DB::getConfig();
53
        $config['charset'] = $charset;
54
        $config['driver'] = 'mysql';
55
        $config['database'] = 'information_schema';
56
        Config::inst()->set(MySQLDatabase::class, 'connection_collation', $customCollation);
57
58
        if (strtolower(substr($config['type'] ?? '', 0, 5)) !== 'mysql') {
59
            return $this->markTestSkipped('The test only relevant for MySQL');
60
        }
61
62
        $connector = new PDOConnector();
63
        $connector->connect($config);
64
        $connection = $connector->getPDOConnection();
65
66
        $cset = $connection->query('show variables like "character_set_connection"')->fetch(PDO::FETCH_NUM)[1];
67
        $collation = $connection->query('show variables like "collation_connection"')->fetch(PDO::FETCH_NUM)[1];
68
69
        $this->assertEquals($this->getUpdatedUtfCharset($charset), $cset);
70
        $this->assertEquals($this->getUpdatedUtfCollation($customCollation), $collation);
71
72
        unset($cset, $connection, $connector, $config);
73
    }
74
75
    public function charsetProvider()
76
    {
77
        return [
78
            ['ascii', 'ascii_general_ci', 'ascii_bin'],
79
            ['utf8', 'utf8_general_ci', 'utf8_unicode_520_ci'],
80
            ['utf8mb4', 'utf8mb4_general_ci', 'utf8mb4_unicode_520_ci']
81
        ];
82
    }
83
84
    public function testUtf8mb4GeneralCollation()
85
    {
86
        $charset = 'utf8mb4';
87
        $collation = 'utf8mb4_general_ci';
88
89
        $config = DB::getConfig();
90
        $config['charset'] = $charset;
91
        $config['driver'] = 'mysql';
92
        $config['database'] = 'information_schema';
93
        Config::inst()->set(MySQLDatabase::class, 'connection_collation', $collation);
94
95
        if (strtolower(substr($config['type'] ?? '', 0, 5)) !== 'mysql') {
96
            return $this->markTestSkipped('The test only relevant for MySQL');
97
        }
98
99
        $connector = new PDOConnector();
100
        $connector->connect($config);
101
        $connection = $connector->getPDOConnection();
102
103
        $result = $connection->query(
104
            "select `a`.`value` from (select 'rst' `value` union select 'rßt' `value`) `a` order by `value`"
105
        )->fetchAll();
106
107
        $this->assertCount(1, $result, '`utf8mb4_general_ci` handles both values as equal to "rst"');
108
        $this->assertEquals('rst', $result[0][0]);
109
    }
110
111
    public function testUtf8mb4UnicodeCollation()
112
    {
113
        $charset = 'utf8mb4';
114
        $collation = 'utf8mb4_unicode_ci';
115
116
        $config = DB::getConfig();
117
        $config['charset'] = $charset;
118
        $config['driver'] = 'mysql';
119
        $config['database'] = 'information_schema';
120
        Config::inst()->set(MySQLDatabase::class, 'connection_collation', $collation);
121
122
        if (strtolower(substr($config['type'] ?? '', 0, 5)) !== 'mysql') {
123
            return $this->markTestSkipped('The test only relevant for MySQL');
124
        }
125
126
        $connector = new PDOConnector();
127
        $connector->connect($config);
128
        $connection = $connector->getPDOConnection();
129
130
        $result = $connection->query(
131
            "select `a`.`value` from (select 'rst' `value` union select 'rßt' `value`) `a` order by `value`"
132
        )->fetchAll();
133
134
        $this->assertCount(2, $result, '`utf8mb4_unicode_ci` must recognise "rst" and "rßt" as different values');
135
        $this->assertEquals('rßt', $result[0][0]);
136
        $this->assertEquals('rst', $result[1][0]);
137
    }
138
139
    private function getUpdatedUtfCharset(string $charset): string
140
    {
141
        if ($charset !== 'utf8') {
142
            return $charset;
143
        }
144
        return $this->isMySqlgte80() || $this->isMariaDBgte106() ? 'utf8mb3' : 'utf8';
145
    }
146
147
    private function getUpdatedUtfCollation(string $collation): string
148
    {
149
        if ($collation === 'utf8_general_ci') {
150
            return $this->isMariaDBgte106() ? 'utf8mb3_general_ci' : 'utf8_general_ci';
151
        }
152
        if ($collation === 'utf8_unicode_520_ci') {
153
            return $this->isMariaDBgte106() ? 'utf8mb3_unicode_520_ci' : 'utf8_unicode_520_ci';
154
        }
155
        return $collation;
156
    }
157
158
    /**
159
     * MySQL has used utf8 as an alias for utf8mb3
160
     * Beginning with MySQL 8.0.28, utf8mb3 is used
161
     * https://dev.mysql.com/doc/refman/8.0/en/charset-unicode-utf8mb3.html
162
     */
163
    private function isMySqlgte80()
164
    {
165
        if (preg_match('#^([0-9]+)\.[0-9]+\.[0-9]+$#', $this->getDBVersion(), $m)) {
166
            return (int) $m[1] >= 8;
167
        }
168
        return false;
169
    }
170
171
    /**
172
     * Until MariaDB 10.5, utf8mb3 was an alias for utf8.
173
     * From MariaDB 10.6, utf8 is by default an alias for utf8mb3
174
     * https://mariadb.com/kb/en/unicode/
175
     */
176
    private function isMariaDBgte106()
177
    {
178
        if (preg_match('#^([0-9]+)\.([0-9]+)\.[0-9]+-mariadb#', $this->getDBVersion(), $m)) {
179
            return (int) $m[1] >= 11 || ((int) $m[1] >= 10 && (int) $m[2] >= 6);
180
        }
181
        return false;
182
    }
183
184
    private $dbVersion = '';
185
186
    private function getDBVersion()
187
    {
188
        if ($this->dbVersion === '') {
189
            $this->dbVersion = DB::query('SELECT VERSION();')->first()['VERSION()'];
190
            $this->dbVersion = strtolower($this->dbVersion);
191
        }
192
        return $this->dbVersion;
193
    }
194
}
195