読者です 読者をやめる 読者になる 読者になる

MySQL X Protocol を解析してみる

MySQL

前回 MySQL X Protocol で使用している Protbuf について書きましたが、それだけでは MySQL のプロトコルは解析できません。

TCP を流れるデータは区切りがないので、書き込み側が Protbuf データをただ垂れ流しても、読み込む側がどう読んで良いのかわかりません。

書き込むデータの大きさと、書き込む Protbuf データの型を相手に伝える必要があります。

MySQL X Protocol のパケットは次のようになっているようです。

┌────┬─────────────
│size(4) │type(1) + Protbuf(size-1)
└────┴─────────────

最初の4バイト(リトリエンディアン)で続くデータ部のサイズを示します。 データ部の先頭1バイトは Protbuf データの型を示します。

Protobuf データの型は、クライアントから送るデータは ClientMessages::Type で、サーバーから送るデータは ServerMessages::Type に enum で定義されています。

TCP 上を流れるデータの形式がわかったので、あとは、どの型のデータがどのタイミングでサーバー/クライアントのどちらから送られるかがわかればいいです。

ドキュメント https://dev.mysql.com/doc/internals/en/x-protocol.html もありますが、実際に mysqlsh の通信を見てみるのが手っ取り早いかもしれません。

次のような MySQL X Protocol を中継するプログラムを作って動かしてみました。

require 'mysqlx.pb'
require 'socket'

ClientMessage = {}
Mysqlx::ClientMessages::Type.constants.each do |c|
  v = Mysqlx::ClientMessages::Type.const_get(c)
  if v.is_a? Protobuf::Enum
    ClientMessage[v.to_i] = c
  end
end

ServerMessage = {}
Mysqlx::ServerMessages::Type.constants.each do |c|
  v = Mysqlx::ServerMessages::Type.const_get(c)
  if v.is_a? Protobuf::Enum
    ServerMessage[v.to_i] = c
  end
end

localport, host, port = ARGV

def relay(r, w, from)
  while true
    head = r.read(5)
    break unless head && head.length == 5
    size, type = head.unpack('VC')
    if from == :client
      puts "C: #{ClientMessage[type] || type}"
    else
      puts "S: #{ServerMessage[type] || type}"
    end
    data = r.read(size-1)
    break unless data && data.length == size-1
    w.write(head + data)
  end
rescue => e
  p e
end

Socket.tcp_server_loop(localport) do |client, _addrinfo|
  server = TCPSocket.new(host, port)
  Thread.new(client) do |_client|
    relay(_client, server, :client)
  end
  Thread.new(client) do |_client|
    relay(server, _client, :server)
  end
end

33061 ポートで待ち受けて 127.0.0.1 の 33060 に中継するように動かします。

% ruby -I. ./mysqlx-relay.rb 33061 127.0.0.1 33060

別の端末から mysqlsh を次のように起動します。

% mysqlsh --uri mysql://hoge@127.0.0.1:33061/test --sql
Creating a Node Session to hoge@127.0.0.1:33061/test
Enter password: 
Default schema `test` accessible through db.

mysql-sql> プロンプトが出るまでのパケット。結構多い…。

C: CON_CAPABILITIES_GET
S: CONN_CAPABILITIES
C: SESS_AUTHENTICATE_START
S: SESS_AUTHENTICATE_CONTINUE
C: SESS_AUTHENTICATE_CONTINUE
S: NOTICE
S: SESS_AUTHENTICATE_OK
C: SQL_STMT_EXECUTE
S: RESULTSET_COLUMN_META_DATA
S: RESULTSET_ROW
S: RESULTSET_ROW
S: RESULTSET_FETCH_DONE
S: NOTICE
S: SQL_STMT_EXECUTE_OK
C: SQL_STMT_EXECUTE
S: RESULTSET_COLUMN_META_DATA
S: RESULTSET_COLUMN_META_DATA
S: RESULTSET_ROW
S: RESULTSET_FETCH_DONE
S: NOTICE
S: SQL_STMT_EXECUTE_OK
C: SQL_STMT_EXECUTE
S: RESULTSET_COLUMN_META_DATA
S: RESULTSET_ROW
S: RESULTSET_FETCH_DONE
S: NOTICE
S: SQL_STMT_EXECUTE_OK
C: SQL_STMT_EXECUTE
S: NOTICE
S: SQL_STMT_EXECUTE_OK
C: SQL_STMT_EXECUTE
S: RESULTSET_COLUMN_META_DATA
S: RESULTSET_ROW
S: RESULTSET_FETCH_DONE
S: NOTICE
S: SQL_STMT_EXECUTE_OK
C: SQL_STMT_EXECUTE
S: RESULTSET_COLUMN_META_DATA
S: RESULTSET_COLUMN_META_DATA
S: RESULTSET_ROW
S: RESULTSET_FETCH_DONE
S: SQL_STMT_EXECUTE_OK
C: SQL_STMT_EXECUTE
S: RESULTSET_COLUMN_META_DATA
S: RESULTSET_ROW
S: RESULTSET_FETCH_DONE
S: NOTICE
S: SQL_STMT_EXECUTE_OK
C: SQL_STMT_EXECUTE
S: RESULTSET_COLUMN_META_DATA
S: RESULTSET_COLUMN_META_DATA
S: RESULTSET_ROW
S: RESULTSET_FETCH_DONE
S: SQL_STMT_EXECUTE_OK

