Skip to content

Commit 58780b5

Browse files
committed
MDEV-22775 [HY000][1553] Changing name of primary key column with foreign key constraint fails.
Problem: The problem happened because of a conceptual flaw in the server code: a. The table level CHARSET/COLLATE clause affected all data types, including numeric and temporal ones: CREATE TABLE t1 (a INT) CHARACTER SET utf8 [COLLATE utf8_general_ci]; In the above example, the Column_definition_attributes (and then the FRM record) for the column "a" erroneously inherited "utf8" as its character set. b. The "ALTER TABLE t1 CONVERT TO CHARACTER SET csname" statement also erroneously affected Column_definition_attributes::charset for numeric and temporal data types and wrote "csname" as their character set into FRM files. So now we have arbitrary non-relevant charset ID values for numeric and temporal data types in all FRM files in the world :) The code in the server and the other engines did not seem to be affected by this flaw. Only InnoDB inplace ALTER was affected. Solution: Fixing the code in the way that only character string data types (CHAR,VARCHAR,TEXT,ENUM,SET): - inherit the table level CHARSET/COLLATE clause - get the charset value according to "CONVERT TO CHARACTER SET csname". Numeric and temporal data types now always get &my_charset_numeric in Column_definition_attributes::charset and always write its ID into FRM files: - no matter what the table level CHARSET/COLLATE clause is, and - no matter what "CONVERT TO CHARACTER SET" says. Details: 1. Adding helper classes to pass small parts of HA_CREATE_INFO into Type_handler methods: - Column_derived_attributes - to pass table level CHARSET/COLLATE, so columns that do not have explicit CHARSET/COLLATE clauses can derive them from the table level, e.g. CREATE TABLE t1 (a VARCHAR(1), b CHAR(1)) CHARACTER SET utf8; - Column_bulk_alter_attributes - to pass bulk attribute changes generated by the ALTER related code. These bulk changes affect multiple columns at the same time: ALTER TABLE ... CONVERT TO CHARACTER SET csname; Note, passing the whole HA_CREATE_INFO directly to Type_handler would not be good: HA_CREATE_INFO is huge and would need not desired dependencies in sql_type.h and sql_type.cc. The Type_handler API should use smallest possible data types! 2. Type_handler::Column_definition_prepare_stage1() is now responsible to set Column_definition::charset properly, according to the data type, for example: - For string data types, Column_definition_attributes::charset is set from the table level CHARSET/COLLATE clause (if not specified explicitly in the column definition). - For numeric and temporal fields, Column_definition_attributes::charset is set to &my_charset_numeric, no matter what the table level CHARSET/COLLATE says. - For GEOMETRY, Column_definition_attributes::charset is set to &my_charset_bin, no matter what the table level CHARSET/COLLATE says. Previously this code (setting `charset`) was outside of of Column_definition_prepare_stage1(), namely in mysql_prepare_create_table(), and was erroneously called for all data types. 3. Adding Type_handler::Column_definition_bulk_alter(), to handle "ALTER TABLE .. CONVERT TO". Previously this code was inside get_sql_field_charset() and was erroneously called for all data types. 4. Removing the Schema_specification_st parameter from Type_handler::Column_definition_redefine_stage1(). Column_definition_attributes::charset is now fully properly initialized by Column_definition_prepare_stage1(). So we don't need access to the table level CHARSET/COLLATE clause in Column_definition_redefine_stage1() any more. 5. Other changes: - Removing global function get_sql_field_charset() - Moving the part of the former get_sql_field_charset(), which was responsible to inherit the table level CHARSET/COLLATE clause to new methods: -- Column_definition_attributes::explicit_or_derived_charset() and -- Column_definition::prepare_charset_for_string(). This code is only needed for string data types. Previously it was erroneously called for all data types. - Moving another part, which was responsible to apply the "CONVERT TO" clause, to Type_handler_general_purpose_string::Column_definition_bulk_alter(). - Replacing the call for get_sql_field_charset() in sql_partition.cc to sql_field->explicit_or_derived_charset() - it is perfectly enough. The old code was redundant: get_sql_field_charset() was called from sql_partition.cc only when there were no a "CONVERT TO CHARACTER SET" clause involved, so its purpose was only to inherit the table level CHARSET/COLLATE clause. - Moving the code handling the BINCMP_FLAG flag from mysql_prepare_create_table() to Column_definition::prepare_charset_for_string(): This code is responsible to resolve the BINARY comparison style into the corresponding _bin collation, to do the following transparent rewrite: CREATE TABLE t1 (a VARCHAR(10) BINARY) CHARSET utf8; -> CREATE TABLE t1 (a VARCHAR(10) CHARACTER SET utf8 COLLATE utf8_bin); This code is only needed for string data types. Previously it was erroneously called for all data types. 6. Renaming Table_scope_and_contents_source_pod_st::table_charset to alter_table_convert_to_charset, because the only purpose it's used for is handlering "ALTER .. CONVERT". The new name is much more self-descriptive.
1 parent f69c1c9 commit 58780b5

