Bug #74816 Work around for charset conversion with FKs can result in wrong data
Submitted: 12 Nov 2014 12:28 Modified: 13 Nov 2014 5:55
Reporter: Daniël van Eeden (OCA) Email Updates:
Status: Verified Impact on me:
None 
Category:MySQL Server: DDL Severity:S3 (Non-critical)
Version:5.6.21, 5.6.23, 5.7.6 OS:Any
Assigned to: CPU Architecture:Any
Tags: character set, foreign keys, utf8

[12 Nov 2014 12:28] Daniël van Eeden
Description:
The workaround for Bug #45290 (charset conversion is not possible if there are foreign keys) is to temporary disable foreign key checks. If this is not done correctly it can result in data with a wrong charset being insert in a table.

How to repeat:
mysql [test] > create table t1 (id int auto_increment primary key, name varchar(100) unique);                                                                   
Query OK, 0 rows affected (0.01 sec)

mysql [test] > create table t2 (id int auto_increment primary key, name varchar(100), fname varchar(100), foreign key (fname) references t1 (name) on update cascade on delete cascade);
Query OK, 0 rows affected (0.01 sec)

mysql [test] > set foreign_key_checks=off;
Query OK, 0 rows affected (0.00 sec)

mysql [test] > alter table t1 convert to character set utf8;
Query OK, 0 rows affected (0.02 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql [test] > set foreign_key_checks=on;
Query OK, 0 rows affected (0.00 sec)

mysql [test] > insert into t1(name) values('test1');
Query OK, 1 row affected (0.00 sec)

mysql [test] > insert into t2(name,fname) values('test1','test1');
Query OK, 1 row affected (0.00 sec)

mysql [test] > update t1 set name='wrocław' where name='test1';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql [test] > select * from t1;
+----+----------+
| id | name     |
+----+----------+
|  1 | wrocław  |
+----+----------+
1 row in set (0.00 sec)

mysql [test] > select * from t2;
+----+-------+-------------+
| id | name  | fname       |
+----+-------+-------------+
|  1 | test1 | wrocław    |
+----+-------+-------------+
1 row in set (0.00 sec)

mysql [test] > delete from t2;
Query OK, 1 row affected (0.00 sec)

mysql [test] > delete from t1;
Query OK, 1 row affected (0.00 sec)

mysql [test] > insert into t1(name) values('test1');
Query OK, 1 row affected (0.00 sec)

mysql [test] > insert into t2(name,fname) values('test1','test1');
Query OK, 1 row affected (0.00 sec)

mysql [test] > set sql_mode='STRICT_ALL_TABLES';
Query OK, 0 rows affected (0.00 sec)

mysql [test] > set innodb_strict_mode=ON;
Query OK, 0 rows affected (0.00 sec)

mysql [test] > update t1 set name='wrocław' where name='test1';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql [test] > select * from t1;
+----+----------+
| id | name     |
+----+----------+
|  2 | wrocław  |
+----+----------+
1 row in set (0.00 sec)

mysql [test] > select * from t2;
+----+-------+-------------+
| id | name  | fname       |
+----+-------+-------------+
|  2 | test1 | wrocław    |
+----+-------+-------------+
1 row in set (0.01 sec)

Suggested fix:
- Make sure the data is inserted with the correct character encoding (especially with strict mode enabled)
- Fully document the workaround
[12 Nov 2014 12:32] Daniël van Eeden
Also affects joins.

mysql [test] > select * from t2 inner join t1 on t1.name=t2.fname;
Empty set (0.00 sec)

mysql [test] > explain extended select * from t2 inner join t1 on t1.name=t2.fname\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t2
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: NULL
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
         type: ref
possible_keys: name
          key: name
      key_len: 303
          ref: func
         rows: 1
     filtered: 100.00
        Extra: Using where; Using index
2 rows in set, 1 warning (0.00 sec)

mysql [test] > show warnings\G
*************************** 1. row ***************************
  Level: Note
   Code: 1003
Message: /* select#1 */ select `test`.`t2`.`id` AS `id`,`test`.`t2`.`name` AS `name`,`test`.`t2`.`fname` AS `fname`,`test`.`t1`.`id` AS `id`,`test`.`t1`.`name` AS `name` from `test`.`t2` join `test`.`t1` where (`test`.`t1`.`name` = convert(`test`.`t2`.`fname` using utf8))
1 row in set (0.00 sec)

mysql [test] > select * from t2 inner join t1 on t1.name=CONVERT(t2.fname USING binary);
+----+-------+-------------+----+----------+
| id | name  | fname       | id | name     |
+----+-------+-------------+----+----------+
|  2 | test1 | wrocÅ‚aw    |  2 | wrocław  |
+----+-------+-------------+----+----------+
1 row in set (0.00 sec)
[13 Nov 2014 5:55] MySQL Verification Team
Hello Daniël,

Thank you for the bug report and test case.

Thanks,
Umesh
[13 Nov 2014 5:56] MySQL Verification Team
// 5.6.23

mysql> show variables like '%version%';
+-------------------------+---------------------------------------------------------+
| Variable_name           | Value                                                   |
+-------------------------+---------------------------------------------------------+
| innodb_version          | 5.6.23                                                  |
| protocol_version        | 10                                                      |
| slave_type_conversions  |                                                         |
| version                 | 5.6.23-enterprise-commercial-advanced                   |
| version_comment         | MySQL Enterprise Server - Advanced Edition (Commercial) |
| version_compile_machine | x86_64                                                  |
| version_compile_os      | Linux                                                   |
+-------------------------+---------------------------------------------------------+
7 rows in set (0.00 sec)

mysql> create table t1 (id int auto_increment primary key, name varchar(100) unique);
Query OK, 0 rows affected (0.03 sec)

mysql> create table t2 (id int auto_increment primary key, name varchar(100), fname varchar(100), foreign key (fname) references t1 (name) on update cascade on delete cascade);
Query OK, 0 rows affected (0.04 sec)

mysql> set foreign_key_checks=off;
Query OK, 0 rows affected (0.00 sec)

mysql>  alter table t1 convert to character set utf8;
Query OK, 0 rows affected (0.05 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql>  set foreign_key_checks=on;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into t1(name) values('test1');
Query OK, 1 row affected (0.00 sec)

mysql> insert into t2(name,fname) values('test1','test1');
Query OK, 1 row affected (0.01 sec)

mysql> update t1 set name='wroclaw' where name='test1';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from t1;
+----+----------+
| id | name     |
+----+----------+
|  1 | wroclaw  |
+----+----------+
1 row in set (0.00 sec)

mysql> select * from t2;
+----+-------+-------------+
| id | name  | fname       |
+----+-------+-------------+
|  1 | test1 | wrocław    |
+----+-------+-------------+
1 row in set (0.00 sec)

mysql> delete from t2;
Query OK, 1 row affected (0.00 sec)

mysql> delete from t1;
Query OK, 1 row affected (0.00 sec)

mysql> insert into t1(name) values('test1');
Query OK, 1 row affected (0.00 sec)

mysql> insert into t2(name,fname) values('test1','test1');
Query OK, 1 row affected (0.00 sec)

mysql> set sql_mode='STRICT_ALL_TABLES';
Query OK, 0 rows affected (0.00 sec)

mysql> set innodb_strict_mode=ON;
Query OK, 0 rows affected (0.00 sec)

mysql> update t1 set name='wroclaw' where name='test1';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from t1;
+----+----------+
| id | name     |
+----+----------+
|  2 | wroclaw  |
+----+----------+
1 row in set (0.00 sec)

mysql> select * from t2;
+----+-------+-------------+
| id | name  | fname       |
+----+-------+-------------+
|  2 | test1 | wrocław    |
+----+-------+-------------+
1 row in set (0.00 sec)
[13 Nov 2014 5:56] MySQL Verification Team
// 5.7.6

mysql> show variables like '%version%';
+-------------------------+---------------------------------------------------------+
| Variable_name           | Value                                                   |
+-------------------------+---------------------------------------------------------+
| innodb_version          | 5.7.6                                                   |
| protocol_version        | 10                                                      |
| slave_type_conversions  |                                                         |
| version                 | 5.7.6-m16-enterprise-commercial-advanced                |
| version_comment         | MySQL Enterprise Server - Advanced Edition (Commercial) |
| version_compile_machine | x86_64                                                  |
| version_compile_os      | Linux                                                   |
+-------------------------+---------------------------------------------------------+
7 rows in set (0.00 sec)

mysql> drop table t1,t2;
Query OK, 0 rows affected (0.02 sec)

mysql>
mysql> create table t1 (id int auto_increment primary key, name varchar(100) unique);
Query OK, 0 rows affected (0.03 sec)

mysql> create table t2 (id int auto_increment primary key, name varchar(100), fname varchar(100), foreign key (fname) references t1 (name) on update cascade on delete cascade);
Query OK, 0 rows affected (0.06 sec)

mysql> set foreign_key_checks=off;
Query OK, 0 rows affected (0.00 sec)

mysql> alter table t1 convert to character set utf8;
Query OK, 0 rows affected (0.13 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> set foreign_key_checks=on;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into t1(name) values('test1');
Query OK, 1 row affected (0.01 sec)

mysql>  insert into t2(name,fname) values('test1','test1');
Query OK, 1 row affected (0.00 sec)

mysql>  update t1 set name='wroclaw' where name='test1';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql>  select * from t1;
+----+----------+
| id | name     |
+----+----------+
|  1 | wroclaw  |
+----+----------+
1 row in set (0.02 sec)

mysql>  select * from t2;
+----+-------+-------------+
| id | name  | fname       |
+----+-------+-------------+
|  1 | test1 | wrocław    |
+----+-------+-------------+
1 row in set (0.00 sec)

mysql> delete from t2;
Query OK, 1 row affected (0.00 sec)

mysql> delete from t1;
Query OK, 1 row affected (0.00 sec)

mysql> insert into t1(name) values('test1');
Query OK, 1 row affected (0.00 sec)

mysql> insert into t2(name,fname) values('test1','test1');
Query OK, 1 row affected (0.01 sec)

mysql>  set sql_mode='STRICT_ALL_TABLES';
Query OK, 0 rows affected (0.00 sec)

mysql> set innodb_strict_mode=ON;
Query OK, 0 rows affected (0.00 sec)

mysql> update t1 set name='wroclaw' where name='test1';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql>  select * from t1;
+----+----------+
| id | name     |
+----+----------+
|  2 | wroclaw  |
+----+----------+
1 row in set (0.00 sec)

mysql> select * from t2;
+----+-------+-------------+
| id | name  | fname       |
+----+-------+-------------+
|  2 | test1 | wrocław    |
+----+-------+-------------+
1 row in set (0.00 sec)
[17 Aug 2015 16:45] Daniel Price
Posted by developer:
 
The following content has been added to the ALTER TABLE documentation:
https://dev.mysql.com/doc/refman/5.6/en/alter-table.html

When foreign_key_checks is enabled, which is the default setting,
character set conversion is not permitted on tables that include a
character string column used in a foreign key constraint. The workaround
is to disable foreign_key_checks before performing the character set
conversion. You must perform the conversion on both tables involved in the
foreign key constraint before re-enabling foreign_key_checks. If you
re-enable foreign_key_checks after converting only one of the tables, an
ON DELETE CASCADE or ON UPDATE CASCADE operation could corrupt data in the
referencing table due to implicit conversion that occurs during these
operations (Bug #45290, Bug #74816).