From 6aee1fca516074884c6c6427c71b686cda913a4b Mon Sep 17 00:00:00 2001 From: Thirunarayanan Balathandayuthapani Date: Fri, 6 Mar 2026 18:07:38 +0530 Subject: [PATCH] MDEV-38993 Assertion `trx->undo_no == 1' fails upon ALTER IGNORE Problem: ======== During ALTER TABLE ... IGNORE, a partial rollback on duplicate key error resets trx->undo_no to 0. The subsequent insert then enters the undo rewrite block with undo_no == 0, hitting the assertion that expected undo_no == 1. Solution: ========= Partial rollback which truncates the last insert undo record via trx_undo_truncate_end(), which rewrites TRX_UNDO_PAGE_FREE on the page. By checking trx->undo_no as part of the rewrite predicate, InnoDB correctly skips the rewrite logic after partial rollback. trx_undo_report_row_operation(): Pre-compute the full predicate (clear_ignore) before trx_undo_assign_low(), since old_offset and top_offset are not modified by that call. trx_undo_rewrite_ignore(): Extract the rewrite body into a separate ATTRIBUTE_COLD ATTRIBUTE_NOINLINE static function. --- mysql-test/suite/innodb/r/alter_crash.result | 7 +++ mysql-test/suite/innodb/t/alter_crash.test | 8 +++ storage/innobase/trx/trx0rec.cc | 54 ++++++++++++-------- 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/mysql-test/suite/innodb/r/alter_crash.result b/mysql-test/suite/innodb/r/alter_crash.result index c44b3e067c6c5..c37bf397b7ae6 100644 --- a/mysql-test/suite/innodb/r/alter_crash.result +++ b/mysql-test/suite/innodb/r/alter_crash.result @@ -268,3 +268,10 @@ CHECK TABLE t1 EXTENDED; Table Op Msg_type Msg_text test.t1 check status OK DROP TABLE t1; +# +# MDEV-38993 Assertion `trx->undo_no == 1' fails upon ALTER IGNORE +# +CREATE TABLE t (a INT) ENGINE=InnoDB; +INSERT INTO t VALUES (1),(2),(1),(2); +ALTER IGNORE TABLE t ADD UNIQUE(a); +DROP TABLE t; diff --git a/mysql-test/suite/innodb/t/alter_crash.test b/mysql-test/suite/innodb/t/alter_crash.test index c185ccb99a317..6f2debb11ed1f 100644 --- a/mysql-test/suite/innodb/t/alter_crash.test +++ b/mysql-test/suite/innodb/t/alter_crash.test @@ -267,3 +267,11 @@ INSERT INTO t1 VALUES(1), (1); ALTER IGNORE TABLE t1 ADD UNIQUE(f1); CHECK TABLE t1 EXTENDED; DROP TABLE t1; + +--echo # +--echo # MDEV-38993 Assertion `trx->undo_no == 1' fails upon ALTER IGNORE +--echo # +CREATE TABLE t (a INT) ENGINE=InnoDB; +INSERT INTO t VALUES (1),(2),(1),(2); +ALTER IGNORE TABLE t ADD UNIQUE(a); +DROP TABLE t; diff --git a/storage/innobase/trx/trx0rec.cc b/storage/innobase/trx/trx0rec.cc index f7c89c4b30538..24cf490082cc7 100644 --- a/storage/innobase/trx/trx0rec.cc +++ b/storage/innobase/trx/trx0rec.cc @@ -1807,6 +1807,30 @@ static bool trx_has_lock_x(const trx_t &trx, dict_table_t& table) return false; } +/** For ALTER TABLE...IGNORE ALGORITHM=COPY, rewind the undo log +to maintain only the latest insert undo record. This allows easy +rollback of the last inserted row on duplicate key errors. +@param table table being altered +@param trx transaction +@param undo insert undo log +@param undo_block undo log page +@param mtr mini-transaction +@param m mod_tables entry to reinitialize */ +static ATTRIBUTE_COLD ATTRIBUTE_NOINLINE +void trx_undo_rewrite_ignore(dict_table_t *table, trx_t *trx, trx_undo_t *undo, + buf_block_t *undo_block, mtr_t *mtr, + std::pair &m) +{ + ut_ad(trx->undo_no == 1); + undo->top_offset= undo->old_offset; + undo->top_undo_no= 0; + trx->undo_no= 0; + trx->mod_tables.clear(); + m= trx->mod_tables.emplace(table, 0); + mtr->write<2>(*undo_block, undo_block->page.frame + TRX_UNDO_PAGE_HDR + + TRX_UNDO_PAGE_FREE, undo->old_offset); +} + /***********************************************************************//** Writes information to an undo log about an insert, update, or a delete marking of a clustered index record. This information is used in a rollback of the @@ -1912,32 +1936,18 @@ trx_undo_report_row_operation( ut_ad(!trx->read_only); ut_ad(trx->id); pundo = &trx->rsegs.m_redo.undo; - const bool empty{!*pundo}; + const bool clear_ignore{*pundo && trx->undo_no && + (*pundo)->old_offset <= + (*pundo)->top_offset && + index->table->skip_alter_undo == + dict_table_t::IGNORE_UNDO}; rseg = trx->rsegs.m_redo.rseg; undo_block = trx_undo_assign_low(trx, rseg, pundo, &mtr, &err); - /* For ALTER IGNORE, implement undo log rewriting - to maintain only the latest insert undo record. - This allows easy rollback of the last inserted row - on duplicate key errors. Before writing a new undo - record, rewind the undo log to the previous record - position, effectively discarding all intermediate - undo records and keeping only the most recent one. */ - if (!empty && (*pundo)->old_offset <= (*pundo)->top_offset && - index->table->skip_alter_undo == - dict_table_t::IGNORE_UNDO) { - + if (clear_ignore) { ut_ad(!rec); - ut_ad(trx->undo_no == 1); - (*pundo)->top_offset = (*pundo)->old_offset; - (*pundo)->top_undo_no = 0; - trx->undo_no = 0; - trx->mod_tables.clear(); - m = trx->mod_tables.emplace(index->table, 0); - mtr.write<2>( - *undo_block, - undo_block->page.frame + TRX_UNDO_PAGE_HDR + - TRX_UNDO_PAGE_FREE, (*pundo)->old_offset); + trx_undo_rewrite_ignore(index->table, trx, *pundo, + undo_block, &mtr, m); } }