File tree

12 files changed

+338
-118
lines changed

12 files changed

+338
-118
lines changed

mysql-test/suite/innodb/r/instant_alter_charset.result

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2032,3 +2032,14 @@ ALTER TABLE t1 MODIFY a VARCHAR(2)
20322032
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
20332033
INSERT INTO t1 VALUES ('a');
20342034
DROP TABLE t1;
2035+
#
2036+
# MDEV-22775 [HY000][1553] Changing name of primary key column with foreign key constraint fails.
2037+
#
2038+
create table t1 (id int primary key) engine=innodb default charset=utf8;
2039+
create table t2 (input_id int primary key, id int not null,
2040+
key a (id),
2041+
constraint a foreign key (id) references t1 (id)
2042+
)engine=innodb default charset=utf8;
2043+
alter table t1 change id id2 int;
2044+
drop table t2;
2045+
drop table t1;

mysql-test/suite/innodb/t/instant_alter_charset.test

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -843,3 +843,17 @@ ALTER TABLE t1 MODIFY a VARCHAR(2)
843843
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
844844
INSERT INTO t1 VALUES ('a');
845845
DROP TABLE t1;
846+
847+
848+
--echo #
849+
--echo # MDEV-22775 [HY000][1553] Changing name of primary key column with foreign key constraint fails.
850+
--echo #
851+
852+
create table t1 (id int primary key) engine=innodb default charset=utf8;
853+
create table t2 (input_id int primary key, id int not null,
854+
key a (id),
855+
constraint a foreign key (id) references t1 (id)
856+
)engine=innodb default charset=utf8;
857+
alter table t1 change id id2 int;
858+
drop table t2;
859+
drop table t1;

sql/field.cc

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11076,16 +11076,26 @@ Column_definition::Column_definition(THD *thd, Field *old_field,
1107611076
CREATE TABLE t1 (a INT) AS SELECT a FROM t2;
1107711077
See Type_handler::Column_definition_redefine_stage1()
1107811078
for data type specific code.
11079+
11080+
@param this - The field definition corresponding to the expression
11081+
in the "AS SELECT.." part.
11082+
11083+
@param dup_field - The field definition from the "CREATE TABLE (...)" part.
11084+
It has already underwent prepare_stage1(), so
11085+
must be fully initialized:
11086+
-- dup_field->charset is set and BINARY
11087+
sorting style is applied, see find_bin_collation().
11088+
11089+
@param file - The table handler
1107911090
*/
1108011091
void
1108111092
Column_definition::redefine_stage1_common(const Column_definition *dup_field,
11082-
const handler *file,
11083-
const Schema_specification_st *schema)
11093+
const handler *file)
1108411094
{
1108511095
set_handler(dup_field->type_handler());
1108611096
default_value= dup_field->default_value;
11087-
charset= dup_field->charset ? dup_field->charset :
11088-
schema->default_table_charset;
11097+
DBUG_ASSERT(dup_field->charset); // Set by prepare_stage1()
11098+
charset= dup_field->charset;
1108911099
length= dup_field->char_length;
1109011100
pack_length= dup_field->pack_length;
1109111101
key_length= dup_field->key_length;

sql/field.h

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4631,6 +4631,11 @@ class Column_definition_attributes
46314631
void frm_pack_charset(uchar *buff) const;
46324632
void frm_unpack_basic(const uchar *buff);
46334633
bool frm_unpack_charset(TABLE_SHARE *share, const uchar *buff);
4634+
CHARSET_INFO *explicit_or_derived_charset(const Column_derived_attributes
4635+
*derived_attr) const
4636+
{
4637+
return charset ? charset : derived_attr->charset();
4638+
}
46344639
};
46354640

