それが僕には楽しかったんです。

僕と MySQL と時々 MariaDB

UDF と char * と時々 --binary-as-hex

はじめに

最近 UUID に頭を悩ませまくった結果、ULID なるいいものがあると知ったので気分転換がてら MySQL の UDF (今で言うところの Loadable Function) を実装している。UDF を書くこと自体が初めてだったので、引っかかったところを後の自分のために残す。

mysql-ulid-plugin

github.com
今作りかけているものがこれ。実際に動かしてみた感じでは、Timestamp の変換は恐らく上手くいっているが乱数部分が全然変わっていない雰囲気を感じるので後でどうにかする。

UDF をインストールする

ビルドした前提で話す。

  • plugin dir 以下に ulid.so を配置する
  • CREATE FUNCTION ulid RETURNS STRING SONAME 'ulid.so' で関数を適用する
  • SELECT ULID() でそれっぽい値が返っているならば成功
mysql> CREATE FUNCTION ulid RETURNS STRING SONAME 'ulid.so';
mysql> SELECT ULID();
+----------------------------+
| ulid()                     |
+----------------------------+
| 0001JANH8A6Q86QD0000000000 |
+----------------------------+
1 row in set (0.00 sec)

やっぱりどう見ても randomness なところが固定値っぽくなっている。まじで怪しい。

ハマりどころ

初期化関数の戻り値問題

_init 関数に UDF の初期化処理を記述していくことになるが、ここでややハマった。
特に今回は UDF 全体で使いたい情報が今のところはないので、実装は空になっているがこんな感じに書いている。

extern "C" bool ulid_init(UDF_INIT *, UDF_ARGS *, char *) {
    return false;
}

問題はこの return false; である。初期化処理に成功したら true を返せば良いと脳死で考えていたらこんな自体にぶち当たった。

mysql> CREATE FUNCTION ulid RETURNS STRING SONAME 'ulid.so';
Query OK, 0 rows affected (0.01 sec)

mysql> SELECT ULID();
ERROR 1123 (HY000): Can't initialize function 'ulid'; 

初期化できないとな…。そう、ここは初期化に成功したら false を返すべきだったのである。まさに予想外。
ちゃんと sql/udf_example.cc とかを見ておけば良かったかもしれない。

char * で返した値が何故か hex で表示される

mysql> CREATE FUNCTION ulid RETURNS STRING SONAME 'ulid.so';
Query OK, 0 rows affected (0.01 sec)

mysql> select ulid();
+--------------------------------------------------------+
| ulid()                                                 |
+--------------------------------------------------------+
| 0x303030314A414E48375850344D57574652303030303030303030 |
+--------------------------------------------------------+
1 row in set (0.01 sec)

次にぶつかったのがこの問題である。 char *result に値を突っ込んで返しているのだから、Base32 の文字列が表示されるはずだったが、何故か 16 進数表記になっている。
この件は bugs.mysql.com に答えがあった。

I confirm that invoking the client with "--binary-as-hex=0" fixes things. I had missed this (for me) crucial change! I should have read

MySQL Bugs: #99480: char* UDF returning hexadecimal string rather than string

mysql-client を使う時に binary-as-hex=0 を入れろよ、とな。
実際にそれでやると上手くいった。

$ ./bin/mysql -uroot -proot --binary-as-hex=0
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 9
Server version: 8.0.33-debug Source distribution

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> select ulid();
+----------------------------+
| ulid()                     |
+----------------------------+
| 0001JANH8A6Q86QD0000000000 |
+----------------------------+
1 row in set (0.00 sec)

ただ、やはり char * が 16 進数表示されるということには初見で元気に躓きそうな気がする。

おわりに

ここだけ抑えておけば文字列を返す UDF はいつでもどうにかできそうな気がしている。
UDF は一段落しそうだが、ULID を構築するための乱数が難しい。乱数ってなんでこんなに難しいんだ。

読むと幸せになれる

公式のドキュメントがある程度充実していて助かった。