From 3a3d5ba2356b85626d34d6a65e4d8e8e6205b60d Mon Sep 17 00:00:00 2001 From: Eugene Kosov Date: Thu, 14 Mar 2019 17:28:20 +0300 Subject: [PATCH] MDEV-13301 Optimize DROP INDEX, ADD INDEX into RENAME INDEX Just rename index in data dictionary and in InnoDB cache when it's possible. Introduce ALTER_INDEX_RENAME for that purpose so that engines can optimize such operation. Unused code between macro MYSQL_RENAME_INDEX was removed. compare_keys_but_name(): compare index definitions except for index names Alter_inplace_info::rename_keys: ha_innobase_inplace_ctx::rename_keys: vector of rename indexes fill_alter_inplace_info():: fills Alter_inplace_info::rename_keys --- .../suite/innodb/r/alter_rename_files.result | 3 +- .../suite/innodb/r/innodb-index-online.result | 4 +- .../r/instant_alter_index_rename.result | 178 +++++++++++++ .../suite/innodb/t/alter_rename_files.test | 2 +- .../suite/innodb/t/innodb-index-online.test | 5 +- .../innodb/t/instant_alter_index_rename.test | 186 ++++++++++++++ sql/handler.cc | 23 ++ sql/handler.h | 44 ++-- sql/mem_root_array.h | 3 + sql/sql_table.cc | 241 ++++++++++-------- storage/innobase/dict/dict0stats.cc | 57 +++++ storage/innobase/handler/handler0alter.cc | 231 ++++++++++++++--- storage/innobase/include/dict0stats.h | 13 + 13 files changed, 820 insertions(+), 170 deletions(-) create mode 100644 mysql-test/suite/innodb/r/instant_alter_index_rename.result create mode 100644 mysql-test/suite/innodb/t/instant_alter_index_rename.test diff --git a/mysql-test/suite/innodb/r/alter_rename_files.result b/mysql-test/suite/innodb/r/alter_rename_files.result index 7df63a051da..490f6773765 100644 --- a/mysql-test/suite/innodb/r/alter_rename_files.result +++ b/mysql-test/suite/innodb/r/alter_rename_files.result @@ -2,7 +2,7 @@ CREATE TABLE t1 (x INT NOT NULL UNIQUE KEY) ENGINE=InnoDB; INSERT INTO t1 VALUES(5); SET GLOBAL innodb_log_checkpoint_now=TRUE; SET DEBUG_SYNC='commit_cache_rebuild SIGNAL ready WAIT_FOR finish'; -ALTER TABLE t1 ADD PRIMARY KEY(x); +ALTER TABLE t1 FORCE;; connect con1,localhost,root,,; SET DEBUG_SYNC='now WAIT_FOR ready'; SET GLOBAL innodb_log_checkpoint_now=TRUE; @@ -13,7 +13,6 @@ SHOW CREATE TABLE t1; Table Create Table t1 CREATE TABLE `t1` ( `x` int(11) NOT NULL, - PRIMARY KEY (`x`), UNIQUE KEY `x` (`x`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 DROP TABLE t1; diff --git a/mysql-test/suite/innodb/r/innodb-index-online.result b/mysql-test/suite/innodb/r/innodb-index-online.result index d21dc21c61e..8eebece46b5 100644 --- a/mysql-test/suite/innodb/r/innodb-index-online.result +++ b/mysql-test/suite/innodb/r/innodb-index-online.result @@ -236,8 +236,7 @@ WHERE variable_name = 'innodb_encryption_n_rowlog_blocks_encrypted'); connection con1; SET DEBUG_SYNC = 'row_log_apply_before SIGNAL c2e_created WAIT_FOR dml2_done'; SET lock_wait_timeout = 10; -ALTER TABLE t1 DROP INDEX c2d, ADD INDEX c2e(c2), -ALGORITHM = INPLACE; +ALTER TABLE t1 CHANGE c2 c22 INT, DROP INDEX c2d, ADD INDEX c2e(c22, c3(10)), ALGORITHM = NOCOPY; connection default; INSERT INTO t1 SELECT 80 + c1, c2, c3 FROM t1; INSERT INTO t1 SELECT 160 + c1, c2, c3 FROM t1; @@ -295,6 +294,7 @@ INNER JOIN INFORMATION_SCHEMA.INNODB_SYS_FIELDS sf ON si.index_id = sf.index_id WHERE si.name = '?c2e'; name pos c2 0 +c3 1 SET @merge_encrypt_1= (SELECT variable_value FROM information_schema.global_status WHERE variable_name = 'innodb_encryption_n_merge_blocks_encrypted'); diff --git a/mysql-test/suite/innodb/r/instant_alter_index_rename.result b/mysql-test/suite/innodb/r/instant_alter_index_rename.result new file mode 100644 index 00000000000..93bbf6ee193 --- /dev/null +++ b/mysql-test/suite/innodb/r/instant_alter_index_rename.result @@ -0,0 +1,178 @@ +create function get_index_id(tbl_id int, index_name char(100)) +returns int +begin +declare res int; +select index_id into res from information_schema.innodb_sys_indexes where +name=index_name and table_id = tbl_id; +return res; +end| +create table t ( +pk int primary key, +a int, +b int, +c int, +unique index a_key (a), +key c_key (c) +) engine=innodb stats_persistent=1; +insert into t values (1, 1, 1, 1); +set @table_id = (select table_id from information_schema.innodb_sys_tables where name='test/t'); +set @a_key_id = get_index_id(@table_id, 'a_key'); +set @c_key_id = get_index_id(@table_id, 'c_key'); +set @primary_id = get_index_id(@table_id, 'primary'); +select distinct(index_name) from mysql.innodb_index_stats where table_name = 't'; +index_name +PRIMARY +a_key +c_key +alter table t +drop index a_key, +add unique index a_key_strikes_back (a); +select distinct(index_name) from mysql.innodb_index_stats where table_name = 't'; +index_name +PRIMARY +a_key_strikes_back +c_key +check table t; +Table Op Msg_type Msg_text +test.t check status OK +select @a_key_id = get_index_id(@table_id, 'a_key_strikes_back'), +@c_key_id = get_index_id(@table_id, 'c_key'), +@primary_id = get_index_id(@table_id, 'primary'); +@a_key_id = get_index_id(@table_id, 'a_key_strikes_back') @c_key_id = get_index_id(@table_id, 'c_key') @primary_id = get_index_id(@table_id, 'primary') +1 1 1 +set @a_key_strikes_back_id = get_index_id(@table_id, 'a_key_strikes_back'); +set @c_key_id = get_index_id(@table_id, 'c_key'); +set @primary_id = get_index_id(@table_id, 'primary'); +alter table t +drop index a_key_strikes_back, +add unique index a_key_returns (a), +drop primary key, +add primary key (pk), +add unique index b_key (b); +check table t; +Table Op Msg_type Msg_text +test.t check status OK +select @a_key_strikes_back_id = get_index_id(@table_id, 'a_key_returns'), +@c_key_id = get_index_id(@table_id, 'c_key'), +@primary_id = get_index_id(@table_id, 'primary'); +@a_key_strikes_back_id = get_index_id(@table_id, 'a_key_returns') @c_key_id = get_index_id(@table_id, 'c_key') @primary_id = get_index_id(@table_id, 'primary') +1 1 1 +set @a_key_returns_id = get_index_id(@table_id, 'a_key_returns'); +set @b_key_id = get_index_id(@table_id, 'b_key'); +set @c_key_id = get_index_id(@table_id, 'c_key'); +set @primary_id = get_index_id(@table_id, 'primary'); +alter table t +drop key c_key, +add key c_key2 (c); +check table t; +Table Op Msg_type Msg_text +test.t check status OK +select @a_key_returns_id = get_index_id(@table_id, 'a_key_returns'), +@b_key_id = get_index_id(@table_id, 'b_key'), +@c_key_id = get_index_id(@table_id, 'c_key2'), +@primary_id = get_index_id(@table_id, 'primary'); +@a_key_returns_id = get_index_id(@table_id, 'a_key_returns') @b_key_id = get_index_id(@table_id, 'b_key') @c_key_id = get_index_id(@table_id, 'c_key2') @primary_id = get_index_id(@table_id, 'primary') +1 1 1 1 +drop table t; +drop function get_index_id; +create table errors ( +a int, +unique key a_key (a), +b int +) engine=innodb; +alter table errors +drop key a_key, +drop key a_key, +add unique key a_key2 (a); +ERROR 42000: Can't DROP INDEX `a_key`; check that it exists +alter table errors +drop key a_key, +drop key a_key2, +add unique key a_key2 (a); +ERROR 42000: Can't DROP INDEX `a_key2`; check that it exists +alter table errors +add key b_key (b), +drop key b_key, +add key bb_key (b); +ERROR 42000: Can't DROP INDEX `b_key`; check that it exists +alter table errors +drop key a_key, +add key a_key2 (a), +drop key a_key, +add key a_key2 (a); +ERROR 42000: Can't DROP INDEX `a_key`; check that it exists +drop table errors; +create table corrupted ( +a int, +key a_key (a) +) engine=innodb; +insert into corrupted values (1); +select * from corrupted; +a +1 +SET @save_dbug = @@SESSION.debug_dbug; +SET debug_dbug = '+d,dict_set_index_corrupted'; +check table corrupted; +Table Op Msg_type Msg_text +test.corrupted check Warning InnoDB: Index a_key is marked as corrupted +test.corrupted check error Corrupt +SET debug_dbug = @save_dbug; +select * from corrupted; +ERROR HY000: Index corrupted is corrupted +alter table corrupted +drop key a_key, +add key a_key2 (a); +ERROR HY000: Index a_key is corrupted +alter table corrupted +drop key a_key; +select * from corrupted; +a +1 +check table corrupted; +Table Op Msg_type Msg_text +test.corrupted check status OK +drop table corrupted; +create table t ( +a int, +unique key a_key (a) +) engine=innodb stats_persistent=1; +SET @save_dbug = @@SESSION.debug_dbug; +SET debug_dbug = '+d,ib_rename_index_fail1'; +alter table t +drop key a_key, +add unique key a_key2 (a), +algorithm=instant; +ERROR 40001: Deadlock found when trying to get lock; try restarting transaction +SET debug_dbug = @save_dbug; +alter table t +drop key a_key, +add unique key `GEN_CLUST_INDEX` (a), +algorithm=instant; +ERROR 42000: Incorrect index name 'GEN_CLUST_INDEX' +show create table t; +Table Create Table +t CREATE TABLE `t` ( + `a` int(11) DEFAULT NULL, + UNIQUE KEY `a_key` (`a`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1 STATS_PERSISTENT=1 +drop table t; +create table rename_column_and_index ( +a int, +unique index a_key(a) +) engine=innodb; +insert into rename_column_and_index values (1), (3); +alter table rename_column_and_index +change a aa int, +drop key a_key, +add unique key aa_key(aa), +algorithm=instant; +show create table rename_column_and_index; +Table Create Table +rename_column_and_index CREATE TABLE `rename_column_and_index` ( + `aa` int(11) DEFAULT NULL, + UNIQUE KEY `aa_key` (`aa`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1 +check table rename_column_and_index; +Table Op Msg_type Msg_text +test.rename_column_and_index check status OK +drop table rename_column_and_index; diff --git a/mysql-test/suite/innodb/t/alter_rename_files.test b/mysql-test/suite/innodb/t/alter_rename_files.test index 3ed1cb5d9fa..27408320f7d 100644 --- a/mysql-test/suite/innodb/t/alter_rename_files.test +++ b/mysql-test/suite/innodb/t/alter_rename_files.test @@ -11,7 +11,7 @@ SET GLOBAL innodb_log_checkpoint_now=TRUE; # Start an ALTER TABLE and stop it before renaming the files SET DEBUG_SYNC='commit_cache_rebuild SIGNAL ready WAIT_FOR finish'; ---send ALTER TABLE t1 ADD PRIMARY KEY(x) +--send ALTER TABLE t1 FORCE; connect (con1,localhost,root,,); diff --git a/mysql-test/suite/innodb/t/innodb-index-online.test b/mysql-test/suite/innodb/t/innodb-index-online.test index 4cdbdb7c584..5e21fa896a4 100644 --- a/mysql-test/suite/innodb/t/innodb-index-online.test +++ b/mysql-test/suite/innodb/t/innodb-index-online.test @@ -225,10 +225,7 @@ SET DEBUG_SYNC = 'row_log_apply_before SIGNAL c2e_created WAIT_FOR dml2_done'; # Ensure that the ALTER TABLE will be executed even with some concurrent DML. SET lock_wait_timeout = 10; --send -# FIXME: MDEV-13668 -#ALTER TABLE t1 CHANGE c2 c22 INT, DROP INDEX c2d, ADD INDEX c2e(c22), -ALTER TABLE t1 DROP INDEX c2d, ADD INDEX c2e(c2), -ALGORITHM = INPLACE; +ALTER TABLE t1 CHANGE c2 c22 INT, DROP INDEX c2d, ADD INDEX c2e(c22, c3(10)), ALGORITHM = NOCOPY; # Generate some log (delete-mark, delete-unmark, insert etc.) # while the index creation is blocked. Some of this may run diff --git a/mysql-test/suite/innodb/t/instant_alter_index_rename.test b/mysql-test/suite/innodb/t/instant_alter_index_rename.test new file mode 100644 index 00000000000..3150503c815 --- /dev/null +++ b/mysql-test/suite/innodb/t/instant_alter_index_rename.test @@ -0,0 +1,186 @@ +--source include/have_innodb.inc +--source include/have_debug.inc + +delimiter |; +create function get_index_id(tbl_id int, index_name char(100)) + returns int +begin + declare res int; + select index_id into res from information_schema.innodb_sys_indexes where + name=index_name and table_id = tbl_id; + return res; +end| + +delimiter ;| + +create table t ( + pk int primary key, + a int, + b int, + c int, + unique index a_key (a), + key c_key (c) +) engine=innodb stats_persistent=1; + +insert into t values (1, 1, 1, 1); + +set @table_id = (select table_id from information_schema.innodb_sys_tables where name='test/t'); + +set @a_key_id = get_index_id(@table_id, 'a_key'); +set @c_key_id = get_index_id(@table_id, 'c_key'); +set @primary_id = get_index_id(@table_id, 'primary'); + +select distinct(index_name) from mysql.innodb_index_stats where table_name = 't'; +alter table t + drop index a_key, + add unique index a_key_strikes_back (a); +select distinct(index_name) from mysql.innodb_index_stats where table_name = 't'; + +check table t; +select @a_key_id = get_index_id(@table_id, 'a_key_strikes_back'), + @c_key_id = get_index_id(@table_id, 'c_key'), + @primary_id = get_index_id(@table_id, 'primary'); + +set @a_key_strikes_back_id = get_index_id(@table_id, 'a_key_strikes_back'); +set @c_key_id = get_index_id(@table_id, 'c_key'); +set @primary_id = get_index_id(@table_id, 'primary'); + +alter table t + drop index a_key_strikes_back, + add unique index a_key_returns (a), + drop primary key, + add primary key (pk), + add unique index b_key (b); + +check table t; +select @a_key_strikes_back_id = get_index_id(@table_id, 'a_key_returns'), + @c_key_id = get_index_id(@table_id, 'c_key'), + @primary_id = get_index_id(@table_id, 'primary'); + +set @a_key_returns_id = get_index_id(@table_id, 'a_key_returns'); +set @b_key_id = get_index_id(@table_id, 'b_key'); +set @c_key_id = get_index_id(@table_id, 'c_key'); +set @primary_id = get_index_id(@table_id, 'primary'); + +alter table t + drop key c_key, + add key c_key2 (c); + +check table t; +select @a_key_returns_id = get_index_id(@table_id, 'a_key_returns'), + @b_key_id = get_index_id(@table_id, 'b_key'), + @c_key_id = get_index_id(@table_id, 'c_key2'), + @primary_id = get_index_id(@table_id, 'primary'); + +drop table t; +drop function get_index_id; + +create table errors ( + a int, + unique key a_key (a), + b int +) engine=innodb; + +--error ER_CANT_DROP_FIELD_OR_KEY +alter table errors + drop key a_key, + drop key a_key, + add unique key a_key2 (a); + +--error ER_CANT_DROP_FIELD_OR_KEY +alter table errors + drop key a_key, + drop key a_key2, + add unique key a_key2 (a); + +--error ER_CANT_DROP_FIELD_OR_KEY +alter table errors + add key b_key (b), + drop key b_key, + add key bb_key (b); + +--error ER_CANT_DROP_FIELD_OR_KEY +alter table errors + drop key a_key, + add key a_key2 (a), + drop key a_key, + add key a_key2 (a); + +drop table errors; + +--disable_query_log +call mtr.add_suppression("Flagged corruption of `a_key` in table `test`.`corrupted` in dict_set_index_corrupted"); +--enable_query_log + +create table corrupted ( + a int, + key a_key (a) +) engine=innodb; + +insert into corrupted values (1); + +select * from corrupted; + +SET @save_dbug = @@SESSION.debug_dbug; +SET debug_dbug = '+d,dict_set_index_corrupted'; +check table corrupted; +SET debug_dbug = @save_dbug; + +--error ER_INDEX_CORRUPT +select * from corrupted; + +--error ER_INDEX_CORRUPT +alter table corrupted + drop key a_key, + add key a_key2 (a); + +alter table corrupted + drop key a_key; + +select * from corrupted; + +check table corrupted; + +drop table corrupted; + +create table t ( + a int, + unique key a_key (a) +) engine=innodb stats_persistent=1; + +SET @save_dbug = @@SESSION.debug_dbug; +SET debug_dbug = '+d,ib_rename_index_fail1'; +-- error ER_LOCK_DEADLOCK +alter table t + drop key a_key, + add unique key a_key2 (a), + algorithm=instant; +SET debug_dbug = @save_dbug; + +--error ER_WRONG_NAME_FOR_INDEX +alter table t + drop key a_key, + add unique key `GEN_CLUST_INDEX` (a), + algorithm=instant; + +show create table t; + +drop table t; + + +create table rename_column_and_index ( + a int, + unique index a_key(a) +) engine=innodb; + +insert into rename_column_and_index values (1), (3); + +alter table rename_column_and_index + change a aa int, + drop key a_key, + add unique key aa_key(aa), + algorithm=instant; + +show create table rename_column_and_index; +check table rename_column_and_index; +drop table rename_column_and_index; diff --git a/sql/handler.cc b/sql/handler.cc index decc9cfd8b4..917a5a56c6d 100644 --- a/sql/handler.cc +++ b/sql/handler.cc @@ -4775,6 +4775,29 @@ handler::check_if_supported_inplace_alter(TABLE *altered_table, DBUG_RETURN(HA_ALTER_INPLACE_NOT_SUPPORTED); } +Alter_inplace_info::Alter_inplace_info(HA_CREATE_INFO *create_info_arg, + Alter_info *alter_info_arg, + KEY *key_info_arg, uint key_count_arg, + partition_info *modified_part_info_arg, + bool ignore_arg) + : create_info(create_info_arg), + alter_info(alter_info_arg), + key_info_buffer(key_info_arg), + key_count(key_count_arg), + index_drop_count(0), + index_drop_buffer(nullptr), + index_add_count(0), + index_add_buffer(nullptr), + rename_keys(current_thd->mem_root), + handler_ctx(nullptr), + group_commit_ctx(nullptr), + handler_flags(0), + modified_part_info(modified_part_info_arg), + ignore(ignore_arg), + online(false), + unsupported_reason(nullptr) + {} + void Alter_inplace_info::report_unsupported_error(const char *not_supported, const char *try_instead) const { diff --git a/sql/handler.h b/sql/handler.h index d7559a21445..676632113c3 100644 --- a/sql/handler.h +++ b/sql/handler.h @@ -43,6 +43,7 @@ #include #include #include "sql_sequence.h" +#include "mem_root_array.h" class Alter_info; class Virtual_column_info; @@ -635,6 +636,8 @@ typedef ulonglong alter_table_operations; #define ALTER_RECREATE (1ULL << 10) // Set for CONVERT TO #define ALTER_CONVERT_TO (1ULL << 11) +// Set for DROP ... ADD some_index +#define ALTER_RENAME_INDEX (1ULL << 12) // Set for ADD FOREIGN KEY #define ALTER_ADD_FOREIGN_KEY (1ULL << 21) // Set for DROP FOREIGN KEY @@ -2367,6 +2370,29 @@ public: */ uint *index_add_buffer; + /** + Old and new index names. Used for index rename. + */ + struct Rename_key_pair + { + Rename_key_pair(const KEY *old_key, const KEY *new_key) + : old_key(old_key), new_key(new_key) + { + } + const KEY *old_key; + const KEY *new_key; + }; + /** + Vector of key pairs from DROP/ADD index which can be renamed. + */ + typedef Mem_root_array Rename_keys_vector; + + /** + A list of indexes which should be renamed. + Index definitions stays the same. + */ + Rename_keys_vector rename_keys; + /** Context information to allow handlers to keep context between in-place alter API calls. @@ -2428,23 +2454,7 @@ public: Alter_info *alter_info_arg, KEY *key_info_arg, uint key_count_arg, partition_info *modified_part_info_arg, - bool ignore_arg) - : create_info(create_info_arg), - alter_info(alter_info_arg), - key_info_buffer(key_info_arg), - key_count(key_count_arg), - index_drop_count(0), - index_drop_buffer(NULL), - index_add_count(0), - index_add_buffer(NULL), - handler_ctx(NULL), - group_commit_ctx(NULL), - handler_flags(0), - modified_part_info(modified_part_info_arg), - ignore(ignore_arg), - online(false), - unsupported_reason(NULL) - {} + bool ignore_arg); ~Alter_inplace_info() { diff --git a/sql/mem_root_array.h b/sql/mem_root_array.h index 5daeedadcba..bf266a60334 100644 --- a/sql/mem_root_array.h +++ b/sql/mem_root_array.h @@ -87,9 +87,11 @@ public: // Returns a pointer to the first element in the array. Element_type *begin() { return &m_array[0]; } + const Element_type *begin() const { return &m_array[0]; } // Returns a pointer to the past-the-end element in the array. Element_type *end() { return &m_array[size()]; } + const Element_type *end() const { return &m_array[size()]; } // Erases all of the elements. void clear() @@ -226,6 +228,7 @@ public: size_t element_size() const { return sizeof(Element_type); } bool empty() const { return size() == 0; } size_t size() const { return m_size; } + const MEM_ROOT *mem_root() const { return m_root; } private: MEM_ROOT *const m_root; diff --git a/sql/sql_table.cc b/sql/sql_table.cc index 34b03b952c0..f516d24bdb6 100644 --- a/sql/sql_table.cc +++ b/sql/sql_table.cc @@ -6502,31 +6502,101 @@ remove_key: } -/** - Get Create_field object for newly created table by field index. - - @param alter_info Alter_info describing newly created table. - @param idx Field index. -*/ - -static Create_field *get_field_by_index(Alter_info *alter_info, uint idx) -{ - List_iterator_fast field_it(alter_info->create_list); - uint field_idx= 0; - Create_field *field; - - while ((field= field_it++) && field_idx < idx) - { field_idx++; } - - return field; -} - - static int compare_uint(const uint *s, const uint *t) { return (*s < *t) ? -1 : ((*s > *t) ? 1 : 0); } +enum class Compare_keys +{ + Equal, + EqualButKeyPartLength, + NotEqual +}; + +Compare_keys compare_keys_but_name(const KEY *table_key, const KEY *new_key, + Alter_info *alter_info, const TABLE *table, + const KEY *const new_pk, + const KEY *const old_pk) +{ + Compare_keys result= Compare_keys::Equal; + + if ((table_key->algorithm != new_key->algorithm) || + ((table_key->flags & HA_KEYFLAG_MASK) != + (new_key->flags & HA_KEYFLAG_MASK)) || + (table_key->user_defined_key_parts != new_key->user_defined_key_parts)) + return Compare_keys::NotEqual; + + if (table_key->block_size != new_key->block_size) + return Compare_keys::NotEqual; + + if (engine_options_differ(table_key->option_struct, new_key->option_struct, + table->file->ht->index_options)) + return Compare_keys::NotEqual; + + const KEY_PART_INFO *end= + table_key->key_part + table_key->user_defined_key_parts; + for (const KEY_PART_INFO *key_part= table_key->key_part, + *new_part= new_key->key_part; + key_part < end; key_part++, new_part++) + { + Create_field *new_field= alter_info->create_list.elem(new_part->fieldnr); + const Field *old_field= table->field[key_part->fieldnr - 1]; + /* + If there is a change in index length due to column expansion + like varchar(X) changed to varchar(X + N) and has a compatible + packed data representation, we mark it for fast/INPLACE change + in index definition. InnoDB supports INPLACE for this cases + + Key definition has changed if we are using a different field or + if the user key part length is different. + */ + auto old_field_len= old_field->pack_length(); + + if (old_field->type() == MYSQL_TYPE_VARCHAR) + { + old_field_len= (old_field->pack_length() - + ((Field_varstring *) old_field)->length_bytes); + } + + if (key_part->length == old_field_len && + key_part->length < new_part->length && + (key_part->field->is_equal((Create_field *) new_field) == + IS_EQUAL_PACK_LENGTH)) + { + result= Compare_keys::EqualButKeyPartLength; + } + else if (key_part->length != new_part->length) + return Compare_keys::NotEqual; + + /* + For prefix keys KEY_PART_INFO::field points to cloned Field + object with adjusted length. So below we have to check field + indexes instead of simply comparing pointers to Field objects. + */ + if (!new_field->field || + new_field->field->field_index != key_part->fieldnr - 1) + return Compare_keys::NotEqual; + } + + /* + Rebuild the index if following condition get satisfied: + + (i) Old table doesn't have primary key, new table has it and vice-versa + (ii) Primary key changed to another existing index +*/ + if ((new_key == new_pk) != (table_key == old_pk)) + return Compare_keys::NotEqual; + + /* Check that key comment is not changed. */ + if (table_key->comment.length != new_key->comment.length || + (table_key->comment.length && + memcmp(table_key->comment.str, new_key->comment.str, + table_key->comment.length) != 0)) + return Compare_keys::NotEqual; + + return result; +} /** Compare original and new versions of a table and fill Alter_inplace_info @@ -6573,21 +6643,21 @@ static int compare_uint(const uint *s, const uint *t) static bool fill_alter_inplace_info(THD *thd, TABLE *table, bool varchar, Alter_inplace_info *ha_alter_info) { - Field **f_ptr, *field, *old_field; + Field **f_ptr, *field; List_iterator_fast new_field_it; Create_field *new_field; - KEY_PART_INFO *key_part, *new_part; - KEY_PART_INFO *end; Alter_info *alter_info= ha_alter_info->alter_info; DBUG_ENTER("fill_alter_inplace_info"); DBUG_PRINT("info", ("alter_info->flags: %llu", alter_info->flags)); /* Allocate result buffers. */ + DBUG_ASSERT(ha_alter_info->rename_keys.mem_root() == thd->mem_root); if (! (ha_alter_info->index_drop_buffer= (KEY**) thd->alloc(sizeof(KEY*) * table->s->keys)) || ! (ha_alter_info->index_add_buffer= (uint*) thd->alloc(sizeof(uint) * - alter_info->key_list.elements))) + alter_info->key_list.elements)) || + ha_alter_info->rename_keys.reserve(ha_alter_info->index_add_count)) DBUG_RETURN(true); /* @@ -6871,7 +6941,6 @@ static bool fill_alter_inplace_info(THD *thd, TABLE *table, bool varchar, Go through keys and check if the original ones are compatible with new table. */ - uint old_field_len= 0; KEY *table_key; KEY *table_key_end= table->key_info + table->s->keys; KEY *new_key; @@ -6918,88 +6987,18 @@ static bool fill_alter_inplace_info(THD *thd, TABLE *table, bool varchar, continue; } - /* Check that the key types are compatible between old and new tables. */ - if ((table_key->algorithm != new_key->algorithm) || - ((table_key->flags & HA_KEYFLAG_MASK) != - (new_key->flags & HA_KEYFLAG_MASK)) || - (table_key->user_defined_key_parts != - new_key->user_defined_key_parts)) - goto index_changed; - - if (table_key->block_size != new_key->block_size) - goto index_changed; - - if (engine_options_differ(table_key->option_struct, new_key->option_struct, - table->file->ht->index_options)) - goto index_changed; - - /* - Check that the key parts remain compatible between the old and - new tables. - */ - end= table_key->key_part + table_key->user_defined_key_parts; - for (key_part= table_key->key_part, new_part= new_key->key_part; - key_part < end; - key_part++, new_part++) + switch (compare_keys_but_name(table_key, new_key, alter_info, table, new_pk, + old_pk)) { - new_field= get_field_by_index(alter_info, new_part->fieldnr); - old_field= table->field[key_part->fieldnr - 1]; - /* - If there is a change in index length due to column expansion - like varchar(X) changed to varchar(X + N) and has a compatible - packed data representation, we mark it for fast/INPLACE change - in index definition. InnoDB supports INPLACE for this cases - - Key definition has changed if we are using a different field or - if the user key part length is different. - */ - old_field_len= old_field->pack_length(); - - if (old_field->type() == MYSQL_TYPE_VARCHAR) - { - old_field_len= (old_field->pack_length() - - ((Field_varstring*) old_field)->length_bytes); - } - - if (key_part->length == old_field_len && - key_part->length < new_part->length && - (key_part->field->is_equal((Create_field*) new_field) - == IS_EQUAL_PACK_LENGTH)) - { - ha_alter_info->handler_flags |= ALTER_COLUMN_INDEX_LENGTH; - } - else if (key_part->length != new_part->length) - goto index_changed; - - /* - For prefix keys KEY_PART_INFO::field points to cloned Field - object with adjusted length. So below we have to check field - indexes instead of simply comparing pointers to Field objects. - */ - if (! new_field->field || - new_field->field->field_index != key_part->fieldnr - 1) - goto index_changed; + case Compare_keys::Equal: + continue; + case Compare_keys::EqualButKeyPartLength: + ha_alter_info->handler_flags|= ALTER_COLUMN_INDEX_LENGTH; + continue; + case Compare_keys::NotEqual: + break; } - /* - Rebuild the index if following condition get satisfied: - - (i) Old table doesn't have primary key, new table has it and vice-versa - (ii) Primary key changed to another existing index - */ - if ((new_key == new_pk) != (table_key == old_pk)) - goto index_changed; - - /* Check that key comment is not changed. */ - if (table_key->comment.length != new_key->comment.length || - (table_key->comment.length && - memcmp(table_key->comment.str, new_key->comment.str, - table_key->comment.length) != 0)) - goto index_changed; - - continue; - - index_changed: /* Key modified. Add the key / key offset to both buffers. */ ha_alter_info->index_drop_buffer [ha_alter_info->index_drop_count++]= @@ -7039,6 +7038,40 @@ static bool fill_alter_inplace_info(THD *thd, TABLE *table, bool varchar, new_key->option_struct; } + for (uint i= 0; i < ha_alter_info->index_add_count; i++) + { + uint *add_buffer= ha_alter_info->index_add_buffer; + const KEY *new_key= ha_alter_info->key_info_buffer + add_buffer[i]; + + for (uint j= 0; j < ha_alter_info->index_drop_count; j++) + { + KEY **drop_buffer= ha_alter_info->index_drop_buffer; + const KEY *old_key= drop_buffer[j]; + + if (compare_keys_but_name(old_key, new_key, alter_info, table, new_pk, + old_pk) != Compare_keys::Equal) + { + continue; + } + + DBUG_ASSERT( + lex_string_cmp(system_charset_info, &old_key->name, &new_key->name)); + + ha_alter_info->handler_flags|= ALTER_RENAME_INDEX; + ha_alter_info->rename_keys.push_back( + Alter_inplace_info::Rename_key_pair(old_key, new_key)); + + --ha_alter_info->index_add_count; + --ha_alter_info->index_drop_count; + memcpy(add_buffer + i, add_buffer + i + 1, + sizeof(add_buffer[0]) * (ha_alter_info->index_add_count - i)); + memcpy(drop_buffer + j, drop_buffer + j + 1, + sizeof(drop_buffer[0]) * (ha_alter_info->index_drop_count - j)); + --i; // this index once again + break; + } + } + /* Sort index_add_buffer according to how key_info_buffer is sorted. I.e. with primary keys first - see sort_keys(). diff --git a/storage/innobase/dict/dict0stats.cc b/storage/innobase/dict/dict0stats.cc index 718202d1950..9492d9ed711 100644 --- a/storage/innobase/dict/dict0stats.cc +++ b/storage/innobase/dict/dict0stats.cc @@ -3840,6 +3840,63 @@ dict_stats_rename_table( return(ret); } +/*********************************************************************//** +Renames an index in InnoDB persistent stats storage. +This function creates its own transaction and commits it. +@return DB_SUCCESS or error code. DB_STATS_DO_NOT_EXIST will be returned +if the persistent stats do not exist. */ +dberr_t +dict_stats_rename_index( +/*====================*/ + const dict_table_t* table, /*!< in: table whose index + is renamed */ + const char* old_index_name, /*!< in: old index name */ + const char* new_index_name) /*!< in: new index name */ +{ + rw_lock_x_lock(dict_operation_lock); + mutex_enter(&dict_sys->mutex); + + if (!dict_stats_persistent_storage_check(true)) { + mutex_exit(&dict_sys->mutex); + rw_lock_x_unlock(dict_operation_lock); + return(DB_STATS_DO_NOT_EXIST); + } + + char dbname_utf8[MAX_DB_UTF8_LEN]; + char tablename_utf8[MAX_TABLE_UTF8_LEN]; + + dict_fs2utf8(table->name.m_name, dbname_utf8, sizeof(dbname_utf8), + tablename_utf8, sizeof(tablename_utf8)); + + pars_info_t* pinfo; + + pinfo = pars_info_create(); + + pars_info_add_str_literal(pinfo, "dbname_utf8", dbname_utf8); + pars_info_add_str_literal(pinfo, "tablename_utf8", tablename_utf8); + pars_info_add_str_literal(pinfo, "new_index_name", new_index_name); + pars_info_add_str_literal(pinfo, "old_index_name", old_index_name); + + dberr_t ret; + + ret = dict_stats_exec_sql( + pinfo, + "PROCEDURE RENAME_INDEX_IN_INDEX_STATS () IS\n" + "BEGIN\n" + "UPDATE \"" INDEX_STATS_NAME "\" SET\n" + "index_name = :new_index_name\n" + "WHERE\n" + "database_name = :dbname_utf8 AND\n" + "table_name = :tablename_utf8 AND\n" + "index_name = :old_index_name;\n" + "END;\n", NULL); + + mutex_exit(&dict_sys->mutex); + rw_lock_x_unlock(dict_operation_lock); + + return(ret); +} + /* tests @{ */ #ifdef UNIV_ENABLE_UNIT_TEST_DICT_STATS diff --git a/storage/innobase/handler/handler0alter.cc b/storage/innobase/handler/handler0alter.cc index 3a0d4a13199..6b99d91532e 100644 --- a/storage/innobase/handler/handler0alter.cc +++ b/storage/innobase/handler/handler0alter.cc @@ -133,6 +133,7 @@ static const alter_table_operations INNOBASE_ALTER_INSTANT | INNOBASE_FOREIGN_OPERATIONS | ALTER_COLUMN_EQUAL_PACK_LENGTH | ALTER_COLUMN_UNVERSIONED + | ALTER_RENAME_INDEX | ALTER_DROP_VIRTUAL_COLUMN; /** Acquire a page latch on the possible metadata record, @@ -863,10 +864,6 @@ struct ha_innobase_inplace_ctx : public inplace_alter_handler_ctx dict_index_t** drop_index; /** number of InnoDB indexes being dropped */ const ulint num_to_drop_index; - /** InnoDB indexes being renamed */ - dict_index_t** rename; - /** number of InnoDB indexes being renamed */ - const ulint num_to_rename; /** InnoDB foreign key constraints being dropped */ dict_foreign_t** drop_fk; /** number of InnoDB foreign key constraints being dropped */ @@ -948,8 +945,6 @@ struct ha_innobase_inplace_ctx : public inplace_alter_handler_ctx ha_innobase_inplace_ctx(row_prebuilt_t*& prebuilt_arg, dict_index_t** drop_arg, ulint num_to_drop_arg, - dict_index_t** rename_arg, - ulint num_to_rename_arg, dict_foreign_t** drop_fk_arg, ulint num_to_drop_fk_arg, dict_foreign_t** add_fk_arg, @@ -968,7 +963,6 @@ struct ha_innobase_inplace_ctx : public inplace_alter_handler_ctx prebuilt (prebuilt_arg), add_index (0), add_key_numbers (0), num_to_add_index (0), drop_index (drop_arg), num_to_drop_index (num_to_drop_arg), - rename (rename_arg), num_to_rename (num_to_rename_arg), drop_fk (drop_fk_arg), num_to_drop_fk (num_to_drop_fk_arg), add_fk (add_fk_arg), num_to_add_fk (num_to_add_fk_arg), online (online_arg), heap (heap_arg), trx (0), @@ -3258,7 +3252,6 @@ innobase_check_index_keys( } } - my_error(ER_WRONG_NAME_FOR_INDEX, MYF(0), key.name.str); return(ER_WRONG_NAME_FOR_INDEX); @@ -7067,6 +7060,120 @@ innobase_check_foreign_key_index( return(false); } +/** +Rename a given index in the InnoDB data dictionary. + +@param index index to rename +@param new_name new name of the index +@param[in,out] trx dict transaction to use, not going to be committed here + +@retval true Failure +@retval false Success */ +static MY_ATTRIBUTE((warn_unused_result)) +bool +rename_index_try( + const dict_index_t* index, + const char* new_name, + trx_t* trx) +{ + DBUG_ENTER("rename_index_try"); + + ut_ad(mutex_own(&dict_sys->mutex)); + ut_ad(rw_lock_own(dict_operation_lock, RW_LOCK_X)); + ut_ad(trx->dict_operation_lock_mode == RW_X_LATCH); + + pars_info_t* pinfo; + dberr_t err; + + pinfo = pars_info_create(); + + pars_info_add_ull_literal(pinfo, "table_id", index->table->id); + pars_info_add_ull_literal(pinfo, "index_id", index->id); + pars_info_add_str_literal(pinfo, "new_name", new_name); + + trx->op_info = "Renaming an index in SYS_INDEXES"; + + DBUG_EXECUTE_IF( + "ib_rename_index_fail1", + DBUG_SET("+d,innodb_report_deadlock"); + ); + + err = que_eval_sql( + pinfo, + "PROCEDURE RENAME_INDEX_IN_SYS_INDEXES () IS\n" + "BEGIN\n" + "UPDATE SYS_INDEXES SET\n" + "NAME = :new_name\n" + "WHERE\n" + "ID = :index_id AND\n" + "TABLE_ID = :table_id;\n" + "END;\n", + FALSE, trx); /* pinfo is freed by que_eval_sql() */ + + DBUG_EXECUTE_IF( + "ib_rename_index_fail1", + DBUG_SET("-d,innodb_report_deadlock"); + ); + + trx->op_info = ""; + + if (err != DB_SUCCESS) { + my_error_innodb(err, index->table->name.m_name, 0); + DBUG_RETURN(true); + } + + DBUG_RETURN(false); +} + + +/** +Rename a given index in the InnoDB data dictionary cache. + +@param[in,out] index index to rename +@param new_name new index name +*/ +static +void +innobase_rename_index_cache(dict_index_t* index, const char* new_name) +{ + DBUG_ENTER("innobase_rename_index_cache"); + + ut_ad(mutex_own(&dict_sys->mutex)); + ut_ad(rw_lock_own(dict_operation_lock, RW_LOCK_X)); + + size_t old_name_len = strlen(index->name); + size_t new_name_len = strlen(new_name); + + if (old_name_len < new_name_len) { + index->name = static_cast( + mem_heap_alloc(index->heap, new_name_len + 1)); + } + + memcpy(const_cast(index->name()), new_name, new_name_len + 1); + + DBUG_VOID_RETURN; +} + + +/** Rename the index name in cache. +@param[in] ctx alter context +@param[in] ha_alter_info Data used during inplace alter. */ +static void innobase_rename_indexes_cache( + const ha_innobase_inplace_ctx* ctx, + const Alter_inplace_info* ha_alter_info) +{ + DBUG_ASSERT(ha_alter_info->handler_flags & ALTER_RENAME_INDEX); + + for (const Alter_inplace_info::Rename_key_pair& pair : + ha_alter_info->rename_keys) { + dict_index_t* index = dict_table_get_index_on_name( + ctx->old_table, pair.old_key->name.str); + ut_ad(index); + + innobase_rename_index_cache(index, pair.new_key->name.str); + } +} + /** Fill the stored column information in s_cols list. @param[in] altered_table mysql table object @@ -7144,8 +7251,6 @@ ha_innobase::prepare_inplace_alter_table( { dict_index_t** drop_index; /*!< Index to be dropped */ ulint n_drop_index; /*!< Number of indexes to drop */ - dict_index_t** rename_index; /*!< Indexes to be dropped */ - ulint n_rename_index; /*!< Number of indexes to rename */ dict_foreign_t**drop_fk; /*!< Foreign key constraints to drop */ ulint n_drop_fk; /*!< Number of foreign keys to drop */ dict_foreign_t**add_fk = NULL; /*!< Foreign key constraints to drop */ @@ -7661,9 +7766,6 @@ check_if_can_drop_indexes: } } - n_rename_index = 0; - rename_index = NULL; - n_add_fk = 0; if (ha_alter_info->handler_flags @@ -7717,6 +7819,20 @@ err_exit: } } + if (ha_alter_info->handler_flags & ALTER_RENAME_INDEX) { + for (const Alter_inplace_info::Rename_key_pair& pair : + ha_alter_info->rename_keys) { + dict_index_t* index = dict_table_get_index_on_name( + indexed_table, pair.old_key->name.str); + + if (!index || index->is_corrupted()) { + my_error(ER_INDEX_CORRUPT, MYF(0), + index->name()); + goto err_exit; + } + } + } + const ha_table_option_struct& alt_opt= *ha_alter_info->create_info->option_struct; @@ -7732,7 +7848,6 @@ err_exit: = new ha_innobase_inplace_ctx( m_prebuilt, drop_index, n_drop_index, - rename_index, n_rename_index, drop_fk, n_drop_fk, add_fk, n_add_fk, ha_alter_info->online, @@ -7860,7 +7975,6 @@ found_col: ha_alter_info->handler_ctx = new ha_innobase_inplace_ctx( m_prebuilt, drop_index, n_drop_index, - rename_index, n_rename_index, drop_fk, n_drop_fk, add_fk, n_add_fk, ha_alter_info->online, heap, m_prebuilt->table, col_names, @@ -9656,6 +9770,38 @@ commit_try_rebuild( } } +/** Rename indexes in dictionary. +@param[in] ctx alter info context +@param[in] ha_alter_info Operation used during inplace alter +@param[out] trx transaction to change the index name + in dictionary +@return true if it failed to rename +@return false if it is success. */ +static +bool +rename_indexes_try( + const ha_innobase_inplace_ctx* ctx, + const Alter_inplace_info* ha_alter_info, + trx_t* trx) +{ + DBUG_ASSERT(ha_alter_info->handler_flags & ALTER_RENAME_INDEX); + + for (const Alter_inplace_info::Rename_key_pair& pair : + ha_alter_info->rename_keys) { + dict_index_t* index = dict_table_get_index_on_name( + ctx->old_table, pair.old_key->name.str); + // This was checked previously in + // ha_innobase::prepare_inplace_alter_table() + ut_ad(index); + + if (rename_index_try(index, pair.new_key->name.str, trx)) { + return true; + } + } + + return false; +} + /** Apply the changes made during commit_try_rebuild(), to the data dictionary cache and the file system. @param ctx In-place ALTER TABLE context */ @@ -9913,6 +10059,11 @@ commit_try_norebuild( DBUG_RETURN(true); } + if ((ha_alter_info->handler_flags & ALTER_RENAME_INDEX) + && rename_indexes_try(ctx, ha_alter_info, trx)) { + DBUG_RETURN(true); + } + if (ctx->is_instant()) { DBUG_RETURN(innobase_instant_try(ha_alter_info, ctx, altered_table, old_table, @@ -10165,6 +10316,10 @@ commit_cache_norebuild( vers_change_fields_cache(ha_alter_info, ctx, table); } + if (ha_alter_info->handler_flags & ALTER_RENAME_INDEX) { + innobase_rename_indexes_cache(ctx, ha_alter_info); + } + ctx->new_table->fts_doc_id_index = ctx->new_table->fts ? dict_table_get_index_on_name( @@ -10234,6 +10389,27 @@ alter_stats_norebuild( } } + for (const Alter_inplace_info::Rename_key_pair& pair : + ha_alter_info->rename_keys) { + dberr_t err = dict_stats_rename_index(ctx->new_table, + pair.old_key->name.str, + pair.new_key->name.str); + + if (err != DB_SUCCESS) { + push_warning_printf( + thd, + Sql_condition::WARN_LEVEL_WARN, + ER_ERROR_ON_RENAME, + "Error renaming an index of table '%s'" + " from '%s' to '%s' in InnoDB persistent" + " statistics storage: %s", + ctx->new_table->name.m_name, + pair.old_key->name.str, + pair.new_key->name.str, + ut_strerr(err)); + } + } + for (i = 0; i < ctx->num_to_add_index; i++) { dict_index_t* index = ctx->add_index[i]; DBUG_ASSERT(index->table == ctx->new_table); @@ -10269,23 +10445,8 @@ alter_stats_rebuild( DBUG_VOID_RETURN; } -#ifndef DBUG_OFF - bool file_unreadable_orig = false; -#endif /* DBUG_OFF */ - - DBUG_EXECUTE_IF( - "ib_rename_index_fail2", - file_unreadable_orig = table->file_unreadable; - table->file_unreadable = true; - ); - dberr_t ret = dict_stats_update(table, DICT_STATS_RECALC_PERSISTENT); - DBUG_EXECUTE_IF( - "ib_rename_index_fail2", - table->file_unreadable = file_unreadable_orig; - ); - if (ret != DB_SUCCESS) { push_warning_printf( thd, @@ -10943,11 +11104,6 @@ foreign_fail: DBUG_ASSERT(0 == strcmp(ctx->old_table->name.m_name, ctx->tmp_name)); - DBUG_EXECUTE_IF( - "ib_rename_index_fail3", - DBUG_SET("+d,innodb_report_deadlock"); - ); - if (dict_stats_drop_table( ctx->new_table->name.m_name, errstr, sizeof(errstr)) @@ -10963,11 +11119,6 @@ foreign_fail: errstr); } - DBUG_EXECUTE_IF( - "ib_rename_index_fail3", - DBUG_SET("-d,innodb_report_deadlock"); - ); - DBUG_EXECUTE_IF("ib_ddl_crash_before_commit", DBUG_SUICIDE();); diff --git a/storage/innobase/include/dict0stats.h b/storage/innobase/include/dict0stats.h index e846ecabf5a..96169e6219a 100644 --- a/storage/innobase/include/dict0stats.h +++ b/storage/innobase/include/dict0stats.h @@ -187,6 +187,19 @@ dict_stats_rename_table( char* errstr, /*!< out: error string if != DB_SUCCESS is returned */ size_t errstr_sz); /*!< in: errstr size */ +/*********************************************************************//** +Renames an index in InnoDB persistent stats storage. +This function creates its own transaction and commits it. +@return DB_SUCCESS or error code. DB_STATS_DO_NOT_EXIST will be returned +if the persistent stats do not exist. */ +dberr_t +dict_stats_rename_index( +/*====================*/ + const dict_table_t* table, /*!< in: table whose index + is renamed */ + const char* old_index_name, /*!< in: old index name */ + const char* new_index_name) /*!< in: new index name */ + __attribute__((warn_unused_result)); /** Save an individual index's statistic into the persistent statistics storage.