46364641

@@ -4765,6 +4770,15 @@ class Column_definition: public Sql_alloc,
47654770
void create_length_to_internal_length_bit();
47664771
void create_length_to_internal_length_newdecimal();
47674772

4773+
/*
4774+
Prepare the "charset" member for string data types,
4775+
such as CHAR, VARCHAR, TEXT, ENUM, SET:
4776+
- derive the charset if not specified explicitly
4777+
- find a _bin collation if the BINARY comparison style was specified, e.g.:
4778+
CREATE TABLE t1 (a VARCHAR(10) BINARY) CHARSET utf8;
4779+
*/
4780+
bool prepare_charset_for_string(const Column_derived_attributes *dattr);
4781+
47684782
/**
47694783
Prepare a SET/ENUM field.
47704784
Create "interval" from "interval_list" if needed, and adjust "length".
@@ -4800,23 +4814,33 @@ class Column_definition: public Sql_alloc,
48004814
bool sp_prepare_create_field(THD *thd, MEM_ROOT *mem_root);
48014815

48024816
bool prepare_stage1(THD *thd, MEM_ROOT *mem_root,
4803-
handler *file, ulonglong table_flags);
4817+
handler *file, ulonglong table_flags,
4818+
const Column_derived_attributes *derived_attr);
4819+
void prepare_stage1_simple(CHARSET_INFO *cs)
4820+
{
4821+
charset= cs;
4822+
create_length_to_internal_length_simple();
4823+
}
48044824
bool prepare_stage1_typelib(THD *thd, MEM_ROOT *mem_root,
48054825
handler *file, ulonglong table_flags);
48064826
bool prepare_stage1_string(THD *thd, MEM_ROOT *mem_root,
48074827
handler *file, ulonglong table_flags);
48084828
bool prepare_stage1_bit(THD *thd, MEM_ROOT *mem_root,
48094829
handler *file, ulonglong table_flags);
48104830

4831+
bool bulk_alter(const Column_derived_attributes *derived_attr,
4832+
const Column_bulk_alter_attributes *bulk_attr)
4833+
{
4834+
return type_handler()->Column_definition_bulk_alter(this,
4835+
derived_attr,
4836+
bulk_attr);
4837+
}
48114838
void redefine_stage1_common(const Column_definition *dup_field,
4812-
const handler *file,
4813-
const Schema_specification_st *schema);
4814-
bool redefine_stage1(const Column_definition *dup_field, const handler *file,
4815-
const Schema_specification_st *schema)
4839+
const handler *file);
4840+
bool redefine_stage1(const Column_definition *dup_field, const handler *file)
48164841
{
48174842
const Type_handler *handler= dup_field->type_handler();
4818-
return handler->Column_definition_redefine_stage1(this, dup_field,
4819-
file, schema);
4843+
return handler->Column_definition_redefine_stage1(this, dup_field, file);
48204844
}
48214845
bool prepare_stage2(handler *handler, ulonglong table_flags);
48224846
bool prepare_stage2_blob(handler *handler,

sql/handler.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2092,7 +2092,7 @@ struct Vers_parse_info: public Table_period_info
20922092

20932093
struct Table_scope_and_contents_source_pod_st // For trivial members
20942094
{
2095-
CHARSET_INFO *table_charset;
2095+
CHARSET_INFO *alter_table_convert_to_charset;
20962096
LEX_CUSTRING tabledef_version;
20972097
LEX_CSTRING connect_string;
20982098
LEX_CSTRING comment;
@@ -2237,7 +2237,7 @@ struct HA_CREATE_INFO: public Table_scope_and_contents_source_st,
22372237
DBUG_ASSERT(cs);
22382238
if (check_conflicting_charset_declarations(cs))
22392239
return true;
2240-
table_charset= default_table_charset= cs;
2240+
alter_table_convert_to_charset= default_table_charset= cs;
22412241
used_fields|= (HA_CREATE_USED_CHARSET | HA_CREATE_USED_DEFAULT_CHARSET);
22422242
return false;
22432243
}

sql/sql_partition.cc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2388,6 +2388,8 @@ static int add_column_list_values(String *str, partition_info *part_info,
23882388
*/
23892389
if (create_info)
23902390
{
2391+
const Column_derived_attributes
2392+
derived_attr(create_info->default_table_charset);
23912393
Create_field *sql_field;
23922394

23932395
if (!(sql_field= get_sql_field(field_name,
@@ -2402,7 +2404,7 @@ static int add_column_list_values(String *str, partition_info *part_info,
24022404
&need_cs_check))
24032405
return 1;
24042406
if (need_cs_check)
2405-
field_cs= get_sql_field_charset(sql_field, create_info);
2407+
field_cs= sql_field->explicit_or_derived_charset(&derived_attr);
24062408
else
24072409
field_cs= NULL;
24082410
}

sql/sql_table.cc

Lines changed: 33 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2926,6 +2926,15 @@ bool check_duplicates_in_interval(const char *set_or_name,
29262926
}
29272927

29282928

2929+
bool Column_definition::
2930+
prepare_charset_for_string(const Column_derived_attributes *dattr)
2931+
{
2932+
if (!charset)
2933+
charset= dattr->charset();
2934+
return (flags & BINCMP_FLAG) && !(charset= find_bin_collation(charset));
2935+
}
2936+
2937+
29292938
bool Column_definition::prepare_stage2_blob(handler *file,
29302939
ulonglong table_flags,
29312940
uint field_flags)
@@ -3007,38 +3016,6 @@ bool Column_definition::prepare_stage2(handler *file,
30073016
}
30083017

30093018

3010-
/*
3011-
Get character set from field object generated by parser using
3012-
default values when not set.
3013-
3014-
SYNOPSIS
3015-
get_sql_field_charset()
3016-
sql_field The sql_field object
3017-
create_info Info generated by parser
3018-
3019-
RETURN VALUES
3020-
cs Character set
3021-
*/
3022-
3023-
CHARSET_INFO* get_sql_field_charset(Column_definition *sql_field,
3024-
HA_CREATE_INFO *create_info)
3025-
{
3026-
CHARSET_INFO *cs= sql_field->charset;
3027-
3028-
if (!cs)
3029-
cs= create_info->default_table_charset;
3030-
/*
3031-
table_charset is set only in ALTER TABLE t1 CONVERT TO CHARACTER SET csname
3032-
if we want change character set for all varchar/char columns.
3033-
But the table charset must not affect the BLOB fields, so don't
3034-
allow to change my_charset_bin to somethig else.
3035-
*/
3036-
if (create_info->table_charset && cs != &my_charset_bin)
3037-
cs= create_info->table_charset;
3038-
return cs;
3039-
}
3040-
3041-
30423019
/**
30433020
Modifies the first column definition whose SQL type is TIMESTAMP
30443021
by adding the features DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP.
@@ -3211,11 +3188,14 @@ bool Column_definition::prepare_stage1_bit(THD *thd,
32113188
bool Column_definition::prepare_stage1(THD *thd,
32123189
MEM_ROOT *mem_root,
32133190
handler *file,
3214-
ulonglong table_flags)
3191+
ulonglong table_flags,
3192+
const Column_derived_attributes
3193+
*derived_attr)
32153194
{
32163195
return type_handler()->Column_definition_prepare_stage1(thd, mem_root,
32173196
this, file,
3218-
table_flags);
3197+
table_flags,
3198+
derived_attr);
32193199
}
32203200

32213201

@@ -3428,6 +3408,9 @@ mysql_prepare_create_table(THD *thd, HA_CREATE_INFO *create_info,
34283408
int select_field_count= C_CREATE_SELECT(create_table_mode);
34293409
bool tmp_table= create_table_mode == C_ALTER_TABLE;
34303410
bool is_hash_field_needed= false;
3411+
const Column_derived_attributes dattr(create_info->default_table_charset);
3412+
const Column_bulk_alter_attributes
3413+
battr(create_info->alter_table_convert_to_charset);
34313414
DBUG_ENTER("mysql_prepare_create_table");
34323415

34333416
DBUG_EXECUTE_IF("test_pseudo_invisible",{
@@ -3484,26 +3467,27 @@ mysql_prepare_create_table(THD *thd, HA_CREATE_INFO *create_info,
34843467

34853468
for (field_no=0; (sql_field=it++) ; field_no++)
34863469
{
3470+
/* Virtual fields are always NULL */
3471+
if (sql_field->vcol_info)
3472+
sql_field->flags&= ~NOT_NULL_FLAG;
3473+
34873474
/*
34883475
Initialize length from its original value (number of characters),
34893476
which was set in the parser. This is necessary if we're
34903477
executing a prepared statement for the second time.
34913478
*/
34923479
sql_field->length= sql_field->char_length;
3493-
/* Set field charset. */
3494-
sql_field->charset= get_sql_field_charset(sql_field, create_info);
3495-
if ((sql_field->flags & BINCMP_FLAG) &&
3496-
!(sql_field->charset= find_bin_collation(sql_field->charset)))
3497-
DBUG_RETURN(true);
34983480

3499-
/* Virtual fields are always NULL */
3500-
if (sql_field->vcol_info)
3501-
sql_field->flags&= ~NOT_NULL_FLAG;
3481+
if (sql_field->bulk_alter(&dattr, &battr))
3482+
DBUG_RETURN(true);
35023483

35033484
if (sql_field->prepare_stage1(thd, thd->mem_root,
3504-
file, file->ha_table_flags()))
3485+
file, file->ha_table_flags(),
3486+
&dattr))
35053487
DBUG_RETURN(true);
35063488

