diff --git a/mysql-test/main/generate_invisible_primary_key.result b/mysql-test/main/generate_invisible_primary_key.result new file mode 100644 index 0000000000000..3ce05c4b19489 --- /dev/null +++ b/mysql-test/main/generate_invisible_primary_key.result @@ -0,0 +1,45 @@ +CREATE TABLE t0 (c1 INT, c2 INT); +SHOW INDEX FROM t0; +Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored +SET sql_generate_invisible_primary_key=ON; +CREATE TABLE t1 (c1 INT, c2 INT); +SHOW INDEX FROM t1; +Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored +INSERT INTO t1 VALUES (3, 0); +INSERT INTO t1 VALUES (4, 0); +SELECT _rowid, t1.* FROM t1; +_rowid c1 c2 +1 3 0 +2 4 0 +ALTER TABLE t1 ADD c3 INT; +SHOW INDEX FROM t1; +Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored +ALTER TABLE t1 ADD _inv_PK INT; +ALTER TABLE t1 ADD _inv_PK1 INT; +SHOW INDEX FROM t1; +Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored +ALTER TABLE t1 DROP PRIMARY KEY; +ERROR 42000: Can't DROP INDEX `PRIMARY`; check that it exists +ALTER TABLE t1 ADD PRIMARY KEY (c1); +SHOW INDEX FROM t1; +Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored +t1 0 PRIMARY 1 c1 A 2 NULL NULL BTREE NO +SELECT _rowid, t1.* FROM t1; +_rowid c1 c2 c3 _inv_PK _inv_PK1 +3 3 0 NULL NULL NULL +4 4 0 NULL NULL NULL +ALTER TABLE t1 DROP PRIMARY KEY; +CREATE TABLE t2 (c1 INT, c2 INT, _inv_PK INT); +SHOW INDEX FROM t2; +Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored +CREATE TABLE t3 (c1 INT, c2 INT); +SHOW INDEX FROM t3; +Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored +CREATE TABLE t4 (_inv_PK SERIAL INVISIBLE PRIMARY KEY, c2 INT); +ALTER TABLE t4 ADD PRIMARY KEY (c2); +ERROR 42000: Multiple primary key defined +SHOW INDEX FROM t4; +Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored +t4 0 PRIMARY 1 _inv_PK A 0 NULL NULL BTREE NO +DROP TABLE t0, t1, t2, t3, t4; +SET sql_generate_invisible_primary_key=OFF; diff --git a/mysql-test/main/generate_invisible_primary_key.test b/mysql-test/main/generate_invisible_primary_key.test new file mode 100644 index 0000000000000..eca7d6aa9c6c4 --- /dev/null +++ b/mysql-test/main/generate_invisible_primary_key.test @@ -0,0 +1,75 @@ +# +# Test of sql_generate_invisible_primary_key option without debug (MDEV-21181) +# + +# +# When option is off, invisible PK is not generated +# +CREATE TABLE t0 (c1 INT, c2 INT); +SHOW INDEX FROM t0; + +SET sql_generate_invisible_primary_key=ON; + +# +# Check that PK index gets created when not specified in CREATE TABLE +# +CREATE TABLE t1 (c1 INT, c2 INT); +SHOW INDEX FROM t1; + +INSERT INTO t1 VALUES (3, 0); +INSERT INTO t1 VALUES (4, 0); +SELECT _rowid, t1.* FROM t1; + +# +# Check that generated PK persists on ALTER TABLE without new PK +# +ALTER TABLE t1 ADD c3 INT; +SHOW INDEX FROM t1; + +# +# Check that adding column with name of generated PK succeeds +# +ALTER TABLE t1 ADD _inv_PK INT; +ALTER TABLE t1 ADD _inv_PK1 INT; +SHOW INDEX FROM t1; + +# +# Trying to drop autogenerated PK should fail +# +--error ER_CANT_DROP_FIELD_OR_KEY +ALTER TABLE t1 DROP PRIMARY KEY; + +# +# Check that generated PK is dropped on ALTER TABLE with new PK +# +ALTER TABLE t1 ADD PRIMARY KEY (c1); +SHOW INDEX FROM t1; +SELECT _rowid, t1.* FROM t1; + +# +# Check that user PK that replaced generated PK can be dropped +# +ALTER TABLE t1 DROP PRIMARY KEY; + +# +# Check that creating table with column named _inv_PK succeeds +# +CREATE TABLE t2 (c1 INT, c2 INT, _inv_PK INT); +SHOW INDEX FROM t2; + +# +# Generated PK should be fully invisible to user +# +CREATE TABLE t3 (c1 INT, c2 INT); +SHOW INDEX FROM t3; + +# +# Manually defined invisible primary key is not automatically dropped +# +CREATE TABLE t4 (_inv_PK SERIAL INVISIBLE PRIMARY KEY, c2 INT); +--error ER_MULTIPLE_PRI_KEY +ALTER TABLE t4 ADD PRIMARY KEY (c2); +SHOW INDEX FROM t4; + +DROP TABLE t0, t1, t2, t3, t4; +SET sql_generate_invisible_primary_key=OFF; diff --git a/mysql-test/main/generate_invisible_primary_key_debug.result b/mysql-test/main/generate_invisible_primary_key_debug.result new file mode 100644 index 0000000000000..4c77fe0d51aec --- /dev/null +++ b/mysql-test/main/generate_invisible_primary_key_debug.result @@ -0,0 +1,57 @@ +SET SESSION debug_dbug="+d,test_invisible_index,test_completely_invisible"; +CREATE TABLE t0 (c1 INT, c2 INT); +SHOW INDEX FROM t0; +Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored +t0 1 invisible 1 invisible A NULL NULL NULL YES BTREE NO +SET sql_generate_invisible_primary_key=ON; +CREATE TABLE t1 (c1 INT, c2 INT); +SHOW INDEX FROM t1; +Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored +t1 0 PRIMARY 1 _inv_PK A 0 NULL NULL BTREE NO +t1 1 invisible 1 invisible A NULL NULL NULL YES BTREE NO +INSERT INTO t1 VALUES (3, 0); +INSERT INTO t1 VALUES (4, 0); +SELECT _rowid, t1.* FROM t1; +_rowid c1 c2 +1 3 0 +2 4 0 +ALTER TABLE t1 ADD c3 INT; +SHOW INDEX FROM t1; +Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored +t1 1 invisible 1 invisible A NULL NULL NULL YES BTREE NO +t1 0 PRIMARY 1 _inv_PK A 2 NULL NULL BTREE NO +ALTER TABLE t1 ADD _inv_PK INT; +ALTER TABLE t1 ADD _inv_PK1 INT; +SHOW INDEX FROM t1; +Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored +t1 1 invisible 1 invisible A NULL NULL NULL YES BTREE NO +t1 0 PRIMARY 1 _inv_PK2 A 2 NULL NULL BTREE NO +ALTER TABLE t1 DROP PRIMARY KEY; +ERROR 42000: Can't DROP INDEX `PRIMARY`; check that it exists +ALTER TABLE t1 ADD PRIMARY KEY (c1); +SHOW INDEX FROM t1; +Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored +t1 0 PRIMARY 1 c1 A 2 NULL NULL BTREE NO +t1 1 invisible 1 invisible A NULL NULL NULL YES BTREE NO +SELECT _rowid, t1.* FROM t1; +_rowid c1 c2 c3 _inv_PK _inv_PK1 +3 3 0 NULL NULL NULL +4 4 0 NULL NULL NULL +ALTER TABLE t1 DROP PRIMARY KEY; +CREATE TABLE t2 (c1 INT, c2 INT, _inv_PK INT); +SHOW INDEX FROM t2; +Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored +t2 0 PRIMARY 1 _inv_PK1 A 0 NULL NULL BTREE NO +t2 1 invisible 1 invisible A NULL NULL NULL YES BTREE NO +SET SESSION debug_dbug=""; +CREATE TABLE t3 (c1 INT, c2 INT); +SHOW INDEX FROM t3; +Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored +CREATE TABLE t4 (_inv_PK SERIAL INVISIBLE PRIMARY KEY, c2 INT); +ALTER TABLE t4 ADD PRIMARY KEY (c2); +ERROR 42000: Multiple primary key defined +SHOW INDEX FROM t4; +Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored +t4 0 PRIMARY 1 _inv_PK A 0 NULL NULL BTREE NO +DROP TABLE t0, t1, t2, t3, t4; +SET sql_generate_invisible_primary_key=OFF; diff --git a/mysql-test/main/generate_invisible_primary_key_debug.test b/mysql-test/main/generate_invisible_primary_key_debug.test new file mode 100644 index 0000000000000..fc733ee3691d1 --- /dev/null +++ b/mysql-test/main/generate_invisible_primary_key_debug.test @@ -0,0 +1,80 @@ +# +# Test of sql_generate_invisible_primary_key option (MDEV-21181) +# +--source include/have_debug.inc + +SET SESSION debug_dbug="+d,test_invisible_index,test_completely_invisible"; + +# +# When option is off, invisible PK is not generated +# +CREATE TABLE t0 (c1 INT, c2 INT); +SHOW INDEX FROM t0; + +SET sql_generate_invisible_primary_key=ON; + +# +# Check that PK index gets created when not specified in CREATE TABLE +# +CREATE TABLE t1 (c1 INT, c2 INT); +SHOW INDEX FROM t1; + +INSERT INTO t1 VALUES (3, 0); +INSERT INTO t1 VALUES (4, 0); +SELECT _rowid, t1.* FROM t1; + +# +# Check that generated PK persists on ALTER TABLE without new PK +# +ALTER TABLE t1 ADD c3 INT; +SHOW INDEX FROM t1; + +# +# Check that adding column with name of generated PK succeeds +# +ALTER TABLE t1 ADD _inv_PK INT; +ALTER TABLE t1 ADD _inv_PK1 INT; +SHOW INDEX FROM t1; + +# +# Trying to drop autogenerated PK should fail +# +--error ER_CANT_DROP_FIELD_OR_KEY +ALTER TABLE t1 DROP PRIMARY KEY; + +# +# Check that generated PK is dropped on ALTER TABLE with new PK +# +ALTER TABLE t1 ADD PRIMARY KEY (c1); +SHOW INDEX FROM t1; +SELECT _rowid, t1.* FROM t1; + +# +# Check that user PK that replaced generated PK can be dropped +# +ALTER TABLE t1 DROP PRIMARY KEY; + +# +# Check that creating table with column named _inv_PK succeeds +# +CREATE TABLE t2 (c1 INT, c2 INT, _inv_PK INT); +SHOW INDEX FROM t2; + +SET SESSION debug_dbug=""; + +# +# Generated PK should be fully invisible to user +# +CREATE TABLE t3 (c1 INT, c2 INT); +SHOW INDEX FROM t3; + +# +# Manually defined invisible primary key is not automatically dropped +# +CREATE TABLE t4 (_inv_PK SERIAL INVISIBLE PRIMARY KEY, c2 INT); +--error ER_MULTIPLE_PRI_KEY +ALTER TABLE t4 ADD PRIMARY KEY (c2); +SHOW INDEX FROM t4; + +DROP TABLE t0, t1, t2, t3, t4; +SET sql_generate_invisible_primary_key=OFF; diff --git a/mysql-test/main/mysqld--help.result b/mysql-test/main/mysqld--help.result index cb87a7ba8f1e6..6d265d2ca7ac1 100644 --- a/mysql-test/main/mysqld--help.result +++ b/mysql-test/main/mysqld--help.result @@ -1569,6 +1569,9 @@ The following specify which files/extra groups are read (specified before remain --sort-buffer-size=# Each thread that needs to do a sort allocates a buffer of this size + --sql-generate-invisible-primary-key + Automatically generate an invisible auto-increment + primary key for tables created without a primary key --sql-mode=name Sets the sql mode. Any combination of: REAL_AS_FLOAT, PIPES_AS_CONCAT, ANSI_QUOTES, IGNORE_SPACE, IGNORE_BAD_TABLE_OPTIONS, ONLY_FULL_GROUP_BY, @@ -2140,6 +2143,7 @@ slave-type-conversions slow-launch-time 2 slow-query-log FALSE sort-buffer-size 2097152 +sql-generate-invisible-primary-key FALSE sql-mode STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION sql-safe-updates FALSE stack-trace TRUE diff --git a/mysql-test/suite/sys_vars/r/sysvars_server_embedded.result b/mysql-test/suite/sys_vars/r/sysvars_server_embedded.result index 3344eea6148c3..2d9667631f62b 100644 --- a/mysql-test/suite/sys_vars/r/sysvars_server_embedded.result +++ b/mysql-test/suite/sys_vars/r/sysvars_server_embedded.result @@ -3672,6 +3672,16 @@ NUMERIC_BLOCK_SIZE NULL ENUM_VALUE_LIST OFF,ON READ_ONLY NO COMMAND_LINE_ARGUMENT NULL +VARIABLE_NAME SQL_GENERATE_INVISIBLE_PRIMARY_KEY +VARIABLE_SCOPE SESSION +VARIABLE_TYPE BOOLEAN +VARIABLE_COMMENT Automatically generate an invisible auto-increment primary key for tables created without a primary key +NUMERIC_MIN_VALUE NULL +NUMERIC_MAX_VALUE NULL +NUMERIC_BLOCK_SIZE NULL +ENUM_VALUE_LIST OFF,ON +READ_ONLY NO +COMMAND_LINE_ARGUMENT OPTIONAL VARIABLE_NAME SQL_IF_EXISTS VARIABLE_SCOPE SESSION VARIABLE_TYPE BOOLEAN diff --git a/mysql-test/suite/sys_vars/r/sysvars_server_notembedded.result b/mysql-test/suite/sys_vars/r/sysvars_server_notembedded.result index 95d90b797e0bf..065dae3aed856 100644 --- a/mysql-test/suite/sys_vars/r/sysvars_server_notembedded.result +++ b/mysql-test/suite/sys_vars/r/sysvars_server_notembedded.result @@ -4472,6 +4472,16 @@ NUMERIC_BLOCK_SIZE NULL ENUM_VALUE_LIST OFF,ON READ_ONLY NO COMMAND_LINE_ARGUMENT NULL +VARIABLE_NAME SQL_GENERATE_INVISIBLE_PRIMARY_KEY +VARIABLE_SCOPE SESSION +VARIABLE_TYPE BOOLEAN +VARIABLE_COMMENT Automatically generate an invisible auto-increment primary key for tables created without a primary key +NUMERIC_MIN_VALUE NULL +NUMERIC_MAX_VALUE NULL +NUMERIC_BLOCK_SIZE NULL +ENUM_VALUE_LIST OFF,ON +READ_ONLY NO +COMMAND_LINE_ARGUMENT OPTIONAL VARIABLE_NAME SQL_IF_EXISTS VARIABLE_SCOPE SESSION VARIABLE_TYPE BOOLEAN diff --git a/sql/sql_class.h b/sql/sql_class.h index e8829839617f7..01c320aeaa171 100644 --- a/sql/sql_class.h +++ b/sql/sql_class.h @@ -928,6 +928,7 @@ typedef struct system_variables #endif // USER_VAR_TRACKING my_bool tcp_nodelay; my_bool optimizer_record_context; + my_bool generate_invisible_primary_key; plugin_ref table_plugin; plugin_ref tmp_table_plugin; plugin_ref enforced_table_plugin; diff --git a/sql/sql_table.cc b/sql/sql_table.cc index 9f47277d1ef60..e431aab44904a 100644 --- a/sql/sql_table.cc +++ b/sql/sql_table.cc @@ -2751,6 +2751,16 @@ int mysql_add_invisible_field(THD *thd, List * field_list, return 0; } +static bool is_auto_pk_field(Field *field) +{ + return field && field->invisible == INVISIBLE_FULL && + (field->flags & (AUTO_INCREMENT_FLAG | NOT_NULL_FLAG | UNSIGNED_FLAG | + UNIQUE_KEY_FLAG)) == + (AUTO_INCREMENT_FLAG | NOT_NULL_FLAG | UNSIGNED_FLAG | + UNIQUE_KEY_FLAG) && + field->type_handler() == &type_handler_ulonglong; +} + #define INTERNAL_FIELD_NAME_LENGTH 30 static Lex_ident_column make_internal_field_name(THD *thd, const char *prefix, @@ -2784,18 +2794,50 @@ static Create_field *add_internal_field(THD *thd, Type_handler *type_handler, return cf; } -Key * -mysql_add_invisible_index(THD *thd, List *key_list, - LEX_CSTRING* field_name, enum Key::Keytype type) +Key *mysql_add_invisible_index(THD *thd, List *key_list, + LEX_CSTRING *field_name, enum Key::Keytype type, + bool generated_arg= false) { - Key *key= new (thd->mem_root) Key(type, &null_clex_str, HA_KEY_ALG_UNDEF, - false, DDL_options(DDL_options::OPT_NONE)); + Key *key= new (thd->mem_root) + Key(type, &null_clex_str, HA_KEY_ALG_UNDEF, generated_arg, + DDL_options(DDL_options::OPT_NONE)); key->columns.push_back(new(thd->mem_root) Key_part_spec(field_name, 0, true), thd->mem_root); key_list->push_back(key, thd->mem_root); return key; } +static int add_generated_invisible_pk(THD *thd, List *field_list, + Alter_info *alter_info) +{ + class Column_derived_attributes dat(&my_charset_bin); + Create_field *fld= new (thd->mem_root) Create_field(); + + Lex_ident_column automatic_pk_name; + if ((automatic_pk_name= make_unique_invisible_field_name( + thd, "_inv_PK"_Lex_ident_column, &alter_info->create_list)) + .str) + fld->field_name= automatic_pk_name; + else + return 1; + + fld->set_handler(&type_handler_ulonglong); + fld->prepare_stage1(thd, thd->mem_root, COLUMN_DEFINITION_TABLE_FIELD, &dat); + fld->invisible= INVISIBLE_FULL; + fld->flags|= + AUTO_INCREMENT_FLAG | NOT_NULL_FLAG | UNSIGNED_FLAG | UNIQUE_KEY_FLAG; + + if (fld->check(thd)) + return 1; + + field_list->push_front(fld, thd->mem_root); + + Key *key= mysql_add_invisible_index(thd, &(alter_info->key_list), + &(fld->field_name), Key::PRIMARY, true); + key->invisible= true; + + return 0; +} bool Type_handler_string::Key_part_spec_init_ft(Key_part_spec *part, const Column_definition &def) @@ -3300,6 +3342,29 @@ mysql_prepare_create_table_finalize(THD *thd, HA_CREATE_INFO *create_info, DBUG_RETURN(TRUE); } + /* + Check if primary_key exists. If a primary key does not exist, and + the generate_invisible_primary_key option is set, and the handler + supports auto increment, add an INVISIBLE_FULL primary key (MDEV-21181) + */ + if (!create_info->sequence && + thd->variables.generate_invisible_primary_key && + !(file->ha_table_flags() & HA_NO_AUTO_INCREMENT)) + { + List_iterator auto_pk_key_iterator(alter_info->key_list); + bool has_primary_key= 0; + Key *auto_pk_key; + while (!has_primary_key && (auto_pk_key= auto_pk_key_iterator++)) + { + if (auto_pk_key->type == Key::PRIMARY) + has_primary_key= 1; + } + + if (!has_primary_key) + if (add_generated_invisible_pk(thd, &alter_info->create_list, + alter_info)) + DBUG_RETURN(TRUE); + } for (field_no=0; (sql_field=it++) ; field_no++) { @@ -8710,6 +8775,36 @@ mysql_prepare_alter_table(THD *thd, TABLE *table, table->file->get_foreign_key_list(thd, &fk_list); + /* + If there is an existing primary key that is defined on a single field + and the field is an autogenerated pk field, check if the user added a + primary key of their own. + */ + bool should_replace_generated_pk= false; + Field *generated_pk_field= nullptr; + if (table->s->primary_key != MAX_KEY) + { + KEY *primary_key_info= table->key_info + table->s->primary_key; + if (primary_key_info->user_defined_key_parts == 1 && + (primary_key_info->flags & HA_GENERATED_KEY) && + is_auto_pk_field( + table->field[primary_key_info->key_part[0].fieldnr - 1])) + { + generated_pk_field= + table->field[primary_key_info->key_part[0].fieldnr - 1]; + + List_iterator add_key_it(alter_info->key_list); + for (Key *key; (key= add_key_it++);) + { + if (key->type == Key::PRIMARY && !key->generated) + { + should_replace_generated_pk= true; + break; + } + } + } + } + /* First collect all fields from table which isn't in drop_list */ @@ -8717,7 +8812,19 @@ mysql_prepare_alter_table(THD *thd, TABLE *table, for (f_ptr=table->field ; (field= *f_ptr) ; f_ptr++) { if (field->invisible == INVISIBLE_FULL) - continue; + { + /* + If the user added a pk of their own, and this was a generated pk field, + we drop the generated pk field. Other wise, we want to carry it over to + the new table. + */ + if (generated_pk_field == field && !should_replace_generated_pk) + { + def= new (root) Create_field(thd, field, field); + new_create_list.push_back(def, root); + } + continue; + } Alter_drop *drop; if (field->type() == MYSQL_TYPE_VARCHAR) create_info->varchar= TRUE; @@ -9118,7 +9225,13 @@ mysql_prepare_alter_table(THD *thd, TABLE *table, for (uint i= 0; i < table->s->total_keys; i++, key_info++) { bool long_hash_key= false; - if (key_info->flags & HA_INVISIBLE_KEY) + /* + If this is the generated PK and the user is not adding their own PK, + keep it + */ + if ((key_info->flags & HA_INVISIBLE_KEY) && + !(generated_pk_field != nullptr && i == table->s->primary_key && + !should_replace_generated_pk)) continue; Lex_ident_column key_name(key_info->name); const bool primary_key= table->s->primary_key == i; diff --git a/sql/sys_vars.cc b/sql/sys_vars.cc index 53b9c7b864ed7..3653c83bf81ca 100644 --- a/sql/sys_vars.cc +++ b/sql/sys_vars.cc @@ -7737,3 +7737,9 @@ static Sys_var_path Sys_path( "path", "Comma-separated list of schema names that defines the search " "order for stored routines", SESSION_VAR(path), NO_CMD_LINE, NOT_IN_BINLOG); + +static Sys_var_mybool Sys_generate_invisible_primary_key( + "sql_generate_invisible_primary_key", + "Automatically generate an invisible auto-increment primary key for " + "tables created without a primary key", + SESSION_VAR(generate_invisible_primary_key), CMD_LINE(OPT_ARG), DEFAULT(FALSE));