Skip to content

Commit 829e2a2

Browse files
committed
MDEV-21181 Add Support for Generated Invisible Primary Keys
This PR implements a new session/global option `sql_generate_invisible_primary_key`. When this option is on, MariaDB will automatically generate an invisible primary key for tables created without an explicit primary key. The type of the auto-generated primary key is `BIGINT UNSIGNED AUTO_INCREMENT`. If an ALTER TABLE ADD PRIMARY KEY statement is executed on a table with an generated primary key, the generated primary key will be silently dropped. Notes: - Internally, the generated primary key has the INVISIBLE_FULL attribute. This means that the key and column effectively do not exist to the user and will not appear in SHOW CREATE TABLE and SHOW COLUMNS. Furthermore, the user cannot explicitly drop the generated primary key. - The column name of the invisible generated primary key is _inv_PK. If the user creates their own column named _inv_PK, the generated primary key column's name will be suffixed with a number. - If the storage engine does not support auto increment, no generated primary key will be added.
1 parent d755574 commit 829e2a2

7 files changed

Lines changed: 272 additions & 6 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
SET SESSION debug_dbug="+d,test_invisible_index,test_completely_invisible";
2+
CREATE TABLE t0 (c1 INT, c2 INT);
3+
SHOW INDEX FROM t0;
4+
Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored
5+
t0 1 invisible 1 invisible A NULL NULL NULL YES BTREE NO
6+
SET sql_generate_invisible_primary_key=ON;
7+
CREATE TABLE t1 (c1 INT, c2 INT);
8+
SHOW INDEX FROM t1;
9+
Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored
10+
t1 0 PRIMARY 1 _inv_PK A 0 NULL NULL BTREE NO
11+
t1 1 invisible 1 invisible A NULL NULL NULL YES BTREE NO
12+
INSERT INTO t1 VALUES (3, 0);
13+
INSERT INTO t1 VALUES (4, 0);
14+
SELECT _rowid, t1.* FROM t1;
15+
_rowid c1 c2
16+
1 3 0
17+
2 4 0
18+
ALTER TABLE t1 ADD c3 INT;
19+
SHOW INDEX FROM t1;
20+
Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored
21+
t1 1 invisible 1 invisible A NULL NULL NULL YES BTREE NO
22+
t1 0 PRIMARY 1 _inv_PK A 2 NULL NULL BTREE NO
23+
ALTER TABLE t1 ADD _inv_PK INT;
24+
ALTER TABLE t1 ADD _inv_PK1 INT;
25+
SHOW INDEX FROM t1;
26+
Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored
27+
t1 1 invisible 1 invisible A NULL NULL NULL YES BTREE NO
28+
t1 0 PRIMARY 1 _inv_PK2 A 2 NULL NULL BTREE NO
29+
ALTER TABLE t1 DROP PRIMARY KEY;
30+
ERROR 42000: Can't DROP INDEX `PRIMARY`; check that it exists
31+
ALTER TABLE t1 ADD PRIMARY KEY (c1);
32+
SHOW INDEX FROM t1;
33+
Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored
34+
t1 0 PRIMARY 1 c1 A 2 NULL NULL BTREE NO
35+
t1 1 invisible 1 invisible A NULL NULL NULL YES BTREE NO
36+
SELECT _rowid, t1.* FROM t1;
37+
_rowid c1 c2 c3 _inv_PK _inv_PK1
38+
3 3 0 NULL NULL NULL
39+
4 4 0 NULL NULL NULL
40+
ALTER TABLE t1 DROP PRIMARY KEY;
41+
CREATE TABLE t2 (c1 INT, c2 INT, _inv_PK INT);
42+
SHOW INDEX FROM t2;
43+
Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored
44+
t2 0 PRIMARY 1 _inv_PK1 A 0 NULL NULL BTREE NO
45+
t2 1 invisible 1 invisible A NULL NULL NULL YES BTREE NO
46+
SET SESSION debug_dbug="";
47+
CREATE TABLE t3 (c1 INT, c2 INT);
48+
SHOW INDEX FROM t3;
49+
Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Ignored
50+
DROP TABLE t0, t1, t2, t3;
51+
SET sql_generate_invisible_primary_key=OFF;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#
2+
# Test of sql_generate_invisible_primary_key option (MDEV-21181)
3+
#
4+
--source include/have_debug.inc
5+
6+
SET SESSION debug_dbug="+d,test_invisible_index,test_completely_invisible";
7+
8+
#
9+
# When option is off, invisible PK is not generated
10+
#
11+
CREATE TABLE t0 (c1 INT, c2 INT);
12+
SHOW INDEX FROM t0;
13+
14+
SET sql_generate_invisible_primary_key=ON;
15+
16+
#
17+
# Check that PK index gets created when not specified in CREATE TABLE
18+
#
19+
CREATE TABLE t1 (c1 INT, c2 INT);
20+
SHOW INDEX FROM t1;
21+
22+
INSERT INTO t1 VALUES (3, 0);
23+
INSERT INTO t1 VALUES (4, 0);
24+
SELECT _rowid, t1.* FROM t1;
25+
26+
#
27+
# Check that generated PK persists on ALTER TABLE without new PK
28+
#
29+
ALTER TABLE t1 ADD c3 INT;
30+
SHOW INDEX FROM t1;
31+
32+
#
33+
# Check that adding column with name of generated PK succeeds
34+
#
35+
ALTER TABLE t1 ADD _inv_PK INT;
36+
ALTER TABLE t1 ADD _inv_PK1 INT;
37+
SHOW INDEX FROM t1;
38+
39+
#
40+
# Trying to drop autogenerated PK should fail
41+
#
42+
--error 1091
43+
ALTER TABLE t1 DROP PRIMARY KEY;
44+
45+
#
46+
# Check that generated PK is dropped on ALTER TABLE with new PK
47+
#
48+
ALTER TABLE t1 ADD PRIMARY KEY (c1);
49+
SHOW INDEX FROM t1;
50+
SELECT _rowid, t1.* FROM t1;
51+
52+
#
53+
# Check that user PK that replaced generated PK can be dropped
54+
#
55+
ALTER TABLE t1 DROP PRIMARY KEY;
56+
57+
#
58+
# Check that creating table with column named _inv_PK succeeds
59+
#
60+
CREATE TABLE t2 (c1 INT, c2 INT, _inv_PK INT);
61+
SHOW INDEX FROM t2;
62+
63+
SET SESSION debug_dbug="";
64+
65+
#
66+
# Generated PK should be fully invisible to user
67+
#
68+
CREATE TABLE t3 (c1 INT, c2 INT);
69+
SHOW INDEX FROM t3;
70+
71+
DROP TABLE t0, t1, t2, t3;
72+
SET sql_generate_invisible_primary_key=OFF;

