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:
None 
Category:MySQL Server: DDL Severity:S3 (Non-critical)
Version:9.7.2 OS:Any
Assigned to: CPU Architecture:Any

[19 Aug 4:50] George Ma
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
```
[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.