SELECT したり、

mysql-sql> SELECT * FROM t;
C: SQL_STMT_EXECUTE
S: RESULTSET_COLUMN_META_DATA
S: RESULTSET_COLUMN_META_DATA
S: RESULTSET_ROW
S: RESULTSET_ROW
S: RESULTSET_ROW
S: RESULTSET_FETCH_DONE
S: NOTICE
S: SQL_STMT_EXECUTE_OK

INSERT したり、

mysql-sql> INSERT INTO t (id, value) VALUES (1, 'abc'),(2,'def');
C: SQL_STMT_EXECUTE
S: NOTICE
S: NOTICE
S: SQL_STMT_EXECUTE_OK

いい感じに動いてるようなので、あとは色々試してみます。

RubyからProtobufを使う

Ruby MySQL

MySQL 5.7.12 から追加された X Protocol は Protobuf というのを使ってるらしいです。 Protobuf というのをそこで初めて知ったので、とりあえず Ruby から Protobuf を利用する方法を調べてみました。

Protobuf はデータ構造をバイト列にエンコードしたり、その逆にバイト列をデータ構造にデコードしたりするライブラリのようです。

Ubuntu で protobuf を使うには、protobuf-compiler パッケージをインストールします。

% sudo apt-get install protobuf-compiler

Ruby から Protobuf を使うには、protobuf gem をインストールします。

% gem install protobuf

データ構造は .proto という拡張子のファイルで定義するようです。

MySQL 5.7.12 では rapid/plugin/x/protocol ディレクトリに置かれていました。

% cd mysql-5.7.12/rapid/plugin/x/protocol
% ls
mysqlx.proto             mysqlx_expect.proto     mysqlx_session.proto
mysqlx_connection.proto  mysqlx_expr.proto       mysqlx_sql.proto
mysqlx_crud.proto        mysqlx_notice.proto
mysqlx_datatypes.proto   mysqlx_resultset.proto

Ruby 用にコンパイル(?)します。

% mkdir /tmp/x
% protoc -I . --ruby_out /tmp/x mysqlx.proto
[libprotobuf WARNING google/protobuf/descriptor.cc:5411] Warning: Unused import: "mysqlx.proto" imports "mysqlx_resultset.proto" which is not used.
[libprotobuf WARNING google/protobuf/descriptor.cc:5411] Warning: Unused import: "mysqlx.proto" imports "mysqlx_session.proto" which is not used.
[libprotobuf WARNING google/protobuf/descriptor.cc:5411] Warning: Unused import: "mysqlx.proto" imports "mysqlx_sql.proto" which is not used.
[libprotobuf WARNING google/protobuf/descriptor.cc:5411] Warning: Unused import: "mysqlx.proto" imports "mysqlx_connection.proto" which is not used.
[libprotobuf WARNING google/protobuf/descriptor.cc:5411] Warning: Unused import: "mysqlx.proto" imports "mysqlx_expect.proto" which is not used.
[libprotobuf WARNING google/protobuf/descriptor.cc:5411] Warning: Unused import: "mysqlx.proto" imports "mysqlx_crud.proto" which is not used.
[libprotobuf WARNING google/protobuf/descriptor.cc:5411] Warning: Unused import: "mysqlx.proto" imports "mysqlx_notice.proto" which is not used.
Suppress tag warning output with PB_NO_TAG_WARNINGS=1.
[WARN] .Mysqlx.Datatypes.Scalar object should have 9 tags (1..9), but found 8 tags.
[WARN] .ColumnMetaData.FieldType object should have 18 tags (1..18), but found 11 tags.
[WARN] .Mysqlx.Crud.Find object should have 10 tags (2..11), but found 9 tags.
[WARN] .SessionStateChanged.Parameter object should have 11 tags (1..11), but found 10 tags.
[WARN] .ClientMessages.Type object should have 25 tags (1..25), but found 14 tags.
[WARN] .ServerMessages.Type object should have 19 tags (0..18), but found 13 tags.

いくつか Warning が出てますが、よくわからないので無視します。

/tmp/x に .proto に対応する .pb.rb ファイルが出来ました。

% cd /tmp/x
% ls
mysqlx.pb.rb             mysqlx_expect.pb.rb     mysqlx_session.pb.rb
mysqlx_connection.pb.rb  mysqlx_expr.pb.rb       mysqlx_sql.pb.rb
mysqlx_crud.pb.rb        mysqlx_notice.pb.rb
mysqlx_datatypes.pb.rb   mysqlx_resultset.pb.rb

mysql.x.pb.rb 中にある Mysqlx::Error で試してみます。

module Mysqlx
...
  class Error < ::Protobuf::Message
    optional ::Mysqlx::Error::Severity, :severity, 1, :default => ::Mysqlx::Error::Severity::ERROR
    required :uint32, :code, 2
    required :string, :sql_state, 4
    required :string, :msg, 3
  end
end
require "mysqlx.pb"

e1 = Mysqlx::Error.new(code: 123, sql_state: "XXXXX", msg: "hoge")
s = e.encode  # => "\x10{\x1A\x04hoge\"\x05XXXXX"

e2 = Mysqlx::Error.decode(s)
e2.severity   # => #<Protobuf::Enum(Mysqlx::Error::Severity)::ERROR=0>
e2.code       # => 123
e2.sql_state  # => "XXXXX"
e2.msg        # => "hoge"

Mysqlx::Error オブジェクトがバイト列にエンコード(シリアライズ)されて、バイト列からオブジェクトにデコード(デシリアライズ)されたことがわかります。

Mysqlx::Error は severity が optional で、その他の code, sql_state, msg が required とされています。

required メンバーを指定せずにシリアライズするとエラーになります。

e = Mysqlx::Error.new(code: 123)
e.encode  # => Required field Mysqlx::Error#msg does not have a value. (Protobuf::SerializationError)

また、定義と異なる型を設定しようとしてもエラーになります。

e = Mysqlx::Error.new
e.code = 123456789    # => Ok
e.code = 12345678901  # => Unacceptable value 12345678901 for field code of type Protobuf::Field::Uint32Field (TypeError)

Mysqlx::Error クラスは Protobuf::Message の継承クラスですが、Protobuf::Message は上記のように型チェックのある構造体のように使えます。

プロトコルのためのデータ構造なので、厳密に型をチェックしているのですね。

とりあえずここまで。

Ruby の文字列データの複製について

Ruby

Ruby で String オブジェクトを複製しても、文字列データは複製されません。

data = "a"*10*1024*1024
system "grep ^VmSize /proc/#$$/status"
t1 = Time.now
a = []
100.times do |i|
  a.push data.dup
end
t2 = Time.now
system "grep ^VmSize /proc/#$$/status"
printf "%.6f\n", t2-t1

実際に10MBの文字列を作って、100回dupする前後でプロセスのメモリサイズを比較してみても変わってません。

% ruby hoge.rb
VmSize:   56140 kB
VmSize:   56140 kB
0.000164

複製後に文字列を変更すると、そこで文字列データも複製されます。

data = "a"*10*1024*1024
system "grep ^VmSize /proc/#$$/status"
t1 = Time.now
a = []
100.times do |i|
  s = data.dup
  s[0] = 'a'
  a.push s
end
t2 = Time.now
system "grep ^VmSize /proc/#$$/status"
printf "%.6f\n", t2-t1

プロセスサイズが増えてるのが確認できます。10MBオブジェクトが100個なので1GBほど増えてます。

VmSize:   56140 kB
VmSize: 1080540 kB
0.337337

まあ、中身を変更したら複製されるのは当然なのですが、実は部分文字列を取り出すだけでも複製されてしまいます。

10MBの文字列のうち、先頭1MBを100回取り出します。

data = "a"*10*1024*1024
system "grep ^VmSize /proc/#$$/status"
t1 = Time.now
a = []
100.times do |i|
  a.push data[0, 1024*1024]
end
t2 = Time.now
system "grep ^VmSize /proc/#$$/status"
printf "%.6f\n", t2-t1

100MBほどサイズが増えてしまいました。

VmSize:   56104 kB
VmSize:  158904 kB
0.044682

なんでこんなことが起きるかというと、Ruby の String オブジェクトが内部で保持してる文字列データは NUL(\0) 終端されているからです。部分文字列の次のバイトを NUL にすると元の文字列が変わってしまうので、複製する必要があるのでした。

