UTF-8のテーブル(MySQL5.6)に竈門禰󠄀豆子が格納できない問題を調べてみた

`竈門禰󠄀豆子`をMySQL5.6のテーブルにinsertしようとすると正しく格納できず、`竈門禰`となってしまうケースがあるという話を聞き、調べてみました。

### 実践

まずは試しにやってみます。

```sql
mysql> show create table verification\G
*************************** 1. row ***************************
       Table: verification
Create Table: CREATE TABLE `verification` (
  `name` varchar(100) COLLATE utf8_bin DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin
1 row in set (0.01 sec)

mysql> insert into verification (name) values ('竈門禰󠄀豆子');
Query OK, 1 row affected, 2 warnings (0.01 sec)


mysql> select * from verification;
+-----------+
| name      |
+-----------+
| 竈門禰    |
+-----------+
1 row in set (0.00 sec)
```

確かに`竈門禰`になっていることがわかりました。

### 調査

文字コード周りの設定を見てみます。

```sql
mysql> SHOW VARIABLES LIKE  'char%';
+--------------------------+----------------------------+
| Variable_name            | Value                      |
+--------------------------+----------------------------+
| character_set_client     | utf8                       |
| character_set_connection | utf8                       |
| character_set_database   | utf8                       |
| character_set_filesystem | binary                     |
| character_set_results    | utf8                       |
| character_set_server     | utf8                       |
| character_set_system     | utf8                       |
| character_sets_dir       | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+
8 rows in set (0.01 sec)
```

テーブルの文字コード設定はutf8です。公式リファレンスにも記載がありますが、MySQLのutf8文字セットは、3byteまでの文字を扱うことができます。

```sql
mysql> select * from CHARACTER_SETS where CHARACTER_SET_NAME like 'utf8%';
+--------------------+----------------------+---------------+--------+
| CHARACTER_SET_NAME | DEFAULT_COLLATE_NAME | DESCRIPTION   | MAXLEN |
+--------------------+----------------------+---------------+--------+
| utf8               | utf8_general_ci      | UTF-8 Unicode |      3 |
| utf8mb4            | utf8mb4_general_ci   | UTF-8 Unicode |      4 |
+--------------------+----------------------+---------------+--------+
2 rows in set (0.00 sec)
```

というわけで、文字が途中で途切れて格納されるのは文字コードの設定がutf8であることが一因のようです。

ただ、素直に考えるなら`竈門禰`で途切れているので`豆`がutf8で扱えない4byte文字ということになります。こんなメジャーな漢字が4byte文字とは到底思えません。確認してみると、

```python
>>> len('豆'.encode('utf-8'))
3
```

やっぱり3byteでした。そうなると疑わしいのは`禰󠄀`です。

よくよく見てみると、insertしようとした`竈門禰󠄀豆子`に対し、実際に登録されたのは`竈門禰`となっていて、偏が`ネ`から`示`に変わっていました。何故こんなことが起きるのでしょうか？

`禰󠄀`について掘り下げて調べてみます。

```python
>>> len('禰󠄀'.encode('utf-8'))
7
```

7byte…？テーブル格納後の`禰`を見てみると…

```python
>>> len('禰'.encode('utf-8'))
3
```

3byteになっています。insertのタイミングで4byteの何らかの情報が欠落してテーブルに格納されたようです。
`禰󠄀`は一体何者なのか、Unicodeのコードポイントを調べてみます。

```python
>>> ord('禰󠄀')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: ord() expected a character, but string of length 2 found       #2文字??
>>> s = '禰󠄀'
>>> list(s)
['禰', '󠄀']
>>> for i in list(s):
...     hex(ord(i))
... 
'0x79b0'
'0xe0100'
```

16進表現で`0x79b0`と`0xe0100`の2文字で構成されていることがわかりました。
`0xe0100`を調べてみると、これは`異体字セレクタ`というもののようです。Wikipediaによれば、

> 異体字セレクタ (英: Variation Selector) は、Unicode および ISO/IEC 10646 (UCS) における、文字の字体をより詳細に指定するためのセレクタ (選択子) である。

とあり、`禰`を基底文字として偏が異なる文字であることを表現するためのセレクタが含まれていることがわかりました。

### まとめ

なぜ`竈門禰󠄀豆子`がinsert時に一部欠落するかというと、

* utf8のテーブルには4byte文字を格納できない
* `禰󠄀`は`禰`の異体字であり、`禰` + 4byteの異体字セレクタの組み合わせで表現される
* したがって、`禰󠄀`を格納しようとすると、4byteである異体字セレクタのみ欠落して格納されてしまう

ということでした。異体字セレクタは今まで全く存在を知らず、文字コードの世界は奥が深い、、、と思わされる結果になりました。

### 追記

この問題を回避したい場合、4byte文字を扱えるutf8mb4を使いましょう。
（参考: https://dev.mysql.com/doc/refman/5.6/ja/charset-unicode-utf8mb4.html ）

なお、MySQL8系ではutf8mb4がデフォルトのようです。
（参考：https://dev.mysql.com/doc/refman/8.0/ja/server-system-variables.html#sysvar_character_set_client ）