diff --git a/mysql-test/suite/innodb/r/import_tablespace_race.result b/mysql-test/suite/innodb/r/import_tablespace_race.result index 6c8b2e3a26e50..90c8c92241b21 100644 --- a/mysql-test/suite/innodb/r/import_tablespace_race.result +++ b/mysql-test/suite/innodb/r/import_tablespace_race.result @@ -16,7 +16,7 @@ END REPEAT; END $ connection default; -ALTER TABLE t NOWAIT ADD INDEX (c); +ALTER TABLE t NOWAIT ADD INDEX (c(20)); connection con1; connection default; FLUSH TABLE t FOR EXPORT; diff --git a/mysql-test/suite/innodb/r/innodb-virtual-columns-debug.result b/mysql-test/suite/innodb/r/innodb-virtual-columns-debug.result index df6bf83d24f6f..21f4ee390930e 100644 --- a/mysql-test/suite/innodb/r/innodb-virtual-columns-debug.result +++ b/mysql-test/suite/innodb/r/innodb-virtual-columns-debug.result @@ -16,3 +16,28 @@ disconnect con1; connection default; SET debug_sync= "RESET"; DROP TABLE t1; +# +# MDEV-26057 Assertion `!vcol->v_indexes.empty() in trx_undo_log_v_idx +# +CREATE TABLE t1(f1 INT NOT NULL, f2 INT NOT NULL, +f3 INT AS (f2) VIRTUAL) ENGINE=InnoDB; +INSERT INTO t1(f1, f2) VALUES(1, 1); +SET DEBUG_SYNC="alter_rollback_complete SIGNAL dml_start WAIT_FOR default_resume"; +set STATEMENT DEBUG_DBUG="d,innodb_OOM_inplace_alter" FOR alter table t1 add index(f3), algorithm=inplace; +connect con1,localhost,root,,,; +SET DEBUG_SYNC="now WAIT_FOR dml_start"; +INSERT INTO t1(f1, f2) VALUES(2, 2); +SET DEBUG_SYNC="now SIGNAL default_resume"; +connection default; +ERROR HY000: Out of memory. +connection default; +disconnect con1; +SET DEBUG_SYNC="RESET"; +SELECT * FROM t1; +f1 f2 f3 +1 1 1 +2 2 2 +CHECK TABLE t1; +Table Op Msg_type Msg_text +test.t1 check status OK +DROP TABLE t1; diff --git a/mysql-test/suite/innodb/t/import_tablespace_race.test b/mysql-test/suite/innodb/t/import_tablespace_race.test index aca0b5df06d03..8f6a1a431d975 100644 --- a/mysql-test/suite/innodb/t/import_tablespace_race.test +++ b/mysql-test/suite/innodb/t/import_tablespace_race.test @@ -26,7 +26,7 @@ $ --connection default --error 0,ER_LOCK_WAIT_TIMEOUT -ALTER TABLE t NOWAIT ADD INDEX (c); +ALTER TABLE t NOWAIT ADD INDEX (c(20)); --connection con1 --reap diff --git a/mysql-test/suite/innodb/t/innodb-virtual-columns-debug.test b/mysql-test/suite/innodb/t/innodb-virtual-columns-debug.test index d4947ca4d8b5f..ed160b977a4e7 100644 --- a/mysql-test/suite/innodb/t/innodb-virtual-columns-debug.test +++ b/mysql-test/suite/innodb/t/innodb-virtual-columns-debug.test @@ -25,3 +25,28 @@ SET debug_sync= "now SIGNAL another_open"; --connection default SET debug_sync= "RESET"; DROP TABLE t1; + +--echo # +--echo # MDEV-26057 Assertion `!vcol->v_indexes.empty() in trx_undo_log_v_idx +--echo # +CREATE TABLE t1(f1 INT NOT NULL, f2 INT NOT NULL, + f3 INT AS (f2) VIRTUAL) ENGINE=InnoDB; +INSERT INTO t1(f1, f2) VALUES(1, 1); + +SET DEBUG_SYNC="alter_rollback_complete SIGNAL dml_start WAIT_FOR default_resume"; +send set STATEMENT DEBUG_DBUG="d,innodb_OOM_inplace_alter" FOR alter table t1 add index(f3), algorithm=inplace; + +connect(con1,localhost,root,,,); +SET DEBUG_SYNC="now WAIT_FOR dml_start"; +INSERT INTO t1(f1, f2) VALUES(2, 2); +SET DEBUG_SYNC="now SIGNAL default_resume"; + +connection default; +--error ER_OUT_OF_RESOURCES +--reap +connection default; +disconnect con1; +SET DEBUG_SYNC="RESET"; +SELECT * FROM t1; +CHECK TABLE t1; +DROP TABLE t1; diff --git a/storage/innobase/handler/handler0alter.cc b/storage/innobase/handler/handler0alter.cc index dfd7b261eead2..b3d94d55956af 100644 --- a/storage/innobase/handler/handler0alter.cc +++ b/storage/innobase/handler/handler0alter.cc @@ -9108,7 +9108,6 @@ argument passed. @param[in] table table @param[in] col_no column number @param[in] is_v if this is a virtual column -@param[in] only_committed whether to consider only committed indexes @retval true column exists @retval false column does not exist, true if column is system column or it is in the index. */ @@ -9117,8 +9116,7 @@ bool check_col_exists_in_indexes( const dict_table_t* table, ulint col_no, - bool is_v, - bool only_committed = false) + bool is_v) { /* This function does not check system columns */ if (!is_v && dict_table_get_nth_col(table, col_no)->mtype == DATA_SYS) { @@ -9129,9 +9127,7 @@ check_col_exists_in_indexes( index; index = dict_table_get_next_index(index)) { - if (only_committed - ? !index->is_committed() - : index->to_be_dropped) { + if (index->to_be_dropped) { continue; } @@ -9342,6 +9338,7 @@ inline bool rollback_inplace_alter_table(Alter_inplace_info *ha_alter_info, ctx->trx->free(); ctx->trx= nullptr; + DEBUG_SYNC_C("alter_rollback_complete"); dict_sys.lock(SRW_LOCK_CALL); if (ctx->add_vcol) @@ -9369,22 +9366,6 @@ inline bool rollback_inplace_alter_table(Alter_inplace_info *ha_alter_info, DBUG_ASSERT(prebuilt->table->indexes.start->online_status == ONLINE_INDEX_COMPLETE); - /* Reset dict_col_t::ord_part for unindexed columns */ - for (ulint i= 0; i < dict_table_get_n_cols(prebuilt->table); i++) - { - dict_col_t &col= prebuilt->table->cols[i]; - if (col.ord_part && !check_col_exists_in_indexes(prebuilt->table, i, false, - true)) - col.ord_part= 0; - } - - for (ulint i = 0; i < dict_table_get_n_v_cols(prebuilt->table); i++) - { - dict_col_t &col = prebuilt->table->v_cols[i].m_col; - if (col.ord_part && !check_col_exists_in_indexes(prebuilt->table, i, true, - true)) - col.ord_part= 0; - } dict_sys.unlock(); trx_commit_for_mysql(prebuilt->trx); prebuilt->trx_id = 0; diff --git a/storage/innobase/row/row0merge.cc b/storage/innobase/row/row0merge.cc index c60032a8d5ec3..e2bec8e5d3653 100644 --- a/storage/innobase/row/row0merge.cc +++ b/storage/innobase/row/row0merge.cc @@ -4014,6 +4014,43 @@ static void row_merge_drop_fulltext_indexes(trx_t *trx, dict_table_t *table) DICT_TF2_FLAG_UNSET(table, DICT_TF2_FTS); } +/** @param table table +@param col a stored column of the table +@return whether the column is a field of any index of the table */ +static bool row_merge_col_is_indexed(const dict_table_t &table, + const dict_col_t &col) +{ + for (const dict_index_t *index= dict_table_get_first_index(&table); index; + index= dict_table_get_next_index(index)) + for (unsigned f= 0; f < index->n_uniq; f++) + if (dict_index_get_nth_col(index, f) == &col) + return true; + return false; +} + +/** Reset dict_col_t::ord_part for the columns that are no +longer a field of any index that remains in the data dictionary cache. +@param table table whose columns are to be adjusted */ +static void row_merge_reset_ord_part(dict_table_t *table) +{ + ut_ad(dict_sys.locked()); + + for (unsigned c= 0; c < table->n_v_def; c++) + { + dict_v_col_t &v= table->v_cols[c]; + if (v.m_col.ord_part && v.v_indexes.empty()) + v.m_col.ord_part= 0; + } + + for (unsigned c= 0; c < table->n_cols; c++) + { + dict_col_t &col= table->cols[c]; + if (col.ord_part && col.mtype != DATA_SYS && + !row_merge_col_is_indexed(*table, col)) + col.ord_part= 0; + } +} + /** Drop indexes that were created before an error occurred. The data dictionary must have been locked exclusively by the caller, because the transaction will not be committed. @@ -4188,6 +4225,14 @@ row_merge_drop_indexes( } row_merge_drop_fulltext_indexes(trx, table); + /* The aborted indexes are gone from the cache. + Adjust dict_col_t::ord_part while still holding the + same dict_sys.latch, so that DML will never observe a + column with ord_part set and an empty dict_v_col_t::v_indexes. + In the lazy drop above, the indexes remain in the cache, + and nothing must be reset; */ + ut_ad(locked || table->get_ref_count() == 1); + row_merge_reset_ord_part(table); table->drop_aborted = FALSE; ut_d(dict_table_check_for_dup_indexes(table, CHECK_ALL_COMPLETE)); } diff --git a/storage/innobase/row/row0quiesce.cc b/storage/innobase/row/row0quiesce.cc index 36d7e5a9c450a..68b5f9e8e2439 100644 --- a/storage/innobase/row/row0quiesce.cc +++ b/storage/innobase/row/row0quiesce.cc @@ -210,6 +210,54 @@ row_quiesce_write_indexes( return(err); } +/** Compute dict_col_t::ord_part and dict_col_t::max_prefix +of a column from the committed indexes that are present in +the data dictionary cache, in the same way as +dict_index_add_to_cache() would compute them when the table +definition is being loaded from SYS_INDEXES. + +The cached values can be stale. A rolled back ADD INDEX +can leave dict_col_t::ord_part set until the aborted index +is removed from the dictionary cache by a subsequent DDL +operation, and dict_col_t::max_prefix is never reset when an +index is being dropped. +@param table table +@param col a column of the table +@param max_prefix computed dict_col_t::max_prefix +@return computed dict_col_t::ord_part */ +static bool row_quiesce_col_ord_part(const dict_table_t &table, + const dict_col_t &col, + ulint &max_prefix) +{ + bool ord_part= false; + max_prefix= 0; + + for (const dict_index_t* index= UT_LIST_GET_FIRST(table.indexes); + index; index= UT_LIST_GET_NEXT(indexes, index)) + { + if (!index->is_committed()) + continue; + + for (unsigned i= 0; i < index->n_uniq; i++) + { + const dict_field_t& field= index->fields[i]; + if (field.col != &col) + continue; + + if (!field.prefix_len) + { + max_prefix= 0; + return true; + } + + if (field.prefix_len > max_prefix) + max_prefix= field.prefix_len; + ord_part= true; + } + } + return ord_part; +} + /*********************************************************************//** Write the meta data (table columns) config file. Serialise the contents of dict_col_t structure, along with the column name. All fields are serialized @@ -251,10 +299,15 @@ row_quiesce_write_table( mach_write_to_4(ptr, col->ind); ptr += sizeof(ib_uint32_t); - mach_write_to_4(ptr, col->ord_part); + /* Derive the values from the indexes rather than using + the cached dict_col_t fields, which can be stale. */ + ulint max_prefix; + + mach_write_to_4(ptr, row_quiesce_col_ord_part(*table, *col, + max_prefix)); ptr += sizeof(ib_uint32_t); - mach_write_to_4(ptr, col->max_prefix); + mach_write_to_4(ptr, max_prefix); DBUG_EXECUTE_IF("ib_export_io_write_failure_2", close(fileno(file)););