From 5df41ba738253a3cbf976c4e394614b0abea7a59 Mon Sep 17 00:00:00 2001 From: Thirunarayanan Balathandayuthapani Date: Fri, 23 Jan 2026 16:50:55 +0530 Subject: [PATCH] MDEV-36436 Acquire MDL on temporary table during DDL Problem: ======= An assertion failure occurs in InnoDB during consecutive ALTER TABLE operations using the COPY algorithm. The crash happens during the table rename phase because the source table (dict_table_t::n_ref_count) is non-zero, despite the thread holding an exclusive Metadata Lock (MDL). Reason: ======== When ALTER IGNORE TABLE is executed via the COPY algorithm, it generates undo logs for every row inserted into the intermediate table (e.g., #sql-alter-...). The background Purge Thread, responsible for cleaning up these undo logs, attempts to take an MDL on the table to prevent the table from being dropped while in use. Race condition: ================== First ALTER: Creates #sql-alter-, copies data, and renames it to t1. Purge Activation: The Purge thread picks up the undo logs from step 1. It takes an MDL on the temporary name (#sql-alter-) and increments the table's n_ref_count. Identity Shift: InnoDB renames the physical table object to t1, but the Purge thread still holds a reference to this object. Second ALTER: Starts a new copy process. When it attempts to rename the "new" t1 to a backup name, it checks if n_ref_count == 0. Because the Purge thread is still "pinning" the object to clean up logs from the first ALTER, the count is > 0, triggering the assertion failure. Solution: ======== By having the Server take an MDL on the temporary table name that maps to the same internal InnoDB lock as the target table, we ensure that the Purge Thread and the DDL thread are serialized. dict_table_t::parse_tbl_name() : Extracts database and table components from full table names mysql_alter_table(): MDL acquistion on temporary tables innobase_rename_table(), delete_table(): Added a check to verify MDL ownership InnoDB purge thread now takes MDL on table name directly instead of relying on mdl_name. Removed mdl_name from dict_table_t innodb_has_mdl(): This function verifies whether current thread hold MDL on the given table. It does the following: 1) Returns true for inplace intermediate temporary tables(#sql-ib) 2) Returns true for FTS Auxiliary Tables becuase the Purge system is explicitly stopped during DDL changes to Full-Text tables, these are inherently safe from the Purge and Rename race condition. 3) Returns true for partition names. This is because when InnoDB operates on a partition while holding an MDL on the base table name, but the scenarios like adding a partition where a base table MDL might not be held in the same way. 4) Returns true only if the thread holds an MDL_EXCLUSIVE lock. 5) Returns FALSE if the thread does not hold the appropriate MDL. --- mysql-test/main/backup_lock.result | 30 +++--- mysql-test/main/backup_lock.test | 4 +- sql/sql_partition_admin.cc | 8 ++ sql/sql_table.cc | 23 ++++ storage/innobase/dict/dict0dict.cc | 150 ++++++++++++++++---------- storage/innobase/dict/dict0mem.cc | 1 - storage/innobase/handler/ha_innodb.cc | 60 ++++++++++- storage/innobase/include/dict0mem.h | 26 +++-- storage/innobase/include/row0mysql.h | 23 ++++ storage/innobase/row/row0mysql.cc | 5 + storage/innobase/trx/trx0purge.cc | 6 +- 11 files changed, 250 insertions(+), 86 deletions(-) diff --git a/mysql-test/main/backup_lock.result b/mysql-test/main/backup_lock.result index 6e2ccad5091ea..fbd04325ede46 100644 --- a/mysql-test/main/backup_lock.result +++ b/mysql-test/main/backup_lock.result @@ -4,27 +4,27 @@ InnoDB 0 transactions not purged BACKUP STAGE START; SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME FROM information_schema.metadata_lock_info -WHERE TABLE_NAME NOT LIKE 'innodb_%_stats'; +WHERE TABLE_NAME NOT LIKE 'innodb_%_stats' ORDER BY LOCK_MODE; LOCK_MODE LOCK_TYPE TABLE_SCHEMA TABLE_NAME MDL_BACKUP_START Backup lock BACKUP STAGE FLUSH; SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME FROM information_schema.metadata_lock_info -WHERE TABLE_NAME NOT LIKE 'innodb_%_stats'; +WHERE TABLE_NAME NOT LIKE 'innodb_%_stats' ORDER BY LOCK_MODE; LOCK_MODE LOCK_TYPE TABLE_SCHEMA TABLE_NAME MDL_BACKUP_FLUSH Backup lock BACKUP STAGE BLOCK_DDL; SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME FROM information_schema.metadata_lock_info -WHERE TABLE_NAME NOT LIKE 'innodb_%_stats'; +WHERE TABLE_NAME NOT LIKE 'innodb_%_stats' ORDER BY LOCK_MODE; LOCK_MODE LOCK_TYPE TABLE_SCHEMA TABLE_NAME MDL_BACKUP_WAIT_DDL Backup lock BACKUP STAGE BLOCK_COMMIT; SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME FROM information_schema.metadata_lock_info -WHERE TABLE_NAME NOT LIKE 'innodb_%_stats'; +WHERE TABLE_NAME NOT LIKE 'innodb_%_stats' ORDER BY LOCK_MODE; LOCK_MODE LOCK_TYPE TABLE_SCHEMA TABLE_NAME MDL_BACKUP_WAIT_COMMIT Backup lock BACKUP STAGE END; SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME FROM information_schema.metadata_lock_info -WHERE TABLE_NAME NOT LIKE 'innodb_%_stats'; +WHERE TABLE_NAME NOT LIKE 'innodb_%_stats' ORDER BY LOCK_MODE; LOCK_MODE LOCK_TYPE TABLE_SCHEMA TABLE_NAME # # testing BACKUP STAGE LOCK's @@ -47,13 +47,14 @@ alter table t1 add column (j int), algorithm copy, lock shared; connection con2; backup stage flush; SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME FROM information_schema.metadata_lock_info -WHERE TABLE_NAME NOT LIKE 'innodb_%_stats'; +WHERE TABLE_NAME NOT LIKE 'innodb_%_stats' ORDER BY LOCK_MODE; LOCK_MODE LOCK_TYPE TABLE_SCHEMA TABLE_NAME MDL_BACKUP_DDL Backup lock MDL_BACKUP_FLUSH Backup lock -MDL_SHARED_WRITE Table metadata lock test t1 -MDL_SHARED_UPGRADABLE Table metadata lock test t1 +MDL_EXCLUSIVE Table metadata lock test #sql-alter MDL_INTENTION_EXCLUSIVE Schema metadata lock test +MDL_SHARED_UPGRADABLE Table metadata lock test t1 +MDL_SHARED_WRITE Table metadata lock test t1 SET STATEMENT max_statement_time=1 FOR backup stage block_ddl; ERROR 70100: Query execution was interrupted (max_statement_time exceeded) backup stage block_ddl; @@ -84,13 +85,14 @@ connection con2; backup stage start; backup stage flush; SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME FROM information_schema.metadata_lock_info -WHERE TABLE_NAME NOT LIKE 'innodb_%_stats'; +WHERE TABLE_NAME NOT LIKE 'innodb_%_stats' ORDER BY LOCK_MODE; LOCK_MODE LOCK_TYPE TABLE_SCHEMA TABLE_NAME MDL_BACKUP_ALTER_COPY Backup lock MDL_BACKUP_FLUSH Backup lock -MDL_SHARED_WRITE Table metadata lock test t1 -MDL_SHARED_UPGRADABLE Table metadata lock test t1 +MDL_EXCLUSIVE Table metadata lock test #sql-alter MDL_INTENTION_EXCLUSIVE Schema metadata lock test +MDL_SHARED_UPGRADABLE Table metadata lock test t1 +MDL_SHARED_WRITE Table metadata lock test t1 backup stage block_ddl; backup stage block_commit; connection default; @@ -121,11 +123,11 @@ SET STATEMENT lock_wait_timeout=0 FOR SELECT * FROM t1; ERROR HY000: Lock wait timeout exceeded; try restarting transaction backup stage block_ddl; SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME FROM information_schema.metadata_lock_info -WHERE TABLE_NAME NOT LIKE 'innodb_%_stats'; +WHERE TABLE_NAME NOT LIKE 'innodb_%_stats' ORDER BY LOCK_MODE; LOCK_MODE LOCK_TYPE TABLE_SCHEMA TABLE_NAME MDL_BACKUP_WAIT_DDL Backup lock -MDL_SHARED_WRITE Table metadata lock test t1 MDL_INTENTION_EXCLUSIVE Schema metadata lock test +MDL_SHARED_WRITE Table metadata lock test t1 backup stage end; connection default; commit; @@ -143,7 +145,7 @@ DROP TABLE t1; connection con2; connection con2; SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME FROM information_schema.metadata_lock_info -WHERE TABLE_NAME NOT LIKE 'innodb_%_stats'; +WHERE TABLE_NAME NOT LIKE 'innodb_%_stats' ORDER BY LOCK_MODE; LOCK_MODE LOCK_TYPE TABLE_SCHEMA TABLE_NAME MDL_BACKUP_WAIT_DDL Backup lock SELECT * FROM t1; diff --git a/mysql-test/main/backup_lock.test b/mysql-test/main/backup_lock.test index eec6c2f01ce6b..1eba9881ff1e7 100644 --- a/mysql-test/main/backup_lock.test +++ b/mysql-test/main/backup_lock.test @@ -13,7 +13,7 @@ --echo # let $mdl= LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME FROM information_schema.metadata_lock_info -WHERE TABLE_NAME NOT LIKE 'innodb_%_stats'; +WHERE TABLE_NAME NOT LIKE 'innodb_%_stats' ORDER BY LOCK_MODE; --source ../suite/innodb/include/wait_all_purged.inc @@ -62,6 +62,7 @@ let $wait_condition= where state = "Waiting for table metadata lock"; --source include/wait_condition.inc backup stage flush; +--replace_regex /#sql-alter-[0-9a-f_\-]*/#sql-alter/ eval SELECT $mdl; # # Do first test with max_statement_time, other tests later are done with @@ -111,6 +112,7 @@ let $wait_condition= --source include/wait_condition.inc backup stage start; backup stage flush; +--replace_regex /#sql-alter-[0-9a-f_\-]*/#sql-alter/ eval SELECT $mdl; backup stage block_ddl; backup stage block_commit; diff --git a/sql/sql_partition_admin.cc b/sql/sql_partition_admin.cc index 6697b4f580e78..8679e1ff042c3 100644 --- a/sql/sql_partition_admin.cc +++ b/sql/sql_partition_admin.cc @@ -716,6 +716,14 @@ bool Sql_cmd_alter_table_exchange_partition:: DEBUG_SYNC(thd, "swap_partition_before_rename"); + { + MDL_request mdl_request; + MDL_REQUEST_INIT(&mdl_request, MDL_key::TABLE, + table_list->next_local->db.str, + temp_name, MDL_EXCLUSIVE, MDL_TRANSACTION); + thd->mdl_context.acquire_lock(&mdl_request, 0); + } + if (unlikely(exchange_name_with_ddl_log(thd, swap_file_name, part_file_name, temp_file_name, table_hton))) goto err; diff --git a/sql/sql_table.cc b/sql/sql_table.cc index 13ed810cac2dd..f060360c673e5 100644 --- a/sql/sql_table.cc +++ b/sql/sql_table.cc @@ -11588,6 +11588,19 @@ do_continue:; */ alter_info->original_table= table; + { + /* + Create an exclusive lock on the temporary table name. + Needed by InnoDB storage engine to avoid MDL on source + table during purge. + */ + MDL_request mdl_request; + MDL_REQUEST_INIT(&mdl_request, MDL_key::TABLE, + alter_ctx.new_db.str, alter_ctx.tmp_name.str, + MDL_EXCLUSIVE, MDL_TRANSACTION); + thd->mdl_context.acquire_lock(&mdl_request, 0); + } + /* Create the .frm file for the new table. Storage engine table will not be created at this stage. @@ -12160,6 +12173,16 @@ do_continue:; If we are changing to use another table handler, we don't have to do the rename as the table names will not interfer. */ + /* + Create an exclusive lock on the backup table name. + Needed by InnoDB storage engine to avoid MDL conflicts + during backup table operations. + */ + MDL_request mdl_request; + MDL_REQUEST_INIT(&mdl_request, MDL_key::TABLE, + alter_ctx.db.str, backup_name.str, + MDL_EXCLUSIVE, MDL_TRANSACTION); + thd->mdl_context.acquire_lock(&mdl_request, 0); if (mysql_rename_table(old_db_type, &alter_ctx.db, &alter_ctx.table_name, &alter_ctx.db, &backup_name, &alter_ctx.id, FN_TO_IS_TMP | diff --git a/storage/innobase/dict/dict0dict.cc b/storage/innobase/dict/dict0dict.cc index 2edc7068f17e3..6b253793bdc5e 100644 --- a/storage/innobase/dict/dict0dict.cc +++ b/storage/innobase/dict/dict0dict.cc @@ -519,59 +519,109 @@ void mdl_release(THD *thd, MDL_ticket *mdl) noexcept thd->mdl_context.release_lock(mdl); } -/** Parse the table file name into table name and database name. -@tparam dict_frozen whether the caller holds dict_sys.latch -@param[in,out] db_name database name buffer -@param[in,out] tbl_name table name buffer -@param[out] db_name_len database name length -@param[out] tbl_name_len table name length -@return whether the table name is visible to SQL */ -template -bool dict_table_t::parse_name(char (&db_name)[NAME_LEN + 1], - char (&tbl_name)[NAME_LEN + 1], - size_t *db_name_len, size_t *tbl_name_len) const +void dict_table_t::parse_tbl_name( + const table_name_t &table_name, + char (&db_name)[NAME_LEN + 1], + char (&tbl_name)[NAME_LEN + 1], + size_t *db_name_len, size_t *tbl_name_len) noexcept { char db_buf[MAX_DATABASE_NAME_LEN + 1]; char tbl_buf[MAX_TABLE_NAME_LEN + 1]; - if (!dict_frozen) - dict_sys.freeze(SRW_LOCK_CALL); /* protect against renaming */ - ut_ad(dict_sys.frozen()); - const size_t db_len= name.dblen(); - ut_ad(db_len <= MAX_DATABASE_NAME_LEN); + const size_t db_len= table_name.dblen(); + const char* tbl_start= table_name.basename(); - memcpy(db_buf, mdl_name.m_name, db_len); - db_buf[db_len]= 0; + if (db_len == 0 || !tbl_start) + return; - size_t tbl_len= strlen(mdl_name.m_name + db_len + 1); - const bool is_temp= mdl_name.is_temporary(); + memcpy(db_buf, table_name.m_name, db_len); + db_buf[db_len]= '\0'; - if (is_temp); - else if (const char *is_part= static_cast - (memchr(mdl_name.m_name + db_len + 1, '#', tbl_len))) - tbl_len= static_cast(is_part - &mdl_name.m_name[db_len + 1]); + size_t tbl_len= strlen(tbl_start); + const bool is_temp= table_name.is_temporary(); - memcpy(tbl_buf, mdl_name.m_name + db_len + 1, tbl_len); - tbl_buf[tbl_len]= 0; + /* For partition tables, find the base table name by removing partition suffix */ + if (const char *is_part = static_cast + (memchr(tbl_start, '#', tbl_len))) + { + /* For temporary tables, we need to find the partition marker after the temp name */ + if (is_temp) + { + /* Look for partition markers like #P# after the temporary table name */ + const char *part_marker= strstr(tbl_start, "#P#"); + if (part_marker) + tbl_len= static_cast(part_marker - tbl_start); + else if (const char *vec_marker= strstr(tbl_start, "#i#")) + tbl_len= static_cast(vec_marker - tbl_start); + } + else + { + /* For regular tables, use first # as partition boundary */ + tbl_len= static_cast(is_part - tbl_start); + } + } - if (!dict_frozen) - dict_sys.unfreeze(); + memcpy(tbl_buf, tbl_start, tbl_len); + tbl_buf[tbl_len]= '\0'; + /* Convert internal names to SQL names */ *db_name_len= filename_to_tablename(db_buf, db_name, MAX_DATABASE_NAME_LEN + 1, true); - if (is_temp) - return false; - + { + memcpy(tbl_name, tbl_buf, tbl_len); + tbl_name[tbl_len]= '\0'; + *tbl_name_len= tbl_len; + return; + } *tbl_name_len= filename_to_tablename(tbl_buf, tbl_name, MAX_TABLE_NAME_LEN + 1, true); - return true; + return; } -template bool +/** Parse the table file name into table name and database name. +@tparam dict_frozen whether the caller holds dict_sys.latch +@param[in,out] db_name database name buffer +@param[in,out] tbl_name table name buffer +@param[out] db_name_len database name length +@param[out] tbl_name_len table name length +@return DB_CORRUPTION if table name contains #sql-ib, +@return DB_SUCCESS_LOCKED_REC if db_len == 0, DB_SUCCESS otherwise */ +template +dberr_t dict_table_t::parse_name(char (&db_name)[NAME_LEN + 1], + char (&tbl_name)[NAME_LEN + 1], + size_t *db_name_len, size_t *tbl_name_len) const +{ + if (!dict_frozen) + dict_sys.freeze(SRW_LOCK_CALL); /* protect against renaming */ + ut_ad(dict_sys.frozen()); + parse_tbl_name(name, db_name, tbl_name, db_name_len, tbl_name_len); + if (!dict_frozen) + dict_sys.unfreeze(); + + /* Check for specific error conditions */ + if (*db_name_len == 0) + return DB_SUCCESS_LOCKED_REC; + + /* Inplace intermediate table should not generate any + undo logs. In that case, throw error */ + if (strstr(tbl_name, "#sql-ib")) + { + sql_print_error("InnoDB: Trying to acquire MDL for inplace " + "intermediate table #sql-ib"); + return DB_CORRUPTION; + } + return DB_SUCCESS; +} + +template dberr_t dict_table_t::parse_name<>(char(&)[NAME_LEN + 1], char(&)[NAME_LEN + 1], size_t*, size_t*) const; +template dberr_t +dict_table_t::parse_name(char(&)[NAME_LEN + 1], char(&)[NAME_LEN + 1], + size_t*, size_t*) const; + dict_table_t *dict_sys_t::acquire_temporary_table(table_id_t id) const noexcept { ut_ad(frozen()); @@ -631,10 +681,13 @@ dict_acquire_mdl_shared(dict_table_t *table, char tbl_buf[NAME_LEN + 1], tbl_buf1[NAME_LEN + 1]; size_t db_len, tbl_len; - if (!table->parse_name(db_buf, tbl_buf, &db_len, &tbl_len)) - /* The name of an intermediate table starts with #sql */ + dberr_t err= table->parse_name( + db_buf, tbl_buf, &db_len, &tbl_len); + if (err == DB_SUCCESS_LOCKED_REC) return table; + if (err == DB_CORRUPTION) + return nullptr; retry: ut_ad(!trylock == dict_sys.frozen()); @@ -701,10 +754,11 @@ dict_acquire_mdl_shared(dict_table_t *table, if (trylock) table->acquire(); - if (!table->parse_name(db_buf1, tbl_buf1, &db1_len, &tbl1_len)) + dberr_t err= + table->parse_name(db_buf1, tbl_buf1, &db1_len, &tbl1_len); + if (err == DB_SUCCESS_LOCKED_REC || err == DB_CORRUPTION) { - /* The table was renamed to #sql prefix. - Release MDL (if any) for the old name and return. */ + /* Release MDL (if any) for the old name and return. */ goto unlock_and_return_without_mdl; } } @@ -1513,22 +1567,6 @@ dict_table_rename_in_cache( old_name_len)) ->remove(*table, &dict_table_t::name_hash); - bool keep_mdl_name = !table->name.is_temporary(); - - if (!keep_mdl_name) { - } else if (const char* s = static_cast - (memchr(new_name.data(), '/', new_name.size()))) { - keep_mdl_name = new_name.end() - s >= 5 - && !memcmp(s, "/#sql", 5); - } - - if (keep_mdl_name) { - /* Preserve the original table name for - dict_table_t::parse_name() and dict_acquire_mdl_shared(). */ - table->mdl_name.m_name = mem_heap_strdup(table->heap, - table->name.m_name); - } - if (new_name.size() > strlen(table->name.m_name)) { /* We allocate MAX_FULL_NAME_LEN + 1 bytes here to avoid memory fragmentation, we assume a repeated calls of @@ -1541,10 +1579,6 @@ dict_table_rename_in_cache( memcpy(table->name.m_name, new_name.data(), new_name.size()); table->name.m_name[new_name.size()] = '\0'; - if (!keep_mdl_name) { - table->mdl_name.m_name = table->name.m_name; - } - /* Add table to hash table of tables */ ut_ad(!table->name_hash); dict_table_t** after = reinterpret_cast( diff --git a/storage/innobase/dict/dict0mem.cc b/storage/innobase/dict/dict0mem.cc index abd51dea27006..2ea5a1260efca 100644 --- a/storage/innobase/dict/dict0mem.cc +++ b/storage/innobase/dict/dict0mem.cc @@ -148,7 +148,6 @@ dict_table_t *dict_table_t::create(const span &name, table->flags= static_cast(flags) & ((1U << DICT_TF_BITS) - 1); table->flags2= static_cast(flags2) & ((1U << DICT_TF2_BITS) - 1); table->name.m_name= mem_strdupl(name.data(), name.size()); - table->mdl_name.m_name= table->name.m_name; table->is_system_db= dict_mem_table_is_system(table->name.m_name); table->space= space; table->space_id= space ? space->id : UINT32_MAX; diff --git a/storage/innobase/handler/ha_innodb.cc b/storage/innobase/handler/ha_innodb.cc index 858e2f3ce8683..bff47b63b2a0e 100644 --- a/storage/innobase/handler/ha_innodb.cc +++ b/storage/innobase/handler/ha_innodb.cc @@ -119,6 +119,7 @@ this program; if not, write to the Free Software Foundation, Inc., #include #include // TT_FOR_UPGRADE #include "sql_type_vector.h" +#include #define thd_get_query_id(thd) uint64_t((thd)->query_id) #define thd_in_lock_tables(thd) (thd)->in_lock_tables @@ -13468,6 +13469,55 @@ static bool delete_table_check_foreigns(const dict_table_t &table, return false; } +/** Check if the current thread has appropriate MDL for a table. +This function is used to verify MDL ownership before performing +table operations like DROP TABLE or RENAME TABLE to ensure +proper locking protocol. +@param[in] thd MySQL thread handle +@param[in] table_name Full table name to check MDL ownership for +@return true if MDL check should pass, false otherwise + +The function returns true for: + Table name contains "sql-ib" (internal temporary tables) + Table is an FTS auxiliary table (FTS__ format) + - Purge system is being stopped when we're making any DDL changes + to fulltext table. + Thread holds MDL_EXCLUSIVE lock on the base table name + Table name represents a partition + - InnoDB takes MDL on base table name when it deals + with one of the partitions of the table. But adding + new partition doesn't take MDL on base table. That's + why we're making exception for partition table +@return FALSE when: + Thread does not hold appropriate MDL lock on the table */ +bool innodb_has_mdl(THD *thd, const char *table_name) +{ + char db_buf[NAME_LEN + 1]; + char tbl_buf[NAME_LEN + 1]; + size_t tbl_len, db_len; + + dict_table_t::parse_tbl_name( + table_name_t(const_cast(table_name)), + db_buf, tbl_buf, &db_len, &tbl_len); + + if (strstr(tbl_buf, "sql-ib")) + return true; + + /* Check if this is an FTS auxiliary table */ + table_id_t table_id; + index_id_t index_id; + if (fts_check_aux_table(table_name, &table_id, &index_id)) + return true; + + MDL_key key; + key.mdl_key_init(MDL_key::TABLE, db_buf, tbl_buf); + if (thd->mdl_context.is_lock_owner(MDL_key::TABLE, db_buf, tbl_buf, + MDL_EXCLUSIVE)) + return true; + return dict_is_partition(table_name); +} + + /** DROP TABLE (possibly as part of DROP DATABASE, CREATE/ALTER TABLE) @param name table name @return error number */ @@ -13716,6 +13766,8 @@ int ha_innobase::delete_table(const char *name) if (err != DB_SUCCESS) goto err_exit; + ut_ad(!purge_sys.enabled() || + innodb_has_mdl(trx->mysql_thd, table->name.m_name)); err= trx->drop_table(*table); if (err != DB_SUCCESS) goto err_exit; @@ -20356,10 +20408,16 @@ static TABLE* innodb_find_table_for_vc(THD* thd, dict_table_t* table) char tbl_buf[NAME_LEN + 1]; ulint db_buf_len, tbl_buf_len; - if (!table->parse_name(db_buf, tbl_buf, &db_buf_len, &tbl_buf_len)) { + dberr_t err= table->parse_name( + db_buf, tbl_buf, &db_buf_len, &tbl_buf_len); + if (err == DB_SUCCESS_LOCKED_REC) { return NULL; } + if (err == DB_CORRUPTION) { + return nullptr; + } + if (bg_thread) { return open_purge_table(thd, db_buf, db_buf_len, tbl_buf, tbl_buf_len); diff --git a/storage/innobase/include/dict0mem.h b/storage/innobase/include/dict0mem.h index 0fa14f747c9f7..478b5332db013 100644 --- a/storage/innobase/include/dict0mem.h +++ b/storage/innobase/include/dict0mem.h @@ -2105,17 +2105,30 @@ struct dict_table_t { /** For overflow fields returns potential max length stored inline */ inline size_t get_overflow_field_local_len() const; + /** Parse the table file name into database namd and table name + @param table_name table file name + @param db_name database name buffer + @param tbl_name table name buffer + @param db_name_len database name length + @param tbl_name_len table name length */ + static void parse_tbl_name( + const table_name_t &table_name, + char (&db_name)[NAME_LEN + 1], + char (&tbl_name)[NAME_LEN + 1], + size_t *db_name_len, size_t *tbl_name_len) noexcept; + /** Parse the table file name into table name and database name. @tparam dict_frozen whether the caller holds dict_sys.latch @param[in,out] db_name database name buffer @param[in,out] tbl_name table name buffer @param[out] db_name_len database name length @param[out] tbl_name_len table name length - @return whether the table name is visible to SQL */ + @return DB_SUCCESS_LOCKED_REC if table name contains #sql-ib, + DB_CORRUPTION if db_len == 0, DB_SUCCESS otherwise */ template - bool parse_name(char (&db_name)[NAME_LEN + 1], - char (&tbl_name)[NAME_LEN + 1], - size_t *db_name_len, size_t *tbl_name_len) const; + dberr_t parse_name(char (&db_name)[NAME_LEN + 1], + char (&tbl_name)[NAME_LEN + 1], + size_t *db_name_len, size_t *tbl_name_len) const; /** Clear the table when rolling back TRX_UNDO_EMPTY @return error code */ @@ -2354,11 +2367,6 @@ struct dict_table_t { an empty leaf page), and an ahi_latch (if btr_search_enabled). */ Atomic_relaxed bulk_trx_id; - /** Original table name, for MDL acquisition in purge. Normally, - this points to the same as name. When is_temporary_name(name.m_name) holds, - this should be a copy of the original table name, allocated from heap. */ - table_name_t mdl_name; - /*!< set of foreign key constraints in the table; these refer to columns in other tables */ dict_foreign_set foreign_set; diff --git a/storage/innobase/include/row0mysql.h b/storage/innobase/include/row0mysql.h index 2f50aa1560eb2..a50632fc16da1 100644 --- a/storage/innobase/include/row0mysql.h +++ b/storage/innobase/include/row0mysql.h @@ -830,6 +830,29 @@ void innobase_rename_vc_templ( dict_table_t* table); + /** Check if the current thread has appropriate MDL for a table. +This function is used to verify MDL ownership before performing +table operations like DROP TABLE or RENAME TABLE to ensure +proper locking protocol. +@param[in] thd MySQL thread handle +@param[in] table_name Full table name to check MDL ownership for +@return true if MDL check should pass, false otherwise + +Function returns true for: + Table name contains "sql-ib" (internal temporary tables) + Table is an FTS auxiliary table (FTS__ format) + - Purge system is being stopped when we're making any DDL changes + to fulltext table. + Thread holds MDL_EXCLUSIVE lock on the base table name + Table name represents a partition + - InnoDB takes MDL on base table name when it deals + with one of the partitions of the table. But adding + new partition doesn't take MDL on base table. That's + why we're making exception for partition table +Function returns FALSE when: + Thread does not hold appropriate MDL lock on the table */ +bool innodb_has_mdl(THD *thd, const char *table_name); + #define ROW_PREBUILT_FETCH_MAGIC_N 465765687 #define ROW_MYSQL_WHOLE_ROW 0 diff --git a/storage/innobase/row/row0mysql.cc b/storage/innobase/row/row0mysql.cc index 6098d098c6478..78dfe72e9fe20 100644 --- a/storage/innobase/row/row0mysql.cc +++ b/storage/innobase/row/row0mysql.cc @@ -2621,6 +2621,11 @@ row_rename_table_for_mysql( } } + ut_ad(!purge_sys.enabled() + || innodb_has_mdl(trx->mysql_thd, table->name.m_name)); + ut_ad(!purge_sys.enabled() + || innodb_has_mdl(trx->mysql_thd, new_name)); + err = trx_undo_report_rename(trx, table); if (err != DB_SUCCESS) { diff --git a/storage/innobase/trx/trx0purge.cc b/storage/innobase/trx/trx0purge.cc index 9ba4a8f3e190f..6fa200ac60973 100644 --- a/storage/innobase/trx/trx0purge.cc +++ b/storage/innobase/trx/trx0purge.cc @@ -1103,8 +1103,10 @@ static dict_table_t *trx_purge_table_acquire(dict_table_t *table, char tbl_buf[NAME_LEN + 1]; size_t tbl_len; - if (!table->parse_name(db_buf, tbl_buf, &db_len, &tbl_len)) - /* The name of an intermediate table starts with #sql */ + dberr_t parse_result = table->parse_name( + db_buf, tbl_buf, &db_len, &tbl_len); + if (parse_result == DB_SUCCESS_LOCKED_REC || + parse_result == DB_CORRUPTION) goto got_table; {