Skip to content

Commit

Permalink
Bug#35869747: Cannot drop index from upgraded instance
Browse files Browse the repository at this point in the history
Analysis

Tables of redundant row format having indexes of size greater than 767 bytes
cannot be accessed after a successful upgrade to 8.0

An index with size greater than 767 bytes was permitted to be created
on a table with redundant row format prior to 5.7.35 version. When
such tables are upgraded to 8.0, they became inaccessible. During open table
i.e dd_open_table(), the index size was validated resulting in an error since
the redundant row format does not support indexes greater than 767 bytes.

Fix

Mark such indexes of invalid size as corrupt during dd_open_table().
This involved moving the index size check to later point in time
in dd_open_table_one() i.e after dict_table_add_to_cache(). All
operations which involves using the index will error out as index
corrupted until the index is dropped.

Change-Id: Iff3915028f1d3631af2fe85669891144ab048856
  • Loading branch information
Nisha Gopalakrishnan authored and dahlerlend committed May 26, 2024
1 parent 7158b81 commit a79ed51
Show file tree
Hide file tree
Showing 7 changed files with 283 additions and 59 deletions.
Binary file added mysql-test/std_data/data841_long_index.zip
Binary file not shown.
93 changes: 93 additions & 0 deletions mysql-test/suite/innodb/r/row_format.result
Original file line number Diff line number Diff line change
Expand Up @@ -298,3 +298,96 @@ TABLE_ID = @TID
# CLEANUP
SET GLOBAL innodb_file_per_table = @orig_innodb_file_per_table;
DROP TABLE t_compressed;
#
# Bug#35869747: Cannot drop index from upgraded instance
#
# Upgrade from 8.4.1 having tables with redundant row format and
# index longer than supported 767 bytes.
# Stop the running the server.
# Unzip the datadir.
# Restart the server against the unzipped datadir.
# restart: --datadir=DATADIR --lower_case_table_names=1
# Verify tables after upgrade.
# Table with single index of invalid length.
SHOW CREATE TABLE test.t1;
Table Create Table
t1 CREATE TABLE `t1` (
`fld1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
KEY `idx1` (`fld1`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
# Table with composite index of invalid length.
SHOW CREATE TABLE test.t2;
Table Create Table
t2 CREATE TABLE `t2` (
`fld1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`fld2` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
KEY `idx1` (`fld1`,`fld2`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
# Table with two indexes of invalid length.
SHOW CREATE TABLE test.t3;
Table Create Table
t3 CREATE TABLE `t3` (
`fld1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`fld2` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
KEY `idx1` (`fld1`),
KEY `idx2` (`fld2`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
# Table with prefix index of invalid length.
SHOW CREATE TABLE test.t4;
Table Create Table
t4 CREATE TABLE `t4` (
`fld1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
KEY `idx1` (`fld1`(250))
) ENGINE=InnoDB DEFAULT CHARSET=latin1
# CHECK TABLE should flag indexes as corrupt after fix.
CHECK TABLE test.t1;
Table Op Msg_type Msg_text
test.t1 check Warning InnoDB: Index idx1 is marked as corrupted
test.t1 check error Corrupt
CHECK TABLE test.t2;
Table Op Msg_type Msg_text
test.t2 check Warning InnoDB: Index idx1 is marked as corrupted
test.t2 check error Corrupt
CHECK TABLE test.t3;
Table Op Msg_type Msg_text
test.t3 check Warning InnoDB: Index idx1 is marked as corrupted
test.t3 check Warning InnoDB: Index idx2 is marked as corrupted
test.t3 check error Corrupt
CHECK TABLE test.t4;
Table Op Msg_type Msg_text
test.t4 check Warning InnoDB: Index idx1 is marked as corrupted
test.t4 check error Corrupt
# TRUNCATE TABLE reports an index too long error since it is DROP + CREATE.
TRUNCATE TABLE test.t1;
ERROR HY000: Index column size too large. The maximum column size is 767 bytes.
TRUNCATE TABLE test.t2;
ERROR HY000: Index column size too large. The maximum column size is 767 bytes.
TRUNCATE TABLE test.t3;
ERROR HY000: Index column size too large. The maximum column size is 767 bytes.
TRUNCATE TABLE test.t4;
ERROR HY000: Index column size too large. The maximum column size is 767 bytes.
# SELECT statement which uses the index errors out flagging the corruption.
SELECT * FROM test.t1 FORCE INDEX (idx1);
ERROR HY000: Index idx1 is corrupted
SELECT * FROM test.t2 FORCE INDEX (idx1);
ERROR HY000: Index idx1 is corrupted
# SELECT statement which does not use the corrupted index succeeds.
SELECT * FROM test.t3;
fld1 fld2
t3abc t3efg
SELECT * FROM test.t4;
fld1
t4abc
# DROP INDEX succeeds after fix.
ALTER TABLE test.t1 DROP INDEX idx1;
ALTER TABLE test.t2 DROP INDEX idx1;
# DROP TABLE succeeds after fix.
DROP TABLE test.t3;
DROP TABLE test.t4;
# Cleanup
DROP TABLE test.t1;
DROP TABLE test.t2;
# Shutdown server
# Clean up data dir
# Restarting server to restore server state
# restart:
101 changes: 101 additions & 0 deletions mysql-test/suite/innodb/t/row_format.test
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,104 @@ SELECT TABLE_ID = @TID FROM INFORMATION_SCHEMA.INNODB_TABLES WHERE NAME LIKE "%t
--echo # CLEANUP
SET GLOBAL innodb_file_per_table = @orig_innodb_file_per_table;
DROP TABLE t_compressed;

--echo #
--echo # Bug#35869747: Cannot drop index from upgraded instance
--echo #

--disable_query_log
call mtr.add_suppression("\\[ERROR\\] \\[MY-014073\\] \\[InnoDB\\] Index idx[0-9] of test\\.t[0-9] exceeds limit of 767 bytes per column.");
--enable_query_log

--echo # Upgrade from 8.4.1 having tables with redundant row format and
--echo # index longer than supported 767 bytes.
# The data directory was created by using the following steps:
# 1. Using mysql-5.7.31 version:
# SET GLOBAL innodb_default_row_format=redundant;
# CREATE TABLE t1 ( fld1 varchar(255) CHARACTER SET utf8mb4);
# CREATE TABLE `t2` (`fld1` varchar(255) CHARACTER SET utf8mb4 default null,
# `fld2` varchar(255) CHARACTER SET utf8mb4 default null);
# CREATE TABLE `t3` (`fld1` varchar(255) CHARACTER SET utf8mb4 default null,
# `fld2` varchar(255) CHARACTER SET utf8mb4 default null);
# CREATE TABLE `t4` (`fld1` varchar(255) CHARACTER SET utf8mb4 default null);
# SET GLOBAL innodb_default_row_format=dynamic;
# CREATE INDEX idx1 ON t1(fld1);
# CREATE INDEX idx1 ON t2(fld1,fld2);
# CREATE INDEX idx1 ON t3(fld1);
# CREATE INDEX idx2 ON t3(fld2);
# CREATE INDEX idx1 ON t4(fld1(250));
# 2. Inplace upgrade to mysql-8.0.38.
# 3. Inplace upgrade to mysql-8.4.1.
# 4. Zip the data directory.

--echo # Stop the running the server.
--source include/shutdown_mysqld.inc

--echo # Unzip the datadir.
--exec unzip -qo $MYSQLTEST_VARDIR/std_data/data841_long_index.zip -d $MYSQL_TMP_DIR
let $DATADIR = $MYSQL_TMP_DIR/data;

--echo # Restart the server against the unzipped datadir.
--replace_result $DATADIR DATADIR
--let $restart_parameters = restart: --datadir=$DATADIR --lower_case_table_names=1
--let $wait_counter=3000
--source include/start_mysqld.inc

--echo # Verify tables after upgrade.

--echo # Table with single index of invalid length.
SHOW CREATE TABLE test.t1;
--echo # Table with composite index of invalid length.
SHOW CREATE TABLE test.t2;
--echo # Table with two indexes of invalid length.
SHOW CREATE TABLE test.t3;
--echo # Table with prefix index of invalid length.
SHOW CREATE TABLE test.t4;

--echo # CHECK TABLE should flag indexes as corrupt after fix.
CHECK TABLE test.t1;
CHECK TABLE test.t2;
CHECK TABLE test.t3;
CHECK TABLE test.t4;

--echo # TRUNCATE TABLE reports an index too long error since it is DROP + CREATE.
--error ER_INDEX_COLUMN_TOO_LONG
TRUNCATE TABLE test.t1;
--error ER_INDEX_COLUMN_TOO_LONG
TRUNCATE TABLE test.t2;
--error ER_INDEX_COLUMN_TOO_LONG
TRUNCATE TABLE test.t3;
--error ER_INDEX_COLUMN_TOO_LONG
TRUNCATE TABLE test.t4;

--echo # SELECT statement which uses the index errors out flagging the corruption.
--error ER_INDEX_CORRUPT
SELECT * FROM test.t1 FORCE INDEX (idx1);
--error ER_INDEX_CORRUPT
SELECT * FROM test.t2 FORCE INDEX (idx1);

--echo # SELECT statement which does not use the corrupted index succeeds.
SELECT * FROM test.t3;
SELECT * FROM test.t4;

--echo # DROP INDEX succeeds after fix.
ALTER TABLE test.t1 DROP INDEX idx1;
ALTER TABLE test.t2 DROP INDEX idx1;

--echo # DROP TABLE succeeds after fix.
DROP TABLE test.t3;
DROP TABLE test.t4;

--echo # Cleanup
DROP TABLE test.t1;
DROP TABLE test.t2;

--echo # Shutdown server
--source include/shutdown_mysqld.inc

--echo # Clean up data dir
--force-rmdir $MYSQL_TMP_DIR/data

--echo # Restarting server to restore server state
--let $restart_parameters = "restart:"
--source include/start_mysqld.inc
3 changes: 3 additions & 0 deletions share/messages_to_error_log.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12295,6 +12295,9 @@ ER_WAITING_FOR_NO_CONNECTIONS
ER_WAITING_FOR_NO_THDS
eng "Waiting for THDs in partition %u to be closed, %u still left. %u THDs left in total for all partitions."

ER_IB_INDEX_PART_TOO_LONG
eng "Index %s of %s.%s exceeds limit of %lu bytes per column."

#
# End of 8.0 error messages intended to be written to the server error log.
#
Expand Down
96 changes: 68 additions & 28 deletions storage/innobase/dict/dict0dd.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1862,6 +1862,23 @@ bool dd_instant_columns_consistent(const dd::Table &dd_table) {
}
#endif /* UNIV_DEBUG */

void dd_visit_keys_with_too_long_parts(
const TABLE *table, const size_t max_part_len,
std::function<void(const KEY &)> visitor) {
for (uint key_num = 0; key_num < table->s->keys; key_num++) {
const KEY &key = table->key_info[key_num];
if (!(key.flags & (HA_SPATIAL | HA_FULLTEXT))) {
for (unsigned i = 0; i < key.user_defined_key_parts; i++) {
const KEY_PART_INFO *key_part = &key.key_part[i];
if (max_part_len < key_part->length) {
visitor(key);
continue;
}
}
}
}
}

static void instant_update_table_cols_count(dict_table_t *dict_table,
uint32_t n_added_column,
uint32_t n_dropped_column) {
Expand Down Expand Up @@ -2768,6 +2785,34 @@ const dict_index_t *dd_find_index(const dict_table_t *table, Index *dd_index) {
}
MY_COMPILER_DIAGNOSTIC_POP()

/** Return the prefix length of the key.
@param[in] key Key information.
@param[in] key_part Key part information.
@return Key prefix length or 0 if whole column is indexed.
*/
static inline uint16_t get_index_prefix_len(const KEY &key,
const KEY_PART_INFO *key_part) {
if (key.flags & (HA_SPATIAL | HA_FULLTEXT)) {
return 0;
}

if (key_part->key_part_flag & HA_PART_KEY_SEG) {
ut_ad(key_part->length > 0);
return key_part->length;
}

#ifdef UNIV_DEBUG
auto field = key_part->field;
ut_ad((!is_blob(field->real_type()) &&
field->real_type() != MYSQL_TYPE_GEOMETRY) ||
key_part->length >= (field->type() == MYSQL_TYPE_VARCHAR
? field->key_length()
: field->pack_length()));
#endif

return 0;
}

template const dict_index_t *dd_find_index<dd::Index>(const dict_table_t *,
dd::Index *);
template const dict_index_t *dd_find_index<dd::Partition_index>(
Expand All @@ -2780,7 +2825,6 @@ template const dict_index_t *dd_find_index<dd::Partition_index>(
@param[in] key_num key_info[] offset
@return error code
@retval 0 on success
@retval HA_ERR_INDEX_COL_TOO_LONG if a column is too long
@retval HA_ERR_TOO_BIG_ROW if the record is too long */
[[nodiscard]] static int dd_fill_one_dict_index(const dd::Index *dd_index,
dict_table_t *table,
Expand Down Expand Up @@ -2831,7 +2875,6 @@ template const dict_index_t *dd_find_index<dd::Partition_index>(

for (unsigned i = 0; i < key.user_defined_key_parts; i++) {
const KEY_PART_INFO *key_part = &key.key_part[i];
unsigned prefix_len = 0;
const Field *field = key_part->field;
ut_ad(field == form->field[key_part->fieldnr - 1]);
ut_ad(field == form->field[field->field_index()]);
Expand All @@ -2853,32 +2896,7 @@ template const dict_index_t *dd_find_index<dd::Partition_index>(
is_asc = false;
}

if (key.flags & HA_SPATIAL) {
prefix_len = 0;
} else if (key.flags & HA_FULLTEXT) {
prefix_len = 0;
} else if (key_part->key_part_flag & HA_PART_KEY_SEG) {
/* SPATIAL and FULLTEXT index always are on
full columns. */
ut_ad(!(key.flags & (HA_SPATIAL | HA_FULLTEXT)));
prefix_len = key_part->length;
ut_ad(prefix_len > 0);
} else {
ut_ad(key.flags & (HA_SPATIAL | HA_FULLTEXT) ||
(!is_blob(field->real_type()) &&
field->real_type() != MYSQL_TYPE_GEOMETRY) ||
key_part->length >= (field->type() == MYSQL_TYPE_VARCHAR
? field->key_length()
: field->pack_length()));
prefix_len = 0;
}

if ((key_part->length > max_len || prefix_len > max_len) &&
!(key.flags & (HA_FULLTEXT))) {
dict_mem_index_free(index);
my_error(ER_INDEX_COLUMN_TOO_LONG, MYF(0), max_len);
return HA_ERR_INDEX_COL_TOO_LONG;
}
const auto prefix_len = get_index_prefix_len(key, key_part);

dict_col_t *col = nullptr;

Expand Down Expand Up @@ -3453,6 +3471,26 @@ void get_field_types(const dd::Table *dd_tab, const dict_table_t *m_table,
}
}

/** Check if the individual parts of the composite index does not exceed the
limit based on the table row format. If yes, mark the index as corrupt.
@param[in] m_table InnoDB table handle
@param[in] table MySQL table definition */
static void validate_index_len(dict_table_t *m_table, const TABLE *table) {
const uint32_t max_part_len = DICT_MAX_FIELD_LEN_BY_FORMAT(m_table);
dd_visit_keys_with_too_long_parts(table, max_part_len, [&](const KEY &key) {
dict_index_t *index = dict_table_get_index_on_name(m_table, key.name, true);
if (index != nullptr) {
std::string schema_name;
std::string table_name;

dict_set_corrupted(index);
m_table->get_table_name(schema_name, table_name);
ib::error(ER_IB_INDEX_PART_TOO_LONG, key.name, schema_name.c_str(),
table_name.c_str(), ulong{max_part_len});
}
});
}

template <typename Table>
static inline void fill_dict_existing_column(
const Table *dd_tab, const TABLE *m_form, dict_table_t *m_table,
Expand Down Expand Up @@ -5096,6 +5134,8 @@ dict_table_t *dd_open_table_one(dd::cache::Dictionary_client *client,
} else {
dict_table_add_to_cache(m_table, true);

validate_index_len(m_table, table);

if (m_table->fts && dict_table_has_fts_index(m_table)) {
fts_optimize_add_table(m_table);
}
Expand Down
Loading

0 comments on commit a79ed51

Please sign in to comment.