ちなみに、文字列末尾の取り出しでは複製されません。文字列末尾は NUL が次にあるからです。

data = "a"*10*1024*1024
system "grep ^VmSize /proc/#$$/status"
t1 = Time.now
a = []
100.times do |i|
  a.push data[-1024*1024, 1024*1024]
end
t2 = Time.now
system "grep ^VmSize /proc/#$$/status"
printf "%.6f\n", t2-t1
VmSize:   56136 kB
VmSize:   56136 kB
0.000061

イマイチだなーとツイートしたら、教えてもらえました。

SHARABLE_MIDDLE_SUBSTRING は Ruby 2.2 で導入されたようです。

ということで、SHARABLE_MIDDLE_SUBSTRING=1 を設定してコンパイルしてみた Ruby で試してみます。

% cflags=-DSHARABLE_MIDDLE_SUBSTRING=0 ./configure
% make install
VmSize:   56232 kB
VmSize:   56232 kB
0.000072

おおー、メモリサイズは増えないし時間も掛かってないです。すばらしい。

もうこれデフォルトでいいのでは? と思ったらまた教えてもらいました。

Rubyの拡張ライブラリ中では RSTRING_PTR() とか StringValuePtr() で String オブジェクトから文字列データの先頭ポインタを取り出すことができるのですが、それが NUL 終端されていると仮定している拡張ライブラリがあるかもしれなくて、それが動かなくなってしまうからってことですね。確かにありそうです。

ということで、行儀のいい拡張ライブラリだけ使ってることが確実なのであれば、SHARABLE_MIDDLE_SUBSTRING=1 を使うと、もしかするとメモリサイズが小さくなって速くなる…ことがあるかもしれません。

追記

Ruby 2.3.1 で SHARABLE_MIDDLE_SUBSTRING=1 でコンパイルした Ruby で gem install が動きませんでした。 調べてみたら、ホスト名からIPアドレスを求める部分に問題があるようで、

TCPSocket.new("rubygems.global.ssl.fastly.net", 80)

は動くんだけど、

TCPSocket.new("rubygems.global.ssl.fastly.netX".chop, 80)

は動きませんでした。(getaddrinfo: Name or service not known)

該当部分のソースはこんな感じです。

[raddrinfo.c]

        name = RSTRING_PTR(host);
        if (!name || *name == 0 || (name[0] == '<' && strcmp(name, "<any>") == 0)) {
            make_inetaddr(INADDR_ANY, hbuf, hbuflen);
            if (flags_ptr) *flags_ptr |= AI_NUMERICHOST;
        }
        else if (name[0] == '<' && strcmp(name, "<broadcast>") == 0) {
            make_inetaddr(INADDR_BROADCAST, hbuf, hbuflen);
            if (flags_ptr) *flags_ptr |= AI_NUMERICHOST;
        }
        else if (strlen(name) >= hbuflen) {
            rb_raise(rb_eArgError, "hostname too long (%"PRIuSIZE")",
                strlen(name));
        }
        else {
            strcpy(hbuf, name);
        }

RSTRING_PTR() で得られたポインタに対して strcmp(), strlen(), strcpy() とか NUL終端文字列を期待している関数を使っちゃってます。

まさか Ruby 本体に罠があるとは思いませんでした。今のところ人柱覚悟で使った方が良いかもしれません。

最近のCrystal

Crystal

昨年末に「Ruby脳にはCrystalつらい Advent Calendar 2015」というのを書きました。

qiita.com

当時の Crystal のバージョンは 0.10 だったのですが、その後バージョンがあがって改善されてたりするので、紹介します。

カレントディレクトリに .crystal を作らなくなった

tmtms.hatenablog.com

Crystal 0.16 で、CRYSTAL_CACHE_DIR 環境変数を設定しなくても、デフォルトで $HOME の下にキャッシュディレクトリを作るようになりました。

$HOME/.cache/crystal または $HOME/.crystal が使用されます。

否定条件でも型の絞り込みができるようになった

tmtms.hatenablog.com

もともと次のように書けば nil かそれ以外かで条件分岐できたのですが、

def hoge(str_or_nil)
  if str_or_nil
    p str_or_nil.size
  else
    puts "nil"
  end
end
hoge("abc") #=> 3
hoge(nil)   #=> nil

条件を反転させると、コンパイル時に型チェックでエラーになってしまってました。

def hoge(str_or_nil)
  if !str_or_nil
    puts "nil"
  else
    p str_or_nil.size  #=> undefined method 'size' for Nil
  end
