mysql_options(mysql, MYSQL_SET_CHARSET_NAME, cs_name) だけして mysql_real_connect(mysql, ...) した後SHOW VARIABLESしてみたら接続のcharsetが設定済みの挙動をするんやけどmysql_real_connectからの一連のコード読んでもどこでそれが起きるのかわからん誰かたすけて🥲https://t.co/ZScoD3tIQ8
— Ryuta Kamizono (@kamipo) February 20, 2021
MySQLのハンドシェイクパケットにcollation_idを1バイトだけ入れられるところがあって、charset name のデフォルトの collation_id を送っています。
— Inada Naoki (@methane) February 20, 2021
クライアントとサーバーのバージョンが違うとデフォルトのcollation_idが違うことがあって罠になります。
この場合デフォルトのcollationはクライアントライブラリ (libmysqlclient) に定義されていて、MySQL 8.0.どこか で utf8mb4 のデフォルトのcollationが変わったので、バージョンアップしたら動作が変わった!ってなり得ます。
— Inada Naoki (@methane) February 20, 2021
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が設定されるが、最悪のケースをカバーするために適切に設定してるひとには必要ない処理が増えて損をすることになるのでなんとか回避したい気持ちがあるけど、現状はそういう感じ。