MDEV-35732 Failed ALTER TABLE causes inconsistency, changes behavior of the next statement - #5496
Open
LukeYL026 wants to merge 1 commit into
Open
Conversation
A failed ALTER TABLE ... RENAME INDEX ... ALGORITHM=INSTANT changed the outcome of a subsequent, unrelated ALTER TABLE ... ADD FOREIGN KEY. A failed statement must have no side effects, but here the second statement wrongly succeeded where on a fresh table it correctly fails with ER_DUP_KEYNAME. Root cause: while rebuilding the key list, mysql_prepare_alter_table() handled a RENAME INDEX request by clearing HA_GENERATED_KEY in place on key_info->flags. key_info points into the (possibly cached) TABLE object that is reused across statements. The ALGORITHM=INSTANT incompatibility is only detected later, after mysql_prepare_alter_table() has returned, so the statement fails with ER_ALTER_OPERATION_NOT_SUPPORTED with the cleared flag never restored. The cached generated FK-support index (fk1) was thus left permanently marked as user-defined. That corrupted flag flips the de-duplication tie-break in the next ALTER: adding FOREIGN KEY ind1 (b) creates a generated support index on column b that prefix-matches fk1(b). Normally fk1 (generated) is dropped and the new ind1 survives, colliding by name with the existing user index ind1(a) and raising ER_DUP_KEYNAME. With fk1 no longer marked generated, the new ind1 is dropped instead, so no name collision is reached and the ADD FOREIGN KEY silently succeeds. Fix: do not mutate the cached key_info->flags. Track the "renamed => no longer generated" decision in a per-key local variable (generated_key), initialised from the flag, set to false on rename, and passed to the Key constructor. This preserves the in-statement behaviour while leaving the cached TABLE metadata untouched, so a failed ALTER has no lingering effect. All new code of the whole pull request, including one or several files that are either new files or modified ones, are contributed under the BSD-new license. I am contributing on behalf of my employer Amazon Web Services, Inc.
gkodinov
approved these changes
Aug 6, 2026
gkodinov
left a comment
Member
There was a problem hiding this comment.
This is a preliminary review. Thank you for your contribution!
LGTM. Please stand by for the final review.
Author
|
For sure - thanks again for taking the time to look at my submissions! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
A failed
ALTER TABLE ... RENAME INDEX ... ALGORITHM=INSTANTcorrupted thein-memory (cached) table metadata, which then changed the outcome of a
subsequent, unrelated
ALTER TABLE ... ADD FOREIGN KEY. The failed statementshould have had no side effects.
Concretely:
Background — the three concepts you need
To follow the bug you need three pieces of vocabulary:
Index vs. column. A column (like
b INT) is where data lives. Anindex is a separate lookup structure built over a column so the engine
can find rows by that column's value quickly. Adding an index does not add a
column.
Generated (auto-created) indexes. When you declare a
FOREIGN KEY,InnoDB needs an index on the referencing column to enforce the constraint.
If you did not also declare an explicit
KEYfor that column, the serversynthesizes a supporting index for you and stamps it with the
HA_GENERATED_KEYflag. The flag means "this index exists only to back aforeign key; the user did not declare it as a standalone index." It is set
unconditionally for a FK's supporting index in the parser
(
sql/sql_yacc.yy,Key(Key::MULTIPLE, name, …, /*generated=*/true, …)),even when you give the FK a name — the name and the generated flag are
independent. So in the repro,
fk1is a named but generated index.The table cache. The server keeps an in-memory
TABLEobject (with itskey_infoarray of index metadata) cached and reused across statements.Mutating it is mutating shared state that outlives the current statement.
Why the generated flag matters: during table preparation the server
de-duplicates indexes. If two indexes cover the same column(s), a generated
one is considered disposable ("I only made it for a constraint") and can be
silently dropped in favour of the other. That single flag is the tie-breaker
deciding which of two colliding indexes is discarded.
The example table therefore has three indexes:
PRIMARY KEYKEY ind1(a)FOREIGN KEY fk1What goes wrong, step by step
Statement 1 — the failed rename corrupts the cache
mysql_prepare_alter_table()speculatively builds what the table's indexlist would look like if this ALTER succeeded. It loops over the cached
TABLE'skey_info.It sees the
RENAME INDEX fk1 TO fkrequest. The new name goes into alocal variable (harmless). But it also decides "the user is renaming this
index by hand, so stop treating it as a generated FK-support index" — a
reasonable decision, because otherwise de-duplication might quietly drop the
renamed index. The bug: it recorded that decision by clearing the flag
directly on the shared cached object:
mysql_prepare_alter_table()finishes and returns success — it built avalid list. (Note: the neighbouring long-hash edits in the same loop are
deliberately undone via
re_setup_keyinfo_hash(); this flag clear was not.)Only later, back in
mysql_alter_table(), does the server determine thatALGORITHM=INSTANTcannot perform theORDER BYrebuild, so the statementfails with ER_ALTER_OPERATION_NOT_SUPPORTED.
The statement is rejected — no rename, no disk write, InnoDB never invoked —
but the speculative flag clear already leaked into the cached
TABLEandis never restored. The cached
fk1is now (wrongly) marked as auser-defined index. The name is still
fk1(the rename never happened);only the generated flag was lost.
Statement 2 — the corrupted flag flips the result
Parsing this creates a new FK constraint named
ind1plus its own generatedsupporting index — also named
ind1, on columnb.The candidate index list now contains two indexes on column
b: the oldfk1(b)and the newind1(b). De-duplication (is_foreign_key_prefix()inmysql_prepare_create_table()) fires for that same-column pair and must dropone. The tie-break looks at whether the older key (
fk1) is generated:fk1(b)is dropped, the newind1(b)survives.
ind1(b)is dropped(
IGNORE_KEY), andfk1(b)survives.Next, the index-naming loop checks each surviving index for a duplicate
name. Indexes marked
IGNORE_KEYare skipped before this check runs.ind1(b)reaches the check; its nameind1collides with the user's existing
ind1(a)→ ER_DUP_KEYNAME. Correct.ind1(b)that carried the colliding name was alreadydropped in step 2, so the name check never sees it → no error, the ALTER
silently succeeds.
So a single lost flag bit changes which same-column index survives
de-duplication, which in turn decides whether the name-collision check is even
reachable.
The end state
After the corrupted run,
ADD FOREIGN KEY ind1wrongly succeeds: the newconstraint reuses the existing
fk1(b)index for support, and no newind1index is created. On a fresh table (or after a server restart, which rebuilds
the cache from disk) the identical statement correctly fails with
ER_DUP_KEYNAME. That is what makes this bug especially nasty:
correctly, so it may not reproduce for whoever investigates.
one, when failed statements are supposed to be no-ops.
Root cause (TL;DR)
mysql_prepare_alter_table()clearedHA_GENERATED_KEYin place on thecached
TABLE'skey_infowhile speculatively handling aRENAME INDEX, andthat mutation was never reverted when the statement later failed — leaving the
cached index metadata corrupted for subsequent statements.
Fix
Do not mutate the cached
key_info->flags. Track the "renamed ⇒ no longergenerated" decision in a per-key local variable (
generated_key):bool generated_key = key_info->flags & HA_GENERATED_KEY;(a read, not awrite).
falsein the rename branch instead of clearing the cachedflag.
Keyconstructor in place ofkey_info->flags & HA_GENERATED_KEY.Because the old code both cleared and later re-read the same
key_info->flags, routing both operations through the local reproduces theexact in-statement result — a successful ALTER produces an identical index
list — while leaving the cached
TABLEuntouched. The speculative decision nowlives and dies on the stack, so a failed ALTER leaves no trace. No other code
in the loop reads the generated bit, and the local needs no cleanup (unlike the
long-hash path), so the change is limited to these three sites.
Files changed
sql/sql_table.cc— replace the in-placeHA_GENERATED_KEYflag mutation inmysql_prepare_alter_table()with a localgenerated_keyvariable.mysql-test/main/alter_table_failed_rename_index.test— new regression test.mysql-test/main/alter_table_failed_rename_index.result— generated withmysql-test-run.pl --record.Release Notes
Resolved an issue where failed ALTER TABLE causes inconsistency, changes behavior of the next statement.
How can this PR be tested?
By running the following test case in MTR:
mysql-test/main/alter_table_failed_rename_index.testAdditionally, it is recommended to also test the following, as they are affected by this code path:
mysql-test/main/alter_table.testmysql-test/suite/innodb/t/innodb-alter.testmysql-test/suite/innodb/t/instant_alter.testmysql-test/suite/innodb/t/foreign_key.testManually, this can also be tested by following Elena's replication instructions in MDEV-35732 Description:
Should now result in:
Basing the PR against the correct MariaDB version
This fix is based against the
11.4branch.Copyright
All new code of the whole pull request, including one or several files that are either new files or modified ones, are contributed under the BSD-new license. I am contributing on behalf of my employer Amazon Web Services, Inc.