Re: d23555b1920: MDEV-30984 Online ALTER table is denied with non-informative error messages
Hi, Nikita, On Jun 24, Nikita Malyavin wrote:
revision-id: d23555b1920 (mariadb-11.0.1-138-gd23555b1920) parent(s): df90bf34644 author: Nikita Malyavin committer: Nikita Malyavin timestamp: 2023-06-20 16:10:48 +0300 message:
MDEV-30984 Online ALTER table is denied with non-informative error messages
diff --git a/mysql-test/main/alter_table_online.result b/mysql-test/main/alter_table_online.result --- a/mysql-test/main/alter_table_online.result +++ b/mysql-test/main/alter_table_online.result @@ -179,25 +179,27 @@ t CREATE TABLE `t` ( UNIQUE KEY `b` (`b`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci alter table t drop b, change c c serial, algorithm=copy, lock=none; -ERROR 0A000: LOCK=NONE is not supported. Reason: COPY algorithm requires a lock. Try LOCK=SHARED +ERROR 0A000: LOCK=NONE is not supported. Reason: CHANGE COLUMN ... AUTO_INCREMENT. Try LOCK=SHARED # Check existed unique keys. create or replace table t(a int, b int not null, c int not null, d int); # No unique in the old table; alter table t add unique(b, c), modify d int auto_increment, add key(d), algorithm=copy, lock=none; -ERROR 0A000: LOCK=NONE is not supported. Reason: COPY algorithm requires a lock. Try LOCK=SHARED +ERROR 0A000: LOCK=NONE is not supported. Reason: CHANGE COLUMN ... AUTO_INCREMENT. Try LOCK=SHARED alter table t add unique(a, b); # Unique in the old table has nulls; alter table t modify d int auto_increment, add key(d), algorithm=copy, lock=none; -ERROR 0A000: LOCK=NONE is not supported. Reason: COPY algorithm requires a lock. Try LOCK=SHARED +ERROR 0A000: LOCK=NONE is not supported. Reason: CHANGE COLUMN ... AUTO_INCREMENT. Try LOCK=SHARED alter table t add unique(b, c); # Change unique'scolumn;
Typo
-alter table t change b x int, modify d int auto_increment, add key(d), +alter table t change b x bigint, modify d int auto_increment, add key(d), algorithm=copy, lock=none; -ERROR 0A000: LOCK=NONE is not supported. Reason: COPY algorithm requires a lock. Try LOCK=SHARED -# Finally good. -alter table t modify d int auto_increment, add key(d), +ERROR 0A000: LOCK=NONE is not supported. Reason: CHANGE COLUMN ... AUTO_INCREMENT. Try LOCK=SHARED +# Finally good. Simple renames with a type unchenged will not affect
typo s/unchenged/unchanged/
+# the result. Also NOT NULL -> NULL transform is fine. +alter table t modify d int auto_increment, add key(d), +change b x int null, algorithm=copy, lock=none; drop table t; # MDEV-31172 Server crash or ASAN errors in online_alter_check_autoinc diff --git a/sql/sql_class.h b/sql/sql_class.h --- a/sql/sql_class.h +++ b/sql/sql_class.h @@ -1228,6 +1228,15 @@ class Query_arena { return strdup_root(mem_root,str); } inline char *strmake(const char *str, size_t size) const { return strmake_root(mem_root,str,size); } + inline LEX_CSTRING strcat(const LEX_CSTRING &a, const LEX_CSTRING &b) const + { + char *buf= (char*)alloc(a.length + b.length + 1); + if (unlikely(!buf)) + return null_clex_str; + strncpy(buf, a.str, a.length);
memcpy both ↑ and ↓ (and then you might need to append \0 explicitly)
+ strncpy(buf + a.length, b.str, b.length + 1); + return {buf, a.length + b.length}; + } inline void *memdup(const void *str, size_t size) const { return memdup_root(mem_root,str,size); } inline void *memdup_w_gap(const void *str, size_t size, size_t gap) const diff --git a/sql/sql_table.cc b/sql/sql_table.cc --- a/sql/sql_table.cc +++ b/sql/sql_table.cc @@ -9869,26 +9869,21 @@ bool online_alter_check_autoinc(const THD *thd, const Alter_info *alter_info, */ for (uint k= 0; k < table->s->keys; k++) { - const KEY &key= table->s->key_info[k]; + const KEY &key= table->key_info[k];
This is generally incorrect. User specified keys are in table->s->key_info[] Keys in table->key_info[] can be modified to match what indexes are actually created, in particular, for long uniques table->key_info[] differs from table->s->key_info[]
if ((key.flags & HA_NOSAME) == 0 || key.flags & HA_NULL_PART_KEY) continue; bool key_parts_good= true; for (uint kp= 0; kp < key.user_defined_key_parts && key_parts_good; kp++) { - const char *field_name= key.key_part[kp].field->field_name.str; - for (const auto &c: alter_info->drop_list) - if (c.type == Alter_drop::COLUMN - && my_strcasecmp(system_charset_info, c.name, field_name) == 0) - { - key_parts_good= false; - break; - } + const Field *f= key.key_part[kp].field; + // tmp_set contains dropped fields after mysql_prepare_alter_table + key_parts_good= !bitmap_is_set(&table->tmp_set, f->field_index); + if (key_parts_good) for (const auto &c: alter_info->create_list) - if (c.change.str && my_strcasecmp(system_charset_info, c.change.str, - field_name) == 0) + if (c.field == f) { - key_parts_good= false; + key_parts_good= f->is_equal(c); break; } } @@ -9896,25 +9891,63 @@ bool online_alter_check_autoinc(const THD *thd, const Alter_info *alter_info, return true; }
- const char *old_autoinc= table->found_next_number_field - ? table->found_next_number_field->field_name.str - : ""; - bool online= true; for (const auto &c: alter_info->create_list) { - if (c.change.str && c.flags & AUTO_INCREMENT_FLAG) + if (c.flags & AUTO_INCREMENT_FLAG) { - if (my_strcasecmp(system_charset_info, c.change.str, old_autoinc) != 0) + if (c.field && !(c.field->flags & AUTO_INCREMENT_FLAG)) + return false; + break; + } + } + return true; +} + +static +const char *online_alter_check_supported(const THD *thd, + const Alter_info *alter_info, + const TABLE *table, bool *online) { - if (c.create_if_not_exists // check IF EXISTS option - && table->find_field_by_name(&c.change) == NULL) - continue; - online= false; + DBUG_ASSERT(!table->s->tmp_table);
that's confusing. *online can be false here already, then return "DROP SYSTEM VERSIONING" will be wrong and you use some other condition to decide whether "DROP SYSTEM VERSIONING" is correct or not. it'd be much clearer not to call online_alter_check_supported in this case. and add here DBUG_ASSERT(*online);
+ + *online= *online && (alter_info->flags & ALTER_DROP_SYSTEM_VERSIONING) == 0;
and then you won't need to use && above, just as you don't use it below
+ if (!*online) + return "DROP SYSTEM VERSIONING"; + + *online= !thd->lex->ignore; + if (!*online) + return "ALTER IGNORE TABLE"; + + *online= !table->versioned(VERS_TRX_ID); + if (!*online) + return "BIGINT GENERATED ALWAYS AS ROW_START"; + + List
fk_list; + table->file->get_foreign_key_list(thd, &fk_list); + for (auto &fk: fk_list) + { + if (fk_modifies_child(fk.delete_method) || + fk_modifies_child(fk.update_method)) + { + *online= false; + // Don't fall to a common unsupported case to avoid heavy string ops. + if (alter_info->requested_lock == Alter_info::ALTER_TABLE_LOCK_NONE) + { + return fk_modifies_child(fk.delete_method) + ? thd->strcat({STRING_WITH_LEN("ON DELETE ")}, + *fk_option_name(fk.delete_method)).str + : thd->strcat({STRING_WITH_LEN("ON UPDATE ")}, + *fk_option_name(fk.update_method)).str; } - break; + return NULL; } } - return online; + + *online= online_alter_check_autoinc(thd, alter_info, table); + if (!*online) + return "CHANGE COLUMN ... AUTO_INCREMENT"; + + return NULL; } @@ -10979,14 +10994,14 @@ do_continue:;
if (!table->s->tmp_table) { - // COPY algorithm doesn't work with concurrent writes. + auto *reason= online_alter_check_supported(thd, alter_info, table, &online); + // COPY algorithm works with concurrent writes only when online is true.
as I wrote above, better put it inside if (online) {
if (!online && alter_info->requested_lock == Alter_info::ALTER_TABLE_LOCK_NONE) { + DBUG_ASSERT(reason); my_error(ER_ALTER_OPERATION_NOT_SUPPORTED_REASON, MYF(0), - "LOCK=NONE", - ER_THD(thd, ER_ALTER_OPERATION_NOT_SUPPORTED_REASON_COPY), - "LOCK=SHARED"); + "LOCK=NONE", reason, "LOCK=SHARED"); goto err_new_table_cleanup; }
Regards, Sergei VP of MariaDB Server Engineering and security@mariadb.org
On Sat, 24 Jun 2023 at 18:51, Sergei Golubchik
@@ -1228,6 +1228,15 @@ class Query_arena { return strdup_root(mem_root,str); } inline char *strmake(const char *str, size_t size) const { return strmake_root(mem_root,str,size); } + inline LEX_CSTRING strcat(const LEX_CSTRING &a, const LEX_CSTRING &b) const + { + char *buf= (char*)alloc(a.length + b.length + 1); + if (unlikely(!buf)) + return null_clex_str; + strncpy(buf, a.str, a.length);
memcpy both ↑ and ↓ (and then you might need to append \0 explicitly)
Ok, ↑ will be faster, but why ↓?
+ strncpy(buf + a.length, b.str, b.length + 1); + return {buf, a.length + b.length}; + } inline void *memdup(const void *str, size_t size) const { return memdup_root(mem_root,str,size); } inline void *memdup_w_gap(const void *str, size_t size, size_t gap) const diff --git a/sql/sql_table.cc b/sql/sql_table.cc --- a/sql/sql_table.cc +++ b/sql/sql_table.cc @@ -9869,26 +9869,21 @@ bool online_alter_check_autoinc(const THD *thd, const Alter_info *alter_info, */ for (uint k= 0; k < table->s->keys; k++) { - const KEY &key= table->s->key_info[k]; + const KEY &key= table->key_info[k];
This is generally incorrect. User specified keys are in table->s->key_info[] Keys in table->key_info[] can be modified to match what indexes are actually created, in particular, for long uniques table->key_info[] differs from table->s->key_info[]
Maybe, but hopefully, long unique is not HA_NOSAME. I need table->key[i].key_part[j].field to compare with copy_field.field.
@@ -9896,25 +9891,63 @@ bool online_alter_check_autoinc(const THD *thd, const Alter_info *alter_info, return true; }
- const char *old_autoinc= table->found_next_number_field - ? table->found_next_number_field->field_name.str - : ""; - bool online= true; for (const auto &c: alter_info->create_list) { - if (c.change.str && c.flags & AUTO_INCREMENT_FLAG) + if (c.flags & AUTO_INCREMENT_FLAG) { - if (my_strcasecmp(system_charset_info, c.change.str, old_autoinc) != 0) + if (c.field && !(c.field->flags & AUTO_INCREMENT_FLAG)) + return false; + break; + } + } + return true; +} + +static +const char *online_alter_check_supported(const THD *thd, + const Alter_info *alter_info, + const TABLE *table, bool *online) { - if (c.create_if_not_exists // check IF EXISTS option - && table->find_field_by_name(&c.change) == NULL) - continue; - online= false; + DBUG_ASSERT(!table->s->tmp_table);
that's confusing. *online can be false here already, then return "DROP SYSTEM VERSIONING" will be wrong and you use some other condition to decide whether "DROP SYSTEM VERSIONING" is correct or not.
it'd be much clearer not to call online_alter_check_supported in this case. and add here DBUG_ASSERT(*online);
Yes, and also some tests failed around this during rebase. I've fixed it in the new patch b16f52177a84 https://github.com/MariaDB/server/commit/b16f52177a84ae6c3b1fe4ca40c879f6c0d... Also agree with all the further following statements. -- Yours truly, Nikita Malyavin
I fixed a few more bits , commit is
https://github.com/MariaDB/server/commit/e7d1cce05f154936aefe18bec4b9a9eb376...
http://e7d1cce05f15
On Wed, 5 Jul 2023 at 20:54, Nikita Malyavin
On Sat, 24 Jun 2023 at 18:51, Sergei Golubchik
wrote: @@ -1228,6 +1228,15 @@ class Query_arena { return strdup_root(mem_root,str); } inline char *strmake(const char *str, size_t size) const { return strmake_root(mem_root,str,size); } + inline LEX_CSTRING strcat(const LEX_CSTRING &a, const LEX_CSTRING &b) const + { + char *buf= (char*)alloc(a.length + b.length + 1); + if (unlikely(!buf)) + return null_clex_str; + strncpy(buf, a.str, a.length);
memcpy both ↑ and ↓ (and then you might need to append \0 explicitly)
Ok, ↑ will be faster, but why ↓?
+ strncpy(buf + a.length, b.str, b.length + 1); + return {buf, a.length + b.length}; + } inline void *memdup(const void *str, size_t size) const { return memdup_root(mem_root,str,size); } inline void *memdup_w_gap(const void *str, size_t size, size_t gap) const diff --git a/sql/sql_table.cc b/sql/sql_table.cc --- a/sql/sql_table.cc +++ b/sql/sql_table.cc @@ -9869,26 +9869,21 @@ bool online_alter_check_autoinc(const THD *thd, const Alter_info *alter_info, */ for (uint k= 0; k < table->s->keys; k++) { - const KEY &key= table->s->key_info[k]; + const KEY &key= table->key_info[k];
This is generally incorrect. User specified keys are in table->s->key_info[] Keys in table->key_info[] can be modified to match what indexes are actually created, in particular, for long uniques table->key_info[] differs from table->s->key_info[]
Maybe, but hopefully, long unique is not HA_NOSAME. I need table->key[i].key_part[j].field to compare with copy_field.field.
@@ -9896,25 +9891,63 @@ bool online_alter_check_autoinc(const THD *thd, const Alter_info *alter_info, return true; }
- const char *old_autoinc= table->found_next_number_field - ? table->found_next_number_field->field_name.str - : ""; - bool online= true; for (const auto &c: alter_info->create_list) { - if (c.change.str && c.flags & AUTO_INCREMENT_FLAG) + if (c.flags & AUTO_INCREMENT_FLAG) { - if (my_strcasecmp(system_charset_info, c.change.str, old_autoinc) != 0) + if (c.field && !(c.field->flags & AUTO_INCREMENT_FLAG)) + return false; + break; + } + } + return true; +} + +static +const char *online_alter_check_supported(const THD *thd, + const Alter_info *alter_info, + const TABLE *table, bool *online) { - if (c.create_if_not_exists // check IF EXISTS option - && table->find_field_by_name(&c.change) == NULL) - continue; - online= false; + DBUG_ASSERT(!table->s->tmp_table);
that's confusing. *online can be false here already, then return "DROP SYSTEM VERSIONING" will be wrong and you use some other condition to decide whether "DROP SYSTEM VERSIONING" is correct or not.
it'd be much clearer not to call online_alter_check_supported in this case. and add here DBUG_ASSERT(*online);
Yes, and also some tests failed around this during rebase. I've fixed it in the new patch b16f52177a84 https://github.com/MariaDB/server/commit/b16f52177a84ae6c3b1fe4ca40c879f6c0d... Also agree with all the further following statements.
-- Yours truly, Nikita Malyavin
-- Yours truly, Nikita Malyavin
Hi, Nikita, On Jul 05, Nikita Malyavin wrote:
On Sat, 24 Jun 2023 at 18:51, Sergei Golubchik
wrote: @@ -1228,6 +1228,15 @@ class Query_arena { return strdup_root(mem_root,str); } inline char *strmake(const char *str, size_t size) const { return strmake_root(mem_root,str,size); } + inline LEX_CSTRING strcat(const LEX_CSTRING &a, const LEX_CSTRING &b) const + { + char *buf= (char*)alloc(a.length + b.length + 1); + if (unlikely(!buf)) + return null_clex_str; + strncpy(buf, a.str, a.length);
memcpy both ↑ and ↓ (and then you might need to append \0 explicitly)
Ok, ↑ will be faster, but why ↓?
LEX_CSTRING defines a string by a pointer and a length. You shouldn't use functions that stop at first '\0'. In some contexts (for table names, for example) it might be ok, but here you create a generic LEX_CSTRING concatenation, let's not implicitly assume that there are not zero bytes inside.
+ strncpy(buf + a.length, b.str, b.length + 1); + return {buf, a.length + b.length}; + } inline void *memdup(const void *str, size_t size) const { return memdup_root(mem_root,str,size); } inline void *memdup_w_gap(const void *str, size_t size, size_t gap) const diff --git a/sql/sql_table.cc b/sql/sql_table.cc --- a/sql/sql_table.cc +++ b/sql/sql_table.cc @@ -9869,26 +9869,21 @@ bool online_alter_check_autoinc(const THD *thd, const Alter_info *alter_info, */ for (uint k= 0; k < table->s->keys; k++) { - const KEY &key= table->s->key_info[k]; + const KEY &key= table->key_info[k];
This is generally incorrect. User specified keys are in table->s->key_info[] Keys in table->key_info[] can be modified to match what indexes are actually created, in particular, for long uniques table->key_info[] differs from table->s->key_info[]
Maybe, but hopefully, long unique is not HA_NOSAME. I need table->key[i].key_part[j].field to compare with copy_field.field.
can you compare indices instead? key[i].key_part[j].field->field_index == copy_field.field->field_index Regards, Sergei VP of MariaDB Server Engineering and security@mariadb.org
On Thu, 6 Jul 2023 at 01:22, Sergei Golubchik
LEX_CSTRING defines a string by a pointer and a length. You shouldn't use functions that stop at first '\0'.
In some contexts (for table names, for example) it might be ok, but here you create a generic LEX_CSTRING concatenation, let's not implicitly assume that there are not zero bytes inside.
Ouch! Right, there are more encodings than I can encounter. In fact, i don't even know if 0 can be met in any sort of utf.
@@ -9869,26 +9869,21 @@ bool online_alter_check_autoinc(const THD *thd, const Alter_info *alter_info,
*/ for (uint k= 0; k < table->s->keys; k++) { - const KEY &key= table->s->key_info[k]; + const KEY &key= table->key_info[k];
This is generally incorrect. User specified keys are in table->s->key_info[] Keys in table->key_info[] can be modified to match what indexes are actually created, in particular, for long uniques table->key_info[] differs from table->s->key_info[]
Maybe, but hopefully, long unique is not HA_NOSAME. I need table->key[i].key_part[j].field to compare with copy_field.field.
can you compare indices instead?
I can, but why do you suppose table->s->key_info is the correct one? Rows_log_event::find_key enumerates table->key-info to find a unique key.
It only checks m_table->s->keys_in_use.is_set(i). Though it seems unrelated, I suppose I could have to check it as well, but I couldn't find what drives its values apart from ALTER TABLE DISABLE KEYS, which skips unique keys. What do you think?
Regards, Sergei VP of MariaDB Server Engineering and security@mariadb.org
-- Yours truly, Nikita Malyavin
On Thu, 2023-07-06 at 01:48 +0400, Nikita Malyavin via developers wrote:
On Thu, 6 Jul 2023 at 01:22, Sergei Golubchik
wrote: LEX_CSTRING defines a string by a pointer and a length. You shouldn't use functions that stop at first '\0'.
In some contexts (for table names, for example) it might be ok, but here you create a generic LEX_CSTRING concatenation, let's not implicitly assume that there are not zero bytes inside.
Ouch! Right, there are more encodings than I can encounter. In fact, i don't even know if 0 can be met in any sort of utf.
UTF-16 certainly *does* include 0 bytes all over the place: $ echo -n "Hello" | iconv -f utf8 -t utf16 | hd 00000000 00 48 00 65 00 6c 00 6c 00 6f |.H.e.l.l.o| 0000000a By its ASCII-compatible design, UTF-8 does not encode 0 bytes except when encoding the 0 code point (https://stackoverflow.com/a/6907327).
Hi, Nikita, On Jul 06, Nikita Malyavin wrote:
@@ -9869,26 +9869,21 @@ bool online_alter_check_autoinc
*/ for (uint k= 0; k < table->s->keys; k++) { - const KEY &key= table->s->key_info[k]; + const KEY &key= table->key_info[k];
This is generally incorrect. User specified keys are in table->s->key_info[] Keys in table->key_info[] can be modified to match what indexes are actually created, in particular, for long uniques table->key_info[] differs from table->s->key_info[]
Maybe, but hopefully, long unique is not HA_NOSAME. I need table->key[i].key_part[j].field to compare with copy_field.field.
can you compare indices instead?
I can, but why do you suppose table->s->key_info is the correct one?
Because that's how the code works. TABLE_SHARE stores index definitions as specified by the user, as shown in SHOW CREATE TABLE and I_S tables. TABLE stores index definitions as seen by the engine. Which is often (almost always) the same thing. But only almost.
Rows_log_event::find_key enumerates table->key-info to find a unique key.
It only checks m_table->s->keys_in_use.is_set(i). Though it seems unrelated, I suppose I could have to check it as well, but I couldn't find what drives its values apart from ALTER TABLE DISABLE KEYS, which skips unique keys. What do you think?
Yes, as far as I remember, keys_in_use show what keys are not disabled. Regards, Sergei VP of MariaDB Server Engineering and security@mariadb.org
Hi, Nikita,
On Jul 06, Nikita Malyavin wrote:
@@ -9869,26 +9869,21 @@ bool online_alter_check_autoinc
*/ for (uint k= 0; k < table->s->keys; k++) { - const KEY &key= table->s->key_info[k]; + const KEY &key= table->key_info[k];
This is generally incorrect. User specified keys are in table->s->key_info[] Keys in table->key_info[] can be modified to match what indexes are actually created, in particular, for long uniques table->key_info[] differs from table->s->key_info[]
Maybe, but hopefully, long unique is not HA_NOSAME. I need table->key[i].key_part[j].field to compare with copy_field.field.
can you compare indices instead?
I can, but why do you suppose table->s->key_info is the correct one?
Because that's how the code works. TABLE_SHARE stores index definitions as specified by the user, as shown in SHOW CREATE TABLE and I_S tables.
And why would I need them as specified by the user?? All I need is to make sure
On Thu, 6 Jul 2023 at 20:06, Sergei Golubchik
TABLE stores index definitions as seen by the engine. Which is often (almost always) the same thing. But only almost.
i didn't know about it when i wrote it first. Disregarding that, I think table->key_info is a correct thing to use here, as it reflects the data that is really stored and can be used for the row identification -- Yours truly, Nikita Malyavin
participants (3)
-
Lenski, Daniel
-
Nikita Malyavin
-
Sergei Golubchik