💎

ruby-mysql

2021/12/12に公開

これはMySQLアドベントカレンダーRubyアドベントカレンダーの12日目の記事です。

ruby-mysql は Ruby だけで書かれた MySQL 用のクライアントライブラリです。
今は Ruby から MySQL を使う場合は普通は mysql2 を使うだろうから、たぶん誰も使ってない。

誰も使ってないだろうし、6年ほど放置してたんだけど、なぜかその気になったのでまたいじり始めた。退職前の有給消化期間で暇だったからかも。

MySQL 8.0 対応

MySQL 8.0 でデフォルトの認証方式が変更になって、そのままでは接続できなくなったので対応。

認証方式はユーザーごとに異なる場合があるので、サーバーのデフォルト認証方式、クライアントのデフォルト認証方式、ユーザーの認証方式が異なっていた場合のプロトコルに対応。

とりあえず、MySQL 5.7 でデフォルトの mysql_native_password と MySQL 8.0 でデフォルトの caching_sha2_password,と、あと sha256_password に対応してみた。

あと、「MySQL 8.0のcaching_sha2_password + 非SSL接続が転ける」ので、TLS 接続にも対応。

caching_sha2_passwordsha256_password は、接続が TLS の場合は特にハッシュ化せずにそのまま送るようになってる。合理的。

認証プロトコルについては「MySQL の認証プロトコル」に書いた。

Ruby ぽい API

ruby-mysql は libmysqlclient の C API に合わせて作ったので、Ruby らしくないところがあった。
たとえば、MySQL サーバーに接続しないでオブジェクトを作るには Mysql.init とか。普通は Mysql.new ですよねぇ。

今まで Mysql.new は接続までしていたが、それはやめてオブジェクトを作るだけにした。
Mysql.connect がオブジェクト生成&接続をするのはかわらない。

接続パラメータは new でも connect でも指定可

m = Mysql.new('hostname', 'user', 'passwd', 'dbname')
m.connect

m = Mysql.new
m.connect('hostname', 'user', 'passwd', 'dbname')

m = Mysql.connect('hostname', 'user', 'passwd', 'dbname')

Mysql.connect() を使ってれば互換はあるはず。

URI でも指定できるようにした

m = Mysql.connect('mysql://user:passwd@hostname/dbname')

uri = URI.parse('mysql://user:passwd@hostname/dbname')
m = Mysql.connect(uri)

Hash やキーワード引数もOK

m = Mysql.connect(host: 'hostname', username: 'user', password: 'passwd', database: 'dbname')

m = Mysql.connect({host: 'hostname', username: 'user', password: 'passwd', database: 'dbname'})

接続用のオプションも

m = Mysql.init
m.options(Mysql::OPT_LOCAL_INFILE, true)
m.connect('hostname', 'user', 'passwd', 'dbname')

みたいにしないといけなかったのを

m = Mysql.new
m.local_infile = true
m.connect('hostname', 'user', 'passwd', 'dbname')

みたいに書ける。
オプションは newconnect 時のキーワード引数や URI のクエリパラメータでも指定可能。

m = Mysql.new('hostname', 'user', 'passwd', 'dbname', local_infile: true)

m = Mysql.new(host: 'hostname', username: 'user', password: 'passwd', database: 'dbname', local_infile: true)

m = Mysql.new('mysql://user:passwd@hostname/dbname?local_infile=true')

Ruby っぽい!(たぶん)

あと、ストアドプロシジャで 0000-00-00 みたいな不正な日付値と、TIME値(日付なしの時刻値)を扱うために Mysql::Time があったんだけど廃止して Time を使うようにした。
不正な日付値は nil として返す。今どきは不正は日付を使うことはないだろうから別に問題ないだろう。
TIME値は秒換算の Numeric を使うようにした。

こんなテーブルがあった場合

mysql> select * from test.t;
+------------+---------------------+-----------+
| date       | datetime            | time      |
+------------+---------------------+-----------+
| 2021-12-12 | 2021-12-12 01:23:45 | 01:23:45  |
| 0000-00-00 | 0000-00-00 01:23:45 | -01:23:45 |
+------------+---------------------+-----------+
2 rows in set (0.00 sec)

こうなる

irb(main):002:0> m.query("select * from test.t").entries
=> 
[["2021-12-12", "2021-12-12 01:23:45", "01:23:45"],
 ["0000-00-00", "0000-00-00 01:23:45", "-01:23:45"]]
irb(main):003:0> m.prepare("select * from test.t").execute.entries
=> 
[[2021-12-12 00:00:00 +0900, 2021-12-12 01:23:45 +0900, 5025.0],
 [nil, nil, -5025.0]]

古い Ruby は非対応

Ruby 2.5 未満は非対応。さすがにもういいだろうってことで。
Encoding がない Ruby 1.8 用のコードとかが残ってたので削除したり。

MySQL で非推奨の API は削除

list_dbs, list_tables, list_fields, list_processes など。
これらの情報は SHOW DATABASES, SHOW TABLES, SHOW COLUMNS, SHOW PROCESSLIST で取得すればいい。

あと、真偽値を返すメソッドは ? ありと無しの2つあったけど、無しなのは削除。

GitHub Actions でテスト

GitHub Actions で自動化っぽいことができそうだったので、テストを実行するようにしてみた。

git push するたびに Ruby 2.5, 2.6, 2.7, 3.0 と MySQL 5.5, 5.6, 5.7, 8.0 の組み合わせでテストする。
(全然関係ないけど、Ruby と MySQL のバージョンの飛び方が似てるな)

mysql2

mysql2 は MySQL の C API である libmysqlclient を使ってるんだけど、この ruby-mysql を使うようにすれば面白いかもなーと思って作業中。

目標は mysql2 のテストコードをパスすること。いざやってみると ruby-mysql のバグがいくつか見つかったりして、mysql2 のテストコード便利。

ほんとはこのアドベントカレンダーを書くまでに作りたかったんだけど、終わらなかった。残念…。

Discussion