mysql-test/suite/sys_vars/r/sysvars_server_embedded.result

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3672,6 +3672,16 @@ NUMERIC_BLOCK_SIZE NULL
36723672
ENUM_VALUE_LIST OFF,ON
36733673
READ_ONLY NO
36743674
COMMAND_LINE_ARGUMENT NULL
3675+
VARIABLE_NAME SQL_GENERATE_INVISIBLE_PRIMARY_KEY
3676+
VARIABLE_SCOPE SESSION
3677+
VARIABLE_TYPE BOOLEAN
3678+
VARIABLE_COMMENT Automatically generate an invisible auto-increment primary key for tables created without a primary key
3679+
NUMERIC_MIN_VALUE NULL
3680+
NUMERIC_MAX_VALUE NULL
3681+
NUMERIC_BLOCK_SIZE NULL
3682+
ENUM_VALUE_LIST OFF,ON
3683+
READ_ONLY NO
3684+
COMMAND_LINE_ARGUMENT OPTIONAL
36753685
VARIABLE_NAME SQL_IF_EXISTS
36763686
VARIABLE_SCOPE SESSION
36773687
VARIABLE_TYPE BOOLEAN

mysql-test/suite/sys_vars/r/sysvars_server_notembedded.result

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4472,6 +4472,16 @@ NUMERIC_BLOCK_SIZE NULL
44724472
ENUM_VALUE_LIST OFF,ON
44734473
READ_ONLY NO
44744474
COMMAND_LINE_ARGUMENT NULL
4475+
VARIABLE_NAME SQL_GENERATE_INVISIBLE_PRIMARY_KEY
4476+
VARIABLE_SCOPE SESSION
4477+
VARIABLE_TYPE BOOLEAN
4478+
VARIABLE_COMMENT Automatically generate an invisible auto-increment primary key for tables created without a primary key
4479+
NUMERIC_MIN_VALUE NULL
4480+
NUMERIC_MAX_VALUE NULL
4481+
NUMERIC_BLOCK_SIZE NULL
4482+
ENUM_VALUE_LIST OFF,ON
4483+
READ_ONLY NO
4484+
COMMAND_LINE_ARGUMENT OPTIONAL
44754485
VARIABLE_NAME SQL_IF_EXISTS
44764486
VARIABLE_SCOPE SESSION
44774487
VARIABLE_TYPE BOOLEAN

sql/sql_class.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,7 @@ typedef struct system_variables
928928
#endif // USER_VAR_TRACKING
929929
my_bool tcp_nodelay;
930930
my_bool optimizer_record_context;
931+
my_bool generate_invisible_primary_key;
931932
plugin_ref table_plugin;
932933
plugin_ref tmp_table_plugin;
933934
plugin_ref enforced_table_plugin;

sql/sql_table.cc

Lines changed: 122 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2751,6 +2751,16 @@ int mysql_add_invisible_field(THD *thd, List<Create_field> * field_list,
27512751
return 0;
27522752
}
27532753

2754+
static bool is_auto_pk_field(Field *field)
2755+
{
2756+
return field && field->invisible == INVISIBLE_FULL &&
2757+
(field->flags & (AUTO_INCREMENT_FLAG | NOT_NULL_FLAG | UNSIGNED_FLAG |
2758+
UNIQUE_KEY_FLAG)) ==
2759+
(AUTO_INCREMENT_FLAG | NOT_NULL_FLAG | UNSIGNED_FLAG |
2760+
UNIQUE_KEY_FLAG) &&
2761+
field->type_handler() == &type_handler_ulonglong;
2762+
}
2763+
27542764
#define INTERNAL_FIELD_NAME_LENGTH 30
27552765

27562766
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,
27842794
return cf;
27852795
}
27862796

2787-
Key *
2788-
mysql_add_invisible_index(THD *thd, List<Key> *key_list,
2789-
LEX_CSTRING* field_name, enum Key::Keytype type)
2797+
Key *mysql_add_invisible_index(THD *thd, List<Key> *key_list,
2798+
LEX_CSTRING *field_name, enum Key::Keytype type,
2799+
bool generated_arg= false)
27902800
{
2791-
Key *key= new (thd->mem_root) Key(type, &null_clex_str, HA_KEY_ALG_UNDEF,
2792-
false, DDL_options(DDL_options::OPT_NONE));
2801+
Key *key= new (thd->mem_root)
2802+
Key(type, &null_clex_str, HA_KEY_ALG_UNDEF, generated_arg,
2803+
DDL_options(DDL_options::OPT_NONE));
27932804
key->columns.push_back(new(thd->mem_root) Key_part_spec(field_name, 0, true),
27942805
thd->mem_root);
27952806
key_list->push_back(key, thd->mem_root);
27962807
return key;
27972808
}
27982809

2810+
static int add_generated_invisible_pk(THD *thd, List<Create_field> *field_list,
2811+
Alter_info *alter_info)
2812+
{
2813+
class Column_derived_attributes dat(&my_charset_bin);
2814+
Create_field *fld= new (thd->mem_root) Create_field();
2815+
2816+
Lex_ident_column automatic_pk_name;
2817+
if ((automatic_pk_name= make_unique_invisible_field_name(
2818+
thd, "_inv_PK"_Lex_ident_column, &alter_info->create_list))
2819+
.str)
2820+
fld->field_name= automatic_pk_name;
2821+
else
2822+
return 1;
2823+
2824+
fld->set_handler(&type_handler_ulonglong);
2825+
fld->prepare_stage1(thd, thd->mem_root, COLUMN_DEFINITION_TABLE_FIELD, &dat);
2826+
fld->invisible= INVISIBLE_FULL;
2827+
fld->flags|=
2828+
AUTO_INCREMENT_FLAG | NOT_NULL_FLAG | UNSIGNED_FLAG | UNIQUE_KEY_FLAG;
2829+
2830+
if (fld->check(thd))
2831+
return 1;
2832+
2833+
field_list->push_front(fld, thd->mem_root);
2834+
2835+
Key *key= mysql_add_invisible_index(thd, &(alter_info->key_list),
2836+
&(fld->field_name), Key::PRIMARY, true);
2837+
key->invisible= true;
2838+
2839+
return 0;
2840+
}
27992841

28002842
bool Type_handler_string::Key_part_spec_init_ft(Key_part_spec *part,
28012843
const Column_definition &def)
@@ -3300,6 +3342,29 @@ mysql_prepare_create_table_finalize(THD *thd, HA_CREATE_INFO *create_info,
33003342
DBUG_RETURN(TRUE);
33013343
}
33023344

3345+
/*
3346+
Check if primary_key exists. If a primary key does not exist, and
3347+
the generate_invisible_primary_key option is set, and the handler
3348+
supports auto increment, add an INVISIBLE_FULL primary key (MDEV-21181)
3349+
*/
3350+
if (!create_info->sequence &&
3351+
thd->variables.generate_invisible_primary_key &&
3352+
!(file->ha_table_flags() & HA_NO_AUTO_INCREMENT))
3353+
{
3354+
List_iterator<Key> auto_pk_key_iterator(alter_info->key_list);
3355+
bool has_primary_key= 0;
3356+
Key *auto_pk_key;
3357+
while (!has_primary_key && (auto_pk_key= auto_pk_key_iterator++))
3358+
{
3359+
if (auto_pk_key->type == Key::PRIMARY)
3360+
has_primary_key= 1;
3361+
}
3362+
3363+
if (!has_primary_key)
3364+
if (add_generated_invisible_pk(thd, &alter_info->create_list,
3365+
alter_info))
3366+
DBUG_RETURN(TRUE);
3367+
}
33033368

33043369
for (field_no=0; (sql_field=it++) ; field_no++)
33053370
{
@@ -8710,14 +8775,59 @@ mysql_prepare_alter_table(THD *thd, TABLE *table,
87108775

87118776
table->file->get_foreign_key_list(thd, &fk_list);
87128777

8778+
/*
8779+
If there is an existing primary key that is defined on a single field
8780+
and the field is an autogenerated pk field, check if the user added a
8781+
primary key of their own.
8782+
*/
8783+
bool should_replace_generated_pk= false;
8784+
Field *generated_pk_field= nullptr;
8785+
if (table->s->primary_key != MAX_KEY)
8786+
{
8787+
KEY *primary_key_info= table->key_info + table->s->primary_key;
8788+
if (primary_key_info->user_defined_key_parts == 1 &&
8789+
is_auto_pk_field(
8790+
table->field[primary_key_info->key_part[0].fieldnr - 1]))
8791+
{
8792+
generated_pk_field=
8793+
table->field[primary_key_info->key_part[0].fieldnr - 1];
8794+
8795+
List_iterator<Key> add_key_it(alter_info->key_list);
8796+
for (Key *key; (key= add_key_it++);)
8797+
{
8798+
if (key->type == Key::PRIMARY && !key->generated)
8799+
{
8800+
should_replace_generated_pk= true;
8801+
break;
8802+
}
8803+
}
8804+
}
8805+
}
8806+
87138807
/*
87148808
First collect all fields from table which isn't in drop_list
87158809
*/
87168810
bitmap_clear_all(&table->tmp_set);
87178811
for (f_ptr=table->field ; (field= *f_ptr) ; f_ptr++)
87188812
{
87198813
if (field->invisible == INVISIBLE_FULL)
8814+
{
8815+
/*
8816+
If the user added a pk of their own, and this was a generated pk field,
8817+
we drop the generated pk field. Other wise, we want to carry it over to
8818+
the new table.
8819+
*/
8820+
if (should_replace_generated_pk && generated_pk_field == field)
8821+
{
87208822
continue;
8823+
}
8824+
if (generated_pk_field == field)
8825+
{
8826+
def= new (root) Create_field(thd, field, field);
8827+
new_create_list.push_back(def, root);
8828+
}
8829+
continue;
8830+
}
87218831
Alter_drop *drop;
87228832
if (field->type() == MYSQL_TYPE_VARCHAR)
87238833
create_info->varchar= TRUE;
@@ -9118,7 +9228,13 @@ mysql_prepare_alter_table(THD *thd, TABLE *table,
91189228
for (uint i= 0; i < table->s->total_keys; i++, key_info++)
91199229
{
91209230
bool long_hash_key= false;
9121-
if (key_info->flags & HA_INVISIBLE_KEY)
9231+
/*
9232+
If this is the generated PK and the user is not adding their own PK,
9233+
keep it
9234+
*/
9235+
if ((key_info->flags & HA_INVISIBLE_KEY) &&
9236+
!(generated_pk_field != nullptr && i == table->s->primary_key &&
9237+
!should_replace_generated_pk))
91229238
continue;
91239239
Lex_ident_column key_name(key_info->name);
91249240
const bool primary_key= table->s->primary_key == i;

sql/sys_vars.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7737,3 +7737,9 @@ static Sys_var_path Sys_path(
77377737
"path", "Comma-separated list of schema names that defines the search "
77387738
"order for stored routines",
77397739
SESSION_VAR(path), NO_CMD_LINE, NOT_IN_BINLOG);
7740+
7741+
static Sys_var_mybool Sys_generate_invisible_primary_key(
7742+
"sql_generate_invisible_primary_key",
7743+
"Automatically generate an invisible auto-increment primary key for "
7744+
"tables created without a primary key",
7745+
SESSION_VAR(generate_invisible_primary_key), CMD_LINE(OPT_ARG), DEFAULT(FALSE));

0 commit comments

Comments
 (0)