初めに
MySQLなどのプログラムを使っていると、マニュアルなどのドキュメントに記述されている挙動と異なる挙動に遭遇して「マニュアルの読み方や使い方を誤ったのか?」や「なにか挙動が変だぞ?」と思うことは多々あります。
弊社がサポートさせていただいているお客様からの問い合わせの中には、MySQLのバグに起因するサポート依頼もあります。
お客様からの問い合わせ内容
お客様からの問い合わせの概要は、以下のような内容でした。
データベース名が34文字以上の場合、database関数の戻り値がUNIONなどをすると34文字に切り詰められる場合があるので、原因を調べてほしい
というわけで、MySQLのデータベース名の最大文字列長は64文字なので、database関数の戻り値が34文字に切り詰められるのは奇妙な挙動です。
手元でも再現を行うために、以下のようなクエリを実行してみます。
1 2 3 4 5 6 7 8 |
/* 64文字のデータベース名を作成 */ CREATE DATABASE example_db123456789012345678901234567890123456789012345678901234; USE example_db123456789012345678901234567890123456789012345678901234; SELECT database() AS DB_name, length(database()) AS DB_name_length; SELECT database() UNION SELECT database(); |
手元のMySQL serverで実行してみると、確かに34文字に切り詰められてしまって、お客様からの問い合わせ内容が再現できました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
mysql> CREATE DATABASE example_db123456789012345678901234567890123456789012345678901234; Query OK, 1 row affected (0.01 sec) mysql> USE example_db123456789012345678901234567890123456789012345678901234; Database changed mysql> SELECT database() AS DB_name, length(database()) AS DB_name_length; +------------------------------------------------------------------+----------------+ | DB_name | DB_name_length | +------------------------------------------------------------------+----------------+ | example_db123456789012345678901234567890123456789012345678901234 | 64 | +------------------------------------------------------------------+----------------+ 1 row in set (0.00 sec) mysql> SELECT database() UNION SELECT database(); +------------------------------------+ | database() | +------------------------------------+ | example_db123456789012345678901234 | +------------------------------------+ 1 row in set (0.00 sec) mysql> SELECT a, length(a) FROM (SELECT database() AS a UNION SELECT database()) AS t; +------------------------------------+-----------+ | a | length(a) | +------------------------------------+-----------+ | example_db123456789012345678901234 | 34 | +------------------------------------+-----------+ 1 row in set (0.00 sec) |
ソースコード調査
問題のdatabase関数の挙動がわかったので、該当するソースコードを見てみると set_data_type_string
で最大文字列長の大きさを MAX_FIELD_NAME
に設定しているのが分かります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/* sql\item_strfunc.h より */ class Item_func_database : public Item_func_sysconst { typedef Item_func_sysconst super; public: explicit Item_func_database(const POS &pos) : Item_func_sysconst(pos) {} bool itemize(Parse_context *pc, Item **res) override; String *val_str(String *) override; bool resolve_type(THD *) override { set_data_type_string(uint32{MAX_FIELD_NAME}); set_nullable(true); return false; } const char *func_name() const override { return "database"; } const Name_string fully_qualified_func_name() const override { return NAME_STRING("database()"); } }; |
MySQL8.0.39 sql/item_strfunc.h
なので、MAX_FIELD_NAME
の値を定義しているヘッダーファイルの中身を見ると問題の34という値で定義されていることが分かりました。
1 2 |
/* sql\sql_const.h より */ constexpr const int MAX_FIELD_NAME{34}; /* Max column name length +2 */ |
というわけで、Oracleのサポートへバグ報告を行うのと並行して、Bugfixが提供されるまでの回避策を探します。
バグの回避方法の検討
上記の原因の調査と並行してバグの修正版がリリースされるまでの間、現状で対応可能なバグ回避策を検討する必要があります。
database関数の中で定義されている最大文字列長が正しくないだけで、単純な出力文字列としては正しく64文字まで出力は出来ています。
なので、文字列長の長さの定義だけ上書きできるような方法をとれば、バグを回避できる可能性が高いと考えて convert関数 でラップする方法を考えました。
1 2 3 |
SELECT CONVERT(database(), CHAR(64) CHARACTER SET utf8mb3) AS db_name UNION SELECT CONVERT(database(), CHAR(64) CHARACTER SET utf8mb3); -- 最大文字列長の64文字にconvert SELECT a, length(a) FROM (SELECT CONVERT(database(), CHAR(64) CHARACTER SET utf8mb3) AS a UNION SELECT CONVERT(database(), CHAR(64) CHARACTER SET utf8mb3)) AS t; |
実行してみると想定通りバグが回避出来て、文字列長が短い mysql データベースの場合でも問題なく動作することも確認できました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
mysql> SELECT CONVERT(database(), CHAR(64) CHARACTER SET utf8mb3) AS db_name UNION SELECT CONVERT(database(), CHAR(64) CHARACTER SET utf8mb3); +------------------------------------------------------------------+ | db_name | +------------------------------------------------------------------+ | example_db123456789012345678901234567890123456789012345678901234 | +------------------------------------------------------------------+ 1 row in set, 2 warnings (0.04 sec) mysql> SELECT a, length(a) FROM (SELECT CONVERT(database(), CHAR(64) CHARACTER SET utf8mb3) AS a UNION SELECT CONVERT(database(), CHAR(64) CHARACTER SET utf8mb3)) AS t; +------------------------------------------------------------------+-----------+ | a | length(a) | +------------------------------------------------------------------+-----------+ | example_db123456789012345678901234567890123456789012345678901234 | 64 | +------------------------------------------------------------------+-----------+ 1 row in set, 2 warnings (0.01 sec) mysql> use mysql Database changed mysql> SELECT a, length(a) FROM (SELECT CONVERT(database(), CHAR(64) CHARACTER SET utf8mb3) AS a UNION SELECT CONVERT(database(), CHAR(64) CHARACTER SET utf8mb3)) AS t; +-------+-----------+ | a | length(a) | +-------+-----------+ | mysql | 5 | +-------+-----------+ 1 row in set, 2 warnings (0.00 sec) |
お客様には、バグ修正がなされたバージョンがリリースされるまで上記の方法でバグを回避していただくことになりました。
MySQL 8.0.40 バグ修正版のリリース
MySQL 8.0.40 のリリースノートに以下のようなBugfixの記載があります
SQL Function and Operator Notes
- The output from
DATABASE()
was truncated when this function was used as part of aUNION
query. (Bug #36871927)
Release notes MySQL8.0.40
バグ修正が行われたので、UNIONをしても正しく動作するようになりました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
mysql> SELECT version(); +-----------+ | version() | +-----------+ | 8.0.40 | +-----------+ 1 row in set (0.01 sec) mysql> CREATE DATABASE example_db123456789012345678901234567890123456789012345678901234; Query OK, 1 row affected (0.02 sec) mysql> USE example_db123456789012345678901234567890123456789012345678901234; Database changed mysql> SELECT database() UNION SELECT database(); +------------------------------------------------------------------+ | database() | +------------------------------------------------------------------+ | example_db123456789012345678901234567890123456789012345678901234 | +------------------------------------------------------------------+ 1 row in set (0.00 sec) mysql> SELECT a, length(a) FROM (SELECT database() AS a UNION SELECT database()) AS t; +------------------------------------------------------------------+-----------+ | a | length(a) | +------------------------------------------------------------------+-----------+ | example_db123456789012345678901234567890123456789012345678901234 | 64 | +------------------------------------------------------------------+-----------+ 1 row in set (0.00 sec) |
バグの修正概要
MySQLのソースコードはGithubに公開されていて、誰でも見ることが可能です。
今回解消さればバグ番号で調べると以下のコミットログが残っています。
Bug#36871927: database() results truncated at 34 bytes due to union
https://github.com/mysql/mysql-server/commit/2dd5e2ededbbc40056428f7b137708e030477eb8
主な修正点は、テストケースの追加と上記のソースコード調査でも判明していた最大文字列長の値を NAME_CHAR_LEN
の64文字に変更された点です。
1 2 3 4 5 6 7 8 9 |
@@ -612,7 +612,7 @@ class Item_func_database : public Item_func_sysconst { String *val_str(String *) override; bool resolve_type(THD *) override { - set_data_type_string(uint32{MAX_FIELD_NAME}); + set_data_type_string(uint32{NAME_CHAR_LEN}); set_nullable(true); return false; } |
他にも MAX_FIELD_NAME
の値を使っていた sql/sql_table.cc
なども変更されて MAX_FIELD_NAME
の値を使っている場所がなくなりました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
@@ -10365,16 +10365,18 @@ static bool check_if_keyname_exists(const char *name, KEY *start, KEY *end) { static const char *make_unique_key_name(const char *field_name, KEY *start, KEY *end) { - char buff[MAX_FIELD_NAME], *buff_end; + // NOTE: This may not handle multi-byte characters properly + char buff[NAME_CHAR_LEN + 1]; if (!check_if_keyname_exists(field_name, start, end) && my_strcasecmp(system_charset_info, field_name, primary_key_name)) return field_name; // Use fieldname - buff_end = strmake(buff, field_name, sizeof(buff) - 4); + // Reserve space for '_', two-digit sequence number and terminating null char: + char *buff_end = strmake(buff, field_name, sizeof(buff) - 4); /* - Only 3 chars + '\0' left, so need to limit to 2 digit - This is ok as we can't have more than 100 keys anyway + 2 digits support up to 100 keys, which is more than the normal MAX_INDEXES + limit (64). */ for (uint i = 2; i < 100; i++) { *buff_end = '_'; |
まとめ
MySQLは、かなり昔からデータベースの文字列長の制限は64文字でした。
Githubで公開されている古い MySQL 3.23.22-beta でも見つかるので、かなり初期のころからずっと発見されずに生き残ってきたバグのようでした。
1 2 3 4 5 6 7 8 |
class Item_func_database :public Item_str_func { public: Item_func_database() {} String *val_str(String *); void fix_length_and_dec() { max_length= MAX_FIELD_NAME; } const char *func_name() const { return "database"; } }; |
MySQL 3.23.22-beta sql/item_strfunc.h
もしMySQLでお困りのことがございましたら、弊社にご相談をいただければ対応できることは多いと思います。お気軽にお問い合わせください。