3489+
DBUG_ASSERT(sql_field->charset);
3490+
35073491
if (sql_field->real_field_type() == MYSQL_TYPE_BIT &&
35083492
file->ha_table_flags() & HA_CAN_BIT_FIELD)
35093493
total_uneven_bit_length+= sql_field->length & 7;
@@ -3554,7 +3538,7 @@ mysql_prepare_create_table(THD *thd, HA_CREATE_INFO *create_info,
35543538
if (!(sql_field->flags & NOT_NULL_FLAG))
35553539
null_fields--;
35563540

3557-
if (sql_field->redefine_stage1(dup_field, file, create_info))
3541+
if (sql_field->redefine_stage1(dup_field, file))
35583542
DBUG_RETURN(true);
35593543

35603544
it2.remove(); // Remove first (create) definition
@@ -4569,7 +4553,9 @@ bool Column_definition::prepare_blob_field(THD *thd)
45694553

45704554
bool Column_definition::sp_prepare_create_field(THD *thd, MEM_ROOT *mem_root)
45714555
{
4572-
return prepare_stage1(thd, mem_root, NULL, HA_CAN_GEOMETRY) ||
4556+
DBUG_ASSERT(charset);
4557+
const Column_derived_attributes dattr(&my_charset_bin);
4558+
return prepare_stage1(thd, mem_root, NULL, HA_CAN_GEOMETRY, &dattr) ||
45734559
prepare_stage2(NULL, HA_CAN_GEOMETRY);
45744560
}
45754561

@@ -11331,8 +11317,8 @@ bool Sql_cmd_create_table_like::execute(THD *thd)
1133111317
{
1133211318
create_info.used_fields&= ~HA_CREATE_USED_CHARSET;
1133311319
create_info.used_fields|= HA_CREATE_USED_DEFAULT_CHARSET;
11334-
create_info.default_table_charset= create_info.table_charset;
11335-
create_info.table_charset= 0;
11320+
create_info.default_table_charset= create_info.alter_table_convert_to_charset;
11321+
create_info.alter_table_convert_to_charset= 0;
1133611322
}
1133711323

1133811324
/*

sql/sql_table.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,8 +252,6 @@ bool quick_rm_table(THD *thd, handlerton *base, const LEX_CSTRING *db,
252252
const char *table_path=0);
253253
void close_cached_table(THD *thd, TABLE *table);
254254
void sp_prepare_create_field(THD *thd, Column_definition *sql_field);
255-
CHARSET_INFO* get_sql_field_charset(Column_definition *sql_field,
256-
HA_CREATE_INFO *create_info);
257255
bool mysql_write_frm(ALTER_PARTITION_PARAM_TYPE *lpt, uint flags);
258256
int write_bin_log(THD *thd, bool clear_error,
259257
char const *query, ulong query_length,

0 commit comments

Comments
 (0)