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: