From 0397e4e4dc4b5f538bdc71a9bc6cd6aa35f824a7 Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Thu, 25 Dec 2025 23:49:52 +0700 Subject: [PATCH] MDEV-27027 Atomic DDL: Assertion failed upon unsuccessful multi-RENAME TABLE When executing an atomic sequence of RENAME operations, such as: RENAME TABLE t1 TO t2, t3 TO t4, ... any failure in the sequence triggers a rollback of previously completed renames to preserve atomicity. However, when an error occurs, `my_error()` is invoked immediately, which sets the `thd->is_error()` flag. This premature flag setting causes the rollback logic to misinterpret the thread state, leading to incorrect reversion behavior and assertion failures. To address this, the errors are now not emitted immediately but captured and postponed. A new class `Postponed_error_handler` is introduced for this purpose. Only after all operations are completed (including possible DDL reversion), the captured errors are emitted. --- mysql-test/main/rename.result | 40 ++++++++++++++++++++ mysql-test/main/rename.test | 41 ++++++++++++++++++++ sql/sql_class.h | 70 +++++++++++++++++++++++++++++++++++ sql/sql_rename.cc | 15 ++++++++ 4 files changed, 166 insertions(+) diff --git a/mysql-test/main/rename.result b/mysql-test/main/rename.result index 64ccf38595895..6da5e0590612c 100644 --- a/mysql-test/main/rename.result +++ b/mysql-test/main/rename.result @@ -173,3 +173,43 @@ drop table t2; rename table if exists t1 to t2; alter table if exists t2 rename to t1; drop table t1; +# +# MDEV-27027 Atomic DDL: Assertion `!param->got_error' failed upon +# unsuccessful multi-RENAME TABLE +# +CREATE TABLE t1 (a INT); +CREATE TRIGGER tr1 AFTER INSERT ON t1 FOR EACH ROW +INSERT INTO db.t SELECT SLEEP(0.05); +# Missing table in RENAME TABLE (should error) +RENAME TABLE t1 TO t2, +missing1 TO m1; +ERROR 42S02: Table 'test.missing1' doesn't exist +SHOW CREATE TRIGGER tr1; +Trigger sql_mode SQL Original Statement character_set_client collation_connection Database Collation Created +tr1 STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION CREATE DEFINER=`root`@`localhost` TRIGGER tr1 AFTER INSERT ON `t1` FOR EACH ROW +INSERT INTO db.t SELECT SLEEP(0.05) latin1 latin1_swedish_ci latin1_swedish_ci # +CREATE TABLE t2 (a INT); +# Attempt to rename to existing table (should error) +RENAME TABLE t2 TO t3, +t1 TO t3; +ERROR 42S01: Table 't3' already exists +SHOW CREATE TRIGGER tr1; +Trigger sql_mode SQL Original Statement character_set_client collation_connection Database Collation Created +tr1 STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION CREATE DEFINER=`root`@`localhost` TRIGGER tr1 AFTER INSERT ON `t1` FOR EACH ROW +INSERT INTO db.t SELECT SLEEP(0.05) latin1 latin1_swedish_ci latin1_swedish_ci # +# Test multiple warnings captured during renaming +RENAME TABLE IF EXISTS +missing1 TO m1, +missing2 TO m2, +t2 TO t3, # this rename succeeds +missing3 TO m3; +Warnings: +Note 1146 Table 'test.missing1' doesn't exist +Note 1146 Table 'test.missing2' doesn't exist +Note 1146 Table 'test.missing3' doesn't exist +RENAME TABLE IF EXISTS +missing1 TO m1, +t1 TO t3, # this will error: table exists +missing2 TO m2; +ERROR 42S01: Table 't3' already exists +DROP TABLE t1, t3; diff --git a/mysql-test/main/rename.test b/mysql-test/main/rename.test index a0b9f38ae2ea6..969c0417b4fe7 100644 --- a/mysql-test/main/rename.test +++ b/mysql-test/main/rename.test @@ -166,3 +166,44 @@ drop table t2; rename table if exists t1 to t2; alter table if exists t2 rename to t1; drop table t1; + +--echo # +--echo # MDEV-27027 Atomic DDL: Assertion `!param->got_error' failed upon +--echo # unsuccessful multi-RENAME TABLE +--echo # +CREATE TABLE t1 (a INT); +CREATE TRIGGER tr1 AFTER INSERT ON t1 FOR EACH ROW + INSERT INTO db.t SELECT SLEEP(0.05); + +--echo # Missing table in RENAME TABLE (should error) +--error ER_NO_SUCH_TABLE +RENAME TABLE t1 TO t2, + missing1 TO m1; # this will error: missing table + +--replace_column 7 # +SHOW CREATE TRIGGER tr1; + +CREATE TABLE t2 (a INT); + +--echo # Attempt to rename to existing table (should error) +--error ER_TABLE_EXISTS_ERROR +RENAME TABLE t2 TO t3, + t1 TO t3; # this will error: table exists + +--replace_column 7 # +SHOW CREATE TRIGGER tr1; + +--echo # Test multiple warnings captured during renaming +RENAME TABLE IF EXISTS + missing1 TO m1, + missing2 TO m2, + t2 TO t3, # this rename succeeds + missing3 TO m3; + +--error ER_TABLE_EXISTS_ERROR +RENAME TABLE IF EXISTS + missing1 TO m1, + t1 TO t3, # this will error: table exists + missing2 TO m2; + +DROP TABLE t1, t3; \ No newline at end of file diff --git a/sql/sql_class.h b/sql/sql_class.h index 42a8567ea5afb..d3c50df01f0ec 100644 --- a/sql/sql_class.h +++ b/sql/sql_class.h @@ -2047,6 +2047,76 @@ class Counting_error_handler : public Internal_error_handler }; +extern "C" void my_message_sql(uint error, const char *str, myf MyFlags); + +/** + Error handler that captures and postpones errors. + Warnings and notes are passed through to the next handler. + Stored errors can be re-emitted later via emit_errors(). +*/ + +class Postponed_error_handler : public Internal_error_handler +{ + struct Error_entry + { + uint sql_errno; + char message[MYSQL_ERRMSG_SIZE]; + Error_entry *next; + }; + + Error_entry *m_first; + Error_entry *m_last; + MEM_ROOT *m_mem_root; + +public: + Postponed_error_handler(MEM_ROOT *mem_root) + : m_first(nullptr), m_last(nullptr), m_mem_root(mem_root) + {} + + bool handle_condition(THD *thd, + uint sql_errno, + const char *sqlstate, + Sql_condition::enum_warning_level *level, + const char *msg, + Sql_condition **cond_hdl) override + { + /* Only capture errors, let warnings and notes pass through */ + if (*level != Sql_condition::WARN_LEVEL_ERROR) + return false; + + Error_entry *entry= (Error_entry*) alloc_root(m_mem_root, + sizeof(Error_entry)); + if (!entry) + return false; // Can't store, let error propagate + + entry->sql_errno= sql_errno; + strmake(entry->message, msg, sizeof(entry->message) - 1); + entry->next= nullptr; + + if (m_last) + m_last->next= entry; + else + m_first= entry; + m_last= entry; + + return true; + } + + bool has_errors() const { return m_first != nullptr; } + + void emit_errors() + { + for (Error_entry *e= m_first; e; e= e->next) + my_message_sql(e->sql_errno, e->message, MYF(0)); + } + + void clear() + { + m_first= m_last= nullptr; + } +}; + + /** This class is an internal error handler implementation for DROP TABLE statements. The thing is that there may be warnings during diff --git a/sql/sql_rename.cc b/sql/sql_rename.cc index 421c7198c10b5..5b909200cae4e 100644 --- a/sql/sql_rename.cc +++ b/sql/sql_rename.cc @@ -20,6 +20,7 @@ */ #include "mariadb.h" +#include "sql_class.h" #include "sql_priv.h" #include "unireg.h" #include "sql_rename.h" @@ -51,6 +52,7 @@ static bool rename_tables(THD *thd, TABLE_LIST *table_list, the new name. */ + bool mysql_rename_tables(THD *thd, TABLE_LIST *table_list, bool silent, bool if_exists) { @@ -60,6 +62,7 @@ bool mysql_rename_tables(THD *thd, TABLE_LIST *table_list, bool silent, int to_table; const char *rename_log_table[2]= {NULL, NULL}; DDL_LOG_STATE ddl_log_state; + Postponed_error_handler postponed_handler(thd->mem_root); DBUG_ENTER("mysql_rename_tables"); /* @@ -162,6 +165,15 @@ bool mysql_rename_tables(THD *thd, TABLE_LIST *table_list, bool silent, An exclusive lock on table names is satisfactory to ensure no other thread accesses this table. */ + + /* + Do not emit errors which may occur during the sequence of renamings, + because they will raise the 'thd->is_error()' flag, and `ddl_log_revert()` + may fail due to this. Instead, capture and collect errors and warnings + and emit them only after the renaming and possible reversion are complete. + */ + thd->push_internal_handler(&postponed_handler); + error= rename_tables(thd, table_list, &ddl_log_state, 0, if_exists, &force_if_exists); @@ -192,15 +204,18 @@ bool mysql_rename_tables(THD *thd, TABLE_LIST *table_list, bool silent, my_ok(thd); } + thd->pop_internal_handler(); if (likely(!error)) { query_cache_invalidate3(thd, table_list, 0); ddl_log_complete(&ddl_log_state); + DBUG_ASSERT(!postponed_handler.has_errors()); } else { /* Revert the renames of normal tables with the help of the ddl log */ ddl_log_revert(thd, &ddl_log_state); + postponed_handler.emit_errors(); } err: