diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 18:07:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 18:07:14 +0000 |
commit | a175314c3e5827eb193872241446f2f8f5c9d33c (patch) | |
tree | cd3d60ca99ae00829c52a6ca79150a5b6e62528b /storage/innobase/handler/handler0alter.cc | |
parent | Initial commit. (diff) | |
download | mariadb-10.5-upstream.tar.xz mariadb-10.5-upstream.zip |
Adding upstream version 1:10.5.12.upstream/1%10.5.12upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'storage/innobase/handler/handler0alter.cc')
-rw-r--r-- | storage/innobase/handler/handler0alter.cc | 11565 |
1 files changed, 11565 insertions, 0 deletions
diff --git a/storage/innobase/handler/handler0alter.cc b/storage/innobase/handler/handler0alter.cc new file mode 100644 index 00000000..f10d534f --- /dev/null +++ b/storage/innobase/handler/handler0alter.cc @@ -0,0 +1,11565 @@ +/***************************************************************************** + +Copyright (c) 2005, 2019, Oracle and/or its affiliates. All Rights Reserved. +Copyright (c) 2013, 2021, MariaDB Corporation. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; version 2 of the License. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA + +*****************************************************************************/ + +/**************************************************//** +@file handler/handler0alter.cc +Smart ALTER TABLE +*******************************************************/ + +/* Include necessary SQL headers */ +#include "univ.i" +#include <debug_sync.h> +#include <log.h> +#include <sql_lex.h> +#include <sql_class.h> +#include <sql_table.h> +#include <mysql/plugin.h> + +/* Include necessary InnoDB headers */ +#include "btr0sea.h" +#include "dict0crea.h" +#include "dict0dict.h" +#include "dict0priv.h" +#include "dict0stats.h" +#include "dict0stats_bg.h" +#include "log0log.h" +#include "rem0types.h" +#include "row0log.h" +#include "row0merge.h" +#include "row0ins.h" +#include "row0row.h" +#include "row0upd.h" +#include "trx0trx.h" +#include "handler0alter.h" +#include "srv0mon.h" +#include "srv0srv.h" +#include "fts0priv.h" +#include "fts0plugin.h" +#include "pars0pars.h" +#include "row0sel.h" +#include "ha_innodb.h" +#include "ut0stage.h" +#include "span.h" +#include <thread> +#include <sstream> + +using st_::span; +/** File format constraint for ALTER TABLE */ +extern ulong innodb_instant_alter_column_allowed; + +static const char *MSG_UNSUPPORTED_ALTER_ONLINE_ON_VIRTUAL_COLUMN= + "INPLACE ADD or DROP of virtual columns cannot be " + "combined with other ALTER TABLE actions"; + +/** Operations for creating secondary indexes (no rebuild needed) */ +static const alter_table_operations INNOBASE_ONLINE_CREATE + = ALTER_ADD_NON_UNIQUE_NON_PRIM_INDEX + | ALTER_ADD_UNIQUE_INDEX; + +/** Operations that require filling in default values for columns */ +static const alter_table_operations INNOBASE_DEFAULTS + = ALTER_COLUMN_NOT_NULLABLE + | ALTER_ADD_STORED_BASE_COLUMN; + + +/** Operations that require knowledge about row_start, row_end values */ +static const alter_table_operations INNOBASE_ALTER_VERSIONED_REBUILD + = ALTER_ADD_SYSTEM_VERSIONING + | ALTER_DROP_SYSTEM_VERSIONING; + +/** Operations for rebuilding a table in place */ +static const alter_table_operations INNOBASE_ALTER_REBUILD + = ALTER_ADD_PK_INDEX + | ALTER_DROP_PK_INDEX + | ALTER_OPTIONS + /* ALTER_OPTIONS needs to check alter_options_need_rebuild() */ + | ALTER_COLUMN_NULLABLE + | INNOBASE_DEFAULTS + | ALTER_STORED_COLUMN_ORDER + | ALTER_DROP_STORED_COLUMN + | ALTER_RECREATE_TABLE + /* + | ALTER_STORED_COLUMN_TYPE + */ + | INNOBASE_ALTER_VERSIONED_REBUILD + ; + +/** Operations that require changes to data */ +static const alter_table_operations INNOBASE_ALTER_DATA + = INNOBASE_ONLINE_CREATE | INNOBASE_ALTER_REBUILD; + +/** Operations for altering a table that InnoDB does not care about */ +static const alter_table_operations INNOBASE_INPLACE_IGNORE + = ALTER_COLUMN_DEFAULT + | ALTER_PARTITIONED + | ALTER_COLUMN_COLUMN_FORMAT + | ALTER_COLUMN_STORAGE_TYPE + | ALTER_CONVERT_TO + | ALTER_VIRTUAL_GCOL_EXPR + | ALTER_DROP_CHECK_CONSTRAINT + | ALTER_RENAME + | ALTER_COLUMN_INDEX_LENGTH + | ALTER_CHANGE_INDEX_COMMENT; + +/** Operations on foreign key definitions (changing the schema only) */ +static const alter_table_operations INNOBASE_FOREIGN_OPERATIONS + = ALTER_DROP_FOREIGN_KEY + | ALTER_ADD_FOREIGN_KEY; + +/** Operations that InnoDB cares about and can perform without creating data */ +static const alter_table_operations INNOBASE_ALTER_NOCREATE + = ALTER_DROP_NON_UNIQUE_NON_PRIM_INDEX + | ALTER_DROP_UNIQUE_INDEX; + +/** Operations that InnoDB cares about and can perform without validation */ +static const alter_table_operations INNOBASE_ALTER_NOVALIDATE + = INNOBASE_ALTER_NOCREATE + | ALTER_VIRTUAL_COLUMN_ORDER + | ALTER_COLUMN_NAME + | INNOBASE_FOREIGN_OPERATIONS + | ALTER_COLUMN_UNVERSIONED + | ALTER_DROP_VIRTUAL_COLUMN; + +/** Operations that InnoDB cares about and can perform without rebuild */ +static const alter_table_operations INNOBASE_ALTER_NOREBUILD + = INNOBASE_ONLINE_CREATE + | INNOBASE_ALTER_NOCREATE; + +/** Operations that can be performed instantly, without inplace_alter_table() */ +static const alter_table_operations INNOBASE_ALTER_INSTANT + = ALTER_VIRTUAL_COLUMN_ORDER + | ALTER_COLUMN_NAME + | ALTER_ADD_VIRTUAL_COLUMN + | INNOBASE_FOREIGN_OPERATIONS + | ALTER_COLUMN_TYPE_CHANGE_BY_ENGINE + | ALTER_COLUMN_UNVERSIONED + | ALTER_RENAME_INDEX + | ALTER_DROP_VIRTUAL_COLUMN; + +/** Initialize instant->field_map. +@param[in] table table definition to copy from */ +inline void dict_table_t::init_instant(const dict_table_t& table) +{ + const dict_index_t& oindex __attribute__((unused))= *table.indexes.start; + dict_index_t& index = *indexes.start; + const unsigned u = index.first_user_field(); + DBUG_ASSERT(u == oindex.first_user_field()); + DBUG_ASSERT(index.n_fields >= oindex.n_fields); + + field_map_element_t* field_map_it = static_cast<field_map_element_t*>( + mem_heap_zalloc(heap, (index.n_fields - u) + * sizeof *field_map_it)); + instant->field_map = field_map_it; + + ut_d(unsigned n_drop = 0); + ut_d(unsigned n_nullable = 0); + for (unsigned i = u; i < index.n_fields; i++) { + auto& f = index.fields[i]; + ut_d(n_nullable += f.col->is_nullable()); + + if (!f.col->is_dropped()) { + (*field_map_it++).set_ind(f.col->ind); + continue; + } + + auto fixed_len = dict_col_get_fixed_size( + f.col, not_redundant()); + field_map_it->set_dropped(); + if (!f.col->is_nullable()) { + field_map_it->set_not_null(); + } + field_map_it->set_ind(fixed_len + ? uint16_t(fixed_len + 1) + : DATA_BIG_COL(f.col)); + field_map_it++; + ut_ad(f.col >= table.instant->dropped); + ut_ad(f.col < table.instant->dropped + + table.instant->n_dropped); + ut_d(n_drop++); + size_t d = f.col - table.instant->dropped; + ut_ad(f.col == &table.instant->dropped[d]); + ut_ad(d <= instant->n_dropped); + f.col = &instant->dropped[d]; + } + ut_ad(n_drop == n_dropped()); + ut_ad(field_map_it == &instant->field_map[index.n_fields - u]); + ut_ad(index.n_nullable == n_nullable); +} + +/** Set is_instant() before instant_column(). +@param[in] old previous table definition +@param[in] col_map map from old.cols[] and old.v_cols[] to this +@param[out] first_alter_pos 0, or 1 + first changed column position */ +inline void dict_table_t::prepare_instant(const dict_table_t& old, + const ulint* col_map, + unsigned& first_alter_pos) +{ + DBUG_ASSERT(!is_instant()); + DBUG_ASSERT(n_dropped() == 0); + DBUG_ASSERT(old.n_cols == old.n_def); + DBUG_ASSERT(n_cols == n_def); + DBUG_ASSERT(old.supports_instant()); + DBUG_ASSERT(not_redundant() == old.not_redundant()); + DBUG_ASSERT(DICT_TF_HAS_ATOMIC_BLOBS(flags) + == DICT_TF_HAS_ATOMIC_BLOBS(old.flags)); + DBUG_ASSERT(!persistent_autoinc + || persistent_autoinc == old.persistent_autoinc); + /* supports_instant() does not necessarily hold here, + in case ROW_FORMAT=COMPRESSED according to the + MariaDB data dictionary, and ALTER_OPTIONS was not set. + If that is the case, the instant ALTER TABLE would keep + the InnoDB table in its current format. */ + + const dict_index_t& oindex = *old.indexes.start; + dict_index_t& index = *indexes.start; + first_alter_pos = 0; + + for (unsigned i = 0; i + DATA_N_SYS_COLS < old.n_cols; i++) { + if (col_map[i] != i) { + first_alter_pos = 1 + i; + goto add_metadata; + } + } + + if (!old.instant) { + /* Columns were not dropped or reordered. + Therefore columns must have been added at the end, + or modified instantly in place. */ + DBUG_ASSERT(index.n_fields >= oindex.n_fields); + DBUG_ASSERT(index.n_fields > oindex.n_fields + || !not_redundant()); +#ifdef UNIV_DEBUG + if (index.n_fields == oindex.n_fields) { + ut_ad(!not_redundant()); + for (unsigned i = index.n_fields; i--; ) { + ut_ad(index.fields[i].col->same_format( + *oindex.fields[i].col)); + } + } +#endif +set_core_fields: + index.n_core_fields = oindex.n_core_fields; + index.n_core_null_bytes = oindex.n_core_null_bytes; + } else { +add_metadata: + const unsigned n_old_drop = old.n_dropped(); + unsigned n_drop = n_old_drop; + for (unsigned i = old.n_cols; i--; ) { + if (col_map[i] == ULINT_UNDEFINED) { + DBUG_ASSERT(i + DATA_N_SYS_COLS + < uint(old.n_cols)); + n_drop++; + } + } + + instant = new (mem_heap_alloc(heap, sizeof(dict_instant_t))) + dict_instant_t(); + instant->n_dropped = n_drop; + if (n_drop) { + instant->dropped + = static_cast<dict_col_t*>( + mem_heap_alloc(heap, n_drop + * sizeof(dict_col_t))); + if (n_old_drop) { + memcpy(instant->dropped, old.instant->dropped, + n_old_drop * sizeof(dict_col_t)); + } + } else { + instant->dropped = NULL; + } + + for (unsigned i = 0, d = n_old_drop; i < old.n_cols; i++) { + if (col_map[i] == ULINT_UNDEFINED) { + (new (&instant->dropped[d++]) + dict_col_t(old.cols[i]))->set_dropped(); + } + } +#ifndef DBUG_OFF + for (unsigned i = 0; i < n_drop; i++) { + DBUG_ASSERT(instant->dropped[i].is_dropped()); + } +#endif + const unsigned n_fields = index.n_fields + n_dropped(); + + DBUG_ASSERT(n_fields >= oindex.n_fields); + dict_field_t* fields = static_cast<dict_field_t*>( + mem_heap_zalloc(heap, n_fields * sizeof *fields)); + unsigned i = 0, j = 0, n_nullable = 0; + ut_d(uint core_null = 0); + for (; i < oindex.n_fields; i++) { + DBUG_ASSERT(j <= i); + dict_field_t&f = fields[i] = oindex.fields[i]; + if (f.col->is_dropped()) { + /* The column has been instantly + dropped earlier. */ + DBUG_ASSERT(f.col >= old.instant->dropped); + { + size_t d = f.col + - old.instant->dropped; + DBUG_ASSERT(d < n_old_drop); + DBUG_ASSERT(&old.instant->dropped[d] + == f.col); + DBUG_ASSERT(!f.name); + f.col = instant->dropped + d; + } + if (f.col->is_nullable()) { +found_nullable: + n_nullable++; + ut_d(core_null + += i < oindex.n_core_fields); + } + continue; + } + + const ulint col_ind = col_map[f.col->ind]; + if (col_ind != ULINT_UNDEFINED) { + if (index.fields[j].col->ind != col_ind) { + /* The fields for instantly + added columns must be placed + last in the clustered index. + Keep pre-existing fields in + the same position. */ + uint k; + for (k = j + 1; k < index.n_fields; + k++) { + if (index.fields[k].col->ind + == col_ind) { + goto found_j; + } + } + DBUG_ASSERT("no such col" == 0); +found_j: + std::swap(index.fields[j], + index.fields[k]); + } + DBUG_ASSERT(index.fields[j].col->ind + == col_ind); + fields[i] = index.fields[j++]; + DBUG_ASSERT(!fields[i].col->is_dropped()); + DBUG_ASSERT(fields[i].name + == fields[i].col->name(*this)); + if (fields[i].col->is_nullable()) { + goto found_nullable; + } + continue; + } + + /* This column is being dropped. */ + unsigned d = n_old_drop; + for (unsigned c = 0; c < f.col->ind; c++) { + d += col_map[c] == ULINT_UNDEFINED; + } + DBUG_ASSERT(d < n_drop); + f.col = &instant->dropped[d]; + f.name = NULL; + if (f.col->is_nullable()) { + goto found_nullable; + } + } + /* The n_core_null_bytes only matters for + ROW_FORMAT=COMPACT and ROW_FORMAT=DYNAMIC tables. */ + ut_ad(UT_BITS_IN_BYTES(core_null) == oindex.n_core_null_bytes + || !not_redundant()); + DBUG_ASSERT(i >= oindex.n_core_fields); + DBUG_ASSERT(j <= i); + DBUG_ASSERT(n_fields - (i - j) == index.n_fields); + std::sort(index.fields + j, index.fields + index.n_fields, + [](const dict_field_t& a, const dict_field_t& b) + { return a.col->ind < b.col->ind; }); + for (; i < n_fields; i++) { + fields[i] = index.fields[j++]; + n_nullable += fields[i].col->is_nullable(); + DBUG_ASSERT(!fields[i].col->is_dropped()); + DBUG_ASSERT(fields[i].name + == fields[i].col->name(*this)); + } + DBUG_ASSERT(j == index.n_fields); + index.n_fields = index.n_def = n_fields + & dict_index_t::MAX_N_FIELDS; + index.fields = fields; + DBUG_ASSERT(n_nullable >= index.n_nullable); + DBUG_ASSERT(n_nullable >= oindex.n_nullable); + index.n_nullable = n_nullable & dict_index_t::MAX_N_FIELDS; + goto set_core_fields; + } + + DBUG_ASSERT(n_cols + n_dropped() >= old.n_cols + old.n_dropped()); + DBUG_ASSERT(n_dropped() >= old.n_dropped()); + DBUG_ASSERT(index.n_core_fields == oindex.n_core_fields); + DBUG_ASSERT(index.n_core_null_bytes == oindex.n_core_null_bytes); +} + +/** Adjust index metadata for instant ADD/DROP/reorder COLUMN. +@param[in] clustered index definition after instant ALTER TABLE */ +inline void dict_index_t::instant_add_field(const dict_index_t& instant) +{ + DBUG_ASSERT(is_primary()); + DBUG_ASSERT(instant.is_primary()); + DBUG_ASSERT(!has_virtual()); + DBUG_ASSERT(!instant.has_virtual()); + DBUG_ASSERT(instant.n_core_fields <= instant.n_fields); + DBUG_ASSERT(n_def == n_fields); + DBUG_ASSERT(instant.n_def == instant.n_fields); + DBUG_ASSERT(type == instant.type); + DBUG_ASSERT(trx_id_offset == instant.trx_id_offset); + DBUG_ASSERT(n_user_defined_cols == instant.n_user_defined_cols); + DBUG_ASSERT(n_uniq == instant.n_uniq); + DBUG_ASSERT(instant.n_fields >= n_fields); + DBUG_ASSERT(instant.n_nullable >= n_nullable); + DBUG_ASSERT(instant.n_core_fields == n_core_fields); + DBUG_ASSERT(instant.n_core_null_bytes == n_core_null_bytes); + + /* instant will have all fields (including ones for columns + that have been or are being instantly dropped) in the same position + as this index. Fields for any added columns are appended at the end. */ +#ifndef DBUG_OFF + for (unsigned i = 0; i < n_fields; i++) { + DBUG_ASSERT(fields[i].same(instant.fields[i])); + DBUG_ASSERT(instant.fields[i].col->same_format(*fields[i] + .col)); + /* Instant conversion from NULL to NOT NULL is not allowed. */ + DBUG_ASSERT(!fields[i].col->is_nullable() + || instant.fields[i].col->is_nullable()); + DBUG_ASSERT(fields[i].col->is_nullable() + == instant.fields[i].col->is_nullable() + || !table->not_redundant()); + } +#endif + n_fields = instant.n_fields; + n_def = instant.n_def; + n_nullable = instant.n_nullable; + fields = static_cast<dict_field_t*>( + mem_heap_dup(heap, instant.fields, n_fields * sizeof *fields)); + + ut_d(unsigned n_null = 0); + ut_d(unsigned n_dropped = 0); + + for (unsigned i = 0; i < n_fields; i++) { + const dict_col_t* icol = instant.fields[i].col; + dict_field_t& f = fields[i]; + ut_d(n_null += icol->is_nullable()); + DBUG_ASSERT(!icol->is_virtual()); + if (icol->is_dropped()) { + ut_d(n_dropped++); + f.col->set_dropped(); + f.name = NULL; + } else { + f.col = &table->cols[icol - instant.table->cols]; + f.name = f.col->name(*table); + } + } + + ut_ad(n_null == n_nullable); + ut_ad(n_dropped == instant.table->n_dropped()); +} + +/** Adjust table metadata for instant ADD/DROP/reorder COLUMN. +@param[in] table altered table (with dropped columns) +@param[in] col_map mapping from cols[] and v_cols[] to table +@return whether the metadata record must be updated */ +inline bool dict_table_t::instant_column(const dict_table_t& table, + const ulint* col_map) +{ + DBUG_ASSERT(!table.cached); + DBUG_ASSERT(table.n_def == table.n_cols); + DBUG_ASSERT(table.n_t_def == table.n_t_cols); + DBUG_ASSERT(n_def == n_cols); + DBUG_ASSERT(n_t_def == n_t_cols); + DBUG_ASSERT(n_v_def == n_v_cols); + DBUG_ASSERT(table.n_v_def == table.n_v_cols); + DBUG_ASSERT(table.n_cols + table.n_dropped() >= n_cols + n_dropped()); + DBUG_ASSERT(!table.persistent_autoinc + || persistent_autoinc == table.persistent_autoinc); + ut_ad(mutex_own(&dict_sys.mutex)); + + { + const char* end = table.col_names; + for (unsigned i = table.n_cols; i--; ) end += strlen(end) + 1; + + col_names = static_cast<char*>( + mem_heap_dup(heap, table.col_names, + ulint(end - table.col_names))); + } + const dict_col_t* const old_cols = cols; + cols = static_cast<dict_col_t*>(mem_heap_dup(heap, table.cols, + table.n_cols + * sizeof *cols)); + + /* Preserve the default values of previously instantly added + columns, or copy the new default values to this->heap. */ + for (uint16_t i = 0; i < table.n_cols; i++) { + dict_col_t& c = cols[i]; + + if (const dict_col_t* o = find(old_cols, col_map, n_cols, i)) { + c.def_val = o->def_val; + DBUG_ASSERT(!((c.prtype ^ o->prtype) + & ~(DATA_NOT_NULL | DATA_VERSIONED + | CHAR_COLL_MASK << 16 + | DATA_LONG_TRUE_VARCHAR))); + DBUG_ASSERT(c.same_type(*o)); + DBUG_ASSERT(c.len >= o->len); + + if (o->vers_sys_start()) { + ut_ad(o->ind == vers_start); + vers_start = i & dict_index_t::MAX_N_FIELDS; + } else if (o->vers_sys_end()) { + ut_ad(o->ind == vers_end); + vers_end = i & dict_index_t::MAX_N_FIELDS; + } + continue; + } + + DBUG_ASSERT(c.is_added()); + if (c.def_val.len <= UNIV_PAGE_SIZE_MAX + && (!c.def_val.len + || !memcmp(c.def_val.data, field_ref_zero, + c.def_val.len))) { + c.def_val.data = field_ref_zero; + } else if (const void*& d = c.def_val.data) { + d = mem_heap_dup(heap, d, c.def_val.len); + } else { + DBUG_ASSERT(c.def_val.len == UNIV_SQL_NULL); + } + } + + n_t_def = (n_t_def + (table.n_cols - n_cols)) + & dict_index_t::MAX_N_FIELDS; + n_t_cols = (n_t_cols + (table.n_cols - n_cols)) + & dict_index_t::MAX_N_FIELDS; + n_def = table.n_cols; + + const dict_v_col_t* const old_v_cols = v_cols; + + if (const char* end = table.v_col_names) { + for (unsigned i = table.n_v_cols; i--; ) { + end += strlen(end) + 1; + } + + v_col_names = static_cast<char*>( + mem_heap_dup(heap, table.v_col_names, + ulint(end - table.v_col_names))); + v_cols = static_cast<dict_v_col_t*>( + mem_heap_alloc(heap, table.n_v_cols * sizeof(*v_cols))); + for (ulint i = table.n_v_cols; i--; ) { + new (&v_cols[i]) dict_v_col_t(table.v_cols[i]); + v_cols[i].v_indexes.clear(); + } + } else { + ut_ad(table.n_v_cols == 0); + v_col_names = NULL; + v_cols = NULL; + } + + n_t_def = (n_t_def + (table.n_v_cols - n_v_cols)) + & dict_index_t::MAX_N_FIELDS; + n_t_cols = (n_t_cols + (table.n_v_cols - n_v_cols)) + & dict_index_t::MAX_N_FIELDS; + n_v_def = table.n_v_cols; + + for (unsigned i = 0; i < n_v_def; i++) { + dict_v_col_t& v = v_cols[i]; + DBUG_ASSERT(v.v_indexes.empty()); + v.base_col = static_cast<dict_col_t**>( + mem_heap_dup(heap, v.base_col, + v.num_base * sizeof *v.base_col)); + + for (ulint n = v.num_base; n--; ) { + dict_col_t*& base = v.base_col[n]; + if (base->is_virtual()) { + } else if (base >= table.cols + && base < table.cols + table.n_cols) { + /* The base column was instantly added. */ + size_t c = base - table.cols; + DBUG_ASSERT(base == &table.cols[c]); + base = &cols[c]; + } else { + DBUG_ASSERT(base >= old_cols); + size_t c = base - old_cols; + DBUG_ASSERT(c + DATA_N_SYS_COLS < n_cols); + DBUG_ASSERT(base == &old_cols[c]); + DBUG_ASSERT(col_map[c] + DATA_N_SYS_COLS + < n_cols); + base = &cols[col_map[c]]; + } + } + } + + dict_index_t* index = dict_table_get_first_index(this); + bool metadata_changed; + { + const dict_index_t& i = *dict_table_get_first_index(&table); + metadata_changed = i.n_fields > index->n_fields; + ut_ad(i.n_fields >= index->n_fields); + index->instant_add_field(i); + } + + if (instant || table.instant) { + const auto old_instant = instant; + /* FIXME: add instant->heap, and transfer ownership here */ + if (!instant) { + instant = new (mem_heap_zalloc(heap, sizeof *instant)) + dict_instant_t(); + goto dup_dropped; + } else if (n_dropped() < table.n_dropped()) { +dup_dropped: + instant->dropped = static_cast<dict_col_t*>( + mem_heap_dup(heap, table.instant->dropped, + table.instant->n_dropped + * sizeof *instant->dropped)); + instant->n_dropped = table.instant->n_dropped; + } else if (table.instant->n_dropped) { + memcpy(instant->dropped, table.instant->dropped, + table.instant->n_dropped + * sizeof *instant->dropped); + } + + const field_map_element_t* field_map = old_instant + ? old_instant->field_map : NULL; + + init_instant(table); + + if (!metadata_changed) { + metadata_changed = !field_map + || memcmp(field_map, + instant->field_map, + (index->n_fields + - index->first_user_field()) + * sizeof *field_map); + } + } + + while ((index = dict_table_get_next_index(index)) != NULL) { + if (index->to_be_dropped) { + continue; + } + for (unsigned i = 0; i < index->n_fields; i++) { + dict_field_t& f = index->fields[i]; + if (f.col >= table.cols + && f.col < table.cols + table.n_cols) { + /* This is an instantly added column + in a newly added index. */ + DBUG_ASSERT(!f.col->is_virtual()); + size_t c = f.col - table.cols; + DBUG_ASSERT(f.col == &table.cols[c]); + f.col = &cols[c]; + } else if (f.col >= &table.v_cols->m_col + && f.col < &table.v_cols[n_v_cols].m_col) { + /* This is an instantly added virtual column + in a newly added index. */ + DBUG_ASSERT(f.col->is_virtual()); + size_t c = reinterpret_cast<dict_v_col_t*>( + f.col) - table.v_cols; + DBUG_ASSERT(f.col == &table.v_cols[c].m_col); + f.col = &v_cols[c].m_col; + } else if (f.col < old_cols + || f.col >= old_cols + n_cols) { + DBUG_ASSERT(f.col->is_virtual()); + f.col = &v_cols[col_map[ + reinterpret_cast<dict_v_col_t*>( + f.col) + - old_v_cols + n_cols]].m_col; + } else { + f.col = &cols[col_map[f.col - old_cols]]; + DBUG_ASSERT(!f.col->is_virtual()); + } + f.name = f.col->name(*this); + if (f.col->is_virtual()) { + dict_v_col_t* v_col = reinterpret_cast + <dict_v_col_t*>(f.col); + v_col->v_indexes.push_front( + dict_v_idx_t(index, i)); + } + } + } + + n_cols = table.n_cols; + n_v_cols = table.n_v_cols; + return metadata_changed; +} + +/** Find the old column number for the given new column position. +@param[in] col_map column map from old column to new column +@param[in] pos new column position +@param[in] n number of columns present in the column map +@return old column position for the given new column position. */ +static ulint find_old_col_no(const ulint* col_map, ulint pos, ulint n) +{ + do { + ut_ad(n); + } while (col_map[--n] != pos); + return n; +} + +/** Roll back instant_column(). +@param[in] old_n_cols original n_cols +@param[in] old_cols original cols +@param[in] old_col_names original col_names +@param[in] old_instant original instant structure +@param[in] old_fields original fields +@param[in] old_n_fields original number of fields +@param[in] old_n_core_fields original number of core fields +@param[in] old_n_v_cols original n_v_cols +@param[in] old_v_cols original v_cols +@param[in] old_v_col_names original v_col_names +@param[in] col_map column map */ +inline void dict_table_t::rollback_instant( + unsigned old_n_cols, + dict_col_t* old_cols, + const char* old_col_names, + dict_instant_t* old_instant, + dict_field_t* old_fields, + unsigned old_n_fields, + unsigned old_n_core_fields, + unsigned old_n_v_cols, + dict_v_col_t* old_v_cols, + const char* old_v_col_names, + const ulint* col_map) +{ + ut_d(dict_sys.assert_locked()); + + if (cols == old_cols) { + /* Alter fails before instant operation happens. + So there is no need to do rollback instant operation */ + return; + } + + dict_index_t* index = indexes.start; + /* index->is_instant() does not necessarily hold here, because + the table may have been emptied */ + DBUG_ASSERT(old_n_cols >= DATA_N_SYS_COLS); + DBUG_ASSERT(n_cols == n_def); + DBUG_ASSERT(index->n_def == index->n_fields); + DBUG_ASSERT(index->n_core_fields <= index->n_fields); + DBUG_ASSERT(old_n_core_fields <= old_n_fields); + DBUG_ASSERT(instant || !old_instant); + + instant = old_instant; + + index->n_nullable = 0; + + for (unsigned i = old_n_fields; i--; ) { + if (old_fields[i].col->is_nullable()) { + index->n_nullable++; + } + } + + for (unsigned i = n_v_cols; i--; ) { + v_cols[i].~dict_v_col_t(); + } + + index->n_core_fields = ((index->n_fields == index->n_core_fields) + ? old_n_fields + : old_n_core_fields) + & dict_index_t::MAX_N_FIELDS; + index->n_def = index->n_fields = old_n_fields + & dict_index_t::MAX_N_FIELDS; + index->n_core_null_bytes = static_cast<uint8_t>( + UT_BITS_IN_BYTES(index->get_n_nullable(index->n_core_fields))); + + const dict_col_t* const new_cols = cols; + const dict_col_t* const new_cols_end __attribute__((unused)) = cols + n_cols; + const dict_v_col_t* const new_v_cols = v_cols; + const dict_v_col_t* const new_v_cols_end __attribute__((unused))= v_cols + n_v_cols; + + cols = old_cols; + col_names = old_col_names; + v_cols = old_v_cols; + v_col_names = old_v_col_names; + n_def = n_cols = old_n_cols & dict_index_t::MAX_N_FIELDS; + n_v_def = n_v_cols = old_n_v_cols & dict_index_t::MAX_N_FIELDS; + n_t_def = n_t_cols = (n_cols + n_v_cols) & dict_index_t::MAX_N_FIELDS; + + if (versioned()) { + for (unsigned i = 0; i < n_cols; ++i) { + if (cols[i].vers_sys_start()) { + vers_start = i & dict_index_t::MAX_N_FIELDS; + } else if (cols[i].vers_sys_end()) { + vers_end = i & dict_index_t::MAX_N_FIELDS; + } + } + } + + index->fields = old_fields; + + while ((index = dict_table_get_next_index(index)) != NULL) { + if (index->to_be_dropped) { + /* instant_column() did not adjust these indexes. */ + continue; + } + + for (unsigned i = 0; i < index->n_fields; i++) { + dict_field_t& f = index->fields[i]; + if (f.col->is_virtual()) { + DBUG_ASSERT(f.col >= &new_v_cols->m_col); + DBUG_ASSERT(f.col < &new_v_cols_end->m_col); + size_t n = size_t( + reinterpret_cast<dict_v_col_t*>(f.col) + - new_v_cols); + DBUG_ASSERT(n <= n_v_cols); + + ulint old_col_no = find_old_col_no( + col_map + n_cols, n, n_v_cols); + DBUG_ASSERT(old_col_no <= n_v_cols); + f.col = &v_cols[old_col_no].m_col; + DBUG_ASSERT(f.col->is_virtual()); + } else { + DBUG_ASSERT(f.col >= new_cols); + DBUG_ASSERT(f.col < new_cols_end); + size_t n = size_t(f.col - new_cols); + DBUG_ASSERT(n <= n_cols); + + ulint old_col_no = find_old_col_no(col_map, + n, n_cols); + DBUG_ASSERT(old_col_no < n_cols); + f.col = &cols[old_col_no]; + DBUG_ASSERT(!f.col->is_virtual()); + } + f.name = f.col->name(*this); + } + } +} + +struct ha_innobase_inplace_ctx : public inplace_alter_handler_ctx +{ + /** Dummy query graph */ + que_thr_t* thr; + /** The prebuilt struct of the creating instance */ + row_prebuilt_t*& prebuilt; + /** InnoDB indexes being created */ + dict_index_t** add_index; + /** MySQL key numbers for the InnoDB indexes that are being created */ + const ulint* add_key_numbers; + /** number of InnoDB indexes being created */ + ulint num_to_add_index; + /** InnoDB indexes being dropped */ + dict_index_t** drop_index; + /** number of InnoDB indexes being dropped */ + const ulint num_to_drop_index; + /** InnoDB foreign key constraints being dropped */ + dict_foreign_t** drop_fk; + /** number of InnoDB foreign key constraints being dropped */ + const ulint num_to_drop_fk; + /** InnoDB foreign key constraints being added */ + dict_foreign_t** add_fk; + /** number of InnoDB foreign key constraints being dropped */ + const ulint num_to_add_fk; + /** whether to create the indexes online */ + bool online; + /** memory heap */ + mem_heap_t* heap; + /** dictionary transaction */ + trx_t* trx; + /** original table (if rebuilt, differs from indexed_table) */ + dict_table_t* old_table; + /** table where the indexes are being created or dropped */ + dict_table_t* new_table; + /** table definition for instant ADD/DROP/reorder COLUMN */ + dict_table_t* instant_table; + /** mapping of old column numbers to new ones, or NULL */ + const ulint* col_map; + /** new column names, or NULL if nothing was renamed */ + const char** col_names; + /** added AUTO_INCREMENT column position, or ULINT_UNDEFINED */ + const ulint add_autoinc; + /** default values of ADD and CHANGE COLUMN, or NULL */ + const dtuple_t* defaults; + /** autoinc sequence to use */ + ib_sequence_t sequence; + /** temporary table name to use for old table when renaming tables */ + const char* tmp_name; + /** whether the order of the clustered index is unchanged */ + bool skip_pk_sort; + /** number of virtual columns to be added */ + unsigned num_to_add_vcol; + /** virtual columns to be added */ + dict_v_col_t* add_vcol; + const char** add_vcol_name; + /** number of virtual columns to be dropped */ + unsigned num_to_drop_vcol; + /** virtual columns to be dropped */ + dict_v_col_t* drop_vcol; + const char** drop_vcol_name; + /** ALTER TABLE stage progress recorder */ + ut_stage_alter_t* m_stage; + /** original number of user columns in the table */ + const unsigned old_n_cols; + /** original columns of the table */ + dict_col_t* const old_cols; + /** original column names of the table */ + const char* const old_col_names; + /** original instantly dropped or reordered columns */ + dict_instant_t* const old_instant; + /** original index fields */ + dict_field_t* const old_fields; + /** size of old_fields */ + const unsigned old_n_fields; + /** original old_table->n_core_fields */ + const unsigned old_n_core_fields; + /** original number of virtual columns in the table */ + const unsigned old_n_v_cols; + /** original virtual columns of the table */ + dict_v_col_t* const old_v_cols; + /** original virtual column names of the table */ + const char* const old_v_col_names; + /** 0, or 1 + first column whose position changes in instant ALTER */ + unsigned first_alter_pos; + /** Allow non-null conversion. + (1) Alter ignore should allow the conversion + irrespective of sql mode. + (2) Don't allow the conversion in strict mode + (3) Allow the conversion only in non-strict mode. */ + const bool allow_not_null; + + /** The page_compression_level attribute, or 0 */ + const uint page_compression_level; + + ha_innobase_inplace_ctx(row_prebuilt_t*& prebuilt_arg, + dict_index_t** drop_arg, + ulint num_to_drop_arg, + dict_foreign_t** drop_fk_arg, + ulint num_to_drop_fk_arg, + dict_foreign_t** add_fk_arg, + ulint num_to_add_fk_arg, + bool online_arg, + mem_heap_t* heap_arg, + dict_table_t* new_table_arg, + const char** col_names_arg, + ulint add_autoinc_arg, + ulonglong autoinc_col_min_value_arg, + ulonglong autoinc_col_max_value_arg, + bool allow_not_null_flag, + bool page_compressed, + ulonglong page_compression_level_arg) : + 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), + 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), + old_table (prebuilt_arg->table), + new_table (new_table_arg), instant_table (0), + col_map (0), col_names (col_names_arg), + add_autoinc (add_autoinc_arg), + defaults (0), + sequence(prebuilt->trx->mysql_thd, + autoinc_col_min_value_arg, autoinc_col_max_value_arg), + tmp_name (0), + skip_pk_sort(false), + num_to_add_vcol(0), + add_vcol(0), + add_vcol_name(0), + num_to_drop_vcol(0), + drop_vcol(0), + drop_vcol_name(0), + m_stage(NULL), + old_n_cols(prebuilt_arg->table->n_cols), + old_cols(prebuilt_arg->table->cols), + old_col_names(prebuilt_arg->table->col_names), + old_instant(prebuilt_arg->table->instant), + old_fields(prebuilt_arg->table->indexes.start->fields), + old_n_fields(prebuilt_arg->table->indexes.start->n_fields), + old_n_core_fields(prebuilt_arg->table->indexes.start + ->n_core_fields), + old_n_v_cols(prebuilt_arg->table->n_v_cols), + old_v_cols(prebuilt_arg->table->v_cols), + old_v_col_names(prebuilt_arg->table->v_col_names), + first_alter_pos(0), + allow_not_null(allow_not_null_flag), + page_compression_level(page_compressed + ? (page_compression_level_arg + ? uint(page_compression_level_arg) + : page_zip_level) + : 0) + { + ut_ad(old_n_cols >= DATA_N_SYS_COLS); + ut_ad(page_compression_level <= 9); +#ifdef UNIV_DEBUG + for (ulint i = 0; i < num_to_add_index; i++) { + ut_ad(!add_index[i]->to_be_dropped); + } + for (ulint i = 0; i < num_to_drop_index; i++) { + ut_ad(drop_index[i]->to_be_dropped); + } +#endif /* UNIV_DEBUG */ + + thr = pars_complete_graph_for_exec(NULL, prebuilt->trx, heap, + prebuilt); + } + + ~ha_innobase_inplace_ctx() + { + UT_DELETE(m_stage); + if (instant_table) { + ut_ad(!instant_table->id); + while (dict_index_t* index + = UT_LIST_GET_LAST(instant_table->indexes)) { + UT_LIST_REMOVE(instant_table->indexes, index); + rw_lock_free(&index->lock); + dict_mem_index_free(index); + } + for (unsigned i = old_n_v_cols; i--; ) { + old_v_cols[i].~dict_v_col_t(); + } + if (instant_table->fts) { + fts_free(instant_table); + } + dict_mem_table_free(instant_table); + } + mem_heap_free(heap); + } + + /** Determine if the table will be rebuilt. + @return whether the table will be rebuilt */ + bool need_rebuild () const { return(old_table != new_table); } + + /** Convert table-rebuilding ALTER to instant ALTER. */ + void prepare_instant() + { + DBUG_ASSERT(need_rebuild()); + DBUG_ASSERT(!is_instant()); + DBUG_ASSERT(old_table->n_cols == old_n_cols); + + instant_table = new_table; + new_table = old_table; + export_vars.innodb_instant_alter_column++; + + instant_table->prepare_instant(*old_table, col_map, + first_alter_pos); + } + + /** Adjust table metadata for instant ADD/DROP/reorder COLUMN. + @return whether the metadata record must be updated */ + bool instant_column() + { + DBUG_ASSERT(is_instant()); + DBUG_ASSERT(old_n_fields + == old_table->indexes.start->n_fields); + return old_table->instant_column(*instant_table, col_map); + } + + /** Revert prepare_instant() if the transaction is rolled back. */ + void rollback_instant() + { + if (!is_instant()) return; + old_table->rollback_instant(old_n_cols, + old_cols, old_col_names, + old_instant, + old_fields, old_n_fields, + old_n_core_fields, + old_n_v_cols, old_v_cols, + old_v_col_names, + col_map); + } + + /** @return whether this is instant ALTER TABLE */ + bool is_instant() const + { + DBUG_ASSERT(!instant_table || !instant_table->can_be_evicted); + return instant_table; + } + + /** Create an index table where indexes are ordered as follows: + + IF a new primary key is defined for the table THEN + + 1) New primary key + 2) The remaining keys in key_info + + ELSE + + 1) All new indexes in the order they arrive from MySQL + + ENDIF + + @return key definitions */ + MY_ATTRIBUTE((nonnull, warn_unused_result, malloc)) + inline index_def_t* + create_key_defs( + const Alter_inplace_info* ha_alter_info, + /*!< in: alter operation */ + const TABLE* altered_table, + /*!< in: MySQL table that is being altered */ + ulint& n_fts_add, + /*!< out: number of FTS indexes to be created */ + ulint& fts_doc_id_col, + /*!< in: The column number for Doc ID */ + bool& add_fts_doc_id, + /*!< in: whether we need to add new DOC ID + column for FTS index */ + bool& add_fts_doc_idx, + /*!< in: whether we need to add new DOC ID + index for FTS index */ + const TABLE* table); + /*!< in: MySQL table that is being altered */ + + /** Share context between partitions. + @param[in] ctx context from another partition of the table */ + void set_shared_data(const inplace_alter_handler_ctx& ctx) + { + if (add_autoinc != ULINT_UNDEFINED) { + const ha_innobase_inplace_ctx& ha_ctx = + static_cast<const ha_innobase_inplace_ctx&> + (ctx); + /* When adding an AUTO_INCREMENT column to a + partitioned InnoDB table, we must share the + sequence for all partitions. */ + ut_ad(ha_ctx.add_autoinc == add_autoinc); + ut_ad(ha_ctx.sequence.last()); + sequence = ha_ctx.sequence; + } + } + + /** @return whether the given column is being added */ + bool is_new_vcol(const dict_v_col_t &v_col) const + { + for (ulint i= 0; i < num_to_add_vcol; i++) + if (&add_vcol[i] == &v_col) + return true; + return false; + } + + /** During rollback, make newly added indexes point to + newly added virtual columns. */ + void clean_new_vcol_index() + { + ut_ad(old_table == new_table); + const dict_index_t *index= dict_table_get_first_index(old_table); + while ((index= dict_table_get_next_index(index)) != NULL) + { + if (!index->has_virtual() || index->is_committed()) + continue; + ulint n_drop_new_vcol= index->get_new_n_vcol(); + for (ulint i= 0; n_drop_new_vcol && i < index->n_fields; i++) + { + dict_col_t *col= index->fields[i].col; + /* Skip the non-virtual and old virtual columns */ + if (!col->is_virtual()) + continue; + dict_v_col_t *vcol= reinterpret_cast<dict_v_col_t*>(col); + if (!is_new_vcol(*vcol)) + continue; + + index->fields[i].col= &index->new_vcol_info-> + add_drop_v_col(index->heap, vcol, --n_drop_new_vcol)->m_col; + } + } + } + +private: + // Disable copying + ha_innobase_inplace_ctx(const ha_innobase_inplace_ctx&); + ha_innobase_inplace_ctx& operator=(const ha_innobase_inplace_ctx&); +}; + +/********************************************************************//** +Get the upper limit of the MySQL integral and floating-point type. +@return maximum allowed value for the field */ +UNIV_INTERN +ulonglong +innobase_get_int_col_max_value( +/*===========================*/ + const Field* field); /*!< in: MySQL field */ + +/* Report an InnoDB error to the client by invoking my_error(). */ +static ATTRIBUTE_COLD __attribute__((nonnull)) +void +my_error_innodb( +/*============*/ + dberr_t error, /*!< in: InnoDB error code */ + const char* table, /*!< in: table name */ + ulint flags) /*!< in: table flags */ +{ + switch (error) { + case DB_MISSING_HISTORY: + my_error(ER_TABLE_DEF_CHANGED, MYF(0)); + break; + case DB_RECORD_NOT_FOUND: + my_error(ER_KEY_NOT_FOUND, MYF(0), table); + break; + case DB_DEADLOCK: + my_error(ER_LOCK_DEADLOCK, MYF(0)); + break; + case DB_LOCK_WAIT_TIMEOUT: + my_error(ER_LOCK_WAIT_TIMEOUT, MYF(0)); + break; + case DB_INTERRUPTED: + my_error(ER_QUERY_INTERRUPTED, MYF(0)); + break; + case DB_OUT_OF_MEMORY: + my_error(ER_OUT_OF_RESOURCES, MYF(0)); + break; + case DB_OUT_OF_FILE_SPACE: + my_error(ER_RECORD_FILE_FULL, MYF(0), table); + break; + case DB_TEMP_FILE_WRITE_FAIL: + my_error(ER_TEMP_FILE_WRITE_FAILURE, MYF(0)); + break; + case DB_TOO_BIG_INDEX_COL: + my_error(ER_INDEX_COLUMN_TOO_LONG, MYF(0), + (ulong) DICT_MAX_FIELD_LEN_BY_FORMAT_FLAG(flags)); + break; + case DB_TOO_MANY_CONCURRENT_TRXS: + my_error(ER_TOO_MANY_CONCURRENT_TRXS, MYF(0)); + break; + case DB_LOCK_TABLE_FULL: + my_error(ER_LOCK_TABLE_FULL, MYF(0)); + break; + case DB_UNDO_RECORD_TOO_BIG: + my_error(ER_UNDO_RECORD_TOO_BIG, MYF(0)); + break; + case DB_CORRUPTION: + my_error(ER_NOT_KEYFILE, MYF(0), table); + break; + case DB_TOO_BIG_RECORD: { + /* Note that in page0zip.ic page_zip_rec_needs_ext() rec_size + is limited to COMPRESSED_REC_MAX_DATA_SIZE (16K) or + REDUNDANT_REC_MAX_DATA_SIZE (16K-1). */ + bool comp = !!(flags & DICT_TF_COMPACT); + ulint free_space = page_get_free_space_of_empty(comp) / 2; + + if (free_space >= ulint(comp ? COMPRESSED_REC_MAX_DATA_SIZE : + REDUNDANT_REC_MAX_DATA_SIZE)) { + free_space = (comp ? COMPRESSED_REC_MAX_DATA_SIZE : + REDUNDANT_REC_MAX_DATA_SIZE) - 1; + } + + my_error(ER_TOO_BIG_ROWSIZE, MYF(0), free_space); + break; + } + case DB_INVALID_NULL: + /* TODO: report the row, as we do for DB_DUPLICATE_KEY */ + my_error(ER_INVALID_USE_OF_NULL, MYF(0)); + break; + case DB_CANT_CREATE_GEOMETRY_OBJECT: + my_error(ER_CANT_CREATE_GEOMETRY_OBJECT, MYF(0)); + break; + case DB_TABLESPACE_EXISTS: + my_error(ER_TABLESPACE_EXISTS, MYF(0), table); + break; + +#ifdef UNIV_DEBUG + case DB_SUCCESS: + case DB_DUPLICATE_KEY: + case DB_ONLINE_LOG_TOO_BIG: + /* These codes should not be passed here. */ + ut_error; +#endif /* UNIV_DEBUG */ + default: + my_error(ER_GET_ERRNO, MYF(0), error, "InnoDB"); + break; + } +} + +/** Determine if fulltext indexes exist in a given table. +@param table MySQL table +@return number of fulltext indexes */ +static uint innobase_fulltext_exist(const TABLE* table) +{ + uint count = 0; + + for (uint i = 0; i < table->s->keys; i++) { + if (table->key_info[i].flags & HA_FULLTEXT) { + count++; + } + } + + return count; +} + +/** Determine whether indexed virtual columns exist in a table. +@param[in] table table definition +@return whether indexes exist on virtual columns */ +static bool innobase_indexed_virtual_exist(const TABLE* table) +{ + const KEY* const end = &table->key_info[table->s->keys]; + + for (const KEY* key = table->key_info; key < end; key++) { + const KEY_PART_INFO* const key_part_end = key->key_part + + key->user_defined_key_parts; + for (const KEY_PART_INFO* key_part = key->key_part; + key_part < key_part_end; key_part++) { + if (!key_part->field->stored_in_db()) + return true; + } + } + + return false; +} + +/** Determine if spatial indexes exist in a given table. +@param table MySQL table +@return whether spatial indexes exist on the table */ +static +bool +innobase_spatial_exist( +/*===================*/ + const TABLE* table) +{ + for (uint i = 0; i < table->s->keys; i++) { + if (table->key_info[i].flags & HA_SPATIAL) { + return(true); + } + } + + return(false); +} + +/** Determine if ALTER_OPTIONS requires rebuilding the table. +@param[in] ha_alter_info the ALTER TABLE operation +@param[in] table metadata before ALTER TABLE +@return whether it is mandatory to rebuild the table */ +static bool alter_options_need_rebuild( + const Alter_inplace_info* ha_alter_info, + const TABLE* table) +{ + DBUG_ASSERT(ha_alter_info->handler_flags & ALTER_OPTIONS); + + if (ha_alter_info->create_info->used_fields + & (HA_CREATE_USED_ROW_FORMAT + | HA_CREATE_USED_KEY_BLOCK_SIZE)) { + /* Specifying ROW_FORMAT or KEY_BLOCK_SIZE requires + rebuilding the table. (These attributes in the .frm + file may disagree with the InnoDB data dictionary, and + the interpretation of thse attributes depends on + InnoDB parameters. That is why we for now always + require a rebuild when these attributes are specified.) */ + return true; + } + + const ha_table_option_struct& alt_opt= + *ha_alter_info->create_info->option_struct; + const ha_table_option_struct& opt= *table->s->option_struct; + + /* Allow an instant change to enable page_compressed, + and any change of page_compression_level. */ + if ((!alt_opt.page_compressed && opt.page_compressed) + || alt_opt.encryption != opt.encryption + || alt_opt.encryption_key_id != opt.encryption_key_id) { + return(true); + } + + return false; +} + +/** Determine if ALTER TABLE needs to rebuild the table +(or perform instant operation). +@param[in] ha_alter_info the ALTER TABLE operation +@param[in] table metadata before ALTER TABLE +@return whether it is necessary to rebuild the table or to alter columns */ +static MY_ATTRIBUTE((nonnull, warn_unused_result)) +bool +innobase_need_rebuild( + const Alter_inplace_info* ha_alter_info, + const TABLE* table) +{ + if ((ha_alter_info->handler_flags & ~(INNOBASE_INPLACE_IGNORE + | INNOBASE_ALTER_NOREBUILD + | INNOBASE_ALTER_INSTANT)) + == ALTER_OPTIONS) { + return alter_options_need_rebuild(ha_alter_info, table); + } + + return !!(ha_alter_info->handler_flags & INNOBASE_ALTER_REBUILD); +} + +/** Check if virtual column in old and new table are in order, excluding +those dropped column. This is needed because when we drop a virtual column, +ALTER_VIRTUAL_COLUMN_ORDER is also turned on, so we can't decide if this +is a real ORDER change or just DROP COLUMN +@param[in] table old TABLE +@param[in] altered_table new TABLE +@param[in] ha_alter_info Structure describing changes to be done +by ALTER TABLE and holding data used during in-place alter. +@return true is all columns in order, false otherwise. */ +static +bool +check_v_col_in_order( + const TABLE* table, + const TABLE* altered_table, + Alter_inplace_info* ha_alter_info) +{ + ulint j = 0; + + /* We don't support any adding new virtual column before + existed virtual column. */ + if (ha_alter_info->handler_flags + & ALTER_ADD_VIRTUAL_COLUMN) { + bool has_new = false; + + for (const Create_field& new_field : + ha_alter_info->alter_info->create_list) { + if (new_field.stored_in_db()) { + continue; + } + + /* Found a new added virtual column. */ + if (!new_field.field) { + has_new = true; + continue; + } + + /* If there's any old virtual column + after the new added virtual column, + order must be changed. */ + if (has_new) { + return(false); + } + } + } + + /* directly return true if ALTER_VIRTUAL_COLUMN_ORDER is not on */ + if (!(ha_alter_info->handler_flags + & ALTER_VIRTUAL_COLUMN_ORDER)) { + return(true); + } + + for (ulint i = 0; i < table->s->fields; i++) { + Field* field = table->field[i]; + + if (field->stored_in_db()) { + continue; + } + + if (field->flags & FIELD_IS_DROPPED) { + continue; + } + + /* Now check if the next virtual column in altered table + matches this column */ + while (j < altered_table->s->fields) { + Field* new_field = altered_table->s->field[j]; + + if (new_field->stored_in_db()) { + j++; + continue; + } + + if (my_strcasecmp(system_charset_info, + field->field_name.str, + new_field->field_name.str) != 0) { + /* different column */ + return(false); + } else { + j++; + break; + } + } + + if (j > altered_table->s->fields) { + /* there should not be less column in new table + without them being in drop list */ + ut_ad(0); + return(false); + } + } + + return(true); +} + +/** Determine if an instant operation is possible for altering columns. +@param[in] ib_table InnoDB table definition +@param[in] ha_alter_info the ALTER TABLE operation +@param[in] table table definition before ALTER TABLE +@param[in] altered_table table definition after ALTER TABLE +@param[in] strict whether to ensure that user records fit */ +static +bool +instant_alter_column_possible( + const dict_table_t& ib_table, + const Alter_inplace_info* ha_alter_info, + const TABLE* table, + const TABLE* altered_table, + bool strict) +{ + const dict_index_t* const pk = ib_table.indexes.start; + ut_ad(pk->is_primary()); + ut_ad(!pk->has_virtual()); + + if (ha_alter_info->handler_flags + & (ALTER_STORED_COLUMN_ORDER | ALTER_DROP_STORED_COLUMN + | ALTER_ADD_STORED_BASE_COLUMN)) { +#if 1 // MDEV-17459: adjust fts_fetch_doc_from_rec() and friends; remove this + if (ib_table.fts || innobase_fulltext_exist(altered_table)) + return false; +#endif +#if 1 // MDEV-17468: fix bugs with indexed virtual columns & remove this + for (const dict_index_t* index = ib_table.indexes.start; + index; index = index->indexes.next) { + if (index->has_virtual()) { + ut_ad(ib_table.n_v_cols + || index->is_corrupted()); + return false; + } + } +#endif + uint n_add = 0, n_nullable = 0, lenlen = 0; + const uint blob_prefix = dict_table_has_atomic_blobs(&ib_table) + ? 0 + : REC_ANTELOPE_MAX_INDEX_COL_LEN; + const uint min_local_len = blob_prefix + ? blob_prefix + FIELD_REF_SIZE + : 2 * FIELD_REF_SIZE; + size_t min_size = 0, max_size = 0; + Field** af = altered_table->field; + Field** const end = altered_table->field + + altered_table->s->fields; + List_iterator_fast<Create_field> cf_it( + ha_alter_info->alter_info->create_list); + + for (; af < end; af++) { + const Create_field* cf = cf_it++; + if (!(*af)->stored_in_db() || cf->field) { + /* Virtual or pre-existing column */ + continue; + } + const bool nullable = (*af)->real_maybe_null(); + const bool is_null = (*af)->is_real_null(); + ut_ad(!is_null || nullable); + n_nullable += nullable; + n_add++; + uint l; + switch ((*af)->type()) { + case MYSQL_TYPE_VARCHAR: + l = reinterpret_cast<const Field_varstring*> + (*af)->get_length(); + variable_length: + if (l >= min_local_len) { + max_size += blob_prefix + + FIELD_REF_SIZE; + if (!is_null) { + min_size += blob_prefix + + FIELD_REF_SIZE; + } + lenlen += 2; + } else { + if (!is_null) { + min_size += l; + } + l = (*af)->pack_length(); + max_size += l; + lenlen += l > 255 ? 2 : 1; + } + break; + case MYSQL_TYPE_GEOMETRY: + case MYSQL_TYPE_TINY_BLOB: + case MYSQL_TYPE_MEDIUM_BLOB: + case MYSQL_TYPE_BLOB: + case MYSQL_TYPE_LONG_BLOB: + l = reinterpret_cast<const Field_blob*> + ((*af))->get_length(); + goto variable_length; + default: + l = (*af)->pack_length(); + if (l > 255 && ib_table.not_redundant()) { + goto variable_length; + } + max_size += l; + if (!is_null) { + min_size += l; + } + } + } + + ulint n_fields = pk->n_fields + n_add; + + if (n_fields >= REC_MAX_N_USER_FIELDS + DATA_N_SYS_COLS) { + return false; + } + + if (pk->is_gen_clust()) { + min_size += DATA_TRX_ID_LEN + DATA_ROLL_PTR_LEN + + DATA_ROW_ID_LEN; + max_size += DATA_TRX_ID_LEN + DATA_ROLL_PTR_LEN + + DATA_ROW_ID_LEN; + } else { + min_size += DATA_TRX_ID_LEN + DATA_ROLL_PTR_LEN; + max_size += DATA_TRX_ID_LEN + DATA_ROLL_PTR_LEN; + } + + uint i = pk->n_fields; + while (i-- > pk->n_core_fields) { + const dict_field_t& f = pk->fields[i]; + if (f.col->is_nullable()) { + n_nullable++; + if (!f.col->is_dropped() + && f.col->def_val.data) { + goto instantly_added_column; + } + } else if (f.fixed_len + && (f.fixed_len <= 255 + || !ib_table.not_redundant())) { + if (ib_table.not_redundant() + || !f.col->is_dropped()) { + min_size += f.fixed_len; + max_size += f.fixed_len; + } + } else if (f.col->is_dropped() || !f.col->is_added()) { + lenlen++; + goto set_max_size; + } else { +instantly_added_column: + ut_ad(f.col->is_added()); + if (f.col->def_val.len >= min_local_len) { + min_size += blob_prefix + + FIELD_REF_SIZE; + lenlen += 2; + } else { + min_size += f.col->def_val.len; + lenlen += f.col->def_val.len + > 255 ? 2 : 1; + } +set_max_size: + if (f.fixed_len + && (f.fixed_len <= 255 + || !ib_table.not_redundant())) { + max_size += f.fixed_len; + } else if (f.col->len >= min_local_len) { + max_size += blob_prefix + + FIELD_REF_SIZE; + } else { + max_size += f.col->len; + } + } + } + + do { + const dict_field_t& f = pk->fields[i]; + if (f.col->is_nullable()) { + n_nullable++; + } else if (f.fixed_len) { + min_size += f.fixed_len; + } else { + lenlen++; + } + } while (i--); + + if (ib_table.instant + || (ha_alter_info->handler_flags + & (ALTER_STORED_COLUMN_ORDER + | ALTER_DROP_STORED_COLUMN))) { + n_fields++; + lenlen += 2; + min_size += FIELD_REF_SIZE; + } + + if (ib_table.not_redundant()) { + min_size += REC_N_NEW_EXTRA_BYTES + + UT_BITS_IN_BYTES(n_nullable) + + lenlen; + } else { + min_size += (n_fields > 255 || min_size > 255) + ? n_fields * 2 : n_fields; + min_size += REC_N_OLD_EXTRA_BYTES; + } + + if (page_zip_rec_needs_ext(min_size, ib_table.not_redundant(), + 0, 0)) { + return false; + } + + if (strict && page_zip_rec_needs_ext(max_size, + ib_table.not_redundant(), + 0, 0)) { + return false; + } + } + // Making table system-versioned instantly is not implemented yet. + if (ha_alter_info->handler_flags & ALTER_ADD_SYSTEM_VERSIONING) { + return false; + } + + static constexpr alter_table_operations avoid_rebuild + = ALTER_ADD_STORED_BASE_COLUMN + | ALTER_DROP_STORED_COLUMN + | ALTER_STORED_COLUMN_ORDER + | ALTER_COLUMN_NULLABLE; + + if (!(ha_alter_info->handler_flags & avoid_rebuild)) { + alter_table_operations flags = ha_alter_info->handler_flags + & ~avoid_rebuild; + /* None of the flags are set that we can handle + specially to avoid rebuild. In this case, we can + allow ALGORITHM=INSTANT, except if some requested + operation requires that the table be rebuilt. */ + if (flags & INNOBASE_ALTER_REBUILD) { + return false; + } + if ((flags & ALTER_OPTIONS) + && alter_options_need_rebuild(ha_alter_info, table)) { + return false; + } + } else if (!ib_table.supports_instant()) { + return false; + } + + /* At the moment, we disallow ADD [UNIQUE] INDEX together with + instant ADD COLUMN. + + The main reason is that the work of instant ADD must be done + in commit_inplace_alter_table(). For the rollback_instant() + to work, we must add the columns to dict_table_t beforehand, + and roll back those changes in case the transaction is rolled + back. + + If we added the columns to the dictionary cache already in the + prepare_inplace_alter_table(), we would have to deal with + column number mismatch in ha_innobase::open(), write_row() and + other functions. */ + + /* FIXME: allow instant ADD COLUMN together with + INNOBASE_ONLINE_CREATE (ADD [UNIQUE] INDEX) on pre-existing + columns. */ + if (ha_alter_info->handler_flags + & ((INNOBASE_ALTER_REBUILD | INNOBASE_ONLINE_CREATE) + & ~ALTER_DROP_STORED_COLUMN + & ~ALTER_STORED_COLUMN_ORDER + & ~ALTER_ADD_STORED_BASE_COLUMN + & ~ALTER_COLUMN_NULLABLE + & ~ALTER_OPTIONS)) { + return false; + } + + if ((ha_alter_info->handler_flags & ALTER_OPTIONS) + && alter_options_need_rebuild(ha_alter_info, table)) { + return false; + } + + if (ha_alter_info->handler_flags & ALTER_COLUMN_NULLABLE) { + if (ib_table.not_redundant()) { + /* Instantaneous removal of NOT NULL is + only supported for ROW_FORMAT=REDUNDANT. */ + return false; + } + if (ib_table.fts_doc_id_index + && !innobase_fulltext_exist(altered_table)) { + /* Removing hidden FTS_DOC_ID_INDEX(FTS_DOC_ID) + requires that the table be rebuilt. */ + return false; + } + + Field** af = altered_table->field; + Field** const end = altered_table->field + + altered_table->s->fields; + for (unsigned c = 0; af < end; af++) { + if (!(*af)->stored_in_db()) { + continue; + } + + const dict_col_t* col = dict_table_get_nth_col( + &ib_table, c++); + + if (!col->ord_part || col->is_nullable() + || !(*af)->real_maybe_null()) { + continue; + } + + /* The column would be changed from NOT NULL. + Ensure that it is not a clustered index key. */ + for (auto i = pk->n_uniq; i--; ) { + if (pk->fields[i].col == col) { + return false; + } + } + } + } + + return true; +} + +/** Check whether the non-const default value for the field +@param[in] field field which could be added or changed +@return true if the non-const default is present. */ +static bool is_non_const_value(Field* field) +{ + return field->default_value + && field->default_value->flags + & uint(~(VCOL_SESSION_FUNC | VCOL_TIME_FUNC)); +} + +/** Set default value for the field. +@param[in] field field which could be added or changed +@return true if the default value is set. */ +static bool set_default_value(Field* field) +{ + /* The added/changed NOT NULL column lacks a DEFAULT value, + or the DEFAULT is the same for all rows. + (Time functions, such as CURRENT_TIMESTAMP(), + are evaluated from a timestamp that is assigned + at the start of the statement. Session + functions, such as USER(), always evaluate the + same within a statement.) */ + + ut_ad(!is_non_const_value(field)); + + /* Compute the DEFAULT values of non-constant columns + (VCOL_SESSION_FUNC | VCOL_TIME_FUNC). */ + switch (field->set_default()) { + case 0: /* OK */ + case 3: /* DATETIME to TIME or DATE conversion */ + return true; + case -1: /* OOM, or GEOMETRY type mismatch */ + case 1: /* A number adjusted to the min/max value */ + case 2: /* String truncation, or conversion problem */ + break; + } + + return false; +} + +/** Check whether the table has the FTS_DOC_ID column +@param[in] table InnoDB table with fulltext index +@param[in] altered_table MySQL table with fulltext index +@param[out] fts_doc_col_no The column number for Doc ID, + or ULINT_UNDEFINED if it is of wrong type +@param[out] num_v Number of virtual column +@param[in] check_only check only whether fts doc id exist. +@return whether there exists an FTS_DOC_ID column */ +static +bool +innobase_fts_check_doc_id_col( + const dict_table_t* table, + const TABLE* altered_table, + ulint* fts_doc_col_no, + ulint* num_v, + bool check_only=false) +{ + *fts_doc_col_no = ULINT_UNDEFINED; + + const uint n_cols = altered_table->s->fields; + ulint i; + int err = 0; + *num_v = 0; + + for (i = 0; i < n_cols; i++) { + const Field* field = altered_table->field[i]; + + if (!field->stored_in_db()) { + (*num_v)++; + } + + if (my_strcasecmp(system_charset_info, + field->field_name.str, FTS_DOC_ID_COL_NAME)) { + continue; + } + + if (strcmp(field->field_name.str, FTS_DOC_ID_COL_NAME)) { + err = ER_WRONG_COLUMN_NAME; + } else if (field->type() != MYSQL_TYPE_LONGLONG + || field->pack_length() != 8 + || field->real_maybe_null() + || !(field->flags & UNSIGNED_FLAG) + || !field->stored_in_db()) { + err = ER_INNODB_FT_WRONG_DOCID_COLUMN; + } else { + *fts_doc_col_no = i - *num_v; + } + + if (err && !check_only) { + my_error(err, MYF(0), field->field_name.str); + } + + return(true); + } + + if (!table) { + return(false); + } + + /* Not to count the virtual columns */ + i -= *num_v; + + for (; i + DATA_N_SYS_COLS < (uint) table->n_cols; i++) { + const char* name = dict_table_get_col_name(table, i); + + if (strcmp(name, FTS_DOC_ID_COL_NAME) == 0) { +#ifdef UNIV_DEBUG + const dict_col_t* col; + + col = dict_table_get_nth_col(table, i); + + /* Because the FTS_DOC_ID does not exist in + the MySQL data dictionary, this must be the + internally created FTS_DOC_ID column. */ + ut_ad(col->mtype == DATA_INT); + ut_ad(col->len == 8); + ut_ad(col->prtype & DATA_NOT_NULL); + ut_ad(col->prtype & DATA_UNSIGNED); +#endif /* UNIV_DEBUG */ + *fts_doc_col_no = i; + return(true); + } + } + + return(false); +} + +/** Check whether the table is empty. +@param[in] table table to be checked +@return true if table is empty */ +static bool innobase_table_is_empty(const dict_table_t *table) +{ + dict_index_t *clust_index= dict_table_get_first_index(table); + mtr_t mtr; + btr_pcur_t pcur; + buf_block_t *block; + page_cur_t *cur; + const rec_t *rec; + bool next_page= false; + + mtr.start(); + btr_pcur_open_at_index_side(true, clust_index, BTR_SEARCH_LEAF, + &pcur, true, 0, &mtr); + btr_pcur_move_to_next_user_rec(&pcur, &mtr); + if (!rec_is_metadata(btr_pcur_get_rec(&pcur), *clust_index)) + btr_pcur_move_to_prev_on_page(&pcur); +scan_leaf: + cur= btr_pcur_get_page_cur(&pcur); + page_cur_move_to_next(cur); +next_page: + if (next_page) + { + uint32_t next_page_no= btr_page_get_next(page_cur_get_page(cur)); + if (next_page_no == FIL_NULL) + { + mtr.commit(); + return true; + } + + next_page= false; + block= page_cur_get_block(cur); + block= btr_block_get(*clust_index, next_page_no, BTR_SEARCH_LEAF, false, + &mtr); + btr_leaf_page_release(page_cur_get_block(cur), BTR_SEARCH_LEAF, &mtr); + page_cur_set_before_first(block, cur); + page_cur_move_to_next(cur); + } + + rec= page_cur_get_rec(cur); + if (rec_get_deleted_flag(rec, dict_table_is_comp(table))); + else if (!page_rec_is_supremum(rec)) + { + mtr.commit(); + return false; + } + else + { + next_page= true; + goto next_page; + } + goto scan_leaf; +} + +/** Check if InnoDB supports a particular alter table in-place +@param altered_table TABLE object for new version of table. +@param ha_alter_info Structure describing changes to be done +by ALTER TABLE and holding data used during in-place alter. + +@retval HA_ALTER_INPLACE_NOT_SUPPORTED Not supported +@retval HA_ALTER_INPLACE_INSTANT +MDL_EXCLUSIVE is needed for executing prepare_inplace_alter_table() +and commit_inplace_alter_table(). inplace_alter_table() will not be called. +@retval HA_ALTER_INPLACE_COPY_NO_LOCK +MDL_EXCLUSIVE in prepare_inplace_alter_table(), which can be downgraded to +LOCK=NONE for rebuilding the table in inplace_alter_table() +@retval HA_ALTER_INPLACE_COPY_LOCK +MDL_EXCLUSIVE in prepare_inplace_alter_table(), which can be downgraded to +LOCK=SHARED for rebuilding the table in inplace_alter_table() +@retval HA_ALTER_INPLACE_NOCOPY_NO_LOCK +MDL_EXCLUSIVE in prepare_inplace_alter_table(), which can be downgraded to +LOCK=NONE for inplace_alter_table() which will not rebuild the table +@retval HA_ALTER_INPLACE_NOCOPY_LOCK +MDL_EXCLUSIVE in prepare_inplace_alter_table(), which can be downgraded to +LOCK=SHARED for inplace_alter_table() which will not rebuild the table +*/ + +enum_alter_inplace_result +ha_innobase::check_if_supported_inplace_alter( + TABLE* altered_table, + Alter_inplace_info* ha_alter_info) +{ + DBUG_ENTER("check_if_supported_inplace_alter"); + + if ((ha_alter_info->handler_flags + & INNOBASE_ALTER_VERSIONED_REBUILD) + && altered_table->versioned(VERS_TIMESTAMP)) { + ha_alter_info->unsupported_reason = + "Not implemented for system-versioned timestamp tables"; + DBUG_RETURN(HA_ALTER_INPLACE_NOT_SUPPORTED); + } + + /* Before 10.2.2 information about virtual columns was not stored in + system tables. We need to do a full alter to rebuild proper 10.2.2+ + metadata with the information about virtual columns */ + if (omits_virtual_cols(*table_share)) { + DBUG_RETURN(HA_ALTER_INPLACE_NOT_SUPPORTED); + } + + if (high_level_read_only) { + ha_alter_info->unsupported_reason = + my_get_err_msg(ER_READ_ONLY_MODE); + + DBUG_RETURN(HA_ALTER_INPLACE_NOT_SUPPORTED); + } + + if (altered_table->s->fields > REC_MAX_N_USER_FIELDS) { + /* Deny the inplace ALTER TABLE. MySQL will try to + re-create the table and ha_innobase::create() will + return an error too. This is how we effectively + deny adding too many columns to a table. */ + ha_alter_info->unsupported_reason = + my_get_err_msg(ER_TOO_MANY_FIELDS); + DBUG_RETURN(HA_ALTER_INPLACE_NOT_SUPPORTED); + } + + update_thd(); + + if (ha_alter_info->handler_flags + & ~(INNOBASE_INPLACE_IGNORE + | INNOBASE_ALTER_INSTANT + | INNOBASE_ALTER_NOREBUILD + | INNOBASE_ALTER_REBUILD)) { + + if (ha_alter_info->handler_flags + & ALTER_STORED_COLUMN_TYPE) { + ha_alter_info->unsupported_reason = my_get_err_msg( + ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_COLUMN_TYPE); + } + + DBUG_RETURN(HA_ALTER_INPLACE_NOT_SUPPORTED); + } + + /* Only support online add foreign key constraint when + check_foreigns is turned off */ + if ((ha_alter_info->handler_flags & ALTER_ADD_FOREIGN_KEY) + && m_prebuilt->trx->check_foreigns) { + ha_alter_info->unsupported_reason = my_get_err_msg( + ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_FK_CHECK); + DBUG_RETURN(HA_ALTER_INPLACE_NOT_SUPPORTED); + } + + const char* reason_rebuild = NULL; + + switch (innodb_instant_alter_column_allowed) { + case 0: /* never */ + if ((ha_alter_info->handler_flags + & (ALTER_ADD_STORED_BASE_COLUMN + | ALTER_STORED_COLUMN_ORDER + | ALTER_DROP_STORED_COLUMN)) + || m_prebuilt->table->is_instant()) { + reason_rebuild = + "innodb_instant_alter_column_allowed=never"; +innodb_instant_alter_column_allowed_reason: + if (ha_alter_info->handler_flags + & ALTER_RECREATE_TABLE) { + reason_rebuild = NULL; + } else { + ha_alter_info->handler_flags + |= ALTER_RECREATE_TABLE; + ha_alter_info->unsupported_reason + = reason_rebuild; + } + } + break; + case 1: /* add_last */ + if ((ha_alter_info->handler_flags + & (ALTER_STORED_COLUMN_ORDER | ALTER_DROP_STORED_COLUMN)) + || m_prebuilt->table->instant) { + reason_rebuild = "innodb_instant_atler_column_allowed=" + "add_last"; + goto innodb_instant_alter_column_allowed_reason; + } + } + + switch (ha_alter_info->handler_flags & ~INNOBASE_INPLACE_IGNORE) { + case ALTER_OPTIONS: + if (alter_options_need_rebuild(ha_alter_info, table)) { + reason_rebuild = my_get_err_msg( + ER_ALTER_OPERATION_TABLE_OPTIONS_NEED_REBUILD); + ha_alter_info->unsupported_reason = reason_rebuild; + break; + } + /* fall through */ + case 0: + DBUG_RETURN(HA_ALTER_INPLACE_INSTANT); + } + + /* InnoDB cannot IGNORE when creating unique indexes. IGNORE + should silently delete some duplicate rows. Our inplace_alter + code will not delete anything from existing indexes. */ + if (ha_alter_info->ignore + && (ha_alter_info->handler_flags + & (ALTER_ADD_PK_INDEX | ALTER_ADD_UNIQUE_INDEX))) { + ha_alter_info->unsupported_reason = my_get_err_msg( + ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_IGNORE); + DBUG_RETURN(HA_ALTER_INPLACE_NOT_SUPPORTED); + } + + /* DROP PRIMARY KEY is only allowed in combination with ADD + PRIMARY KEY. */ + if ((ha_alter_info->handler_flags + & (ALTER_ADD_PK_INDEX | ALTER_DROP_PK_INDEX)) + == ALTER_DROP_PK_INDEX) { + ha_alter_info->unsupported_reason = my_get_err_msg( + ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_NOPK); + DBUG_RETURN(HA_ALTER_INPLACE_NOT_SUPPORTED); + } + + if (ha_alter_info->handler_flags & ALTER_COLUMN_NULLABLE) { + /* If a NOT NULL attribute is going to be removed and + a UNIQUE INDEX on the column had been promoted to an + implicit PRIMARY KEY, the table should be rebuilt by + ALGORITHM=COPY. (Theoretically, we could support + rebuilding by ALGORITHM=INPLACE if a PRIMARY KEY is + going to be added, either explicitly or by promoting + another UNIQUE KEY.) */ + const uint my_primary_key = altered_table->s->primary_key; + + if (UNIV_UNLIKELY(my_primary_key >= MAX_KEY) + && !dict_index_is_auto_gen_clust( + dict_table_get_first_index(m_prebuilt->table))) { + ha_alter_info->unsupported_reason = my_get_err_msg( + ER_PRIMARY_CANT_HAVE_NULL); + DBUG_RETURN(HA_ALTER_INPLACE_NOT_SUPPORTED); + } + } + + /* + InnoDB in different MariaDB versions was generating different mtype + codes for certain types. In some cases the signed/unsigned bit was + generated differently too. + + Inplace ALTER would change the mtype/unsigned_flag (to what the + current code generates) without changing the underlying data + represenation, and it might result in data corruption. + + Don't do inplace ALTER if mtype/unsigned_flag are wrong. + */ + for (ulint i = 0, icol= 0; i < table->s->fields; i++) { + const Field* field = table->field[i]; + const dict_col_t* col = dict_table_get_nth_col( + m_prebuilt->table, icol); + unsigned unsigned_flag; + + if (!field->stored_in_db()) { + continue; + } + + icol++; + + if (col->mtype != get_innobase_type_from_mysql_type( + &unsigned_flag, field)) { + + DBUG_RETURN(HA_ALTER_INPLACE_NOT_SUPPORTED); + } + + if ((col->prtype & DATA_UNSIGNED) != unsigned_flag) { + + DBUG_RETURN(HA_ALTER_INPLACE_NOT_SUPPORTED); + } + } + + ulint n_indexes = UT_LIST_GET_LEN((m_prebuilt->table)->indexes); + + /* If InnoDB dictionary and MySQL frm file are not consistent + use "Copy" method. */ + if (m_prebuilt->table->dict_frm_mismatch) { + + ha_alter_info->unsupported_reason = my_get_err_msg( + ER_NO_SUCH_INDEX); + ib_push_frm_error(m_user_thd, m_prebuilt->table, altered_table, + n_indexes, true); + + DBUG_RETURN(HA_ALTER_INPLACE_NOT_SUPPORTED); + } + + /* '0000-00-00' value isn't allowed for datetime datatype + for newly added column when table is not empty */ + if (ha_alter_info->error_if_not_empty + && !innobase_table_is_empty(m_prebuilt->table)) { + DBUG_RETURN(HA_ALTER_INPLACE_NOT_SUPPORTED); + } + + const bool add_drop_v_cols = !!(ha_alter_info->handler_flags + & (ALTER_ADD_VIRTUAL_COLUMN + | ALTER_DROP_VIRTUAL_COLUMN + | ALTER_VIRTUAL_COLUMN_ORDER)); + + /* We should be able to do the operation in-place. + See if we can do it online (LOCK=NONE) or without rebuild. */ + bool online = true, need_rebuild = false; + const uint fulltext_indexes = innobase_fulltext_exist(altered_table); + + /* Fix the key parts. */ + for (KEY* new_key = ha_alter_info->key_info_buffer; + new_key < ha_alter_info->key_info_buffer + + ha_alter_info->key_count; + new_key++) { + + /* Do not support adding/droping a virtual column, while + there is a table rebuild caused by adding a new FTS_DOC_ID */ + if ((new_key->flags & HA_FULLTEXT) && add_drop_v_cols + && !DICT_TF2_FLAG_IS_SET(m_prebuilt->table, + DICT_TF2_FTS_HAS_DOC_ID)) { + ha_alter_info->unsupported_reason = + MSG_UNSUPPORTED_ALTER_ONLINE_ON_VIRTUAL_COLUMN; + DBUG_RETURN(HA_ALTER_INPLACE_NOT_SUPPORTED); + } + + for (KEY_PART_INFO* key_part = new_key->key_part; + key_part < (new_key->key_part + + new_key->user_defined_key_parts); + key_part++) { + DBUG_ASSERT(key_part->fieldnr + < altered_table->s->fields); + + const Create_field* new_field + = ha_alter_info->alter_info->create_list.elem( + key_part->fieldnr); + + DBUG_ASSERT(new_field); + + key_part->field = altered_table->field[ + key_part->fieldnr]; + + /* In some special cases InnoDB emits "false" + duplicate key errors with NULL key values. Let + us play safe and ensure that we can correctly + print key values even in such cases. */ + key_part->null_offset = key_part->field->null_offset(); + key_part->null_bit = key_part->field->null_bit; + + if (new_field->field) { + /* This is an existing column. */ + continue; + } + + /* This is an added column. */ + DBUG_ASSERT(ha_alter_info->handler_flags + & ALTER_ADD_COLUMN); + + /* We cannot replace a hidden FTS_DOC_ID + with a user-visible FTS_DOC_ID. */ + if (fulltext_indexes && m_prebuilt->table->fts + && !my_strcasecmp( + system_charset_info, + key_part->field->field_name.str, + FTS_DOC_ID_COL_NAME)) { + ha_alter_info->unsupported_reason = my_get_err_msg( + ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_HIDDEN_FTS); + DBUG_RETURN(HA_ALTER_INPLACE_NOT_SUPPORTED); + } + + DBUG_ASSERT((MTYP_TYPENR(key_part->field->unireg_check) + == Field::NEXT_NUMBER) + == !!(key_part->field->flags + & AUTO_INCREMENT_FLAG)); + + if (key_part->field->flags & AUTO_INCREMENT_FLAG) { + /* We cannot assign AUTO_INCREMENT values + during online or instant ALTER. */ + DBUG_ASSERT(key_part->field == altered_table + -> found_next_number_field); + + if (ha_alter_info->online) { + ha_alter_info->unsupported_reason = my_get_err_msg( + ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_AUTOINC); + } + + online = false; + need_rebuild = true; + } + + if (!key_part->field->stored_in_db()) { + /* Do not support adding index on newly added + virtual column, while there is also a drop + virtual column in the same clause */ + if (ha_alter_info->handler_flags + & ALTER_DROP_VIRTUAL_COLUMN) { + ha_alter_info->unsupported_reason = + MSG_UNSUPPORTED_ALTER_ONLINE_ON_VIRTUAL_COLUMN; + + DBUG_RETURN(HA_ALTER_INPLACE_NOT_SUPPORTED); + } + + if (ha_alter_info->online + && !ha_alter_info->unsupported_reason) { + ha_alter_info->unsupported_reason = + MSG_UNSUPPORTED_ALTER_ONLINE_ON_VIRTUAL_COLUMN; + } + + online = false; + } + } + } + + DBUG_ASSERT(!m_prebuilt->table->fts + || (m_prebuilt->table->fts->doc_col <= table->s->fields)); + + DBUG_ASSERT(!m_prebuilt->table->fts + || (m_prebuilt->table->fts->doc_col + < dict_table_get_n_user_cols(m_prebuilt->table))); + + if (fulltext_indexes && m_prebuilt->table->fts) { + /* FULLTEXT indexes are supposed to remain. */ + /* Disallow DROP INDEX FTS_DOC_ID_INDEX */ + + for (uint i = 0; i < ha_alter_info->index_drop_count; i++) { + if (!my_strcasecmp( + system_charset_info, + ha_alter_info->index_drop_buffer[i]->name.str, + FTS_DOC_ID_INDEX_NAME)) { + ha_alter_info->unsupported_reason = my_get_err_msg( + ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_CHANGE_FTS); + DBUG_RETURN(HA_ALTER_INPLACE_NOT_SUPPORTED); + } + } + + /* InnoDB can have a hidden FTS_DOC_ID_INDEX on a + visible FTS_DOC_ID column as well. Prevent dropping or + renaming the FTS_DOC_ID. */ + + for (Field** fp = table->field; *fp; fp++) { + if (!((*fp)->flags + & (FIELD_IS_RENAMED | FIELD_IS_DROPPED))) { + continue; + } + + if (!my_strcasecmp( + system_charset_info, + (*fp)->field_name.str, + FTS_DOC_ID_COL_NAME)) { + ha_alter_info->unsupported_reason = my_get_err_msg( + ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_CHANGE_FTS); + DBUG_RETURN(HA_ALTER_INPLACE_NOT_SUPPORTED); + } + } + } + + m_prebuilt->trx->will_lock = true; + + /* When changing a NULL column to NOT NULL and specifying a + DEFAULT value, ensure that the DEFAULT expression is a constant. + Also, in ADD COLUMN, for now we only support a + constant DEFAULT expression. */ + Field **af = altered_table->field; + bool fts_need_rebuild = false; + need_rebuild = need_rebuild + || innobase_need_rebuild(ha_alter_info, table); + + for (Create_field& cf : ha_alter_info->alter_info->create_list) { + DBUG_ASSERT(cf.field + || (ha_alter_info->handler_flags + & ALTER_ADD_COLUMN)); + + if (const Field* f = cf.field) { + /* An AUTO_INCREMENT attribute can only + be added to an existing column by ALGORITHM=COPY, + but we can remove the attribute. */ + ut_ad((MTYP_TYPENR((*af)->unireg_check) + != Field::NEXT_NUMBER) + || (MTYP_TYPENR(f->unireg_check) + == Field::NEXT_NUMBER)); + if (!f->real_maybe_null() || (*af)->real_maybe_null()) + goto next_column; + /* We are changing an existing column + from NULL to NOT NULL. */ + DBUG_ASSERT(ha_alter_info->handler_flags + & ALTER_COLUMN_NOT_NULLABLE); + /* Virtual columns are never NOT NULL. */ + DBUG_ASSERT(f->stored_in_db()); + switch ((*af)->type()) { + case MYSQL_TYPE_TIMESTAMP: + case MYSQL_TYPE_TIMESTAMP2: + /* Inserting NULL into a TIMESTAMP column + would cause the DEFAULT value to be + replaced. Ensure that the DEFAULT + expression is not changing during + ALTER TABLE. */ + if (!(*af)->default_value + && (*af)->is_real_null()) { + /* No DEFAULT value is + specified. We can report + errors for any NULL values for + the TIMESTAMP. */ + goto next_column; + } + break; + default: + /* For any other data type, NULL + values are not converted. */ + goto next_column; + } + + ha_alter_info->unsupported_reason = my_get_err_msg( + ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_NOT_NULL); + } else if (!is_non_const_value(*af) + && set_default_value(*af)) { + if (fulltext_indexes > 1 + && !my_strcasecmp(system_charset_info, + (*af)->field_name.str, + FTS_DOC_ID_COL_NAME)) { + /* If a hidden FTS_DOC_ID column exists + (because of FULLTEXT INDEX), it cannot + be replaced with a user-created one + except when using ALGORITHM=COPY. */ + ha_alter_info->unsupported_reason = + my_get_err_msg(ER_INNODB_FT_LIMIT); + DBUG_RETURN(HA_ALTER_INPLACE_NOT_SUPPORTED); + } + goto next_column; + } + + DBUG_RETURN(HA_ALTER_INPLACE_NOT_SUPPORTED); + +next_column: + af++; + } + + const bool supports_instant = instant_alter_column_possible( + *m_prebuilt->table, ha_alter_info, table, altered_table, + is_innodb_strict_mode()); + if (add_drop_v_cols) { + ulonglong flags = ha_alter_info->handler_flags; + + /* TODO: uncomment the flags below, once we start to + support them */ + + flags &= ~(ALTER_ADD_VIRTUAL_COLUMN + | ALTER_DROP_VIRTUAL_COLUMN + | ALTER_VIRTUAL_COLUMN_ORDER + | ALTER_VIRTUAL_GCOL_EXPR + | ALTER_COLUMN_VCOL + /* + | ALTER_ADD_STORED_BASE_COLUMN + | ALTER_DROP_STORED_COLUMN + | ALTER_STORED_COLUMN_ORDER + | ALTER_ADD_UNIQUE_INDEX + */ + | ALTER_ADD_NON_UNIQUE_NON_PRIM_INDEX + | ALTER_DROP_NON_UNIQUE_NON_PRIM_INDEX); + if (supports_instant) { + flags &= ~(ALTER_DROP_STORED_COLUMN +#if 0 /* MDEV-17468: remove check_v_col_in_order() and fix the code */ + | ALTER_ADD_STORED_BASE_COLUMN +#endif + | ALTER_STORED_COLUMN_ORDER); + } + if (flags != 0 + || IF_PARTITIONING((altered_table->s->partition_info_str + && altered_table->s->partition_info_str_len), 0) + || (!check_v_col_in_order( + this->table, altered_table, ha_alter_info))) { + ha_alter_info->unsupported_reason = + MSG_UNSUPPORTED_ALTER_ONLINE_ON_VIRTUAL_COLUMN; + DBUG_RETURN(HA_ALTER_INPLACE_NOT_SUPPORTED); + } + } + + if (supports_instant && !(ha_alter_info->handler_flags + & INNOBASE_ALTER_NOREBUILD)) { + DBUG_RETURN(HA_ALTER_INPLACE_INSTANT); + } + + if (need_rebuild + && (fulltext_indexes + || innobase_spatial_exist(altered_table) + || innobase_indexed_virtual_exist(altered_table))) { + /* If the table already contains fulltext indexes, + refuse to rebuild the table natively altogether. */ + if (fulltext_indexes > 1) { +cannot_create_many_fulltext_index: + ha_alter_info->unsupported_reason = + my_get_err_msg(ER_INNODB_FT_LIMIT); + DBUG_RETURN(HA_ALTER_INPLACE_NOT_SUPPORTED); + } + + if (!online || !ha_alter_info->online + || ha_alter_info->unsupported_reason != reason_rebuild) { + /* Either LOCK=NONE was not requested, or we already + gave specific reason to refuse it. */ + } else if (fulltext_indexes) { + ha_alter_info->unsupported_reason = my_get_err_msg( + ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_FTS); + } else if (innobase_spatial_exist(altered_table)) { + ha_alter_info->unsupported_reason = my_get_err_msg( + ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_GIS); + } else { + /* MDEV-14341 FIXME: Remove this limitation. */ + ha_alter_info->unsupported_reason = + "online rebuild with indexed virtual columns"; + } + + online = false; + } + + if (ha_alter_info->handler_flags + & ALTER_ADD_NON_UNIQUE_NON_PRIM_INDEX) { + /* ADD FULLTEXT|SPATIAL INDEX requires a lock. + + We could do ADD FULLTEXT INDEX without a lock if the + table already contains an FTS_DOC_ID column, but in + that case we would have to apply the modification log + to the full-text indexes. + + We could also do ADD SPATIAL INDEX by implementing + row_log_apply() for it. */ + bool add_fulltext = false; + + for (uint i = 0; i < ha_alter_info->index_add_count; i++) { + const KEY* key = + &ha_alter_info->key_info_buffer[ + ha_alter_info->index_add_buffer[i]]; + if (key->flags & HA_FULLTEXT) { + DBUG_ASSERT(!(key->flags & HA_KEYFLAG_MASK + & ~(HA_FULLTEXT + | HA_PACK_KEY + | HA_GENERATED_KEY + | HA_BINARY_PACK_KEY))); + if (add_fulltext) { + goto cannot_create_many_fulltext_index; + } + + add_fulltext = true; + if (ha_alter_info->online + && !ha_alter_info->unsupported_reason) { + ha_alter_info->unsupported_reason = my_get_err_msg( + ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_FTS); + } + + online = false; + + /* Full text search index exists, check + whether the table already has DOC ID column. + If not, InnoDB have to rebuild the table to + add a Doc ID hidden column and change + primary index. */ + ulint fts_doc_col_no; + ulint num_v = 0; + + fts_need_rebuild = + !innobase_fts_check_doc_id_col( + m_prebuilt->table, + altered_table, + &fts_doc_col_no, &num_v, true); + } + + if (online && (key->flags & HA_SPATIAL)) { + + if (ha_alter_info->online) { + ha_alter_info->unsupported_reason = my_get_err_msg( + ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_GIS); + } + + online = false; + } + } + } + + // FIXME: implement Online DDL for system-versioned operations + if (ha_alter_info->handler_flags & INNOBASE_ALTER_VERSIONED_REBUILD) { + + if (ha_alter_info->online) { + ha_alter_info->unsupported_reason = + "Not implemented for system-versioned operations"; + } + + online = false; + } + + if ((need_rebuild && !supports_instant) || fts_need_rebuild) { + ha_alter_info->handler_flags |= ALTER_RECREATE_TABLE; + DBUG_RETURN(online + ? HA_ALTER_INPLACE_COPY_NO_LOCK + : HA_ALTER_INPLACE_COPY_LOCK); + } + + if (ha_alter_info->unsupported_reason) { + } else if (ha_alter_info->handler_flags & INNOBASE_ONLINE_CREATE) { + ha_alter_info->unsupported_reason = "ADD INDEX"; + } else { + ha_alter_info->unsupported_reason = "DROP INDEX"; + } + + DBUG_RETURN(online + ? HA_ALTER_INPLACE_NOCOPY_NO_LOCK + : HA_ALTER_INPLACE_NOCOPY_LOCK); +} + +/*************************************************************//** +Initialize the dict_foreign_t structure with supplied info +@return true if added, false if duplicate foreign->id */ +static MY_ATTRIBUTE((nonnull(1,3,5,7))) +bool +innobase_init_foreign( +/*==================*/ + dict_foreign_t* foreign, /*!< in/out: structure to + initialize */ + const char* constraint_name, /*!< in/out: constraint name if + exists */ + dict_table_t* table, /*!< in: foreign table */ + dict_index_t* index, /*!< in: foreign key index */ + const char** column_names, /*!< in: foreign key column + names */ + ulint num_field, /*!< in: number of columns */ + const char* referenced_table_name, /*!< in: referenced table + name */ + dict_table_t* referenced_table, /*!< in: referenced table */ + dict_index_t* referenced_index, /*!< in: referenced index */ + const char** referenced_column_names,/*!< in: referenced column + names */ + ulint referenced_num_field) /*!< in: number of referenced + columns */ +{ + ut_ad(mutex_own(&dict_sys.mutex)); + + if (constraint_name) { + ulint db_len; + + /* Catenate 'databasename/' to the constraint name specified + by the user: we conceive the constraint as belonging to the + same MySQL 'database' as the table itself. We store the name + to foreign->id. */ + + db_len = dict_get_db_name_len(table->name.m_name); + + foreign->id = static_cast<char*>(mem_heap_alloc( + foreign->heap, db_len + strlen(constraint_name) + 2)); + + memcpy(foreign->id, table->name.m_name, db_len); + foreign->id[db_len] = '/'; + strcpy(foreign->id + db_len + 1, constraint_name); + + /* Check if any existing foreign key has the same id, + this is needed only if user supplies the constraint name */ + + if (table->foreign_set.find(foreign) + != table->foreign_set.end()) { + return(false); + } + } + + foreign->foreign_table = table; + foreign->foreign_table_name = mem_heap_strdup( + foreign->heap, table->name.m_name); + dict_mem_foreign_table_name_lookup_set(foreign, TRUE); + + foreign->foreign_index = index; + foreign->n_fields = static_cast<unsigned>(num_field) + & dict_index_t::MAX_N_FIELDS; + + foreign->foreign_col_names = static_cast<const char**>( + mem_heap_alloc(foreign->heap, num_field * sizeof(void*))); + + for (ulint i = 0; i < foreign->n_fields; i++) { + foreign->foreign_col_names[i] = mem_heap_strdup( + foreign->heap, column_names[i]); + } + + foreign->referenced_index = referenced_index; + foreign->referenced_table = referenced_table; + + foreign->referenced_table_name = mem_heap_strdup( + foreign->heap, referenced_table_name); + dict_mem_referenced_table_name_lookup_set(foreign, TRUE); + + foreign->referenced_col_names = static_cast<const char**>( + mem_heap_alloc(foreign->heap, + referenced_num_field * sizeof(void*))); + + for (ulint i = 0; i < foreign->n_fields; i++) { + foreign->referenced_col_names[i] + = mem_heap_strdup(foreign->heap, + referenced_column_names[i]); + } + + return(true); +} + +/*************************************************************//** +Check whether the foreign key options is legit +@return true if it is */ +static MY_ATTRIBUTE((nonnull, warn_unused_result)) +bool +innobase_check_fk_option( +/*=====================*/ + const dict_foreign_t* foreign) /*!< in: foreign key */ +{ + if (!foreign->foreign_index) { + return(true); + } + + if (foreign->type & (DICT_FOREIGN_ON_UPDATE_SET_NULL + | DICT_FOREIGN_ON_DELETE_SET_NULL)) { + + for (ulint j = 0; j < foreign->n_fields; j++) { + if ((dict_index_get_nth_col( + foreign->foreign_index, j)->prtype) + & DATA_NOT_NULL) { + + /* It is not sensible to define + SET NULL if the column is not + allowed to be NULL! */ + return(false); + } + } + } + + return(true); +} + +/*************************************************************//** +Set foreign key options +@return true if successfully set */ +static MY_ATTRIBUTE((nonnull, warn_unused_result)) +bool +innobase_set_foreign_key_option( +/*============================*/ + dict_foreign_t* foreign, /*!< in:InnoDB Foreign key */ + Foreign_key* fk_key) /*!< in: Foreign key info from + MySQL */ +{ + ut_ad(!foreign->type); + + switch (fk_key->delete_opt) { + case FK_OPTION_NO_ACTION: + case FK_OPTION_RESTRICT: + case FK_OPTION_SET_DEFAULT: + foreign->type = DICT_FOREIGN_ON_DELETE_NO_ACTION; + break; + case FK_OPTION_CASCADE: + foreign->type = DICT_FOREIGN_ON_DELETE_CASCADE; + break; + case FK_OPTION_SET_NULL: + foreign->type = DICT_FOREIGN_ON_DELETE_SET_NULL; + break; + case FK_OPTION_UNDEF: + break; + } + + switch (fk_key->update_opt) { + case FK_OPTION_NO_ACTION: + case FK_OPTION_RESTRICT: + case FK_OPTION_SET_DEFAULT: + foreign->type |= DICT_FOREIGN_ON_UPDATE_NO_ACTION; + break; + case FK_OPTION_CASCADE: + foreign->type |= DICT_FOREIGN_ON_UPDATE_CASCADE; + break; + case FK_OPTION_SET_NULL: + foreign->type |= DICT_FOREIGN_ON_UPDATE_SET_NULL; + break; + case FK_OPTION_UNDEF: + break; + } + + return(innobase_check_fk_option(foreign)); +} + +/*******************************************************************//** +Check if a foreign key constraint can make use of an index +that is being created. +@param[in] col_names column names +@param[in] n_cols number of columns +@param[in] keys index information +@param[in] add indexes being created +@return useable index, or NULL if none found */ +static MY_ATTRIBUTE((nonnull, warn_unused_result)) +const KEY* +innobase_find_equiv_index( + const char*const* col_names, + uint n_cols, + const KEY* keys, + span<uint> add) +{ + for (span<uint>::iterator it = add.begin(), end = add.end(); it != end; + ++it) { + const KEY* key = &keys[*it]; + + if (key->user_defined_key_parts < n_cols + || key->flags & HA_SPATIAL) { +no_match: + continue; + } + + for (uint j = 0; j < n_cols; j++) { + const KEY_PART_INFO& key_part = key->key_part[j]; + uint32 col_len + = key_part.field->pack_length(); + + /* Any index on virtual columns cannot be used + for reference constaint */ + if (!key_part.field->stored_in_db()) { + goto no_match; + } + + /* The MySQL pack length contains 1 or 2 bytes + length field for a true VARCHAR. */ + + if (key_part.field->type() == MYSQL_TYPE_VARCHAR) { + col_len -= static_cast<const Field_varstring*>( + key_part.field)->length_bytes; + } + + if (key_part.length < col_len) { + + /* Column prefix indexes cannot be + used for FOREIGN KEY constraints. */ + goto no_match; + } + + if (innobase_strcasecmp(col_names[j], + key_part.field->field_name.str)) { + /* Name mismatch */ + goto no_match; + } + } + + return(key); + } + + return(NULL); +} + +/*************************************************************//** +Find an index whose first fields are the columns in the array +in the same order and is not marked for deletion +@return matching index, NULL if not found */ +static MY_ATTRIBUTE((nonnull(1,4), warn_unused_result)) +dict_index_t* +innobase_find_fk_index( +/*===================*/ + dict_table_t* table, /*!< in: table */ + const char** col_names, + /*!< in: column names, or NULL + to use table->col_names */ + span<dict_index_t*> drop_index, + /*!< in: indexes to be dropped */ + const char** columns,/*!< in: array of column names */ + ulint n_cols) /*!< in: number of columns */ +{ + dict_index_t* index; + + index = dict_table_get_first_index(table); + + while (index != NULL) { + if (dict_foreign_qualify_index(table, col_names, columns, + n_cols, index, NULL, true, 0, + NULL, NULL, NULL) + && std::find(drop_index.begin(), drop_index.end(), index) + == drop_index.end()) { + return index; + } + + index = dict_table_get_next_index(index); + } + + return(NULL); +} + +/** Check whether given column is a base of stored column. +@param[in] col_name column name +@param[in] table table +@param[in] s_cols list of stored columns +@return true if the given column is a base of stored column,else false. */ +static +bool +innobase_col_check_fk( + const char* col_name, + const dict_table_t* table, + dict_s_col_list* s_cols) +{ + dict_s_col_list::const_iterator it; + + for (it = s_cols->begin(); it != s_cols->end(); ++it) { + for (ulint j = it->num_base; j--; ) { + if (!strcmp(col_name, dict_table_get_col_name( + table, it->base_col[j]->ind))) { + return(true); + } + } + } + + return(false); +} + +/** Check whether the foreign key constraint is on base of any stored columns. +@param[in] foreign Foriegn key constraing information +@param[in] table table to which the foreign key objects +to be added +@param[in] s_cols list of stored column information in the table. +@return true if yes, otherwise false. */ +static +bool +innobase_check_fk_stored( + const dict_foreign_t* foreign, + const dict_table_t* table, + dict_s_col_list* s_cols) +{ + ulint type = foreign->type; + + type &= ~(DICT_FOREIGN_ON_DELETE_NO_ACTION + | DICT_FOREIGN_ON_UPDATE_NO_ACTION); + + if (type == 0 || s_cols == NULL) { + return(false); + } + + for (ulint i = 0; i < foreign->n_fields; i++) { + if (innobase_col_check_fk( + foreign->foreign_col_names[i], table, s_cols)) { + return(true); + } + } + + return(false); +} + +/** Create InnoDB foreign key structure from MySQL alter_info +@param[in] ha_alter_info alter table info +@param[in] table_share TABLE_SHARE +@param[in] table table object +@param[in] col_names column names, or NULL to use +table->col_names +@param[in] drop_index indexes to be dropped +@param[in] n_drop_index size of drop_index +@param[out] add_fk foreign constraint added +@param[out] n_add_fk number of foreign constraints +added +@param[in] trx user transaction +@param[in] s_cols list of stored column information +@retval true if successful +@retval false on error (will call my_error()) */ +static MY_ATTRIBUTE((nonnull(1,2,3,7,8), warn_unused_result)) +bool +innobase_get_foreign_key_info( + Alter_inplace_info* + ha_alter_info, + const TABLE_SHARE* + table_share, + dict_table_t* table, + const char** col_names, + dict_index_t** drop_index, + ulint n_drop_index, + dict_foreign_t**add_fk, + ulint* n_add_fk, + const trx_t* trx, + dict_s_col_list*s_cols) +{ + dict_table_t* referenced_table = NULL; + char* referenced_table_name = NULL; + ulint num_fk = 0; + Alter_info* alter_info = ha_alter_info->alter_info; + const CHARSET_INFO* cs = thd_charset(trx->mysql_thd); + + DBUG_ENTER("innobase_get_foreign_key_info"); + + *n_add_fk = 0; + + for (Key& key : alter_info->key_list) { + if (key.type != Key::FOREIGN_KEY) { + continue; + } + + const char* column_names[MAX_NUM_FK_COLUMNS]; + dict_index_t* index = NULL; + const char* referenced_column_names[MAX_NUM_FK_COLUMNS]; + dict_index_t* referenced_index = NULL; + ulint num_col = 0; + ulint referenced_num_col = 0; + bool correct_option; + + Foreign_key* fk_key = static_cast<Foreign_key*>(&key); + + if (fk_key->columns.elements > 0) { + ulint i = 0; + + /* Get all the foreign key column info for the + current table */ + for (const Key_part_spec& column : fk_key->columns) { + column_names[i] = column.field_name.str; + ut_ad(i < MAX_NUM_FK_COLUMNS); + i++; + } + + index = innobase_find_fk_index( + table, col_names, + span<dict_index_t*>(drop_index, n_drop_index), + column_names, i); + + /* MySQL would add a index in the creation + list if no such index for foreign table, + so we have to use DBUG_EXECUTE_IF to simulate + the scenario */ + DBUG_EXECUTE_IF("innodb_test_no_foreign_idx", + index = NULL;); + + /* Check whether there exist such + index in the the index create clause */ + if (!index && !innobase_find_equiv_index( + column_names, static_cast<uint>(i), + ha_alter_info->key_info_buffer, + span<uint>(ha_alter_info->index_add_buffer, + ha_alter_info->index_add_count))) { + my_error( + ER_FK_NO_INDEX_CHILD, + MYF(0), + fk_key->name.str + ? fk_key->name.str : "", + table_share->table_name.str); + goto err_exit; + } + + num_col = i; + } + + add_fk[num_fk] = dict_mem_foreign_create(); + + mutex_enter(&dict_sys.mutex); + + referenced_table_name = dict_get_referenced_table( + table->name.m_name, + LEX_STRING_WITH_LEN(fk_key->ref_db), + LEX_STRING_WITH_LEN(fk_key->ref_table), + &referenced_table, + add_fk[num_fk]->heap, cs); + + /* Test the case when referenced_table failed to + open, if trx->check_foreigns is not set, we should + still be able to add the foreign key */ + DBUG_EXECUTE_IF("innodb_test_open_ref_fail", + referenced_table = NULL;); + + if (!referenced_table && trx->check_foreigns) { + mutex_exit(&dict_sys.mutex); + my_error(ER_FK_CANNOT_OPEN_PARENT, + MYF(0), fk_key->ref_table.str); + + goto err_exit; + } + + if (fk_key->ref_columns.elements > 0) { + ulint i = 0; + + for (Key_part_spec &column : fk_key->ref_columns) { + referenced_column_names[i] = + column.field_name.str; + ut_ad(i < MAX_NUM_FK_COLUMNS); + i++; + } + + if (referenced_table) { + referenced_index = + dict_foreign_find_index( + referenced_table, 0, + referenced_column_names, + i, index, + TRUE, FALSE, + NULL, NULL, NULL); + + DBUG_EXECUTE_IF( + "innodb_test_no_reference_idx", + referenced_index = NULL;); + + /* Check whether there exist such + index in the the index create clause */ + if (!referenced_index) { + mutex_exit(&dict_sys.mutex); + my_error(ER_FK_NO_INDEX_PARENT, MYF(0), + fk_key->name.str + ? fk_key->name.str : "", + fk_key->ref_table.str); + goto err_exit; + } + } else { + ut_a(!trx->check_foreigns); + } + + referenced_num_col = i; + } else { + /* Not possible to add a foreign key without a + referenced column */ + mutex_exit(&dict_sys.mutex); + my_error(ER_CANNOT_ADD_FOREIGN, MYF(0), + fk_key->ref_table.str); + goto err_exit; + } + + if (!innobase_init_foreign( + add_fk[num_fk], fk_key->name.str, + table, index, column_names, + num_col, referenced_table_name, + referenced_table, referenced_index, + referenced_column_names, referenced_num_col)) { + mutex_exit(&dict_sys.mutex); + my_error( + ER_DUP_CONSTRAINT_NAME, + MYF(0), + "FOREIGN KEY", add_fk[num_fk]->id); + goto err_exit; + } + + mutex_exit(&dict_sys.mutex); + + correct_option = innobase_set_foreign_key_option( + add_fk[num_fk], fk_key); + + DBUG_EXECUTE_IF("innodb_test_wrong_fk_option", + correct_option = false;); + + if (!correct_option) { + my_error(ER_FK_INCORRECT_OPTION, + MYF(0), + table_share->table_name.str, + add_fk[num_fk]->id); + goto err_exit; + } + + if (innobase_check_fk_stored( + add_fk[num_fk], table, s_cols)) { + my_printf_error( + HA_ERR_UNSUPPORTED, + "Cannot add foreign key on the base column " + "of stored column", MYF(0)); + goto err_exit; + } + + num_fk++; + } + + *n_add_fk = num_fk; + + DBUG_RETURN(true); +err_exit: + for (ulint i = 0; i <= num_fk; i++) { + if (add_fk[i]) { + dict_foreign_free(add_fk[i]); + } + } + + DBUG_RETURN(false); +} + +/*************************************************************//** +Copies an InnoDB column to a MySQL field. This function is +adapted from row_sel_field_store_in_mysql_format(). */ +static +void +innobase_col_to_mysql( +/*==================*/ + const dict_col_t* col, /*!< in: InnoDB column */ + const uchar* data, /*!< in: InnoDB column data */ + ulint len, /*!< in: length of data, in bytes */ + Field* field) /*!< in/out: MySQL field */ +{ + uchar* ptr; + uchar* dest = field->ptr; + ulint flen = field->pack_length(); + + switch (col->mtype) { + case DATA_INT: + ut_ad(len == flen); + + /* Convert integer data from Innobase to little-endian + format, sign bit restored to normal */ + + for (ptr = dest + len; ptr != dest; ) { + *--ptr = *data++; + } + + if (!(col->prtype & DATA_UNSIGNED)) { + ((byte*) dest)[len - 1] ^= 0x80; + } + + break; + + case DATA_VARCHAR: + case DATA_VARMYSQL: + case DATA_BINARY: + field->reset(); + + if (field->type() == MYSQL_TYPE_VARCHAR) { + /* This is a >= 5.0.3 type true VARCHAR. Store the + length of the data to the first byte or the first + two bytes of dest. */ + + dest = row_mysql_store_true_var_len( + dest, len, flen - field->key_length()); + } + + /* Copy the actual data */ + memcpy(dest, data, len); + break; + + case DATA_GEOMETRY: + case DATA_BLOB: + /* Skip MySQL BLOBs when reporting an erroneous row + during index creation or table rebuild. */ + field->set_null(); + break; + +#ifdef UNIV_DEBUG + case DATA_MYSQL: + ut_ad(flen >= len); + ut_ad(col->mbmaxlen >= col->mbminlen); + memcpy(dest, data, len); + break; + + default: + case DATA_SYS_CHILD: + case DATA_SYS: + /* These column types should never be shipped to MySQL. */ + ut_ad(0); + /* fall through */ + case DATA_FLOAT: + case DATA_DOUBLE: + case DATA_DECIMAL: + /* Above are the valid column types for MySQL data. */ + ut_ad(flen == len); + /* fall through */ + case DATA_FIXBINARY: + case DATA_CHAR: + /* We may have flen > len when there is a shorter + prefix on the CHAR and BINARY column. */ + ut_ad(flen >= len); +#else /* UNIV_DEBUG */ + default: +#endif /* UNIV_DEBUG */ + memcpy(dest, data, len); + } +} + +/*************************************************************//** +Copies an InnoDB record to table->record[0]. */ +void +innobase_rec_to_mysql( +/*==================*/ + struct TABLE* table, /*!< in/out: MySQL table */ + const rec_t* rec, /*!< in: record */ + const dict_index_t* index, /*!< in: index */ + const rec_offs* offsets)/*!< in: rec_get_offsets( + rec, index, ...) */ +{ + uint n_fields = table->s->fields; + + ut_ad(n_fields == dict_table_get_n_user_cols(index->table) + - !!(DICT_TF2_FLAG_IS_SET(index->table, + DICT_TF2_FTS_HAS_DOC_ID))); + + for (uint i = 0; i < n_fields; i++) { + Field* field = table->field[i]; + ulint ipos; + ulint ilen; + const uchar* ifield; + ulint prefix_col; + + field->reset(); + + ipos = dict_index_get_nth_col_or_prefix_pos( + index, i, true, false, &prefix_col); + + if (ipos == ULINT_UNDEFINED + || rec_offs_nth_extern(offsets, ipos)) { +null_field: + field->set_null(); + continue; + } + + ifield = rec_get_nth_cfield(rec, index, offsets, ipos, &ilen); + + /* Assign the NULL flag */ + if (ilen == UNIV_SQL_NULL) { + ut_ad(field->real_maybe_null()); + goto null_field; + } + + field->set_notnull(); + + innobase_col_to_mysql( + dict_field_get_col( + dict_index_get_nth_field(index, ipos)), + ifield, ilen, field); + } +} + +/*************************************************************//** +Copies an InnoDB index entry to table->record[0]. +This is used in preparation for print_keydup_error() from +inline add index */ +void +innobase_fields_to_mysql( +/*=====================*/ + struct TABLE* table, /*!< in/out: MySQL table */ + const dict_index_t* index, /*!< in: InnoDB index */ + const dfield_t* fields) /*!< in: InnoDB index fields */ +{ + uint n_fields = table->s->fields; + ulint num_v = 0; + + ut_ad(n_fields == dict_table_get_n_user_cols(index->table) + + dict_table_get_n_v_cols(index->table) + - !!(DICT_TF2_FLAG_IS_SET(index->table, + DICT_TF2_FTS_HAS_DOC_ID))); + + for (uint i = 0; i < n_fields; i++) { + Field* field = table->field[i]; + ulint ipos; + ulint prefix_col; + + field->reset(); + + const bool is_v = !field->stored_in_db(); + const ulint col_n = is_v ? num_v++ : i - num_v; + + ipos = dict_index_get_nth_col_or_prefix_pos( + index, col_n, true, is_v, &prefix_col); + + if (ipos == ULINT_UNDEFINED + || dfield_is_ext(&fields[ipos]) + || dfield_is_null(&fields[ipos])) { + + field->set_null(); + } else { + field->set_notnull(); + + const dfield_t* df = &fields[ipos]; + + innobase_col_to_mysql( + dict_field_get_col( + dict_index_get_nth_field(index, ipos)), + static_cast<const uchar*>(dfield_get_data(df)), + dfield_get_len(df), field); + } + } +} + +/*************************************************************//** +Copies an InnoDB row to table->record[0]. +This is used in preparation for print_keydup_error() from +row_log_table_apply() */ +void +innobase_row_to_mysql( +/*==================*/ + struct TABLE* table, /*!< in/out: MySQL table */ + const dict_table_t* itab, /*!< in: InnoDB table */ + const dtuple_t* row) /*!< in: InnoDB row */ +{ + uint n_fields = table->s->fields; + ulint num_v = 0; + + /* The InnoDB row may contain an extra FTS_DOC_ID column at the end. */ + ut_ad(row->n_fields == dict_table_get_n_cols(itab)); + ut_ad(n_fields == row->n_fields - DATA_N_SYS_COLS + + dict_table_get_n_v_cols(itab) + - !!(DICT_TF2_FLAG_IS_SET(itab, DICT_TF2_FTS_HAS_DOC_ID))); + + for (uint i = 0; i < n_fields; i++) { + Field* field = table->field[i]; + + field->reset(); + + if (!field->stored_in_db()) { + /* Virtual column are not stored in InnoDB table, so + skip it */ + num_v++; + continue; + } + + const dfield_t* df = dtuple_get_nth_field(row, i - num_v); + + if (dfield_is_ext(df) || dfield_is_null(df)) { + field->set_null(); + } else { + field->set_notnull(); + + innobase_col_to_mysql( + dict_table_get_nth_col(itab, i - num_v), + static_cast<const uchar*>(dfield_get_data(df)), + dfield_get_len(df), field); + } + } + if (table->vfield) { + MY_BITMAP* old_read_set = tmp_use_all_columns(table, &table->read_set); + table->update_virtual_fields(table->file, VCOL_UPDATE_FOR_READ); + tmp_restore_column_map(&table->read_set, old_read_set); + } +} + +/*******************************************************************//** +This function checks that index keys are sensible. +@return 0 or error number */ +static MY_ATTRIBUTE((nonnull, warn_unused_result)) +int +innobase_check_index_keys( +/*======================*/ + const Alter_inplace_info* info, + /*!< in: indexes to be created or dropped */ + const dict_table_t* innodb_table) + /*!< in: Existing indexes */ +{ + for (uint key_num = 0; key_num < info->index_add_count; + key_num++) { + const KEY& key = info->key_info_buffer[ + info->index_add_buffer[key_num]]; + + /* Check that the same index name does not appear + twice in indexes to be created. */ + + for (ulint i = 0; i < key_num; i++) { + const KEY& key2 = info->key_info_buffer[ + info->index_add_buffer[i]]; + + if (0 == strcmp(key.name.str, key2.name.str)) { + my_error(ER_WRONG_NAME_FOR_INDEX, MYF(0), + key.name.str); + + return(ER_WRONG_NAME_FOR_INDEX); + } + } + + /* Check that the same index name does not already exist. */ + + const dict_index_t* index; + + for (index = dict_table_get_first_index(innodb_table); + index; index = dict_table_get_next_index(index)) { + + if (index->is_committed() + && !strcmp(key.name.str, index->name)) { + break; + } + } + + /* Now we are in a situation where we have "ADD INDEX x" + and an index by the same name already exists. We have 4 + possible cases: + 1. No further clauses for an index x are given. Should reject + the operation. + 2. "DROP INDEX x" is given. Should allow the operation. + 3. "RENAME INDEX x TO y" is given. Should allow the operation. + 4. "DROP INDEX x, RENAME INDEX x TO y" is given. Should allow + the operation, since no name clash occurs. In this particular + case MySQL cancels the operation without calling InnoDB + methods. */ + + if (index) { + /* If a key by the same name is being created and + dropped, the name clash is OK. */ + for (uint i = 0; i < info->index_drop_count; + i++) { + const KEY* drop_key + = info->index_drop_buffer[i]; + + if (0 == strcmp(key.name.str, + drop_key->name.str)) { + goto name_ok; + } + } + + for (const Alter_inplace_info::Rename_key_pair& pair : + info->rename_keys) { + if (0 == strcmp(key.name.str, + pair.old_key->name.str)) { + goto name_ok; + } + } + + my_error(ER_WRONG_NAME_FOR_INDEX, MYF(0), + key.name.str); + return(ER_WRONG_NAME_FOR_INDEX); + } + +name_ok: + for (ulint i = 0; i < key.user_defined_key_parts; i++) { + const KEY_PART_INFO& key_part1 + = key.key_part[i]; + const Field* field + = key_part1.field; + unsigned is_unsigned; + + switch (get_innobase_type_from_mysql_type( + &is_unsigned, field)) { + default: + break; + case DATA_INT: + case DATA_FLOAT: + case DATA_DOUBLE: + case DATA_DECIMAL: + /* Check that MySQL does not try to + create a column prefix index field on + an inappropriate data type. */ + + if (field->type() == MYSQL_TYPE_VARCHAR) { + if (key_part1.length + >= field->pack_length() + - ((Field_varstring*) field) + ->length_bytes) { + break; + } + } else { + if (key_part1.length + >= field->pack_length()) { + break; + } + } + + my_error(ER_WRONG_KEY_COLUMN, MYF(0), "InnoDB", + field->field_name.str); + return(ER_WRONG_KEY_COLUMN); + } + + /* Check that the same column does not appear + twice in the index. */ + + for (ulint j = 0; j < i; j++) { + const KEY_PART_INFO& key_part2 + = key.key_part[j]; + + if (key_part1.fieldnr != key_part2.fieldnr) { + continue; + } + + my_error(ER_WRONG_KEY_COLUMN, MYF(0), "InnoDB", + field->field_name.str); + return(ER_WRONG_KEY_COLUMN); + } + } + } + + return(0); +} + +/** Create index field definition for key part +@param[in] new_clustered true if alter is generating a new clustered +index +@param[in] altered_table MySQL table that is being altered +@param[in] key_part MySQL key definition +@param[out] index_field index field definition for key_part */ +static MY_ATTRIBUTE((nonnull)) +void +innobase_create_index_field_def( + bool new_clustered, + const TABLE* altered_table, + const KEY_PART_INFO* key_part, + index_field_t* index_field) +{ + const Field* field; + unsigned is_unsigned; + unsigned num_v = 0; + + DBUG_ENTER("innobase_create_index_field_def"); + + field = new_clustered + ? altered_table->field[key_part->fieldnr] + : key_part->field; + + for (ulint i = 0; i < key_part->fieldnr; i++) { + if (!altered_table->field[i]->stored_in_db()) { + num_v++; + } + } + + auto col_type = get_innobase_type_from_mysql_type( + &is_unsigned, field); + + if ((index_field->is_v_col = !field->stored_in_db())) { + index_field->col_no = num_v; + } else { + index_field->col_no = key_part->fieldnr - num_v; + } + + if (DATA_LARGE_MTYPE(col_type) + || (key_part->length < field->pack_length() + && field->type() != MYSQL_TYPE_VARCHAR) + || (field->type() == MYSQL_TYPE_VARCHAR + && key_part->length < field->pack_length() + - ((Field_varstring*) field)->length_bytes)) { + + index_field->prefix_len = key_part->length; + } else { + index_field->prefix_len = 0; + } + + DBUG_VOID_RETURN; +} + +/** Create index definition for key +@param[in] altered_table MySQL table that is being altered +@param[in] keys key definitions +@param[in] key_number MySQL key number +@param[in] new_clustered true if generating a new clustered +index on the table +@param[in] key_clustered true if this is the new clustered index +@param[out] index index definition +@param[in] heap heap where memory is allocated */ +static MY_ATTRIBUTE((nonnull)) +void +innobase_create_index_def( + const TABLE* altered_table, + const KEY* keys, + ulint key_number, + bool new_clustered, + bool key_clustered, + index_def_t* index, + mem_heap_t* heap) +{ + const KEY* key = &keys[key_number]; + ulint i; + ulint n_fields = key->user_defined_key_parts; + + DBUG_ENTER("innobase_create_index_def"); + DBUG_ASSERT(!key_clustered || new_clustered); + + index->fields = static_cast<index_field_t*>( + mem_heap_alloc(heap, n_fields * sizeof *index->fields)); + + index->parser = NULL; + index->key_number = key_number; + index->n_fields = n_fields; + index->name = mem_heap_strdup(heap, key->name.str); + index->rebuild = new_clustered; + + if (key_clustered) { + DBUG_ASSERT(!(key->flags & (HA_FULLTEXT | HA_SPATIAL))); + DBUG_ASSERT(key->flags & HA_NOSAME); + index->ind_type = DICT_CLUSTERED | DICT_UNIQUE; + } else if (key->flags & HA_FULLTEXT) { + DBUG_ASSERT(!(key->flags & (HA_SPATIAL | HA_NOSAME))); + DBUG_ASSERT(!(key->flags & HA_KEYFLAG_MASK + & ~(HA_FULLTEXT + | HA_PACK_KEY + | HA_BINARY_PACK_KEY))); + index->ind_type = DICT_FTS; + + /* Note: key->parser is only parser name, + we need to get parser from altered_table instead */ + + if (key->flags & HA_USES_PARSER) { + for (ulint j = 0; j < altered_table->s->keys; j++) { + if (!strcmp(altered_table->key_info[j].name.str, + key->name.str)) { + ut_ad(altered_table->key_info[j].flags + & HA_USES_PARSER); + + plugin_ref parser = + altered_table->key_info[j].parser; + index->parser = + static_cast<st_mysql_ftparser*>( + plugin_decl(parser)->info); + + break; + } + } + + DBUG_EXECUTE_IF("fts_instrument_use_default_parser", + index->parser = &fts_default_parser;); + ut_ad(index->parser); + } + } else if (key->flags & HA_SPATIAL) { + DBUG_ASSERT(!(key->flags & HA_NOSAME)); + index->ind_type = DICT_SPATIAL; + ut_ad(n_fields == 1); + ulint num_v = 0; + + /* Need to count the virtual fields before this spatial + indexed field */ + for (ulint i = 0; i < key->key_part->fieldnr; i++) { + num_v += !altered_table->field[i]->stored_in_db(); + } + index->fields[0].col_no = key->key_part[0].fieldnr - num_v; + index->fields[0].prefix_len = 0; + index->fields[0].is_v_col = false; + + /* Currently, the spatial index cannot be created + on virtual columns. It is blocked in the SQL layer. */ + DBUG_ASSERT(key->key_part[0].field->stored_in_db()); + } else { + index->ind_type = (key->flags & HA_NOSAME) ? DICT_UNIQUE : 0; + } + + if (!(key->flags & HA_SPATIAL)) { + for (i = 0; i < n_fields; i++) { + innobase_create_index_field_def( + new_clustered, altered_table, + &key->key_part[i], &index->fields[i]); + + if (index->fields[i].is_v_col) { + index->ind_type |= DICT_VIRTUAL; + } + } + } + + DBUG_VOID_RETURN; +} + +/*******************************************************************//** +Check whether the table has a unique index with FTS_DOC_ID_INDEX_NAME +on the Doc ID column. +@return the status of the FTS_DOC_ID index */ +enum fts_doc_id_index_enum +innobase_fts_check_doc_id_index( +/*============================*/ + const dict_table_t* table, /*!< in: table definition */ + const TABLE* altered_table, /*!< in: MySQL table + that is being altered */ + ulint* fts_doc_col_no) /*!< out: The column number for + Doc ID, or ULINT_UNDEFINED + if it is being created in + ha_alter_info */ +{ + const dict_index_t* index; + const dict_field_t* field; + + if (altered_table) { + /* Check if a unique index with the name of + FTS_DOC_ID_INDEX_NAME is being created. */ + + for (uint i = 0; i < altered_table->s->keys; i++) { + const KEY& key = altered_table->key_info[i]; + + if (innobase_strcasecmp( + key.name.str, FTS_DOC_ID_INDEX_NAME)) { + continue; + } + + if ((key.flags & HA_NOSAME) + && key.user_defined_key_parts == 1 + && !strcmp(key.name.str, FTS_DOC_ID_INDEX_NAME) + && !strcmp(key.key_part[0].field->field_name.str, + FTS_DOC_ID_COL_NAME)) { + if (fts_doc_col_no) { + *fts_doc_col_no = ULINT_UNDEFINED; + } + return(FTS_EXIST_DOC_ID_INDEX); + } else { + return(FTS_INCORRECT_DOC_ID_INDEX); + } + } + } + + if (!table) { + return(FTS_NOT_EXIST_DOC_ID_INDEX); + } + + for (index = dict_table_get_first_index(table); + index; index = dict_table_get_next_index(index)) { + + + /* Check if there exists a unique index with the name of + FTS_DOC_ID_INDEX_NAME and ignore the corrupted index */ + if (index->type & DICT_CORRUPT + || innobase_strcasecmp(index->name, FTS_DOC_ID_INDEX_NAME)) { + continue; + } + + if (!dict_index_is_unique(index) + || dict_index_get_n_unique(index) > 1 + || strcmp(index->name, FTS_DOC_ID_INDEX_NAME)) { + return(FTS_INCORRECT_DOC_ID_INDEX); + } + + /* Check whether the index has FTS_DOC_ID as its + first column */ + field = dict_index_get_nth_field(index, 0); + + /* The column would be of a BIGINT data type */ + if (strcmp(field->name, FTS_DOC_ID_COL_NAME) == 0 + && field->col->mtype == DATA_INT + && field->col->len == 8 + && field->col->prtype & DATA_NOT_NULL + && !field->col->is_virtual()) { + if (fts_doc_col_no) { + *fts_doc_col_no = dict_col_get_no(field->col); + } + return(FTS_EXIST_DOC_ID_INDEX); + } else { + return(FTS_INCORRECT_DOC_ID_INDEX); + } + } + + + /* Not found */ + return(FTS_NOT_EXIST_DOC_ID_INDEX); +} +/*******************************************************************//** +Check whether the table has a unique index with FTS_DOC_ID_INDEX_NAME +on the Doc ID column in MySQL create index definition. +@return FTS_EXIST_DOC_ID_INDEX if there exists the FTS_DOC_ID index, +FTS_INCORRECT_DOC_ID_INDEX if the FTS_DOC_ID index is of wrong format */ +enum fts_doc_id_index_enum +innobase_fts_check_doc_id_index_in_def( +/*===================================*/ + ulint n_key, /*!< in: Number of keys */ + const KEY* key_info) /*!< in: Key definition */ +{ + /* Check whether there is a "FTS_DOC_ID_INDEX" in the to be built index + list */ + for (ulint j = 0; j < n_key; j++) { + const KEY* key = &key_info[j]; + + if (innobase_strcasecmp(key->name.str, FTS_DOC_ID_INDEX_NAME)) { + continue; + } + + /* Do a check on FTS DOC ID_INDEX, it must be unique, + named as "FTS_DOC_ID_INDEX" and on column "FTS_DOC_ID" */ + if (!(key->flags & HA_NOSAME) + || key->user_defined_key_parts != 1 + || strcmp(key->name.str, FTS_DOC_ID_INDEX_NAME) + || strcmp(key->key_part[0].field->field_name.str, + FTS_DOC_ID_COL_NAME)) { + return(FTS_INCORRECT_DOC_ID_INDEX); + } + + return(FTS_EXIST_DOC_ID_INDEX); + } + + return(FTS_NOT_EXIST_DOC_ID_INDEX); +} + +/** Create an index table where indexes are ordered as follows: + +IF a new primary key is defined for the table THEN + + 1) New primary key + 2) The remaining keys in key_info + +ELSE + + 1) All new indexes in the order they arrive from MySQL + +ENDIF + +@return key definitions */ +MY_ATTRIBUTE((nonnull, warn_unused_result, malloc)) +inline index_def_t* +ha_innobase_inplace_ctx::create_key_defs( + const Alter_inplace_info* ha_alter_info, + /*!< in: alter operation */ + const TABLE* altered_table, + /*!< in: MySQL table that is being altered */ + ulint& n_fts_add, + /*!< out: number of FTS indexes to be created */ + ulint& fts_doc_id_col, + /*!< in: The column number for Doc ID */ + bool& add_fts_doc_id, + /*!< in: whether we need to add new DOC ID + column for FTS index */ + bool& add_fts_doc_idx, + /*!< in: whether we need to add new DOC ID + index for FTS index */ + const TABLE* table) + /*!< in: MySQL table that is being altered */ +{ + ulint& n_add = num_to_add_index; + const bool got_default_clust = new_table->indexes.start->is_gen_clust(); + + index_def_t* indexdef; + index_def_t* indexdefs; + bool new_primary; + const uint*const add + = ha_alter_info->index_add_buffer; + const KEY*const key_info + = ha_alter_info->key_info_buffer; + + DBUG_ENTER("ha_innobase_inplace_ctx::create_key_defs"); + DBUG_ASSERT(!add_fts_doc_id || add_fts_doc_idx); + DBUG_ASSERT(ha_alter_info->index_add_count == n_add); + + /* If there is a primary key, it is always the first index + defined for the innodb_table. */ + + new_primary = n_add > 0 + && !my_strcasecmp(system_charset_info, + key_info[*add].name.str, "PRIMARY"); + n_fts_add = 0; + + /* If there is a UNIQUE INDEX consisting entirely of NOT NULL + columns and if the index does not contain column prefix(es) + (only prefix/part of the column is indexed), MySQL will treat the + index as a PRIMARY KEY unless the table already has one. */ + + ut_ad(altered_table->s->primary_key == 0 + || altered_table->s->primary_key == MAX_KEY); + + if (got_default_clust && !new_primary) { + new_primary = (altered_table->s->primary_key != MAX_KEY); + } + + const bool rebuild = new_primary || add_fts_doc_id + || innobase_need_rebuild(ha_alter_info, table); + + /* Reserve one more space if new_primary is true, and we might + need to add the FTS_DOC_ID_INDEX */ + indexdef = indexdefs = static_cast<index_def_t*>( + mem_heap_alloc( + heap, sizeof *indexdef + * (ha_alter_info->key_count + + rebuild + + got_default_clust))); + + if (rebuild) { + ulint primary_key_number; + + if (new_primary) { + DBUG_ASSERT(n_add || got_default_clust); + DBUG_ASSERT(n_add || !altered_table->s->primary_key); + primary_key_number = altered_table->s->primary_key; + } else if (got_default_clust) { + /* Create the GEN_CLUST_INDEX */ + index_def_t* index = indexdef++; + + index->fields = NULL; + index->n_fields = 0; + index->ind_type = DICT_CLUSTERED; + index->name = innobase_index_reserve_name; + index->rebuild = true; + index->key_number = ~0U; + primary_key_number = ULINT_UNDEFINED; + goto created_clustered; + } else { + primary_key_number = 0; + } + + /* Create the PRIMARY key index definition */ + innobase_create_index_def( + altered_table, key_info, primary_key_number, + true, true, indexdef++, heap); + +created_clustered: + n_add = 1; + + for (ulint i = 0; i < ha_alter_info->key_count; i++) { + if (i == primary_key_number) { + continue; + } + /* Copy the index definitions. */ + innobase_create_index_def( + altered_table, key_info, i, true, + false, indexdef, heap); + + if (indexdef->ind_type & DICT_FTS) { + n_fts_add++; + } + + indexdef++; + n_add++; + } + + if (n_fts_add > 0) { + ulint num_v = 0; + + if (!add_fts_doc_id + && !innobase_fts_check_doc_id_col( + NULL, altered_table, + &fts_doc_id_col, &num_v)) { + fts_doc_id_col = altered_table->s->fields - num_v; + add_fts_doc_id = true; + } + + if (!add_fts_doc_idx) { + fts_doc_id_index_enum ret; + ulint doc_col_no; + + ret = innobase_fts_check_doc_id_index( + NULL, altered_table, &doc_col_no); + + /* This should have been checked before */ + ut_ad(ret != FTS_INCORRECT_DOC_ID_INDEX); + + if (ret == FTS_NOT_EXIST_DOC_ID_INDEX) { + add_fts_doc_idx = true; + } else { + ut_ad(ret == FTS_EXIST_DOC_ID_INDEX); + ut_ad(doc_col_no == ULINT_UNDEFINED + || doc_col_no == fts_doc_id_col); + } + } + } + } else { + /* Create definitions for added secondary indexes. */ + + for (ulint i = 0; i < n_add; i++) { + innobase_create_index_def( + altered_table, key_info, add[i], + false, false, indexdef, heap); + + if (indexdef->ind_type & DICT_FTS) { + n_fts_add++; + } + + indexdef++; + } + } + + DBUG_ASSERT(indexdefs + n_add == indexdef); + + if (add_fts_doc_idx) { + index_def_t* index = indexdef++; + + index->fields = static_cast<index_field_t*>( + mem_heap_alloc(heap, sizeof *index->fields)); + index->n_fields = 1; + index->fields->col_no = fts_doc_id_col; + index->fields->prefix_len = 0; + index->fields->is_v_col = false; + index->ind_type = DICT_UNIQUE; + ut_ad(!rebuild + || !add_fts_doc_id + || fts_doc_id_col <= altered_table->s->fields); + + index->name = FTS_DOC_ID_INDEX_NAME; + index->rebuild = rebuild; + + /* TODO: assign a real MySQL key number for this */ + index->key_number = ULINT_UNDEFINED; + n_add++; + } + + DBUG_ASSERT(indexdef > indexdefs); + DBUG_ASSERT((ulint) (indexdef - indexdefs) + <= ha_alter_info->key_count + + add_fts_doc_idx + got_default_clust); + DBUG_ASSERT(ha_alter_info->index_add_count <= n_add); + DBUG_RETURN(indexdefs); +} + +MY_ATTRIBUTE((warn_unused_result)) +bool too_big_key_part_length(size_t max_field_len, const KEY& key) +{ + for (ulint i = 0; i < key.user_defined_key_parts; i++) { + if (key.key_part[i].length > max_field_len) { + return true; + } + } + return false; +} + +/********************************************************************//** +Drop any indexes that we were not able to free previously due to +open table handles. */ +static +void +online_retry_drop_indexes_low( +/*==========================*/ + dict_table_t* table, /*!< in/out: table */ + trx_t* trx) /*!< in/out: transaction */ +{ + ut_ad(mutex_own(&dict_sys.mutex)); + ut_ad(trx->dict_operation_lock_mode == RW_X_LATCH); + ut_ad(trx_get_dict_operation(trx) == TRX_DICT_OP_INDEX); + + /* We can have table->n_ref_count > 1, because other threads + may have prebuilt->table pointing to the table. However, these + other threads should be between statements, waiting for the + next statement to execute, or for a meta-data lock. */ + ut_ad(table->get_ref_count() >= 1); + + if (table->drop_aborted) { + row_merge_drop_indexes(trx, table, true); + } +} + +/********************************************************************//** +Drop any indexes that we were not able to free previously due to +open table handles. */ +static MY_ATTRIBUTE((nonnull)) +void +online_retry_drop_indexes( +/*======================*/ + dict_table_t* table, /*!< in/out: table */ + THD* user_thd) /*!< in/out: MySQL connection */ +{ + if (table->drop_aborted) { + trx_t* trx = innobase_trx_allocate(user_thd); + + trx_start_for_ddl(trx, TRX_DICT_OP_INDEX); + + row_mysql_lock_data_dictionary(trx); + online_retry_drop_indexes_low(table, trx); + trx_commit_for_mysql(trx); + row_mysql_unlock_data_dictionary(trx); + trx->free(); + } + + ut_d(mutex_enter(&dict_sys.mutex)); + ut_d(dict_table_check_for_dup_indexes(table, CHECK_ALL_COMPLETE)); + ut_d(mutex_exit(&dict_sys.mutex)); + ut_ad(!table->drop_aborted); +} + +/********************************************************************//** +Commit a dictionary transaction and drop any indexes that we were not +able to free previously due to open table handles. */ +static MY_ATTRIBUTE((nonnull)) +void +online_retry_drop_indexes_with_trx( +/*===============================*/ + dict_table_t* table, /*!< in/out: table */ + trx_t* trx) /*!< in/out: transaction */ +{ + ut_ad(trx_state_eq(trx, TRX_STATE_NOT_STARTED)); + + ut_ad(trx->dict_operation_lock_mode == RW_X_LATCH); + + /* Now that the dictionary is being locked, check if we can + drop any incompletely created indexes that may have been left + behind in rollback_inplace_alter_table() earlier. */ + if (table->drop_aborted) { + + trx->table_id = 0; + + trx_start_for_ddl(trx, TRX_DICT_OP_INDEX); + + online_retry_drop_indexes_low(table, trx); + trx_commit_for_mysql(trx); + } +} + +/** Determines if InnoDB is dropping a foreign key constraint. +@param foreign the constraint +@param drop_fk constraints being dropped +@param n_drop_fk number of constraints that are being dropped +@return whether the constraint is being dropped */ +MY_ATTRIBUTE((pure, nonnull(1), warn_unused_result)) +inline +bool +innobase_dropping_foreign( + const dict_foreign_t* foreign, + dict_foreign_t** drop_fk, + ulint n_drop_fk) +{ + while (n_drop_fk--) { + if (*drop_fk++ == foreign) { + return(true); + } + } + + return(false); +} + +/** Determines if an InnoDB FOREIGN KEY constraint depends on a +column that is being dropped or modified to NOT NULL. +@param user_table InnoDB table as it is before the ALTER operation +@param col_name Name of the column being altered +@param drop_fk constraints being dropped +@param n_drop_fk number of constraints that are being dropped +@param drop true=drop column, false=set NOT NULL +@retval true Not allowed (will call my_error()) +@retval false Allowed +*/ +MY_ATTRIBUTE((pure, nonnull(1,4), warn_unused_result)) +static +bool +innobase_check_foreigns_low( + const dict_table_t* user_table, + dict_foreign_t** drop_fk, + ulint n_drop_fk, + const char* col_name, + bool drop) +{ + dict_foreign_t* foreign; + ut_ad(mutex_own(&dict_sys.mutex)); + + /* Check if any FOREIGN KEY constraints are defined on this + column. */ + + for (dict_foreign_set::const_iterator it = user_table->foreign_set.begin(); + it != user_table->foreign_set.end(); + ++it) { + + foreign = *it; + + if (!drop && !(foreign->type + & (DICT_FOREIGN_ON_DELETE_SET_NULL + | DICT_FOREIGN_ON_UPDATE_SET_NULL))) { + continue; + } + + if (innobase_dropping_foreign(foreign, drop_fk, n_drop_fk)) { + continue; + } + + for (unsigned f = 0; f < foreign->n_fields; f++) { + if (!strcmp(foreign->foreign_col_names[f], + col_name)) { + my_error(drop + ? ER_FK_COLUMN_CANNOT_DROP + : ER_FK_COLUMN_NOT_NULL, MYF(0), + col_name, foreign->id); + return(true); + } + } + } + + if (!drop) { + /* SET NULL clauses on foreign key constraints of + child tables affect the child tables, not the parent table. + The column can be NOT NULL in the parent table. */ + return(false); + } + + /* Check if any FOREIGN KEY constraints in other tables are + referring to the column that is being dropped. */ + for (dict_foreign_set::const_iterator it + = user_table->referenced_set.begin(); + it != user_table->referenced_set.end(); + ++it) { + + foreign = *it; + + if (innobase_dropping_foreign(foreign, drop_fk, n_drop_fk)) { + continue; + } + + for (unsigned f = 0; f < foreign->n_fields; f++) { + char display_name[FN_REFLEN]; + + if (strcmp(foreign->referenced_col_names[f], + col_name)) { + continue; + } + + char* buf_end = innobase_convert_name( + display_name, (sizeof display_name) - 1, + foreign->foreign_table_name, + strlen(foreign->foreign_table_name), + NULL); + *buf_end = '\0'; + my_error(ER_FK_COLUMN_CANNOT_DROP_CHILD, + MYF(0), col_name, foreign->id, + display_name); + + return(true); + } + } + + return(false); +} + +/** Determines if an InnoDB FOREIGN KEY constraint depends on a +column that is being dropped or modified to NOT NULL. +@param ha_alter_info Data used during in-place alter +@param altered_table MySQL table that is being altered +@param old_table MySQL table as it is before the ALTER operation +@param user_table InnoDB table as it is before the ALTER operation +@param drop_fk constraints being dropped +@param n_drop_fk number of constraints that are being dropped +@retval true Not allowed (will call my_error()) +@retval false Allowed +*/ +MY_ATTRIBUTE((pure, nonnull(1,2,3), warn_unused_result)) +static +bool +innobase_check_foreigns( + Alter_inplace_info* ha_alter_info, + const TABLE* old_table, + const dict_table_t* user_table, + dict_foreign_t** drop_fk, + ulint n_drop_fk) +{ + for (Field** fp = old_table->field; *fp; fp++) { + ut_ad(!(*fp)->real_maybe_null() + == !!((*fp)->flags & NOT_NULL_FLAG)); + + auto end = ha_alter_info->alter_info->create_list.end(); + auto it = std::find_if( + ha_alter_info->alter_info->create_list.begin(), end, + [fp](const Create_field& field) { + return field.field == *fp; + }); + + if (it == end || (it->flags & NOT_NULL_FLAG)) { + if (innobase_check_foreigns_low( + user_table, drop_fk, n_drop_fk, + (*fp)->field_name.str, it == end)) { + return(true); + } + } + } + + return(false); +} + +/** Convert a default value for ADD COLUMN. +@param[in,out] heap Memory heap where allocated +@param[out] dfield InnoDB data field to copy to +@param[in] field MySQL value for the column +@param[in] old_field Old column if altering; NULL for ADD COLUMN +@param[in] comp nonzero if in compact format. */ +static void innobase_build_col_map_add( + mem_heap_t* heap, + dfield_t* dfield, + const Field* field, + const Field* old_field, + ulint comp) +{ + if (old_field && old_field->real_maybe_null() + && field->real_maybe_null()) { + return; + } + + if (field->is_real_null()) { + dfield_set_null(dfield); + return; + } + + const Field& from = old_field ? *old_field : *field; + ulint size = from.pack_length(); + + byte* buf = static_cast<byte*>(mem_heap_alloc(heap, size)); + + row_mysql_store_col_in_innobase_format( + dfield, buf, true, from.ptr, size, comp); +} + +/** Construct the translation table for reordering, dropping or +adding columns. + +@param ha_alter_info Data used during in-place alter +@param altered_table MySQL table that is being altered +@param table MySQL table as it is before the ALTER operation +@param new_table InnoDB table corresponding to MySQL altered_table +@param old_table InnoDB table corresponding to MYSQL table +@param defaults Default values for ADD COLUMN, or NULL if no ADD COLUMN +@param heap Memory heap where allocated +@return array of integers, mapping column numbers in the table +to column numbers in altered_table */ +static MY_ATTRIBUTE((nonnull(1,2,3,4,5,7), warn_unused_result)) +const ulint* +innobase_build_col_map( +/*===================*/ + Alter_inplace_info* ha_alter_info, + const TABLE* altered_table, + const TABLE* table, + dict_table_t* new_table, + const dict_table_t* old_table, + dtuple_t* defaults, + mem_heap_t* heap) +{ + DBUG_ENTER("innobase_build_col_map"); + DBUG_ASSERT(altered_table != table); + DBUG_ASSERT(new_table != old_table); + DBUG_ASSERT(dict_table_get_n_cols(new_table) + + dict_table_get_n_v_cols(new_table) + >= altered_table->s->fields + DATA_N_SYS_COLS); + DBUG_ASSERT(dict_table_get_n_cols(old_table) + + dict_table_get_n_v_cols(old_table) + >= table->s->fields + DATA_N_SYS_COLS + || ha_innobase::omits_virtual_cols(*table->s)); + DBUG_ASSERT(!!defaults == !!(ha_alter_info->handler_flags + & INNOBASE_DEFAULTS)); + DBUG_ASSERT(!defaults || dtuple_get_n_fields(defaults) + == dict_table_get_n_cols(new_table)); + + const uint old_n_v_cols = uint(table->s->fields + - table->s->stored_fields); + DBUG_ASSERT(old_n_v_cols == old_table->n_v_cols + || table->s->frm_version < FRM_VER_EXPRESSSIONS); + DBUG_ASSERT(!old_n_v_cols || table->s->virtual_fields); + + ulint* col_map = static_cast<ulint*>( + mem_heap_alloc( + heap, (size_t(old_table->n_cols) + old_n_v_cols) + * sizeof *col_map)); + + uint i = 0; + uint num_v = 0; + + /* Any dropped columns will map to ULINT_UNDEFINED. */ + for (uint old_i = 0; old_i + DATA_N_SYS_COLS < old_table->n_cols; + old_i++) { + col_map[old_i] = ULINT_UNDEFINED; + } + + for (uint old_i = 0; old_i < old_n_v_cols; old_i++) { + col_map[old_i + old_table->n_cols] = ULINT_UNDEFINED; + } + + const bool omits_virtual = ha_innobase::omits_virtual_cols(*table->s); + + for (const Create_field& new_field : + ha_alter_info->alter_info->create_list) { + bool is_v = !new_field.stored_in_db(); + ulint num_old_v = 0; + + for (uint old_i = 0; table->field[old_i]; old_i++) { + const Field* field = table->field[old_i]; + if (!field->stored_in_db()) { + if (is_v && new_field.field == field) { + if (!omits_virtual) { + col_map[old_table->n_cols + + num_v] + = num_old_v; + } + num_old_v++; + goto found_col; + } + num_old_v++; + continue; + } + + if (new_field.field == field) { + + const Field* altered_field = + altered_table->field[i + num_v]; + + if (defaults) { + innobase_build_col_map_add( + heap, + dtuple_get_nth_field( + defaults, i), + altered_field, + field, + dict_table_is_comp( + new_table)); + } + + col_map[old_i - num_old_v] = i; + if (!old_table->versioned() + || !altered_table->versioned()) { + } else if (old_i == old_table->vers_start) { + new_table->vers_start = (i + num_v) + & dict_index_t::MAX_N_FIELDS; + } else if (old_i == old_table->vers_end) { + new_table->vers_end = (i + num_v) + & dict_index_t::MAX_N_FIELDS; + } + goto found_col; + } + } + + if (!is_v) { + innobase_build_col_map_add( + heap, dtuple_get_nth_field(defaults, i), + altered_table->field[i + num_v], + NULL, + dict_table_is_comp(new_table)); + } +found_col: + if (is_v) { + num_v++; + } else { + i++; + } + } + + DBUG_ASSERT(i == altered_table->s->fields - num_v); + + i = table->s->fields - old_n_v_cols; + + /* Add the InnoDB hidden FTS_DOC_ID column, if any. */ + if (i + DATA_N_SYS_COLS < old_table->n_cols) { + /* There should be exactly one extra field, + the FTS_DOC_ID. */ + DBUG_ASSERT(DICT_TF2_FLAG_IS_SET(old_table, + DICT_TF2_FTS_HAS_DOC_ID)); + DBUG_ASSERT(i + DATA_N_SYS_COLS + 1 == old_table->n_cols); + DBUG_ASSERT(!strcmp(dict_table_get_col_name( + old_table, i), + FTS_DOC_ID_COL_NAME)); + if (altered_table->s->fields + DATA_N_SYS_COLS + - new_table->n_v_cols + < new_table->n_cols) { + DBUG_ASSERT(DICT_TF2_FLAG_IS_SET( + new_table, + DICT_TF2_FTS_HAS_DOC_ID)); + DBUG_ASSERT(altered_table->s->fields + + DATA_N_SYS_COLS + 1 + == static_cast<ulint>( + new_table->n_cols + + new_table->n_v_cols)); + col_map[i] = altered_table->s->fields + - new_table->n_v_cols; + } else { + DBUG_ASSERT(!DICT_TF2_FLAG_IS_SET( + new_table, + DICT_TF2_FTS_HAS_DOC_ID)); + col_map[i] = ULINT_UNDEFINED; + } + + i++; + } else { + DBUG_ASSERT(!DICT_TF2_FLAG_IS_SET( + old_table, + DICT_TF2_FTS_HAS_DOC_ID)); + } + + for (; i < old_table->n_cols; i++) { + col_map[i] = i + new_table->n_cols - old_table->n_cols; + } + + DBUG_RETURN(col_map); +} + +/** Drop newly create FTS index related auxiliary table during +FIC create index process, before fts_add_index is called +@param table table that was being rebuilt online +@param trx transaction +@return DB_SUCCESS if successful, otherwise last error code +*/ +static +dberr_t +innobase_drop_fts_index_table( +/*==========================*/ + dict_table_t* table, + trx_t* trx) +{ + dberr_t ret_err = DB_SUCCESS; + + for (dict_index_t* index = dict_table_get_first_index(table); + index != NULL; + index = dict_table_get_next_index(index)) { + if (index->type & DICT_FTS) { + dberr_t err; + + err = fts_drop_index_tables(trx, index); + + if (err != DB_SUCCESS) { + ret_err = err; + } + } + } + + return(ret_err); +} + +/** Get the new non-virtual column names if any columns were renamed +@param ha_alter_info Data used during in-place alter +@param altered_table MySQL table that is being altered +@param table MySQL table as it is before the ALTER operation +@param user_table InnoDB table as it is before the ALTER operation +@param heap Memory heap for the allocation +@return array of new column names in rebuilt_table, or NULL if not renamed */ +static MY_ATTRIBUTE((nonnull, warn_unused_result)) +const char** +innobase_get_col_names( + Alter_inplace_info* ha_alter_info, + const TABLE* altered_table, + const TABLE* table, + const dict_table_t* user_table, + mem_heap_t* heap) +{ + const char** cols; + uint i; + + DBUG_ENTER("innobase_get_col_names"); + DBUG_ASSERT(user_table->n_t_def > table->s->fields); + DBUG_ASSERT(ha_alter_info->handler_flags + & ALTER_COLUMN_NAME); + + cols = static_cast<const char**>( + mem_heap_zalloc(heap, user_table->n_def * sizeof *cols)); + + i = 0; + for (const Create_field& new_field : + ha_alter_info->alter_info->create_list) { + ulint num_v = 0; + DBUG_ASSERT(i < altered_table->s->fields); + + if (!new_field.stored_in_db()) { + continue; + } + + for (uint old_i = 0; table->field[old_i]; old_i++) { + num_v += !table->field[old_i]->stored_in_db(); + + if (new_field.field == table->field[old_i]) { + cols[old_i - num_v] = new_field.field_name.str; + break; + } + } + + i++; + } + + /* Copy the internal column names. */ + i = table->s->fields - user_table->n_v_def; + cols[i] = dict_table_get_col_name(user_table, i); + + while (++i < user_table->n_def) { + cols[i] = cols[i - 1] + strlen(cols[i - 1]) + 1; + } + + DBUG_RETURN(cols); +} + +/** Check whether the column prefix is increased, decreased, or unchanged. +@param[in] new_prefix_len new prefix length +@param[in] old_prefix_len new prefix length +@retval 1 prefix is increased +@retval 0 prefix is unchanged +@retval -1 prefix is decreased */ +static inline +lint +innobase_pk_col_prefix_compare( + ulint new_prefix_len, + ulint old_prefix_len) +{ + ut_ad(new_prefix_len < COMPRESSED_REC_MAX_DATA_SIZE); + ut_ad(old_prefix_len < COMPRESSED_REC_MAX_DATA_SIZE); + + if (new_prefix_len == old_prefix_len) { + return(0); + } + + if (new_prefix_len == 0) { + new_prefix_len = ULINT_MAX; + } + + if (old_prefix_len == 0) { + old_prefix_len = ULINT_MAX; + } + + if (new_prefix_len > old_prefix_len) { + return(1); + } else { + return(-1); + } +} + +/** Check whether the column is existing in old table. +@param[in] new_col_no new column no +@param[in] col_map mapping of old column numbers to new ones +@param[in] col_map_size the column map size +@return true if the column is existing, otherwise false. */ +static inline +bool +innobase_pk_col_is_existing( + const ulint new_col_no, + const ulint* col_map, + const ulint col_map_size) +{ + for (ulint i = 0; i < col_map_size; i++) { + if (col_map[i] == new_col_no) { + return(true); + } + } + + return(false); +} + +/** Determine whether both the indexes have same set of primary key +fields arranged in the same order. + +Rules when we cannot skip sorting: +(1) Removing existing PK columns somewhere else than at the end of the PK; +(2) Adding existing columns to the PK, except at the end of the PK when no +columns are removed from the PK; +(3) Changing the order of existing PK columns; +(4) Decreasing the prefix length just like removing existing PK columns +follows rule(1), Increasing the prefix length just like adding existing +PK columns follows rule(2). +@param[in] col_map mapping of old column numbers to new ones +@param[in] ha_alter_info Data used during in-place alter +@param[in] old_clust_index index to be compared +@param[in] new_clust_index index to be compared +@retval true if both indexes have same order. +@retval false. */ +static MY_ATTRIBUTE((warn_unused_result)) +bool +innobase_pk_order_preserved( + const ulint* col_map, + const dict_index_t* old_clust_index, + const dict_index_t* new_clust_index) +{ + ulint old_n_uniq + = dict_index_get_n_ordering_defined_by_user( + old_clust_index); + ulint new_n_uniq + = dict_index_get_n_ordering_defined_by_user( + new_clust_index); + + ut_ad(dict_index_is_clust(old_clust_index)); + ut_ad(dict_index_is_clust(new_clust_index)); + ut_ad(old_clust_index->table != new_clust_index->table); + ut_ad(col_map != NULL); + + if (old_n_uniq == 0) { + /* There was no PRIMARY KEY in the table. + If there is no PRIMARY KEY after the ALTER either, + no sorting is needed. */ + return(new_n_uniq == old_n_uniq); + } + + /* DROP PRIMARY KEY is only allowed in combination with + ADD PRIMARY KEY. */ + ut_ad(new_n_uniq > 0); + + /* The order of the last processed new_clust_index key field, + not counting ADD COLUMN, which are constant. */ + lint last_field_order = -1; + ulint existing_field_count = 0; + ulint old_n_cols = dict_table_get_n_cols(old_clust_index->table); + for (ulint new_field = 0; new_field < new_n_uniq; new_field++) { + ulint new_col_no = + new_clust_index->fields[new_field].col->ind; + + /* Check if there is a match in old primary key. */ + ulint old_field = 0; + while (old_field < old_n_uniq) { + ulint old_col_no = + old_clust_index->fields[old_field].col->ind; + + if (col_map[old_col_no] == new_col_no) { + break; + } + + old_field++; + } + + /* The order of key field in the new primary key. + 1. old PK column: idx in old primary key + 2. existing column: old_n_uniq + sequence no + 3. newly added column: no order */ + lint new_field_order; + const bool old_pk_column = old_field < old_n_uniq; + + if (old_pk_column) { + new_field_order = lint(old_field); + } else if (innobase_pk_col_is_existing(new_col_no, col_map, + old_n_cols) + || new_clust_index->table->persistent_autoinc + == new_field + 1) { + /* Adding an existing column or an AUTO_INCREMENT + column may change the existing ordering. */ + new_field_order = lint(old_n_uniq + + existing_field_count++); + } else { + /* Skip newly added column. */ + continue; + } + + if (last_field_order + 1 != new_field_order) { + /* Old PK order is not kept, or existing column + is not added at the end of old PK. */ + return(false); + } + + last_field_order = new_field_order; + + if (!old_pk_column) { + continue; + } + + /* Check prefix length change. */ + const lint prefix_change = innobase_pk_col_prefix_compare( + new_clust_index->fields[new_field].prefix_len, + old_clust_index->fields[old_field].prefix_len); + + if (prefix_change < 0) { + /* If a column's prefix length is decreased, it should + be the last old PK column in new PK. + Note: we set last_field_order to -2, so that if there + are any old PK colmns or existing columns after it in + new PK, the comparison to new_field_order will fail in + the next round.*/ + last_field_order = -2; + } else if (prefix_change > 0) { + /* If a column's prefix length is increased, it should + be the last PK column in old PK. */ + if (old_field != old_n_uniq - 1) { + return(false); + } + } + } + + return(true); +} + +/** Update the mtype from DATA_BLOB to DATA_GEOMETRY for a specified +GIS column of a table. This is used when we want to create spatial index +on legacy GIS columns coming from 5.6, where we store GIS data as DATA_BLOB +in innodb layer. +@param[in] table_id table id +@param[in] col_name column name +@param[in] trx data dictionary transaction +@retval true Failure +@retval false Success */ +static +bool +innobase_update_gis_column_type( + table_id_t table_id, + const char* col_name, + trx_t* trx) +{ + pars_info_t* info; + dberr_t error; + + DBUG_ENTER("innobase_update_gis_column_type"); + + DBUG_ASSERT(trx_get_dict_operation(trx) == TRX_DICT_OP_INDEX); + ut_ad(trx->dict_operation_lock_mode == RW_X_LATCH); + ut_d(dict_sys.assert_locked()); + + info = pars_info_create(); + + pars_info_add_ull_literal(info, "tableid", table_id); + pars_info_add_str_literal(info, "name", col_name); + pars_info_add_int4_literal(info, "mtype", DATA_GEOMETRY); + + trx->op_info = "update column type to DATA_GEOMETRY"; + + error = que_eval_sql( + info, + "PROCEDURE UPDATE_SYS_COLUMNS_PROC () IS\n" + "BEGIN\n" + "UPDATE SYS_COLUMNS SET MTYPE=:mtype\n" + "WHERE TABLE_ID=:tableid AND NAME=:name;\n" + "END;\n", + false, trx); + + trx->error_state = DB_SUCCESS; + trx->op_info = ""; + + DBUG_RETURN(error != DB_SUCCESS); +} + +/** Check if we are creating spatial indexes on GIS columns, which are +legacy columns from earlier MySQL, such as 5.6. If so, we have to update +the mtypes of the old GIS columns to DATA_GEOMETRY. +In 5.6, we store GIS columns as DATA_BLOB in InnoDB layer, it will introduce +confusion when we run latest server on older data. That's why we need to +do the upgrade. +@param[in] ha_alter_info Data used during in-place alter +@param[in] table Table on which we want to add indexes +@param[in] trx Transaction +@return DB_SUCCESS if update successfully or no columns need to be updated, +otherwise DB_ERROR, which means we can't update the mtype for some +column, and creating spatial index on it should be dangerous */ +static +dberr_t +innobase_check_gis_columns( + Alter_inplace_info* ha_alter_info, + dict_table_t* table, + trx_t* trx) +{ + DBUG_ENTER("innobase_check_gis_columns"); + + for (uint key_num = 0; + key_num < ha_alter_info->index_add_count; + key_num++) { + + const KEY& key = ha_alter_info->key_info_buffer[ + ha_alter_info->index_add_buffer[key_num]]; + + if (!(key.flags & HA_SPATIAL)) { + continue; + } + + ut_ad(key.user_defined_key_parts == 1); + const KEY_PART_INFO& key_part = key.key_part[0]; + + /* Does not support spatial index on virtual columns */ + if (!key_part.field->stored_in_db()) { + DBUG_RETURN(DB_UNSUPPORTED); + } + + ulint col_nr = dict_table_has_column( + table, + key_part.field->field_name.str, + key_part.fieldnr); + ut_ad(col_nr != table->n_def); + dict_col_t* col = &table->cols[col_nr]; + + if (col->mtype != DATA_BLOB) { + ut_ad(DATA_GEOMETRY_MTYPE(col->mtype)); + continue; + } + + const char* col_name = dict_table_get_col_name( + table, col_nr); + + if (innobase_update_gis_column_type( + table->id, col_name, trx)) { + + DBUG_RETURN(DB_ERROR); + } else { + col->mtype = DATA_GEOMETRY; + + ib::info() << "Updated mtype of column" << col_name + << " in table " << table->name + << ", whose id is " << table->id + << " to DATA_GEOMETRY"; + } + } + + DBUG_RETURN(DB_SUCCESS); +} + +/** Collect virtual column info for its addition +@param[in] ha_alter_info Data used during in-place alter +@param[in] altered_table MySQL table that is being altered to +@param[in] table MySQL table as it is before the ALTER operation +@retval true Failure +@retval false Success */ +static +bool +prepare_inplace_add_virtual( + Alter_inplace_info* ha_alter_info, + const TABLE* altered_table, + const TABLE* table) +{ + ha_innobase_inplace_ctx* ctx; + uint16_t i = 0, j = 0; + + ctx = static_cast<ha_innobase_inplace_ctx*> + (ha_alter_info->handler_ctx); + + ctx->num_to_add_vcol = altered_table->s->virtual_fields + + ctx->num_to_drop_vcol - table->s->virtual_fields; + + ctx->add_vcol = static_cast<dict_v_col_t*>( + mem_heap_zalloc(ctx->heap, ctx->num_to_add_vcol + * sizeof *ctx->add_vcol)); + ctx->add_vcol_name = static_cast<const char**>( + mem_heap_alloc(ctx->heap, ctx->num_to_add_vcol + * sizeof *ctx->add_vcol_name)); + + for (const Create_field& new_field : + ha_alter_info->alter_info->create_list) { + const Field* field = altered_table->field[i++]; + + if (new_field.field || field->stored_in_db()) { + continue; + } + + unsigned is_unsigned; + auto col_type = get_innobase_type_from_mysql_type( + &is_unsigned, field); + + auto col_len = field->pack_length(); + unsigned field_type = field->type() | is_unsigned; + + if (!field->real_maybe_null()) { + field_type |= DATA_NOT_NULL; + } + + if (field->binary()) { + field_type |= DATA_BINARY_TYPE; + } + + unsigned charset_no; + + if (dtype_is_string_type(col_type)) { + charset_no = field->charset()->number; + + DBUG_EXECUTE_IF( + "ib_alter_add_virtual_fail", + charset_no += MAX_CHAR_COLL_NUM;); + + if (charset_no > MAX_CHAR_COLL_NUM) { + my_error(ER_WRONG_KEY_COLUMN, MYF(0), "InnoDB", + field->field_name.str); + return(true); + } + } else { + charset_no = 0; + } + + if (field->type() == MYSQL_TYPE_VARCHAR) { + uint32 length_bytes + = static_cast<const Field_varstring*>( + field)->length_bytes; + + col_len -= length_bytes; + + if (length_bytes == 2) { + field_type |= DATA_LONG_TRUE_VARCHAR; + } + } + + new (&ctx->add_vcol[j]) dict_v_col_t(); + ctx->add_vcol[j].m_col.prtype = dtype_form_prtype( + field_type, charset_no); + + ctx->add_vcol[j].m_col.prtype |= DATA_VIRTUAL; + + ctx->add_vcol[j].m_col.mtype = col_type; + + ctx->add_vcol[j].m_col.len = static_cast<uint16_t>(col_len); + + ctx->add_vcol[j].m_col.ind = (i - 1) + & dict_index_t::MAX_N_FIELDS; + ctx->add_vcol[j].num_base = 0; + ctx->add_vcol_name[j] = field->field_name.str; + ctx->add_vcol[j].base_col = NULL; + ctx->add_vcol[j].v_pos = (ctx->old_table->n_v_cols + - ctx->num_to_drop_vcol + j) + & dict_index_t::MAX_N_FIELDS; + + /* MDEV-17468: Do this on ctx->instant_table later */ + innodb_base_col_setup(ctx->old_table, field, &ctx->add_vcol[j]); + j++; + } + + return(false); +} + +/** Collect virtual column info for its addition +@param[in] ha_alter_info Data used during in-place alter +@param[in] table MySQL table as it is before the ALTER operation +@retval true Failure +@retval false Success */ +static +bool +prepare_inplace_drop_virtual( + Alter_inplace_info* ha_alter_info, + const TABLE* table) +{ + ha_innobase_inplace_ctx* ctx; + unsigned i = 0, j = 0; + + ctx = static_cast<ha_innobase_inplace_ctx*> + (ha_alter_info->handler_ctx); + + ctx->num_to_drop_vcol = 0; + for (i = 0; table->field[i]; i++) { + const Field* field = table->field[i]; + if (field->flags & FIELD_IS_DROPPED && !field->stored_in_db()) { + ctx->num_to_drop_vcol++; + } + } + + ctx->drop_vcol = static_cast<dict_v_col_t*>( + mem_heap_alloc(ctx->heap, ctx->num_to_drop_vcol + * sizeof *ctx->drop_vcol)); + ctx->drop_vcol_name = static_cast<const char**>( + mem_heap_alloc(ctx->heap, ctx->num_to_drop_vcol + * sizeof *ctx->drop_vcol_name)); + + for (i = 0; table->field[i]; i++) { + Field *field = table->field[i]; + if (!(field->flags & FIELD_IS_DROPPED) || field->stored_in_db()) { + continue; + } + + unsigned is_unsigned; + + auto col_type = get_innobase_type_from_mysql_type( + &is_unsigned, field); + + auto col_len = field->pack_length(); + unsigned field_type = field->type() | is_unsigned; + + if (!field->real_maybe_null()) { + field_type |= DATA_NOT_NULL; + } + + if (field->binary()) { + field_type |= DATA_BINARY_TYPE; + } + + unsigned charset_no = 0; + + if (dtype_is_string_type(col_type)) { + charset_no = field->charset()->number; + + DBUG_EXECUTE_IF( + "ib_alter_add_virtual_fail", + charset_no += MAX_CHAR_COLL_NUM;); + + if (charset_no > MAX_CHAR_COLL_NUM) { + my_error(ER_WRONG_KEY_COLUMN, MYF(0), "InnoDB", + field->field_name.str); + return(true); + } + } else { + charset_no = 0; + } + + if (field->type() == MYSQL_TYPE_VARCHAR) { + uint32 length_bytes + = static_cast<const Field_varstring*>( + field)->length_bytes; + + col_len -= length_bytes; + + if (length_bytes == 2) { + field_type |= DATA_LONG_TRUE_VARCHAR; + } + } + + + ctx->drop_vcol[j].m_col.prtype = dtype_form_prtype( + field_type, charset_no); + + ctx->drop_vcol[j].m_col.prtype |= DATA_VIRTUAL; + + ctx->drop_vcol[j].m_col.mtype = col_type; + + ctx->drop_vcol[j].m_col.len = static_cast<uint16_t>(col_len); + + ctx->drop_vcol[j].m_col.ind = i & dict_index_t::MAX_N_FIELDS; + + ctx->drop_vcol_name[j] = field->field_name.str; + + dict_v_col_t* v_col = dict_table_get_nth_v_col_mysql( + ctx->old_table, i); + ctx->drop_vcol[j].v_pos = v_col->v_pos; + j++; + } + + return(false); +} + +/** Insert a new record to INNODB SYS_VIRTUAL +@param[in] table InnoDB table +@param[in] pos virtual column column no +@param[in] base_pos base column pos +@param[in] trx transaction +@retval false on success +@retval true on failure (my_error() will have been called) */ +static bool innobase_insert_sys_virtual( + const dict_table_t* table, + ulint pos, + ulint base_pos, + trx_t* trx) +{ + pars_info_t* info = pars_info_create(); + pars_info_add_ull_literal(info, "id", table->id); + pars_info_add_int4_literal(info, "pos", pos); + pars_info_add_int4_literal(info, "base_pos", base_pos); + + if (DB_SUCCESS != que_eval_sql( + info, + "PROCEDURE P () IS\n" + "BEGIN\n" + "INSERT INTO SYS_VIRTUAL VALUES (:id, :pos, :base_pos);\n" + "END;\n", + FALSE, trx)) { + my_error(ER_INTERNAL_ERROR, MYF(0), + "InnoDB: ADD COLUMN...VIRTUAL"); + return true; + } + + return false; +} + +/** Insert a record to the SYS_COLUMNS dictionary table. +@param[in] table_id table id +@param[in] pos position of the column +@param[in] field_name field name +@param[in] mtype main type +@param[in] prtype precise type +@param[in] len fixed length in bytes, or 0 +@param[in] n_base number of base columns of virtual columns, or 0 +@param[in] update whether to update instead of inserting +@retval false on success +@retval true on failure (my_error() will have been called) */ +static bool innodb_insert_sys_columns( + table_id_t table_id, + ulint pos, + const char* field_name, + ulint mtype, + ulint prtype, + ulint len, + ulint n_base, + trx_t* trx, + bool update = false) +{ + pars_info_t* info = pars_info_create(); + pars_info_add_ull_literal(info, "id", table_id); + pars_info_add_int4_literal(info, "pos", pos); + pars_info_add_str_literal(info, "name", field_name); + pars_info_add_int4_literal(info, "mtype", mtype); + pars_info_add_int4_literal(info, "prtype", prtype); + pars_info_add_int4_literal(info, "len", len); + pars_info_add_int4_literal(info, "base", n_base); + + if (update) { + if (DB_SUCCESS != que_eval_sql( + info, + "PROCEDURE UPD_COL () IS\n" + "BEGIN\n" + "UPDATE SYS_COLUMNS SET\n" + "NAME=:name, MTYPE=:mtype, PRTYPE=:prtype, " + "LEN=:len, PREC=:base\n" + "WHERE TABLE_ID=:id AND POS=:pos;\n" + "END;\n", FALSE, trx)) { + my_error(ER_INTERNAL_ERROR, MYF(0), + "InnoDB: Updating SYS_COLUMNS failed"); + return true; + } + + return false; + } + + if (DB_SUCCESS != que_eval_sql( + info, + "PROCEDURE ADD_COL () IS\n" + "BEGIN\n" + "INSERT INTO SYS_COLUMNS VALUES" + "(:id,:pos,:name,:mtype,:prtype,:len,:base);\n" + "END;\n", FALSE, trx)) { + my_error(ER_INTERNAL_ERROR, MYF(0), + "InnoDB: Insert into SYS_COLUMNS failed"); + return true; + } + + return false; +} + +/** Update INNODB SYS_COLUMNS on new virtual columns +@param[in] table InnoDB table +@param[in] col_name column name +@param[in] vcol virtual column +@param[in] trx transaction +@retval false on success +@retval true on failure (my_error() will have been called) */ +static bool innobase_add_one_virtual( + const dict_table_t* table, + const char* col_name, + dict_v_col_t* vcol, + trx_t* trx) +{ + ulint pos = dict_create_v_col_pos(vcol->v_pos, + vcol->m_col.ind); + + if (innodb_insert_sys_columns(table->id, pos, col_name, + vcol->m_col.mtype, vcol->m_col.prtype, + vcol->m_col.len, vcol->num_base, trx)) { + return true; + } + + for (unsigned i = 0; i < vcol->num_base; i++) { + if (innobase_insert_sys_virtual( + table, pos, vcol->base_col[i]->ind, trx)) { + return true; + } + } + + return false; +} + +/** Update SYS_TABLES.N_COLS in the data dictionary. +@param[in] user_table InnoDB table +@param[in] n the new value of SYS_TABLES.N_COLS +@param[in] trx transaction +@return whether the operation failed */ +static bool innodb_update_cols(const dict_table_t* table, ulint n, trx_t* trx) +{ + pars_info_t* info = pars_info_create(); + + pars_info_add_int4_literal(info, "n", n); + pars_info_add_ull_literal(info, "id", table->id); + + if (DB_SUCCESS != que_eval_sql(info, + "PROCEDURE UPDATE_N_COLS () IS\n" + "BEGIN\n" + "UPDATE SYS_TABLES SET N_COLS = :n" + " WHERE ID = :id;\n" + "END;\n", FALSE, trx)) { + my_error(ER_INTERNAL_ERROR, MYF(0), + "InnoDB: Updating SYS_TABLES.N_COLS failed"); + return true; + } + + return false; +} + +/** Update system table for adding virtual column(s) +@param[in] ha_alter_info Data used during in-place alter +@param[in] user_table InnoDB table +@param[in] trx transaction +@retval true Failure +@retval false Success */ +static +bool +innobase_add_virtual_try( + const Alter_inplace_info* ha_alter_info, + const dict_table_t* user_table, + trx_t* trx) +{ + ha_innobase_inplace_ctx* ctx = static_cast<ha_innobase_inplace_ctx*>( + ha_alter_info->handler_ctx); + + for (ulint i = 0; i < ctx->num_to_add_vcol; i++) { + if (innobase_add_one_virtual( + user_table, ctx->add_vcol_name[i], + &ctx->add_vcol[i], trx)) { + return true; + } + } + + return false; +} + +/** Delete metadata from SYS_COLUMNS and SYS_VIRTUAL. +@param[in] id table id +@param[in] pos first SYS_COLUMNS.POS +@param[in,out] trx data dictionary transaction +@retval true Failure +@retval false Success. */ +static bool innobase_instant_drop_cols(table_id_t id, ulint pos, trx_t* trx) +{ + pars_info_t* info = pars_info_create(); + pars_info_add_ull_literal(info, "id", id); + pars_info_add_int4_literal(info, "pos", pos); + + dberr_t err = que_eval_sql( + info, + "PROCEDURE DELETE_COL () IS\n" + "BEGIN\n" + "DELETE FROM SYS_COLUMNS WHERE\n" + "TABLE_ID = :id AND POS >= :pos;\n" + "DELETE FROM SYS_VIRTUAL WHERE TABLE_ID = :id;\n" + "END;\n", FALSE, trx); + if (err != DB_SUCCESS) { + my_error(ER_INTERNAL_ERROR, MYF(0), + "InnoDB: DELETE from SYS_COLUMNS/SYS_VIRTUAL failed"); + return true; + } + + return false; +} + +/** Update INNODB SYS_COLUMNS on new virtual column's position +@param[in] table InnoDB table +@param[in] old_pos old position +@param[in] new_pos new position +@param[in] trx transaction +@return DB_SUCCESS if successful, otherwise error code */ +static +dberr_t +innobase_update_v_pos_sys_columns( + const dict_table_t* table, + ulint old_pos, + ulint new_pos, + trx_t* trx) +{ + pars_info_t* info = pars_info_create(); + + pars_info_add_int4_literal(info, "pos", old_pos); + pars_info_add_int4_literal(info, "val", new_pos); + pars_info_add_ull_literal(info, "id", table->id); + + dberr_t error = que_eval_sql( + info, + "PROCEDURE P () IS\n" + "BEGIN\n" + "UPDATE SYS_COLUMNS\n" + "SET POS = :val\n" + "WHERE POS = :pos\n" + "AND TABLE_ID = :id;\n" + "END;\n", + FALSE, trx); + + return(error); +} + +/** Update INNODB SYS_VIRTUAL table with new virtual column position +@param[in] table InnoDB table +@param[in] old_pos old position +@param[in] new_pos new position +@param[in] trx transaction +@return DB_SUCCESS if successful, otherwise error code */ +static +dberr_t +innobase_update_v_pos_sys_virtual( + const dict_table_t* table, + ulint old_pos, + ulint new_pos, + trx_t* trx) +{ + pars_info_t* info = pars_info_create(); + + pars_info_add_int4_literal(info, "pos", old_pos); + pars_info_add_int4_literal(info, "val", new_pos); + pars_info_add_ull_literal(info, "id", table->id); + + dberr_t error = que_eval_sql( + info, + "PROCEDURE P () IS\n" + "BEGIN\n" + "UPDATE SYS_VIRTUAL\n" + "SET POS = :val\n" + "WHERE POS = :pos\n" + "AND TABLE_ID = :id;\n" + "END;\n", + FALSE, trx); + + return(error); +} + +/** Update InnoDB system tables on dropping a virtual column +@param[in] table InnoDB table +@param[in] col_name column name of the dropping column +@param[in] drop_col col information for the dropping column +@param[in] n_prev_dropped number of previously dropped columns in the + same alter clause +@param[in] trx transaction +@return DB_SUCCESS if successful, otherwise error code */ +static +dberr_t +innobase_drop_one_virtual_sys_columns( + const dict_table_t* table, + const char* col_name, + dict_col_t* drop_col, + ulint n_prev_dropped, + trx_t* trx) +{ + pars_info_t* info = pars_info_create(); + pars_info_add_ull_literal(info, "id", table->id); + + pars_info_add_str_literal(info, "name", col_name); + + dberr_t error = que_eval_sql( + info, + "PROCEDURE P () IS\n" + "BEGIN\n" + "DELETE FROM SYS_COLUMNS\n" + "WHERE TABLE_ID = :id\n" + "AND NAME = :name;\n" + "END;\n", + FALSE, trx); + + if (error != DB_SUCCESS) { + return(error); + } + + dict_v_col_t* v_col = dict_table_get_nth_v_col_mysql( + table, drop_col->ind); + + /* Adjust column positions for all subsequent columns */ + for (ulint i = v_col->v_pos + 1; i < table->n_v_cols; i++) { + dict_v_col_t* t_col = dict_table_get_nth_v_col(table, i); + ulint old_p = dict_create_v_col_pos( + t_col->v_pos - n_prev_dropped, + t_col->m_col.ind - n_prev_dropped); + ulint new_p = dict_create_v_col_pos( + t_col->v_pos - 1 - n_prev_dropped, + ulint(t_col->m_col.ind) - 1 - n_prev_dropped); + + error = innobase_update_v_pos_sys_columns( + table, old_p, new_p, trx); + if (error != DB_SUCCESS) { + return(error); + } + error = innobase_update_v_pos_sys_virtual( + table, old_p, new_p, trx); + if (error != DB_SUCCESS) { + return(error); + } + } + + return(error); +} + +/** Delete virtual column's info from INNODB SYS_VIRTUAL +@param[in] table InnoDB table +@param[in] pos position of the virtual column to be deleted +@param[in] trx transaction +@return DB_SUCCESS if successful, otherwise error code */ +static +dberr_t +innobase_drop_one_virtual_sys_virtual( + const dict_table_t* table, + ulint pos, + trx_t* trx) +{ + pars_info_t* info = pars_info_create(); + pars_info_add_ull_literal(info, "id", table->id); + + pars_info_add_int4_literal(info, "pos", pos); + + dberr_t error = que_eval_sql( + info, + "PROCEDURE P () IS\n" + "BEGIN\n" + "DELETE FROM SYS_VIRTUAL\n" + "WHERE TABLE_ID = :id\n" + "AND POS = :pos;\n" + "END;\n", + FALSE, trx); + + return(error); +} + +/** Update system table for dropping virtual column(s) +@param[in] ha_alter_info Data used during in-place alter +@param[in] user_table InnoDB table +@param[in] trx transaction +@retval true Failure +@retval false Success */ +static +bool +innobase_drop_virtual_try( + const Alter_inplace_info* ha_alter_info, + const dict_table_t* user_table, + trx_t* trx) +{ + ha_innobase_inplace_ctx* ctx; + dberr_t err = DB_SUCCESS; + + ctx = static_cast<ha_innobase_inplace_ctx*> + (ha_alter_info->handler_ctx); + + for (unsigned i = 0; i < ctx->num_to_drop_vcol; i++) { + + ulint pos = dict_create_v_col_pos( + ctx->drop_vcol[i].v_pos - i, + ctx->drop_vcol[i].m_col.ind - i); + err = innobase_drop_one_virtual_sys_virtual( + user_table, pos, trx); + + if (err != DB_SUCCESS) { + my_error(ER_INTERNAL_ERROR, MYF(0), + "InnoDB: DROP COLUMN...VIRTUAL"); + return(true); + } + + err = innobase_drop_one_virtual_sys_columns( + user_table, ctx->drop_vcol_name[i], + &(ctx->drop_vcol[i].m_col), i, trx); + + if (err != DB_SUCCESS) { + my_error(ER_INTERNAL_ERROR, MYF(0), + "InnoDB: DROP COLUMN...VIRTUAL"); + return(true); + } + } + + return false; +} + +/** Serialise metadata of dropped or reordered columns. +@param[in,out] heap memory heap for allocation +@param[out] field data field with the metadata */ +inline +void dict_table_t::serialise_columns(mem_heap_t* heap, dfield_t* field) const +{ + DBUG_ASSERT(instant); + const dict_index_t& index = *UT_LIST_GET_FIRST(indexes); + unsigned n_fixed = index.first_user_field(); + unsigned num_non_pk_fields = index.n_fields - n_fixed; + + ulint len = 4 + num_non_pk_fields * 2; + + byte* data = static_cast<byte*>(mem_heap_alloc(heap, len)); + + dfield_set_data(field, data, len); + + mach_write_to_4(data, num_non_pk_fields); + + data += 4; + + for (ulint i = n_fixed; i < index.n_fields; i++) { + mach_write_to_2(data, instant->field_map[i - n_fixed]); + data += 2; + } +} + +/** Construct the metadata record for instant ALTER TABLE. +@param[in] row dummy or default values for existing columns +@param[in,out] heap memory heap for allocations +@return metadata record */ +inline +dtuple_t* +dict_index_t::instant_metadata(const dtuple_t& row, mem_heap_t* heap) const +{ + ut_ad(is_primary()); + dtuple_t* entry; + + if (!table->instant) { + entry = row_build_index_entry(&row, NULL, this, heap); + entry->info_bits = REC_INFO_METADATA_ADD; + return entry; + } + + entry = dtuple_create(heap, n_fields + 1); + entry->n_fields_cmp = n_uniq; + entry->info_bits = REC_INFO_METADATA_ALTER; + + const dict_field_t* field = fields; + + for (uint i = 0; i <= n_fields; i++, field++) { + dfield_t* dfield = dtuple_get_nth_field(entry, i); + + if (i == first_user_field()) { + table->serialise_columns(heap, dfield); + dfield->type.metadata_blob_init(); + field--; + continue; + } + + ut_ad(!field->col->is_virtual()); + + if (field->col->is_dropped()) { + dict_col_copy_type(field->col, &dfield->type); + if (field->col->is_nullable()) { + dfield_set_null(dfield); + } else { + dfield_set_data(dfield, field_ref_zero, + field->fixed_len); + } + continue; + } + + const dfield_t* s = dtuple_get_nth_field(&row, field->col->ind); + ut_ad(dict_col_type_assert_equal(field->col, &s->type)); + *dfield = *s; + + if (dfield_is_null(dfield)) { + continue; + } + + if (dfield_is_ext(dfield)) { + ut_ad(i > first_user_field()); + ut_ad(!field->prefix_len); + ut_ad(dfield->len >= FIELD_REF_SIZE); + dfield_set_len(dfield, dfield->len - FIELD_REF_SIZE); + } + + if (!field->prefix_len) { + continue; + } + + ut_ad(field->col->ord_part); + ut_ad(i < n_uniq); + + ulint len = dtype_get_at_most_n_mbchars( + field->col->prtype, + field->col->mbminlen, field->col->mbmaxlen, + field->prefix_len, dfield->len, + static_cast<char*>(dfield_get_data(dfield))); + dfield_set_len(dfield, len); + } + + return entry; +} + +/** Insert or update SYS_COLUMNS and the hidden metadata record +for instant ALTER TABLE. +@param[in] ha_alter_info ALTER TABLE context +@param[in,out] ctx ALTER TABLE context for the current partition +@param[in] altered_table MySQL table that is being altered +@param[in] table MySQL table as it is before the ALTER operation +@param[in,out] trx dictionary transaction +@retval true failure +@retval false success */ +static bool innobase_instant_try( + const Alter_inplace_info* ha_alter_info, + ha_innobase_inplace_ctx* ctx, + const TABLE* altered_table, + const TABLE* table, + trx_t* trx) +{ + DBUG_ASSERT(!ctx->need_rebuild()); + DBUG_ASSERT(ctx->is_instant()); + + dict_table_t* user_table = ctx->old_table; + + dict_index_t* index = dict_table_get_first_index(user_table); + const unsigned n_old_fields = index->n_fields; + const dict_col_t* old_cols = user_table->cols; + DBUG_ASSERT(user_table->n_cols == ctx->old_n_cols); + + const bool metadata_changed = ctx->instant_column(); + + DBUG_ASSERT(index->n_fields >= n_old_fields); + /* The table may have been emptied and may have lost its + 'instantness' during this ALTER TABLE. */ + + /* Construct a table row of default values for the stored columns. */ + dtuple_t* row = dtuple_create(ctx->heap, user_table->n_cols); + dict_table_copy_types(row, user_table); + Field** af = altered_table->field; + Field** const end = altered_table->field + altered_table->s->fields; + ut_d(List_iterator_fast<Create_field> cf_it( + ha_alter_info->alter_info->create_list)); + if (ctx->first_alter_pos + && innobase_instant_drop_cols(user_table->id, + ctx->first_alter_pos - 1, trx)) { + return true; + } + for (uint i = 0; af < end; af++) { + if (!(*af)->stored_in_db()) { + ut_d(cf_it++); + continue; + } + + const dict_col_t* old = dict_table_t::find(old_cols, + ctx->col_map, + ctx->old_n_cols, i); + DBUG_ASSERT(!old || i >= ctx->old_n_cols - DATA_N_SYS_COLS + || old->ind == i + || (ctx->first_alter_pos + && old->ind >= ctx->first_alter_pos - 1)); + + dfield_t* d = dtuple_get_nth_field(row, i); + const dict_col_t* col = dict_table_get_nth_col(user_table, i); + DBUG_ASSERT(!col->is_virtual()); + DBUG_ASSERT(!col->is_dropped()); + DBUG_ASSERT(col->mtype != DATA_SYS); + DBUG_ASSERT(!strcmp((*af)->field_name.str, + dict_table_get_col_name(user_table, i))); + DBUG_ASSERT(old || col->is_added()); + + ut_d(const Create_field* new_field = cf_it++); + /* new_field->field would point to an existing column. + If it is NULL, the column was added by this ALTER TABLE. */ + ut_ad(!new_field->field == !old); + + if (col->is_added()) { + dfield_set_data(d, col->def_val.data, + col->def_val.len); + } else if ((*af)->real_maybe_null()) { + /* Store NULL for nullable 'core' columns. */ + dfield_set_null(d); + } else { + switch ((*af)->type()) { + case MYSQL_TYPE_VARCHAR: + case MYSQL_TYPE_GEOMETRY: + case MYSQL_TYPE_TINY_BLOB: + case MYSQL_TYPE_MEDIUM_BLOB: + case MYSQL_TYPE_BLOB: + case MYSQL_TYPE_LONG_BLOB: + variable_length: + /* Store the empty string for 'core' + variable-length NOT NULL columns. */ + dfield_set_data(d, field_ref_zero, 0); + break; + case MYSQL_TYPE_STRING: + if (col->mbminlen != col->mbmaxlen + && user_table->not_redundant()) { + goto variable_length; + } + /* fall through */ + default: + /* For fixed-length NOT NULL 'core' columns, + get a dummy default value from SQL. Note that + we will preserve the old values of these + columns when updating the metadata + record, to avoid unnecessary updates. */ + ulint len = (*af)->pack_length(); + DBUG_ASSERT(d->type.mtype != DATA_INT + || len <= 8); + row_mysql_store_col_in_innobase_format( + d, d->type.mtype == DATA_INT + ? static_cast<byte*>( + mem_heap_alloc(ctx->heap, len)) + : NULL, true, (*af)->ptr, len, + dict_table_is_comp(user_table)); + ut_ad(new_field->field->pack_length() == len); + } + } + + bool update = old && (!ctx->first_alter_pos + || i < ctx->first_alter_pos - 1); + DBUG_ASSERT(!old || col->same_format(*old)); + if (update + && old->prtype == d->type.prtype) { + /* The record is already present in SYS_COLUMNS. */ + } else if (innodb_insert_sys_columns(user_table->id, i, + (*af)->field_name.str, + d->type.mtype, + d->type.prtype, + d->type.len, 0, trx, + update)) { + return true; + } + + i++; + } + + if (innodb_update_cols(user_table, dict_table_encode_n_col( + unsigned(user_table->n_cols) + - DATA_N_SYS_COLS, + user_table->n_v_cols) + | (user_table->flags & DICT_TF_COMPACT) << 31, + trx)) { + return true; + } + + if (ctx->first_alter_pos) { +add_all_virtual: + for (uint i = 0; i < user_table->n_v_cols; i++) { + if (innobase_add_one_virtual( + user_table, + dict_table_get_v_col_name(user_table, i), + &user_table->v_cols[i], trx)) { + return true; + } + } + } else if (ha_alter_info->handler_flags & ALTER_DROP_VIRTUAL_COLUMN) { + if (innobase_instant_drop_cols(user_table->id, 65536, trx)) { + return true; + } + goto add_all_virtual; + } else if ((ha_alter_info->handler_flags & ALTER_ADD_VIRTUAL_COLUMN) + && innobase_add_virtual_try(ha_alter_info, user_table, + trx)) { + return true; + } + + if (!user_table->space) { + /* In case of ALTER TABLE...DISCARD TABLESPACE, + update only the metadata and transform the dictionary + cache entry to the canonical format. */ + index->clear_instant_alter(); + return false; + } + + unsigned i = unsigned(user_table->n_cols) - DATA_N_SYS_COLS; + DBUG_ASSERT(i >= altered_table->s->stored_fields); + DBUG_ASSERT(i <= altered_table->s->stored_fields + 1); + if (i > altered_table->s->fields) { + const dict_col_t& fts_doc_id = user_table->cols[i - 1]; + DBUG_ASSERT(!strcmp(fts_doc_id.name(*user_table), + FTS_DOC_ID_COL_NAME)); + DBUG_ASSERT(!fts_doc_id.is_nullable()); + DBUG_ASSERT(fts_doc_id.len == 8); + dfield_set_data(dtuple_get_nth_field(row, i - 1), + field_ref_zero, fts_doc_id.len); + } + byte trx_id[DATA_TRX_ID_LEN], roll_ptr[DATA_ROLL_PTR_LEN]; + dfield_set_data(dtuple_get_nth_field(row, i++), field_ref_zero, + DATA_ROW_ID_LEN); + dfield_set_data(dtuple_get_nth_field(row, i++), trx_id, sizeof trx_id); + dfield_set_data(dtuple_get_nth_field(row, i),roll_ptr,sizeof roll_ptr); + DBUG_ASSERT(i + 1 == user_table->n_cols); + + trx_write_trx_id(trx_id, trx->id); + /* The DB_ROLL_PTR will be assigned later, when allocating undo log. + Silence a Valgrind warning in dtuple_validate() when + row_ins_clust_index_entry_low() searches for the insert position. */ + memset(roll_ptr, 0, sizeof roll_ptr); + + dtuple_t* entry = index->instant_metadata(*row, ctx->heap); + mtr_t mtr; + mtr.start(); + index->set_modified(mtr); + btr_pcur_t pcur; + btr_pcur_open_at_index_side(true, index, BTR_MODIFY_TREE, &pcur, true, + 0, &mtr); + ut_ad(btr_pcur_is_before_first_on_page(&pcur)); + btr_pcur_move_to_next_on_page(&pcur); + + buf_block_t* block = btr_pcur_get_block(&pcur); + ut_ad(page_is_leaf(block->frame)); + ut_ad(!page_has_prev(block->frame)); + ut_ad(!buf_block_get_page_zip(block)); + const rec_t* rec = btr_pcur_get_rec(&pcur); + que_thr_t* thr = pars_complete_graph_for_exec( + NULL, trx, ctx->heap, NULL); + const bool is_root = block->page.id().page_no() == index->page; + + dberr_t err = DB_SUCCESS; + if (rec_is_metadata(rec, *index)) { + ut_ad(page_rec_is_user_rec(rec)); + if (is_root + && !rec_is_alter_metadata(rec, *index) + && !index->table->instant + && !page_has_next(block->frame) + && page_rec_is_last(rec, block->frame)) { + goto empty_table; + } + + if (!metadata_changed) { + goto func_exit; + } + + /* Ensure that the root page is in the correct format. */ + buf_block_t* root = btr_root_block_get(index, RW_X_LATCH, + &mtr); + DBUG_ASSERT(root); + if (fil_page_get_type(root->frame) != FIL_PAGE_TYPE_INSTANT) { + DBUG_ASSERT("wrong page type" == 0); + err = DB_CORRUPTION; + goto func_exit; + } + + btr_set_instant(root, *index, &mtr); + + /* Extend the record with any added columns. */ + uint n = uint(index->n_fields) - n_old_fields; + /* Reserve room for DB_TRX_ID,DB_ROLL_PTR and any + non-updated off-page columns in case they are moved off + page as a result of the update. */ + const uint16_t f = user_table->instant != NULL; + upd_t* update = upd_create(index->n_fields + f, ctx->heap); + update->n_fields = n + f; + update->info_bits = f + ? REC_INFO_METADATA_ALTER + : REC_INFO_METADATA_ADD; + if (f) { + upd_field_t* uf = upd_get_nth_field(update, 0); + uf->field_no = index->first_user_field(); + uf->new_val = entry->fields[uf->field_no]; + DBUG_ASSERT(!dfield_is_ext(&uf->new_val)); + DBUG_ASSERT(!dfield_is_null(&uf->new_val)); + } + + /* Add the default values for instantly added columns */ + unsigned j = f; + + for (unsigned k = n_old_fields; k < index->n_fields; k++) { + upd_field_t* uf = upd_get_nth_field(update, j++); + uf->field_no = static_cast<uint16_t>(k + f); + uf->new_val = entry->fields[k + f]; + + ut_ad(j <= n + f); + } + + ut_ad(j == n + f); + + rec_offs* offsets = NULL; + mem_heap_t* offsets_heap = NULL; + big_rec_t* big_rec; + err = btr_cur_pessimistic_update( + BTR_NO_LOCKING_FLAG | BTR_KEEP_POS_FLAG, + btr_pcur_get_btr_cur(&pcur), + &offsets, &offsets_heap, ctx->heap, + &big_rec, update, UPD_NODE_NO_ORD_CHANGE, + thr, trx->id, &mtr); + + offsets = rec_get_offsets( + btr_pcur_get_rec(&pcur), index, offsets, + index->n_core_fields, ULINT_UNDEFINED, &offsets_heap); + if (big_rec) { + if (err == DB_SUCCESS) { + err = btr_store_big_rec_extern_fields( + &pcur, offsets, big_rec, &mtr, + BTR_STORE_UPDATE); + } + + dtuple_big_rec_free(big_rec); + } + if (offsets_heap) { + mem_heap_free(offsets_heap); + } + btr_pcur_close(&pcur); + goto func_exit; + } else if (is_root && page_rec_is_supremum(rec) + && !index->table->instant) { +empty_table: + /* The table is empty. */ + ut_ad(fil_page_index_page_check(block->frame)); + ut_ad(!page_has_siblings(block->frame)); + ut_ad(block->page.id().page_no() == index->page); + /* MDEV-17383: free metadata BLOBs! */ + btr_page_empty(block, NULL, index, 0, &mtr); + if (index->is_instant()) { + index->clear_instant_add(); + } + goto func_exit; + } else if (!user_table->is_instant()) { + ut_ad(!user_table->not_redundant()); + goto func_exit; + } + + /* Convert the table to the instant ALTER TABLE format. */ + mtr.commit(); + mtr.start(); + index->set_modified(mtr); + if (buf_block_t* root = btr_root_block_get(index, RW_SX_LATCH, &mtr)) { + if (fil_page_get_type(root->frame) != FIL_PAGE_INDEX) { + DBUG_ASSERT("wrong page type" == 0); + goto err_exit; + } + + btr_set_instant(root, *index, &mtr); + mtr.commit(); + mtr.start(); + index->set_modified(mtr); + err = row_ins_clust_index_entry_low( + BTR_NO_LOCKING_FLAG, BTR_MODIFY_TREE, index, + index->n_uniq, entry, 0, thr); + } else { +err_exit: + err = DB_CORRUPTION; + } + +func_exit: + mtr.commit(); + + if (err != DB_SUCCESS) { + my_error_innodb(err, table->s->table_name.str, + user_table->flags); + return true; + } + + return false; +} + +/** Adjust the create index column number from "New table" to +"old InnoDB table" while we are doing dropping virtual column. Since we do +not create separate new table for the dropping/adding virtual columns. +To correctly find the indexed column, we will need to find its col_no +in the "Old Table", not the "New table". +@param[in] ha_alter_info Data used during in-place alter +@param[in] old_table MySQL table as it is before the ALTER operation +@param[in] num_v_dropped number of virtual column dropped +@param[in,out] index_def index definition */ +static +void +innodb_v_adjust_idx_col( + const Alter_inplace_info* ha_alter_info, + const TABLE* old_table, + ulint num_v_dropped, + index_def_t* index_def) +{ + for (ulint i = 0; i < index_def->n_fields; i++) { +#ifdef UNIV_DEBUG + bool col_found = false; +#endif /* UNIV_DEBUG */ + ulint num_v = 0; + + index_field_t* index_field = &index_def->fields[i]; + + /* Only adjust virtual column col_no, since non-virtual + column position (in non-vcol list) won't change unless + table rebuild */ + if (!index_field->is_v_col) { + continue; + } + + const Field* field = NULL; + + /* Found the field in the new table */ + for (const Create_field& new_field : + ha_alter_info->alter_info->create_list) { + if (new_field.stored_in_db()) { + continue; + } + + field = new_field.field; + + if (num_v == index_field->col_no) { + break; + } + num_v++; + } + + if (!field) { + /* this means the field is a newly added field, this + should have been blocked when we drop virtual column + at the same time */ + ut_ad(num_v_dropped > 0); + ut_a(0); + } + + ut_ad(!field->stored_in_db()); + + num_v = 0; + + /* Look for its position in old table */ + for (uint old_i = 0; old_table->field[old_i]; old_i++) { + if (old_table->field[old_i] == field) { + /* Found it, adjust its col_no to its position + in old table */ + index_def->fields[i].col_no = num_v; + ut_d(col_found = true); + break; + } + + num_v += !old_table->field[old_i]->stored_in_db(); + } + + ut_ad(col_found); + } +} + +/** Create index metadata in the data dictionary. +@param[in,out] trx dictionary transaction +@param[in,out] index index being created +@param[in] add_v virtual columns that are being added, or NULL +@return the created index */ +MY_ATTRIBUTE((nonnull(1,2), warn_unused_result)) +static +dict_index_t* +create_index_dict( + trx_t* trx, + dict_index_t* index, + const dict_add_v_col_t* add_v) +{ + DBUG_ENTER("create_index_dict"); + + mem_heap_t* heap = mem_heap_create(512); + ind_node_t* node = ind_create_graph_create( + index, index->table->name.m_name, heap, add_v); + que_thr_t* thr = pars_complete_graph_for_exec(node, trx, heap, NULL); + + que_fork_start_command( + static_cast<que_fork_t*>(que_node_get_parent(thr))); + + que_run_threads(thr); + + DBUG_ASSERT(trx->error_state != DB_SUCCESS || index != node->index); + DBUG_ASSERT(trx->error_state != DB_SUCCESS || node->index); + index = node->index; + + que_graph_free((que_t*) que_node_get_parent(thr)); + + DBUG_RETURN(index); +} + +/** Update internal structures with concurrent writes blocked, +while preparing ALTER TABLE. + +@param ha_alter_info Data used during in-place alter +@param altered_table MySQL table that is being altered +@param old_table MySQL table as it is before the ALTER operation +@param table_name Table name in MySQL +@param flags Table and tablespace flags +@param flags2 Additional table flags +@param fts_doc_id_col The column number of FTS_DOC_ID +@param add_fts_doc_id Flag: add column FTS_DOC_ID? +@param add_fts_doc_id_idx Flag: add index FTS_DOC_ID_INDEX (FTS_DOC_ID)? + +@retval true Failure +@retval false Success +*/ +static MY_ATTRIBUTE((warn_unused_result, nonnull(1,2,3,4))) +bool +prepare_inplace_alter_table_dict( +/*=============================*/ + Alter_inplace_info* ha_alter_info, + const TABLE* altered_table, + const TABLE* old_table, + const char* table_name, + ulint flags, + ulint flags2, + ulint fts_doc_id_col, + bool add_fts_doc_id, + bool add_fts_doc_id_idx) +{ + bool dict_locked = false; + ulint* add_key_nums; /* MySQL key numbers */ + index_def_t* index_defs; /* index definitions */ + dict_table_t* user_table; + dict_index_t* fts_index = NULL; + bool new_clustered = false; + dberr_t error; + ulint num_fts_index; + dict_add_v_col_t* add_v = NULL; + ha_innobase_inplace_ctx*ctx; + + DBUG_ENTER("prepare_inplace_alter_table_dict"); + + ctx = static_cast<ha_innobase_inplace_ctx*> + (ha_alter_info->handler_ctx); + + DBUG_ASSERT((ctx->add_autoinc != ULINT_UNDEFINED) + == (ctx->sequence.max_value() > 0)); + DBUG_ASSERT(!ctx->num_to_drop_index == !ctx->drop_index); + DBUG_ASSERT(!ctx->num_to_drop_fk == !ctx->drop_fk); + DBUG_ASSERT(!add_fts_doc_id || add_fts_doc_id_idx); + DBUG_ASSERT(!add_fts_doc_id_idx + || innobase_fulltext_exist(altered_table)); + DBUG_ASSERT(!ctx->defaults); + DBUG_ASSERT(!ctx->add_index); + DBUG_ASSERT(!ctx->add_key_numbers); + DBUG_ASSERT(!ctx->num_to_add_index); + + user_table = ctx->new_table; + + switch (ha_alter_info->inplace_supported) { + default: break; + case HA_ALTER_INPLACE_INSTANT: + case HA_ALTER_INPLACE_NOCOPY_LOCK: + case HA_ALTER_INPLACE_NOCOPY_NO_LOCK: + /* If we promised ALGORITHM=NOCOPY or ALGORITHM=INSTANT, + we must retain the original ROW_FORMAT of the table. */ + flags = (user_table->flags & (DICT_TF_MASK_COMPACT + | DICT_TF_MASK_ATOMIC_BLOBS)) + | (flags & ~(DICT_TF_MASK_COMPACT + | DICT_TF_MASK_ATOMIC_BLOBS)); + } + + trx_start_if_not_started_xa(ctx->prebuilt->trx, true); + + if (ha_alter_info->handler_flags + & ALTER_DROP_VIRTUAL_COLUMN) { + if (prepare_inplace_drop_virtual(ha_alter_info, old_table)) { + DBUG_RETURN(true); + } + } + + if (ha_alter_info->handler_flags + & ALTER_ADD_VIRTUAL_COLUMN) { + if (prepare_inplace_add_virtual( + ha_alter_info, altered_table, old_table)) { + DBUG_RETURN(true); + } + + /* Need information for newly added virtual columns + for create index */ + + if (ha_alter_info->handler_flags + & ALTER_ADD_NON_UNIQUE_NON_PRIM_INDEX) { + for (ulint i = 0; i < ctx->num_to_add_vcol; i++) { + /* Set mbminmax for newly added column */ + dict_col_t& col = ctx->add_vcol[i].m_col; + unsigned mbminlen, mbmaxlen; + dtype_get_mblen(col.mtype, col.prtype, + &mbminlen, &mbmaxlen); + col.mbminlen = mbminlen & 7; + col.mbmaxlen = mbmaxlen & 7; + } + add_v = static_cast<dict_add_v_col_t*>( + mem_heap_alloc(ctx->heap, sizeof *add_v)); + add_v->n_v_col = ctx->num_to_add_vcol; + add_v->v_col = ctx->add_vcol; + add_v->v_col_name = ctx->add_vcol_name; + } + } + + /* There should be no order change for virtual columns coming in + here */ + ut_ad(check_v_col_in_order(old_table, altered_table, ha_alter_info)); + + /* Create table containing all indexes to be built in this + ALTER TABLE ADD INDEX so that they are in the correct order + in the table. */ + + ctx->num_to_add_index = ha_alter_info->index_add_count; + + ut_ad(ctx->prebuilt->trx->mysql_thd != NULL); + const char* path = thd_innodb_tmpdir( + ctx->prebuilt->trx->mysql_thd); + + index_defs = ctx->create_key_defs( + ha_alter_info, altered_table, + num_fts_index, + fts_doc_id_col, add_fts_doc_id, add_fts_doc_id_idx, + old_table); + + new_clustered = (DICT_CLUSTERED & index_defs[0].ind_type) != 0; + + create_table_info_t info(ctx->prebuilt->trx->mysql_thd, altered_table, + ha_alter_info->create_info, NULL, NULL, + srv_file_per_table); + ut_d(bool stats_wait = false); + + /* The primary index would be rebuilt if a FTS Doc ID + column is to be added, and the primary index definition + is just copied from old table and stored in indexdefs[0] */ + DBUG_ASSERT(!add_fts_doc_id || new_clustered); + DBUG_ASSERT(!!new_clustered == + (innobase_need_rebuild(ha_alter_info, old_table) + || add_fts_doc_id)); + + /* Allocate memory for dictionary index definitions */ + + ctx->add_index = static_cast<dict_index_t**>( + mem_heap_zalloc(ctx->heap, ctx->num_to_add_index + * sizeof *ctx->add_index)); + ctx->add_key_numbers = add_key_nums = static_cast<ulint*>( + mem_heap_alloc(ctx->heap, ctx->num_to_add_index + * sizeof *ctx->add_key_numbers)); + + /* Acquire a lock on the table before creating any indexes. */ + + if (ctx->online) { + error = DB_SUCCESS; + } else { + error = row_merge_lock_table( + ctx->prebuilt->trx, ctx->new_table, LOCK_S); + + if (error != DB_SUCCESS) { + + goto error_handling; + } + } + + /* Create a background transaction for the operations on + the data dictionary tables. */ + ctx->trx = innobase_trx_allocate(ctx->prebuilt->trx->mysql_thd); + + trx_start_for_ddl(ctx->trx, TRX_DICT_OP_INDEX); + + /* Latch the InnoDB data dictionary exclusively so that no deadlocks + or lock waits can happen in it during an index create operation. */ + + row_mysql_lock_data_dictionary(ctx->trx); + dict_locked = true; + + /* Wait for background stats processing to stop using the table that + we are going to alter. We know bg stats will not start using it again + until we are holding the data dict locked and we are holding it here + at least until checking ut_ad(user_table->n_ref_count == 1) below. + XXX what may happen if bg stats opens the table after we + have unlocked data dictionary below? */ + dict_stats_wait_bg_to_stop_using_table(user_table, ctx->trx); + ut_d(stats_wait = true); + + online_retry_drop_indexes_low(ctx->new_table, ctx->trx); + + ut_d(dict_table_check_for_dup_indexes( + ctx->new_table, CHECK_ABORTED_OK)); + + DBUG_EXECUTE_IF("innodb_OOM_prepare_inplace_alter", + error = DB_OUT_OF_MEMORY; + goto error_handling;); + + /* If a new clustered index is defined for the table we need + to rebuild the table with a temporary name. */ + + if (new_clustered) { + if (innobase_check_foreigns( + ha_alter_info, old_table, + user_table, ctx->drop_fk, ctx->num_to_drop_fk)) { +new_clustered_failed: + DBUG_ASSERT(ctx->trx != ctx->prebuilt->trx); + ctx->trx->rollback(); + + ut_ad(user_table->get_ref_count() == 1); + + online_retry_drop_indexes_with_trx( + user_table, ctx->trx); + + if (ctx->need_rebuild()) { + if (ctx->new_table) { + ut_ad(!ctx->new_table->cached); + dict_mem_table_free(ctx->new_table); + } + ctx->new_table = ctx->old_table; + } + + while (ctx->num_to_add_index--) { + if (dict_index_t*& i = ctx->add_index[ + ctx->num_to_add_index]) { + dict_mem_index_free(i); + i = NULL; + } + } + + goto err_exit; + } + + size_t prefixlen= strlen(mysql_data_home); + if (mysql_data_home[prefixlen-1] != FN_LIBCHAR) + prefixlen++; + size_t tablen = altered_table->s->path.length - prefixlen; + const char* part = ctx->old_table->name.part(); + size_t partlen = part ? strlen(part) : 0; + char* new_table_name = static_cast<char*>( + mem_heap_alloc(ctx->heap, tablen + partlen + 1)); + memcpy(new_table_name, + altered_table->s->path.str + prefixlen, tablen); +#ifdef _WIN32 + { + char *sep= strchr(new_table_name, FN_LIBCHAR); + sep[0]= '/'; + } +#endif + memcpy(new_table_name + tablen, part ? part : "", partlen + 1); + ulint n_cols = 0; + ulint n_v_cols = 0; + dtuple_t* defaults; + ulint z = 0; + + for (uint i = 0; i < altered_table->s->fields; i++) { + const Field* field = altered_table->field[i]; + + if (!field->stored_in_db()) { + n_v_cols++; + } else { + n_cols++; + } + } + + ut_ad(n_cols + n_v_cols == altered_table->s->fields); + + if (add_fts_doc_id) { + n_cols++; + DBUG_ASSERT(flags2 & DICT_TF2_FTS); + DBUG_ASSERT(add_fts_doc_id_idx); + flags2 |= DICT_TF2_FTS_ADD_DOC_ID + | DICT_TF2_FTS_HAS_DOC_ID + | DICT_TF2_FTS; + } + + DBUG_ASSERT(!add_fts_doc_id_idx || (flags2 & DICT_TF2_FTS)); + + ctx->new_table = dict_mem_table_create( + new_table_name, NULL, n_cols + n_v_cols, n_v_cols, + flags, flags2); + + /* The rebuilt indexed_table will use the renamed + column names. */ + ctx->col_names = NULL; + + if (DICT_TF_HAS_DATA_DIR(flags)) { + ctx->new_table->data_dir_path = + mem_heap_strdup(ctx->new_table->heap, + user_table->data_dir_path); + } + + for (uint i = 0; i < altered_table->s->fields; i++) { + const Field* field = altered_table->field[i]; + unsigned is_unsigned; + auto col_type = get_innobase_type_from_mysql_type( + &is_unsigned, field); + unsigned field_type = field->type() | is_unsigned; + const bool is_virtual = !field->stored_in_db(); + + /* we assume in dtype_form_prtype() that this + fits in two bytes */ + ut_a(field_type <= MAX_CHAR_COLL_NUM); + + if (!field->real_maybe_null()) { + field_type |= DATA_NOT_NULL; + } + + if (field->binary()) { + field_type |= DATA_BINARY_TYPE; + } + + if (altered_table->versioned()) { + if (i == altered_table->s->vers.start_fieldno) { + field_type |= DATA_VERS_START; + } else if (i == + altered_table->s->vers.end_fieldno) { + field_type |= DATA_VERS_END; + } else if (!(field->flags + & VERS_UPDATE_UNVERSIONED_FLAG)) { + field_type |= DATA_VERSIONED; + } + } + + unsigned charset_no; + + if (dtype_is_string_type(col_type)) { + charset_no = field->charset()->number; + + if (charset_no > MAX_CHAR_COLL_NUM) { + my_error(ER_WRONG_KEY_COLUMN, MYF(0), "InnoDB", + field->field_name.str); + goto new_clustered_failed; + } + } else { + charset_no = 0; + } + + auto col_len = field->pack_length(); + + /* The MySQL pack length contains 1 or 2 bytes + length field for a true VARCHAR. Let us + subtract that, so that the InnoDB column + length in the InnoDB data dictionary is the + real maximum byte length of the actual data. */ + + if (field->type() == MYSQL_TYPE_VARCHAR) { + uint32 length_bytes + = static_cast<const Field_varstring*>( + field)->length_bytes; + + col_len -= length_bytes; + + if (length_bytes == 2) { + field_type |= DATA_LONG_TRUE_VARCHAR; + } + + } + + if (dict_col_name_is_reserved(field->field_name.str)) { +wrong_column_name: + dict_mem_table_free(ctx->new_table); + ctx->new_table = ctx->old_table; + my_error(ER_WRONG_COLUMN_NAME, MYF(0), + field->field_name.str); + goto new_clustered_failed; + } + + /** Note the FTS_DOC_ID name is case sensitive due + to internal query parser. + FTS_DOC_ID column must be of BIGINT NOT NULL type + and it should be in all capitalized characters */ + if (!innobase_strcasecmp(field->field_name.str, + FTS_DOC_ID_COL_NAME)) { + if (col_type != DATA_INT + || field->real_maybe_null() + || col_len != sizeof(doc_id_t) + || strcmp(field->field_name.str, + FTS_DOC_ID_COL_NAME)) { + goto wrong_column_name; + } + } + + if (is_virtual) { + dict_mem_table_add_v_col( + ctx->new_table, ctx->heap, + field->field_name.str, + col_type, + dtype_form_prtype( + field_type, charset_no) + | DATA_VIRTUAL, + col_len, i, 0); + } else { + dict_mem_table_add_col( + ctx->new_table, ctx->heap, + field->field_name.str, + col_type, + dtype_form_prtype( + field_type, charset_no), + col_len); + } + } + + if (n_v_cols) { + for (uint i = 0; i < altered_table->s->fields; i++) { + dict_v_col_t* v_col; + const Field* field = altered_table->field[i]; + + if (!!field->stored_in_db()) { + continue; + } + v_col = dict_table_get_nth_v_col( + ctx->new_table, z); + z++; + innodb_base_col_setup( + ctx->new_table, field, v_col); + } + } + + if (add_fts_doc_id) { + fts_add_doc_id_column(ctx->new_table, ctx->heap); + ctx->new_table->fts->doc_col = fts_doc_id_col; + ut_ad(fts_doc_id_col + == altered_table->s->fields - n_v_cols); + } else if (ctx->new_table->fts) { + ctx->new_table->fts->doc_col = fts_doc_id_col; + } + + dict_table_add_system_columns(ctx->new_table, ctx->heap); + + if (ha_alter_info->handler_flags & INNOBASE_DEFAULTS) { + defaults = dtuple_create_with_vcol( + ctx->heap, + dict_table_get_n_cols(ctx->new_table), + dict_table_get_n_v_cols(ctx->new_table)); + + dict_table_copy_types(defaults, ctx->new_table); + } else { + defaults = NULL; + } + + ctx->col_map = innobase_build_col_map( + ha_alter_info, altered_table, old_table, + ctx->new_table, user_table, defaults, ctx->heap); + ctx->defaults = defaults; + } else { + DBUG_ASSERT(!innobase_need_rebuild(ha_alter_info, old_table)); + DBUG_ASSERT(old_table->s->primary_key + == altered_table->s->primary_key); + + for (dict_index_t* index + = dict_table_get_first_index(user_table); + index != NULL; + index = dict_table_get_next_index(index)) { + if (!index->to_be_dropped && index->is_corrupted()) { + my_error(ER_CHECK_NO_SUCH_TABLE, MYF(0)); + goto error_handled; + } + } + + for (dict_index_t* index + = dict_table_get_first_index(user_table); + index != NULL; + index = dict_table_get_next_index(index)) { + if (!index->to_be_dropped && index->is_corrupted()) { + my_error(ER_CHECK_NO_SUCH_TABLE, MYF(0)); + goto error_handled; + } + } + + if (!ctx->new_table->fts + && innobase_fulltext_exist(altered_table)) { + ctx->new_table->fts = fts_create( + ctx->new_table); + ctx->new_table->fts->doc_col = fts_doc_id_col; + } + + /* Check if we need to update mtypes of legacy GIS columns. + This check is only needed when we don't have to rebuild + the table, since rebuild would update all mtypes for GIS + columns */ + error = innobase_check_gis_columns( + ha_alter_info, ctx->new_table, ctx->trx); + if (error != DB_SUCCESS) { + ut_ad(error == DB_ERROR); + error = DB_UNSUPPORTED; + goto error_handling; + } + } + + ut_ad(new_clustered == ctx->need_rebuild()); + + /* Create the index metadata. */ + for (ulint a = 0; a < ctx->num_to_add_index; a++) { + if (index_defs[a].ind_type & DICT_VIRTUAL + && ctx->num_to_drop_vcol > 0 && !new_clustered) { + innodb_v_adjust_idx_col(ha_alter_info, old_table, + ctx->num_to_drop_vcol, + &index_defs[a]); + } + + ctx->add_index[a] = row_merge_create_index( + ctx->new_table, &index_defs[a], add_v); + + add_key_nums[a] = index_defs[a].key_number; + + DBUG_ASSERT(ctx->add_index[a]->is_committed() + == !!new_clustered); + } + + DBUG_ASSERT(!ctx->need_rebuild() + || !ctx->new_table->persistent_autoinc); + + if (ctx->need_rebuild() && instant_alter_column_possible( + *user_table, ha_alter_info, old_table, altered_table, + ha_innobase::is_innodb_strict_mode(ctx->trx->mysql_thd))) { + for (uint a = 0; a < ctx->num_to_add_index; a++) { + ctx->add_index[a]->table = ctx->new_table; + error = dict_index_add_to_cache( + ctx->add_index[a], FIL_NULL, add_v); + ut_a(error == DB_SUCCESS); + } + + DBUG_ASSERT(ha_alter_info->key_count + /* hidden GEN_CLUST_INDEX in InnoDB */ + + dict_index_is_auto_gen_clust( + dict_table_get_first_index(ctx->new_table)) + /* hidden FTS_DOC_ID_INDEX in InnoDB */ + + (ctx->old_table->fts_doc_id_index + && innobase_fts_check_doc_id_index_in_def( + altered_table->s->keys, + altered_table->key_info) + != FTS_EXIST_DOC_ID_INDEX) + == ctx->num_to_add_index); + + ctx->num_to_add_index = 0; + ctx->add_index = NULL; + + uint i = 0; // index of stored columns ctx->new_table->cols[] + Field **af = altered_table->field; + + for (const Create_field& new_field : + ha_alter_info->alter_info->create_list) { + DBUG_ASSERT(!new_field.field + || std::find(old_table->field, + old_table->field + + old_table->s->fields, + new_field.field) != + old_table->field + old_table->s->fields); + DBUG_ASSERT(new_field.field + || !strcmp(new_field.field_name.str, + (*af)->field_name.str)); + + if (!(*af)->stored_in_db()) { + af++; + continue; + } + + dict_col_t* col = dict_table_get_nth_col( + ctx->new_table, i); + DBUG_ASSERT(!strcmp((*af)->field_name.str, + dict_table_get_col_name(ctx->new_table, + i))); + DBUG_ASSERT(!col->is_added()); + + if (new_field.field) { + /* This is a pre-existing column, + possibly at a different position. */ + } else if ((*af)->is_real_null()) { + /* DEFAULT NULL */ + col->def_val.len = UNIV_SQL_NULL; + } else { + switch ((*af)->type()) { + case MYSQL_TYPE_VARCHAR: + col->def_val.len = reinterpret_cast + <const Field_varstring*> + ((*af))->get_length(); + col->def_val.data = reinterpret_cast + <const Field_varstring*> + ((*af))->get_data(); + break; + case MYSQL_TYPE_GEOMETRY: + case MYSQL_TYPE_TINY_BLOB: + case MYSQL_TYPE_MEDIUM_BLOB: + case MYSQL_TYPE_BLOB: + case MYSQL_TYPE_LONG_BLOB: + col->def_val.len = reinterpret_cast + <const Field_blob*> + ((*af))->get_length(); + col->def_val.data = reinterpret_cast + <const Field_blob*> + ((*af))->get_ptr(); + break; + default: + dfield_t d; + dict_col_copy_type(col, &d.type); + ulint len = (*af)->pack_length(); + DBUG_ASSERT(len <= 8 + || d.type.mtype + != DATA_INT); + row_mysql_store_col_in_innobase_format( + &d, + d.type.mtype == DATA_INT + ? static_cast<byte*>( + mem_heap_alloc( + ctx->heap, + len)) + : NULL, + true, (*af)->ptr, len, + dict_table_is_comp( + user_table)); + col->def_val.len = d.len; + col->def_val.data = d.data; + } + } + + i++; + af++; + } + + DBUG_ASSERT(af == altered_table->field + + altered_table->s->fields); + /* There might exist a hidden FTS_DOC_ID column for + FULLTEXT INDEX. If it exists, the columns should have + been implicitly added by ADD FULLTEXT INDEX together + with instant ADD COLUMN. (If a hidden FTS_DOC_ID pre-existed, + then the ctx->col_map[] check should have prevented + adding visible user columns after that.) */ + DBUG_ASSERT(DATA_N_SYS_COLS + i == ctx->new_table->n_cols + || (1 + DATA_N_SYS_COLS + i + == ctx->new_table->n_cols + && !strcmp(dict_table_get_col_name( + ctx->new_table, i), + FTS_DOC_ID_COL_NAME))); + + if (altered_table->found_next_number_field) { + ctx->new_table->persistent_autoinc + = ctx->old_table->persistent_autoinc; + } + + ctx->prepare_instant(); + } + + if (ctx->need_rebuild()) { + DBUG_ASSERT(ctx->need_rebuild()); + DBUG_ASSERT(!ctx->is_instant()); + DBUG_ASSERT(num_fts_index <= 1); + DBUG_ASSERT(!ctx->online || num_fts_index == 0); + DBUG_ASSERT(!ctx->online + || ctx->add_autoinc == ULINT_UNDEFINED); + DBUG_ASSERT(!ctx->online + || !innobase_need_rebuild(ha_alter_info, old_table) + || !innobase_fulltext_exist(altered_table)); + + uint32_t key_id = FIL_DEFAULT_ENCRYPTION_KEY; + fil_encryption_t mode = FIL_ENCRYPTION_DEFAULT; + + if (fil_space_t* s = user_table->space) { + if (const fil_space_crypt_t* c = s->crypt_data) { + key_id = c->key_id; + mode = c->encryption; + } + } + + if (ha_alter_info->handler_flags & ALTER_OPTIONS) { + const ha_table_option_struct& alt_opt= + *ha_alter_info->create_info->option_struct; + const ha_table_option_struct& opt= + *old_table->s->option_struct; + if (alt_opt.encryption != opt.encryption + || alt_opt.encryption_key_id + != opt.encryption_key_id) { + key_id = uint32_t(alt_opt.encryption_key_id); + mode = fil_encryption_t(alt_opt.encryption); + } + } + + if (dict_table_get_low(ctx->new_table->name.m_name)) { + my_error(ER_TABLE_EXISTS_ERROR, MYF(0), + ctx->new_table->name.m_name); + goto new_clustered_failed; + } + + /* Create the table. */ + trx_set_dict_operation(ctx->trx, TRX_DICT_OP_TABLE); + + error = row_create_table_for_mysql( + ctx->new_table, ctx->trx, mode, key_id); + + switch (error) { + dict_table_t* temp_table; + case DB_SUCCESS: + /* We need to bump up the table ref count and + before we can use it we need to open the + table. The new_table must be in the data + dictionary cache, because we are still holding + the dict_sys.mutex. */ + ut_ad(mutex_own(&dict_sys.mutex)); + temp_table = dict_table_open_on_name( + ctx->new_table->name.m_name, TRUE, FALSE, + DICT_ERR_IGNORE_NONE); + ut_a(ctx->new_table == temp_table); + /* n_ref_count must be 1, because purge cannot + be executing on this very table as we are + holding dict_sys.latch X-latch. */ + DBUG_ASSERT(ctx->new_table->get_ref_count() == 1); + DBUG_ASSERT(ctx->new_table->id != 0); + DBUG_ASSERT(ctx->new_table->id == ctx->trx->table_id); + break; + case DB_TABLESPACE_EXISTS: + my_error(ER_TABLESPACE_EXISTS, MYF(0), + altered_table->s->table_name.str); + goto new_table_failed; + case DB_DUPLICATE_KEY: + my_error(HA_ERR_TABLE_EXIST, MYF(0), + altered_table->s->table_name.str); + goto new_table_failed; + case DB_UNSUPPORTED: + my_error(ER_UNSUPPORTED_EXTENSION, MYF(0), + altered_table->s->table_name.str); + goto new_table_failed; + default: + my_error_innodb(error, table_name, flags); +new_table_failed: + DBUG_ASSERT(ctx->trx != ctx->prebuilt->trx); + ctx->new_table = NULL; + goto new_clustered_failed; + } + + for (ulint a = 0; a < ctx->num_to_add_index; a++) { + dict_index_t* index = ctx->add_index[a]; + const ulint n_v_col = index->get_new_n_vcol(); + index = create_index_dict(ctx->trx, index, add_v); + error = ctx->trx->error_state; + if (error != DB_SUCCESS) { + if (index) { + dict_mem_index_free(index); + } +error_handling_drop_uncached_1: + while (++a < ctx->num_to_add_index) { + dict_mem_index_free(ctx->add_index[a]); + } + goto error_handling; + } else { + DBUG_ASSERT(index != ctx->add_index[a]); + } + + ctx->add_index[a] = index; + /* For ALTER TABLE...FORCE or OPTIMIZE TABLE, + we may only issue warnings, because there will + be no schema change from the user perspective. */ + if (!info.row_size_is_acceptable( + *index, + !!(ha_alter_info->handler_flags + & ~(INNOBASE_INPLACE_IGNORE + | INNOBASE_ALTER_NOVALIDATE + | ALTER_RECREATE_TABLE)))) { + error = DB_TOO_BIG_RECORD; + goto error_handling_drop_uncached_1; + } + index->parser = index_defs[a].parser; + if (n_v_col) { + index->assign_new_v_col(n_v_col); + } + /* Note the id of the transaction that created this + index, we use it to restrict readers from accessing + this index, to ensure read consistency. */ + ut_ad(index->trx_id == ctx->trx->id); + + if (index->type & DICT_FTS) { + DBUG_ASSERT(num_fts_index == 1); + DBUG_ASSERT(!fts_index); + DBUG_ASSERT(index->type == DICT_FTS); + fts_index = ctx->add_index[a]; + } + } + + dict_index_t* clust_index = dict_table_get_first_index( + user_table); + dict_index_t* new_clust_index = dict_table_get_first_index( + ctx->new_table); + ut_ad(!new_clust_index->is_instant()); + /* row_merge_build_index() depends on the correct value */ + ut_ad(new_clust_index->n_core_null_bytes + == UT_BITS_IN_BYTES(new_clust_index->n_nullable)); + + if (const Field* ai = altered_table->found_next_number_field) { + const unsigned col_no = innodb_col_no(ai); + + ctx->new_table->persistent_autoinc = + (dict_table_get_nth_col_pos( + ctx->new_table, col_no, NULL) + 1) + & dict_index_t::MAX_N_FIELDS; + + /* Initialize the AUTO_INCREMENT sequence + to the rebuilt table from the old one. */ + if (!old_table->found_next_number_field + || !user_table->space) { + } else if (ib_uint64_t autoinc + = btr_read_autoinc(clust_index)) { + btr_write_autoinc(new_clust_index, autoinc); + } + } + + ctx->skip_pk_sort = innobase_pk_order_preserved( + ctx->col_map, clust_index, new_clust_index); + + DBUG_EXECUTE_IF("innodb_alter_table_pk_assert_no_sort", + DBUG_ASSERT(ctx->skip_pk_sort);); + + if (ctx->online) { + /* Allocate a log for online table rebuild. */ + rw_lock_x_lock(&clust_index->lock); + bool ok = row_log_allocate( + ctx->prebuilt->trx, + clust_index, ctx->new_table, + !(ha_alter_info->handler_flags + & ALTER_ADD_PK_INDEX), + ctx->defaults, ctx->col_map, path, + old_table, + ctx->allow_not_null); + rw_lock_x_unlock(&clust_index->lock); + + if (!ok) { + error = DB_OUT_OF_MEMORY; + goto error_handling; + } + } + } else if (ctx->num_to_add_index) { + ut_ad(!ctx->is_instant()); + ctx->trx->table_id = user_table->id; + + for (ulint a = 0; a < ctx->num_to_add_index; a++) { + dict_index_t* index = ctx->add_index[a]; + const ulint n_v_col = index->get_new_n_vcol(); + DBUG_EXECUTE_IF( + "create_index_metadata_fail", + if (a + 1 == ctx->num_to_add_index) { + ctx->trx->error_state = + DB_OUT_OF_FILE_SPACE; + goto index_created; + }); + index = create_index_dict(ctx->trx, index, add_v); +#ifndef DBUG_OFF +index_created: +#endif + error = ctx->trx->error_state; + if (error != DB_SUCCESS) { + if (index) { + dict_mem_index_free(index); + } +error_handling_drop_uncached: + while (++a < ctx->num_to_add_index) { + dict_mem_index_free(ctx->add_index[a]); + } + goto error_handling; + } else { + DBUG_ASSERT(index != ctx->add_index[a]); + } + ctx->add_index[a]= index; + if (!info.row_size_is_acceptable(*index, true)) { + error = DB_TOO_BIG_RECORD; + goto error_handling_drop_uncached; + } + + index->parser = index_defs[a].parser; + if (n_v_col) { + index->assign_new_v_col(n_v_col); + } + /* Note the id of the transaction that created this + index, we use it to restrict readers from accessing + this index, to ensure read consistency. */ + ut_ad(index->trx_id == ctx->trx->id); + + /* If ADD INDEX with LOCK=NONE has been + requested, allocate a modification log. */ + if (index->type & DICT_FTS) { + DBUG_ASSERT(num_fts_index == 1); + DBUG_ASSERT(!fts_index); + DBUG_ASSERT(index->type == DICT_FTS); + fts_index = ctx->add_index[a]; + /* Fulltext indexes are not covered + by a modification log. */ + } else if (!ctx->online + || !user_table->is_readable() + || !user_table->space) { + /* No need to allocate a modification log. */ + DBUG_ASSERT(!index->online_log); + } else { + rw_lock_x_lock(&ctx->add_index[a]->lock); + + bool ok = row_log_allocate( + ctx->prebuilt->trx, + index, + NULL, true, NULL, NULL, + path, old_table, + ctx->allow_not_null); + + rw_lock_x_unlock(&index->lock); + + DBUG_EXECUTE_IF( + "innodb_OOM_prepare_add_index", + if (ok && a == 1) { + row_log_free( + index->online_log); + index->online_log = NULL; + ok = false; + }); + + if (!ok) { + error = DB_OUT_OF_MEMORY; + goto error_handling_drop_uncached; + } + } + } + } else if (ctx->is_instant() + && !info.row_size_is_acceptable(*user_table, true)) { + error = DB_TOO_BIG_RECORD; + goto error_handling; + } + + if (ctx->online && ctx->num_to_add_index) { + /* Assign a consistent read view for + row_merge_read_clustered_index(). */ + ctx->prebuilt->trx->read_view.open(ctx->prebuilt->trx); + } + + if (fts_index) { + /* Ensure that the dictionary operation mode will + not change while creating the auxiliary tables. */ + trx_dict_op_t op = trx_get_dict_operation(ctx->trx); + +#ifdef UNIV_DEBUG + switch (op) { + case TRX_DICT_OP_NONE: + break; + case TRX_DICT_OP_TABLE: + case TRX_DICT_OP_INDEX: + goto op_ok; + } + ut_error; +op_ok: +#endif /* UNIV_DEBUG */ + ut_ad(ctx->trx->dict_operation_lock_mode == RW_X_LATCH); + ut_d(dict_sys.assert_locked()); + + DICT_TF2_FLAG_SET(ctx->new_table, DICT_TF2_FTS); + if (ctx->need_rebuild()) { + /* For !ctx->need_rebuild(), this will be set at + commit_cache_norebuild(). */ + ctx->new_table->fts_doc_id_index + = dict_table_get_index_on_name( + ctx->new_table, FTS_DOC_ID_INDEX_NAME); + DBUG_ASSERT(ctx->new_table->fts_doc_id_index != NULL); + } + + error = fts_create_index_tables(ctx->trx, fts_index, + ctx->new_table->id); + + DBUG_EXECUTE_IF("innodb_test_fail_after_fts_index_table", + error = DB_LOCK_WAIT_TIMEOUT; + goto error_handling;); + + if (error != DB_SUCCESS) { + goto error_handling; + } + + ctx->trx->commit(); + trx_start_for_ddl(ctx->trx, op); + + if (!ctx->new_table->fts + || ib_vector_size(ctx->new_table->fts->indexes) == 0) { + error = fts_create_common_tables( + ctx->trx, ctx->new_table, true); + + DBUG_EXECUTE_IF( + "innodb_test_fail_after_fts_common_table", + error = DB_LOCK_WAIT_TIMEOUT;); + + if (error != DB_SUCCESS) { + goto error_handling; + } + + ctx->new_table->fts->dict_locked = true; + + error = innobase_fts_load_stopword( + ctx->new_table, ctx->trx, + ctx->prebuilt->trx->mysql_thd) + ? DB_SUCCESS : DB_ERROR; + ctx->new_table->fts->dict_locked = false; + + if (error != DB_SUCCESS) { + goto error_handling; + } + } + + ut_ad(trx_get_dict_operation(ctx->trx) == op); + } + + DBUG_ASSERT(error == DB_SUCCESS); + + /* Commit the data dictionary transaction in order to release + the table locks on the system tables. This means that if + MySQL crashes while creating a new primary key inside + row_merge_build_indexes(), ctx->new_table will not be dropped + by trx_rollback_active(). It will have to be recovered or + dropped by the database administrator. */ + trx_commit_for_mysql(ctx->trx); + + row_mysql_unlock_data_dictionary(ctx->trx); + dict_locked = false; + + ut_ad(!ctx->trx->lock.n_active_thrs); + + if (ctx->old_table->fts) { + fts_sync_during_ddl(ctx->old_table); + } + +error_handling: + /* After an error, remove all those index definitions from the + dictionary which were defined. */ + + switch (error) { + case DB_SUCCESS: + ut_a(!dict_locked); + + ut_d(mutex_enter(&dict_sys.mutex)); + ut_d(dict_table_check_for_dup_indexes( + user_table, CHECK_PARTIAL_OK)); + ut_d(mutex_exit(&dict_sys.mutex)); + DBUG_RETURN(false); + case DB_TABLESPACE_EXISTS: + my_error(ER_TABLESPACE_EXISTS, MYF(0), "(unknown)"); + break; + case DB_DUPLICATE_KEY: + my_error(ER_DUP_KEY, MYF(0), "SYS_INDEXES"); + break; + case DB_UNSUPPORTED: + my_error(ER_TABLE_CANT_HANDLE_SPKEYS, MYF(0), "SYS_COLUMNS"); + break; + default: + my_error_innodb(error, table_name, user_table->flags); + } + +error_handled: + + ctx->prebuilt->trx->error_info = NULL; + + if (!ctx->trx) { + goto err_exit; + } + + ctx->trx->error_state = DB_SUCCESS; + + if (!dict_locked) { + row_mysql_lock_data_dictionary(ctx->trx); + } + + if (new_clustered) { + if (ctx->need_rebuild()) { + + if (DICT_TF2_FLAG_IS_SET( + ctx->new_table, DICT_TF2_FTS)) { + innobase_drop_fts_index_table( + ctx->new_table, ctx->trx); + } + + dict_table_close_and_drop(ctx->trx, ctx->new_table); + + /* Free the log for online table rebuild, if + one was allocated. */ + + dict_index_t* clust_index = dict_table_get_first_index( + user_table); + + rw_lock_x_lock(&clust_index->lock); + + if (clust_index->online_log) { + ut_ad(ctx->online); + row_log_abort_sec(clust_index); + clust_index->online_status + = ONLINE_INDEX_COMPLETE; + } + + rw_lock_x_unlock(&clust_index->lock); + } + + trx_commit_for_mysql(ctx->trx); + /* n_ref_count must be 1, because purge cannot + be executing on this very table as we are + holding dict_sys.latch X-latch. */ + ut_ad(!stats_wait || ctx->online + || user_table->get_ref_count() == 1); + + online_retry_drop_indexes_with_trx(user_table, ctx->trx); + } else { + ut_ad(!ctx->need_rebuild()); + row_merge_drop_indexes(ctx->trx, user_table, true); + trx_commit_for_mysql(ctx->trx); + } + + ut_d(dict_table_check_for_dup_indexes(user_table, CHECK_ALL_COMPLETE)); + ut_ad(!user_table->drop_aborted); + +err_exit: + /* Clear the to_be_dropped flag in the data dictionary cache. */ + for (ulint i = 0; i < ctx->num_to_drop_index; i++) { + DBUG_ASSERT(ctx->drop_index[i]->is_committed()); + DBUG_ASSERT(ctx->drop_index[i]->to_be_dropped); + ctx->drop_index[i]->to_be_dropped = 0; + } + + if (ctx->trx) { + row_mysql_unlock_data_dictionary(ctx->trx); + + ctx->trx->free(); + } + trx_commit_for_mysql(ctx->prebuilt->trx); + + for (uint i = 0; i < ctx->num_to_add_fk; i++) { + if (ctx->add_fk[i]) { + dict_foreign_free(ctx->add_fk[i]); + } + } + + delete ctx; + ha_alter_info->handler_ctx = NULL; + + DBUG_RETURN(true); +} + +/* Check whether an index is needed for the foreign key constraint. +If so, if it is dropped, is there an equivalent index can play its role. +@return true if the index is needed and can't be dropped */ +static MY_ATTRIBUTE((nonnull(1,2,3,5), warn_unused_result)) +bool +innobase_check_foreign_key_index( +/*=============================*/ + Alter_inplace_info* ha_alter_info, /*!< in: Structure describing + changes to be done by ALTER + TABLE */ + dict_index_t* index, /*!< in: index to check */ + dict_table_t* indexed_table, /*!< in: table that owns the + foreign keys */ + const char** col_names, /*!< in: column names, or NULL + for indexed_table->col_names */ + trx_t* trx, /*!< in/out: transaction */ + dict_foreign_t** drop_fk, /*!< in: Foreign key constraints + to drop */ + ulint n_drop_fk) /*!< in: Number of foreign keys + to drop */ +{ + const dict_foreign_set* fks = &indexed_table->referenced_set; + + /* Check for all FK references from other tables to the index. */ + for (dict_foreign_set::const_iterator it = fks->begin(); + it != fks->end(); ++it) { + + dict_foreign_t* foreign = *it; + if (foreign->referenced_index != index) { + continue; + } + ut_ad(indexed_table == foreign->referenced_table); + + if (NULL == dict_foreign_find_index( + indexed_table, col_names, + foreign->referenced_col_names, + foreign->n_fields, index, + /*check_charsets=*/TRUE, + /*check_null=*/FALSE, + NULL, NULL, NULL) + && NULL == innobase_find_equiv_index( + foreign->referenced_col_names, + foreign->n_fields, + ha_alter_info->key_info_buffer, + span<uint>(ha_alter_info->index_add_buffer, + ha_alter_info->index_add_count))) { + + /* Index cannot be dropped. */ + trx->error_info = index; + return(true); + } + } + + fks = &indexed_table->foreign_set; + + /* Check for all FK references in current table using the index. */ + for (dict_foreign_set::const_iterator it = fks->begin(); + it != fks->end(); ++it) { + + dict_foreign_t* foreign = *it; + if (foreign->foreign_index != index) { + continue; + } + + ut_ad(indexed_table == foreign->foreign_table); + + if (!innobase_dropping_foreign( + foreign, drop_fk, n_drop_fk) + && NULL == dict_foreign_find_index( + indexed_table, col_names, + foreign->foreign_col_names, + foreign->n_fields, index, + /*check_charsets=*/TRUE, + /*check_null=*/FALSE, + NULL, NULL, NULL) + && NULL == innobase_find_equiv_index( + foreign->foreign_col_names, + foreign->n_fields, + ha_alter_info->key_info_buffer, + span<uint>(ha_alter_info->index_add_buffer, + ha_alter_info->index_add_count))) { + + /* Index cannot be dropped. */ + trx->error_info = index; + return(true); + } + } + + 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_d(dict_sys.assert_locked()); + 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_d(dict_sys.assert_locked()); + + 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<char*>( + mem_heap_alloc(index->heap, new_name_len + 1)); + } + + memcpy(const_cast<char*>(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); + + std::vector<std::pair<dict_index_t *, const char *>> rename_info; + rename_info.reserve(ha_alter_info->rename_keys.size()); + + 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); + + rename_info.emplace_back(index, pair.new_key->name.str); + } + + for (const auto &pair : rename_info) + innobase_rename_index_cache(pair.first, pair.second); +} + +/** Fill the stored column information in s_cols list. +@param[in] altered_table mysql table object +@param[in] table innodb table object +@param[out] s_cols list of stored column +@param[out] s_heap heap for storing stored +column information. */ +static +void +alter_fill_stored_column( + const TABLE* altered_table, + dict_table_t* table, + dict_s_col_list** s_cols, + mem_heap_t** s_heap) +{ + ulint n_cols = altered_table->s->fields; + ulint stored_col_no = 0; + + for (ulint i = 0; i < n_cols; i++) { + Field* field = altered_table->field[i]; + dict_s_col_t s_col; + + if (field->stored_in_db()) { + stored_col_no++; + } + + if (!innobase_is_s_fld(field)) { + continue; + } + + ulint num_base = 0; + dict_col_t* col = dict_table_get_nth_col(table, + stored_col_no); + + s_col.m_col = col; + s_col.s_pos = i; + + if (*s_cols == NULL) { + *s_cols = UT_NEW_NOKEY(dict_s_col_list()); + *s_heap = mem_heap_create(1000); + } + + if (num_base != 0) { + s_col.base_col = static_cast<dict_col_t**>(mem_heap_zalloc( + *s_heap, num_base * sizeof(dict_col_t*))); + } else { + s_col.base_col = NULL; + } + + s_col.num_base = num_base; + innodb_base_col_setup_for_stored(table, field, &s_col); + (*s_cols)->push_front(s_col); + } +} + +static bool alter_templ_needs_rebuild(const TABLE* altered_table, + const Alter_inplace_info* ha_alter_info, + const dict_table_t* table); + + +/** Allows InnoDB to update internal structures with concurrent +writes blocked (provided that check_if_supported_inplace_alter() +did not return HA_ALTER_INPLACE_NO_LOCK). +This will be invoked before inplace_alter_table(). + +@param altered_table TABLE object for new version of table. +@param ha_alter_info Structure describing changes to be done +by ALTER TABLE and holding data used during in-place alter. + +@retval true Failure +@retval false Success +*/ + +bool +ha_innobase::prepare_inplace_alter_table( +/*=====================================*/ + TABLE* altered_table, + Alter_inplace_info* ha_alter_info) +{ + dict_index_t** drop_index; /*!< Index to be dropped */ + ulint n_drop_index; /*!< Number of indexes to drop */ + 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 */ + ulint n_add_fk; /*!< Number of foreign keys to drop */ + dict_table_t* indexed_table; /*!< Table where indexes are created */ + mem_heap_t* heap; + const char** col_names; + int error; + ulint add_autoinc_col_no = ULINT_UNDEFINED; + ulonglong autoinc_col_max_value = 0; + ulint fts_doc_col_no = ULINT_UNDEFINED; + bool add_fts_doc_id = false; + bool add_fts_doc_id_idx = false; + bool add_fts_idx = false; + dict_s_col_list*s_cols = NULL; + mem_heap_t* s_heap = NULL; + + DBUG_ENTER("prepare_inplace_alter_table"); + DBUG_ASSERT(!ha_alter_info->handler_ctx); + DBUG_ASSERT(ha_alter_info->create_info); + DBUG_ASSERT(!srv_read_only_mode); + + /* Init online ddl status variables */ + onlineddl_rowlog_rows = 0; + onlineddl_rowlog_pct_used = 0; + onlineddl_pct_progress = 0; + + MONITOR_ATOMIC_INC(MONITOR_PENDING_ALTER_TABLE); + +#ifdef UNIV_DEBUG + for (dict_index_t* index = dict_table_get_first_index(m_prebuilt->table); + index; + index = dict_table_get_next_index(index)) { + ut_ad(!index->to_be_dropped); + } +#endif /* UNIV_DEBUG */ + + ut_d(mutex_enter(&dict_sys.mutex)); + ut_d(dict_table_check_for_dup_indexes( + m_prebuilt->table, CHECK_ABORTED_OK)); + ut_d(mutex_exit(&dict_sys.mutex)); + + if (!(ha_alter_info->handler_flags & ~INNOBASE_INPLACE_IGNORE)) { + /* Nothing to do */ + DBUG_ASSERT(m_prebuilt->trx->dict_operation_lock_mode == 0); + DBUG_RETURN(false); + } + + indexed_table = m_prebuilt->table; + + /* ALTER TABLE will not implicitly move a table from a single-table + tablespace to the system tablespace when innodb_file_per_table=OFF. + But it will implicitly move a table from the system tablespace to a + single-table tablespace if innodb_file_per_table = ON. */ + + create_table_info_t info(m_user_thd, + altered_table, + ha_alter_info->create_info, + NULL, + NULL, + srv_file_per_table); + + info.set_tablespace_type(indexed_table->space != fil_system.sys_space); + + if (ha_alter_info->handler_flags & ALTER_ADD_NON_UNIQUE_NON_PRIM_INDEX) { + if (info.gcols_in_fulltext_or_spatial()) { + goto err_exit_no_heap; + } + } + + if (indexed_table->is_readable()) { + } else { + if (indexed_table->corrupted) { + /* Handled below */ + } else { + if (const fil_space_t* space = indexed_table->space) { + String str; + const char* engine= table_type(); + + push_warning_printf( + m_user_thd, + Sql_condition::WARN_LEVEL_WARN, + HA_ERR_DECRYPTION_FAILED, + "Table %s in file %s is encrypted but encryption service or" + " used key_id is not available. " + " Can't continue reading table.", + table_share->table_name.str, + space->chain.start->name); + + my_error(ER_GET_ERRMSG, MYF(0), HA_ERR_DECRYPTION_FAILED, str.c_ptr(), engine); + DBUG_RETURN(true); + } + } + } + + if (indexed_table->corrupted + || dict_table_get_first_index(indexed_table) == NULL + || dict_table_get_first_index(indexed_table)->is_corrupted()) { + /* The clustered index is corrupted. */ + my_error(ER_CHECK_NO_SUCH_TABLE, MYF(0)); + DBUG_RETURN(true); + } else { + const char* invalid_opt = info.create_options_are_invalid(); + + /* Check engine specific table options */ + if (const char* invalid_tbopt = info.check_table_options()) { + my_error(ER_ILLEGAL_HA_CREATE_OPTION, MYF(0), + table_type(), invalid_tbopt); + goto err_exit_no_heap; + } + + if (invalid_opt) { + my_error(ER_ILLEGAL_HA_CREATE_OPTION, MYF(0), + table_type(), invalid_opt); + goto err_exit_no_heap; + } + } + + /* Check if any index name is reserved. */ + if (innobase_index_name_is_reserved( + m_user_thd, + ha_alter_info->key_info_buffer, + ha_alter_info->key_count)) { +err_exit_no_heap: + DBUG_ASSERT(m_prebuilt->trx->dict_operation_lock_mode == 0); + online_retry_drop_indexes(m_prebuilt->table, m_user_thd); + DBUG_RETURN(true); + } + + indexed_table = m_prebuilt->table; + + /* Check that index keys are sensible */ + error = innobase_check_index_keys(ha_alter_info, indexed_table); + + if (error) { + goto err_exit_no_heap; + } + + /* Prohibit renaming a column to something that the table + already contains. */ + if (ha_alter_info->handler_flags + & ALTER_COLUMN_NAME) { + for (Field** fp = table->field; *fp; fp++) { + if (!((*fp)->flags & FIELD_IS_RENAMED)) { + continue; + } + + const char* name = 0; + + for (const Create_field& cf : + ha_alter_info->alter_info->create_list) { + if (cf.field == *fp) { + name = cf.field_name.str; + goto check_if_ok_to_rename; + } + } + + ut_error; +check_if_ok_to_rename: + /* Prohibit renaming a column from FTS_DOC_ID + if full-text indexes exist. */ + if (!my_strcasecmp(system_charset_info, + (*fp)->field_name.str, + FTS_DOC_ID_COL_NAME) + && innobase_fulltext_exist(altered_table)) { + my_error(ER_INNODB_FT_WRONG_DOCID_COLUMN, + MYF(0), name); + goto err_exit_no_heap; + } + + /* Prohibit renaming a column to an internal column. */ + const char* s = m_prebuilt->table->col_names; + unsigned j; + /* Skip user columns. + MySQL should have checked these already. + We want to allow renaming of c1 to c2, c2 to c1. */ + for (j = 0; j < table->s->fields; j++) { + if (table->field[j]->stored_in_db()) { + s += strlen(s) + 1; + } + } + + for (; j < m_prebuilt->table->n_def; j++) { + if (!my_strcasecmp( + system_charset_info, name, s)) { + my_error(ER_WRONG_COLUMN_NAME, MYF(0), + s); + goto err_exit_no_heap; + } + + s += strlen(s) + 1; + } + } + } + + if (!info.innobase_table_flags()) { + goto err_exit_no_heap; + } + + if (info.flags2() & DICT_TF2_USE_FILE_PER_TABLE) { + /* Preserve the DATA DIRECTORY attribute, because it + currently cannot be changed during ALTER TABLE. */ + info.flags_set(m_prebuilt->table->flags + & 1U << DICT_TF_POS_DATA_DIR); + } + + + /* ALGORITHM=INPLACE without rebuild (10.3+ ALGORITHM=NOCOPY) + must use the current ROW_FORMAT of the table. */ + const ulint max_col_len = DICT_MAX_FIELD_LEN_BY_FORMAT_FLAG( + innobase_need_rebuild(ha_alter_info, this->table) + ? info.flags() + : m_prebuilt->table->flags); + + /* Check each index's column length to make sure they do not + exceed limit */ + for (ulint i = 0; i < ha_alter_info->key_count; i++) { + const KEY* key = &ha_alter_info->key_info_buffer[i]; + + if (key->flags & HA_FULLTEXT) { + /* The column length does not matter for + fulltext search indexes. But, UNIQUE + fulltext indexes are not supported. */ + DBUG_ASSERT(!(key->flags & HA_NOSAME)); + DBUG_ASSERT(!(key->flags & HA_KEYFLAG_MASK + & ~(HA_FULLTEXT + | HA_PACK_KEY + | HA_BINARY_PACK_KEY))); + add_fts_idx = true; + continue; + } + + if (too_big_key_part_length(max_col_len, *key)) { + my_error(ER_INDEX_COLUMN_TOO_LONG, MYF(0), + max_col_len); + goto err_exit_no_heap; + } + } + + /* We won't be allowed to add fts index to a table with + fts indexes already but without AUX_HEX_NAME set. + This means the aux tables of the table failed to + rename to hex format but new created aux tables + shall be in hex format, which is contradictory. */ + if (!DICT_TF2_FLAG_IS_SET(indexed_table, DICT_TF2_FTS_AUX_HEX_NAME) + && indexed_table->fts != NULL && add_fts_idx) { + my_error(ER_INNODB_FT_AUX_NOT_HEX_ID, MYF(0)); + goto err_exit_no_heap; + } + + /* Check existing index definitions for too-long column + prefixes as well, in case max_col_len shrunk. */ + for (const dict_index_t* index + = dict_table_get_first_index(indexed_table); + index; + index = dict_table_get_next_index(index)) { + if (index->type & DICT_FTS) { + DBUG_ASSERT(index->type == DICT_FTS + || (index->type & DICT_CORRUPT)); + + /* We need to drop any corrupted fts indexes + before we add a new fts index. */ + if (add_fts_idx && index->type & DICT_CORRUPT) { + ib_errf(m_user_thd, IB_LOG_LEVEL_ERROR, + ER_INNODB_INDEX_CORRUPT, + "Fulltext index '%s' is corrupt. " + "you should drop this index first.", + index->name()); + + goto err_exit_no_heap; + } + + continue; + } + + for (ulint i = 0; i < dict_index_get_n_fields(index); i++) { + const dict_field_t* field + = dict_index_get_nth_field(index, i); + if (field->prefix_len > max_col_len) { + my_error(ER_INDEX_COLUMN_TOO_LONG, MYF(0), + max_col_len); + goto err_exit_no_heap; + } + } + } + + n_drop_index = 0; + n_drop_fk = 0; + + if (ha_alter_info->handler_flags + & (INNOBASE_ALTER_NOREBUILD | INNOBASE_ALTER_REBUILD + | INNOBASE_ALTER_INSTANT)) { + heap = mem_heap_create(1024); + + if (ha_alter_info->handler_flags + & ALTER_COLUMN_NAME) { + col_names = innobase_get_col_names( + ha_alter_info, altered_table, table, + indexed_table, heap); + } else { + col_names = NULL; + } + } else { + heap = NULL; + col_names = NULL; + } + + if (ha_alter_info->handler_flags + & ALTER_DROP_FOREIGN_KEY) { + DBUG_ASSERT(ha_alter_info->alter_info->drop_list.elements > 0); + + drop_fk = static_cast<dict_foreign_t**>( + mem_heap_alloc( + heap, + ha_alter_info->alter_info->drop_list.elements + * sizeof(dict_foreign_t*))); + + for (Alter_drop& drop : ha_alter_info->alter_info->drop_list) { + if (drop.type != Alter_drop::FOREIGN_KEY) { + continue; + } + + dict_foreign_t* foreign; + + for (dict_foreign_set::iterator it + = m_prebuilt->table->foreign_set.begin(); + it != m_prebuilt->table->foreign_set.end(); + ++it) { + + foreign = *it; + const char* fid = strchr(foreign->id, '/'); + + DBUG_ASSERT(fid); + /* If no database/ prefix was present in + the FOREIGN KEY constraint name, compare + to the full constraint name. */ + fid = fid ? fid + 1 : foreign->id; + + if (!my_strcasecmp(system_charset_info, + fid, drop.name)) { + goto found_fk; + } + } + + my_error(ER_CANT_DROP_FIELD_OR_KEY, MYF(0), + drop.type_name(), drop.name); + goto err_exit; +found_fk: + for (ulint i = n_drop_fk; i--; ) { + if (drop_fk[i] == foreign) { + goto dup_fk; + } + } + drop_fk[n_drop_fk++] = foreign; +dup_fk: + continue; + } + + DBUG_ASSERT(n_drop_fk > 0); + + DBUG_ASSERT(n_drop_fk + <= ha_alter_info->alter_info->drop_list.elements); + } else { + drop_fk = NULL; + } + + if (ha_alter_info->index_drop_count) { + dict_index_t* drop_primary = NULL; + + DBUG_ASSERT(ha_alter_info->handler_flags + & (ALTER_DROP_NON_UNIQUE_NON_PRIM_INDEX + | ALTER_DROP_UNIQUE_INDEX + | ALTER_DROP_PK_INDEX)); + /* Check which indexes to drop. */ + drop_index = static_cast<dict_index_t**>( + mem_heap_alloc( + heap, (ha_alter_info->index_drop_count + 1) + * sizeof *drop_index)); + + for (uint i = 0; i < ha_alter_info->index_drop_count; i++) { + const KEY* key + = ha_alter_info->index_drop_buffer[i]; + dict_index_t* index + = dict_table_get_index_on_name( + indexed_table, key->name.str); + + if (!index) { + push_warning_printf( + m_user_thd, + Sql_condition::WARN_LEVEL_WARN, + HA_ERR_WRONG_INDEX, + "InnoDB could not find key" + " with name %s", key->name.str); + } else { + ut_ad(!index->to_be_dropped); + if (!index->is_primary()) { + drop_index[n_drop_index++] = index; + } else { + drop_primary = index; + } + } + } + + /* If all FULLTEXT indexes were removed, drop an + internal FTS_DOC_ID_INDEX as well, unless it exists in + the table. */ + + if (innobase_fulltext_exist(table) + && !innobase_fulltext_exist(altered_table) + && !DICT_TF2_FLAG_IS_SET( + indexed_table, DICT_TF2_FTS_HAS_DOC_ID)) { + dict_index_t* fts_doc_index + = indexed_table->fts_doc_id_index; + ut_ad(fts_doc_index); + + // Add some fault tolerance for non-debug builds. + if (fts_doc_index == NULL) { + goto check_if_can_drop_indexes; + } + + DBUG_ASSERT(!fts_doc_index->to_be_dropped); + + for (uint i = 0; i < table->s->keys; i++) { + if (!my_strcasecmp( + system_charset_info, + FTS_DOC_ID_INDEX_NAME, + table->key_info[i].name.str)) { + /* The index exists in the MySQL + data dictionary. Do not drop it, + even though it is no longer needed + by InnoDB fulltext search. */ + goto check_if_can_drop_indexes; + } + } + + drop_index[n_drop_index++] = fts_doc_index; + } + +check_if_can_drop_indexes: + /* Check if the indexes can be dropped. */ + + /* Prevent a race condition between DROP INDEX and + CREATE TABLE adding FOREIGN KEY constraints. */ + row_mysql_lock_data_dictionary(m_prebuilt->trx); + + if (!n_drop_index) { + drop_index = NULL; + } else { + /* Flag all indexes that are to be dropped. */ + for (ulint i = 0; i < n_drop_index; i++) { + ut_ad(!drop_index[i]->to_be_dropped); + drop_index[i]->to_be_dropped = 1; + } + } + + if (m_prebuilt->trx->check_foreigns) { + for (uint i = 0; i < n_drop_index; i++) { + dict_index_t* index = drop_index[i]; + + if (innobase_check_foreign_key_index( + ha_alter_info, index, + indexed_table, col_names, + m_prebuilt->trx, drop_fk, n_drop_fk)) { + row_mysql_unlock_data_dictionary( + m_prebuilt->trx); + m_prebuilt->trx->error_info = index; + print_error(HA_ERR_DROP_INDEX_FK, + MYF(0)); + goto err_exit; + } + } + + /* If a primary index is dropped, need to check + any depending foreign constraints get affected */ + if (drop_primary + && innobase_check_foreign_key_index( + ha_alter_info, drop_primary, + indexed_table, col_names, + m_prebuilt->trx, drop_fk, n_drop_fk)) { + row_mysql_unlock_data_dictionary(m_prebuilt->trx); + print_error(HA_ERR_DROP_INDEX_FK, MYF(0)); + goto err_exit; + } + } + + row_mysql_unlock_data_dictionary(m_prebuilt->trx); + } else { + drop_index = NULL; + } + + /* Check if any of the existing indexes are marked as corruption + and if they are, refuse adding more indexes. */ + if (ha_alter_info->handler_flags & ALTER_ADD_NON_UNIQUE_NON_PRIM_INDEX) { + for (dict_index_t* index = dict_table_get_first_index(indexed_table); + index != NULL; index = dict_table_get_next_index(index)) { + + if (!index->to_be_dropped && index->is_committed() + && index->is_corrupted()) { + my_error(ER_INDEX_CORRUPT, MYF(0), index->name()); + goto err_exit; + } + } + } + + n_add_fk = 0; + + if (ha_alter_info->handler_flags + & ALTER_ADD_FOREIGN_KEY) { + ut_ad(!m_prebuilt->trx->check_foreigns); + + alter_fill_stored_column(altered_table, m_prebuilt->table, + &s_cols, &s_heap); + + add_fk = static_cast<dict_foreign_t**>( + mem_heap_zalloc( + heap, + ha_alter_info->alter_info->key_list.elements + * sizeof(dict_foreign_t*))); + + if (!innobase_get_foreign_key_info( + ha_alter_info, table_share, + m_prebuilt->table, col_names, + drop_index, n_drop_index, + add_fk, &n_add_fk, m_prebuilt->trx, s_cols)) { +err_exit: + if (n_drop_index) { + row_mysql_lock_data_dictionary(m_prebuilt->trx); + + /* Clear the to_be_dropped flags, which might + have been set at this point. */ + for (ulint i = 0; i < n_drop_index; i++) { + ut_ad(drop_index[i]->is_committed()); + drop_index[i]->to_be_dropped = 0; + } + + row_mysql_unlock_data_dictionary( + m_prebuilt->trx); + } + + if (heap) { + mem_heap_free(heap); + } + + if (s_cols != NULL) { + UT_DELETE(s_cols); + mem_heap_free(s_heap); + } + + goto err_exit_no_heap; + } + + if (s_cols != NULL) { + UT_DELETE(s_cols); + mem_heap_free(s_heap); + } + } + + 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; + + if (!(ha_alter_info->handler_flags & INNOBASE_ALTER_DATA) + || ((ha_alter_info->handler_flags & ~(INNOBASE_INPLACE_IGNORE + | INNOBASE_ALTER_NOCREATE + | INNOBASE_ALTER_INSTANT)) + == ALTER_OPTIONS + && !alter_options_need_rebuild(ha_alter_info, table))) { + + ha_innobase_inplace_ctx *ctx = NULL; + if (heap) { + ctx = new ha_innobase_inplace_ctx( + m_prebuilt, + drop_index, n_drop_index, + drop_fk, n_drop_fk, + add_fk, n_add_fk, + ha_alter_info->online, + heap, indexed_table, + col_names, ULINT_UNDEFINED, 0, 0, + (ha_alter_info->ignore + || !thd_is_strict_mode(m_user_thd)), + alt_opt.page_compressed, + alt_opt.page_compression_level); + ha_alter_info->handler_ctx = ctx; + } + + DBUG_ASSERT(m_prebuilt->trx->dict_operation_lock_mode == 0); + online_retry_drop_indexes(m_prebuilt->table, m_user_thd); + + if ((ha_alter_info->handler_flags + & ALTER_DROP_VIRTUAL_COLUMN) + && prepare_inplace_drop_virtual(ha_alter_info, table)) { + DBUG_RETURN(true); + } + + if ((ha_alter_info->handler_flags + & ALTER_ADD_VIRTUAL_COLUMN) + && prepare_inplace_add_virtual( + ha_alter_info, altered_table, table)) { + DBUG_RETURN(true); + } + + if (!(ha_alter_info->handler_flags & INNOBASE_ALTER_DATA) + && alter_templ_needs_rebuild(altered_table, ha_alter_info, + ctx->new_table) + && ctx->new_table->n_v_cols > 0) { + /* Changing maria record structure may end up here only + if virtual columns were altered. In this case, however, + vc_templ should be rebuilt. Since we don't actually + change any stored data, we can just dispose vc_templ; + it will be recreated on next ha_innobase::open(). */ + + DBUG_ASSERT(ctx->new_table == ctx->old_table); + + dict_free_vc_templ(ctx->new_table->vc_templ); + UT_DELETE(ctx->new_table->vc_templ); + + ctx->new_table->vc_templ = NULL; + } + + DBUG_RETURN(false); + } + + /* If we are to build a full-text search index, check whether + the table already has a DOC ID column. If not, we will need to + add a Doc ID hidden column and rebuild the primary index */ + if (innobase_fulltext_exist(altered_table)) { + ulint doc_col_no; + ulint num_v = 0; + + if (!innobase_fts_check_doc_id_col( + m_prebuilt->table, + altered_table, &fts_doc_col_no, &num_v)) { + + fts_doc_col_no = altered_table->s->fields - num_v; + add_fts_doc_id = true; + add_fts_doc_id_idx = true; + + } else if (fts_doc_col_no == ULINT_UNDEFINED) { + goto err_exit; + } + + switch (innobase_fts_check_doc_id_index( + m_prebuilt->table, altered_table, + &doc_col_no)) { + case FTS_NOT_EXIST_DOC_ID_INDEX: + add_fts_doc_id_idx = true; + break; + case FTS_INCORRECT_DOC_ID_INDEX: + my_error(ER_INNODB_FT_WRONG_DOCID_INDEX, MYF(0), + FTS_DOC_ID_INDEX_NAME); + goto err_exit; + case FTS_EXIST_DOC_ID_INDEX: + DBUG_ASSERT( + doc_col_no == fts_doc_col_no + || doc_col_no == ULINT_UNDEFINED + || (ha_alter_info->handler_flags + & (ALTER_STORED_COLUMN_ORDER + | ALTER_DROP_STORED_COLUMN + | ALTER_ADD_STORED_BASE_COLUMN))); + } + } + + /* See if an AUTO_INCREMENT column was added. */ + uint i = 0; + ulint num_v = 0; + for (const Create_field& new_field : + ha_alter_info->alter_info->create_list) { + const Field* field; + + DBUG_ASSERT(i < altered_table->s->fields); + + for (uint old_i = 0; table->field[old_i]; old_i++) { + if (new_field.field == table->field[old_i]) { + goto found_col; + } + } + + /* This is an added column. */ + DBUG_ASSERT(!new_field.field); + DBUG_ASSERT(ha_alter_info->handler_flags + & ALTER_ADD_COLUMN); + + field = altered_table->field[i]; + + DBUG_ASSERT((MTYP_TYPENR(field->unireg_check) + == Field::NEXT_NUMBER) + == !!(field->flags & AUTO_INCREMENT_FLAG)); + + if (field->flags & AUTO_INCREMENT_FLAG) { + if (add_autoinc_col_no != ULINT_UNDEFINED) { + /* This should have been blocked earlier. */ + ut_ad(0); + my_error(ER_WRONG_AUTO_KEY, MYF(0)); + goto err_exit; + } + + /* Get the col no of the old table non-virtual column array */ + add_autoinc_col_no = i - num_v; + + autoinc_col_max_value = innobase_get_int_col_max_value(field); + } +found_col: + num_v += !new_field.stored_in_db(); + i++; + } + + DBUG_ASSERT(heap); + DBUG_ASSERT(m_user_thd == m_prebuilt->trx->mysql_thd); + DBUG_ASSERT(!ha_alter_info->handler_ctx); + + ha_alter_info->handler_ctx = new ha_innobase_inplace_ctx( + m_prebuilt, + drop_index, n_drop_index, + drop_fk, n_drop_fk, add_fk, n_add_fk, + ha_alter_info->online, + heap, m_prebuilt->table, col_names, + add_autoinc_col_no, + ha_alter_info->create_info->auto_increment_value, + autoinc_col_max_value, + ha_alter_info->ignore || !thd_is_strict_mode(m_user_thd), + alt_opt.page_compressed, alt_opt.page_compression_level); + + DBUG_RETURN(prepare_inplace_alter_table_dict( + ha_alter_info, altered_table, table, + table_share->table_name.str, + info.flags(), info.flags2(), + fts_doc_col_no, add_fts_doc_id, + add_fts_doc_id_idx)); +} + +/* Check whether a columnn length change alter operation requires +to rebuild the template. +@param[in] altered_table TABLE object for new version of table. +@param[in] ha_alter_info Structure describing changes to be done + by ALTER TABLE and holding data used + during in-place alter. +@param[in] table table being altered +@return TRUE if needs rebuild. */ +static +bool +alter_templ_needs_rebuild( + const TABLE* altered_table, + const Alter_inplace_info* ha_alter_info, + const dict_table_t* table) +{ + ulint i = 0; + + for (Field** fp = altered_table->field; *fp; fp++, i++) { + for (const Create_field& cf : + ha_alter_info->alter_info->create_list) { + for (ulint j=0; j < table->n_cols; j++) { + dict_col_t* cols + = dict_table_get_nth_col(table, j); + if (cf.length > cols->len) { + return(true); + } + } + } + } + + return(false); +} + +/** Get the name of an erroneous key. +@param[in] error_key_num InnoDB number of the erroneus key +@param[in] ha_alter_info changes that were being performed +@param[in] table InnoDB table +@return the name of the erroneous key */ +static +const char* +get_error_key_name( + ulint error_key_num, + const Alter_inplace_info* ha_alter_info, + const dict_table_t* table) +{ + if (error_key_num == ULINT_UNDEFINED) { + return(FTS_DOC_ID_INDEX_NAME); + } else if (ha_alter_info->key_count == 0) { + return(dict_table_get_first_index(table)->name); + } else { + return(ha_alter_info->key_info_buffer[error_key_num].name.str); + } +} + +/** Alter the table structure in-place with operations +specified using Alter_inplace_info. +The level of concurrency allowed during this operation depends +on the return value from check_if_supported_inplace_alter(). + +@param altered_table TABLE object for new version of table. +@param ha_alter_info Structure describing changes to be done +by ALTER TABLE and holding data used during in-place alter. + +@retval true Failure +@retval false Success +*/ + +bool +ha_innobase::inplace_alter_table( +/*=============================*/ + TABLE* altered_table, + Alter_inplace_info* ha_alter_info) +{ + dberr_t error; + dict_add_v_col_t* add_v = NULL; + dict_vcol_templ_t* s_templ = NULL; + dict_vcol_templ_t* old_templ = NULL; + struct TABLE* eval_table = altered_table; + bool rebuild_templ = false; + DBUG_ENTER("inplace_alter_table"); + DBUG_ASSERT(!srv_read_only_mode); + ut_ad(!sync_check_iterate(sync_check())); + ut_ad(!rw_lock_own_flagged(&dict_sys.latch, + RW_LOCK_FLAG_X | RW_LOCK_FLAG_S)); + + DEBUG_SYNC(m_user_thd, "innodb_inplace_alter_table_enter"); + + if (!(ha_alter_info->handler_flags & INNOBASE_ALTER_DATA)) { +ok_exit: + DEBUG_SYNC(m_user_thd, "innodb_after_inplace_alter_table"); + DBUG_RETURN(false); + } + + if ((ha_alter_info->handler_flags & ~(INNOBASE_INPLACE_IGNORE + | INNOBASE_ALTER_NOCREATE + | INNOBASE_ALTER_INSTANT)) + == ALTER_OPTIONS + && !alter_options_need_rebuild(ha_alter_info, table)) { + goto ok_exit; + } + + ha_innobase_inplace_ctx* ctx + = static_cast<ha_innobase_inplace_ctx*> + (ha_alter_info->handler_ctx); + + DBUG_ASSERT(ctx); + DBUG_ASSERT(ctx->trx); + DBUG_ASSERT(ctx->prebuilt == m_prebuilt); + + if (ctx->is_instant()) goto ok_exit; + + dict_index_t* pk = dict_table_get_first_index(m_prebuilt->table); + ut_ad(pk != NULL); + + /* For partitioned tables this could be already allocated from a + previous partition invocation. For normal tables this is NULL. */ + UT_DELETE(ctx->m_stage); + + ctx->m_stage = UT_NEW_NOKEY(ut_stage_alter_t(pk)); + + if (!m_prebuilt->table->is_readable()) { + goto all_done; + } + + /* If we are doing a table rebuilding or having added virtual + columns in the same clause, we will need to build a table template + that carries translation information between MySQL TABLE and InnoDB + table, which indicates the virtual columns and their base columns + info. This is used to do the computation callback, so that the + data in base columns can be extracted send to server. + If the Column length changes and it is a part of virtual + index then we need to rebuild the template. */ + rebuild_templ + = ctx->need_rebuild() + || ((ha_alter_info->handler_flags + & ALTER_COLUMN_TYPE_CHANGE_BY_ENGINE) + && alter_templ_needs_rebuild( + altered_table, ha_alter_info, ctx->new_table)); + + if ((ctx->new_table->n_v_cols > 0) && rebuild_templ) { + /* Save the templ if isn't NULL so as to restore the + original state in case of alter operation failures. */ + if (ctx->new_table->vc_templ != NULL && !ctx->need_rebuild()) { + old_templ = ctx->new_table->vc_templ; + } + s_templ = UT_NEW_NOKEY(dict_vcol_templ_t()); + + innobase_build_v_templ( + altered_table, ctx->new_table, s_templ, NULL, false); + + ctx->new_table->vc_templ = s_templ; + } else if (ctx->num_to_add_vcol > 0 && ctx->num_to_drop_vcol == 0) { + /* if there is ongoing drop virtual column, then we disallow + inplace add index on newly added virtual column, so it does + not need to come in here to rebuild template with add_v. + Please also see the assertion in innodb_v_adjust_idx_col() */ + + s_templ = UT_NEW_NOKEY(dict_vcol_templ_t()); + + add_v = static_cast<dict_add_v_col_t*>( + mem_heap_alloc(ctx->heap, sizeof *add_v)); + add_v->n_v_col = ctx->num_to_add_vcol; + add_v->v_col = ctx->add_vcol; + add_v->v_col_name = ctx->add_vcol_name; + + innobase_build_v_templ( + altered_table, ctx->new_table, s_templ, add_v, false); + old_templ = ctx->new_table->vc_templ; + ctx->new_table->vc_templ = s_templ; + } + + /* Drop virtual column without rebuild will keep dict table + unchanged, we use old table to evaluate virtual column value + in innobase_get_computed_value(). */ + if (!ctx->need_rebuild() && ctx->num_to_drop_vcol > 0) { + eval_table = table; + } + + /* Read the clustered index of the table and build + indexes based on this information using temporary + files and merge sort. */ + DBUG_EXECUTE_IF("innodb_OOM_inplace_alter", + error = DB_OUT_OF_MEMORY; goto oom;); + + error = row_merge_build_indexes( + m_prebuilt->trx, + m_prebuilt->table, ctx->new_table, + ctx->online, + ctx->add_index, ctx->add_key_numbers, ctx->num_to_add_index, + altered_table, ctx->defaults, ctx->col_map, + ctx->add_autoinc, ctx->sequence, ctx->skip_pk_sort, + ctx->m_stage, add_v, eval_table, ctx->allow_not_null); + +#ifndef DBUG_OFF +oom: +#endif /* !DBUG_OFF */ + if (error == DB_SUCCESS && ctx->online && ctx->need_rebuild()) { + DEBUG_SYNC_C("row_log_table_apply1_before"); + error = row_log_table_apply( + ctx->thr, m_prebuilt->table, altered_table, + ctx->m_stage, ctx->new_table); + } + + /* Init online ddl status variables */ + onlineddl_rowlog_rows = 0; + onlineddl_rowlog_pct_used = 0; + onlineddl_pct_progress = 0; + + if (s_templ) { + ut_ad(ctx->need_rebuild() || ctx->num_to_add_vcol > 0 + || rebuild_templ); + dict_free_vc_templ(s_templ); + UT_DELETE(s_templ); + + ctx->new_table->vc_templ = old_templ; + } + + DEBUG_SYNC_C("inplace_after_index_build"); + + DBUG_EXECUTE_IF("create_index_fail", + error = DB_DUPLICATE_KEY; + m_prebuilt->trx->error_key_num = ULINT_UNDEFINED;); + + /* After an error, remove all those index definitions + from the dictionary which were defined. */ + + switch (error) { + KEY* dup_key; + all_done: + case DB_SUCCESS: + ut_d(mutex_enter(&dict_sys.mutex)); + ut_d(dict_table_check_for_dup_indexes( + m_prebuilt->table, CHECK_PARTIAL_OK)); + ut_d(mutex_exit(&dict_sys.mutex)); + /* prebuilt->table->n_ref_count can be anything here, + given that we hold at most a shared lock on the table. */ + goto ok_exit; + case DB_DUPLICATE_KEY: + if (m_prebuilt->trx->error_key_num == ULINT_UNDEFINED + || ha_alter_info->key_count == 0) { + /* This should be the hidden index on + FTS_DOC_ID, or there is no PRIMARY KEY in the + table. Either way, we should be seeing and + reporting a bogus duplicate key error. */ + dup_key = NULL; + } else { + DBUG_ASSERT(m_prebuilt->trx->error_key_num + < ha_alter_info->key_count); + dup_key = &ha_alter_info->key_info_buffer[ + m_prebuilt->trx->error_key_num]; + } + print_keydup_error(altered_table, dup_key, MYF(0)); + break; + case DB_ONLINE_LOG_TOO_BIG: + DBUG_ASSERT(ctx->online); + my_error(ER_INNODB_ONLINE_LOG_TOO_BIG, MYF(0), + get_error_key_name(m_prebuilt->trx->error_key_num, + ha_alter_info, m_prebuilt->table)); + break; + case DB_INDEX_CORRUPT: + my_error(ER_INDEX_CORRUPT, MYF(0), + get_error_key_name(m_prebuilt->trx->error_key_num, + ha_alter_info, m_prebuilt->table)); + break; + case DB_DECRYPTION_FAILED: { + String str; + const char* engine= table_type(); + get_error_message(HA_ERR_DECRYPTION_FAILED, &str); + my_error(ER_GET_ERRMSG, MYF(0), HA_ERR_DECRYPTION_FAILED, str.c_ptr(), engine); + break; + } + default: + my_error_innodb(error, + table_share->table_name.str, + m_prebuilt->table->flags); + } + + /* prebuilt->table->n_ref_count can be anything here, given + that we hold at most a shared lock on the table. */ + m_prebuilt->trx->error_info = NULL; + ctx->trx->error_state = DB_SUCCESS; + + DBUG_RETURN(true); +} + +/** Free the modification log for online table rebuild. +@param table table that was being rebuilt online */ +static +void +innobase_online_rebuild_log_free( +/*=============================*/ + dict_table_t* table) +{ + dict_index_t* clust_index = dict_table_get_first_index(table); + ut_d(dict_sys.assert_locked()); + rw_lock_x_lock(&clust_index->lock); + + if (clust_index->online_log) { + ut_ad(dict_index_get_online_status(clust_index) + == ONLINE_INDEX_CREATION); + clust_index->online_status = ONLINE_INDEX_COMPLETE; + row_log_free(clust_index->online_log); + clust_index->online_log = NULL; + DEBUG_SYNC_C("innodb_online_rebuild_log_free_aborted"); + } + + DBUG_ASSERT(dict_index_get_online_status(clust_index) + == ONLINE_INDEX_COMPLETE); + rw_lock_x_unlock(&clust_index->lock); +} + +/** For each user column, which is part of an index which is not going to be +dropped, it checks if the column number of the column is same as col_no +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. */ +static +bool +check_col_exists_in_indexes( + const dict_table_t* table, + ulint col_no, + bool is_v, + bool only_committed = false) +{ + /* This function does not check system columns */ + if (!is_v && dict_table_get_nth_col(table, col_no)->mtype == DATA_SYS) { + return(true); + } + + for (const dict_index_t* index = dict_table_get_first_index(table); + index; + index = dict_table_get_next_index(index)) { + + if (only_committed + ? !index->is_committed() + : index->to_be_dropped) { + continue; + } + + for (ulint i = 0; i < index->n_user_defined_cols; i++) { + const dict_col_t* idx_col + = dict_index_get_nth_col(index, i); + + if (is_v && idx_col->is_virtual()) { + const dict_v_col_t* v_col = reinterpret_cast< + const dict_v_col_t*>(idx_col); + if (v_col->v_pos == col_no) { + return(true); + } + } + + if (!is_v && !idx_col->is_virtual() + && dict_col_get_no(idx_col) == col_no) { + return(true); + } + } + } + + return(false); +} + +/** Rollback a secondary index creation, drop the indexes with +temparary index prefix +@param user_table InnoDB table +@param table the TABLE +@param locked TRUE=table locked, FALSE=may need to do a lazy drop +@param trx the transaction +@param alter_trx transaction which takes S-lock on the table + while creating the index */ +static +void +innobase_rollback_sec_index( + dict_table_t* user_table, + const TABLE* table, + bool locked, + trx_t* trx, + const trx_t* alter_trx=NULL) +{ + row_merge_drop_indexes(trx, user_table, locked, alter_trx); + + /* Free the table->fts only if there is no FTS_DOC_ID + in the table */ + if (user_table->fts + && !DICT_TF2_FLAG_IS_SET(user_table, + DICT_TF2_FTS_HAS_DOC_ID) + && !innobase_fulltext_exist(table)) { + fts_free(user_table); + } +} + +/* Get the number of uncommitted fts index during rollback +operation. +@param[in] table table which undergoes rollback for alter +@return number of uncommitted fts indexes. */ +static +ulint innobase_get_uncommitted_fts_indexes(const dict_table_t* table) +{ + ut_ad(mutex_own(&dict_sys.mutex)); + dict_index_t* index = dict_table_get_first_index(table); + ulint n_uncommitted_fts = 0; + + for (; index ; index = dict_table_get_next_index(index)) + { + if (index->type & DICT_FTS && !index->is_committed()) + n_uncommitted_fts++; + } + + return n_uncommitted_fts; +} + +/** Roll back the changes made during prepare_inplace_alter_table() +and inplace_alter_table() inside the storage engine. Note that the +allowed level of concurrency during this operation will be the same as +for inplace_alter_table() and thus might be higher than during +prepare_inplace_alter_table(). (E.g concurrent writes were blocked +during prepare, but might not be during commit). + +@param ha_alter_info Data used during in-place alter. +@param table the TABLE +@param prebuilt the prebuilt struct +@retval true Failure +@retval false Success +*/ +inline MY_ATTRIBUTE((nonnull, warn_unused_result)) +bool +rollback_inplace_alter_table( +/*=========================*/ + Alter_inplace_info* ha_alter_info, + const TABLE* table, + row_prebuilt_t* prebuilt) +{ + bool fail = false; + + ha_innobase_inplace_ctx* ctx + = static_cast<ha_innobase_inplace_ctx*> + (ha_alter_info->handler_ctx); + + DBUG_ENTER("rollback_inplace_alter_table"); + + if (!ctx || !ctx->trx) { + /* If we have not started a transaction yet, + (almost) nothing has been or needs to be done. */ + goto func_exit; + } + + trx_start_for_ddl(ctx->trx, ctx->need_rebuild() + ? TRX_DICT_OP_TABLE : TRX_DICT_OP_INDEX); + row_mysql_lock_data_dictionary(ctx->trx); + + if (ctx->need_rebuild()) { + /* DML threads can access ctx->new_table via the + online rebuild log. Free it first. */ + innobase_online_rebuild_log_free(prebuilt->table); + } + + if (!ctx->new_table) { + ut_ad(ctx->need_rebuild()); + } else if (ctx->need_rebuild()) { + dberr_t err= DB_SUCCESS; + ulint flags = ctx->new_table->flags; + + /* Since the FTS index specific auxiliary tables has + not yet registered with "table->fts" by fts_add_index(), + we will need explicitly delete them here */ + if (dict_table_has_fts_index(ctx->new_table)) { + + err = innobase_drop_fts_index_table( + ctx->new_table, ctx->trx); + + if (err != DB_SUCCESS) { + my_error_innodb( + err, table->s->table_name.str, + flags); + fail = true; + } + } + + dict_table_close_and_drop(ctx->trx, ctx->new_table); + + switch (err) { + case DB_SUCCESS: + break; + default: + my_error_innodb(err, table->s->table_name.str, + flags); + fail = true; + } + } else { + DBUG_ASSERT(!(ha_alter_info->handler_flags + & ALTER_ADD_PK_INDEX)); + DBUG_ASSERT(ctx->new_table == prebuilt->table); + + /* Remove the fts table from fts_optimize_wq if + there is only one fts index exist. */ + if (prebuilt->table->fts + && innobase_get_uncommitted_fts_indexes( + prebuilt->table) == 1 + && (ib_vector_is_empty(prebuilt->table->fts->indexes) + || ib_vector_size(prebuilt->table->fts->indexes) + == 1)) { + row_mysql_unlock_data_dictionary(ctx->trx); + fts_optimize_remove_table(prebuilt->table); + row_mysql_lock_data_dictionary(ctx->trx); + } + + innobase_rollback_sec_index( + prebuilt->table, table, + (ha_alter_info->alter_info->requested_lock + == Alter_info::ALTER_TABLE_LOCK_EXCLUSIVE), + ctx->trx, prebuilt->trx); + + ctx->clean_new_vcol_index(); + } + + trx_commit_for_mysql(ctx->trx); + row_mysql_unlock_data_dictionary(ctx->trx); + ctx->trx->free(); + ctx->trx = NULL; + +func_exit: +#ifndef DBUG_OFF + dict_index_t* clust_index = dict_table_get_first_index( + prebuilt->table); + DBUG_ASSERT(!clust_index->online_log); + DBUG_ASSERT(dict_index_get_online_status(clust_index) + == ONLINE_INDEX_COMPLETE); +#endif /* !DBUG_OFF */ + + if (ctx) { + DBUG_ASSERT(ctx->prebuilt == prebuilt); + + if (ctx->num_to_add_fk) { + for (ulint i = 0; i < ctx->num_to_add_fk; i++) { + dict_foreign_free(ctx->add_fk[i]); + } + } + + if (ctx->num_to_drop_index) { + row_mysql_lock_data_dictionary(prebuilt->trx); + + /* Clear the to_be_dropped flags + in the data dictionary cache. + The flags may already have been cleared, + in case an error was detected in + commit_inplace_alter_table(). */ + for (ulint i = 0; i < ctx->num_to_drop_index; i++) { + dict_index_t* index = ctx->drop_index[i]; + DBUG_ASSERT(index->is_committed()); + index->to_be_dropped = 0; + } + + row_mysql_unlock_data_dictionary(prebuilt->trx); + } + } + + /* Reset dict_col_t::ord_part for those columns fail to be indexed, + we do this by checking every existing column, if any current + index would index them */ + 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) { + continue; + } + if (!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) { + continue; + } + if (!check_col_exists_in_indexes(prebuilt->table, i, true, + true)) { + col.ord_part = 0; + } + } + + trx_commit_for_mysql(prebuilt->trx); + MONITOR_ATOMIC_DEC(MONITOR_PENDING_ALTER_TABLE); + DBUG_RETURN(fail); +} + +/** Drop a FOREIGN KEY constraint from the data dictionary tables. +@param trx data dictionary transaction +@param table_name Table name in MySQL +@param foreign_id Foreign key constraint identifier +@retval true Failure +@retval false Success */ +static MY_ATTRIBUTE((nonnull, warn_unused_result)) +bool +innobase_drop_foreign_try( +/*======================*/ + trx_t* trx, + const char* table_name, + const char* foreign_id) +{ + DBUG_ENTER("innobase_drop_foreign_try"); + + DBUG_ASSERT(trx_get_dict_operation(trx) == TRX_DICT_OP_INDEX); + ut_ad(trx->dict_operation_lock_mode == RW_X_LATCH); + ut_d(dict_sys.assert_locked()); + + /* Drop the constraint from the data dictionary. */ + static const char sql[] = + "PROCEDURE DROP_FOREIGN_PROC () IS\n" + "BEGIN\n" + "DELETE FROM SYS_FOREIGN WHERE ID=:id;\n" + "DELETE FROM SYS_FOREIGN_COLS WHERE ID=:id;\n" + "END;\n"; + + dberr_t error; + pars_info_t* info; + + info = pars_info_create(); + pars_info_add_str_literal(info, "id", foreign_id); + + trx->op_info = "dropping foreign key constraint from dictionary"; + error = que_eval_sql(info, sql, FALSE, trx); + trx->op_info = ""; + + DBUG_EXECUTE_IF("ib_drop_foreign_error", + error = DB_OUT_OF_FILE_SPACE;); + + if (error != DB_SUCCESS) { + my_error_innodb(error, table_name, 0); + trx->error_state = DB_SUCCESS; + DBUG_RETURN(true); + } + + DBUG_RETURN(false); +} + +/** Rename a column in the data dictionary tables. +@param[in] ctx ALTER TABLE context +@param[in,out] trx Data dictionary transaction +@param[in] table_name Table name in MySQL +@param[in] from old column name +@param[in] to new column name +@retval true Failure +@retval false Success */ +static MY_ATTRIBUTE((nonnull, warn_unused_result)) +bool +innobase_rename_column_try( + const ha_innobase_inplace_ctx& ctx, + trx_t* trx, + const char* table_name, + const char* from, + const char* to) +{ + dberr_t error; + bool clust_has_prefixes = false; + + DBUG_ENTER("innobase_rename_column_try"); + + DBUG_ASSERT(trx_get_dict_operation(trx) == TRX_DICT_OP_INDEX); + ut_ad(trx->dict_operation_lock_mode == RW_X_LATCH); + ut_d(dict_sys.assert_locked()); + + if (ctx.need_rebuild()) { + goto rename_foreign; + } + + error = DB_SUCCESS; + + trx->op_info = "renaming column in SYS_FIELDS"; + + for (const dict_index_t* index = dict_table_get_first_index( + ctx.old_table); + index != NULL; + index = dict_table_get_next_index(index)) { + + bool has_prefixes = false; + for (size_t i = 0; i < dict_index_get_n_fields(index); i++) { + if (dict_index_get_nth_field(index, i)->prefix_len) { + has_prefixes = true; + break; + } + } + + for (ulint i = 0; i < dict_index_get_n_fields(index); i++) { + const dict_field_t& f = index->fields[i]; + DBUG_ASSERT(!f.name == f.col->is_dropped()); + + if (!f.name || my_strcasecmp(system_charset_info, + f.name, from)) { + continue; + } + + pars_info_t* info = pars_info_create(); + ulint pos = has_prefixes ? i << 16 | f.prefix_len : i; + + pars_info_add_ull_literal(info, "indexid", index->id); + pars_info_add_int4_literal(info, "nth", pos); + pars_info_add_str_literal(info, "new", to); + + error = que_eval_sql( + info, + "PROCEDURE RENAME_SYS_FIELDS_PROC () IS\n" + "BEGIN\n" + "UPDATE SYS_FIELDS SET COL_NAME=:new\n" + "WHERE INDEX_ID=:indexid\n" + "AND POS=:nth;\n" + "END;\n", + FALSE, trx); + DBUG_EXECUTE_IF("ib_rename_column_error", + error = DB_OUT_OF_FILE_SPACE;); + + if (error != DB_SUCCESS) { + goto err_exit; + } + + if (!has_prefixes || !clust_has_prefixes + || f.prefix_len) { + continue; + } + + /* For secondary indexes, the + has_prefixes check can be 'polluted' + by PRIMARY KEY column prefix. Try also + the simpler encoding of SYS_FIELDS.POS. */ + info = pars_info_create(); + + pars_info_add_ull_literal(info, "indexid", index->id); + pars_info_add_int4_literal(info, "nth", i); + pars_info_add_str_literal(info, "new", to); + + error = que_eval_sql( + info, + "PROCEDURE RENAME_SYS_FIELDS_PROC () IS\n" + "BEGIN\n" + "UPDATE SYS_FIELDS SET COL_NAME=:new\n" + "WHERE INDEX_ID=:indexid\n" + "AND POS=:nth;\n" + "END;\n", + FALSE, trx); + + if (error != DB_SUCCESS) { + goto err_exit; + } + } + + if (index == dict_table_get_first_index(ctx.old_table)) { + clust_has_prefixes = has_prefixes; + } + } + + if (error != DB_SUCCESS) { +err_exit: + my_error_innodb(error, table_name, 0); + trx->error_state = DB_SUCCESS; + trx->op_info = ""; + DBUG_RETURN(true); + } + +rename_foreign: + trx->op_info = "renaming column in SYS_FOREIGN_COLS"; + + std::set<dict_foreign_t*> fk_evict; + bool foreign_modified; + + for (dict_foreign_set::const_iterator it = ctx.old_table->foreign_set.begin(); + it != ctx.old_table->foreign_set.end(); + ++it) { + + dict_foreign_t* foreign = *it; + foreign_modified = false; + + for (unsigned i = 0; i < foreign->n_fields; i++) { + if (my_strcasecmp(system_charset_info, + foreign->foreign_col_names[i], + from)) { + continue; + } + + /* Ignore the foreign key rename if fk info + is being dropped. */ + if (innobase_dropping_foreign( + foreign, ctx.drop_fk, + ctx.num_to_drop_fk)) { + continue; + } + + pars_info_t* info = pars_info_create(); + + pars_info_add_str_literal(info, "id", foreign->id); + pars_info_add_int4_literal(info, "nth", i); + pars_info_add_str_literal(info, "new", to); + + error = que_eval_sql( + info, + "PROCEDURE RENAME_SYS_FOREIGN_F_PROC () IS\n" + "BEGIN\n" + "UPDATE SYS_FOREIGN_COLS\n" + "SET FOR_COL_NAME=:new\n" + "WHERE ID=:id AND POS=:nth;\n" + "END;\n", + FALSE, trx); + + if (error != DB_SUCCESS) { + goto err_exit; + } + foreign_modified = true; + } + + if (foreign_modified) { + fk_evict.insert(foreign); + } + } + + for (dict_foreign_set::const_iterator it + = ctx.old_table->referenced_set.begin(); + it != ctx.old_table->referenced_set.end(); + ++it) { + + foreign_modified = false; + dict_foreign_t* foreign = *it; + + for (unsigned i = 0; i < foreign->n_fields; i++) { + if (my_strcasecmp(system_charset_info, + foreign->referenced_col_names[i], + from)) { + continue; + } + + pars_info_t* info = pars_info_create(); + + pars_info_add_str_literal(info, "id", foreign->id); + pars_info_add_int4_literal(info, "nth", i); + pars_info_add_str_literal(info, "new", to); + + error = que_eval_sql( + info, + "PROCEDURE RENAME_SYS_FOREIGN_R_PROC () IS\n" + "BEGIN\n" + "UPDATE SYS_FOREIGN_COLS\n" + "SET REF_COL_NAME=:new\n" + "WHERE ID=:id AND POS=:nth;\n" + "END;\n", + FALSE, trx); + + if (error != DB_SUCCESS) { + goto err_exit; + } + foreign_modified = true; + } + + if (foreign_modified) { + fk_evict.insert(foreign); + } + } + + /* Reload the foreign key info for instant table too. */ + if (ctx.need_rebuild() || ctx.is_instant()) { + std::for_each(fk_evict.begin(), fk_evict.end(), + dict_foreign_remove_from_cache); + } + + trx->op_info = ""; + DBUG_RETURN(false); +} + +/** Rename columns in the data dictionary tables. +@param ha_alter_info Data used during in-place alter. +@param ctx In-place ALTER TABLE context +@param table the TABLE +@param trx data dictionary transaction +@param table_name Table name in MySQL +@retval true Failure +@retval false Success */ +static MY_ATTRIBUTE((nonnull, warn_unused_result)) +bool +innobase_rename_columns_try( +/*========================*/ + Alter_inplace_info* ha_alter_info, + ha_innobase_inplace_ctx*ctx, + const TABLE* table, + trx_t* trx, + const char* table_name) +{ + uint i = 0; + ulint num_v = 0; + + DBUG_ASSERT(ctx->need_rebuild()); + DBUG_ASSERT(ha_alter_info->handler_flags + & ALTER_COLUMN_NAME); + + for (Field** fp = table->field; *fp; fp++, i++) { + const bool is_virtual = !(*fp)->stored_in_db(); + if (!((*fp)->flags & FIELD_IS_RENAMED)) { + goto processed_field; + } + + for (const Create_field& cf : + ha_alter_info->alter_info->create_list) { + if (cf.field == *fp) { + if (innobase_rename_column_try( + *ctx, trx, table_name, + cf.field->field_name.str, + cf.field_name.str)) { + return(true); + } + goto processed_field; + } + } + + ut_error; +processed_field: + if (is_virtual) { + num_v++; + } + + continue; + } + + return(false); +} + +/** Convert field type and length to InnoDB format */ +static void get_type(const Field& f, uint& prtype, uint8_t& mtype, + uint16_t& len) +{ + mtype = get_innobase_type_from_mysql_type(&prtype, &f); + len = static_cast<uint16_t>(f.pack_length()); + prtype |= f.type(); + if (f.type() == MYSQL_TYPE_VARCHAR) { + auto l = static_cast<const Field_varstring&>(f).length_bytes; + len = static_cast<uint16_t>(len - l); + if (l == 2) prtype |= DATA_LONG_TRUE_VARCHAR; + } + if (!f.real_maybe_null()) prtype |= DATA_NOT_NULL; + if (f.binary()) prtype |= DATA_BINARY_TYPE; + if (f.table->versioned()) { + if (&f == f.table->field[f.table->s->vers.start_fieldno]) { + prtype |= DATA_VERS_START; + } else if (&f == f.table->field[f.table->s->vers.end_fieldno]) { + prtype |= DATA_VERS_END; + } else if (!(f.flags & VERS_UPDATE_UNVERSIONED_FLAG)) { + prtype |= DATA_VERSIONED; + } + } + if (!f.stored_in_db()) prtype |= DATA_VIRTUAL; + + if (dtype_is_string_type(mtype)) { + prtype |= f.charset()->number << 16; + } +} + +/** Enlarge a column in the data dictionary tables. +@param ctx In-place ALTER TABLE context +@param trx data dictionary transaction +@param table_name Table name in MySQL +@param pos 0-based index to user_table->cols[] or user_table->v_cols[] +@param f new column +@param is_v if it's a virtual column +@retval true Failure +@retval false Success */ +static MY_ATTRIBUTE((nonnull, warn_unused_result)) +bool +innobase_rename_or_enlarge_column_try( + ha_innobase_inplace_ctx*ctx, + trx_t* trx, + const char* table_name, + ulint pos, + const Field& f, + bool is_v) +{ + dict_col_t* col; + dict_table_t* user_table = ctx->old_table; + + DBUG_ENTER("innobase_rename_or_enlarge_column_try"); + DBUG_ASSERT(!ctx->need_rebuild()); + + DBUG_ASSERT(trx_get_dict_operation(trx) == TRX_DICT_OP_INDEX); + ut_ad(trx->dict_operation_lock_mode == RW_X_LATCH); + ut_d(dict_sys.assert_locked()); + + ulint n_base; + + if (is_v) { + dict_v_col_t* v_col= dict_table_get_nth_v_col(user_table, pos); + pos = dict_create_v_col_pos(v_col->v_pos, v_col->m_col.ind); + col = &v_col->m_col; + n_base = v_col->num_base; + } else { + col = dict_table_get_nth_col(user_table, pos); + n_base = 0; + } + + unsigned prtype; + uint8_t mtype; + uint16_t len; + get_type(f, prtype, mtype, len); + DBUG_ASSERT(!dtype_is_string_type(col->mtype) + || col->mbminlen == f.charset()->mbminlen); + DBUG_ASSERT(col->len <= len); + +#ifdef UNIV_DEBUG + ut_ad(col->mbminlen <= col->mbmaxlen); + switch (mtype) { + case DATA_MYSQL: + if (!(prtype & DATA_BINARY_TYPE) || user_table->not_redundant() + || col->mbminlen != col->mbmaxlen) { + /* NOTE: we could allow this when !(prtype & + DATA_BINARY_TYPE) and ROW_FORMAT is not REDUNDANT and + mbminlen<mbmaxlen. That is, we treat a UTF-8 CHAR(n) + column somewhat like a VARCHAR. */ + break; + } + /* fall through */ + case DATA_FIXBINARY: + case DATA_CHAR: + ut_ad(col->len == len); + break; + case DATA_BINARY: + case DATA_VARCHAR: + case DATA_VARMYSQL: + case DATA_DECIMAL: + case DATA_BLOB: + break; + default: + ut_ad(!((col->prtype ^ prtype) & ~DATA_VERSIONED)); + ut_ad(col->mtype == mtype); + ut_ad(col->len == len); + } +#endif /* UNIV_DEBUG */ + + const char* col_name = col->name(*user_table); + const bool same_name = !strcmp(col_name, f.field_name.str); + + if (!same_name + && innobase_rename_column_try(*ctx, trx, table_name, + col_name, f.field_name.str)) { + DBUG_RETURN(true); + } + + if (same_name + && col->prtype == prtype && col->mtype == mtype + && col->len == len) { + DBUG_RETURN(false); + } + + DBUG_RETURN(innodb_insert_sys_columns(user_table->id, pos, + f.field_name.str, + mtype, prtype, len, + n_base, trx, true)); +} + +/** Rename or enlarge columns in the data dictionary cache +as part of commit_try_norebuild(). +@param ha_alter_info Data used during in-place alter. +@param ctx In-place ALTER TABLE context +@param altered_table metadata after ALTER TABLE +@param table metadata before ALTER TABLE +@param trx data dictionary transaction +@param table_name Table name in MySQL +@retval true Failure +@retval false Success */ +static MY_ATTRIBUTE((nonnull, warn_unused_result)) +bool +innobase_rename_or_enlarge_columns_try( + Alter_inplace_info* ha_alter_info, + ha_innobase_inplace_ctx*ctx, + const TABLE* altered_table, + const TABLE* table, + trx_t* trx, + const char* table_name) +{ + DBUG_ENTER("innobase_rename_or_enlarge_columns_try"); + + if (!(ha_alter_info->handler_flags + & (ALTER_COLUMN_TYPE_CHANGE_BY_ENGINE + | ALTER_COLUMN_NAME))) { + DBUG_RETURN(false); + } + + ulint i = 0; + ulint num_v = 0; + + for (Field** fp = table->field; *fp; fp++, i++) { + const bool is_v = !(*fp)->stored_in_db(); + ulint idx = is_v ? num_v++ : i - num_v; + + Field** af = altered_table->field; + for (const Create_field& cf : + ha_alter_info->alter_info->create_list) { + if (cf.field == *fp) { + if (innobase_rename_or_enlarge_column_try( + ctx, trx, table_name, + idx, **af, is_v)) { + DBUG_RETURN(true); + } + break; + } + af++; + } + } + + DBUG_RETURN(false); +} + +/** Rename or enlarge columns in the data dictionary cache +as part of commit_cache_norebuild(). +@param ha_alter_info Data used during in-place alter. +@param altered_table metadata after ALTER TABLE +@param table metadata before ALTER TABLE +@param user_table InnoDB table that was being altered */ +static MY_ATTRIBUTE((nonnull)) +void +innobase_rename_or_enlarge_columns_cache( +/*=====================================*/ + Alter_inplace_info* ha_alter_info, + const TABLE* altered_table, + const TABLE* table, + dict_table_t* user_table) +{ + if (!(ha_alter_info->handler_flags + & (ALTER_COLUMN_TYPE_CHANGE_BY_ENGINE + | ALTER_COLUMN_NAME))) { + return; + } + + uint i = 0; + ulint num_v = 0; + + for (Field** fp = table->field; *fp; fp++, i++) { + const bool is_virtual = !(*fp)->stored_in_db(); + + Field** af = altered_table->field; + for (Create_field& cf : + ha_alter_info->alter_info->create_list) { + if (cf.field != *fp) { + af++; + continue; + } + + ulint col_n = is_virtual ? num_v : i - num_v; + dict_col_t *col = is_virtual + ? &dict_table_get_nth_v_col(user_table, col_n) + ->m_col + : dict_table_get_nth_col(user_table, col_n); + const bool is_string= dtype_is_string_type(col->mtype); + DBUG_ASSERT(col->mbminlen + == (is_string + ? (*af)->charset()->mbminlen : 0)); + unsigned prtype; + uint8_t mtype; + uint16_t len; + get_type(**af, prtype, mtype, len); + DBUG_ASSERT(is_string == dtype_is_string_type(mtype)); + + col->prtype = prtype; + col->mtype = mtype; + col->len = len; + col->mbmaxlen = is_string + ? (*af)->charset()->mbmaxlen & 7: 0; + + if ((*fp)->flags & FIELD_IS_RENAMED) { + dict_mem_table_col_rename( + user_table, col_n, + cf.field->field_name.str, + (*af)->field_name.str, is_virtual); + } + + break; + } + + if (is_virtual) { + num_v++; + } + } +} + +/** Set the auto-increment value of the table on commit. +@param ha_alter_info Data used during in-place alter +@param ctx In-place ALTER TABLE context +@param altered_table MySQL table that is being altered +@param old_table MySQL table as it is before the ALTER operation +@return whether the operation failed (and my_error() was called) */ +static MY_ATTRIBUTE((nonnull)) +bool +commit_set_autoinc( + Alter_inplace_info* ha_alter_info, + ha_innobase_inplace_ctx*ctx, + const TABLE* altered_table, + const TABLE* old_table) +{ + DBUG_ENTER("commit_set_autoinc"); + + if (!altered_table->found_next_number_field) { + /* There is no AUTO_INCREMENT column in the table + after the ALTER operation. */ + } else if (ctx->add_autoinc != ULINT_UNDEFINED) { + ut_ad(ctx->need_rebuild()); + /* An AUTO_INCREMENT column was added. Get the last + value from the sequence, which may be based on a + supplied AUTO_INCREMENT value. */ + ib_uint64_t autoinc = ctx->sequence.last(); + ctx->new_table->autoinc = autoinc; + /* Bulk index creation does not update + PAGE_ROOT_AUTO_INC, so we must persist the "last used" + value here. */ + btr_write_autoinc(dict_table_get_first_index(ctx->new_table), + autoinc - 1, true); + } else if ((ha_alter_info->handler_flags + & ALTER_CHANGE_CREATE_OPTION) + && (ha_alter_info->create_info->used_fields + & HA_CREATE_USED_AUTO)) { + + if (!ctx->old_table->space) { + my_error(ER_TABLESPACE_DISCARDED, MYF(0), + old_table->s->table_name.str); + DBUG_RETURN(true); + } + + /* An AUTO_INCREMENT value was supplied by the user. + It must be persisted to the data file. */ + const Field* ai = old_table->found_next_number_field; + ut_ad(!strcmp(dict_table_get_col_name(ctx->old_table, + innodb_col_no(ai)), + ai->field_name.str)); + + ib_uint64_t autoinc + = ha_alter_info->create_info->auto_increment_value; + if (autoinc == 0) { + autoinc = 1; + } + + if (autoinc >= ctx->old_table->autoinc) { + /* Persist the predecessor of the + AUTO_INCREMENT value as the last used one. */ + ctx->new_table->autoinc = autoinc--; + } else { + /* Mimic ALGORITHM=COPY in the following scenario: + + CREATE TABLE t (a SERIAL); + INSERT INTO t SET a=100; + ALTER TABLE t AUTO_INCREMENT = 1; + INSERT INTO t SET a=NULL; + SELECT * FROM t; + + By default, ALGORITHM=INPLACE would reset the + sequence to 1, while after ALGORITHM=COPY, the + last INSERT would use a value larger than 100. + + We could only search the tree to know current + max counter in the table and compare. */ + const dict_col_t* autoinc_col + = dict_table_get_nth_col(ctx->old_table, + innodb_col_no(ai)); + dict_index_t* index + = dict_table_get_first_index(ctx->old_table); + while (index != NULL + && index->fields[0].col != autoinc_col) { + index = dict_table_get_next_index(index); + } + + ut_ad(index); + + ib_uint64_t max_in_table = index + ? row_search_max_autoinc(index) + : 0; + + if (autoinc <= max_in_table) { + ctx->new_table->autoinc = innobase_next_autoinc( + max_in_table, 1, + ctx->prebuilt->autoinc_increment, + ctx->prebuilt->autoinc_offset, + innobase_get_int_col_max_value(ai)); + /* Persist the maximum value as the + last used one. */ + autoinc = max_in_table; + } else { + /* Persist the predecessor of the + AUTO_INCREMENT value as the last used one. */ + ctx->new_table->autoinc = autoinc--; + } + } + + btr_write_autoinc(dict_table_get_first_index(ctx->new_table), + autoinc, true); + } else if (ctx->need_rebuild()) { + /* No AUTO_INCREMENT value was specified. + Copy it from the old table. */ + ctx->new_table->autoinc = ctx->old_table->autoinc; + /* The persistent value was already copied in + prepare_inplace_alter_table_dict() when ctx->new_table + was created. If this was a LOCK=NONE operation, the + AUTO_INCREMENT values would be updated during + row_log_table_apply(). If this was LOCK!=NONE, + the table contents could not possibly have changed + between prepare_inplace and commit_inplace. */ + } + + DBUG_RETURN(false); +} + +/** Add or drop foreign key constraints to the data dictionary tables, +but do not touch the data dictionary cache. +@param ha_alter_info Data used during in-place alter +@param ctx In-place ALTER TABLE context +@param trx Data dictionary transaction +@param table_name Table name in MySQL +@retval true Failure +@retval false Success +*/ +static MY_ATTRIBUTE((nonnull, warn_unused_result)) +bool +innobase_update_foreign_try( +/*========================*/ + ha_innobase_inplace_ctx*ctx, + trx_t* trx, + const char* table_name) +{ + ulint foreign_id; + ulint i; + + DBUG_ENTER("innobase_update_foreign_try"); + + foreign_id = dict_table_get_highest_foreign_id(ctx->new_table); + + foreign_id++; + + for (i = 0; i < ctx->num_to_add_fk; i++) { + dict_foreign_t* fk = ctx->add_fk[i]; + + ut_ad(fk->foreign_table == ctx->new_table + || fk->foreign_table == ctx->old_table); + + dberr_t error = dict_create_add_foreign_id( + &foreign_id, ctx->old_table->name.m_name, fk); + + if (error != DB_SUCCESS) { + my_error(ER_TOO_LONG_IDENT, MYF(0), + fk->id); + DBUG_RETURN(true); + } + + if (!fk->foreign_index) { + fk->foreign_index = dict_foreign_find_index( + ctx->new_table, ctx->col_names, + fk->foreign_col_names, + fk->n_fields, fk->referenced_index, TRUE, + fk->type + & (DICT_FOREIGN_ON_DELETE_SET_NULL + | DICT_FOREIGN_ON_UPDATE_SET_NULL), + NULL, NULL, NULL); + if (!fk->foreign_index) { + my_error(ER_FK_INCORRECT_OPTION, + MYF(0), table_name, fk->id); + DBUG_RETURN(true); + } + } + + /* The fk->foreign_col_names[] uses renamed column + names, while the columns in ctx->old_table have not + been renamed yet. */ + error = dict_create_add_foreign_to_dictionary( + ctx->old_table->name.m_name, fk, trx); + + DBUG_EXECUTE_IF( + "innodb_test_cannot_add_fk_system", + error = DB_ERROR;); + + if (error != DB_SUCCESS) { + my_error(ER_FK_FAIL_ADD_SYSTEM, MYF(0), + fk->id); + DBUG_RETURN(true); + } + } + + for (i = 0; i < ctx->num_to_drop_fk; i++) { + dict_foreign_t* fk = ctx->drop_fk[i]; + + DBUG_ASSERT(fk->foreign_table == ctx->old_table); + + if (innobase_drop_foreign_try(trx, table_name, fk->id)) { + DBUG_RETURN(true); + } + } + + DBUG_RETURN(false); +} + +/** Update the foreign key constraint definitions in the data dictionary cache +after the changes to data dictionary tables were committed. +@param ctx In-place ALTER TABLE context +@param user_thd MySQL connection +@return InnoDB error code (should always be DB_SUCCESS) */ +static MY_ATTRIBUTE((nonnull, warn_unused_result)) +dberr_t +innobase_update_foreign_cache( +/*==========================*/ + ha_innobase_inplace_ctx* ctx, + THD* user_thd) +{ + dict_table_t* user_table; + dberr_t err = DB_SUCCESS; + + DBUG_ENTER("innobase_update_foreign_cache"); + + ut_ad(mutex_own(&dict_sys.mutex)); + + user_table = ctx->old_table; + + /* Discard the added foreign keys, because we will + load them from the data dictionary. */ + for (ulint i = 0; i < ctx->num_to_add_fk; i++) { + dict_foreign_t* fk = ctx->add_fk[i]; + dict_foreign_free(fk); + } + + if (ctx->need_rebuild()) { + /* The rebuilt table is already using the renamed + column names. No need to pass col_names or to drop + constraints from the data dictionary cache. */ + DBUG_ASSERT(!ctx->col_names); + DBUG_ASSERT(user_table->foreign_set.empty()); + DBUG_ASSERT(user_table->referenced_set.empty()); + user_table = ctx->new_table; + } else { + /* Drop the foreign key constraints if the + table was not rebuilt. If the table is rebuilt, + there would not be any foreign key contraints for + it yet in the data dictionary cache. */ + for (ulint i = 0; i < ctx->num_to_drop_fk; i++) { + dict_foreign_t* fk = ctx->drop_fk[i]; + dict_foreign_remove_from_cache(fk); + } + } + + /* Load the old or added foreign keys from the data dictionary + and prevent the table from being evicted from the data + dictionary cache (work around the lack of WL#6049). */ + dict_names_t fk_tables; + + err = dict_load_foreigns(user_table->name.m_name, + ctx->col_names, false, true, + DICT_ERR_IGNORE_NONE, + fk_tables); + + if (err == DB_CANNOT_ADD_CONSTRAINT) { + fk_tables.clear(); + + /* It is possible there are existing foreign key are + loaded with "foreign_key checks" off, + so let's retry the loading with charset_check is off */ + err = dict_load_foreigns(user_table->name.m_name, + ctx->col_names, false, false, + DICT_ERR_IGNORE_NONE, + fk_tables); + + /* The load with "charset_check" off is successful, warn + the user that the foreign key has loaded with mis-matched + charset */ + if (err == DB_SUCCESS) { + push_warning_printf( + user_thd, + Sql_condition::WARN_LEVEL_WARN, + ER_ALTER_INFO, + "Foreign key constraints for table '%s'" + " are loaded with charset check off", + user_table->name.m_name); + } + } + + /* For complete loading of foreign keys, all associated tables must + also be loaded. */ + while (err == DB_SUCCESS && !fk_tables.empty()) { + dict_table_t* table = dict_load_table( + fk_tables.front(), DICT_ERR_IGNORE_NONE); + + if (table == NULL) { + err = DB_TABLE_NOT_FOUND; + ib::error() + << "Failed to load table '" + << table_name_t(const_cast<char*> + (fk_tables.front())) + << "' which has a foreign key constraint with" + << " table '" << user_table->name << "'."; + break; + } + + fk_tables.pop_front(); + } + + DBUG_RETURN(err); +} + +/** Changes SYS_COLUMNS.PRTYPE for one column. +@param[in,out] trx transaction +@param[in] table_name table name +@param[in] tableid table ID as in SYS_TABLES +@param[in] pos column position +@param[in] prtype new precise type +@return boolean flag +@retval true on failure +@retval false on success */ +static +bool +vers_change_field_try( + trx_t* trx, + const char* table_name, + const table_id_t tableid, + const ulint pos, + const ulint prtype) +{ + DBUG_ENTER("vers_change_field_try"); + + pars_info_t* info = pars_info_create(); + + pars_info_add_int4_literal(info, "prtype", prtype); + pars_info_add_ull_literal(info,"tableid", tableid); + pars_info_add_int4_literal(info, "pos", pos); + + dberr_t error = que_eval_sql(info, + "PROCEDURE CHANGE_COLUMN_MTYPE () IS\n" + "BEGIN\n" + "UPDATE SYS_COLUMNS SET PRTYPE=:prtype\n" + "WHERE TABLE_ID=:tableid AND POS=:pos;\n" + "END;\n", + false, trx); + + if (error != DB_SUCCESS) { + my_error_innodb(error, table_name, 0); + trx->error_state = DB_SUCCESS; + trx->op_info = ""; + DBUG_RETURN(true); + } + + DBUG_RETURN(false); +} + +/** Changes fields WITH/WITHOUT SYSTEM VERSIONING property in SYS_COLUMNS. +@param[in] ha_alter_info alter info +@param[in] ctx alter inplace context +@param[in] trx transaction +@param[in] table old table +@return boolean flag +@retval true on failure +@retval false on success */ +static +bool +vers_change_fields_try( + const Alter_inplace_info* ha_alter_info, + const ha_innobase_inplace_ctx* ctx, + trx_t* trx, + const TABLE* table) +{ + DBUG_ENTER("vers_change_fields_try"); + + DBUG_ASSERT(ha_alter_info); + DBUG_ASSERT(ctx); + + for (const Create_field& create_field : ha_alter_info->alter_info->create_list) { + if (!create_field.field) { + continue; + } + if (create_field.versioning + == Column_definition::VERSIONING_NOT_SET) { + continue; + } + + const dict_table_t* new_table = ctx->new_table; + const uint pos = innodb_col_no(create_field.field); + const dict_col_t* col = dict_table_get_nth_col(new_table, pos); + + DBUG_ASSERT(!col->vers_sys_start()); + DBUG_ASSERT(!col->vers_sys_end()); + + ulint new_prtype + = create_field.versioning + == Column_definition::WITHOUT_VERSIONING + ? col->prtype & ~DATA_VERSIONED + : col->prtype | DATA_VERSIONED; + + if (vers_change_field_try(trx, table->s->table_name.str, + new_table->id, pos, + new_prtype)) { + DBUG_RETURN(true); + } + } + + DBUG_RETURN(false); +} + +/** Changes WITH/WITHOUT SYSTEM VERSIONING for fields +in the data dictionary cache. +@param ha_alter_info Data used during in-place alter +@param ctx In-place ALTER TABLE context +@param table MySQL table as it is before the ALTER operation */ +static +void +vers_change_fields_cache( + Alter_inplace_info* ha_alter_info, + const ha_innobase_inplace_ctx* ctx, + const TABLE* table) +{ + DBUG_ENTER("vers_change_fields_cache"); + + DBUG_ASSERT(ha_alter_info); + DBUG_ASSERT(ctx); + DBUG_ASSERT(ha_alter_info->handler_flags & ALTER_COLUMN_UNVERSIONED); + + for (const Create_field& create_field : + ha_alter_info->alter_info->create_list) { + if (!create_field.field || create_field.field->vcol_info) { + continue; + } + dict_col_t* col = dict_table_get_nth_col( + ctx->new_table, innodb_col_no(create_field.field)); + + if (create_field.versioning + == Column_definition::WITHOUT_VERSIONING) { + + DBUG_ASSERT(!col->vers_sys_start()); + DBUG_ASSERT(!col->vers_sys_end()); + col->prtype &= ~DATA_VERSIONED; + } else if (create_field.versioning + == Column_definition::WITH_VERSIONING) { + + DBUG_ASSERT(!col->vers_sys_start()); + DBUG_ASSERT(!col->vers_sys_end()); + col->prtype |= DATA_VERSIONED; + } + } + + DBUG_VOID_RETURN; +} + +/** Commit the changes made during prepare_inplace_alter_table() +and inplace_alter_table() inside the data dictionary tables, +when rebuilding the table. +@param ha_alter_info Data used during in-place alter +@param ctx In-place ALTER TABLE context +@param altered_table MySQL table that is being altered +@param old_table MySQL table as it is before the ALTER operation +@param trx Data dictionary transaction +@param table_name Table name in MySQL +@retval true Failure +@retval false Success +*/ +inline MY_ATTRIBUTE((nonnull, warn_unused_result)) +bool +commit_try_rebuild( +/*===============*/ + Alter_inplace_info* ha_alter_info, + ha_innobase_inplace_ctx*ctx, + TABLE* altered_table, + const TABLE* old_table, + trx_t* trx, + const char* table_name) +{ + dict_table_t* rebuilt_table = ctx->new_table; + dict_table_t* user_table = ctx->old_table; + + DBUG_ENTER("commit_try_rebuild"); + DBUG_ASSERT(ctx->need_rebuild()); + DBUG_ASSERT(trx->dict_operation_lock_mode == RW_X_LATCH); + DBUG_ASSERT(!(ha_alter_info->handler_flags + & ALTER_DROP_FOREIGN_KEY) + || ctx->num_to_drop_fk > 0); + DBUG_ASSERT(ctx->num_to_drop_fk + <= ha_alter_info->alter_info->drop_list.elements); + + for (dict_index_t* index = dict_table_get_first_index(rebuilt_table); + index; + index = dict_table_get_next_index(index)) { + DBUG_ASSERT(dict_index_get_online_status(index) + == ONLINE_INDEX_COMPLETE); + DBUG_ASSERT(index->is_committed()); + if (index->is_corrupted()) { + my_error(ER_INDEX_CORRUPT, MYF(0), index->name()); + DBUG_RETURN(true); + } + } + + if (innobase_update_foreign_try(ctx, trx, table_name)) { + DBUG_RETURN(true); + } + + dberr_t error; + + /* Clear the to_be_dropped flag in the data dictionary cache + of user_table. */ + for (ulint i = 0; i < ctx->num_to_drop_index; i++) { + dict_index_t* index = ctx->drop_index[i]; + DBUG_ASSERT(index->table == user_table); + DBUG_ASSERT(index->is_committed()); + DBUG_ASSERT(index->to_be_dropped); + index->to_be_dropped = 0; + } + + if ((ha_alter_info->handler_flags + & ALTER_COLUMN_NAME) + && innobase_rename_columns_try(ha_alter_info, ctx, old_table, + trx, table_name)) { + DBUG_RETURN(true); + } + + DBUG_EXECUTE_IF("ib_ddl_crash_before_rename", DBUG_SUICIDE();); + + /* The new table must inherit the flag from the + "parent" table. */ + if (!user_table->space) { + rebuilt_table->file_unreadable = true; + rebuilt_table->flags2 |= DICT_TF2_DISCARDED; + } + + /* We can now rename the old table as a temporary table, + rename the new temporary table as the old table and drop the + old table. */ + char* old_name= mem_heap_strdup(ctx->heap, user_table->name.m_name); + + error = row_rename_table_for_mysql(user_table->name.m_name, + ctx->tmp_name, trx, false, false); + if (error == DB_SUCCESS) { + error = row_rename_table_for_mysql(rebuilt_table->name.m_name, + old_name, trx, + false, false); + } + + /* We must be still holding a table handle. */ + DBUG_ASSERT(user_table->get_ref_count() == 1); + + DBUG_EXECUTE_IF("ib_ddl_crash_after_rename", DBUG_SUICIDE();); + DBUG_EXECUTE_IF("ib_rebuild_cannot_rename", error = DB_ERROR;); + + switch (error) { + case DB_SUCCESS: + DBUG_RETURN(false); + case DB_TABLESPACE_EXISTS: + ut_a(rebuilt_table->get_ref_count() == 1); + my_error(ER_TABLESPACE_EXISTS, MYF(0), ctx->tmp_name); + DBUG_RETURN(true); + case DB_DUPLICATE_KEY: + ut_a(rebuilt_table->get_ref_count() == 1); + my_error(ER_TABLE_EXISTS_ERROR, MYF(0), ctx->tmp_name); + DBUG_RETURN(true); + default: + my_error_innodb(error, table_name, user_table->flags); + DBUG_RETURN(true); + } +} + +/** 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; +} + +/** Set of column numbers */ +typedef std::set<ulint, std::less<ulint>, ut_allocator<ulint> > col_set; + +/** Collect (not instantly dropped) columns from dropped indexes +@param[in] ctx In-place ALTER TABLE context +@param[in, out] drop_col_list list which will be set, containing columns + which is part of index being dropped +@param[in, out] drop_v_col_list list which will be set, containing + virtual columns which is part of index + being dropped */ +static +void +collect_columns_from_dropped_indexes( + const ha_innobase_inplace_ctx* ctx, + col_set& drop_col_list, + col_set& drop_v_col_list) +{ + for (ulint index_count = 0; index_count < ctx->num_to_drop_index; + index_count++) { + const dict_index_t* index = ctx->drop_index[index_count]; + + for (ulint col = 0; col < index->n_user_defined_cols; col++) { + const dict_col_t* idx_col + = dict_index_get_nth_col(index, col); + + if (idx_col->is_virtual()) { + const dict_v_col_t* v_col + = reinterpret_cast< + const dict_v_col_t*>(idx_col); + drop_v_col_list.insert(v_col->v_pos); + + } else { + ulint col_no = dict_col_get_no(idx_col); + if (ctx->col_map + && ctx->col_map[col_no] + == ULINT_UNDEFINED) { + // this column was instantly dropped + continue; + } + drop_col_list.insert(col_no); + } + } + } +} + +/** Change PAGE_COMPRESSED to ON or change the PAGE_COMPRESSION_LEVEL. +@param[in] level PAGE_COMPRESSION_LEVEL +@param[in] table table before the change +@param[in,out] trx data dictionary transaction +@param[in] table_name table name in MariaDB +@return whether the operation succeeded */ +MY_ATTRIBUTE((nonnull, warn_unused_result)) +static +bool +innobase_page_compression_try( + uint level, + const dict_table_t* table, + trx_t* trx, + const char* table_name) +{ + DBUG_ENTER("innobase_page_compression_try"); + DBUG_ASSERT(level >= 1); + DBUG_ASSERT(level <= 9); + + unsigned flags = table->flags + & ~(0xFU << DICT_TF_POS_PAGE_COMPRESSION_LEVEL); + flags |= 1U << DICT_TF_POS_PAGE_COMPRESSION + | level << DICT_TF_POS_PAGE_COMPRESSION_LEVEL; + + if (table->flags == flags) { + DBUG_RETURN(false); + } + + pars_info_t* info = pars_info_create(); + + pars_info_add_ull_literal(info, "id", table->id); + pars_info_add_int4_literal(info, "type", + dict_tf_to_sys_tables_type(flags)); + + dberr_t error = que_eval_sql(info, + "PROCEDURE CHANGE_COMPRESSION () IS\n" + "BEGIN\n" + "UPDATE SYS_TABLES SET TYPE=:type\n" + "WHERE ID=:id;\n" + "END;\n", + false, trx); + + if (error != DB_SUCCESS) { + my_error_innodb(error, table_name, 0); + trx->error_state = DB_SUCCESS; + trx->op_info = ""; + DBUG_RETURN(true); + } + + DBUG_RETURN(false); +} + +static +void +dict_stats_try_drop_table(THD *thd, const table_name_t &name, + const LEX_CSTRING &table_name) +{ + char errstr[1024]; + if (dict_stats_drop_table(name.m_name, errstr, sizeof(errstr)) != DB_SUCCESS) + { + push_warning_printf(thd, Sql_condition::WARN_LEVEL_WARN, ER_ALTER_INFO, + "Deleting persistent statistics" + " for table '%s' in InnoDB failed: %s", + table_name.str, + errstr); + } +} + +/** Evict the table from cache and reopen it. Drop outdated statistics. +@param thd mariadb THD entity +@param table innodb table +@param table_name user-friendly table name for errors +@param ctx ALTER TABLE context +@return newly opened table */ +static dict_table_t *innobase_reload_table(THD *thd, dict_table_t *table, + const LEX_CSTRING &table_name, + ha_innobase_inplace_ctx &ctx) +{ + char *tb_name= strdup(table->name.m_name); + dict_table_close(table, true, false); + + if (ctx.is_instant()) + { + for (auto i = ctx.old_n_v_cols; i--; ) + { + ctx.old_v_cols[i].~dict_v_col_t(); + const_cast<unsigned&>(ctx.old_n_v_cols) = 0; + } + } + + dict_sys.remove(table); + table= dict_table_open_on_name(tb_name, TRUE, TRUE, + DICT_ERR_IGNORE_FK_NOKEY); + + /* Drop outdated table stats. */ + dict_stats_try_drop_table(thd, table->name, table_name); + free(tb_name); + return table; +} + +/** Commit the changes made during prepare_inplace_alter_table() +and inplace_alter_table() inside the data dictionary tables, +when not rebuilding the table. +@param ha_alter_info Data used during in-place alter +@param ctx In-place ALTER TABLE context +@param old_table MySQL table as it is before the ALTER operation +@param trx Data dictionary transaction +@param table_name Table name in MySQL +@retval true Failure +@retval false Success +*/ +inline MY_ATTRIBUTE((nonnull, warn_unused_result)) +bool +commit_try_norebuild( +/*=================*/ + Alter_inplace_info* ha_alter_info, + ha_innobase_inplace_ctx*ctx, + TABLE* altered_table, + const TABLE* old_table, + trx_t* trx, + const char* table_name) +{ + DBUG_ENTER("commit_try_norebuild"); + DBUG_ASSERT(!ctx->need_rebuild()); + DBUG_ASSERT(trx->dict_operation_lock_mode == RW_X_LATCH); + DBUG_ASSERT(!(ha_alter_info->handler_flags + & ALTER_DROP_FOREIGN_KEY) + || ctx->num_to_drop_fk > 0); + DBUG_ASSERT(ctx->num_to_drop_fk + <= ha_alter_info->alter_info->drop_list.elements + || ctx->num_to_drop_vcol + == ha_alter_info->alter_info->drop_list.elements); + + if (ctx->page_compression_level + && innobase_page_compression_try(ctx->page_compression_level, + ctx->new_table, trx, + table_name)) { + DBUG_RETURN(true); + } + + for (ulint i = 0; i < ctx->num_to_add_index; i++) { + dict_index_t* index = ctx->add_index[i]; + DBUG_ASSERT(dict_index_get_online_status(index) + == ONLINE_INDEX_COMPLETE); + DBUG_ASSERT(!index->is_committed()); + if (index->is_corrupted()) { + /* Report a duplicate key + error for the index that was + flagged corrupted, most likely + because a duplicate value was + inserted (directly or by + rollback) after + ha_innobase::inplace_alter_table() + completed. + TODO: report this as a corruption + with a detailed reason once + WL#6379 has been implemented. */ + my_error(ER_DUP_UNKNOWN_IN_INDEX, + MYF(0), index->name()); + DBUG_RETURN(true); + } + } + + if (innobase_update_foreign_try(ctx, trx, table_name)) { + DBUG_RETURN(true); + } + + if ((ha_alter_info->handler_flags & ALTER_COLUMN_UNVERSIONED) + && vers_change_fields_try(ha_alter_info, ctx, trx, old_table)) { + DBUG_RETURN(true); + } + + dberr_t error; + + /* We altered the table in place. Mark the indexes as committed. */ + for (ulint i = 0; i < ctx->num_to_add_index; i++) { + dict_index_t* index = ctx->add_index[i]; + DBUG_ASSERT(dict_index_get_online_status(index) + == ONLINE_INDEX_COMPLETE); + DBUG_ASSERT(!index->is_committed()); + error = row_merge_rename_index_to_add( + trx, ctx->new_table->id, index->id); + switch (error) { + case DB_SUCCESS: + break; + case DB_TOO_MANY_CONCURRENT_TRXS: + /* If we wrote some undo log here, then the + persistent data dictionary for this table may + probably be corrupted. This is because a + 'trigger' on SYS_INDEXES could already have invoked + btr_free_if_exists(), which cannot be rolled back. */ + DBUG_ASSERT(trx->undo_no == 0); + my_error(ER_TOO_MANY_CONCURRENT_TRXS, MYF(0)); + DBUG_RETURN(true); + default: + sql_print_error( + "InnoDB: rename index to add: %lu\n", + (ulong) error); + DBUG_ASSERT(0); + my_error(ER_INTERNAL_ERROR, MYF(0), + "rename index to add"); + DBUG_RETURN(true); + } + } + + /* Drop any indexes that were requested to be dropped. + Flag them in the data dictionary first. */ + + for (ulint i = 0; i < ctx->num_to_drop_index; i++) { + dict_index_t* index = ctx->drop_index[i]; + DBUG_ASSERT(index->is_committed()); + DBUG_ASSERT(index->table == ctx->new_table); + DBUG_ASSERT(index->to_be_dropped); + + error = row_merge_rename_index_to_drop( + trx, index->table->id, index->id); + if (error != DB_SUCCESS) { + sql_print_error( + "InnoDB: rename index to drop: %lu\n", + (ulong) error); + DBUG_ASSERT(0); + my_error(ER_INTERNAL_ERROR, MYF(0), + "rename index to drop"); + DBUG_RETURN(true); + } + } + + if (innobase_rename_or_enlarge_columns_try(ha_alter_info, ctx, + altered_table, old_table, + trx, table_name)) { + 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, + trx)); + } + + if (ha_alter_info->handler_flags + & (ALTER_DROP_VIRTUAL_COLUMN | ALTER_ADD_VIRTUAL_COLUMN)) { + if ((ha_alter_info->handler_flags & ALTER_DROP_VIRTUAL_COLUMN) + && innobase_drop_virtual_try(ha_alter_info, ctx->old_table, + trx)) { + DBUG_RETURN(true); + } + + if ((ha_alter_info->handler_flags & ALTER_ADD_VIRTUAL_COLUMN) + && innobase_add_virtual_try(ha_alter_info, ctx->old_table, + trx)) { + DBUG_RETURN(true); + } + + unsigned n_col = ctx->old_table->n_cols + - DATA_N_SYS_COLS; + unsigned n_v_col = ctx->old_table->n_v_cols + + ctx->num_to_add_vcol - ctx->num_to_drop_vcol; + + if (innodb_update_cols( + ctx->old_table, + dict_table_encode_n_col(n_col, n_v_col) + | unsigned(ctx->old_table->flags & DICT_TF_COMPACT) + << 31, trx)) { + DBUG_RETURN(true); + } + } + + DBUG_RETURN(false); +} + +/** Commit the changes to the data dictionary cache +after a successful commit_try_norebuild() call. +@param ha_alter_info algorithm=inplace context +@param ctx In-place ALTER TABLE context for the current partition +@param altered_table the TABLE after the ALTER +@param table the TABLE before the ALTER +@param trx Data dictionary transaction +(will be started and committed, for DROP INDEX) +@return whether all replacements were found for dropped indexes */ +inline MY_ATTRIBUTE((nonnull)) +bool +commit_cache_norebuild( +/*===================*/ + Alter_inplace_info* ha_alter_info, + ha_innobase_inplace_ctx*ctx, + const TABLE* altered_table, + const TABLE* table, + trx_t* trx) +{ + DBUG_ENTER("commit_cache_norebuild"); + DBUG_ASSERT(!ctx->need_rebuild()); + DBUG_ASSERT(ctx->new_table->space != fil_system.temp_space); + DBUG_ASSERT(!ctx->new_table->is_temporary()); + + bool found = true; + + if (ctx->page_compression_level) { + DBUG_ASSERT(ctx->new_table->space != fil_system.sys_space); +#if defined __GNUC__ && !defined __clang__ && __GNUC__ < 6 +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wconversion" /* GCC 4 and 5 need this here */ +#endif + ctx->new_table->flags + = static_cast<uint16_t>( + (ctx->new_table->flags + & ~(0xFU + << DICT_TF_POS_PAGE_COMPRESSION_LEVEL)) + | 1 << DICT_TF_POS_PAGE_COMPRESSION + | (ctx->page_compression_level & 0xF) + << DICT_TF_POS_PAGE_COMPRESSION_LEVEL) + & ((1U << DICT_TF_BITS) - 1); +#if defined __GNUC__ && !defined __clang__ && __GNUC__ < 6 +# pragma GCC diagnostic pop +#endif + + if (fil_space_t* space = ctx->new_table->space) { + bool update = !(space->flags + & FSP_FLAGS_MASK_PAGE_COMPRESSION); + mutex_enter(&fil_system.mutex); + space->flags &= ~FSP_FLAGS_MASK_MEM_COMPRESSION_LEVEL; + space->flags |= ctx->page_compression_level + << FSP_FLAGS_MEM_COMPRESSION_LEVEL; + if (!space->full_crc32()) { + space->flags + |= FSP_FLAGS_MASK_PAGE_COMPRESSION; + } else if (!space->is_compressed()) { + space->flags + |= innodb_compression_algorithm + << FSP_FLAGS_FCRC32_POS_COMPRESSED_ALGO; + } + mutex_exit(&fil_system.mutex); + + if (update) { + /* Maybe we should introduce an undo + log record for updating tablespace + flags, and perform the update already + in innobase_page_compression_try(). + + If the server is killed before the + following mini-transaction commit + becomes durable, fsp_flags_try_adjust() + will perform the equivalent adjustment + and warn "adjusting FSP_SPACE_FLAGS". */ + mtr_t mtr; + mtr.start(); + if (buf_block_t* b = buf_page_get( + page_id_t(space->id, 0), + space->zip_size(), + RW_X_LATCH, &mtr)) { + byte* f = FSP_HEADER_OFFSET + + FSP_SPACE_FLAGS + b->frame; + const auto sf = space->flags + & ~FSP_FLAGS_MEM_MASK; + if (mach_read_from_4(f) != sf) { + mtr.set_named_space(space); + mtr.write<4,mtr_t::FORCED>( + *b, f, sf); + } + } + mtr.commit(); + } + } + } + + col_set drop_list; + col_set v_drop_list; + + /* Check if the column, part of an index to be dropped is part of any + other index which is not being dropped. If it so, then set the ord_part + of the column to 0. */ + collect_columns_from_dropped_indexes(ctx, drop_list, v_drop_list); + + for (ulint col : drop_list) { + if (!check_col_exists_in_indexes(ctx->new_table, col, false)) { + ctx->new_table->cols[col].ord_part = 0; + } + } + + for (ulint col : v_drop_list) { + if (!check_col_exists_in_indexes(ctx->new_table, col, true)) { + ctx->new_table->v_cols[col].m_col.ord_part = 0; + } + } + + for (ulint i = 0; i < ctx->num_to_add_index; i++) { + dict_index_t* index = ctx->add_index[i]; + DBUG_ASSERT(dict_index_get_online_status(index) + == ONLINE_INDEX_COMPLETE); + DBUG_ASSERT(!index->is_committed()); + index->set_committed(true); + } + + if (ctx->num_to_drop_index) { + /* Really drop the indexes that were dropped. + The transaction had to be committed first + (after renaming the indexes), so that in the + event of a crash, crash recovery will drop the + indexes, because it drops all indexes whose + names start with TEMP_INDEX_PREFIX_STR. Once we + have started dropping an index tree, there is + no way to roll it back. */ + + for (ulint i = 0; i < ctx->num_to_drop_index; i++) { + dict_index_t* index = ctx->drop_index[i]; + DBUG_ASSERT(index->is_committed()); + DBUG_ASSERT(index->table == ctx->new_table); + DBUG_ASSERT(index->to_be_dropped); + + /* Replace the indexes in foreign key + constraints if needed. */ + + if (!dict_foreign_replace_index( + index->table, ctx->col_names, index)) { + found = false; + } + + /* Mark the index dropped + in the data dictionary cache. */ + rw_lock_x_lock(dict_index_get_lock(index)); + index->page = FIL_NULL; + rw_lock_x_unlock(dict_index_get_lock(index)); + } + + trx_start_for_ddl(trx, TRX_DICT_OP_INDEX); + row_merge_drop_indexes_dict(trx, ctx->new_table->id); + + for (ulint i = 0; i < ctx->num_to_drop_index; i++) { + dict_index_t* index = ctx->drop_index[i]; + DBUG_ASSERT(index->is_committed()); + DBUG_ASSERT(index->table == ctx->new_table); + + if (index->type & DICT_FTS) { + DBUG_ASSERT(index->type == DICT_FTS + || (index->type + & DICT_CORRUPT)); + DBUG_ASSERT(index->table->fts); + DEBUG_SYNC_C("norebuild_fts_drop"); + fts_drop_index(index->table, index, trx); + } + + dict_index_remove_from_cache(index->table, index); + } + + fts_clear_all(ctx->old_table, trx); + trx_commit_for_mysql(trx); + } + + if (!ctx->is_instant()) { + innobase_rename_or_enlarge_columns_cache( + ha_alter_info, altered_table, table, ctx->new_table); + } else { + ut_ad(ctx->col_map); + + if (fts_t* fts = ctx->new_table->fts) { + ut_ad(fts->doc_col != ULINT_UNDEFINED); + ut_ad(ctx->new_table->n_cols > DATA_N_SYS_COLS); + const ulint c = ctx->col_map[fts->doc_col]; + ut_ad(c < ulint(ctx->new_table->n_cols) + - DATA_N_SYS_COLS); + ut_d(const dict_col_t& col = ctx->new_table->cols[c]); + ut_ad(!col.is_nullable()); + ut_ad(!col.is_virtual()); + ut_ad(!col.is_added()); + ut_ad(col.prtype & DATA_UNSIGNED); + ut_ad(col.mtype == DATA_INT); + ut_ad(col.len == 8); + ut_ad(col.ord_part); + fts->doc_col = c; + } + + if (ha_alter_info->handler_flags & ALTER_DROP_STORED_COLUMN) { + const dict_index_t* index = ctx->new_table->indexes.start; + + for (const dict_field_t* f = index->fields, + * const end = f + index->n_fields; + f != end; f++) { + dict_col_t& c = *f->col; + if (c.is_dropped()) { + c.set_dropped(!c.is_nullable(), + DATA_LARGE_MTYPE(c.mtype) + || (!f->fixed_len + && c.len > 255), + f->fixed_len); + } + } + } + + if (!ctx->instant_table->persistent_autoinc) { + ctx->new_table->persistent_autoinc = 0; + } + } + + if (ha_alter_info->handler_flags & ALTER_COLUMN_UNVERSIONED) { + 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( + ctx->new_table, FTS_DOC_ID_INDEX_NAME) + : NULL; + DBUG_ASSERT((ctx->new_table->fts == NULL) + == (ctx->new_table->fts_doc_id_index == NULL)); + DBUG_RETURN(found); +} + +/** Adjust the persistent statistics after non-rebuilding ALTER TABLE. +Remove statistics for dropped indexes, add statistics for created indexes +and rename statistics for renamed indexes. +@param ha_alter_info Data used during in-place alter +@param ctx In-place ALTER TABLE context +@param thd MySQL connection +*/ +static +void +alter_stats_norebuild( +/*==================*/ + Alter_inplace_info* ha_alter_info, + ha_innobase_inplace_ctx* ctx, + THD* thd) +{ + ulint i; + + DBUG_ENTER("alter_stats_norebuild"); + DBUG_ASSERT(!ctx->need_rebuild()); + + if (!dict_stats_is_persistent_enabled(ctx->new_table)) { + DBUG_VOID_RETURN; + } + + /* Delete corresponding rows from the stats table. We do this + in a separate transaction from trx, because lock waits are not + allowed in a data dictionary transaction. (Lock waits are possible + on the statistics table, because it is directly accessible by users, + not covered by the dict_sys.latch.) + + Because the data dictionary changes were already committed, orphaned + rows may be left in the statistics table if the system crashes. + + FIXME: each change to the statistics tables is being committed in a + separate transaction, meaning that the operation is not atomic + + FIXME: This will not drop the (unused) statistics for + FTS_DOC_ID_INDEX if it was a hidden index, dropped together + with the last renamining FULLTEXT index. */ + for (i = 0; i < ha_alter_info->index_drop_count; i++) { + const KEY* key = ha_alter_info->index_drop_buffer[i]; + + if (key->flags & HA_FULLTEXT) { + /* There are no index cardinality + statistics for FULLTEXT indexes. */ + continue; + } + + char errstr[1024]; + + if (dict_stats_drop_index( + ctx->new_table->name.m_name, key->name.str, + errstr, sizeof errstr) != DB_SUCCESS) { + push_warning(thd, + Sql_condition::WARN_LEVEL_WARN, + ER_LOCK_WAIT_TIMEOUT, errstr); + } + } + + for (size_t i = 0; i < ha_alter_info->rename_keys.size(); i++) { + const Alter_inplace_info::Rename_key_pair& pair + = ha_alter_info->rename_keys[i]; + + std::stringstream ss; + ss << TEMP_FILE_PREFIX_INNODB << std::this_thread::get_id() + << i; + auto tmp_name = ss.str(); + + dberr_t err = dict_stats_rename_index(ctx->new_table, + pair.old_key->name.str, + tmp_name.c_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, + tmp_name.c_str(), + ut_strerr(err)); + } + } + + for (size_t i = 0; i < ha_alter_info->rename_keys.size(); i++) { + const Alter_inplace_info::Rename_key_pair& pair + = ha_alter_info->rename_keys[i]; + + std::stringstream ss; + ss << TEMP_FILE_PREFIX_INNODB << std::this_thread::get_id() + << i; + auto tmp_name = ss.str(); + + dberr_t err = dict_stats_rename_index(ctx->new_table, + tmp_name.c_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, + tmp_name.c_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); + + if (!(index->type & DICT_FTS)) { + dict_stats_init(ctx->new_table); + dict_stats_update_for_index(index); + } + } + + DBUG_VOID_RETURN; +} + +/** Adjust the persistent statistics after rebuilding ALTER TABLE. +Remove statistics for dropped indexes, add statistics for created indexes +and rename statistics for renamed indexes. +@param table InnoDB table that was rebuilt by ALTER TABLE +@param table_name Table name in MySQL +@param thd MySQL connection +*/ +static +void +alter_stats_rebuild( +/*================*/ + dict_table_t* table, + const char* table_name, + THD* thd) +{ + DBUG_ENTER("alter_stats_rebuild"); + + if (!table->space + || !dict_stats_is_persistent_enabled(table)) { + DBUG_VOID_RETURN; + } + + dberr_t ret = dict_stats_update(table, DICT_STATS_RECALC_PERSISTENT); + + if (ret != DB_SUCCESS) { + push_warning_printf( + thd, + Sql_condition::WARN_LEVEL_WARN, + ER_ALTER_INFO, + "Error updating stats for table '%s'" + " after table rebuild: %s", + table_name, ut_strerr(ret)); + } + + DBUG_VOID_RETURN; +} + +#ifndef DBUG_OFF +# define DBUG_INJECT_CRASH(prefix, count) \ +do { \ + char buf[32]; \ + snprintf(buf, sizeof buf, prefix "_%u", count); \ + DBUG_EXECUTE_IF(buf, DBUG_SUICIDE();); \ +} while (0) +#else +# define DBUG_INJECT_CRASH(prefix, count) +#endif + +/** Apply the log for the table rebuild operation. +@param[in] ctx Inplace Alter table context +@param[in] altered_table MySQL table that is being altered +@return true Failure, else false. */ +static bool alter_rebuild_apply_log( + ha_innobase_inplace_ctx* ctx, + Alter_inplace_info* ha_alter_info, + TABLE* altered_table) +{ + DBUG_ENTER("alter_rebuild_apply_log"); + + if (!ctx->online) { + DBUG_RETURN(false); + } + + /* We copied the table. Any indexes that were requested to be + dropped were not created in the copy of the table. Apply any + last bit of the rebuild log and then rename the tables. */ + dict_table_t* user_table = ctx->old_table; + dict_table_t* rebuilt_table = ctx->new_table; + + DEBUG_SYNC_C("row_log_table_apply2_before"); + + dict_vcol_templ_t* s_templ = NULL; + + if (ctx->new_table->n_v_cols > 0) { + s_templ = UT_NEW_NOKEY( + dict_vcol_templ_t()); + s_templ->vtempl = NULL; + + innobase_build_v_templ(altered_table, ctx->new_table, s_templ, + NULL, true); + ctx->new_table->vc_templ = s_templ; + } + + dberr_t error = row_log_table_apply( + ctx->thr, user_table, altered_table, + static_cast<ha_innobase_inplace_ctx*>( + ha_alter_info->handler_ctx)->m_stage, + ctx->new_table); + + if (s_templ) { + ut_ad(ctx->need_rebuild()); + dict_free_vc_templ(s_templ); + UT_DELETE(s_templ); + ctx->new_table->vc_templ = NULL; + } + + ulint err_key = thr_get_trx(ctx->thr)->error_key_num; + + switch (error) { + KEY* dup_key; + case DB_SUCCESS: + break; + case DB_DUPLICATE_KEY: + if (err_key == ULINT_UNDEFINED) { + /* This should be the hidden index on + FTS_DOC_ID. */ + dup_key = NULL; + } else { + DBUG_ASSERT(err_key < ha_alter_info->key_count); + dup_key = &ha_alter_info->key_info_buffer[err_key]; + } + + print_keydup_error(altered_table, dup_key, MYF(0)); + DBUG_RETURN(true); + case DB_ONLINE_LOG_TOO_BIG: + my_error(ER_INNODB_ONLINE_LOG_TOO_BIG, MYF(0), + get_error_key_name(err_key, ha_alter_info, + rebuilt_table)); + DBUG_RETURN(true); + case DB_INDEX_CORRUPT: + my_error(ER_INDEX_CORRUPT, MYF(0), + get_error_key_name(err_key, ha_alter_info, + rebuilt_table)); + DBUG_RETURN(true); + default: + my_error_innodb(error, ctx->old_table->name.m_name, + user_table->flags); + DBUG_RETURN(true); + } + + DBUG_RETURN(false); +} + +/** Commit or rollback the changes made during +prepare_inplace_alter_table() and inplace_alter_table() inside +the storage engine. Note that the allowed level of concurrency +during this operation will be the same as for +inplace_alter_table() and thus might be higher than during +prepare_inplace_alter_table(). (E.g concurrent writes were +blocked during prepare, but might not be during commit). +@param altered_table TABLE object for new version of table. +@param ha_alter_info Structure describing changes to be done +by ALTER TABLE and holding data used during in-place alter. +@param commit true => Commit, false => Rollback. +@retval true Failure +@retval false Success +*/ + +bool +ha_innobase::commit_inplace_alter_table( +/*====================================*/ + TABLE* altered_table, + Alter_inplace_info* ha_alter_info, + bool commit) +{ + ha_innobase_inplace_ctx*ctx0; + + ctx0 = static_cast<ha_innobase_inplace_ctx*> + (ha_alter_info->handler_ctx); + +#ifndef DBUG_OFF + uint crash_inject_count = 1; + uint crash_fail_inject_count = 1; + uint failure_inject_count = 1; +#endif /* DBUG_OFF */ + + DBUG_ENTER("commit_inplace_alter_table"); + DBUG_ASSERT(!srv_read_only_mode); + DBUG_ASSERT(!ctx0 || ctx0->prebuilt == m_prebuilt); + DBUG_ASSERT(!ctx0 || ctx0->old_table == m_prebuilt->table); + + DEBUG_SYNC_C("innodb_commit_inplace_alter_table_enter"); + + DEBUG_SYNC_C("innodb_commit_inplace_alter_table_wait"); + + if (ctx0 != NULL && ctx0->m_stage != NULL) { + ctx0->m_stage->begin_phase_end(); + } + + if (!commit) { + /* A rollback is being requested. So far we may at + most have created some indexes. If any indexes were to + be dropped, they would actually be dropped in this + method if commit=true. */ + const bool ret = rollback_inplace_alter_table( + ha_alter_info, table, m_prebuilt); + DBUG_RETURN(ret); + } + + if (!(ha_alter_info->handler_flags & ~INNOBASE_INPLACE_IGNORE)) { + DBUG_ASSERT(!ctx0); + MONITOR_ATOMIC_DEC(MONITOR_PENDING_ALTER_TABLE); + ha_alter_info->group_commit_ctx = NULL; + DBUG_RETURN(false); + } + + DBUG_ASSERT(ctx0); + + inplace_alter_handler_ctx** ctx_array; + inplace_alter_handler_ctx* ctx_single[2]; + + if (ha_alter_info->group_commit_ctx) { + ctx_array = ha_alter_info->group_commit_ctx; + } else { + ctx_single[0] = ctx0; + ctx_single[1] = NULL; + ctx_array = ctx_single; + } + + DBUG_ASSERT(ctx0 == ctx_array[0]); + ut_ad(m_prebuilt->table == ctx0->old_table); + ha_alter_info->group_commit_ctx = NULL; + + trx_start_if_not_started_xa(m_prebuilt->trx, true); + + for (inplace_alter_handler_ctx** pctx = ctx_array; *pctx; pctx++) { + ha_innobase_inplace_ctx* ctx + = static_cast<ha_innobase_inplace_ctx*>(*pctx); + DBUG_ASSERT(ctx->prebuilt->trx == m_prebuilt->trx); + + /* If decryption failed for old table or new table + fail here. */ + if ((!ctx->old_table->is_readable() + && ctx->old_table->space) + || (!ctx->new_table->is_readable() + && ctx->new_table->space)) { + String str; + const char* engine= table_type(); + get_error_message(HA_ERR_DECRYPTION_FAILED, &str); + my_error(ER_GET_ERRMSG, MYF(0), HA_ERR_DECRYPTION_FAILED, str.c_ptr(), engine); + DBUG_RETURN(true); + } + + /* Exclusively lock the table, to ensure that no other + transaction is holding locks on the table while we + change the table definition. The MySQL meta-data lock + should normally guarantee that no conflicting locks + exist. However, FOREIGN KEY constraints checks and any + transactions collected during crash recovery could be + holding InnoDB locks only, not MySQL locks. */ + + dberr_t error = row_merge_lock_table( + m_prebuilt->trx, ctx->old_table, LOCK_X); + + if (error != DB_SUCCESS) { + my_error_innodb( + error, table_share->table_name.str, 0); + DBUG_RETURN(true); + } + } + + DEBUG_SYNC(m_user_thd, "innodb_alter_commit_after_lock_table"); + + const bool new_clustered = ctx0->need_rebuild(); + trx_t* trx = ctx0->trx; + bool fail = false; + + /* Stop background FTS operations. */ + for (inplace_alter_handler_ctx** pctx = ctx_array; + *pctx; pctx++) { + ha_innobase_inplace_ctx* ctx + = static_cast<ha_innobase_inplace_ctx*>(*pctx); + + DBUG_ASSERT(new_clustered == ctx->need_rebuild()); + + if (new_clustered) { + if (ctx->old_table->fts) { + ut_ad(!ctx->old_table->fts->add_wq); + fts_optimize_remove_table(ctx->old_table); + } + } + + if (ctx->new_table->fts) { + ut_ad(!ctx->new_table->fts->add_wq); + fts_optimize_remove_table(ctx->new_table); + fts_sync_during_ddl(ctx->new_table); + } + + /* Apply the online log of the table before acquiring + data dictionary latches. Here alter thread already acquired + MDL_EXCLUSIVE on the table. So there can't be anymore DDLs, DMLs + for the altered table. By applying the log here, InnoDB + makes sure that concurrent DDLs, purge thread or any other + background thread doesn't wait for the dict_operation_lock + for longer time. */ + if (new_clustered && commit + && alter_rebuild_apply_log( + ctx, ha_alter_info, altered_table)) { + DBUG_RETURN(true); + } + } + + if (!trx) { + DBUG_ASSERT(!new_clustered); + trx = innobase_trx_allocate(m_user_thd); + } + + trx_start_for_ddl(trx, TRX_DICT_OP_INDEX); + /* Latch the InnoDB data dictionary exclusively so that no deadlocks + or lock waits can happen in it during the data dictionary operation. */ + row_mysql_lock_data_dictionary(trx); + + /* Prevent the background statistics collection from accessing + the tables. */ + for (;;) { + bool retry = false; + + for (inplace_alter_handler_ctx** pctx = ctx_array; + *pctx; pctx++) { + ha_innobase_inplace_ctx* ctx + = static_cast<ha_innobase_inplace_ctx*>(*pctx); + + DBUG_ASSERT(new_clustered == ctx->need_rebuild()); + + if (new_clustered + && !dict_stats_stop_bg(ctx->old_table)) { + retry = true; + } + + if (!dict_stats_stop_bg(ctx->new_table)) { + retry = true; + } + } + + if (!retry) { + break; + } + + DICT_BG_YIELD(trx); + } + + /* Apply the changes to the data dictionary tables, for all + partitions. */ + + for (inplace_alter_handler_ctx** pctx = ctx_array; + *pctx && !fail; pctx++) { + ha_innobase_inplace_ctx* ctx + = static_cast<ha_innobase_inplace_ctx*>(*pctx); + + DBUG_ASSERT(new_clustered == ctx->need_rebuild()); + if (ctx->need_rebuild() && !ctx->old_table->space) { + my_error(ER_TABLESPACE_DISCARDED, MYF(0), + table->s->table_name.str); + fail = true; + } else { + fail = commit_set_autoinc(ha_alter_info, ctx, + altered_table, table); + } + + if (fail) { + } else if (ctx->need_rebuild()) { + ctx->tmp_name = dict_mem_create_temporary_tablename( + ctx->heap, ctx->new_table->name.m_name, + ctx->new_table->id); + + fail = commit_try_rebuild( + ha_alter_info, ctx, altered_table, table, + trx, table_share->table_name.str); + } else { + fail = commit_try_norebuild( + ha_alter_info, ctx, altered_table, table, trx, + table_share->table_name.str); + } + DBUG_INJECT_CRASH("ib_commit_inplace_crash", + crash_inject_count++); +#ifndef DBUG_OFF + { + /* Generate a dynamic dbug text. */ + char buf[32]; + + snprintf(buf, sizeof buf, + "ib_commit_inplace_fail_%u", + failure_inject_count++); + + DBUG_EXECUTE_IF(buf, + my_error(ER_INTERNAL_ERROR, MYF(0), + "Injected error!"); + fail = true; + ); + } +#endif + } + + /* Commit or roll back the changes to the data dictionary. */ + DEBUG_SYNC(m_user_thd, "innodb_alter_inplace_before_commit"); + + if (fail) { + trx_rollback_for_mysql(trx); + for (inplace_alter_handler_ctx** pctx = ctx_array; + *pctx; pctx++) { + ha_innobase_inplace_ctx* ctx + = static_cast<ha_innobase_inplace_ctx*>(*pctx); + ctx->rollback_instant(); + } + } else if (!new_clustered) { + trx_commit_for_mysql(trx); + } else { + /* Test what happens on crash if the redo logs + are flushed to disk here. The log records + about the rename should not be committed, and + the data dictionary transaction should be + rolled back, restoring the old table. */ + DBUG_EXECUTE_IF("innodb_alter_commit_crash_before_commit", + log_buffer_flush_to_disk(); + DBUG_SUICIDE();); + ut_ad(!trx->fts_trx); + + if (fail) { + trx_rollback_for_mysql(trx); + } else { + ut_ad(trx_state_eq(trx, TRX_STATE_ACTIVE)); + ut_ad(trx->has_logged()); + trx->commit(); + } + + /* If server crashes here, the dictionary in + InnoDB and MySQL will differ. The .ibd files + and the .frm files must be swapped manually by + the administrator. No loss of data. */ + DBUG_EXECUTE_IF("innodb_alter_commit_crash_after_commit", + log_buffer_flush_to_disk(); + DBUG_SUICIDE();); + } + + /* Flush the log to reduce probability that the .frm files and + the InnoDB data dictionary get out-of-sync if the user runs + with innodb_flush_log_at_trx_commit = 0 */ + + log_buffer_flush_to_disk(); + + /* At this point, the changes to the persistent storage have + been committed or rolled back. What remains to be done is to + update the in-memory structures, close some handles, release + temporary files, and (unless we rolled back) update persistent + statistics. */ + for (inplace_alter_handler_ctx** pctx = ctx_array; + *pctx; pctx++) { + ha_innobase_inplace_ctx* ctx + = static_cast<ha_innobase_inplace_ctx*>(*pctx); + + DBUG_ASSERT(ctx->need_rebuild() == new_clustered); + + if (new_clustered) { + innobase_online_rebuild_log_free(ctx->old_table); + } + + if (fail) { + if (new_clustered) { + trx_start_for_ddl(trx, TRX_DICT_OP_TABLE); + + dict_table_close_and_drop(trx, ctx->new_table); + + trx_commit_for_mysql(trx); + ctx->new_table = NULL; + } else { + /* We failed, but did not rebuild the table. + Roll back any ADD INDEX, or get rid of garbage + ADD INDEX that was left over from a previous + ALTER TABLE statement. */ + trx_start_for_ddl(trx, TRX_DICT_OP_INDEX); + innobase_rollback_sec_index( + ctx->new_table, table, TRUE, trx); + trx_commit_for_mysql(trx); + } + DBUG_INJECT_CRASH("ib_commit_inplace_crash_fail", + crash_fail_inject_count++); + + continue; + } + + innobase_copy_frm_flags_from_table_share( + ctx->new_table, altered_table->s); + + if (new_clustered) { + /* We will reload and refresh the + in-memory foreign key constraint + metadata. This is a rename operation + in preparing for dropping the old + table. Set the table to_be_dropped bit + here, so to make sure DML foreign key + constraint check does not use the + stale dict_foreign_t. This is done + because WL#6049 (FK MDL) has not been + implemented yet. */ + ctx->old_table->to_be_dropped = true; + + DBUG_PRINT("to_be_dropped", + ("table: %s", ctx->old_table->name.m_name)); + + if (innobase_update_foreign_cache(ctx, m_user_thd) + != DB_SUCCESS + && m_prebuilt->trx->check_foreigns) { +foreign_fail: + push_warning_printf( + m_user_thd, + Sql_condition::WARN_LEVEL_WARN, + ER_ALTER_INFO, + "failed to load FOREIGN KEY" + " constraints"); + } + } else { + bool fk_fail = innobase_update_foreign_cache( + ctx, m_user_thd) != DB_SUCCESS; + + if (!commit_cache_norebuild(ha_alter_info, ctx, + altered_table, table, + trx)) { + fk_fail = true; + } + + if (fk_fail && m_prebuilt->trx->check_foreigns) { + goto foreign_fail; + } + } + + dict_mem_table_free_foreign_vcol_set(ctx->new_table); + dict_mem_table_fill_foreign_vcol_set(ctx->new_table); + + DBUG_INJECT_CRASH("ib_commit_inplace_crash", + crash_inject_count++); + } + + if (fail) { + for (inplace_alter_handler_ctx** pctx = ctx_array; + *pctx; pctx++) { + ha_innobase_inplace_ctx* ctx + = static_cast<ha_innobase_inplace_ctx*> + (*pctx); + DBUG_ASSERT(ctx->need_rebuild() == new_clustered); + + ut_d(dict_table_check_for_dup_indexes( + ctx->old_table, + CHECK_ABORTED_OK)); + ut_a(fts_check_cached_index(ctx->old_table)); + DBUG_INJECT_CRASH("ib_commit_inplace_crash_fail", + crash_fail_inject_count++); + + /* Restart the FTS background operations. */ + if (ctx->old_table->fts) { + fts_optimize_add_table(ctx->old_table); + } + } + + row_mysql_unlock_data_dictionary(trx); + if (trx != ctx0->trx) { + trx->free(); + } + DBUG_RETURN(true); + } + + if (trx == ctx0->trx) { + ctx0->trx = NULL; + } + + /* Free the ctx->trx of other partitions, if any. We will only + use the ctx0->trx here. Others may have been allocated in + the prepare stage. */ + + for (inplace_alter_handler_ctx** pctx = &ctx_array[1]; *pctx; + pctx++) { + ha_innobase_inplace_ctx* ctx + = static_cast<ha_innobase_inplace_ctx*>(*pctx); + + if (ctx->trx) { + ctx->trx->free(); + ctx->trx = NULL; + } + } + + /* MDEV-17468: Avoid this at least when ctx->is_instant(). + Currently dict_load_column_low() is the only place where + num_base for virtual columns is assigned to nonzero. */ + if (ctx0->num_to_drop_vcol || ctx0->num_to_add_vcol + || (ctx0->new_table->n_v_cols && !new_clustered + && (ha_alter_info->alter_info->drop_list.elements + || ha_alter_info->alter_info->create_list.elements)) + || (ctx0->is_instant() + && m_prebuilt->table->n_v_cols + && ha_alter_info->handler_flags & ALTER_STORED_COLUMN_ORDER)) { + DBUG_ASSERT(ctx0->old_table->get_ref_count() == 1); + ut_ad(ctx0->prebuilt == m_prebuilt); + trx_commit_for_mysql(m_prebuilt->trx); + + for (inplace_alter_handler_ctx** pctx = ctx_array; *pctx; + pctx++) { + auto ctx= static_cast<ha_innobase_inplace_ctx*>(*pctx); + ctx->prebuilt->table = innobase_reload_table( + m_user_thd, ctx->prebuilt->table, + table->s->table_name, *ctx); + innobase_copy_frm_flags_from_table_share( + ctx->prebuilt->table, altered_table->s); + } + + row_mysql_unlock_data_dictionary(trx); + trx->free(); + MONITOR_ATOMIC_DEC(MONITOR_PENDING_ALTER_TABLE); + DBUG_RETURN(false); + } + + /* Release the table locks. */ + trx_commit_for_mysql(m_prebuilt->trx); + + DBUG_EXECUTE_IF("ib_ddl_crash_after_user_trx_commit", DBUG_SUICIDE();); + + for (inplace_alter_handler_ctx** pctx = ctx_array; + *pctx; pctx++) { + ha_innobase_inplace_ctx* ctx + = static_cast<ha_innobase_inplace_ctx*> + (*pctx); + DBUG_ASSERT(ctx->need_rebuild() == new_clustered); + + /* Publish the created fulltext index, if any. + Note that a fulltext index can be created without + creating the clustered index, if there already exists + a suitable FTS_DOC_ID column. If not, one will be + created, implying new_clustered */ + for (ulint i = 0; i < ctx->num_to_add_index; i++) { + dict_index_t* index = ctx->add_index[i]; + + if (index->type & DICT_FTS) { + DBUG_ASSERT(index->type == DICT_FTS); + /* We reset DICT_TF2_FTS here because the bit + is left unset when a drop proceeds the add. */ + DICT_TF2_FLAG_SET(ctx->new_table, DICT_TF2_FTS); + fts_add_index(index, ctx->new_table); + } + } + + ut_d(dict_table_check_for_dup_indexes( + ctx->new_table, CHECK_ALL_COMPLETE)); + + /* Start/Restart the FTS background operations. */ + if (ctx->new_table->fts) { + fts_optimize_add_table(ctx->new_table); + } + + ut_d(dict_table_check_for_dup_indexes( + ctx->new_table, CHECK_ABORTED_OK)); + +#ifdef UNIV_DEBUG + if (!(ctx->new_table->fts != NULL + && ctx->new_table->fts->cache->sync->in_progress)) { + ut_a(fts_check_cached_index(ctx->new_table)); + } +#endif + if (new_clustered) { + /* Since the table has been rebuilt, we remove + all persistent statistics corresponding to the + old copy of the table (which was renamed to + ctx->tmp_name). */ + + DBUG_ASSERT(0 == strcmp(ctx->old_table->name.m_name, + ctx->tmp_name)); + + dict_stats_try_drop_table(m_user_thd, + ctx->new_table->name, + table->s->table_name); + + DBUG_EXECUTE_IF("ib_ddl_crash_before_commit", + DBUG_SUICIDE();); + + ut_ad(m_prebuilt != ctx->prebuilt + || ctx == ctx0); + bool update_own_prebuilt = + (m_prebuilt == ctx->prebuilt); + trx_t* const user_trx = m_prebuilt->trx; + + row_prebuilt_free(ctx->prebuilt, TRUE); + + /* Drop the copy of the old table, which was + renamed to ctx->tmp_name at the atomic DDL + transaction commit. If the system crashes + before this is completed, some orphan tables + with ctx->tmp_name may be recovered. */ + trx_start_for_ddl(trx, TRX_DICT_OP_TABLE); + dberr_t error = row_merge_drop_table(trx, ctx->old_table); + + if (UNIV_UNLIKELY(error != DB_SUCCESS)) { + ib::error() << "Inplace alter table " << ctx->old_table->name + << " dropping copy of the old table failed error " + << error + << ". tmp_name " << (ctx->tmp_name ? ctx->tmp_name : "N/A") + << " new_table " << ctx->new_table->name; + } + + trx_commit_for_mysql(trx); + + /* Rebuild the prebuilt object. */ + ctx->prebuilt = row_create_prebuilt( + ctx->new_table, altered_table->s->reclength); + if (update_own_prebuilt) { + m_prebuilt = ctx->prebuilt; + } + trx_start_if_not_started(user_trx, true); + m_prebuilt->trx = user_trx; + } + DBUG_INJECT_CRASH("ib_commit_inplace_crash", + crash_inject_count++); + } + + row_mysql_unlock_data_dictionary(trx); + trx->free(); + + /* TODO: The following code could be executed + while allowing concurrent access to the table + (MDL downgrade). */ + + if (new_clustered) { + for (inplace_alter_handler_ctx** pctx = ctx_array; + *pctx; pctx++) { + ha_innobase_inplace_ctx* ctx + = static_cast<ha_innobase_inplace_ctx*> + (*pctx); + DBUG_ASSERT(ctx->need_rebuild()); + + alter_stats_rebuild( + ctx->new_table, table->s->table_name.str, + m_user_thd); + DBUG_INJECT_CRASH("ib_commit_inplace_crash", + crash_inject_count++); + } + } else { + for (inplace_alter_handler_ctx** pctx = ctx_array; + *pctx; pctx++) { + ha_innobase_inplace_ctx* ctx + = static_cast<ha_innobase_inplace_ctx*> + (*pctx); + DBUG_ASSERT(!ctx->need_rebuild()); + + alter_stats_norebuild(ha_alter_info, ctx, m_user_thd); + DBUG_INJECT_CRASH("ib_commit_inplace_crash", + crash_inject_count++); + } + } + + innobase_parse_hint_from_comment( + m_user_thd, m_prebuilt->table, altered_table->s); + + /* TODO: Also perform DROP TABLE and DROP INDEX after + the MDL downgrade. */ + +#ifndef DBUG_OFF + dict_index_t* clust_index = dict_table_get_first_index( + ctx0->prebuilt->table); + DBUG_ASSERT(!clust_index->online_log); + DBUG_ASSERT(dict_index_get_online_status(clust_index) + == ONLINE_INDEX_COMPLETE); + + for (dict_index_t* index = clust_index; + index; + index = dict_table_get_next_index(index)) { + DBUG_ASSERT(!index->to_be_dropped); + } +#endif /* DBUG_OFF */ + MONITOR_ATOMIC_DEC(MONITOR_PENDING_ALTER_TABLE); + DBUG_RETURN(false); +} + +/** +@param thd the session +@param start_value the lower bound +@param max_value the upper bound (inclusive) */ + +ib_sequence_t::ib_sequence_t( + THD* thd, + ulonglong start_value, + ulonglong max_value) + : + m_max_value(max_value), + m_increment(0), + m_offset(0), + m_next_value(start_value), + m_eof(false) +{ + if (thd != 0 && m_max_value > 0) { + + thd_get_autoinc(thd, &m_offset, &m_increment); + + if (m_increment > 1 || m_offset > 1) { + + /* If there is an offset or increment specified + then we need to work out the exact next value. */ + + m_next_value = innobase_next_autoinc( + start_value, 1, + m_increment, m_offset, m_max_value); + + } else if (start_value == 0) { + /* The next value can never be 0. */ + m_next_value = 1; + } + } else { + m_eof = true; + } +} + +/** +Postfix increment +@return the next value to insert */ + +ulonglong +ib_sequence_t::operator++(int) UNIV_NOTHROW +{ + ulonglong current = m_next_value; + + ut_ad(!m_eof); + ut_ad(m_max_value > 0); + + m_next_value = innobase_next_autoinc( + current, 1, m_increment, m_offset, m_max_value); + + if (m_next_value == m_max_value && current == m_next_value) { + m_eof = true; + } + + return(current); +} |