MySQL 8.0のクライアントでMySQL 5.7のサーバーに接続するとcharsetが設定されないかもしれない

methaneさんにMySQLのハンドシェイクパケットにはcollation_idが入ってることを教えてもらったので、本当にHandshake Response Packetからcharsetを設定しているのか調べてみた。

MySQL :: MySQL Internals Manual :: 14.2 Connection Phase

Handshake Response Packetのペイロードの構造を見ると先頭から8バイト目にたしかにcharacter_setのidを1バイト入れられるっぽい。

MySQL :: MySQL Internals Manual :: 14.2.5 Connection Phase Packets

MySQL 8.0のdefault collationのid一覧はこれ。

MySQL :: MySQL Internals Manual :: 14.1.4 Character Set

このパケットは、サーバーからのInitial Handshake Packetをパースしたあと、最初にレスポンスするときにmysql_fill_packet_headerで作られてサーバーに送られる。

https://github.com/mysql/mysql-server/blob/mysql-8.0.23/sql-common/client.cc#L4060

サーバーはparse_client_handshake_packetでクライアントからのレスポンスの先頭から8バイト目をcharset_codeとして取り出している。

https://github.com/mysql/mysql-server/blob/mysql-8.0.23/sql/auth/sql_authentication.cc#L2476

https://github.com/mysql/mysql-server/blob/mysql-8.0.23/sql/auth/sql_authentication.cc#L2581

最終的にthd_init_client_charsetで取り出したcs_numberから現在のスレッドハンドルのcharsetを設定している。

https://github.com/mysql/mysql-server/blob/mysql-8.0.23/sql/sql_connect.cc#L422-L423

これにより、mysql_options(mysql, MYSQL_SET_CHARSET_NAME, cs_name)してmysql_real_connect(mysql, ...)するとcs_nameのdefault collationがコネクションのcharsetとして設定されるわけですね。

ここで表題の "MySQL 8.0のクライアントでMySQL 5.7のサーバーに接続するとcharsetが設定されないかもしれない" についてなんですが、MySQL 8.0.1からutf8mb4のdefault collationがutf8mb4_general_ci (id: 45)からutf8mb4_0900_ai_ci (id: 255)に変更されたため、MySQL 8.0のクライアントがuff8mb4でサーバーに接続するとid: 255のcs_numberを送るけどMySQL 5.7はid: 255のcs_numberを知らないのでサーバー側のデフォルトの設定が採用されるという仕組み。

理想的なケースでは、サーバーに接続したらcharsetは適切に設定されるけど、最悪のケース、サーバーはMySQL 5.7でサーバー側のcharsetはutf8mb4に設定されておらずMySQL 8.0のクライアントからutf8mb4で接続するケースではコネクションのcharsetはutf8mb4に設定されない。

一応、接続後にSET NAMES utf8mb4すればサーバー側のutf8mb4のdefault collationが設定されるが、最悪のケースをカバーするために適切に設定してるひとには必要ない処理が増えて損をすることになるのでなんとか回避したい気持ちがあるけど、現状はそういう感じ。