| Bug #121120 | INPLACE utf8mb3-to-utf8mb4 conversion corrupts unindexed CHAR in REDUNDANT tables | ||
|---|---|---|---|
| Submitted: | 19 Aug 4:50 | Modified: | 19 Aug 4:51 |
| Reporter: | George Ma (OCA) | Email Updates: | |
| Status: | Open | Impact on me: | |
| Category: | MySQL Server: DDL | Severity: | S3 (Non-critical) |
| Version: | 9.7.2 | OS: | Any |
| Assigned to: | CPU Architecture: | Any | |
[19 Aug 4:51]
George Ma
The InnoDB row-format documentation states that fixed-length character columns in `REDUNDANT` format use the maximum byte length of the character set. Therefore `CHAR(20)` requires 60 bytes under `utf8mb3` and 80 bytes under `utf8mb4`; changing the character set changes the physical fixed-field size even though the character count remains 20. The MySQL 9.7.2 source tag `mysql-9.7.2` (commit `008e09c2834b98143a8c067d4d225c90953050cf`) appears to classify this change as metadata-only for the reproducer: - `Field_str::is_equal()` in `sql/field.cc:6161-6179` returns `IS_EQUAL_PACK_LENGTH` when the character set differs after comparing the maximum display width in code points, without comparing the maximum byte width. - `sql/sql_table.cc:13128-13148` maps that result to `ALTER_COLUMN_EQUAL_PACK_LENGTH` when the column is neither indexed nor used by a partitioning expression. - `INNOBASE_ALTER_NOREBUILD` explicitly includes `ALTER_COLUMN_EQUAL_PACK_LENGTH` in `storage/innobase/handler/handler0alter.cc:170-180`; it is not part of `INNOBASE_ALTER_REBUILD` at `handler0alter.cc:127-142`. - During the no-rebuild commit path, `innobase_rename_or_enlarge_columns_cache()` at `handler0alter.cc:6534-6586` updates `col->len` to `cf->max_display_width_in_bytes()`. The cache is committed through this path at `handler0alter.cc:7763-7768`. Consequently, the dictionary is changed from 60 to 80 bytes without rebuilding the pre-existing REDUNDANT record. The `Field 3 len is 60, should be 80` diagnostic and the corrupted PRIMARY B-tree reported by `CHECK TABLE` match that sequence. This is distinct from [Bug #121115](https://bugs.mysql.com/bug.php?id=121115), which requires an online rebuild from DYNAMIC to REDUNDANT plus a concurrent row-log insert and fails in row-log record conversion. The reproduction above starts and remains in REDUNDANT format, uses no concurrent DML, and reaches the no-rebuild metadata commit path instead.

Description: MySQL Community Server 9.7.2 can leave an InnoDB table physically inconsistent when an unindexed `CHAR(N)` column in `ROW_FORMAT=REDUNDANT` is changed from `utf8mb3` to `utf8mb4` with the same character length and `ALGORITHM=INPLACE`. The ALTER succeeds, but old clustered-index records are not rewritten. Their fixed field remains 3 * N bytes, while the new InnoDB metadata expects 4 * N bytes. A subsequent `CHECK TABLE` reports that the PRIMARY B-tree is corrupted. This was reproduced twice in independent fresh schemas on the official Oracle MySQL Community Server 9.7.2 Linux generic minimal binary (`MySQL Community Server - GPL`), started with `--no-defaults --skip-networking --mysqlx=0`. The server was running on Linux 5.10.134-007.ali5000.al8.x86_64, x86_64. The downloaded archive matched Oracle's published MD5 (`fd5dacf5177894fbe527f0cce132147e`) and its detached signature verified against the MySQL Release Engineering 2025 signing key. Expected result: a successful ALTER must leave `CHECK TABLE` reporting `status OK`. If this conversion cannot be performed without rewriting the records, `ALGORITHM=INPLACE` must be rejected before metadata is committed. Actual result: the ALTER succeeds, and `CHECK TABLE` reports corruption. The error log contains `Field 3 len is 60, should be 80` for the affected record. How to repeat: Run the following on a fresh MySQL Community Server 9.7.2 instance: ```sql DROP DATABASE IF EXISTS bug_repro; CREATE DATABASE bug_repro; USE bug_repro; SET NAMES utf8mb4; CREATE TABLE t1 ( id INT NOT NULL, c CHAR(20) CHARACTER SET utf8mb3 NOT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB ROW_FORMAT=REDUNDANT; INSERT INTO t1 VALUES (1, REPEAT('δΈ', 20)); ALTER TABLE t1 MODIFY c CHAR(20) CHARACTER SET utf8mb4 NOT NULL, ALGORITHM=INPLACE; CHECK TABLE t1; ``` Actual output: ``` Table Op Msg_type Msg_text bug_repro.t1 check Warning InnoDB: The B-tree of index PRIMARY is corrupted. bug_repro.t1 check error Corrupt ```