end
hoge("abc")
hoge(nil)

Crystal 0.15 で後者の例もエラーにならなくなりました。地味に嬉しいです。

デフォルト値を持たない引数も名前付き引数として扱える

tmtms.hatenablog.com

Crystal は Ruby と異なり、メソッド定義時に名前付き引数を受けつけるための専用の構文はなく、デフォルト値つきの引数は名前付き引数として扱うことができていました。

def hoge(a, b=1, c=2)
  p [a, b, c]
end

hoge(123)                  #=> [123, 1, 2]
hoge(123, b: 456)          #=> [123, 456, 2]
hoge(123, c: 789)          #=> [123, 1, 789]
hoge(123, b: 456, c: 789)  #=> [123, 456, 789]
hoge(123, 456)             #=> [123, 456, 2]
hoge(123, 456, c: 789)     #=> [123, 456, 789]
hoge(123, 456, 789)        #=> [123, 456, 789]
hoge(a: 123)               #=> wrong number of arguments for 'hoge' (given 0, expected 1..3)

最後の行はデフォルト値を持たない引数を名前付き引数として使おうとしてコンパイル時エラーになります。

Crystal 0.16 から、デフォルト値がない引数でも名前付き引数として扱うことができるようになりました。

def hoge(a, b=1, c=2)
  p [a, b, c]
end

hoge(123)                  #=> [123, 1, 2]
hoge(123, b: 456)          #=> [123, 456, 2]
hoge(123, c: 789)          #=> [123, 1, 789]
hoge(123, b: 456, c: 789)  #=> [123, 456, 789]
hoge(123, 456)             #=> [123, 456, 2]
hoge(123, 456, c: 789)     #=> [123, 456, 789]
hoge(123, 456, 789)        #=> [123, 456, 789]
hoge(a: 123)               #=> [123, 1, 2]

Ruby の Hash 構文がタプルになってつらい!

Crystal 0.17 から、タプルリテラルが導入されました。Ruby の新し目の Hash リテラルと同じ表記です。

Crystal で Hash をリテラルで書こうとすると、シンボルをキーとする場合でも Ruby の古い表記で記述する必要があります。

hoge = {a: 123, b: "abc"}        #=> タプル
hoge = {:a => 123, :b => "abc"}  #=> Hash

ちょっとRuby脳にはつらいです。

ということで「Ruby脳にはCrystalつらい Advent Calendar 2015」26日目の記事でした!

MySQL Shell / X DevAPI / X Protocol

MySQL

MySQL 5.7.12 で突如登場した MySQL Shell とか X DevAPI とか X Protocol とかが面白そうだったので調べてみました。

Document Store とかも同じ文脈で語られてて、それぞれの用語が何を表してるのかややこしかったので、まずその辺から。

X Protocol

mysqlx プラグインを使用することで追加されるサーバー/クライアントプロトコル。ポート番号は 33060。

詳細→ https://dev.mysql.com/doc/internals/en/x-protocol.html

X DevAPI

各プログラミング言語用の新しいAPI。Document Store用のAPIも含む。今のところ、MySQL Shell JavaScript, MySQL Shell Python, Java, .Net, Node.js 用の API がある。X Protocol を使用。

詳細→ https://dev.mysql.com/doc/x-devapi-userguide/en/

MySQL Shell

X DevAPI が組み込まれた JavaScript / Python の対話型コマンドラインツール。

詳細→ https://dev.mysql.com/doc/refman/5.7/en/mysql-shell.html

Document Store

MySQLをドキュメントデータベースとして使う方法。 内部的には、JSON型の doc という名前の1カラムだけを持つテーブル(実際には _id という自動生成カラムもある)を作って、すべてのデータをJSONで突っ込むことでドキュメントデータベースとして使用している。

詳細→ https://dev.mysql.com/doc/refman/5.7/en/document-store.html

ということで MySQL Shell を使ってみます。

MySQL 5.7.12 のインストール

自分は Xubuntu を使っていて apt-get で MySQL をインストールすると 5.7.12 が入るので、お手軽だと思ったんですけど、これは mysqlx プラグインを含んでいないため使えませんでした。ここ http://dev.mysql.com/downloads/mysql/ から適当にインストールします。

個人的に MySQL の rpm や deb パッケージは信用してないので(my.cnf を /usr に置いたりする)、tarball からインストールします。

