MySQL プロトコル上のテキストの闇
MySQL 5.7 で JSON 型が追加されたのに関連して、私がメンテナをしている MySQL ドライバーで罠にハマったので、 MySQL のテキストプロトコルの闇を書き残しておくことにします。
Text Resultset
MySQL にクエリを投げるコマンドが COM_QUERY で、そのレスポンスとして返されるのが Text Resultset と呼ばれる一連のパケットです。その中身は次のようになっています。
- column count (整数1つだけのパケット)
- column definition * column count (各カラムの定義)
- EOF
- row * N (結果行数分)
- EOF
この記事で注目するのは column definition パケット (定義) の中にある、 column_type と character set です。
Python の MySQL ドライバーは、この2つの値を元に、 row に含まれる各バイト列を Python の適切な型の値に変換しています。
なお、以降に書く MySQL の振る舞いは、 MySQL 5.7.13 に基いています。
Character Set の闇
Q. connection encoding が latin1 のコネクションで、UTF-8 の VARCHAR 型のカラムに保存された「こんにちは」を SELECT したとき、 column definition の charset と row に含まれるバイト列はどうなるでしょう?
- charset=utf8, row には UTF-8 のバイト列
- charset=latin1, row には latin1で "?????"
A. 2
どうやら、 Column Definition にある character set は Column Character Set と無関係に connection encoding になるようです。 カラム毎に変わらないなら、なんで Column Definition に character set 書いてるんでしょうね?
あと、カラムの値は、カラムのエンコーディングからコネクションのエンコーディングにサーバーサイドで変換されるようです。 テキストの関数を使う必要が無いのなら、 VARCHAR や TEXT より VARBINARY や BLOB 使ったほうがエンコーディングに関わる問題を踏みにくそうです。
TEXT 型の闇
column type の一覧 を見てみると、 VARCHAR, VAR_STRING, STRING,
TINY_BLOB, MEDIUM_BLOB, LONG_BLOB, BLOB
といった型があるのですが、 TEXT 型がありません。
Q. TEXT 型のカラムを SELECT したとき、 column definition の column_type は何になるでしょうか?
- VARCHAR
- STRING
- BLOB
A. 3
どうやら、 column_type は特にテキストとバイナリの区別を付けていないようですね。 区別付けないのはいいとして、型の名前として VARBINARY じゃなくて VARCHAR を使っているのに、 TEXT じゃなくて BLOB を使ってるのはどうなんでしょうか?テキストかバイナリのどちらかに寄せておいたほうが混乱が少ないと思うのですが。
さて、 Python ではテキストとバイナリを明確に区別したいです。 Java や C# もきっと同じだと思います。 その場合、 character set を見てテキストとバイナリを区別することができます。 character set が connection encoding と同じならテキストで、 binary ならバイナリです。
なお、 INT 型などは charset は binary が設定されます。 例えば 42 は ASCII の "42" として返ってくるので、文字列を10進整数としてパースしたい場合はいったん ASCII でデコードしてからパースすることになると思います。 これを擬似コードで書くとこうなります。
if column_type in [VARCHAR, VAR_STRING, ... 全ての文字列/バイナリ型]: if charset == CHARSET_BINARY: # バイト列 return data else: return data.decode(connection_encoding) else: # 文字列以外 data = data.decode('ascii') # ascii でデコードしておいて、 return converters[column_type](data) # column_type ごとに用意した変換関数で変換する
JSON 型の闇
column_type には JSON が追加されました。そして MySQL 内部では JSON は常に UTF-8 (utf8mb4) で扱っているはずです。
さて、 column definition の character set はどうなっているでしょうか?
Q. connection encoding が utf8 のコネクションで、JSON 型のカラムを SELECT したとき、 character set は?
- binary
- utf8
- utf8mb4
A. 1
きっとテキスト型とちがって、カラムに character set を保存して無いので、文字列/バイナリ型以外の整数型などと同じように binary を返してしまうんでしょうね。
Q. connection encoding が utf8 のコネクションで、 SELECT CAST('{"hello": "world"}' AS JSON) AS anon_1
したとき、 character set は?
- binary
- utf8
- utf8mb4
A. 2
カラムと違ってコネクションには character set があるので、 それを返しているんでしょうね。でも、整数型とかは SELECT 42
しても binary なんですよね…
さて、上に書いた擬似コードを見て、JSON型のカラムをテキストとして返すにはどうすればいいか考えてみましょう。
結論: プロトコルを考えるときは、受け取る側がシステマチックに 解釈できるように(個別のif文が少なくなるように)設計しましょうね!!@methane