MySQL 8.3.0 で --character-set-client-handshake が削除された件について ~ go-sql-driver/mysqlとmysqlndを添えて ~
こんにちわ。せじまです。
相変わらずPHPよくわかんないんですが、今日もPHPの話をします。あと、goの話もちょっとだけします。書いてるうちに少し長文になってしまいましたが、内容としてはゆるふわと言えるんじゃないでしょうか。
でははじめます。
はじめに
MySQL 8.2.0 の release notes で
と書かれていた --character-set-client-handshake が、8.3.0 では
ということで削除されました。8.2.0で deprecated と告知されて三ヶ月後の8.3.0で削除されたので、確かに as soon as possible ですね。さすが Innovation Release です。
--character-set-client-handshake は、MySQL 8.0公式ドキュメントの A.11 MySQL 8.0 FAQ: MySQL Chinese, Japanese, and Korean Character Sets で
と記述されていたものです。
アジアのユーザとしては、 --skip-character-set-client-handshake がなくなることによってどのような影響が出るか気になるところなので、公式mysqlクライアント、Go、PHPでどのような影響がでそうか、軽く調べてみました。
予め雑なまとめ
最初にざっくりまとめておきます。
- 公式のmysqlクライアント、go-sql-driver/mysql、PHPのmysqlndで、 character set や collation のデフォルトの振る舞いはそれぞれ異なる。
- 公式のmysqlクライアントはlibmysqlclientのMYSQL_AUTODETECT_CHARSET_NAMEを使っている。
- go-sql-driver/mysql は内部でデフォルトのcollationを定義している。 1.5.0 でデフォルトの collation が utf8_general_ci から utf8mb4_general_ci になった。
- PHPのmysqlndはサーバのgreet packetで返ってきたcharacter set/collationをデフォルトで使う。
- PHPのmysqlndのデフォルトは --skip-character-set-client-handshake に近い振る舞いをしている。--skip-character-set-client-handshake を使っていたPHPユーザは、 --skip-character-set-client-handshake がなくても、今までと近い感覚で扱えると思われる。
- PHP以外のプログラミング言語やクライアントは、それぞれの振る舞いを確認した上で、必要に応じて明示的にcharacter setやcollationを設定したほうが良い。
- character set や collation の設定方法については、各プログラミング言語のクライアントライブラリの振る舞いを確認したほうが良い。
- 例えば、PHPでは 「mysqli_query() で (SET NAMES utf8 などとして) 設定する方法はお勧めできません。」と明記されている。
- character set や collation の設定方法については、各プログラミング言語のクライアントライブラリの振る舞いを確認したほうが良い。
- 現在、 --skip-character-set-client-handshake を有効にして運用している場合、go-sql-driver/mysqlのようにutf8mb4_general_ciをデフォルトにして接続している環境だと、将来MySQLのバージョンを上げたとき、予期せぬcollationが使われる可能性がある。
- go-sql-driver/mysqlのようにクライアント側でutf8mb4_general_ciを設定していても、--skip-character-set-client-handshake が設定されていると、 collation_serverの値が使われる。
- init_connect でものすごい頑張ると、character set や collation についてある程度融通は効く。ただし、PHPにおけるSET NAMESのような問題を内包している可能性はある。
では詳細に移ります。
公式の mysql クライアントの振る舞い(libmysqlclientのMYSQL_AUTODETECT_CHARSET_NAMEの振る舞い)
公式のMySQLのaptリポジトリから sudo apt-get install mysql-client すると
1 2 3 4 5 6 |
$ mysql --version mysql Ver 8.0.36 for Linux on x86_64 (MySQL Community Server - GPL) $ mysql --help | grep ^default-character-set default-character-set auto $ |
default-character-set=auto になっていて、この auto ってなんぞ?って話ですが
https://github.com/mysql/mysql-server/blob/mysql-8.0.36/client/mysql.cc#L189
https://github.com/mysql/mysql-server/blob/mysql-8.0.36/include/mysql_com.h#L71
公式ドキュメントにあるMYSQL_AUTODETECT_CHARSET_NAMEです。公式ドキュメント曰く
各クライアントは、Unix システムの LANG または LC_ALL ロケール環境変数の値や Windows システムのコードページ設定など、オペレーティングシステムの設定に基づいて、使用する文字セットを自動検出できます。 ロケールが OS から利用できるシステムの場合、クライアントはコンパイル時のデフォルトを使用するのではなく、このロケールを使用してデフォルトの文字セットを設定します。
というやつですね。Ubuntu 22.04 LTS on WSL2とdockerで試してみましょう。
1 2 3 |
$ sudo apt install language-pack-ja $ sudo locale-gen ja_JP.EUC-JP |
した後、LANGを切り替えたりしながらmysqlクライアントで接続してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
$ docker run -p 127.0.0.1:13306:3306 -e MYSQL_ROOT_HOST=172.17.0.1 -e MYSQL_ROOT_PASSWORD=sejima --name mysql8036 -d container-registry.oracle.com/mysql/community-server:8.0.36 244e458b9670f04059684bd710724a1d71430e2f2d0d8fbd26c549dcf11ea3c1 $ MYSQL_PWD=sejima mysql -u root -e "show variables like '%character%'" -h 127.0.0.1 -P 13306 +--------------------------+--------------------------------+ | Variable_name | Value | +--------------------------+--------------------------------+ | character_set_client | utf8mb4 | | character_set_connection | utf8mb4 | | character_set_database | utf8mb4 | | character_set_filesystem | binary | | character_set_results | utf8mb4 | | character_set_server | utf8mb4 | | character_set_system | utf8mb3 | | character_sets_dir | /usr/share/mysql-8.0/charsets/ | +--------------------------+--------------------------------+ $ LANG=ja_JP.eucjp MYSQL_PWD=sejima mysql -u root -e "show variables like '%character%'" -h 127.0.0.1 -P 13306 +--------------------------+--------------------------------+ | Variable_name | Value | +--------------------------+--------------------------------+ | character_set_client | ujis | | character_set_connection | ujis | | character_set_database | utf8mb4 | | character_set_filesystem | binary | | character_set_results | ujis | | character_set_server | utf8mb4 | | character_set_system | utf8mb3 | | character_sets_dir | /usr/share/mysql-8.0/charsets/ | +--------------------------+--------------------------------+ $ |
LANG=ja_JP.eucjpにすると、character_set_client、character_set_connection、character_set_resultsがujisになりました。
なお、LANG=ja_JP.eucjpであっても、--default-character-setや--init-commandで上書き可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
$ LANG=ja_JP.eucjp MYSQL_PWD=sejima mysql -u root -e "show variables like '%character%'" -h 127.0.0.1 -P 13306 --default-character-set=utf8mb4 +--------------------------+--------------------------------+ | Variable_name | Value | +--------------------------+--------------------------------+ | character_set_client | utf8mb4 | | character_set_connection | utf8mb4 | | character_set_database | utf8mb4 | | character_set_filesystem | binary | | character_set_results | utf8mb4 | | character_set_server | utf8mb4 | | character_set_system | utf8mb3 | | character_sets_dir | /usr/share/mysql-8.0/charsets/ | +--------------------------+--------------------------------+ $ LANG=ja_JP.eucjp MYSQL_PWD=sejima mysql -u root -e "show variables like '%character%'" -h 127.0.0.1 -P 13306 --init-command='SET NAMES utf8mb4' +--------------------------+--------------------------------+ | Variable_name | Value | +--------------------------+--------------------------------+ | character_set_client | utf8mb4 | | character_set_connection | utf8mb4 | | character_set_database | utf8mb4 | | character_set_filesystem | binary | | character_set_results | utf8mb4 | | character_set_server | utf8mb4 | | character_set_system | utf8mb3 | | character_sets_dir | /usr/share/mysql-8.0/charsets/ | +--------------------------+--------------------------------+ $ |
このように、mysqlクライアントで設定されたcharacter setをmysqldはcharacter_set_clientに設定するわけですが、--character-set-client-handshake=off(--skip-character-set-client-handshake)にすることによって、character_set_serverの値で強制することができます。具体的には、次のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
$ docker run -p 127.0.0.1:13306:3306 -e MYSQL_ROOT_HOST=172.17.0.1 -e MYSQL_ROOT_PASSWORD=sejima --name mysql8036 -d container-registry.oracle.com/mysql/community-server:8.0.36 --character-set-client-handshake=off f589a9b2472fc2c4638f13c182a1446a5e6ca1db75963941f53e9faaa16d5d00 $ LANG=ja_JP.eucjp MYSQL_PWD=sejima mysql -u root -e "show variables like '%character%'" -h 127.0.0.1 -P 13306 +--------------------------+--------------------------------+ | Variable_name | Value | +--------------------------+--------------------------------+ | character_set_client | utf8mb4 | | character_set_connection | utf8mb4 | | character_set_database | utf8mb4 | | character_set_filesystem | binary | | character_set_results | utf8mb4 | | character_set_server | utf8mb4 | | character_set_system | utf8mb3 | | character_sets_dir | /usr/share/mysql-8.0/charsets/ | +--------------------------+--------------------------------+ $ |
go-sql-driver/mysql の振る舞い
go-sql-driver/mysql ではデフォルトのcharacter set(正確にはcollation)が定義されており、1.5 (2020-01-07) でデフォルトが utf8_general_ci から utf8mb4_general_ci になりました。
Update collations and make utf8mb4 default #877
README: update default collation to utf8mb4_general_ci #1054
雑に検証用のコードを書いて確認してみます。次のコードは、go 1.18.1、github.com/go-sql-driver/mysql v1.7.1で確認しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
package main import ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" ) func main() { db, err := sql.Open("mysql", "root:sejima@(127.0.0.1:13306)/") if err != nil { panic(err.Error()) } defer db.Close() rows, err := db.Query("SHOW VARIABLES LIKE 'character_set_%'") if err != nil { panic(err.Error()) } var col1, col2 []byte for rows.Next() { err = rows.Scan(&col1, &col2) if err != nil { panic(err.Error()) } fmt.Printf("%s (%s)\n", col1, col2) } if err = rows.Err(); err != nil { panic(err.Error()) } rows, err = db.Query("SHOW VARIABLES LIKE 'collation_%'") if err != nil { panic(err.Error()) } for rows.Next() { err = rows.Scan(&col1, &col2) if err != nil { panic(err.Error()) } fmt.Printf("%s (%s)\n", col1, col2) } if err = rows.Err(); err != nil { panic(err.Error()) } } |
go-sql-driver/mysql も MySQL 8.0 もデフォルトのままだと、collation_connectionはutf8mb4_general_ci、collation_serverはutf8mb4_0900_ai_ciになります。いずれも character set としてはutf8mb4ですが、utf8mb4_general_ciとutf8mb4_0900_ai_ciは振る舞いが大きく異なるので、Goでutf8mb4を使うなら、意図したcollationが適用されているか注意が必要です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$ docker run -p 127.0.0.1:13306:3306 -e MYSQL_ROOT_HOST=172.17.0.1 -e MYSQL_ROOT_PASSWORD=sejima --name mysql8036 -d container-registry.oracle.com/mysql/community-server:8.0.36 f7bbeda9c4ca8c4b0b46173c05d6499b015f7c821e94d21e081563b8a4bb2ded $ go run test.go character_set_client (utf8mb4) character_set_connection (utf8mb4) character_set_database (utf8mb4) character_set_filesystem (binary) character_set_results (utf8mb4) character_set_server (utf8mb4) character_set_system (utf8mb3) character_sets_dir (/usr/share/mysql-8.0/charsets/) collation_connection (utf8mb4_general_ci) collation_database (utf8mb4_0900_ai_ci) collation_server (utf8mb4_0900_ai_ci) $ |
--character-set-server=ujis のとき、go-sql-driver/mysqlはデフォルトのままだと、character_set_client は utf8mb4 になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$ docker run -p 127.0.0.1:13306:3306 -e MYSQL_ROOT_HOST=172.17.0.1 -e MYSQL_ROOT_PASSWORD=sejima --name mysql8036 -d container-registry.oracle.com/mysql/community-server:8.0.36 --character-set-server=ujis 75ee78d977d12fbef29a0b2366d288e2d5e074cc329ca53660f913c6e26a809d $ go run test.go character_set_client (utf8mb4) character_set_connection (utf8mb4) character_set_database (ujis) character_set_filesystem (binary) character_set_results (utf8mb4) character_set_server (ujis) character_set_system (utf8mb3) character_sets_dir (/usr/share/mysql-8.0/charsets/) collation_connection (utf8mb4_general_ci) collation_database (ujis_japanese_ci) collation_server (ujis_japanese_ci) $ |
--character-set-client-handshake=off にすると、collation_connectionもcollation_serverと同様にutf8mb4_0900_ai_ciになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$ docker run -p 127.0.0.1:13306:3306 -e MYSQL_ROOT_HOST=172.17.0.1 -e MYSQL_ROOT_PASSWORD=sejima --name mysql8036 -d container-registry.oracle.com/mysql/community-server:8.0.36 --character-set-client-handshake=off ea94ccbe5cc91c90fa3e768e260cb436bf8b2989f0629e84623e4afa0fef3df8 $ go run test.go character_set_client (utf8mb4) character_set_connection (utf8mb4) character_set_database (utf8mb4) character_set_filesystem (binary) character_set_results (utf8mb4) character_set_server (utf8mb4) character_set_system (utf8mb3) character_sets_dir (/usr/share/mysql-8.0/charsets/) collation_connection (utf8mb4_0900_ai_ci) collation_database (utf8mb4_0900_ai_ci) collation_server (utf8mb4_0900_ai_ci) $ |
また、 --character-set-server=ujis かつ --character-set-client-handshake=off にすると、character_set_clientはujisになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$ docker run -p 127.0.0.1:13306:3306 -e MYSQL_ROOT_HOST=172.17.0.1 -e MYSQL_ROOT_PASSWORD=sejima --name mysql8036 -d container-registry.oracle.com/mysql/community-server:8.0.36 --character-set-client-handshake=off --character-set-server=ujis 5e0a1b5ca885c258fc1ade3d5e716cab54cce94ebaeba2bb07bc3e48d9cb5c9a $ go run test.go character_set_client (ujis) character_set_connection (ujis) character_set_database (ujis) character_set_filesystem (binary) character_set_results (ujis) character_set_server (ujis) character_set_system (utf8mb3) character_sets_dir (/usr/share/mysql-8.0/charsets/) collation_connection (ujis_japanese_ci) collation_database (ujis_japanese_ci) collation_server (ujis_japanese_ci) $ |
go-sql-driver/mysql の デフォルトの collation が MySQL 8.0 のデフォルトと異なるというのは要注意ですが、 go-sql-driver/mysql の振る舞いは比較的素直なものに見えます。
PHPのmysqlnd
PHPよくわからないので、まずは https://www.php.net/downloads.php から 8.1.27 のソースコードをダウンロードしてデバッグビルドします。
1 2 3 4 5 6 |
$ tar xf php-8.1.27.tar.xz $ cd php-8.1.27/ $ ./configure --enable-debug --with-mysqli=mysqlnd --with-openssl $ make -j $ cd sapi/cli/ |
デバッグビルドすることで mysqli_debug が使えるようになるので、次のような検証用のスクリプトを書いてみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
<?php mysqli_debug("T:n:t:m:x:F:L:o,/tmp/client.trace"); printf("Client library version: %s\n", mysqli_get_client_info()); $mysqli = new mysqli('127.0.0.1', 'root', 'sejima', null, 13306); if ($mysqli->connect_errno) { echo "Errno: " . $mysqli->connect_errno . PHP_EOL; echo "Error: " . $mysqli->connect_error . PHP_EOL; exit; } echo "PHP" . phpversion() . PHP_EOL; $sql = "SHOW VARIABLES LIKE 'character_set_%'"; if (!$result = $mysqli->query($sql)) { echo "Query: " . $sql . PHP_EOL; echo "Errno: " . $mysqli->errno . PHP_EOL; echo "Error: " . $mysqli->error . PHP_EOL; exit; } $rows = $result->fetch_all(MYSQLI_NUM); foreach ($rows as $row) { printf("%s (%s)\n", $row[0], $row[1]); } $result->free(); $sql = "SHOW VARIABLES LIKE 'collation_%'"; if (!$result = $mysqli->query($sql)) { echo "Query: " . $sql . PHP_EOL; echo "Errno: " . $mysqli->errno . PHP_EOL; echo "Error: " . $mysqli->error . PHP_EOL; exit; } $rows = $result->fetch_all(MYSQLI_NUM); foreach ($rows as $row) { printf("%s (%s)\n", $row[0], $row[1]); } $result->free(); $mysqli->close(); ?> |
--character-set-client-handshake の設定を変えずに MySQL 8.0 のデフォルトの設定で繋いでみると、 mysqlnd では collation_connection は utf8mb4_0900_ai_ci になりました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
$ docker run -p 127.0.0.1:13306:3306 -e MYSQL_ROOT_HOST=172.17.0.1 -e MYSQL_ROOT_PASSWORD=sejima --name mysql8036 -d container-registry.oracle.com/mysql/community-server:8.0.36 1956580bf6adea2cda020ad71e67674417ad27118c4b2ff905c1f5c59c3ab2b0 $ ./php ~/test.php Client library version: mysqlnd 8.1.27 PHP8.1.27 character_set_client (utf8mb4) character_set_connection (utf8mb4) character_set_database (utf8mb4) character_set_filesystem (binary) character_set_results (utf8mb4) character_set_server (utf8mb4) character_set_system (utf8mb3) character_sets_dir (/usr/share/mysql-8.0/charsets/) collation_connection (utf8mb4_0900_ai_ci) collation_database (utf8mb4_0900_ai_ci) collation_server (utf8mb4_0900_ai_ci) $ |
次に、 --character-set-server=ujis で起動してみると、今度はcharacter_set_clientがujisになりました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
$ docker run -p 127.0.0.1:13306:3306 -e MYSQL_ROOT_HOST=172.17.0.1 -e MYSQL_ROOT_PASSWORD=sejima --name mysql8036 -d container-registry.oracle.com/mysql/community-server:8.0.36 --character-set-server=ujis 7966361f49e0c7a20aa22351a3353d520a0a005d9c338b1cce59173e1faaad60 $ ./php ~/test.php Client library version: mysqlnd 8.1.27 PHP8.1.27 character_set_client (ujis) character_set_connection (ujis) character_set_database (ujis) character_set_filesystem (binary) character_set_results (ujis) character_set_server (ujis) character_set_system (utf8mb3) character_sets_dir (/usr/share/mysql-8.0/charsets/) collation_connection (ujis_japanese_ci) collation_database (ujis_japanese_ci) collation_server (ujis_japanese_ci) $ |
なんでやねん?
そうお思いの方もいらっしゃることでしょう。私は思いました。これがPHPです。
mysqli_optionsのドキュメントには次のように書かれています。
MySQLnd always assumes the server default charset. This charset is sent during connection hand-shake/authentication, which mysqlnd will use.
ここで、デバッグビルドすることで有効にできたmysqli_debugが活きてきます。出力したログを見ると、php_mysqlnd_greet_read()でcharset_noを読んでいることがわかります。
mysqldの設定がデフォルトのときは charset_no=255 が
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
$ sed -n '/>.*greet_read.*/,/<.*greet_read.*/p' /tmp/client.trace 14:40:26.300264 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 33 4:| | | | >php_mysqlnd_greet_read 14:40:26.300290 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 29 5:| | | | | >mysqlnd_read_packet_header_and_body 14:40:26.300292 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 29 6:| | | | | | info : buf=0x7ffed16badd0 size=2048 14:40:26.300294 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 24 6:| | | | | | >mysqlnd_read_header 14:40:26.300296 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 24 7:| | | | | | | info : compressed=0 14:40:26.300299 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_protocol_frame_codec.c: 32 7:| | | | | | | >mysqlnd_pfc::receive 14:40:26.300301 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_protocol_frame_codec.c: 37 7:| | | | | | | <mysqlnd_pfc::receive (total=1 own=1 in_calls=0) 14:40:26.300303 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_vio.c: 71 7:| | | | | | | >mysqlnd_vio::get_stream 14:40:26.300305 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_vio.c: 71 8:| | | | | | | | info : 0x7f87d5a83600 14:40:26.300307 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_vio.c: 71 7:| | | | | | | <mysqlnd_vio::get_stream (total=2 own=2 in_calls=0) 14:40:26.300309 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_vio.c: 8 7:| | | | | | | >mysqlnd_vio::network_read 14:40:26.300311 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_vio.c: 8 8:| | | | | | | | info : count=4 14:40:26.300980 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_vio.c: 9 7:| | | | | | | <mysqlnd_vio::network_read (total=668 own=668 in_calls=0) 14:40:26.300994 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 25 7:| | | | | | | info : HEADER: prot_packet_no=0 size= 74 14:40:26.300998 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 26 6:| | | | | | <mysqlnd_read_header (total=702 own=31 in_calls=671) 14:40:26.301001 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_protocol_frame_codec.c: 32 6:| | | | | | >mysqlnd_pfc::receive 14:40:26.301003 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_protocol_frame_codec.c: 37 6:| | | | | | <mysqlnd_pfc::receive (total=0 own=0 in_calls=0) 14:40:26.301006 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_vio.c: 71 6:| | | | | | >mysqlnd_vio::get_stream 14:40:26.301008 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_vio.c: 71 7:| | | | | | | info : 0x7f87d5a83600 14:40:26.301010 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_vio.c: 71 6:| | | | | | <mysqlnd_vio::get_stream (total=2 own=2 in_calls=0) 14:40:26.301014 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_vio.c: 8 6:| | | | | | >mysqlnd_vio::network_read 14:40:26.301016 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_vio.c: 8 7:| | | | | | | info : count=74 14:40:26.301027 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_vio.c: 9 6:| | | | | | <mysqlnd_vio::network_read (total=11 own=11 in_calls=0) 14:40:26.301030 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 31 5:| | | | | <mysqlnd_read_packet_header_and_body (total=737 own=22 in_calls=715) 14:40:26.301033 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 40 5:| | | | | info : 4.1 server_caps=65535 14:40:26.301035 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 43 5:| | | | | info : additional 5.5+ caps=57343 14:40:26.301038 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 45 5:| | | | | info : proto=10 server=8.0.36 thread_id=8 14:40:26.301040 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 45 5:| | | | | info : server_capabilities=3758096383 charset_no=255 server_status=2 auth_protocol=caching_sha2_password scramble_length=21 14:40:26.301044 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 46 4:| | | | <php_mysqlnd_greet_read (total=778 own=41 in_calls=737) $ |
--character-set-server=ujis のときは charset_no=12 が返ってきています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
$ sed -n '/>.*greet_read.*/,/<.*greet_read.*/p' /tmp/client.trace 14:41:36.132148 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 33 4:| | | | >php_mysqlnd_greet_read 14:41:36.132151 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 29 5:| | | | | >mysqlnd_read_packet_header_and_body 14:41:36.132153 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 29 6:| | | | | | info : buf=0x7fff644e9a00 size=2048 14:41:36.132155 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 24 6:| | | | | | >mysqlnd_read_header 14:41:36.132157 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 24 7:| | | | | | | info : compressed=0 14:41:36.132159 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_protocol_frame_codec.c: 32 7:| | | | | | | >mysqlnd_pfc::receive 14:41:36.132162 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_protocol_frame_codec.c: 37 7:| | | | | | | <mysqlnd_pfc::receive (total=1 own=1 in_calls=0) 14:41:36.132164 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_vio.c: 71 7:| | | | | | | >mysqlnd_vio::get_stream 14:41:36.132166 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_vio.c: 71 8:| | | | | | | | info : 0x7f8dd3683600 14:41:36.132168 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_vio.c: 71 7:| | | | | | | <mysqlnd_vio::get_stream (total=2 own=2 in_calls=0) 14:41:36.132171 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_vio.c: 8 7:| | | | | | | >mysqlnd_vio::network_read 14:41:36.132173 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_vio.c: 8 8:| | | | | | | | info : count=4 14:41:36.133045 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_vio.c: 9 7:| | | | | | | <mysqlnd_vio::network_read (total=872 own=872 in_calls=0) 14:41:36.133072 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 25 7:| | | | | | | info : HEADER: prot_packet_no=0 size= 74 14:41:36.133076 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 26 6:| | | | | | <mysqlnd_read_header (total=919 own=44 in_calls=875) 14:41:36.133080 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_protocol_frame_codec.c: 32 6:| | | | | | >mysqlnd_pfc::receive 14:41:36.133082 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_protocol_frame_codec.c: 37 6:| | | | | | <mysqlnd_pfc::receive (total=0 own=0 in_calls=0) 14:41:36.133085 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_vio.c: 71 6:| | | | | | >mysqlnd_vio::get_stream 14:41:36.133087 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_vio.c: 71 7:| | | | | | | info : 0x7f8dd3683600 14:41:36.133089 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_vio.c: 71 6:| | | | | | <mysqlnd_vio::get_stream (total=2 own=2 in_calls=0) 14:41:36.133092 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_vio.c: 8 6:| | | | | | >mysqlnd_vio::network_read 14:41:36.133094 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_vio.c: 8 7:| | | | | | | info : count=74 14:41:36.133100 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_vio.c: 9 6:| | | | | | <mysqlnd_vio::network_read (total=6 own=6 in_calls=0) 14:41:36.133103 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 31 5:| | | | | <mysqlnd_read_packet_header_and_body (total=951 own=24 in_calls=927) 14:41:36.133106 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 40 5:| | | | | info : 4.1 server_caps=65535 14:41:36.133108 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 43 5:| | | | | info : additional 5.5+ caps=57343 14:41:36.133111 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 45 5:| | | | | info : proto=10 server=8.0.36 thread_id=8 14:41:36.133114 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 45 5:| | | | | info : server_capabilities=3758096383 charset_no=12 server_status=2 auth_protocol=caching_sha2_password scramble_length=21 14:41:36.133117 /home/sejima/src/php/php-8.1.27/ext/mysqlnd/mysqlnd_wireprotocol.c: 46 4:| | | | <php_mysqlnd_greet_read (total=967 own=16 in_calls=951) $ |
ここでMySQLの認証周り、handshakeについて振り返ってみます。
The initial handshake starts with the server sending the Protocol::Handshake packet.
とありますね。Handshake Packetですが
を見ると、
int<1> character_set default server a_protocol_character_set, only the lower 8-bits
といったフィールドがあります。character set のIDは、Source Code Documentation
を参照すると、information_schema.collationsから collation_name を取得できるとわかります。
実際に information_schema.collations にSELECTしてみると
1 2 3 4 5 6 7 8 9 10 |
mysql> SELECT id, collation_name FROM information_schema.collations WHERE id IN (12,255); +-----+--------------------+ | id | collation_name | +-----+--------------------+ | 12 | ujis_japanese_ci | | 255 | utf8mb4_0900_ai_ci | +-----+--------------------+ 2 rows in set (0.01 sec) mysql> |
charset_no=255はutf8mb4_0900_ai_ci、charset_no=12はujis_japanese_ciとわかります。
mysqlnd_command::handshake() からソースコードを読み進めていくと、 greet_packet.charset_no を関数の引数で渡していって
https://github.com/php/php-src/blob/php-8.1.27/ext/mysqlnd/mysqlnd_commands.c#L603-L658
https://github.com/php/php-src/blob/php-8.1.27/ext/mysqlnd/mysqlnd_auth.c#L202-L224
https://github.com/php/php-src/blob/php-8.1.27/ext/mysqlnd/mysqlnd_auth.c#L36-L122
https://github.com/php/php-src/blob/php-8.1.27/ext/mysqlnd/mysqlnd_auth.c#L233-L289
session_options->charset_name が設定されていなければ、関数の引数で渡されてきたserver_charset_noをauth_packet.charset_noに設定しています。
285 286 287 288 289 |
if (session_options->charset_name && (charset = mysqlnd_find_charset_name(session_options->charset_name))) { auth_packet.charset_no = charset->nr; } else { auth_packet.charset_no = server_charset_no; } |
auth_packet.charset_no と実際の character set や collation のマッピングは
https://github.com/php/php-src/blob/php-8.1.27/ext/mysqlnd/mysqlnd_auth.c#L310
https://github.com/php/php-src/blob/php-8.1.27/ext/mysqlnd/mysqlnd_charset.c#L741-L752
https://github.com/php/php-src/blob/php-8.1.27/ext/mysqlnd/mysqlnd_charset.c#L496
496 |
{ 12, "ujis", "ujis_japanese_ci", 1, 3, "", mysqlnd_mbcharlen_ujis, check_mb_ujis}, |
https://github.com/php/php-src/blob/php-8.1.27/ext/mysqlnd/mysqlnd_charset.c#L688
688 |
{ 255, UTF8_MB4, UTF8_MB4"_0900_ai_ci", 1, 4, "", mysqlnd_mbcharlen_utf8, check_mb_utf8_valid}, |
というように、mysqlnd内部で定義されているわけですね。
mysqlnd における SET NAMES の注意事項
SET NAMES ステートメントを使うことでcharacter_set_client、character_set_connection および character_set_resultsを変更することが可能ではあるんですが、クライアントライブラリによっては問題が発生することもあります。例えば、PHPのmysqli_real_escape_stringのドキュメントには
サーバーレベルで設定するなり API 関数 mysqli_set_charset() を使うなりして、 文字セットを明示しておく必要があります。この文字セットが mysqli_real_escape_string() に影響を及ぼします。詳細は 文字セットの概念 を参照ください。
とあります。
具体的には、mysqlnd_conn_data::escape_string()で mysqlnd_cset_escape_slashes() を呼ぶと
843 844 |
/* check unicode characters */ if (cset->char_maxlen > 1 && (len = cset->mb_valid(escapestr, end))) { |
や
857 |
if (cset->char_maxlen > 1 && cset->mb_charlen(*escapestr) > 1) { |
といった箇所がありますが、この cset->mb_valid() や cset->mb_charlen() が、さきほど見た
496 |
{ 12, "ujis", "ujis_japanese_ci", 1, 3, "", mysqlnd_mbcharlen_ujis, check_mb_ujis}, |
や
688 |
{ 255, UTF8_MB4, UTF8_MB4"_0900_ai_ci", 1, 4, "", mysqlnd_mbcharlen_utf8, check_mb_utf8_valid}, |
の mysqlnd_mbcharlen_ujis や check_mb_ujis 、mysqlnd_mbcharlen_utf8 や check_mb_utf8_valid なわけですね。
例えば、 utf8mb4_general_ci は
https://github.com/php/php-src/blob/php-8.1.27/ext/mysqlnd/mysqlnd_charset.c#L532
532 |
{ 45, UTF8_MB4, UTF8_MB4"_general_ci", 1, 4, "UTF-8 Unicode", mysqlnd_mbcharlen_utf8, check_mb_utf8_valid}, |
mysqlnd_mbcharlen_utf8とcheck_mb_utf8_validなので、utf8mb4_0900_ai_ciと互換性がありそうですけど、mysqlnd_mbcharlen_ujisやcheck_mb_ujisのujisでは文字列のエスケープがうまく動かなそうだ、というところです。
mb_charlenやmb_validなども切り替えるために、PHPのmysqli_real_escape_stringのドキュメントには、mysqli_set_charset()を使うよう書いてあるわけですね。
こういったエスケープに関する振る舞いは、各プログラミング言語のライブラリ間で挙動が異なる可能性も否めません。ライブラリでcharacter setをどのように設定すべきなのかは、ライブラリのドキュメント等確認されるのがよろしいでしょう。
おまけ: init_connet
「ujisとutf8mb4くらい違うならしょうがないけど、collationくらいならなんとかインチキできんのか?」
そうお考えの方もいらっしゃるでしょう。私は考えます。
というわけで、 --skip-character-set-client-handshake を使わずに --init-connect で collation を強制的に上書きしてみようと思います。
次のように指定しつつmysqld起動して
1 2 |
$ docker run -p 127.0.0.1:13306:3306 -e MYSQL_ROOT_HOST=172.17.0.1 -e MYSQL_ROOT_PASSWORD=sejima -e MYSQL_USER=bang -e MYSQL_PASSWORD=brave --name mysql8036 -d container-registry.oracle.com/mysql/community-server:8.0.36 --init-connect="SET NAMES utf8mb4 COLLATE utf8mb4_general_ci; SET SESSION default_collation_for_utf8mb4=utf8mb4_general_ci; SET SESSION collation_database=utf8mb4_general_ci; SET SESSION collation_server=utf8mb4_general_ci;" |
まずはmysqlクライアントで試してみましょう。rootはSUPER権限があるので、init_connect の影響を受けません。LANG=ja_JP.eucjpではcharacter_set_client=ujisになります。また、collation_serverなどはMySQL8.0のデフォルトであるutf8mb4_0900_ai_ciになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
$ LANG=ja_JP.eucjp MYSQL_PWD=sejima mysql -u root -e "show variables like '%character%'" -h 127.0.0.1 -P 13306 +--------------------------+--------------------------------+ | Variable_name | Value | +--------------------------+--------------------------------+ | character_set_client | ujis | | character_set_connection | ujis | | character_set_database | utf8mb4 | | character_set_filesystem | binary | | character_set_results | ujis | | character_set_server | utf8mb4 | | character_set_system | utf8mb3 | | character_sets_dir | /usr/share/mysql-8.0/charsets/ | +--------------------------+--------------------------------+ $ LANG=ja_JP.eucjp MYSQL_PWD=sejima mysql -u root -e "show variables like '%collation%'" -h 127.0.0.1 -P 13306 +-------------------------------+--------------------+ | Variable_name | Value | +-------------------------------+--------------------+ | collation_connection | ujis_japanese_ci | | collation_database | utf8mb4_0900_ai_ci | | collation_server | utf8mb4_0900_ai_ci | | default_collation_for_utf8mb4 | utf8mb4_0900_ai_ci | +-------------------------------+--------------------+ $ |
SUPER権限もCONNECTION_ADMIN権限もないユーザでアクセスすると、次のように書き換わります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
$ LANG=ja_JP.eucjp MYSQL_PWD=brave mysql -u bang -e "show variables like '%character%'" -h 127.0.0.1 -P 13306 +--------------------------+--------------------------------+ | Variable_name | Value | +--------------------------+--------------------------------+ | character_set_client | utf8mb4 | | character_set_connection | utf8mb4 | | character_set_database | utf8mb4 | | character_set_filesystem | binary | | character_set_results | utf8mb4 | | character_set_server | utf8mb4 | | character_set_system | utf8mb3 | | character_sets_dir | /usr/share/mysql-8.0/charsets/ | +--------------------------+--------------------------------+ $ LANG=ja_JP.eucjp MYSQL_PWD=brave mysql -u bang -e "show variables like '%collation%'" -h 127.0.0.1 -P 13306 +-------------------------------+--------------------+ | Variable_name | Value | +-------------------------------+--------------------+ | collation_connection | utf8mb4_general_ci | | collation_database | utf8mb4_general_ci | | collation_server | utf8mb4_general_ci | | default_collation_for_utf8mb4 | utf8mb4_general_ci | +-------------------------------+--------------------+ $ |
次は、go-sql-driver/mysqlで確認してみましょう。sed を使って test.go のアカウントとパスワードを書き換えます。
1 2 |
$ sed -i.orig -e 's/root/bang/' -e 's/sejima/brave/' test.go |
次のように、 --character-set-client-handshake=off を使わなくても、collation_serverやcollation_databaseをutf8mb4_general_ciで置き換えることが可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$ go run test.go character_set_client (utf8mb4) character_set_connection (utf8mb4) character_set_database (utf8mb4) character_set_filesystem (binary) character_set_results (utf8mb4) character_set_server (utf8mb4) character_set_system (utf8mb3) character_sets_dir (/usr/share/mysql-8.0/charsets/) collation_connection (utf8mb4_general_ci) collation_database (utf8mb4_general_ci) collation_server (utf8mb4_general_ci) $ |
ただし、これはcollation_connectionやcollation_serverなど書き換えられただけなので、これでクライアントライブラリが文字列のエスケープなど正しく実行できるかどうかは、ライブラリのドキュメントなど要確認ですね。
※今回、 --init-connect で default_collation_for_utf8mb4 というシステム変数を指定しました。デフォルトのcollationをutf8mb4_0900_ai_ciから変更する場合、default_collation_for_utf8mb4についても考慮した方が良いので指定しましたが、このシステム変数についても解説しようとすると、それだけで記事が一本書けてしまうシロモノになります。ゆえに、今回の記事ではdefault_collation_for_utf8mb4の詳細については触れません。
実際、北川さんがdefault_collation_for_utf8mb4などで解説記事を一本執筆されていますので、気になる方はこちらの記事を参照してください。
おわりに
振り返ってみると、個人的には
- character setやcollationのデフォルトの振る舞いがクライアントライブラリ側で異なっているので、 --skip-character-set-client-handshake でサーバ側の設定を強制させることにより、各プログラミング言語ごとの振る舞いの違いを吸収しやすかった。
という一面もあったんじゃないかと思いました。ただ、古いソースコードや機能を整理していくことは、ソフトウェアを長期的にメンテナンスしていく上で大切なことだと思いますから、 --skip-character-set-client-handshake を廃止するメリットも理解できるところです。少なくとも、プログラミング言語のライブラリごとにこれだけ振る舞いが異なって、さらに、 --character-set-client-handshake の on/off でまた振る舞いが変わるのであれば、複雑怪奇という他ありません。分岐する条件は減らせるなら減らしたいと思うのは、自然なことだろうと思います。
とりあえず、PHPで --skip-character-set-client-handshake の設定された mysqld 使ってた方々は、そんなに考えることはないかもしれません。ただ、PHP以外のプログラミング言語を使われていて今後MySQLのバージョンアップを予定されている方々は、character set や collation の設定を、一度見直してみるとよろしいのかもしれません。
References
- 10.4 接続文字セットおよび照合順序
- 5.4.54 mysql_options()
- 第65回 MySQLと文字コード | gihyo.jp
- 第157回 MySQLのデフォルトcollationの注意点 | gihyo.jp
- MySQL 8.0 でも utf8mb4_general_ci を使い続けたい僕らは | mita2 database life