2016年07月27日

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_typecharacter 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 に含まれるバイト列はどうなるでしょう?

  1. charset=utf8, row には UTF-8 のバイト列
  2. 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 は何になるでしょうか?

  1. VARCHAR
  2. STRING
  3. 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 は?

  1. binary
  2. utf8
  3. utf8mb4

A. 1

きっとテキスト型とちがって、カラムに character set を保存して無いので、文字列/バイナリ型以外の整数型などと同じように binary を返してしまうんでしょうね。

Q. connection encoding が utf8 のコネクションで、 SELECT CAST('{"hello": "world"}' AS JSON) AS anon_1 したとき、 character set は?

  1. binary
  2. utf8
  3. utf8mb4

A. 2

カラムと違ってコネクションには character set があるので、 それを返しているんでしょうね。でも、整数型とかは SELECT 42 しても binary なんですよね…

さて、上に書いた擬似コードを見て、JSON型のカラムをテキストとして返すにはどうすればいいか考えてみましょう。

結論: プロトコルを考えるときは、受け取る側がシステマチックに 解釈できるように(個別のif文が少なくなるように)設計しましょうね!!

@methane


songofacandy at 21:40│Comments(0)TrackBack(0)mysql 

トラックバックURL

この記事にコメントする

名前:
URL:
  情報を記憶: 評価: 顔   
 
 
 
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
KLabについて
KLab株式会社は、信頼性の高いクラウドサービス、ソフトウェアパッケージ、自社で企画・開発したソーシャルアプリやデジタルコンテンツを提供しています。
Blog内検索
最新コメント
最新トラックバック
Archives