# cd /usr/local
# tar xf /tmp/mysql-5.7.12-linux-glibc2.5-x86_64.tar.gz
# ln -s mysql-5.7.12-linux-glibc2.5-x86_64 mysql
# cd mysql
# ./bin/mysqld --no-defaults --initialize
...
2016-05-10T02:24:00.559906Z 1 [Note] A temporary password is generated for root@localhost: krJ&LWZFv3:Q
# chown -R mysql:mysql .
# ./bin/mysqld --no-defaults --user=mysql --log-error=/tmp/mysql.err --daemonize >> /tmp/mysql.err 2>&1

mysqld --initialize の最後の行にパスワードが表示されるのでメモっときます。 いちいち --no-defaults をつけてるのは、Ubuntu の MySQL の設定を読まないようにするためです。 本当はちゃんと my.cnf を書けばいいんですけど、お試しなのでこのままで。

% mysql -uroot -p
Enter password: krJ&LWZFv3:Q
mysql>

この状態で何かクエリを実行しようとすると、

ERROR 1820 (HY000): You must reset your password using ALTER USER statement before executing this statement.

って怒られるので、ぶつぶつ言いながらパスワードを設定します。

mysql> set password = 'abcdefg';

テスト用のデータベースを作成し、mysqlx プラグインを有効にしておきます。

mysql> create database test;
mysql> install plugin mysqlx soname 'mysqlx.so';

MySQL Shell のインストール

MySQL Shell は http://dev.mysql.com/downloads/shell/ から持ってきます。

残念ながらバイナリ tarball は無いみたいなので、deb をダウンロードして入れました。

# dpkg -i mysql-shell_1.0.3-1ubuntu15.10_amd64.deb

MySQL Shell を使う

コマンド名は mysqlsh です。

% mysqlsh --sql --js --user=root test
Creating an X Session to root@localhost:33060
Enter password:
...
mysql-js>

URI形式でも接続できます。

% mysqlsh --sql --js --uri root@localhost/test

コマンドラインでパスワードを指定することもできます。

% mysqlsh --sql --js --user=root --password=abcdefg test
% mysqlsh --sql --js --uri root:abcdefg@localhost/test

--sql」がないと、後で出てくる「session.sql()」が何故か使えないので指定しています。

起動後は対話型 JavaScript として動きます。何故か日本語は入力できませんでした。

mysql-js> function fib(n) { if (n < 2) return n; else return fib(n-1) + fib(n-2); }
mysql-js> fib(10)
55

複数行でも記述できます。「...」に対して何も入力せずに改行すると複数行入力の終了とみなされます。

mysql-js> function fib(n) {
      ... if (n < 2)
      ...   return n;
      ... else
      ...   return fib(n-1) + fib(n-2);
      ... }
      ...
mysql-js> fib(10)
55

session, db という変数が接続とデータベースを表しているようです。

mysql-js> session
<NodeSession:root@localhost:33060/test>
mysql-js> db
<Schema:test>

普通にSQLとして使ってみます。

mysql-js> session.sql("CREATE TABLE hoge (id INT, str VARCHAR(32))")

mysql-js> db.tables
{
    "hoge": <Table:hoge>
}
mysql-js> db.hoge.insert(['id', 'str']).values(123, 'abc')
Query OK, 1 item affected (0.06 sec)

mysql-js> db.hoge.insert(['id', 'str']).values(456, 'def').values(789, 'ghi')
Query OK, 2 items affected (0.05 sec)

mysql-js> db.hoge.select(['id', 'str'])
+-----+-----+
| id  | str |
+-----+-----+
| 123 | abc |
| 456 | def |
| 789 | ghi |
+-----+-----+
3 rows in set (0.00 sec)

mysql-js> db.hoge.select(['id', 'str']).where('id=456')
+-----+-----+
| id  | str |
+-----+-----+
| 456 | def |
+-----+-----+
1 row in set (0.01 sec)

mysql-js> db.hoge.update().set('str', 'hoge').where('id = 456')
Query OK, 1 item affected (0.06 sec)

mysql-js> db.hoge.select(['id', 'str'])
+-----+------+
| id  | str  |
+-----+------+
| 123 | abc  |
| 456 | hoge |
| 789 | ghi  |
+-----+------+
3 rows in set (0.00 sec)

mysql-js> db.hoge.delete().where('id=456');
Query OK, 1 item affected (0.04 sec)

mysql-js> db.hoge.select(['id', 'str'])
+-----+-----+
| id  | str |
+-----+-----+
| 123 | abc |
| 789 | ghi |
+-----+-----+
2 rows in set (0.00 sec)

mysql-js> db.hoge.delete()
Query OK, 2 items affected (0.03 sec)

mysql-js> db.hoge.select(['id', 'str'])
Empty set (0.00 sec)

