diff --git a/include/my_base.h b/include/my_base.h index 1768be0094075..255c35229c0e9 100644 --- a/include/my_base.h +++ b/include/my_base.h @@ -58,6 +58,7 @@ */ #define HA_OPEN_FORCE_MODE (1U << 16) /* Force open mode */ #define HA_OPEN_DATA_READONLY (1U << 17) /* Use readonly for data */ +#define HA_OPEN_FOR_FK_CHECK (1U << 18) /* Suppress errors for FK metadata check */ /* Allow opening even if table is incompatible as this is for ALTER TABLE which diff --git a/mysql-test/main/backup_lock.result b/mysql-test/main/backup_lock.result index 0103a0fc92626..601530d972fdb 100644 --- a/mysql-test/main/backup_lock.result +++ b/mysql-test/main/backup_lock.result @@ -125,6 +125,7 @@ WHERE TABLE_NAME NOT LIKE 'innodb_%_stats'; LOCK_MODE LOCK_TYPE TABLE_SCHEMA TABLE_NAME MDL_BACKUP_WAIT_DDL Backup lock MDL_SHARED_WRITE Table metadata lock test t1 +MDL_SHARED_UPGRADABLE Table metadata lock test t1 MDL_INTENTION_EXCLUSIVE Schema metadata lock test backup stage end; connection default; diff --git a/mysql-test/main/backup_locks.result b/mysql-test/main/backup_locks.result index 0d4f24cfb611c..41beed3d08803 100644 --- a/mysql-test/main/backup_locks.result +++ b/mysql-test/main/backup_locks.result @@ -35,6 +35,7 @@ connection default; SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME FROM information_schema.metadata_lock_info where table_name not like "innodb_%"; LOCK_MODE LOCK_TYPE TABLE_SCHEMA TABLE_NAME MDL_SHARED_HIGH_PRIO Table metadata lock test t1 +MDL_SHARED_UPGRADABLE Table metadata lock test t1 MDL_INTENTION_EXCLUSIVE Schema metadata lock test select * from t1; ERROR 40001: Deadlock found when trying to get lock; try restarting transaction diff --git a/mysql-test/main/innodb_fk_mdl.result b/mysql-test/main/innodb_fk_mdl.result new file mode 100644 index 0000000000000..0b7b0125db5f2 --- /dev/null +++ b/mysql-test/main/innodb_fk_mdl.result @@ -0,0 +1,155 @@ +# +# MDEV-37365: Crash when ALTER TABLE on parent runs concurrently +# with INSERT on FK child +# +# +# Test 1: Verify MDL_SHARED_READ is acquired on FK parent +# during INSERT into child +# +CREATE TABLE parent(a INT PRIMARY KEY) ENGINE=InnoDB; +CREATE TABLE child(a INT PRIMARY KEY, +FOREIGN KEY(a) REFERENCES parent(a)) ENGINE=InnoDB; +INSERT INTO parent VALUES (1),(2); +connect con_insert,localhost,root,,test; +SET DEBUG_SYNC= 'after_lock_tables_takes_lock SIGNAL locked WAIT_FOR go'; +INSERT INTO child VALUES (1); +connection default; +SET DEBUG_SYNC= 'now WAIT_FOR locked'; +# Expect MDL_SHARED_READ on parent from FK prelocking +SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME +FROM information_schema.metadata_lock_info +WHERE THREAD_ID=CON_INSERT_ID +AND TABLE_SCHEMA='test' + AND TABLE_NAME IN ('parent','child') +AND LOCK_TYPE='Table metadata lock'; +LOCK_MODE LOCK_TYPE TABLE_SCHEMA TABLE_NAME +MDL_SHARED_READ Table metadata lock test parent +MDL_SHARED_WRITE Table metadata lock test child +SET DEBUG_SYNC= 'now SIGNAL go'; +connection con_insert; +connection default; +SET DEBUG_SYNC= 'RESET'; +DELETE FROM child; +# +# Test 2: Verify MDL_SHARED_READ on parent for UPDATE of FK column +# +INSERT INTO parent VALUES (3); +INSERT INTO child VALUES (1); +connection con_insert; +SET DEBUG_SYNC= 'after_lock_tables_takes_lock SIGNAL locked WAIT_FOR go'; +UPDATE child SET a=2 WHERE a=1; +connection default; +SET DEBUG_SYNC= 'now WAIT_FOR locked'; +SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME +FROM information_schema.metadata_lock_info +WHERE THREAD_ID=CON_INSERT_ID +AND TABLE_SCHEMA='test' + AND TABLE_NAME IN ('parent','child') +AND LOCK_TYPE='Table metadata lock'; +LOCK_MODE LOCK_TYPE TABLE_SCHEMA TABLE_NAME +MDL_SHARED_READ Table metadata lock test parent +MDL_SHARED_WRITE Table metadata lock test child +SET DEBUG_SYNC= 'now SIGNAL go'; +connection con_insert; +connection default; +SET DEBUG_SYNC= 'RESET'; +DROP TABLE child, parent; +# +# Test 3: Multi-level FK chain — only direct parents are prelocked +# +CREATE TABLE grandparent(a INT PRIMARY KEY) ENGINE=InnoDB; +CREATE TABLE parent(a INT PRIMARY KEY, +FOREIGN KEY(a) REFERENCES grandparent(a)) ENGINE=InnoDB; +CREATE TABLE child(a INT PRIMARY KEY, +FOREIGN KEY(a) REFERENCES parent(a)) ENGINE=InnoDB; +INSERT INTO grandparent VALUES (1),(2); +INSERT INTO parent VALUES (1),(2); +connection con_insert; +SET DEBUG_SYNC= 'after_lock_tables_takes_lock SIGNAL locked WAIT_FOR go'; +INSERT INTO child VALUES (1); +connection default; +SET DEBUG_SYNC= 'now WAIT_FOR locked'; +# Expect MDL_SHARED_READ on parent only, NOT on grandparent +SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME +FROM information_schema.metadata_lock_info +WHERE THREAD_ID=CON_INSERT_ID +AND TABLE_SCHEMA='test' + AND TABLE_NAME IN ('grandparent','parent','child') +AND LOCK_TYPE='Table metadata lock'; +LOCK_MODE LOCK_TYPE TABLE_SCHEMA TABLE_NAME +MDL_SHARED_READ Table metadata lock test parent +MDL_SHARED_WRITE Table metadata lock test child +SET DEBUG_SYNC= 'now SIGNAL go'; +connection con_insert; +connection default; +SET DEBUG_SYNC= 'RESET'; +DROP TABLE child, parent, grandparent; +# +# Test 4: Self-referencing FK — no extra lock needed +# +CREATE TABLE self_ref(a INT PRIMARY KEY, parent_a INT, +FOREIGN KEY(parent_a) REFERENCES self_ref(a)) ENGINE=InnoDB; +INSERT INTO self_ref VALUES (1, NULL); +connection con_insert; +SET DEBUG_SYNC= 'after_lock_tables_takes_lock SIGNAL locked WAIT_FOR go'; +INSERT INTO self_ref VALUES (2, 1); +connection default; +SET DEBUG_SYNC= 'now WAIT_FOR locked'; +# Self-referencing FK: only one MDL on the table (SHARED_WRITE from DML) +SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME +FROM information_schema.metadata_lock_info +WHERE THREAD_ID=CON_INSERT_ID +AND TABLE_SCHEMA='test' + AND TABLE_NAME='self_ref' + AND LOCK_TYPE='Table metadata lock'; +LOCK_MODE LOCK_TYPE TABLE_SCHEMA TABLE_NAME +MDL_SHARED_WRITE Table metadata lock test self_ref +SET DEBUG_SYNC= 'now SIGNAL go'; +connection con_insert; +connection default; +SET DEBUG_SYNC= 'RESET'; +DROP TABLE self_ref; +# +# Test 5: Concurrent ALTER TABLE parent + INSERT INTO child +# (crash reproducer from MDEV-37365) +# +CREATE TABLE parent(a INT PRIMARY KEY, b INT, c INT AS(b) VIRTUAL) +ENGINE=InnoDB; +CREATE TABLE child(a INT PRIMARY KEY REFERENCES parent(a)) ENGINE=InnoDB; +INSERT INTO parent(a,b) VALUES (1,1),(2,2),(3,3),(4,4),(5,5); +CREATE PROCEDURE dml(IN lo INT, IN hi INT) +BEGIN +DECLARE i INT DEFAULT lo; +WHILE i <= hi DO +INSERT IGNORE INTO child SET a=i; +SET i = i + 1; +END WHILE; +END$$ +connect dml1,localhost,root,,test; +connect dml2,localhost,root,,test; +connect dml3,localhost,root,,test; +connection default; +disconnect dml1; +disconnect dml2; +disconnect dml3; +DROP PROCEDURE dml; +DROP TABLE child, parent; +# +# Test 6: INSERT into child is blocked by EXCLUSIVE MDL on parent +# +CREATE TABLE parent(a INT PRIMARY KEY) ENGINE=InnoDB; +CREATE TABLE child(a INT PRIMARY KEY, +FOREIGN KEY(a) REFERENCES parent(a)) ENGINE=InnoDB; +INSERT INTO parent VALUES (1),(2); +# LOCK TABLE WRITE acquires SHARED_NO_READ_WRITE, which blocks +# MDL_SHARED_READ. The INSERT should wait. +LOCK TABLE parent WRITE; +connection con_insert; +INSERT INTO child VALUES (1); +connection default; +# INSERT is waiting for MDL on parent as expected +UNLOCK TABLES; +connection con_insert; +connection default; +DROP TABLE child, parent; +disconnect con_insert; diff --git a/mysql-test/main/innodb_fk_mdl.test b/mysql-test/main/innodb_fk_mdl.test new file mode 100644 index 0000000000000..418883e8237a2 --- /dev/null +++ b/mysql-test/main/innodb_fk_mdl.test @@ -0,0 +1,250 @@ +--source include/have_innodb.inc +--source include/have_metadata_lock_info.inc +--source include/have_debug_sync.inc +--source include/count_sessions.inc +--disable_service_connection + +--echo # +--echo # MDEV-37365: Crash when ALTER TABLE on parent runs concurrently +--echo # with INSERT on FK child +--echo # + +--echo # +--echo # Test 1: Verify MDL_SHARED_READ is acquired on FK parent +--echo # during INSERT into child +--echo # + +CREATE TABLE parent(a INT PRIMARY KEY) ENGINE=InnoDB; +CREATE TABLE child(a INT PRIMARY KEY, + FOREIGN KEY(a) REFERENCES parent(a)) ENGINE=InnoDB; +INSERT INTO parent VALUES (1),(2); + +--connect(con_insert,localhost,root,,test) +--let $con_insert_id= `SELECT CONNECTION_ID()` +SET DEBUG_SYNC= 'after_lock_tables_takes_lock SIGNAL locked WAIT_FOR go'; +--send INSERT INTO child VALUES (1) + +--connection default +SET DEBUG_SYNC= 'now WAIT_FOR locked'; + +--echo # Expect MDL_SHARED_READ on parent from FK prelocking +--sorted_result +--replace_result $con_insert_id CON_INSERT_ID +eval SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME + FROM information_schema.metadata_lock_info + WHERE THREAD_ID=$con_insert_id + AND TABLE_SCHEMA='test' + AND TABLE_NAME IN ('parent','child') + AND LOCK_TYPE='Table metadata lock'; + +SET DEBUG_SYNC= 'now SIGNAL go'; +--connection con_insert +--reap + +--connection default +SET DEBUG_SYNC= 'RESET'; + +DELETE FROM child; + +--echo # +--echo # Test 2: Verify MDL_SHARED_READ on parent for UPDATE of FK column +--echo # +INSERT INTO parent VALUES (3); +INSERT INTO child VALUES (1); + +--connection con_insert +SET DEBUG_SYNC= 'after_lock_tables_takes_lock SIGNAL locked WAIT_FOR go'; +--send UPDATE child SET a=2 WHERE a=1 + +--connection default +SET DEBUG_SYNC= 'now WAIT_FOR locked'; + +--sorted_result +--replace_result $con_insert_id CON_INSERT_ID +eval SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME + FROM information_schema.metadata_lock_info + WHERE THREAD_ID=$con_insert_id + AND TABLE_SCHEMA='test' + AND TABLE_NAME IN ('parent','child') + AND LOCK_TYPE='Table metadata lock'; + +SET DEBUG_SYNC= 'now SIGNAL go'; +--connection con_insert +--reap + +--connection default +SET DEBUG_SYNC= 'RESET'; + +DROP TABLE child, parent; + +--echo # +--echo # Test 3: Multi-level FK chain — only direct parents are prelocked +--echo # + +CREATE TABLE grandparent(a INT PRIMARY KEY) ENGINE=InnoDB; +CREATE TABLE parent(a INT PRIMARY KEY, + FOREIGN KEY(a) REFERENCES grandparent(a)) ENGINE=InnoDB; +CREATE TABLE child(a INT PRIMARY KEY, + FOREIGN KEY(a) REFERENCES parent(a)) ENGINE=InnoDB; +INSERT INTO grandparent VALUES (1),(2); +INSERT INTO parent VALUES (1),(2); + +--connection con_insert +SET DEBUG_SYNC= 'after_lock_tables_takes_lock SIGNAL locked WAIT_FOR go'; +--send INSERT INTO child VALUES (1) + +--connection default +SET DEBUG_SYNC= 'now WAIT_FOR locked'; + +--echo # Expect MDL_SHARED_READ on parent only, NOT on grandparent +--sorted_result +--replace_result $con_insert_id CON_INSERT_ID +eval SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME + FROM information_schema.metadata_lock_info + WHERE THREAD_ID=$con_insert_id + AND TABLE_SCHEMA='test' + AND TABLE_NAME IN ('grandparent','parent','child') + AND LOCK_TYPE='Table metadata lock'; + +SET DEBUG_SYNC= 'now SIGNAL go'; +--connection con_insert +--reap + +--connection default +SET DEBUG_SYNC= 'RESET'; + +DROP TABLE child, parent, grandparent; + +--echo # +--echo # Test 4: Self-referencing FK — no extra lock needed +--echo # + +CREATE TABLE self_ref(a INT PRIMARY KEY, parent_a INT, + FOREIGN KEY(parent_a) REFERENCES self_ref(a)) ENGINE=InnoDB; +INSERT INTO self_ref VALUES (1, NULL); + +--connection con_insert +SET DEBUG_SYNC= 'after_lock_tables_takes_lock SIGNAL locked WAIT_FOR go'; +--send INSERT INTO self_ref VALUES (2, 1) + +--connection default +SET DEBUG_SYNC= 'now WAIT_FOR locked'; + +--echo # Self-referencing FK: only one MDL on the table (SHARED_WRITE from DML) +--sorted_result +--replace_result $con_insert_id CON_INSERT_ID +eval SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME + FROM information_schema.metadata_lock_info + WHERE THREAD_ID=$con_insert_id + AND TABLE_SCHEMA='test' + AND TABLE_NAME='self_ref' + AND LOCK_TYPE='Table metadata lock'; + +SET DEBUG_SYNC= 'now SIGNAL go'; +--connection con_insert +--reap + +--connection default +SET DEBUG_SYNC= 'RESET'; + +DROP TABLE self_ref; + +--echo # +--echo # Test 5: Concurrent ALTER TABLE parent + INSERT INTO child +--echo # (crash reproducer from MDEV-37365) +--echo # + +CREATE TABLE parent(a INT PRIMARY KEY, b INT, c INT AS(b) VIRTUAL) + ENGINE=InnoDB; +CREATE TABLE child(a INT PRIMARY KEY REFERENCES parent(a)) ENGINE=InnoDB; +INSERT INTO parent(a,b) VALUES (1,1),(2,2),(3,3),(4,4),(5,5); + +--delimiter $$ +CREATE PROCEDURE dml(IN lo INT, IN hi INT) +BEGIN + DECLARE i INT DEFAULT lo; + WHILE i <= hi DO + INSERT IGNORE INTO child SET a=i; + SET i = i + 1; + END WHILE; +END$$ +--delimiter ; + +--connect(dml1,localhost,root,,test) +--connect(dml2,localhost,root,,test) +--connect(dml3,localhost,root,,test) + +--disable_query_log +--disable_result_log +--disable_warnings +--let $i= 0 +while ($i < 50) +{ + --connection dml1 + --send CALL dml(1, 5) + --connection dml2 + --send CALL dml(1, 5) + --connection dml3 + --send CALL dml(1, 5) + + --connection default + --error 0,ER_DUP_KEYNAME + ALTER TABLE parent ADD INDEX(c); + --error 0,ER_CANT_DROP_FIELD_OR_KEY + ALTER TABLE parent DROP INDEX c; + + --connection dml1 + --reap + --connection dml2 + --reap + --connection dml3 + --reap + --inc $i +} +--enable_warnings +--enable_result_log +--enable_query_log + +--connection default +--disconnect dml1 +--disconnect dml2 +--disconnect dml3 + +DROP PROCEDURE dml; +DROP TABLE child, parent; + +--echo # +--echo # Test 6: INSERT into child is blocked by EXCLUSIVE MDL on parent +--echo # + +CREATE TABLE parent(a INT PRIMARY KEY) ENGINE=InnoDB; +CREATE TABLE child(a INT PRIMARY KEY, + FOREIGN KEY(a) REFERENCES parent(a)) ENGINE=InnoDB; +INSERT INTO parent VALUES (1),(2); + +--echo # LOCK TABLE WRITE acquires SHARED_NO_READ_WRITE, which blocks +--echo # MDL_SHARED_READ. The INSERT should wait. +LOCK TABLE parent WRITE; + +--connection con_insert +--send INSERT INTO child VALUES (1) + +--connection default +let $wait_condition= + select count(*) > 0 from information_schema.processlist + where state LIKE 'Waiting for table metadata lock%' + AND info LIKE 'INSERT INTO child%'; +--source include/wait_condition.inc + +--echo # INSERT is waiting for MDL on parent as expected +UNLOCK TABLES; + +--connection con_insert +--reap + +--connection default +DROP TABLE child, parent; + +--disconnect con_insert + +--source include/wait_until_count_sessions.inc diff --git a/mysql-test/main/lock_multi.result b/mysql-test/main/lock_multi.result index 361e1c63bfdf7..93f3738ec5187 100644 --- a/mysql-test/main/lock_multi.result +++ b/mysql-test/main/lock_multi.result @@ -541,9 +541,7 @@ connection con2; SELECT table_name, table_comment FROM information_schema.tables WHERE table_schema= 'test' AND table_name= 't1'; table_name table_comment -t1 Lock wait timeout exceeded; try restarting transaction -Warnings: -Warning 1205 Lock wait timeout exceeded; try restarting transaction +t1 connection default; UNLOCK TABLES; connection con3; diff --git a/mysql-test/main/partition_debug_sync.result b/mysql-test/main/partition_debug_sync.result index 74698626f5ea5..f010206b92617 100644 --- a/mysql-test/main/partition_debug_sync.result +++ b/mysql-test/main/partition_debug_sync.result @@ -20,15 +20,12 @@ PARTITION p1 VALUES LESS THAN (20), PARTITION p2 VALUES LESS THAN (100), PARTITION p3 VALUES LESS THAN MAXVALUE ) */; SET DEBUG_SYNC= 'alter_table_before_create_table_no_lock SIGNAL removing_partitioning WAIT_FOR waiting_for_alter'; -SET DEBUG_SYNC= 'mdl_acquire_lock_wait SIGNAL waiting_for_upgrade'; ALTER TABLE t1 REMOVE PARTITIONING; connection default; SET DEBUG_SYNC= 'now WAIT_FOR removing_partitioning'; SET DEBUG_SYNC= 'mdl_acquire_lock_wait SIGNAL waiting_for_alter'; -SET DEBUG_SYNC= 'rm_table_no_locks_before_delete_table WAIT_FOR waiting_for_upgrade'; DROP TABLE IF EXISTS t1; connection con1; -ERROR 40001: Deadlock found when trying to get lock; try restarting transaction connection default; SET DEBUG_SYNC= 'RESET'; connection con1; diff --git a/mysql-test/main/partition_debug_sync.test b/mysql-test/main/partition_debug_sync.test index 2e10706fbef9e..04a314bd95b05 100644 --- a/mysql-test/main/partition_debug_sync.test +++ b/mysql-test/main/partition_debug_sync.test @@ -31,15 +31,12 @@ ENGINE = MYISAM PARTITION p2 VALUES LESS THAN (100), PARTITION p3 VALUES LESS THAN MAXVALUE ) */; SET DEBUG_SYNC= 'alter_table_before_create_table_no_lock SIGNAL removing_partitioning WAIT_FOR waiting_for_alter'; -SET DEBUG_SYNC= 'mdl_acquire_lock_wait SIGNAL waiting_for_upgrade'; --send ALTER TABLE t1 REMOVE PARTITIONING connection default; SET DEBUG_SYNC= 'now WAIT_FOR removing_partitioning'; SET DEBUG_SYNC= 'mdl_acquire_lock_wait SIGNAL waiting_for_alter'; -SET DEBUG_SYNC= 'rm_table_no_locks_before_delete_table WAIT_FOR waiting_for_upgrade'; DROP TABLE IF EXISTS t1; connection con1; ---error ER_LOCK_DEADLOCK --reap connection default; SET DEBUG_SYNC= 'RESET'; diff --git a/mysql-test/main/query_cache_innodb.result b/mysql-test/main/query_cache_innodb.result index e5569c53d5f30..c64c9ead8beb5 100644 --- a/mysql-test/main/query_cache_innodb.result +++ b/mysql-test/main/query_cache_innodb.result @@ -85,7 +85,7 @@ t2id id use test; drop database `#mysql50#-`; SET NAMES default; -FOUND 10 /\[ERROR\] Invalid \(old\?\) table or database name/ in mysqld.1.err +FOUND 14 /\[ERROR\] Invalid \(old\?\) table or database name/ in mysqld.1.err set global query_cache_type=DEFAULT; set global query_cache_size=@save_query_cache_size; End of 10.2 tests diff --git a/mysql-test/suite/innodb/r/foreign_key.result b/mysql-test/suite/innodb/r/foreign_key.result index 15be7190bfd5b..879b0be63e990 100644 --- a/mysql-test/suite/innodb/r/foreign_key.result +++ b/mysql-test/suite/innodb/r/foreign_key.result @@ -1076,14 +1076,14 @@ TRUNCATE TABLE parent; ERROR 42000: Cannot truncate a table referenced in a foreign key constraint (`test`.`child`, CONSTRAINT `1` FOREIGN KEY (`a`) REFERENCES `test`.`parent` (`a`)) DROP TABLE parent; ERROR 23000: Cannot delete or update a parent row: a foreign key constraint fails -SET innodb_lock_wait_timeout=0; +SET lock_wait_timeout=0, innodb_lock_wait_timeout=0; RENAME TABLE parent TO transparent; ERROR HY000: Lock wait timeout exceeded; try restarting transaction ALTER TABLE parent FORCE, ALGORITHM=COPY; ERROR HY000: Lock wait timeout exceeded; try restarting transaction ALTER TABLE parent FORCE, ALGORITHM=INPLACE; ERROR HY000: Lock wait timeout exceeded; try restarting transaction -SET innodb_lock_wait_timeout=0, foreign_key_checks=0; +SET foreign_key_checks=0; TRUNCATE TABLE parent; ERROR HY000: Lock wait timeout exceeded; try restarting transaction DROP TABLE parent; @@ -1097,7 +1097,7 @@ ERROR HY000: Lock wait timeout exceeded; try restarting transaction connection con1; COMMIT; connection default; -SET innodb_lock_wait_timeout=DEFAULT; +SET lock_wait_timeout=DEFAULT, innodb_lock_wait_timeout=DEFAULT; TRUNCATE TABLE parent; ALTER TABLE parent FORCE, ALGORITHM=COPY; ALTER TABLE parent FORCE, ALGORITHM=INPLACE; diff --git a/mysql-test/suite/innodb/r/monitor.result b/mysql-test/suite/innodb/r/monitor.result index d97f741efdd75..d44140e43c044 100644 --- a/mysql-test/suite/innodb/r/monitor.result +++ b/mysql-test/suite/innodb/r/monitor.result @@ -285,7 +285,7 @@ if(enabled,'enabled','disabled') status from information_schema.innodb_metrics where name = "metadata_table_handles_opened"; name max_count min_count count max_count_reset min_count_reset count_reset status -metadata_table_handles_opened 2 NULL 2 1 NULL 1 enabled +metadata_table_handles_opened 3 NULL 3 2 NULL 2 enabled set global innodb_monitor_reset_all = metadata_table_handles_opened; select name, max_count, min_count, count, max_count_reset, min_count_reset, count_reset, @@ -293,7 +293,7 @@ if(enabled,'enabled','disabled') status from information_schema.innodb_metrics where name = "metadata_table_handles_opened"; name max_count min_count count max_count_reset min_count_reset count_reset status -metadata_table_handles_opened 2 NULL 2 1 NULL 1 enabled +metadata_table_handles_opened 3 NULL 3 2 NULL 2 enabled set global innodb_monitor_disable = metadata_table_handles_opened; set global innodb_monitor_reset = metadata_table_handles_opened; select name, max_count, min_count, count, @@ -302,7 +302,7 @@ if(enabled,'enabled','disabled') status from information_schema.innodb_metrics where name = "metadata_table_handles_opened"; name max_count min_count count max_count_reset min_count_reset count_reset status -metadata_table_handles_opened 2 NULL 2 NULL NULL 0 disabled +metadata_table_handles_opened 3 NULL 3 NULL NULL 0 disabled drop table monitor_test; create table monitor_test(col int) engine = innodb; select * from monitor_test; @@ -313,7 +313,7 @@ if(enabled,'enabled','disabled') status from information_schema.innodb_metrics where name = "metadata_table_handles_opened"; name max_count min_count count max_count_reset min_count_reset count_reset status -metadata_table_handles_opened 2 NULL 2 NULL NULL 0 disabled +metadata_table_handles_opened 3 NULL 3 NULL NULL 0 disabled set global innodb_monitor_reset_all = metadata_table_handles_opened; select name, max_count, min_count, count, max_count_reset, min_count_reset, count_reset, @@ -333,7 +333,7 @@ if(enabled,'enabled','disabled') status from information_schema.innodb_metrics where name like "metadata%"; name max_count min_count count max_count_reset min_count_reset count_reset status -metadata_table_handles_opened 1 NULL 1 1 NULL 1 enabled +metadata_table_handles_opened 2 NULL 2 2 NULL 2 enabled set global innodb_monitor_disable = module_metadata; set global innodb_monitor_reset = module_metadata; select name, max_count, min_count, count, @@ -342,7 +342,7 @@ if(enabled,'enabled','disabled') status from information_schema.innodb_metrics where name like "metadata%"; name max_count min_count count max_count_reset min_count_reset count_reset status -metadata_table_handles_opened 1 NULL 1 NULL NULL 0 disabled +metadata_table_handles_opened 2 NULL 2 NULL NULL 0 disabled set global innodb_monitor_reset_all = module_metadata; select name, max_count, min_count, count, max_count_reset, min_count_reset, count_reset, diff --git a/mysql-test/suite/innodb/t/foreign_key.test b/mysql-test/suite/innodb/t/foreign_key.test index 88033e89130fa..e16c4c6585e24 100644 --- a/mysql-test/suite/innodb/t/foreign_key.test +++ b/mysql-test/suite/innodb/t/foreign_key.test @@ -1094,18 +1094,28 @@ BEGIN; --error ER_NO_REFERENCED_ROW_2 INSERT INTO child SET a=1; connection default; +# MDEV-37365: DML on a child table now acquires MDL_SHARED_READ on the FK +# parent as part of prelocking. Even though the INSERT above failed, the +# transaction in con1 is still open, so its MDL_SHARED_READ on parent is +# held until COMMIT/ROLLBACK. DDL on parent needs MDL_EXCLUSIVE, which +# conflicts with con1's MDL_SHARED_READ. TRUNCATE and DROP perform an +# early FK check under MDL_SHARED_UPGRADABLE (compatible with con1's SR) +# and return FK-specific errors. RENAME and ALTER have no early check, +# so they block at the MDL layer and return ER_LOCK_WAIT_TIMEOUT. --error ER_TRUNCATE_ILLEGAL_FK TRUNCATE TABLE parent; --error ER_ROW_IS_REFERENCED_2 DROP TABLE parent; -SET innodb_lock_wait_timeout=0; +SET lock_wait_timeout=0, innodb_lock_wait_timeout=0; --error ER_LOCK_WAIT_TIMEOUT RENAME TABLE parent TO transparent; --error ER_LOCK_WAIT_TIMEOUT ALTER TABLE parent FORCE, ALGORITHM=COPY; --error ER_LOCK_WAIT_TIMEOUT ALTER TABLE parent FORCE, ALGORITHM=INPLACE; -SET innodb_lock_wait_timeout=0, foreign_key_checks=0; +# With foreign_key_checks=0, the early FK check is skipped. All DDL must +# acquire MDL_EXCLUSIVE, which conflicts with con1's MDL_SHARED_READ. +SET foreign_key_checks=0; --error ER_LOCK_WAIT_TIMEOUT TRUNCATE TABLE parent; --error ER_LOCK_WAIT_TIMEOUT @@ -1119,8 +1129,7 @@ ALTER TABLE parent ADD COLUMN b INT, ALGORITHM=INSTANT; connection con1; COMMIT; connection default; -# Restore the timeout to avoid occasional races with purge. -SET innodb_lock_wait_timeout=DEFAULT; +SET lock_wait_timeout=DEFAULT, innodb_lock_wait_timeout=DEFAULT; TRUNCATE TABLE parent; ALTER TABLE parent FORCE, ALGORITHM=COPY; ALTER TABLE parent FORCE, ALGORITHM=INPLACE; diff --git a/mysql-test/suite/perfschema/r/mdl_func.result b/mysql-test/suite/perfschema/r/mdl_func.result index e359f48d2ead6..c83df296592b1 100644 --- a/mysql-test/suite/perfschema/r/mdl_func.result +++ b/mysql-test/suite/perfschema/r/mdl_func.result @@ -224,6 +224,13 @@ OWNER_THREAD_ID USER2 OBJECT_TYPE TABLE OBJECT_SCHEMA test OBJECT_NAME t1 +LOCK_TYPE SHARED_UPGRADABLE +LOCK_DURATION TRANSACTION +LOCK_STATUS GRANTED +OWNER_THREAD_ID USER2 +OBJECT_TYPE TABLE +OBJECT_SCHEMA test +OBJECT_NAME t1 LOCK_TYPE SHARED_WRITE LOCK_DURATION TRANSACTION LOCK_STATUS GRANTED diff --git a/sql/handler.h b/sql/handler.h index 354e5d6192d6a..1d8acdf5d5e47 100644 --- a/sql/handler.h +++ b/sql/handler.h @@ -4711,6 +4711,7 @@ class handler :public Sql_alloc get_parent_foreign_key_list(THD *thd, List *f_key_list) { return 0; } virtual bool referenced_by_foreign_key() const noexcept { return false;} + virtual bool references_foreign_key() const noexcept { return false; } virtual void init_table_handle_for_HANDLER() { return; } /* prepare InnoDB for HANDLER */ virtual void free_foreign_key_create_info(char* str) {} diff --git a/sql/sql_base.cc b/sql/sql_base.cc index 95abfa798bf65..7d8afb8e0866c 100644 --- a/sql/sql_base.cc +++ b/sql/sql_base.cc @@ -5106,6 +5106,85 @@ add_internal_tables(THD *thd, Query_tables_list *prelocking_ctx, DBUG_RETURN(FALSE); } +/** + Extend the table_list to include FK-referenced (parent) tables for + prelocking. + + When performing DML on a child table that has foreign keys referencing + other tables, InnoDB's FK constraint check acquires InnoDB-internal locks + on the parent table. Without corresponding MDL on the parent, concurrent + DDL on the parent can crash (MDEV-37365). + + This function prelocks FK parent tables with TL_READ (which maps to + MDL_SHARED_READ). Because the lock is TL_READ and the prelocking type + is PRELOCK_FK, init_one_table_for_prelocking() sets open_strategy to + OPEN_STUB, so only the MDL is acquired — the parent table is not + actually opened. + + @param[in] thd Thread context. + @param[in] prelocking_ctx Prelocking context of the statement. + @param[in] table_list Table list element for table. + @param[out] need_prelocking Set to TRUE if method detects that prelocking + required, not changed otherwise. + + @retval FALSE Success. + @retval TRUE Failure (OOM). +*/ +static bool +prepare_fk_referenced_prelocking_list(THD *thd, + Query_tables_list *prelocking_ctx, + TABLE_LIST *table_list, + bool *need_prelocking) +{ + DBUG_ENTER("prepare_fk_referenced_prelocking_list"); + List fk_list; + List_iterator fk_list_it(fk_list); + FOREIGN_KEY_INFO *fk; + Query_arena *arena, backup; + TABLE *table= table_list->table; + + /* Avoid the heavier get_foreign_key_list() (which acquires dict_sys + exclusive latch) for tables that have no FK references to parents. */ + if (!table->file->references_foreign_key()) + DBUG_RETURN(FALSE); + + arena= thd->activate_stmt_arena_if_needed(&backup); + + table->file->get_foreign_key_list(thd, &fk_list); + if (unlikely(thd->is_error())) + { + if (arena) + thd->restore_active_arena(arena, &backup); + DBUG_RETURN(TRUE); + } + + if (fk_list.is_empty()) + { + if (arena) + thd->restore_active_arena(arena, &backup); + DBUG_RETURN(FALSE); + } + + *need_prelocking= TRUE; + + while ((fk= fk_list_it++)) + { + if (table_already_fk_prelocked(prelocking_ctx->query_tables, + fk->referenced_db, fk->referenced_table, TL_READ)) + continue; + + TABLE_LIST *tl= thd->alloc(1); + tl->init_one_table_for_prelocking(fk->referenced_db, fk->referenced_table, + NULL, TL_READ, TABLE_LIST::PRELOCK_FK, table_list->belong_to_view, + 0, &prelocking_ctx->query_tables_last, table_list->for_insert_data); + } + + if (arena) + thd->restore_active_arena(arena, &backup); + DBUG_RETURN(FALSE); +} + + /** Extend the table_list to include foreign tables for prelocking. @@ -5253,6 +5332,10 @@ bool DML_prelocking_strategy::handle_table(THD *thd, need_prelocking, table_list->trg_event_map)) return TRUE; + + if (prepare_fk_referenced_prelocking_list(thd, prelocking_ctx, table_list, + need_prelocking)) + return TRUE; } else if (table_list->slave_fk_event_map) { @@ -5260,6 +5343,10 @@ bool DML_prelocking_strategy::handle_table(THD *thd, need_prelocking, table_list->slave_fk_event_map)) return TRUE; + + if (prepare_fk_referenced_prelocking_list(thd, prelocking_ctx, table_list, + need_prelocking)) + return TRUE; } /* Open any tables used by DEFAULT (like sequence tables) */ diff --git a/sql/sql_table.cc b/sql/sql_table.cc index b07f16102a6bb..fcb6dd9a9c6f6 100644 --- a/sql/sql_table.cc +++ b/sql/sql_table.cc @@ -1158,6 +1158,59 @@ int write_bin_log_with_if_exists(THD *thd, bool clear_error, */ +/** + Check if the table is a parent in a non-self-referencing foreign key + whose child is not in the DROP list. + + @param thd Thread context. + @param table Table handle (must have handler opened). + @param drop_tables Tables being dropped (to skip child-in-list FKs). + + @retval FALSE No blocking FK children. Statement can proceed. + @retval TRUE FK child exists that is not in the DROP list; error emitted. +*/ +static bool +fk_drop_illegal_if_parent(THD *thd, TABLE *table, TABLE_LIST *drop_tables) +{ + if (!table->file->referenced_by_foreign_key()) + return FALSE; + + FOREIGN_KEY_INFO *fk_info; + List fk_list; + table->file->get_parent_foreign_key_list(thd, &fk_list); + + if (unlikely(thd->is_error())) + return TRUE; + + List_iterator_fast it(fk_list); + while ((fk_info= it++)) + { + if (table->s->db.streq(*fk_info->foreign_db) && + table->s->table_name.streq(*fk_info->foreign_table)) + continue; + + bool in_drop_list= false; + for (TABLE_LIST *tl= drop_tables; tl; tl= tl->next_local) + { + if (tl->db.streq(*fk_info->foreign_db) && + tl->table_name.streq(*fk_info->foreign_table)) + { + in_drop_list= true; + break; + } + } + if (in_drop_list) + continue; + + my_printf_error(ER_ROW_IS_REFERENCED_2, + ER(ER_ROW_IS_REFERENCED), MYF(0), ""); + return TRUE; + } + + return FALSE; +} + + bool mysql_rm_table(THD *thd,TABLE_LIST *tables, bool if_exists, bool drop_temporary, bool drop_sequence, bool dont_log_query) @@ -1197,6 +1250,141 @@ bool mysql_rm_table(THD *thd,TABLE_LIST *tables, bool if_exists, } } } + /* + MDEV-37365: Acquire SU on FK-capable tables, check FK, then + upgrade SU→X with BACKUP_DDL (matching lock_table_names + protocol to avoid deadlock with FTWRL). SU blocks SNW (FK + creation) and is held through upgrade (no TOCTOU). On FK + violation, upgrade with timeout=0: succeeds if no contention + (main loop produces warnings/binlog), fails instantly under + contention (return ER_ROW_IS_REFERENCED_2). + */ + bool fk_violation= false; + if (!(thd->variables.option_bits & OPTION_NO_FOREIGN_KEY_CHECKS)) + { + MDL_savepoint mdl_svp= thd->mdl_context.mdl_savepoint(); + MDL_request backup_request; + MDL_REQUEST_INIT(&backup_request, MDL_key::BACKUP, "", "", + MDL_BACKUP_DDL, MDL_STATEMENT); + + for (;;) + { + fk_violation= false; + bool mdl_error= false; + bool has_non_temp= false; + + for (table= tables; table; table= table->next_local) + { + if (is_temporary_table(table)) + continue; + has_non_temp= true; + + MDL_request schema_request; + MDL_REQUEST_INIT(&schema_request, MDL_key::SCHEMA, + table->db.str, "", + MDL_INTENTION_EXCLUSIVE, MDL_TRANSACTION); + if (thd->mdl_context.acquire_lock( + &schema_request, thd->variables.lock_wait_timeout)) + { + mdl_error= true; + break; + } + + MDL_request su_request; + MDL_REQUEST_INIT_BY_KEY(&su_request, &table->mdl_request.key, + MDL_SHARED_UPGRADABLE, MDL_TRANSACTION); + if (thd->mdl_context.acquire_lock( + &su_request, thd->variables.lock_wait_timeout)) + { + mdl_error= true; + break; + } + table->mdl_request.ticket= su_request.ticket; + + if (fk_violation) + continue; + + Dummy_error_handler err_handler; + thd->push_internal_handler(&err_handler); + TABLE_SHARE *share= tdc_acquire_share(thd, table, GTS_TABLE); + TABLE tmp_table; + bool table_opened= share + && (share->db_type()->flags & HTON_SUPPORTS_FOREIGN_KEYS) + && !open_table_from_share( + thd, share, &empty_clex_str, HA_OPEN_KEYFILE, 0, + HA_OPEN_FOR_ALTER | HA_OPEN_FOR_FK_CHECK, + &tmp_table, false); + thd->pop_internal_handler(); + + if (table_opened) + { + if (fk_drop_illegal_if_parent(thd, &tmp_table, tables)) + fk_violation= true; + closefrm(&tmp_table); + } + if (share) + tdc_release_share(share); + } + if (mdl_error) + DBUG_RETURN(true); + + if (fk_violation) + { + thd->get_stmt_da()->clear_warning_info(thd->query_id); + thd->clear_error(); + } + + ulong timeout= fk_violation ? 0 : + thd->variables.lock_wait_timeout; + for (table= tables; table; table= table->next_local) + { + MDL_ticket *ticket= table->mdl_request.ticket; + table->mdl_request.ticket= NULL; + if (!ticket) + continue; + if (thd->mdl_context.upgrade_shared_lock( + ticket, MDL_EXCLUSIVE, timeout)) + { + if (fk_violation) + { + thd->mdl_context.rollback_to_savepoint(mdl_svp); + for (table= tables; table; table= table->next_local) + table->mdl_request.ticket= NULL; + thd->get_stmt_da()->clear_warning_info(thd->query_id); + thd->clear_error(); + my_printf_error(ER_ROW_IS_REFERENCED_2, + ER(ER_ROW_IS_REFERENCED), MYF(0), ""); + } + DBUG_RETURN(true); + } + } + + if (!has_non_temp) + break; + + /* + Acquire BACKUP_DDL to protect against FTWRL/BACKUP STAGE, + matching lock_table_names() protocol. If unavailable, + rollback all locks and retry after waiting. + */ + if (thd->mdl_context.try_acquire_lock(&backup_request)) + DBUG_RETURN(true); + if (backup_request.ticket) + { + thd->mdl_backup_ticket= backup_request.ticket; + break; + } + + thd->mdl_context.rollback_to_savepoint(mdl_svp); + if (thd->mdl_context.acquire_lock(&backup_request, + thd->variables.lock_wait_timeout)) + DBUG_RETURN(true); + thd->mdl_context.rollback_to_savepoint(mdl_svp); + backup_request.ticket= NULL; + for (table= tables; table; table= table->next_local) + table->mdl_request.ticket= NULL; + } /* for (;;) BACKUP_DDL retry */ + } if (lock_table_names(thd, tables, NULL, thd->variables.lock_wait_timeout, 0)) DBUG_RETURN(true); diff --git a/sql/sql_truncate.cc b/sql/sql_truncate.cc index 0a773ba3cda7e..062be082e4b80 100644 --- a/sql/sql_truncate.cc +++ b/sql/sql_truncate.cc @@ -500,6 +500,68 @@ bool Sql_cmd_truncate_table::truncate_table(THD *thd, TABLE_LIST *table_ref) } #endif /* WITH_WSREP */ + /* + MDEV-37365: Acquire SU, check FK, upgrade SU→X or return FK + error. SU blocks SNW (FK creation) and is held through upgrade + (no TOCTOU). Lock ordering: schema IX before table SU/X. + */ + if (!(thd->variables.option_bits & OPTION_NO_FOREIGN_KEY_CHECKS) && + !thd->locked_tables_mode) + { + MDL_savepoint mdl_svp= thd->mdl_context.mdl_savepoint(); + bool fk_checked= false; + + MDL_request schema_request; + MDL_REQUEST_INIT(&schema_request, MDL_key::SCHEMA, + table_ref->db.str, "", + MDL_INTENTION_EXCLUSIVE, MDL_TRANSACTION); + if (thd->mdl_context.acquire_lock(&schema_request, + thd->variables.lock_wait_timeout)) + DBUG_RETURN(TRUE); + + MDL_request su_request; + MDL_REQUEST_INIT_BY_KEY(&su_request, &table_ref->mdl_request.key, + MDL_SHARED_UPGRADABLE, MDL_TRANSACTION); + if (thd->mdl_context.acquire_lock(&su_request, + thd->variables.lock_wait_timeout)) + DBUG_RETURN(TRUE); + + Dummy_error_handler err_handler; + thd->push_internal_handler(&err_handler); + TABLE_SHARE *share= tdc_acquire_share(thd, table_ref, GTS_TABLE); + TABLE tmp_table; + bool fk_error= false; + bool table_opened= share + && (share->db_type()->flags & HTON_SUPPORTS_FOREIGN_KEYS) + && !open_table_from_share( + thd, share, &empty_clex_str, HA_OPEN_KEYFILE, 0, + HA_OPEN_FOR_ALTER | HA_OPEN_FOR_FK_CHECK, &tmp_table, false); + thd->pop_internal_handler(); + + if (table_opened) + { + fk_error= fk_truncate_illegal_if_parent(thd, &tmp_table); + closefrm(&tmp_table); + fk_checked= true; + } + if (share) + tdc_release_share(share); + if (fk_error) + { + thd->mdl_context.rollback_to_savepoint(mdl_svp); + DBUG_RETURN(TRUE); + } + if (fk_checked) + { + if (thd->mdl_context.upgrade_shared_lock( + su_request.ticket, MDL_EXCLUSIVE, + thd->variables.lock_wait_timeout)) + DBUG_RETURN(TRUE); + } + else + thd->mdl_context.rollback_to_savepoint(mdl_svp); + } + if (lock_table(thd, table_ref, &hton_can_recreate)) DBUG_RETURN(TRUE); diff --git a/storage/innobase/dict/dict0dict.cc b/storage/innobase/dict/dict0dict.cc index 1d3e5c0e0543a..65b1db77f77ea 100644 --- a/storage/innobase/dict/dict0dict.cc +++ b/storage/innobase/dict/dict0dict.cc @@ -1031,20 +1031,33 @@ dict_table_open_on_name( if (!(ignore_err & ~DICT_ERR_IGNORE_FK_NOKEY) && !table->is_readable() && table->corrupted) { - ulint algo= table->space->get_compression_algo(); - if (algo <= PAGE_ALGORITHM_LAST && !fil_comp_algo_loaded(algo)) - my_printf_error(ER_PROVIDER_NOT_LOADED, - "Table %.*sQ.%sQ is compressed with %s," - " which is not currently loaded. " - "Please load the %s provider plugin" - " to open the table", - MYF(ME_ERROR_LOG), - int(table->name.dblen()), table->name.m_name, - table->name.basename(), - page_compression_algorithms[algo], - page_compression_algorithms[algo]); - else + if (table->space) + { + ulint algo= table->space->get_compression_algo(); + if (algo <= PAGE_ALGORITHM_LAST && !fil_comp_algo_loaded(algo)) + { + my_printf_error(ER_PROVIDER_NOT_LOADED, + "Table %.*sQ.%sQ is compressed with %s," + " which is not currently loaded. " + "Please load the %s provider plugin" + " to open the table", + MYF(ME_ERROR_LOG), + int(table->name.dblen()), table->name.m_name, + table->name.basename(), + page_compression_algorithms[algo], + page_compression_algorithms[algo]); + dict_sys.unfreeze(); + DBUG_RETURN(nullptr); + } dict_table_open_failed(table->name); + } + else + my_printf_error(ER_TABLE_CORRUPT, + "Table %.*sQ.%sQ is corrupted." + " Please drop the table and recreate.", + MYF(0), + int(table->name.dblen()), table->name.m_name, + table->name.basename()); dict_sys.unfreeze(); DBUG_RETURN(nullptr); } diff --git a/storage/innobase/handler/ha_innodb.cc b/storage/innobase/handler/ha_innodb.cc index 5c2ea50657521..26a6cab560d0f 100644 --- a/storage/innobase/handler/ha_innodb.cc +++ b/storage/innobase/handler/ha_innodb.cc @@ -5851,7 +5851,7 @@ dberr_t ha_innobase::statistics_init(dict_table_t *table, bool recalc) @return error code @retval 0 on success */ int -ha_innobase::open(const char* name, int, uint) +ha_innobase::open(const char* name, int, uint test_if_locked) { char norm_name[FN_REFLEN]; @@ -5868,8 +5868,13 @@ ha_innobase::open(const char* name, int, uint) const char* is_part = dict_is_partition(norm_name); THD* thd = ha_thd(); + const dict_err_ignore_t ignore_err = + (test_if_locked & HA_OPEN_FOR_FK_CHECK) + ? dict_err_ignore_t(DICT_ERR_IGNORE_FK_NOKEY + | DICT_ERR_IGNORE_INDEX) + : DICT_ERR_IGNORE_FK_NOKEY; dict_table_t* ib_table = open_dict_table(name, norm_name, is_part, - DICT_ERR_IGNORE_FK_NOKEY); + ignore_err); DEBUG_SYNC(thd, "ib_open_after_dict_open"); @@ -15948,6 +15953,14 @@ bool ha_innobase::referenced_by_foreign_key() const noexcept return !empty; } +bool ha_innobase::references_foreign_key() const noexcept +{ + dict_sys.freeze(SRW_LOCK_CALL); + const bool empty= m_prebuilt->table->foreign_set.empty(); + dict_sys.unfreeze(); + return !empty; +} + inline void trx_t::reset_and_truncate_undo() noexcept { ut_ad(undo_no <= 1); diff --git a/storage/innobase/handler/ha_innodb.h b/storage/innobase/handler/ha_innodb.h index b6fb571078bea..a62abab731453 100644 --- a/storage/innobase/handler/ha_innodb.h +++ b/storage/innobase/handler/ha_innodb.h @@ -221,6 +221,7 @@ class ha_innobase final : public handler bool can_switch_engines() override; bool referenced_by_foreign_key() const noexcept override; + bool references_foreign_key() const noexcept override; void free_foreign_key_create_info(char* str) override { my_free(str); }