Document Store として使ってみます。 ドキュメントを格納するテーブルは Collection となり普通のテーブルとしては扱われません。

mysql-js> db.createCollection('fuga')
<Collection:fuga>
mysql-js> db.tables
{
    "hoge": <Table:hoge>
}
mysql-js> db.collections
{
    "fuga": <Collection:fuga>
}

実体は普通にテーブルです。_id は生成カラムなので、実質 JSON の doc カラムひとつだけのテーブルです。

mysql> show tables;
+----------------+
| Tables_in_test |
+----------------+
| fuga           |
| hoge           |
+----------------+
2 rows in set (0.00 sec)

mysql> show create table fuga\G
*************************** 1. row ***************************
       Table: fuga
Create Table: CREATE TABLE `fuga` (
  `doc` json DEFAULT NULL,
  `_id` varchar(32) GENERATED ALWAYS AS (json_unquote(json_extract(`doc`,'$._id'))) STORED NOT NULL,
  UNIQUE KEY `_id` (`_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

どうやらテーブル構造がポイントらしく、hoge テーブルに同じ型の doc と _id カラムを追加すると、Collection として扱われます。

mysql> ALTER TABLE hoge ADD doc json,
    -> add _id varchar(32) GENERATED ALWAYS AS (json_unquote(json_extract(`doc`,'$._id')))
    -> STORED NOT NULL UNIQUE;

テーブル構造はキャッシュされてるようなので、再接続してから確認します。

mysql-js> \connect root:abcdefg@localhost/test
Closing old connection...
Creating an X Session to root@localhost:33060/test
Default schema `test` accessible through db.

mysql-js> db.tables
{

}
mysql-js> db.collections
{
    "fuga": <Collection:fuga>, 
    "hoge": <Collection:hoge>
}
mysql-js> 

Collection として使ってみます。

mysql-js> db.fuga.add({abc: 123})
Query OK, 1 item affected (0.05 sec)

mysql-js> db.fuga.add([{name: 'tmtms'}, {text: 'AIUEO'}])
Query OK, 2 items affected (0.04 sec)

mysql-js> db.fuga.find()
[
    {
        "_id": "682d6ac3b616e6111432022c3a710274",
        "name": "tmtms"
    },
    {
        "_id": "a0478da8b616e6111432022c3a710274",
        "abc": 123
    },
    {
        "_id": "e82f6ac3b616e6111432022c3a710274",
        "text": "AIUEO"
    }
]
3 documents in set (0.00 sec)

mysql-js> db.fuga.find('abc=123')
[
    {
        "_id": "a0478da8b616e6111432022c3a710274",
        "abc": 123
    }
]
1 document in set (0.00 sec)

mysql-js> db.fuga.modify('abc=123').set('xyz', 789)
Query OK, 1 item affected (0.06 sec)

mysql-js> db.fuga.find('abc=123')
[
    {
        "_id": "a0478da8b616e6111432022c3a710274",
        "abc": 123,
        "xyz": 789
    }
]
1 document in set (0.00 sec)

mysql-js> db.fuga.modify('abc=123').unset('abc')
Query OK, 1 item affected (0.06 sec)

mysql-js> db.fuga.find('xyz=789')
[
    {
        "_id": "a0478da8b616e6111432022c3a710274",
        "xyz": 789
    }
]
1 document in set (0.00 sec)

mysql-js> db.fuga.remove('xyz=789')
Query OK, 1 item affected (0.06 sec)

mysql-js> db.fuga.find()
[
    {
        "_id": "682d6ac3b616e6111432022c3a710274",
        "name": "tmtms"
    },
    {
        "_id": "e82f6ac3b616e6111432022c3a710274",
        "text": "AIUEO"
    }
]
2 documents in set (0.01 sec)

おわりに

5.6 以降強化され続けている JSON 機能を使ってドキュメントデータベースのように使える API を用意し、そのためのプロトコルも作成したって感じでしょうか。

プロトコルはパイプライン処理ができるようになっていたりして、それ単体で見ても面白そうなので、そのうちちゃんと調べてみたいです。

MySQL ソケットピア証明書認証プラグイン

MySQL

全然知らなかったんですが、MySQLに「ソケットピア証明書認証プラグイン」というのがあるのを知りました。

http://dev.mysql.com/doc/refman/5.6/ja/socket-authentication-plugin.html

これを使うとOSのログインユーザーと同じ名前のMySQLユーザーであれば、パスワード無しでMySQLに接続することができます。ただしローカルホストでUNIXソケット経由での接続のみ。

試してみます。

% mysql -uroot
mysql> INSTALL PLUGIN auth_socket SONAME 'auth_socket.so';
mysql> CREATE USER tommy@localhost IDENTIFIED WITH auth_socket;
% whoami
tommy
% mysql
mysql> SELECT USER();
+-----------------+
| USER()          |
+-----------------+
| tommy@localhost |
+-----------------+

どうやら 5.5.10 からあるようです。今までずっとMySQLのユーザーとOSのユーザーは無関係だと思ってたのでちょっと驚きました。

Twitter からの文字化けメール

Twitter を名乗るメールアドレスからメールが届いたのですが、Subjectが文字化けしていました。

Subject: ã¨ã¿ãŸã¾ã•ã²ã‚さん、Twitterからの感謝のメッセージです。

いまのTwitterがあるのは、皆さんのおかげです。
10年前の今日、ひとつのツイートからすべてが始まりました。 
そのときから、Twitterは驚くべきことを皆さんと一緒に行いました。<BR> この瞬間にも何百万人の人々がつながり、それぞれの思いを自由に語り、<BR> かつては想像もできなかったインパクトを世界中に与えています。
Twitterの誕生日に、皆さんにあらためて感謝いたします。<BR> これまで支えてくださり、ありがとうございます!<BR> そして次の10年に向けてもよろしくお願いいたします。

Subject が文字化けしていることだけでなく、本文中に「<BR>」がそのまま入っていたり、本文中のリンク先が Twitter ではなく、http://cl.S7.exct.net/ というサイトへのリンクになっているので、かなり怪しい雰囲気なのですが、変なメール配信サイトを使ってるだけで多分本物だと個人的には思ってます。知りませんけど。

まあ本物かフィッシングメールかはどうでも良くて、本題は文字化けです。

「◯◯さん、Twitterからの感謝のメッセージです」という文面から、文字化け部分は名前だと推測できます。

メールの生ヘッダを見てみます。

Subject: =?UTF-8?B?w6PCgcKow6PCgcK/w6PCgcW4w6PCgcK+w6PCgeKAosOjwoHCssOj?=
 =?UTF-8?B?4oCawo3jgZXjgpPjgIFUd2l0dGVy44GL44KJ44Gu5oSf6Kyd44Gu44Oh44OD?=
 =?UTF-8?B?44K744O844K444Gn44GZ44CC?=

MIMEデコードして文字化けしている部分のバイト列を見てみます。

$ ruby -e 'p "w6PCgcKow6PCgcK/w6PCgcW4w6PCgcK+w6PCgeKAosOjwoHCssOj4oCawo3=".unpack("m")[0]'
"\xC3\xA3\xC2\x81\xC2\xA8\xC3\xA3\xC2\x81\xC2\xBF\xC3\xA3\xC2\x81\xC5\xB8\xC3\xA3\xC2\x81\xC2\xBE\xC3\xA3\xC2\x81\xE2\x80\xA2\xC3\xA3\xC2\x81\xC2\xB2\xC3\xA3\xE2\x80\x9A\xC2\x8D"

「C2 xx」や「C3 xx」が繰り返されているのは、たいてい Latin-1(ISO8859-1) を UTF-8 に変換した時のバイト列です。

元々何か別のエンコーディング文字列(おそらく UTF-8)を Latin-1 とみなして UTF-8 に変換してしまったものと思われます。

ということで、逆に変換してみます。

$ ruby -e 'p "w6PCgcKow6PCgcK/w6PCgcW4w6PCgcK+w6PCgeKAosOjwoHCssOj4oCawo3=".unpack("m")[0].encode("iso8859-1", "utf-8", undef: :replace)'
"\xE3\x81\xA8\xE3\x81\xBF\xE3\x81?\xE3\x81\xBE\xE3\x81?\xE3\x81\xB2\xE3?\x8D"

UTF-8 ぽいバイトの並びになりました。このバイト列を文字で表すと「とみ???ま???ひ???」となりました。 当初の推測通り私の名前「とみたまさひろ」が文字化けしていたようです。

「とみたまさひろ」をそのまま UTF-8 バイト列で表すと次のようになります。

$ ruby -e 'p "とみたまさひろ".b'
"\xE3\x81\xA8\xE3\x81\xBF\xE3\x81\x9F\xE3\x81\xBE\xE3\x81\x95\xE3\x81\xB2\xE3\x82\x8D"

9バイト目の\x9F, 15バイト目の\x95, 20バイト目の\x82 の文字の部分がうまく変換できなかったようですが、これの原因はわかりませんでした。

天下の Twitter さんなんだから、まともなメール配信システムを使って欲しいところです。